Display table of flight info. gps is not working yet though
[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.AbstractTableModel;
25 import java.io.*;
26 import java.util.*;
27 import java.text.*;
28 import gnu.io.CommPortIdentifier;
29
30 import altosui.AltosSerial;
31 import altosui.AltosSerialMonitor;
32 import altosui.AltosTelemetry;
33 import altosui.AltosState;
34
35 class AltosUIMonitor implements AltosSerialMonitor {
36         public void data(String data) {
37                 System.out.println(data);
38         }
39 }
40
41 class AltosFlightStatusTableModel extends AbstractTableModel {
42         private String[] columnNames = {"Height (m)", "State", "RSSI (dBm)", "Speed (m/s)" };
43         private Object[] data = { 0, "idle", 0, 0 };
44
45         public int getColumnCount() { return columnNames.length; }
46         public int getRowCount() { return 1; }
47         public String getColumnName(int col) { return columnNames[col]; }
48         public Object getValueAt(int row, int col) { return data[col]; }
49
50         public void setValueAt(Object value, int col) {
51                 data[col] = value;
52                 fireTableCellUpdated(0, col);
53         }
54
55         public void setValueAt(Object value, int row, int col) {
56                 setValueAt(value, col);
57         }
58
59         public void set(AltosState state) {
60                 setValueAt(String.format("%1.0f", state.height), 0);
61                 setValueAt(state.data.state, 1);
62                 setValueAt(state.data.rssi, 2);
63                 double speed = state.baro_speed;
64                 if (state.ascent)
65                         speed = state.speed;
66                 setValueAt(String.format("%1.0f", speed), 3);
67         }
68 }
69
70 class AltosFlightInfoTableModel extends AbstractTableModel {
71         private String[] columnNames = {"Field", "Value"};
72
73         class InfoLine {
74                 String  name;
75                 String  value;
76
77                 public InfoLine(String n, String v) {
78                         name = n;
79                         value = v;
80                 }
81         }
82
83         private ArrayList<InfoLine> rows = new ArrayList<InfoLine>();
84
85         public int getColumnCount() { return columnNames.length; }
86         public String getColumnName(int col) { return columnNames[col]; }
87
88         public int getRowCount() { return 20; }
89
90         public Object getValueAt(int row, int col) {
91                 if (row >= rows.size())
92                         return "";
93                 if (col == 0)
94                         return rows.get(row).name;
95                 else
96                         return rows.get(row).value;
97         }
98
99         int     current_row = 0;
100         int     prev_num_rows = 0;
101
102         public void resetRow() {
103                 current_row = 0;
104         }
105         public void addRow(String name, String value) {
106                 if (current_row >= rows.size())
107                         rows.add(current_row, new InfoLine(name, value));
108                 else
109                         rows.set(current_row, new InfoLine(name, value));
110                 current_row++;
111         }
112         public void finish() {
113                 if (current_row > prev_num_rows) {
114                         fireTableRowsInserted(prev_num_rows, current_row - 1);
115                         prev_num_rows = current_row;
116                 }
117                 fireTableDataChanged();
118         }
119 }
120
121 public class AltosUI extends JFrame {
122         private int channel = -1;
123
124         private AltosFlightStatusTableModel flightStatusModel;
125         private JTable flightStatus;
126
127         static final int info_columns = 3;
128
129         private AltosFlightInfoTableModel[] flightInfoModel;
130         private JTable[] flightInfo;
131         private AltosSerial serialLine;
132         private Box[] ibox;
133         private Box vbox;
134         private Box hbox;
135
136         public AltosUI() {
137
138                 String[] statusNames = { "Height (m)", "State", "RSSI (dBm)", "Speed (m/s)" };
139                 Object[][] statusData = { { "0", "pad", "-50", "0" } };
140
141                 vbox = Box.createVerticalBox();
142                 this.add(vbox);
143
144                 flightStatusModel = new AltosFlightStatusTableModel();
145                 flightStatus = new JTable(flightStatusModel);
146
147                 flightStatus.setShowGrid(false);
148
149                 flightInfo = new JTable[3];
150                 flightInfoModel = new AltosFlightInfoTableModel[3];
151                 ibox = new Box[3];
152
153                 vbox.add(flightStatus.getTableHeader());
154                 vbox.add(flightStatus);
155
156                 hbox = Box.createHorizontalBox();
157                 vbox.add(hbox);
158
159                 for (int i = 0; i < info_columns; i++) {
160                         ibox[i] = Box.createVerticalBox();
161                         flightInfoModel[i] = new AltosFlightInfoTableModel();
162                         flightInfo[i] = new JTable(flightInfoModel[i]);
163                         flightInfo[i].setShowGrid(true);
164                         ibox[i].add(flightInfo[i].getTableHeader());
165                         ibox[i].add(flightInfo[i]);
166                         hbox.add(ibox[i]);
167                 }
168
169                 setTitle("AltOS");
170
171                 createMenu();
172
173                 serialLine = new AltosSerial();
174                 serialLine.monitor(new AltosUIMonitor());
175                 int dpi = Toolkit.getDefaultToolkit().getScreenResolution();
176                 this.setSize(new Dimension (dpi * 5, dpi * 4));
177                 this.validate();
178                 setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
179                 addWindowListener(new WindowAdapter() {
180                         @Override
181                         public void windowClosing(WindowEvent e) {
182                                 System.exit(0);
183                         }
184                 });
185         }
186
187         public void info_reset() {
188                 for (int i = 0; i < info_columns; i++)
189                         flightInfoModel[i].resetRow();
190         }
191
192         public void info_add_row(int col, String name, String value) {
193                 flightInfoModel[col].addRow(name, value);
194         }
195
196         public void info_add_row(int col, String name, String format, Object value) {
197                 flightInfoModel[col].addRow(name, String.format(format, value));
198         }
199
200         public void info_add_row(int col, String name, String format, Object v1, Object v2) {
201                 flightInfoModel[col].addRow(name, String.format(format, v1, v2));
202         }
203
204         public void info_add_row(int col, String name, String format, Object v1, Object v2, Object v3) {
205                 flightInfoModel[col].addRow(name, String.format(format, v1, v2, v3));
206         }
207
208         public void info_add_deg(int col, String name, double v, int pos, int neg) {
209                 int     c = pos;
210                 if (v < 0) {
211                         c = neg;
212                         v = -v;
213                 }
214                 double  deg = Math.floor(v);
215                 double  min = (v - deg) * 60;
216
217                 flightInfoModel[col].addRow(name, String.format("%3.0f°%08.5f'", deg, min));
218         }
219
220         public void info_finish() {
221                 for (int i = 0; i < info_columns; i++)
222                         flightInfoModel[i].finish();
223         }
224
225         static final int MIN_PAD_SAMPLES = 10;
226
227         public void show(AltosState state) {
228                 flightStatusModel.set(state);
229
230                 info_reset();
231                 if (state.npad >= MIN_PAD_SAMPLES)
232                         info_add_row(0, "Ground state", "%s", "ready");
233                 else
234                         info_add_row(0, "Ground state", "waiting for gps (%d)",
235                                      MIN_PAD_SAMPLES - state.npad);
236                 info_add_row(0, "Rocket state", "%s", state.data.state);
237                 info_add_row(0, "Callsign", "%s", state.data.callsign);
238                 info_add_row(0, "Rocket serial", "%d", state.data.serial);
239                 info_add_row(0, "Rocket flight", "%d", state.data.flight);
240
241                 info_add_row(0, "RSSI", "%6ddBm", state.data.rssi);
242                 info_add_row(0, "Height", "%6.0fm", state.height);
243                 info_add_row(0, "Max height", "%6.0fm", state.max_height);
244                 info_add_row(0, "Acceleration", "%7.1fm/s²", state.acceleration);
245                 info_add_row(0, "Max acceleration", "%7.1fm/s²", state.max_acceleration);
246                 info_add_row(0, "Speed", "%7.1fm/s", state.ascent ? state.speed : state.baro_speed);
247                 info_add_row(0, "Max Speed", "%7.1fm/s", state.max_speed);
248                 info_add_row(0, "Temperature", "%6.2f°C", state.temperature);
249                 info_add_row(0, "Battery", "%5.2fV", state.battery);
250                 info_add_row(0, "Drogue", "%5.2fV", state.drogue_sense);
251                 info_add_row(0, "Main", "%5.2fV", state.main_sense);
252                 info_add_row(0, "Pad altitude", "%6.0fm", state.ground_altitude);
253                 if (state.gps != null)
254                         info_add_row(1, "Satellites", "%d", state.gps.nsat);
255                 else
256                         info_add_row(1, "Satellites", "%d", 0);
257                 if (state.gps != null && state.gps.gps_locked) {
258                         info_add_row(1, "GPS", "locked");
259                 } else if (state.gps != null && state.gps.gps_connected) {
260                         info_add_row(1, "GPS", "unlocked");
261                 } else {
262                         info_add_row(1, "GPS", "not available");
263                 }
264                 if (state.gps != null) {
265                         info_add_deg(1, "Latitude", state.gps.lat, 'N', 'S');
266                         info_add_deg(1, "Longitude", state.gps.lon, 'E', 'W');
267                         info_add_row(1, "GPS altitude", "%d", state.gps.alt);
268                         info_add_row(1, "GPS height", "%d", state.gps_height);
269                         info_add_row(1, "GPS date", "%04d-%02d-%02d",
270                                        state.gps.gps_time.year,
271                                        state.gps.gps_time.month,
272                                        state.gps.gps_time.day);
273                         info_add_row(1, "GPS time", "%02d:%02d:%02d",
274                                        state.gps.gps_time.hour,
275                                        state.gps.gps_time.minute,
276                                        state.gps.gps_time.second);
277                         info_add_row(1, "GPS ground speed", "%7.1fm/s %d°",
278                                        state.gps.ground_speed,
279                                        state.gps.course);
280                         info_add_row(1, "GPS climb rate", "%7.1fm/s",
281                                      state.gps.climb_rate);
282                         info_add_row(1, "GPS precision", "%4.1f(hdop) %3dm(h) %3dm(v)",
283                                      state.gps.hdop, state.gps.h_error, state.gps.v_error);
284                 }
285                 if (state.npad > 0) {
286                         info_add_row(1, "Distance from pad", "%5.0fm", state.from_pad.distance);
287                         info_add_row(1, "Direction from pad", "%4.0f°", state.from_pad.bearing);
288                         info_add_deg(1, "Pad latitude", state.pad_lat, 'N', 'S');
289                         info_add_deg(1, "Pad longitude", state.pad_lon, 'E', 'W');
290                         info_add_row(1, "Pad GPS alt", "%gm", state.pad_alt);
291                 }
292                 if (state.gps != null && state.gps.gps_connected) {
293                         int     nsat_vis = 0;
294                         int     c;
295
296                         if (state.gps.cc_gps_sat == null)
297                                 info_add_row(2, "Satellites Visible", "%d", 0);
298                         else {
299                                 info_add_row(2, "Satellites Visible", "%d", state.gps.cc_gps_sat.length);
300                                 for (c = 0; c < state.gps.cc_gps_sat.length; c++) {
301                                         info_add_row(2, "Satellite id,C/N0",
302                                                      "%3d,%2d",
303                                                      state.gps.cc_gps_sat[c].svid,
304                                                      state.gps.cc_gps_sat[c].c_n0);
305                                 }
306                         }
307                 }
308                 info_finish();
309         }
310
311
312         final JFileChooser deviceChooser = new JFileChooser();
313         final JFileChooser logdirChooser = new JFileChooser();
314         final String logdirName = "TeleMetrum";
315         File logdir = null;
316
317         private void setLogdir() {
318                 if (logdir == null)
319                         logdir = new File(logdirChooser.getCurrentDirectory(), logdirName);
320                 logdirChooser.setCurrentDirectory(logdir);
321         }
322
323         private void makeLogdir() {
324                 setLogdir();
325                 if (!logdir.exists()) {
326                         if (!logdir.mkdirs())
327                                 JOptionPane.showMessageDialog(AltosUI.this,
328                                                               logdir.getName(),
329                                                               "Cannot create directory",
330                                                               JOptionPane.ERROR_MESSAGE);
331                 } else if (!logdir.isDirectory()) {
332                         JOptionPane.showMessageDialog(AltosUI.this,
333                                                       logdir.getName(),
334                                                       "Is not a directory",
335                                                       JOptionPane.ERROR_MESSAGE);
336                 }
337         }
338
339         private void PickSerialDevice() {
340                 java.util.Enumeration<CommPortIdentifier> port_list = CommPortIdentifier.getPortIdentifiers();
341                 while (port_list.hasMoreElements()) {
342                         CommPortIdentifier identifier = port_list.nextElement();
343                         System.out.println("Serial port " + identifier.getName());
344                 }
345         }
346
347         private void ConnectToDevice() {
348                 PickSerialDevice();
349                 int returnVal = deviceChooser.showOpenDialog(AltosUI.this);
350
351                 if (returnVal == JFileChooser.APPROVE_OPTION) {
352                         File file = deviceChooser.getSelectedFile();
353                         try {
354                                 serialLine.open(file);
355                         } catch (FileNotFoundException ee) {
356                                 JOptionPane.showMessageDialog(AltosUI.this,
357                                                               file.getName(),
358                                                               "Cannot open serial port",
359                                                               JOptionPane.ERROR_MESSAGE);
360                         }
361                 }
362         }
363
364         String readline(FileInputStream s) throws IOException {
365                 int c;
366                 String  line = "";
367
368                 while ((c = s.read()) != -1) {
369                         if (c == '\r')
370                                 continue;
371                         if (c == '\n')
372                                 return line;
373                         line = line + (char) c;
374                 }
375                 return null;
376         }
377
378         /*
379          * Open an existing telemetry file and replay it in realtime
380          */
381
382         class ReplayThread extends Thread {
383                 FileInputStream replay;
384                 String filename;
385
386                 ReplayThread(FileInputStream in, String name) {
387                         replay = in;
388                         filename = name;
389                 }
390
391                 public void run() {
392                         String  line;
393                         AltosState      state = null;
394                         try {
395                                 while ((line = readline(replay)) != null) {
396                                         try {
397                                                 AltosTelemetry  t = new AltosTelemetry(line);
398                                                 state = new AltosState(t, state);
399                                                 show(state);
400                                                 try {
401                                                         if (state.state > AltosTelemetry.ao_flight_pad)
402                                                                 Thread.sleep((int) (state.time_change * 1000));
403                                                 } catch (InterruptedException e) {}
404                                         } catch (ParseException pp) {
405                                                 JOptionPane.showMessageDialog(AltosUI.this,
406                                                                               line,
407                                                                               "error parsing",
408                                                                               JOptionPane.ERROR_MESSAGE);
409                                                 break;
410                                         }
411                                 }
412                         } catch (IOException ee) {
413                                 JOptionPane.showMessageDialog(AltosUI.this,
414                                                               filename,
415                                                               "error reading",
416                                                               JOptionPane.ERROR_MESSAGE);
417                         } finally {
418                                 try {
419                                         replay.close();
420                                 } catch (IOException e) {}
421                         }
422                 }
423         }
424
425         private void Replay() {
426                 setLogdir();
427                 logdirChooser.setDialogTitle("Select Telemetry File");
428                 logdirChooser.setFileFilter(new FileNameExtensionFilter("Telemetry file", "telem"));
429                 int returnVal = logdirChooser.showOpenDialog(AltosUI.this);
430
431                 if (returnVal == JFileChooser.APPROVE_OPTION) {
432                         File file = logdirChooser.getSelectedFile();
433                         if (file == null)
434                                 System.out.println("No file selected?");
435                         String  filename = file.getName();
436                         try {
437                                 FileInputStream replay = new FileInputStream(file);
438                                 ReplayThread    thread = new ReplayThread(replay, filename);
439                                 thread.start();
440                         } catch (FileNotFoundException ee) {
441                                 JOptionPane.showMessageDialog(AltosUI.this,
442                                                               filename,
443                                                               "Cannot open serial port",
444                                                               JOptionPane.ERROR_MESSAGE);
445                         }
446                 }
447         }
448
449         private void SaveFlightData() {
450         }
451
452         private void createMenu() {
453                 JMenuBar menubar = new JMenuBar();
454                 JMenu menu;
455                 JMenuItem item;
456                 JRadioButtonMenuItem radioitem;
457
458                 // File menu
459                 {
460                         menu = new JMenu("File");
461                         menu.setMnemonic(KeyEvent.VK_F);
462                         menubar.add(menu);
463
464                         item = new JMenuItem("Quit",KeyEvent.VK_Q);
465                         item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q,
466                                                                    ActionEvent.CTRL_MASK));
467                         item.addActionListener(new ActionListener() {
468                                         public void actionPerformed(ActionEvent e) {
469                                                 System.exit(0);
470                                         }
471                                 });
472                         menu.add(item);
473                 }
474
475                 // Device menu
476                 {
477                         menu = new JMenu("Device");
478                         menu.setMnemonic(KeyEvent.VK_D);
479                         menubar.add(menu);
480
481                         item = new JMenuItem("Connect to Device",KeyEvent.VK_C);
482                         item.addActionListener(new ActionListener() {
483                                         public void actionPerformed(ActionEvent e) {
484                                                 ConnectToDevice();
485                                         }
486                                 });
487                         menu.add(item);
488
489                         item = new JMenuItem("Disconnect from Device",KeyEvent.VK_D);
490                         item.addActionListener(new ActionListener() {
491                                         public void actionPerformed(ActionEvent e) {
492                                                 serialLine.close();
493                                         }
494                                 });
495                         menu.add(item);
496
497                         menu.addSeparator();
498
499                         item = new JMenuItem("Save Flight Data",KeyEvent.VK_S);
500                         item.addActionListener(new ActionListener() {
501                                         public void actionPerformed(ActionEvent e) {
502                                                 SaveFlightData();
503                                         }
504                                 });
505                         menu.add(item);
506
507                         item = new JMenuItem("Replay",KeyEvent.VK_R);
508                         item.addActionListener(new ActionListener() {
509                                         public void actionPerformed(ActionEvent e) {
510                                                 Replay();
511                                         }
512                                 });
513                         menu.add(item);
514                 }
515                 // Log menu
516                 {
517                         menu = new JMenu("Log");
518                         menu.setMnemonic(KeyEvent.VK_L);
519                         menubar.add(menu);
520
521                         item = new JMenuItem("New Log",KeyEvent.VK_N);
522                         item.addActionListener(new ActionListener() {
523                                         public void actionPerformed(ActionEvent e) {
524                                         }
525                                 });
526                         menu.add(item);
527
528                         item = new JMenuItem("Configure Log",KeyEvent.VK_C);
529                         item.addActionListener(new ActionListener() {
530                                         public void actionPerformed(ActionEvent e) {
531                                         }
532                                 });
533                         menu.add(item);
534                 }
535                 // Voice menu
536                 {
537                         menu = new JMenu("Voice", true);
538                         menu.setMnemonic(KeyEvent.VK_V);
539                         menubar.add(menu);
540
541                         radioitem = new JRadioButtonMenuItem("Enable Voice");
542                         radioitem.addActionListener(new ActionListener() {
543                                         public void actionPerformed(ActionEvent e) {
544                                         }
545                                 });
546                         menu.add(radioitem);
547                 }
548
549                 // Channel menu
550                 {
551                         menu = new JMenu("Channel", true);
552                         menu.setMnemonic(KeyEvent.VK_C);
553                         menubar.add(menu);
554                         ButtonGroup group = new ButtonGroup();
555
556                         for (int c = 0; c <= 9; c++) {
557                                 radioitem = new JRadioButtonMenuItem(String.format("Channel %1d (%7.3fMHz)", c,
558                                                                                    434.550 + c * 0.1),
559                                                                      c == 0);
560                                 radioitem.setActionCommand(String.format("%d", c));
561                                 radioitem.addActionListener(new ActionListener() {
562                                                 public void actionPerformed(ActionEvent e) {
563                                                         System.out.println("Command: " + e.getActionCommand() + " param: " +
564                                                                            e.paramString());
565                                                 }
566                                         });
567                                 menu.add(radioitem);
568                                 group.add(radioitem);
569                         }
570                 }
571
572                 this.setJMenuBar(menubar);
573
574         }
575         public static void main(final String[] args) {
576                 AltosUI altosui = new AltosUI();
577                 altosui.setVisible(true);
578         }
579 }