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