Merge branch 'master' of git://git.gag.com/fw/altos
[fw/altos] / ao-tools / altosui / AltosUI.java
1 /*
2  * Copyright © 2010 Keith Packard <keithp@keithp.com>
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; version 2 of the License.
7  *
8  * This program is distributed in the hope that it will be useful, but
9  * WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11  * General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License along
14  * with this program; if not, write to the Free Software Foundation, Inc.,
15  * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
16  */
17
18 package altosui;
19
20 import java.awt.*;
21 import java.awt.event.*;
22 import javax.swing.*;
23 import javax.swing.filechooser.FileNameExtensionFilter;
24 import javax.swing.table.*;
25 import java.io.*;
26 import java.util.*;
27 import java.text.*;
28 import java.util.prefs.*;
29 import java.util.concurrent.LinkedBlockingQueue;
30
31 import altosui.Altos;
32 import altosui.AltosSerial;
33 import altosui.AltosSerialMonitor;
34 import altosui.AltosRecord;
35 import altosui.AltosTelemetry;
36 import altosui.AltosState;
37 import altosui.AltosDeviceDialog;
38 import altosui.AltosPreferences;
39 import altosui.AltosLog;
40 import altosui.AltosVoice;
41 import altosui.AltosFlightInfoTableModel;
42 import altosui.AltosChannelMenu;
43 import altosui.AltosFlashUI;
44 import altosui.AltosLogfileChooser;
45 import altosui.AltosCSVUI;
46 import altosui.AltosLine;
47 import altosui.AltosStatusTable;
48 import altosui.AltosInfoTable;
49
50 import libaltosJNI.*;
51
52 public class AltosUI extends JFrame {
53         private int channel = -1;
54
55         private AltosStatusTable flightStatus;
56         private AltosInfoTable flightInfo;
57         private AltosSerial serial_line;
58         private AltosLog altos_log;
59         private Box vbox;
60
61         private Font statusFont = new Font("SansSerif", Font.BOLD, 24);
62         private Font infoLabelFont = new Font("SansSerif", Font.PLAIN, 14);
63         private Font infoValueFont = new Font("Monospaced", Font.PLAIN, 14);
64
65         public AltosVoice voice = new AltosVoice();
66
67         public static boolean load_library(Frame frame) {
68                 if (!AltosDevice.load_library()) {
69                         JOptionPane.showMessageDialog(frame,
70                                                       String.format("No AltOS library in \"%s\"",
71                                                                     System.getProperty("java.library.path","<undefined>")),
72                                                       "Cannot load device access library",
73                                                       JOptionPane.ERROR_MESSAGE);
74                         return false;
75                 }
76                 return true;
77         }
78
79         public AltosUI() {
80
81                 load_library(null);
82
83                 String[] statusNames = { "Height (m)", "State", "RSSI (dBm)", "Speed (m/s)" };
84                 Object[][] statusData = { { "0", "pad", "-50", "0" } };
85
86                 java.net.URL imgURL = AltosUI.class.getResource("/altus-metrum-16x16.jpg");
87                 if (imgURL != null)
88                         setIconImage(new ImageIcon(imgURL).getImage());
89
90                 AltosPreferences.init(this);
91
92                 vbox = Box.createVerticalBox();
93                 this.add(vbox);
94
95                 flightStatus = new AltosStatusTable(this);
96
97                 vbox.add(flightStatus);
98
99                 flightInfo = new AltosInfoTable();
100                 vbox.add(flightInfo.box());
101
102                 setTitle("AltOS");
103
104                 createMenu();
105
106                 serial_line = new AltosSerial();
107                 altos_log = new AltosLog(serial_line);
108                 int dpi = Toolkit.getDefaultToolkit().getScreenResolution();
109                 this.setSize(new Dimension (flightInfo.width(),
110                                             flightStatus.height() + flightInfo.height()));
111                 this.validate();
112                 setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
113                 addWindowListener(new WindowAdapter() {
114                         @Override
115                         public void windowClosing(WindowEvent e) {
116                                 System.exit(0);
117                         }
118                 });
119                 voice.speak("Rocket flight monitor ready.");
120         }
121
122         void show(AltosState state, int crc_errors) {
123                 if (state != null) {
124                         flightStatus.set(state);
125                         flightInfo.show(state, crc_errors);
126                 }
127         }
128
129         class IdleThread extends Thread {
130
131                 boolean started;
132                 private AltosState state;
133                 int     reported_landing;
134                 int     report_interval;
135                 long    report_time;
136
137                 public synchronized void report(boolean last) {
138                         if (state == null)
139                                 return;
140
141                         /* reset the landing count once we hear about a new flight */
142                         if (state.state < Altos.ao_flight_drogue)
143                                 reported_landing = 0;
144
145                         /* Shut up once the rocket is on the ground */
146                         if (reported_landing > 2) {
147                                 return;
148                         }
149
150                         /* If the rocket isn't on the pad, then report height */
151                         if (Altos.ao_flight_drogue <= state.state &&
152                             state.state < Altos.ao_flight_landed &&
153                             state.range >= 0)
154                         {
155                                 voice.speak("Height %d, bearing %d, elevation %d, range %d.\n",
156                                             (int) (state.height + 0.5),
157                                             (int) (state.from_pad.bearing + 0.5),
158                                             (int) (state.elevation + 0.5),
159                                             (int) (state.range + 0.5));
160                         } else if (state.state > Altos.ao_flight_pad) {
161                                 voice.speak("%d meters", (int) (state.height + 0.5));
162                         } else {
163                                 reported_landing = 0;
164                         }
165
166                         /* If the rocket is coming down, check to see if it has landed;
167                          * either we've got a landed report or we haven't heard from it in
168                          * a long time
169                          */
170                         if (state.state >= Altos.ao_flight_drogue &&
171                             (last ||
172                              System.currentTimeMillis() - state.report_time >= 15000 ||
173                              state.state == Altos.ao_flight_landed))
174                         {
175                                 if (Math.abs(state.baro_speed) < 20 && state.height < 100)
176                                         voice.speak("rocket landed safely");
177                                 else
178                                         voice.speak("rocket may have crashed");
179                                 if (state.from_pad != null)
180                                         voice.speak("Bearing %d degrees, range %d meters.",
181                                                     (int) (state.from_pad.bearing + 0.5),
182                                                     (int) (state.from_pad.distance + 0.5));
183                                 ++reported_landing;
184                         }
185                 }
186
187                 long now () {
188                         return System.currentTimeMillis();
189                 }
190
191                 void set_report_time() {
192                         report_time = now() + report_interval;
193                 }
194
195                 public void run () {
196
197                         reported_landing = 0;
198                         state = null;
199                         report_interval = 10000;
200                         try {
201                                 for (;;) {
202                                         set_report_time();
203                                         for (;;) {
204                                                 voice.drain();
205                                                 synchronized (this) {
206                                                         long    sleep_time = report_time - now();
207                                                         if (sleep_time <= 0)
208                                                                 break;
209                                                         wait(sleep_time);
210                                                 }
211                                         }
212                                         report(false);
213                                 }
214                         } catch (InterruptedException ie) {
215                                 try {
216                                         voice.drain();
217                                 } catch (InterruptedException iie) { }
218                         }
219                 }
220
221                 public synchronized void notice(AltosState new_state, boolean spoken) {
222                         AltosState old_state = state;
223                         state = new_state;
224                         if (!started && state.state > Altos.ao_flight_pad) {
225                                 started = true;
226                                 start();
227                         }
228
229                         if (state.state < Altos.ao_flight_drogue)
230                                 report_interval = 10000;
231                         else
232                                 report_interval = 20000;
233                         if (old_state != null && old_state.state != state.state) {
234                                 report_time = now();
235                                 this.notify();
236                         } else if (spoken)
237                                 set_report_time();
238                 }
239         }
240
241         private boolean tell(AltosState state, AltosState old_state) {
242                 boolean ret = false;
243                 if (old_state == null || old_state.state != state.state) {
244                         voice.speak(state.data.state());
245                         if ((old_state == null || old_state.state <= Altos.ao_flight_boost) &&
246                             state.state > Altos.ao_flight_boost) {
247                                 voice.speak("max speed: %d meters per second.",
248                                             (int) (state.max_speed + 0.5));
249                                 ret = true;
250                         } else if ((old_state == null || old_state.state < Altos.ao_flight_drogue) &&
251                                    state.state >= Altos.ao_flight_drogue) {
252                                 voice.speak("max height: %d meters.",
253                                             (int) (state.max_height + 0.5));
254                                 ret = true;
255                         }
256                 }
257                 if (old_state == null || old_state.gps_ready != state.gps_ready) {
258                         if (state.gps_ready) {
259                                 voice.speak("GPS ready");
260                                 ret = true;
261                         }
262                         else if (old_state != null) {
263                                 voice.speak("GPS lost");
264                                 ret = true;
265                         }
266                 }
267                 old_state = state;
268                 return ret;
269         }
270
271         class DisplayThread extends Thread {
272                 IdleThread      idle_thread;
273
274                 String          name;
275
276                 int             crc_errors;
277
278                 void init() { }
279
280                 AltosRecord read() throws InterruptedException, ParseException, AltosCRCException, IOException { return null; }
281
282                 void close(boolean interrupted) { }
283
284                 void update(AltosState state) throws InterruptedException { }
285
286                 public void run() {
287                         boolean         interrupted = false;
288                         String          line;
289                         AltosState      state = null;
290                         AltosState      old_state = null;
291                         boolean         told;
292
293                         idle_thread = new IdleThread();
294
295                         flightInfo.clear();
296                         try {
297                                 for (;;) {
298                                         try {
299                                                 AltosRecord record = read();
300                                                 if (record == null)
301                                                         break;
302                                                 old_state = state;
303                                                 state = new AltosState(record, state);
304                                                 update(state);
305                                                 show(state, crc_errors);
306                                                 told = tell(state, old_state);
307                                                 idle_thread.notice(state, told);
308                                         } catch (ParseException pp) {
309                                                 System.out.printf("Parse error: %d \"%s\"\n", pp.getErrorOffset(), pp.getMessage());
310                                         } catch (AltosCRCException ce) {
311                                                 ++crc_errors;
312                                                 show(state, crc_errors);
313                                         }
314                                 }
315                         } catch (InterruptedException ee) {
316                                 interrupted = true;
317                         } catch (IOException ie) {
318                                 JOptionPane.showMessageDialog(AltosUI.this,
319                                                               String.format("Error reading from \"%s\"", name),
320                                                               "Telemetry Read Error",
321                                                               JOptionPane.ERROR_MESSAGE);
322                         } finally {
323                                 close(interrupted);
324                                 idle_thread.interrupt();
325                                 try {
326                                         idle_thread.join();
327                                 } catch (InterruptedException ie) {}
328                         }
329                 }
330
331                 public void report() {
332                         if (idle_thread != null)
333                                 idle_thread.report(true);
334                 }
335         }
336
337         class DeviceThread extends DisplayThread {
338                 AltosSerial     serial;
339                 LinkedBlockingQueue<AltosLine> telem;
340
341                 AltosRecord read() throws InterruptedException, ParseException, AltosCRCException, IOException {
342                         AltosLine l = telem.take();
343                         if (l.line == null)
344                                 throw new IOException("IO error");
345                         return new AltosTelemetry(l.line);
346                 }
347
348                 void close(boolean interrupted) {
349                         serial.close();
350                         serial.remove_monitor(telem);
351                 }
352
353                 public DeviceThread(AltosSerial s, String in_name) {
354                         serial = s;
355                         telem = new LinkedBlockingQueue<AltosLine>();
356                         serial.add_monitor(telem);
357                         name = in_name;
358                 }
359         }
360
361         private void ConnectToDevice() {
362                 AltosDevice     device = AltosDeviceDialog.show(AltosUI.this,
363                                                                 AltosDevice.product_basestation);
364
365                 if (device != null) {
366                         try {
367                                 stop_display();
368                                 serial_line.open(device);
369                                 DeviceThread thread = new DeviceThread(serial_line, device.getPath());
370                                 serial_line.set_channel(AltosPreferences.channel());
371                                 serial_line.set_callsign(AltosPreferences.callsign());
372                                 run_display(thread);
373                         } catch (FileNotFoundException ee) {
374                                 JOptionPane.showMessageDialog(AltosUI.this,
375                                                               String.format("Cannot open device \"%s\"",
376                                                                             device.getPath()),
377                                                               "Cannot open target device",
378                                                               JOptionPane.ERROR_MESSAGE);
379                         } catch (IOException ee) {
380                                 JOptionPane.showMessageDialog(AltosUI.this,
381                                                               device.getPath(),
382                                                               "Unkonwn I/O error",
383                                                               JOptionPane.ERROR_MESSAGE);
384                         }
385                 }
386         }
387
388         void DisconnectFromDevice () {
389                 stop_display();
390         }
391
392         void ConfigureCallsign() {
393                 String  result;
394                 result = JOptionPane.showInputDialog(AltosUI.this,
395                                                      "Configure Callsign",
396                                                      AltosPreferences.callsign());
397                 if (result != null) {
398                         AltosPreferences.set_callsign(result);
399                         if (serial_line != null)
400                                 serial_line.set_callsign(result);
401                 }
402         }
403
404         void ConfigureTeleMetrum() {
405                 new AltosConfig(AltosUI.this);
406         }
407
408         void FlashImage() {
409                 new AltosFlashUI(AltosUI.this);
410         }
411
412         /*
413          * Open an existing telemetry file and replay it in realtime
414          */
415
416         class ReplayThread extends DisplayThread {
417                 AltosReader     reader;
418                 String          name;
419
420                 public AltosRecord read() {
421                         try {
422                                 return reader.read();
423                         } catch (IOException ie) {
424                                 JOptionPane.showMessageDialog(AltosUI.this,
425                                                               name,
426                                                               "error reading",
427                                                               JOptionPane.ERROR_MESSAGE);
428                         } catch (ParseException pe) {
429                         }
430                         return null;
431                 }
432
433                 public void close (boolean interrupted) {
434                         if (!interrupted)
435                                 report();
436                 }
437
438                 public ReplayThread(AltosReader in_reader, String in_name) {
439                         reader = in_reader;
440                 }
441                 void update(AltosState state) throws InterruptedException {
442                         /* Make it run in realtime after the rocket leaves the pad */
443                         if (state.state > Altos.ao_flight_pad)
444                                 Thread.sleep((int) (Math.min(state.time_change,10) * 1000));
445                 }
446         }
447
448         Thread          display_thread;
449
450         private void stop_display() {
451                 if (display_thread != null && display_thread.isAlive()) {
452                         display_thread.interrupt();
453                         try {
454                                 display_thread.join();
455                         } catch (InterruptedException ie) {}
456                 }
457                 display_thread = null;
458         }
459
460         private void run_display(Thread thread) {
461                 stop_display();
462                 display_thread = thread;
463                 display_thread.start();
464         }
465
466         /*
467          * Replay a flight from telemetry data
468          */
469         private void Replay() {
470                 AltosLogfileChooser chooser = new AltosLogfileChooser(
471                         AltosUI.this);
472                 AltosReader reader = chooser.runDialog();
473                 if (reader != null)
474                         run_display(new ReplayThread(reader,
475                                                      chooser.filename()));
476         }
477
478         /* Connect to TeleMetrum, either directly or through
479          * a TeleDongle over the packet link
480          */
481         private void SaveFlightData() {
482                 new AltosEepromDownload(AltosUI.this);
483         }
484
485         /* Load a flight log file and write out a CSV file containing
486          * all of the data in standard units
487          */
488
489         private void ExportData() {
490                 new AltosCSVUI(AltosUI.this);
491         }
492
493         /* Load a flight log CSV file and display a pretty graph.
494          */
495
496         private void GraphData() {
497                 new AltosGraphUI(AltosUI.this);
498         }
499
500         /* Create the AltosUI menus
501          */
502         private void createMenu() {
503                 JMenuBar menubar = new JMenuBar();
504                 JMenu menu;
505                 JMenuItem item;
506                 JRadioButtonMenuItem radioitem;
507
508                 // File menu
509                 {
510                         menu = new JMenu("File");
511                         menu.setMnemonic(KeyEvent.VK_F);
512                         menubar.add(menu);
513
514                         item = new JMenuItem("Replay File",KeyEvent.VK_R);
515                         item.addActionListener(new ActionListener() {
516                                         public void actionPerformed(ActionEvent e) {
517                                                 Replay();
518                                         }
519                                 });
520                         menu.add(item);
521
522                         item = new JMenuItem("Save Flight Data",KeyEvent.VK_S);
523                         item.addActionListener(new ActionListener() {
524                                         public void actionPerformed(ActionEvent e) {
525                                                 SaveFlightData();
526                                         }
527                                 });
528                         menu.add(item);
529
530                         item = new JMenuItem("Flash Image",KeyEvent.VK_F);
531                         item.addActionListener(new ActionListener() {
532                                         public void actionPerformed(ActionEvent e) {
533                                                 FlashImage();
534                                         }
535                                 });
536                         menu.add(item);
537
538                         item = new JMenuItem("Export Data",KeyEvent.VK_F);
539                         item.addActionListener(new ActionListener() {
540                                         public void actionPerformed(ActionEvent e) {
541                                                 ExportData();
542                                         }
543                                 });
544                         menu.add(item);
545
546                         item = new JMenuItem("Graph Data",KeyEvent.VK_F);
547                         item.addActionListener(new ActionListener() {
548                                         public void actionPerformed(ActionEvent e) {
549                                                 GraphData();
550                                         }
551                                 });
552                         menu.add(item);
553
554                         item = new JMenuItem("Quit",KeyEvent.VK_Q);
555                         item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q,
556                                                                    ActionEvent.CTRL_MASK));
557                         item.addActionListener(new ActionListener() {
558                                         public void actionPerformed(ActionEvent e) {
559                                                 System.exit(0);
560                                         }
561                                 });
562                         menu.add(item);
563                 }
564
565                 // Device menu
566                 {
567                         menu = new JMenu("Device");
568                         menu.setMnemonic(KeyEvent.VK_D);
569                         menubar.add(menu);
570
571                         item = new JMenuItem("Connect to Device",KeyEvent.VK_C);
572                         item.addActionListener(new ActionListener() {
573                                         public void actionPerformed(ActionEvent e) {
574                                                 ConnectToDevice();
575                                         }
576                                 });
577                         menu.add(item);
578
579                         item = new JMenuItem("Disconnect from Device",KeyEvent.VK_D);
580                         item.addActionListener(new ActionListener() {
581                                         public void actionPerformed(ActionEvent e) {
582                                                 DisconnectFromDevice();
583                                         }
584                                 });
585                         menu.add(item);
586
587                         menu.addSeparator();
588
589                         item = new JMenuItem("Set Callsign",KeyEvent.VK_S);
590                         item.addActionListener(new ActionListener() {
591                                         public void actionPerformed(ActionEvent e) {
592                                                 ConfigureCallsign();
593                                         }
594                                 });
595
596                         menu.add(item);
597
598                         item = new JMenuItem("Configure TeleMetrum device",KeyEvent.VK_T);
599                         item.addActionListener(new ActionListener() {
600                                         public void actionPerformed(ActionEvent e) {
601                                                 ConfigureTeleMetrum();
602                                         }
603                                 });
604
605                         menu.add(item);
606                 }
607                 // Log menu
608                 {
609                         menu = new JMenu("Log");
610                         menu.setMnemonic(KeyEvent.VK_L);
611                         menubar.add(menu);
612
613                         item = new JMenuItem("New Log",KeyEvent.VK_N);
614                         item.addActionListener(new ActionListener() {
615                                         public void actionPerformed(ActionEvent e) {
616                                         }
617                                 });
618                         menu.add(item);
619
620                         item = new JMenuItem("Configure Log",KeyEvent.VK_C);
621                         item.addActionListener(new ActionListener() {
622                                         public void actionPerformed(ActionEvent e) {
623                                                 AltosPreferences.ConfigureLog();
624                                         }
625                                 });
626                         menu.add(item);
627                 }
628                 // Voice menu
629                 {
630                         menu = new JMenu("Voice", true);
631                         menu.setMnemonic(KeyEvent.VK_V);
632                         menubar.add(menu);
633
634                         radioitem = new JRadioButtonMenuItem("Enable Voice", AltosPreferences.voice());
635                         radioitem.addActionListener(new ActionListener() {
636                                         public void actionPerformed(ActionEvent e) {
637                                                 JRadioButtonMenuItem item = (JRadioButtonMenuItem) e.getSource();
638                                                 boolean enabled = item.isSelected();
639                                                 AltosPreferences.set_voice(enabled);
640                                                 if (enabled)
641                                                         voice.speak_always("Enable voice.");
642                                                 else
643                                                         voice.speak_always("Disable voice.");
644                                         }
645                                 });
646                         menu.add(radioitem);
647                         item = new JMenuItem("Test Voice",KeyEvent.VK_T);
648                         item.addActionListener(new ActionListener() {
649                                         public void actionPerformed(ActionEvent e) {
650                                                 voice.speak("That's one small step for man; one giant leap for mankind.");
651                                         }
652                                 });
653                         menu.add(item);
654                 }
655
656                 // Channel menu
657                 {
658                         menu = new AltosChannelMenu(AltosPreferences.channel());
659                         menu.addActionListener(new ActionListener() {
660                                                 public void actionPerformed(ActionEvent e) {
661                                                         int new_channel = Integer.parseInt(e.getActionCommand());
662                                                         AltosPreferences.set_channel(new_channel);
663                                                         serial_line.set_channel(new_channel);
664                                                 }
665                                 });
666                         menu.setMnemonic(KeyEvent.VK_C);
667                         menubar.add(menu);
668                 }
669
670                 this.setJMenuBar(menubar);
671
672         }
673
674         static String replace_extension(String input, String extension) {
675                 int dot = input.lastIndexOf(".");
676                 if (dot > 0)
677                         input = input.substring(0,dot);
678                 return input.concat(extension);
679         }
680
681         static AltosReader open_logfile(String filename) {
682                 File file = new File (filename);
683                 try {
684                         FileInputStream in;
685
686                         in = new FileInputStream(file);
687                         if (filename.endsWith("eeprom"))
688                                 return new AltosEepromReader(in);
689                         else
690                                 return new AltosTelemetryReader(in);
691                 } catch (FileNotFoundException fe) {
692                         System.out.printf("Cannot open '%s'\n", filename);
693                         return null;
694                 }
695         }
696
697         static AltosCSV open_csv(String filename) {
698                 File file = new File (filename);
699                 try {
700                         return new AltosCSV(file);
701                 } catch (FileNotFoundException fe) {
702                         System.out.printf("Cannot open '%s'\n", filename);
703                         return null;
704                 }
705         }
706
707         static void process_file(String input) {
708                 String output = replace_extension(input,".csv");
709                 if (input.equals(output)) {
710                         System.out.printf("Not processing '%s'\n", input);
711                         return;
712                 }
713                 System.out.printf("Processing \"%s\" to \"%s\"\n", input, output);
714                 AltosReader reader = open_logfile(input);
715                 if (reader == null)
716                         return;
717                 AltosCSV writer = open_csv(output);
718                 if (writer == null)
719                         return;
720                 writer.write(reader);
721                 reader.close();
722                 writer.close();
723         }
724
725         public static void main(final String[] args) {
726
727                 /* Handle batch-mode */
728                 if (args.length > 0) {
729                         for (int i = 0; i < args.length; i++)
730                                 process_file(args[i]);
731                 } else {
732                         AltosUI altosui = new AltosUI();
733                         altosui.setVisible(true);
734                 }
735         }
736 }