altosui: remove unused ReplayThread wrapper 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         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         /* Create the AltosUI menus
494          */
495         private void createMenu() {
496                 JMenuBar menubar = new JMenuBar();
497                 JMenu menu;
498                 JMenuItem item;
499                 JRadioButtonMenuItem radioitem;
500
501                 // File menu
502                 {
503                         menu = new JMenu("File");
504                         menu.setMnemonic(KeyEvent.VK_F);
505                         menubar.add(menu);
506
507                         item = new JMenuItem("Replay File",KeyEvent.VK_R);
508                         item.addActionListener(new ActionListener() {
509                                         public void actionPerformed(ActionEvent e) {
510                                                 Replay();
511                                         }
512                                 });
513                         menu.add(item);
514
515                         item = new JMenuItem("Save Flight Data",KeyEvent.VK_S);
516                         item.addActionListener(new ActionListener() {
517                                         public void actionPerformed(ActionEvent e) {
518                                                 SaveFlightData();
519                                         }
520                                 });
521                         menu.add(item);
522
523                         item = new JMenuItem("Flash Image",KeyEvent.VK_F);
524                         item.addActionListener(new ActionListener() {
525                                         public void actionPerformed(ActionEvent e) {
526                                                 FlashImage();
527                                         }
528                                 });
529                         menu.add(item);
530
531                         item = new JMenuItem("Export Data",KeyEvent.VK_F);
532                         item.addActionListener(new ActionListener() {
533                                         public void actionPerformed(ActionEvent e) {
534                                                 ExportData();
535                                         }
536                                 });
537                         menu.add(item);
538
539                         item = new JMenuItem("Quit",KeyEvent.VK_Q);
540                         item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q,
541                                                                    ActionEvent.CTRL_MASK));
542                         item.addActionListener(new ActionListener() {
543                                         public void actionPerformed(ActionEvent e) {
544                                                 System.exit(0);
545                                         }
546                                 });
547                         menu.add(item);
548                 }
549
550                 // Device menu
551                 {
552                         menu = new JMenu("Device");
553                         menu.setMnemonic(KeyEvent.VK_D);
554                         menubar.add(menu);
555
556                         item = new JMenuItem("Connect to Device",KeyEvent.VK_C);
557                         item.addActionListener(new ActionListener() {
558                                         public void actionPerformed(ActionEvent e) {
559                                                 ConnectToDevice();
560                                         }
561                                 });
562                         menu.add(item);
563
564                         item = new JMenuItem("Disconnect from Device",KeyEvent.VK_D);
565                         item.addActionListener(new ActionListener() {
566                                         public void actionPerformed(ActionEvent e) {
567                                                 DisconnectFromDevice();
568                                         }
569                                 });
570                         menu.add(item);
571
572                         menu.addSeparator();
573
574                         item = new JMenuItem("Set Callsign",KeyEvent.VK_S);
575                         item.addActionListener(new ActionListener() {
576                                         public void actionPerformed(ActionEvent e) {
577                                                 ConfigureCallsign();
578                                         }
579                                 });
580
581                         menu.add(item);
582
583                         item = new JMenuItem("Configure TeleMetrum device",KeyEvent.VK_T);
584                         item.addActionListener(new ActionListener() {
585                                         public void actionPerformed(ActionEvent e) {
586                                                 ConfigureTeleMetrum();
587                                         }
588                                 });
589
590                         menu.add(item);
591                 }
592                 // Log menu
593                 {
594                         menu = new JMenu("Log");
595                         menu.setMnemonic(KeyEvent.VK_L);
596                         menubar.add(menu);
597
598                         item = new JMenuItem("New Log",KeyEvent.VK_N);
599                         item.addActionListener(new ActionListener() {
600                                         public void actionPerformed(ActionEvent e) {
601                                         }
602                                 });
603                         menu.add(item);
604
605                         item = new JMenuItem("Configure Log",KeyEvent.VK_C);
606                         item.addActionListener(new ActionListener() {
607                                         public void actionPerformed(ActionEvent e) {
608                                                 AltosPreferences.ConfigureLog();
609                                         }
610                                 });
611                         menu.add(item);
612                 }
613                 // Voice menu
614                 {
615                         menu = new JMenu("Voice", true);
616                         menu.setMnemonic(KeyEvent.VK_V);
617                         menubar.add(menu);
618
619                         radioitem = new JRadioButtonMenuItem("Enable Voice", AltosPreferences.voice());
620                         radioitem.addActionListener(new ActionListener() {
621                                         public void actionPerformed(ActionEvent e) {
622                                                 JRadioButtonMenuItem item = (JRadioButtonMenuItem) e.getSource();
623                                                 boolean enabled = item.isSelected();
624                                                 AltosPreferences.set_voice(enabled);
625                                                 if (enabled)
626                                                         voice.speak_always("Enable voice.");
627                                                 else
628                                                         voice.speak_always("Disable voice.");
629                                         }
630                                 });
631                         menu.add(radioitem);
632                         item = new JMenuItem("Test Voice",KeyEvent.VK_T);
633                         item.addActionListener(new ActionListener() {
634                                         public void actionPerformed(ActionEvent e) {
635                                                 voice.speak("That's one small step for man; one giant leap for mankind.");
636                                         }
637                                 });
638                         menu.add(item);
639                 }
640
641                 // Channel menu
642                 {
643                         menu = new AltosChannelMenu(AltosPreferences.channel());
644                         menu.addActionListener(new ActionListener() {
645                                                 public void actionPerformed(ActionEvent e) {
646                                                         int new_channel = Integer.parseInt(e.getActionCommand());
647                                                         AltosPreferences.set_channel(new_channel);
648                                                         serial_line.set_channel(new_channel);
649                                                 }
650                                 });
651                         menu.setMnemonic(KeyEvent.VK_C);
652                         menubar.add(menu);
653                 }
654
655                 this.setJMenuBar(menubar);
656
657         }
658
659         static String replace_extension(String input, String extension) {
660                 int dot = input.lastIndexOf(".");
661                 if (dot > 0)
662                         input = input.substring(0,dot);
663                 return input.concat(extension);
664         }
665
666         static AltosReader open_logfile(String filename) {
667                 File file = new File (filename);
668                 try {
669                         FileInputStream in;
670
671                         in = new FileInputStream(file);
672                         if (filename.endsWith("eeprom"))
673                                 return new AltosEepromReader(in);
674                         else
675                                 return new AltosTelemetryReader(in);
676                 } catch (FileNotFoundException fe) {
677                         System.out.printf("Cannot open '%s'\n", filename);
678                         return null;
679                 }
680         }
681
682         static AltosCSV open_csv(String filename) {
683                 File file = new File (filename);
684                 try {
685                         return new AltosCSV(file);
686                 } catch (FileNotFoundException fe) {
687                         System.out.printf("Cannot open '%s'\n", filename);
688                         return null;
689                 }
690         }
691
692         static void process_file(String input) {
693                 String output = replace_extension(input,".csv");
694                 if (input.equals(output)) {
695                         System.out.printf("Not processing '%s'\n", input);
696                         return;
697                 }
698                 System.out.printf("Processing \"%s\" to \"%s\"\n", input, output);
699                 AltosReader reader = open_logfile(input);
700                 if (reader == null)
701                         return;
702                 AltosCSV writer = open_csv(output);
703                 if (writer == null)
704                         return;
705                 writer.write(reader);
706                 reader.close();
707                 writer.close();
708         }
709
710         public static void main(final String[] args) {
711
712                 /* Handle batch-mode */
713                 if (args.length > 0) {
714                         for (int i = 0; i < args.length; i++)
715                                 process_file(args[i]);
716                 } else {
717                         AltosUI altosui = new AltosUI();
718                         altosui.setVisible(true);
719                 }
720         }
721 }