altosui: Factor some UI elements into separate classes
[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         class ReplayTelemetryThread extends ReplayThread {
449                 ReplayTelemetryThread(FileInputStream in, String in_name) {
450                         super(new AltosTelemetryReader(in), in_name);
451                 }
452
453         }
454
455         class ReplayEepromThread extends ReplayThread {
456                 ReplayEepromThread(FileInputStream in, String in_name) {
457                         super(new AltosEepromReader(in), in_name);
458                 }
459         }
460
461         Thread          display_thread;
462
463         private void stop_display() {
464                 if (display_thread != null && display_thread.isAlive()) {
465                         display_thread.interrupt();
466                         try {
467                                 display_thread.join();
468                         } catch (InterruptedException ie) {}
469                 }
470                 display_thread = null;
471         }
472
473         private void run_display(Thread thread) {
474                 stop_display();
475                 display_thread = thread;
476                 display_thread.start();
477         }
478
479         /*
480          * Replay a flight from telemetry data
481          */
482         private void Replay() {
483                 AltosLogfileChooser chooser = new AltosLogfileChooser(
484                         AltosUI.this);
485                 AltosReader reader = chooser.runDialog();
486                 if (reader != null)
487                         run_display(new ReplayThread(reader,
488                                                      chooser.filename()));
489         }
490
491         /* Connect to TeleMetrum, either directly or through
492          * a TeleDongle over the packet link
493          */
494         private void SaveFlightData() {
495                 new AltosEepromDownload(AltosUI.this);
496         }
497
498         /* Load a flight log file and write out a CSV file containing
499          * all of the data in standard units
500          */
501
502         private void ExportData() {
503                 new AltosCSVUI(AltosUI.this);
504         }
505
506         /* Create the AltosUI menus
507          */
508         private void createMenu() {
509                 JMenuBar menubar = new JMenuBar();
510                 JMenu menu;
511                 JMenuItem item;
512                 JRadioButtonMenuItem radioitem;
513
514                 // File menu
515                 {
516                         menu = new JMenu("File");
517                         menu.setMnemonic(KeyEvent.VK_F);
518                         menubar.add(menu);
519
520                         item = new JMenuItem("Replay File",KeyEvent.VK_R);
521                         item.addActionListener(new ActionListener() {
522                                         public void actionPerformed(ActionEvent e) {
523                                                 Replay();
524                                         }
525                                 });
526                         menu.add(item);
527
528                         item = new JMenuItem("Save Flight Data",KeyEvent.VK_S);
529                         item.addActionListener(new ActionListener() {
530                                         public void actionPerformed(ActionEvent e) {
531                                                 SaveFlightData();
532                                         }
533                                 });
534                         menu.add(item);
535
536                         item = new JMenuItem("Flash Image",KeyEvent.VK_F);
537                         item.addActionListener(new ActionListener() {
538                                         public void actionPerformed(ActionEvent e) {
539                                                 FlashImage();
540                                         }
541                                 });
542                         menu.add(item);
543
544                         item = new JMenuItem("Export Data",KeyEvent.VK_F);
545                         item.addActionListener(new ActionListener() {
546                                         public void actionPerformed(ActionEvent e) {
547                                                 ExportData();
548                                         }
549                                 });
550                         menu.add(item);
551
552                         item = new JMenuItem("Quit",KeyEvent.VK_Q);
553                         item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q,
554                                                                    ActionEvent.CTRL_MASK));
555                         item.addActionListener(new ActionListener() {
556                                         public void actionPerformed(ActionEvent e) {
557                                                 System.exit(0);
558                                         }
559                                 });
560                         menu.add(item);
561                 }
562
563                 // Device menu
564                 {
565                         menu = new JMenu("Device");
566                         menu.setMnemonic(KeyEvent.VK_D);
567                         menubar.add(menu);
568
569                         item = new JMenuItem("Connect to Device",KeyEvent.VK_C);
570                         item.addActionListener(new ActionListener() {
571                                         public void actionPerformed(ActionEvent e) {
572                                                 ConnectToDevice();
573                                         }
574                                 });
575                         menu.add(item);
576
577                         item = new JMenuItem("Disconnect from Device",KeyEvent.VK_D);
578                         item.addActionListener(new ActionListener() {
579                                         public void actionPerformed(ActionEvent e) {
580                                                 DisconnectFromDevice();
581                                         }
582                                 });
583                         menu.add(item);
584
585                         menu.addSeparator();
586
587                         item = new JMenuItem("Set Callsign",KeyEvent.VK_S);
588                         item.addActionListener(new ActionListener() {
589                                         public void actionPerformed(ActionEvent e) {
590                                                 ConfigureCallsign();
591                                         }
592                                 });
593
594                         menu.add(item);
595
596                         item = new JMenuItem("Configure TeleMetrum device",KeyEvent.VK_T);
597                         item.addActionListener(new ActionListener() {
598                                         public void actionPerformed(ActionEvent e) {
599                                                 ConfigureTeleMetrum();
600                                         }
601                                 });
602
603                         menu.add(item);
604                 }
605                 // Log menu
606                 {
607                         menu = new JMenu("Log");
608                         menu.setMnemonic(KeyEvent.VK_L);
609                         menubar.add(menu);
610
611                         item = new JMenuItem("New Log",KeyEvent.VK_N);
612                         item.addActionListener(new ActionListener() {
613                                         public void actionPerformed(ActionEvent e) {
614                                         }
615                                 });
616                         menu.add(item);
617
618                         item = new JMenuItem("Configure Log",KeyEvent.VK_C);
619                         item.addActionListener(new ActionListener() {
620                                         public void actionPerformed(ActionEvent e) {
621                                                 AltosPreferences.ConfigureLog();
622                                         }
623                                 });
624                         menu.add(item);
625                 }
626                 // Voice menu
627                 {
628                         menu = new JMenu("Voice", true);
629                         menu.setMnemonic(KeyEvent.VK_V);
630                         menubar.add(menu);
631
632                         radioitem = new JRadioButtonMenuItem("Enable Voice", AltosPreferences.voice());
633                         radioitem.addActionListener(new ActionListener() {
634                                         public void actionPerformed(ActionEvent e) {
635                                                 JRadioButtonMenuItem item = (JRadioButtonMenuItem) e.getSource();
636                                                 boolean enabled = item.isSelected();
637                                                 AltosPreferences.set_voice(enabled);
638                                                 if (enabled)
639                                                         voice.speak_always("Enable voice.");
640                                                 else
641                                                         voice.speak_always("Disable voice.");
642                                         }
643                                 });
644                         menu.add(radioitem);
645                         item = new JMenuItem("Test Voice",KeyEvent.VK_T);
646                         item.addActionListener(new ActionListener() {
647                                         public void actionPerformed(ActionEvent e) {
648                                                 voice.speak("That's one small step for man; one giant leap for mankind.");
649                                         }
650                                 });
651                         menu.add(item);
652                 }
653
654                 // Channel menu
655                 {
656                         menu = new AltosChannelMenu(AltosPreferences.channel());
657                         menu.addActionListener(new ActionListener() {
658                                                 public void actionPerformed(ActionEvent e) {
659                                                         int new_channel = Integer.parseInt(e.getActionCommand());
660                                                         AltosPreferences.set_channel(new_channel);
661                                                         serial_line.set_channel(new_channel);
662                                                 }
663                                 });
664                         menu.setMnemonic(KeyEvent.VK_C);
665                         menubar.add(menu);
666                 }
667
668                 this.setJMenuBar(menubar);
669
670         }
671
672         static String replace_extension(String input, String extension) {
673                 int dot = input.lastIndexOf(".");
674                 if (dot > 0)
675                         input = input.substring(0,dot);
676                 return input.concat(extension);
677         }
678
679         static AltosReader open_logfile(String filename) {
680                 File file = new File (filename);
681                 try {
682                         FileInputStream in;
683
684                         in = new FileInputStream(file);
685                         if (filename.endsWith("eeprom"))
686                                 return new AltosEepromReader(in);
687                         else
688                                 return new AltosTelemetryReader(in);
689                 } catch (FileNotFoundException fe) {
690                         System.out.printf("Cannot open '%s'\n", filename);
691                         return null;
692                 }
693         }
694
695         static AltosCSV open_csv(String filename) {
696                 File file = new File (filename);
697                 try {
698                         return new AltosCSV(file);
699                 } catch (FileNotFoundException fe) {
700                         System.out.printf("Cannot open '%s'\n", filename);
701                         return null;
702                 }
703         }
704
705         static void process_file(String input) {
706                 String output = replace_extension(input,".csv");
707                 if (input.equals(output)) {
708                         System.out.printf("Not processing '%s'\n", input);
709                         return;
710                 }
711                 System.out.printf("Processing \"%s\" to \"%s\"\n", input, output);
712                 AltosReader reader = open_logfile(input);
713                 if (reader == null)
714                         return;
715                 AltosCSV writer = open_csv(output);
716                 if (writer == null)
717                         return;
718                 writer.write(reader);
719                 reader.close();
720                 writer.close();
721         }
722
723         public static void main(final String[] args) {
724
725                 /* Handle batch-mode */
726                 if (args.length > 0) {
727                         for (int i = 0; i < args.length; i++)
728                                 process_file(args[i]);
729                 } else {
730                         AltosUI altosui = new AltosUI();
731                         altosui.setVisible(true);
732                 }
733         }
734 }