49d1f11a67f23e04c52d69ea768d9d0a9aff8393
[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.AltosFlightStatusTableModel;
42 import altosui.AltosFlightInfoTableModel;
43 import altosui.AltosChannelMenu;
44
45 import libaltosJNI.*;
46
47 public class AltosUI extends JFrame {
48         private int channel = -1;
49
50         private AltosFlightStatusTableModel flightStatusModel;
51         private JTable flightStatus;
52
53         static final int info_columns = 3;
54
55         private AltosFlightInfoTableModel[] flightInfoModel;
56         private JTable[] flightInfo;
57         private AltosSerial serial_line;
58         private AltosLog altos_log;
59         private Box[] ibox;
60         private Box vbox;
61         private Box hbox;
62
63         private Font statusFont = new Font("SansSerif", Font.BOLD, 24);
64         private Font infoLabelFont = new Font("SansSerif", Font.PLAIN, 14);
65         private Font infoValueFont = new Font("Monospaced", Font.PLAIN, 14);
66
67         public AltosVoice voice = new AltosVoice();
68
69         public AltosUI() {
70
71                 String[] statusNames = { "Height (m)", "State", "RSSI (dBm)", "Speed (m/s)" };
72                 Object[][] statusData = { { "0", "pad", "-50", "0" } };
73
74                 AltosPreferences.init(this);
75
76                 vbox = Box.createVerticalBox();
77                 this.add(vbox);
78
79                 flightStatusModel = new AltosFlightStatusTableModel();
80                 flightStatus = new JTable(flightStatusModel);
81                 flightStatus.setFont(statusFont);
82                 TableColumnModel tcm = flightStatus.getColumnModel();
83                 for (int i = 0; i < flightStatusModel.getColumnCount(); i++) {
84                         DefaultTableCellRenderer       r = new DefaultTableCellRenderer();
85                         r.setFont(statusFont);
86                         r.setHorizontalAlignment(SwingConstants.CENTER);
87                         tcm.getColumn(i).setCellRenderer(r);
88                 }
89
90                 FontMetrics     statusMetrics = flightStatus.getFontMetrics(statusFont);
91                 int statusHeight = (statusMetrics.getHeight() + statusMetrics.getLeading()) * 15 / 10;
92                 flightStatus.setRowHeight(statusHeight);
93                 flightStatus.setShowGrid(false);
94
95                 vbox.add(flightStatus);
96
97                 hbox = Box.createHorizontalBox();
98                 vbox.add(hbox);
99
100                 flightInfo = new JTable[3];
101                 flightInfoModel = new AltosFlightInfoTableModel[3];
102                 ibox = new Box[3];
103                 FontMetrics     infoValueMetrics = flightStatus.getFontMetrics(infoValueFont);
104                 int infoHeight = (infoValueMetrics.getHeight() + infoValueMetrics.getLeading()) * 20 / 10;
105
106                 for (int i = 0; i < info_columns; i++) {
107                         ibox[i] = Box.createVerticalBox();
108                         flightInfoModel[i] = new AltosFlightInfoTableModel();
109                         flightInfo[i] = new JTable(flightInfoModel[i]);
110                         flightInfo[i].setFont(infoValueFont);
111                         flightInfo[i].setRowHeight(infoHeight);
112                         flightInfo[i].setShowGrid(true);
113                         ibox[i].add(flightInfo[i].getTableHeader());
114                         ibox[i].add(flightInfo[i]);
115                         hbox.add(ibox[i]);
116                 }
117
118                 setTitle("AltOS");
119
120                 createMenu();
121
122                 serial_line = new AltosSerial();
123                 altos_log = new AltosLog(serial_line);
124                 int dpi = Toolkit.getDefaultToolkit().getScreenResolution();
125                 this.setSize(new Dimension (infoValueMetrics.charWidth('0') * 6 * 20,
126                                             statusHeight * 4 + infoHeight * 17));
127                 this.validate();
128                 setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
129                 addWindowListener(new WindowAdapter() {
130                         @Override
131                         public void windowClosing(WindowEvent e) {
132                                 System.exit(0);
133                         }
134                 });
135                 voice.speak("Rocket flight monitor ready.");
136         }
137
138         public void info_reset() {
139                 for (int i = 0; i < info_columns; i++)
140                         flightInfoModel[i].resetRow();
141         }
142
143         public void info_add_row(int col, String name, String value) {
144                 flightInfoModel[col].addRow(name, value);
145         }
146
147         public void info_add_row(int col, String name, String format, Object... parameters) {
148                 flightInfoModel[col].addRow(name, String.format(format, parameters));
149         }
150
151         public void info_add_deg(int col, String name, double v, int pos, int neg) {
152                 int     c = pos;
153                 if (v < 0) {
154                         c = neg;
155                         v = -v;
156                 }
157                 double  deg = Math.floor(v);
158                 double  min = (v - deg) * 60;
159
160                 flightInfoModel[col].addRow(name, String.format("%3.0f°%08.5f'", deg, min));
161         }
162
163         public void info_finish() {
164                 for (int i = 0; i < info_columns; i++)
165                         flightInfoModel[i].finish();
166         }
167
168         public void show(AltosState state) {
169                 flightStatusModel.set(state);
170
171                 info_reset();
172                 if (state.gps_ready)
173                         info_add_row(0, "Ground state", "%s", "ready");
174                 else
175                         info_add_row(0, "Ground state", "wait (%d)",
176                                      state.gps_waiting);
177                 info_add_row(0, "Rocket state", "%s", state.data.state());
178                 info_add_row(0, "Callsign", "%s", state.data.callsign);
179                 info_add_row(0, "Rocket serial", "%6d", state.data.serial);
180                 info_add_row(0, "Rocket flight", "%6d", state.data.flight);
181
182                 info_add_row(0, "RSSI", "%6d    dBm", state.data.rssi);
183                 info_add_row(0, "Height", "%6.0f    m", state.height);
184                 info_add_row(0, "Max height", "%6.0f    m", state.max_height);
185                 info_add_row(0, "Acceleration", "%8.1f  m/s²", state.acceleration);
186                 info_add_row(0, "Max acceleration", "%8.1f  m/s²", state.max_acceleration);
187                 info_add_row(0, "Speed", "%8.1f  m/s", state.ascent ? state.speed : state.baro_speed);
188                 info_add_row(0, "Max Speed", "%8.1f  m/s", state.max_speed);
189                 info_add_row(0, "Temperature", "%9.2f °C", state.temperature);
190                 info_add_row(0, "Battery", "%9.2f V", state.battery);
191                 info_add_row(0, "Drogue", "%9.2f V", state.drogue_sense);
192                 info_add_row(0, "Main", "%9.2f V", state.main_sense);
193                 info_add_row(0, "Pad altitude", "%6.0f    m", state.ground_altitude);
194                 if (state.gps == null) {
195                         info_add_row(1, "GPS", "not available");
196                 } else {
197                         if (state.data.gps.locked)
198                                 info_add_row(1, "GPS", "   locked");
199                         else if (state.data.gps.connected)
200                                 info_add_row(1, "GPS", " unlocked");
201                         else
202                                 info_add_row(1, "GPS", "  missing");
203                         info_add_row(1, "Satellites", "%6d", state.data.gps.nsat);
204                         info_add_deg(1, "Latitude", state.gps.lat, 'N', 'S');
205                         info_add_deg(1, "Longitude", state.gps.lon, 'E', 'W');
206                         info_add_row(1, "GPS altitude", "%6d", state.gps.alt);
207                         info_add_row(1, "GPS height", "%6.0f", state.gps_height);
208
209                         /* The SkyTraq GPS doesn't report these values */
210                         if (false) {
211                                 info_add_row(1, "GPS ground speed", "%8.1f m/s %3d°",
212                                              state.gps.ground_speed,
213                                              state.gps.course);
214                                 info_add_row(1, "GPS climb rate", "%8.1f m/s",
215                                              state.gps.climb_rate);
216                                 info_add_row(1, "GPS error", "%6d m(h)%3d m(v)",
217                                              state.gps.h_error, state.gps.v_error);
218                         }
219                         info_add_row(1, "GPS hdop", "%8.1f", state.gps.hdop);
220
221                         if (state.npad > 0) {
222                                 if (state.from_pad != null) {
223                                         info_add_row(1, "Distance from pad", "%6.0f m", state.from_pad.distance);
224                                         info_add_row(1, "Direction from pad", "%6.0f°", state.from_pad.bearing);
225                                 } else {
226                                         info_add_row(1, "Distance from pad", "unknown");
227                                         info_add_row(1, "Direction from pad", "unknown");
228                                 }
229                                 info_add_deg(1, "Pad latitude", state.pad_lat, 'N', 'S');
230                                 info_add_deg(1, "Pad longitude", state.pad_lon, 'E', 'W');
231                                 info_add_row(1, "Pad GPS alt", "%6.0f m", state.pad_alt);
232                         }
233                         info_add_row(1, "GPS date", "%04d-%02d-%02d",
234                                        state.gps.year,
235                                        state.gps.month,
236                                        state.gps.day);
237                         info_add_row(1, "GPS time", "  %02d:%02d:%02d",
238                                        state.gps.hour,
239                                        state.gps.minute,
240                                        state.gps.second);
241                         int     nsat_vis = 0;
242                         int     c;
243
244                         if (state.gps.cc_gps_sat == null)
245                                 info_add_row(2, "Satellites Visible", "%4d", 0);
246                         else {
247                                 info_add_row(2, "Satellites Visible", "%4d", state.gps.cc_gps_sat.length);
248                                 for (c = 0; c < state.gps.cc_gps_sat.length; c++) {
249                                         info_add_row(2, "Satellite id,C/N0",
250                                                      "%4d, %4d",
251                                                      state.gps.cc_gps_sat[c].svid,
252                                                      state.gps.cc_gps_sat[c].c_n0);
253                                 }
254                         }
255                 }
256                 info_finish();
257         }
258
259         class IdleThread extends Thread {
260
261                 private AltosState state;
262                 int     reported_landing;
263
264                 public void report(boolean last) {
265                         if (state == null)
266                                 return;
267
268                         /* reset the landing count once we hear about a new flight */
269                         if (state.state < Altos.ao_flight_drogue)
270                                 reported_landing = 0;
271
272                         /* Shut up once the rocket is on the ground */
273                         if (reported_landing > 2) {
274                                 return;
275                         }
276
277                         /* If the rocket isn't on the pad, then report height */
278                         if (state.state > Altos.ao_flight_pad) {
279                                 voice.speak("%d meters", (int) (state.height + 0.5));
280                         } else {
281                                 reported_landing = 0;
282                         }
283
284                         /* If the rocket is coming down, check to see if it has landed;
285                          * either we've got a landed report or we haven't heard from it in
286                          * a long time
287                          */
288                         if (!state.ascent &&
289                             (last ||
290                              System.currentTimeMillis() - state.report_time >= 15000 ||
291                              state.state == Altos.ao_flight_landed))
292                         {
293                                 if (Math.abs(state.baro_speed) < 20 && state.height < 100)
294                                         voice.speak("rocket landed safely");
295                                 else
296                                         voice.speak("rocket may have crashed");
297                                 if (state.from_pad != null)
298                                         voice.speak("bearing %d degrees, range %d meters",
299                                                     (int) (state.from_pad.bearing + 0.5),
300                                                     (int) (state.from_pad.distance + 0.5));
301                                 ++reported_landing;
302                         }
303                 }
304
305                 public void run () {
306
307                         reported_landing = 0;
308                         state = null;
309                         try {
310                                 for (;;) {
311                                         Thread.sleep(10000);
312                                         report(false);
313                                 }
314                         } catch (InterruptedException ie) {
315                         }
316                 }
317
318                 public void notice(AltosState new_state) {
319                         state = new_state;
320                 }
321         }
322
323         private void tell(AltosState state, AltosState old_state) {
324                 if (old_state == null || old_state.state != state.state) {
325                         voice.speak(state.data.state());
326                         if ((old_state == null || old_state.state <= Altos.ao_flight_boost) &&
327                             state.state > Altos.ao_flight_boost) {
328                                 voice.speak("max speed: %d meters per second.",
329                                             (int) (state.max_speed + 0.5));
330                         } else if ((old_state == null || old_state.state < Altos.ao_flight_drogue) &&
331                                    state.state >= Altos.ao_flight_drogue) {
332                                 voice.speak("max height: %d meters.",
333                                             (int) (state.max_height + 0.5));
334                         }
335                 }
336                 if (old_state == null || old_state.gps_ready != state.gps_ready) {
337                         if (state.gps_ready)
338                                 voice.speak("GPS ready");
339                         else if (old_state != null)
340                                 voice.speak("GPS lost");
341                 }
342                 old_state = state;
343         }
344
345         class DisplayThread extends Thread {
346                 IdleThread      idle_thread;
347
348                 String          name;
349
350                 AltosRecord read() throws InterruptedException, ParseException { return null; }
351
352                 void close() { }
353
354                 void update(AltosState state) throws InterruptedException { }
355
356                 public void run() {
357                         String          line;
358                         AltosState      state = null;
359                         AltosState      old_state = null;
360
361                         idle_thread = new IdleThread();
362
363                         info_reset();
364                         info_finish();
365                         idle_thread.start();
366                         try {
367                                 for (;;) {
368                                         try {
369                                                 AltosRecord record = read();
370                                                 if (record == null)
371                                                         break;
372                                                 old_state = state;
373                                                 state = new AltosState(record, state);
374                                                 update(state);
375                                                 show(state);
376                                                 tell(state, old_state);
377                                                 idle_thread.notice(state);
378                                         } catch (ParseException pp) {
379                                                 System.out.printf("Parse error: %d \"%s\"\n", pp.getErrorOffset(), pp.getMessage());
380                                         }
381                                 }
382                         } catch (InterruptedException ee) {
383                         } finally {
384                                 close();
385                                 idle_thread.interrupt();
386                         }
387                 }
388
389                 public void report() {
390                         if (idle_thread != null)
391                                 idle_thread.report(true);
392                 }
393         }
394
395         class DeviceThread extends DisplayThread {
396                 AltosSerial     serial;
397                 LinkedBlockingQueue<String> telem;
398
399                 AltosRecord read() throws InterruptedException, ParseException {
400                         return new AltosTelemetry(telem.take());
401                 }
402
403                 void close() {
404                         serial.close();
405                         serial.remove_monitor(telem);
406                 }
407
408                 public DeviceThread(AltosSerial s) {
409                         serial = s;
410                         telem = new LinkedBlockingQueue<String>();
411                         serial.add_monitor(telem);
412                         name = "telemetry";
413                 }
414         }
415
416         private void ConnectToDevice() {
417                 AltosDevice     device = AltosDeviceDialog.show(AltosUI.this, AltosDevice.BaseStation);
418
419                 if (device != null) {
420                         try {
421                                 serial_line.open(device);
422                                 DeviceThread thread = new DeviceThread(serial_line);
423                                 serial_line.set_channel(AltosPreferences.channel());
424                                 serial_line.set_callsign(AltosPreferences.callsign());
425                                 run_display(thread);
426                         } catch (FileNotFoundException ee) {
427                                 JOptionPane.showMessageDialog(AltosUI.this,
428                                                               String.format("Cannot open device \"%s\"",
429                                                                             device.getPath()),
430                                                               "Cannot open target device",
431                                                               JOptionPane.ERROR_MESSAGE);
432                         } catch (IOException ee) {
433                                 JOptionPane.showMessageDialog(AltosUI.this,
434                                                               device.getPath(),
435                                                               "Unkonwn I/O error",
436                                                               JOptionPane.ERROR_MESSAGE);
437                         }
438                 }
439         }
440
441         void DisconnectFromDevice () {
442                 stop_display();
443         }
444
445         void ConfigureCallsign() {
446                 String  result;
447                 result = JOptionPane.showInputDialog(AltosUI.this,
448                                                      "Configure Callsign",
449                                                      AltosPreferences.callsign());
450                 if (result != null) {
451                         AltosPreferences.set_callsign(result);
452                         if (serial_line != null)
453                                 serial_line.set_callsign(result);
454                 }
455         }
456
457         void ConfigureTeleMetrum() {
458                 new AltosConfig(AltosUI.this);
459         }
460         /*
461          * Open an existing telemetry file and replay it in realtime
462          */
463
464         class ReplayThread extends DisplayThread {
465                 AltosReader     reader;
466                 String          name;
467
468                 public AltosRecord read() {
469                         try {
470                                 return reader.read();
471                         } catch (IOException ie) {
472                                 JOptionPane.showMessageDialog(AltosUI.this,
473                                                               name,
474                                                               "error reading",
475                                                               JOptionPane.ERROR_MESSAGE);
476                         } catch (ParseException pe) {
477                         }
478                         return null;
479                 }
480
481                 public void close () {
482                         report();
483                 }
484
485                 public ReplayThread(AltosReader in_reader, String in_name) {
486                         reader = in_reader;
487                 }
488                 void update(AltosState state) throws InterruptedException {
489                         /* Make it run in realtime after the rocket leaves the pad */
490                         if (state.state > Altos.ao_flight_pad)
491                                 Thread.sleep((int) (Math.min(state.time_change,10) * 1000));
492                 }
493         }
494
495         class ReplayTelemetryThread extends ReplayThread {
496                 ReplayTelemetryThread(FileInputStream in, String in_name) {
497                         super(new AltosTelemetryReader(in), in_name);
498                 }
499
500         }
501
502         class ReplayEepromThread extends ReplayThread {
503                 ReplayEepromThread(FileInputStream in, String in_name) {
504                         super(new AltosEepromReader(in), in_name);
505                 }
506         }
507
508         Thread          display_thread;
509
510         private void stop_display() {
511                 if (display_thread != null && display_thread.isAlive())
512                         display_thread.interrupt();
513                 display_thread = null;
514         }
515
516         private void run_display(Thread thread) {
517                 stop_display();
518                 display_thread = thread;
519                 display_thread.start();
520         }
521
522         /*
523          * Replay a flight from telemetry data
524          */
525         private void Replay() {
526                 JFileChooser    logfile_chooser = new JFileChooser();
527
528                 logfile_chooser.setDialogTitle("Select Flight Record File");
529                 logfile_chooser.setFileFilter(new FileNameExtensionFilter("Flight data file", "eeprom", "telem"));
530                 logfile_chooser.setCurrentDirectory(AltosPreferences.logdir());
531                 int returnVal = logfile_chooser.showOpenDialog(AltosUI.this);
532
533                 if (returnVal == JFileChooser.APPROVE_OPTION) {
534                         File file = logfile_chooser.getSelectedFile();
535                         if (file == null)
536                                 System.out.println("No file selected?");
537                         String  filename = file.getName();
538                         try {
539                                 FileInputStream replay = new FileInputStream(file);
540                                 DisplayThread   thread;
541                                 if (filename.endsWith("eeprom"))
542                                     thread = new ReplayEepromThread(replay, filename);
543                                 else
544                                     thread = new ReplayTelemetryThread(replay, filename);
545                                 run_display(thread);
546                         } catch (FileNotFoundException ee) {
547                                 JOptionPane.showMessageDialog(AltosUI.this,
548                                                               filename,
549                                                               "Cannot open telemetry file",
550                                                               JOptionPane.ERROR_MESSAGE);
551                         }
552                 }
553         }
554
555         /* Connect to TeleMetrum, either directly or through
556          * a TeleDongle over the packet link
557          */
558         private void SaveFlightData() {
559                 new AltosEepromDownload(AltosUI.this);
560         }
561
562         /* Create the AltosUI menus
563          */
564         private void createMenu() {
565                 JMenuBar menubar = new JMenuBar();
566                 JMenu menu;
567                 JMenuItem item;
568                 JRadioButtonMenuItem radioitem;
569
570                 // File menu
571                 {
572                         menu = new JMenu("File");
573                         menu.setMnemonic(KeyEvent.VK_F);
574                         menubar.add(menu);
575
576                         item = new JMenuItem("Replay File",KeyEvent.VK_R);
577                         item.addActionListener(new ActionListener() {
578                                         public void actionPerformed(ActionEvent e) {
579                                                 Replay();
580                                         }
581                                 });
582                         menu.add(item);
583
584                         item = new JMenuItem("Save Flight Data",KeyEvent.VK_S);
585                         item.addActionListener(new ActionListener() {
586                                         public void actionPerformed(ActionEvent e) {
587                                                 SaveFlightData();
588                                         }
589                                 });
590                         menu.add(item);
591
592                         item = new JMenuItem("Quit",KeyEvent.VK_Q);
593                         item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q,
594                                                                    ActionEvent.CTRL_MASK));
595                         item.addActionListener(new ActionListener() {
596                                         public void actionPerformed(ActionEvent e) {
597                                                 System.exit(0);
598                                         }
599                                 });
600                         menu.add(item);
601                 }
602
603                 // Device menu
604                 {
605                         menu = new JMenu("Device");
606                         menu.setMnemonic(KeyEvent.VK_D);
607                         menubar.add(menu);
608
609                         item = new JMenuItem("Connect to Device",KeyEvent.VK_C);
610                         item.addActionListener(new ActionListener() {
611                                         public void actionPerformed(ActionEvent e) {
612                                                 ConnectToDevice();
613                                         }
614                                 });
615                         menu.add(item);
616
617                         item = new JMenuItem("Disconnect from Device",KeyEvent.VK_D);
618                         item.addActionListener(new ActionListener() {
619                                         public void actionPerformed(ActionEvent e) {
620                                                 DisconnectFromDevice();
621                                         }
622                                 });
623                         menu.add(item);
624
625                         menu.addSeparator();
626
627                         item = new JMenuItem("Set Callsign",KeyEvent.VK_S);
628                         item.addActionListener(new ActionListener() {
629                                         public void actionPerformed(ActionEvent e) {
630                                                 ConfigureCallsign();
631                                         }
632                                 });
633
634                         menu.add(item);
635
636                         item = new JMenuItem("Configure TeleMetrum device",KeyEvent.VK_T);
637                         item.addActionListener(new ActionListener() {
638                                         public void actionPerformed(ActionEvent e) {
639                                                 ConfigureTeleMetrum();
640                                         }
641                                 });
642
643                         menu.add(item);
644                 }
645                 // Log menu
646                 {
647                         menu = new JMenu("Log");
648                         menu.setMnemonic(KeyEvent.VK_L);
649                         menubar.add(menu);
650
651                         item = new JMenuItem("New Log",KeyEvent.VK_N);
652                         item.addActionListener(new ActionListener() {
653                                         public void actionPerformed(ActionEvent e) {
654                                         }
655                                 });
656                         menu.add(item);
657
658                         item = new JMenuItem("Configure Log",KeyEvent.VK_C);
659                         item.addActionListener(new ActionListener() {
660                                         public void actionPerformed(ActionEvent e) {
661                                                 AltosPreferences.ConfigureLog();
662                                         }
663                                 });
664                         menu.add(item);
665                 }
666                 // Voice menu
667                 {
668                         menu = new JMenu("Voice", true);
669                         menu.setMnemonic(KeyEvent.VK_V);
670                         menubar.add(menu);
671
672                         radioitem = new JRadioButtonMenuItem("Enable Voice", AltosPreferences.voice());
673                         radioitem.addActionListener(new ActionListener() {
674                                         public void actionPerformed(ActionEvent e) {
675                                                 JRadioButtonMenuItem item = (JRadioButtonMenuItem) e.getSource();
676                                                 boolean enabled = item.isSelected();
677                                                 AltosPreferences.set_voice(enabled);
678                                                 if (enabled)
679                                                         voice.speak_always("Enable voice.");
680                                                 else
681                                                         voice.speak_always("Disable voice.");
682                                         }
683                                 });
684                         menu.add(radioitem);
685                         item = new JMenuItem("Test Voice",KeyEvent.VK_T);
686                         item.addActionListener(new ActionListener() {
687                                         public void actionPerformed(ActionEvent e) {
688                                                 voice.speak("That's one small step for man; one giant leap for mankind.");
689                                         }
690                                 });
691                         menu.add(item);
692                 }
693
694                 // Channel menu
695                 {
696                         menu = new AltosChannelMenu(AltosPreferences.channel());
697                         menu.addActionListener(new ActionListener() {
698                                                 public void actionPerformed(ActionEvent e) {
699                                                         int new_channel = Integer.parseInt(e.getActionCommand());
700                                                         AltosPreferences.set_channel(new_channel);
701                                                         serial_line.set_channel(new_channel);
702                                                 }
703                                 });
704                         menu.setMnemonic(KeyEvent.VK_C);
705                         menubar.add(menu);
706                 }
707
708                 this.setJMenuBar(menubar);
709
710         }
711         public static void main(final String[] args) {
712                 AltosUI altosui = new AltosUI();
713                 altosui.setVisible(true);
714         }
715 }