altosui: Create iterables for log file scanning. Split out display threads
[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 import altosui.AltosDisplayThread;
50
51 import libaltosJNI.*;
52
53 public class AltosUI extends JFrame {
54         private int channel = -1;
55
56         private AltosStatusTable flightStatus;
57         private AltosInfoTable flightInfo;
58         private AltosSerial serial_line;
59         private AltosLog altos_log;
60         private Box vbox;
61
62         private Font statusFont = new Font("SansSerif", Font.BOLD, 24);
63         private Font infoLabelFont = new Font("SansSerif", Font.PLAIN, 14);
64         private Font infoValueFont = new Font("Monospaced", Font.PLAIN, 14);
65
66         public AltosVoice voice = new AltosVoice();
67
68         public static boolean load_library(Frame frame) {
69                 if (!AltosDevice.load_library()) {
70                         JOptionPane.showMessageDialog(frame,
71                                                       String.format("No AltOS library in \"%s\"",
72                                                                     System.getProperty("java.library.path","<undefined>")),
73                                                       "Cannot load device access library",
74                                                       JOptionPane.ERROR_MESSAGE);
75                         return false;
76                 }
77                 return true;
78         }
79
80         public AltosUI() {
81
82                 load_library(null);
83
84                 String[] statusNames = { "Height (m)", "State", "RSSI (dBm)", "Speed (m/s)" };
85                 Object[][] statusData = { { "0", "pad", "-50", "0" } };
86
87                 java.net.URL imgURL = AltosUI.class.getResource("/altus-metrum-16x16.jpg");
88                 if (imgURL != null)
89                         setIconImage(new ImageIcon(imgURL).getImage());
90
91                 AltosPreferences.init(this);
92
93                 vbox = Box.createVerticalBox();
94                 this.add(vbox);
95
96                 flightStatus = new AltosStatusTable(this);
97
98                 vbox.add(flightStatus);
99
100                 flightInfo = new AltosInfoTable();
101                 vbox.add(flightInfo.box());
102
103                 setTitle("AltOS");
104
105                 createMenu();
106
107                 serial_line = new AltosSerial();
108                 altos_log = new AltosLog(serial_line);
109                 int dpi = Toolkit.getDefaultToolkit().getScreenResolution();
110                 this.setSize(new Dimension (flightInfo.width(),
111                                             flightStatus.height() + flightInfo.height()));
112                 this.validate();
113                 setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
114                 addWindowListener(new WindowAdapter() {
115                         @Override
116                         public void windowClosing(WindowEvent e) {
117                                 System.exit(0);
118                         }
119                 });
120                 voice.speak("Rocket flight monitor ready.");
121         }
122
123         class DeviceThread extends AltosDisplayThread {
124                 AltosSerial     serial;
125                 LinkedBlockingQueue<AltosLine> telem;
126
127                 AltosRecord read() throws InterruptedException, ParseException, AltosCRCException, IOException {
128                         AltosLine l = telem.take();
129                         if (l.line == null)
130                                 throw new IOException("IO error");
131                         return new AltosTelemetry(l.line);
132                 }
133
134                 void close(boolean interrupted) {
135                         serial.close();
136                         serial.remove_monitor(telem);
137                 }
138
139                 public DeviceThread(AltosSerial s, String in_name, AltosVoice voice, AltosStatusTable status, AltosInfoTable info) {
140                         super(AltosUI.this, voice, status, info);
141                         serial = s;
142                         telem = new LinkedBlockingQueue<AltosLine>();
143                         serial.add_monitor(telem);
144                         name = in_name;
145                 }
146         }
147
148         private void ConnectToDevice() {
149                 AltosDevice     device = AltosDeviceDialog.show(AltosUI.this,
150                                                                 AltosDevice.product_basestation);
151
152                 if (device != null) {
153                         try {
154                                 stop_display();
155                                 serial_line.open(device);
156                                 DeviceThread thread = new DeviceThread(serial_line, device.getPath(), voice, flightStatus, flightInfo);
157                                 serial_line.set_channel(AltosPreferences.channel());
158                                 serial_line.set_callsign(AltosPreferences.callsign());
159                                 run_display(thread);
160                         } catch (FileNotFoundException ee) {
161                                 JOptionPane.showMessageDialog(AltosUI.this,
162                                                               String.format("Cannot open device \"%s\"",
163                                                                             device.getPath()),
164                                                               "Cannot open target device",
165                                                               JOptionPane.ERROR_MESSAGE);
166                         } catch (IOException ee) {
167                                 JOptionPane.showMessageDialog(AltosUI.this,
168                                                               device.getPath(),
169                                                               "Unkonwn I/O error",
170                                                               JOptionPane.ERROR_MESSAGE);
171                         }
172                 }
173         }
174
175         void DisconnectFromDevice () {
176                 stop_display();
177         }
178
179         void ConfigureCallsign() {
180                 String  result;
181                 result = JOptionPane.showInputDialog(AltosUI.this,
182                                                      "Configure Callsign",
183                                                      AltosPreferences.callsign());
184                 if (result != null) {
185                         AltosPreferences.set_callsign(result);
186                         if (serial_line != null)
187                                 serial_line.set_callsign(result);
188                 }
189         }
190
191         void ConfigureTeleMetrum() {
192                 new AltosConfig(AltosUI.this);
193         }
194
195         void FlashImage() {
196                 new AltosFlashUI(AltosUI.this);
197         }
198
199
200         Thread          display_thread;
201
202         private void stop_display() {
203                 if (display_thread != null && display_thread.isAlive()) {
204                         display_thread.interrupt();
205                         try {
206                                 display_thread.join();
207                         } catch (InterruptedException ie) {}
208                 }
209                 display_thread = null;
210         }
211
212         private void run_display(Thread thread) {
213                 stop_display();
214                 display_thread = thread;
215                 display_thread.start();
216         }
217
218         /*
219          * Replay a flight from telemetry data
220          */
221         private void Replay() {
222                 AltosLogfileChooser chooser = new AltosLogfileChooser(
223                         AltosUI.this);
224                 AltosRecordIterable iterable = chooser.runDialog();
225                 if (iterable != null)
226                         run_display(new AltosReplayThread(this, iterable.iterator(),
227                                                           chooser.filename(),
228                                                           voice,
229                                                           flightStatus,
230                                                           flightInfo));
231         }
232
233         /* Connect to TeleMetrum, either directly or through
234          * a TeleDongle over the packet link
235          */
236         private void SaveFlightData() {
237                 new AltosEepromDownload(AltosUI.this);
238         }
239
240         /* Load a flight log file and write out a CSV file containing
241          * all of the data in standard units
242          */
243
244         private void ExportData() {
245                 new AltosCSVUI(AltosUI.this);
246         }
247
248         /* Create the AltosUI menus
249          */
250         private void createMenu() {
251                 JMenuBar menubar = new JMenuBar();
252                 JMenu menu;
253                 JMenuItem item;
254                 JRadioButtonMenuItem radioitem;
255
256                 // File menu
257                 {
258                         menu = new JMenu("File");
259                         menu.setMnemonic(KeyEvent.VK_F);
260                         menubar.add(menu);
261
262                         item = new JMenuItem("Replay File",KeyEvent.VK_R);
263                         item.addActionListener(new ActionListener() {
264                                         public void actionPerformed(ActionEvent e) {
265                                                 Replay();
266                                         }
267                                 });
268                         menu.add(item);
269
270                         item = new JMenuItem("Save Flight Data",KeyEvent.VK_S);
271                         item.addActionListener(new ActionListener() {
272                                         public void actionPerformed(ActionEvent e) {
273                                                 SaveFlightData();
274                                         }
275                                 });
276                         menu.add(item);
277
278                         item = new JMenuItem("Flash Image",KeyEvent.VK_F);
279                         item.addActionListener(new ActionListener() {
280                                         public void actionPerformed(ActionEvent e) {
281                                                 FlashImage();
282                                         }
283                                 });
284                         menu.add(item);
285
286                         item = new JMenuItem("Export Data",KeyEvent.VK_F);
287                         item.addActionListener(new ActionListener() {
288                                         public void actionPerformed(ActionEvent e) {
289                                                 ExportData();
290                                         }
291                                 });
292                         menu.add(item);
293
294                         item = new JMenuItem("Quit",KeyEvent.VK_Q);
295                         item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q,
296                                                                    ActionEvent.CTRL_MASK));
297                         item.addActionListener(new ActionListener() {
298                                         public void actionPerformed(ActionEvent e) {
299                                                 System.exit(0);
300                                         }
301                                 });
302                         menu.add(item);
303                 }
304
305                 // Device menu
306                 {
307                         menu = new JMenu("Device");
308                         menu.setMnemonic(KeyEvent.VK_D);
309                         menubar.add(menu);
310
311                         item = new JMenuItem("Connect to Device",KeyEvent.VK_C);
312                         item.addActionListener(new ActionListener() {
313                                         public void actionPerformed(ActionEvent e) {
314                                                 ConnectToDevice();
315                                         }
316                                 });
317                         menu.add(item);
318
319                         item = new JMenuItem("Disconnect from Device",KeyEvent.VK_D);
320                         item.addActionListener(new ActionListener() {
321                                         public void actionPerformed(ActionEvent e) {
322                                                 DisconnectFromDevice();
323                                         }
324                                 });
325                         menu.add(item);
326
327                         menu.addSeparator();
328
329                         item = new JMenuItem("Set Callsign",KeyEvent.VK_S);
330                         item.addActionListener(new ActionListener() {
331                                         public void actionPerformed(ActionEvent e) {
332                                                 ConfigureCallsign();
333                                         }
334                                 });
335
336                         menu.add(item);
337
338                         item = new JMenuItem("Configure TeleMetrum device",KeyEvent.VK_T);
339                         item.addActionListener(new ActionListener() {
340                                         public void actionPerformed(ActionEvent e) {
341                                                 ConfigureTeleMetrum();
342                                         }
343                                 });
344
345                         menu.add(item);
346                 }
347                 // Log menu
348                 {
349                         menu = new JMenu("Log");
350                         menu.setMnemonic(KeyEvent.VK_L);
351                         menubar.add(menu);
352
353                         item = new JMenuItem("New Log",KeyEvent.VK_N);
354                         item.addActionListener(new ActionListener() {
355                                         public void actionPerformed(ActionEvent e) {
356                                         }
357                                 });
358                         menu.add(item);
359
360                         item = new JMenuItem("Configure Log",KeyEvent.VK_C);
361                         item.addActionListener(new ActionListener() {
362                                         public void actionPerformed(ActionEvent e) {
363                                                 AltosPreferences.ConfigureLog();
364                                         }
365                                 });
366                         menu.add(item);
367                 }
368                 // Voice menu
369                 {
370                         menu = new JMenu("Voice", true);
371                         menu.setMnemonic(KeyEvent.VK_V);
372                         menubar.add(menu);
373
374                         radioitem = new JRadioButtonMenuItem("Enable Voice", AltosPreferences.voice());
375                         radioitem.addActionListener(new ActionListener() {
376                                         public void actionPerformed(ActionEvent e) {
377                                                 JRadioButtonMenuItem item = (JRadioButtonMenuItem) e.getSource();
378                                                 boolean enabled = item.isSelected();
379                                                 AltosPreferences.set_voice(enabled);
380                                                 if (enabled)
381                                                         voice.speak_always("Enable voice.");
382                                                 else
383                                                         voice.speak_always("Disable voice.");
384                                         }
385                                 });
386                         menu.add(radioitem);
387                         item = new JMenuItem("Test Voice",KeyEvent.VK_T);
388                         item.addActionListener(new ActionListener() {
389                                         public void actionPerformed(ActionEvent e) {
390                                                 voice.speak("That's one small step for man; one giant leap for mankind.");
391                                         }
392                                 });
393                         menu.add(item);
394                 }
395
396                 // Channel menu
397                 {
398                         menu = new AltosChannelMenu(AltosPreferences.channel());
399                         menu.addActionListener(new ActionListener() {
400                                                 public void actionPerformed(ActionEvent e) {
401                                                         int new_channel = Integer.parseInt(e.getActionCommand());
402                                                         AltosPreferences.set_channel(new_channel);
403                                                         serial_line.set_channel(new_channel);
404                                                 }
405                                 });
406                         menu.setMnemonic(KeyEvent.VK_C);
407                         menubar.add(menu);
408                 }
409
410                 this.setJMenuBar(menubar);
411
412         }
413
414         static String replace_extension(String input, String extension) {
415                 int dot = input.lastIndexOf(".");
416                 if (dot > 0)
417                         input = input.substring(0,dot);
418                 return input.concat(extension);
419         }
420
421         static AltosRecordIterable open_logfile(String filename) {
422                 File file = new File (filename);
423                 try {
424                         FileInputStream in;
425
426                         in = new FileInputStream(file);
427                         if (filename.endsWith("eeprom"))
428                                 return new AltosEepromIterable(in);
429                         else
430                                 return new AltosTelemetryIterable(in);
431                 } catch (FileNotFoundException fe) {
432                         System.out.printf("Cannot open '%s'\n", filename);
433                         return null;
434                 }
435         }
436
437         static AltosCSV open_csv(String filename) {
438                 File file = new File (filename);
439                 try {
440                         return new AltosCSV(file);
441                 } catch (FileNotFoundException fe) {
442                         System.out.printf("Cannot open '%s'\n", filename);
443                         return null;
444                 }
445         }
446
447         static void process_file(String input) {
448                 String output = replace_extension(input,".csv");
449                 if (input.equals(output)) {
450                         System.out.printf("Not processing '%s'\n", input);
451                         return;
452                 }
453                 System.out.printf("Processing \"%s\" to \"%s\"\n", input, output);
454                 AltosRecordIterable iterable = open_logfile(input);
455                 if (iterable == null)
456                         return;
457                 AltosCSV writer = open_csv(output);
458                 if (writer == null)
459                         return;
460                 writer.write(iterable);
461                 writer.close();
462         }
463
464         public static void main(final String[] args) {
465
466                 /* Handle batch-mode */
467                 if (args.length > 0) {
468                         for (int i = 0; i < args.length; i++)
469                                 process_file(args[i]);
470                 } else {
471                         AltosUI altosui = new AltosUI();
472                         altosui.setVisible(true);
473                 }
474         }
475 }