create teststand/ starting with a clone of altosui/
authorBdale Garbee <bdale@gag.com>
Sun, 14 Jan 2018 18:43:48 +0000 (15:43 -0300)
committerBdale Garbee <bdale@gag.com>
Sun, 14 Jan 2018 18:43:48 +0000 (15:43 -0300)
54 files changed:
Makefile.am
configure.ac
teststand/Altos.java [new file with mode: 0644]
teststand/AltosAscent.java [new file with mode: 0644]
teststand/AltosChannelMenu.java [new file with mode: 0644]
teststand/AltosCompanionInfo.java [new file with mode: 0644]
teststand/AltosConfigFC.java [new file with mode: 0644]
teststand/AltosConfigFCUI.java [new file with mode: 0644]
teststand/AltosConfigPyroUI.java [new file with mode: 0644]
teststand/AltosConfigTD.java [new file with mode: 0644]
teststand/AltosConfigTDUI.java [new file with mode: 0644]
teststand/AltosConfigureUI.java [new file with mode: 0644]
teststand/AltosDescent.java [new file with mode: 0644]
teststand/AltosDevice.java [new file with mode: 0644]
teststand/AltosFlightStatus.java [new file with mode: 0644]
teststand/AltosFlightStatusTableModel.java [new file with mode: 0644]
teststand/AltosFlightStatusUpdate.java [new file with mode: 0644]
teststand/AltosFlightUI.java [new file with mode: 0644]
teststand/AltosGraphUI.java [new file with mode: 0644]
teststand/AltosIdleMonitorUI.java [new file with mode: 0644]
teststand/AltosIgniteUI.java [new file with mode: 0644]
teststand/AltosIgnitor.java [new file with mode: 0644]
teststand/AltosLanded.java [new file with mode: 0644]
teststand/AltosLaunch.java [new file with mode: 0644]
teststand/AltosLaunchUI.java [new file with mode: 0644]
teststand/AltosLib.jar [new symlink]
teststand/AltosPad.java [new file with mode: 0644]
teststand/AltosVersion.java [new file with mode: 0644]
teststand/Info.plist.in [new file with mode: 0644]
teststand/Instdrv/NSIS/Contrib/InstDrv/Example.nsi [new file with mode: 0644]
teststand/Instdrv/NSIS/Contrib/InstDrv/InstDrv-Test.exe [new file with mode: 0644]
teststand/Instdrv/NSIS/Contrib/InstDrv/InstDrv.c [new file with mode: 0644]
teststand/Instdrv/NSIS/Contrib/InstDrv/InstDrv.dsp [new file with mode: 0644]
teststand/Instdrv/NSIS/Contrib/InstDrv/InstDrv.dsw [new file with mode: 0644]
teststand/Instdrv/NSIS/Contrib/InstDrv/Readme.txt [new file with mode: 0644]
teststand/Instdrv/NSIS/Contrib/InstDrv/ircomm2k.inf [new file with mode: 0644]
teststand/Instdrv/NSIS/Contrib/InstDrv/ircomm2k.sys [new file with mode: 0644]
teststand/Instdrv/NSIS/Includes/java.nsh [new file with mode: 0644]
teststand/Instdrv/NSIS/Includes/refresh-sh.nsh [new file with mode: 0644]
teststand/Instdrv/NSIS/Plugins/InstDrv.dll [new file with mode: 0644]
teststand/Makefile-standalone [new file with mode: 0644]
teststand/Makefile.am [new file with mode: 0644]
teststand/ReadMe-Mac.rtf [new file with mode: 0644]
teststand/TestStand.app/Contents/MacOS/JavaApplicationStub [new file with mode: 0755]
teststand/TestStand.app/Contents/PkgInfo [new file with mode: 0644]
teststand/TestStand.java [new file with mode: 0644]
teststand/altosui-fat [new file with mode: 0755]
teststand/altosuilib.jar [new symlink]
teststand/altusmetrum-teststand.desktop.in [new file with mode: 0644]
teststand/altusmetrum.jpg [new file with mode: 0644]
teststand/linux-install.sh [new file with mode: 0644]
teststand/mdwn.tmpl [new file with mode: 0644]
teststand/teststand-windows.nsi.in [new file with mode: 0644]
teststand/teststand.1 [new file with mode: 0644]

index 4f47417eccf00c9bc5846d97a4847339ab366664..3623043793d84839f1d11cc638e653fc75edaced 100644 (file)
@@ -1,4 +1,4 @@
-SUBDIRS=ao-tools src doc icon altoslib libaltos altosuilib altosui micropeak ao-utils altosdroid telegps
+SUBDIRS=ao-tools src doc icon altoslib libaltos altosuilib altosui micropeak ao-utils altosdroid telegps teststand
 
 EXTRA_DIST = ChangeLog
 
@@ -23,6 +23,7 @@ fat-install: fat
        cd altosui && $(MAKE) fat-install
        cd telegps && $(MAKE) fat-install
        cd micropeak && $(MAKE) fat-install
+       cd teststand && $(MAKE) fat-install
 endif
 
 fat:
@@ -35,19 +36,23 @@ fat:
        cd altosui && $(MAKE) fat
        cd micropeak && $(MAKE) fat
        cd telegps && $(MAKE) fat
+       cd teststand && $(MAKE) fat
 
 fat_linux = \
        altosui/Altos-Linux-$(VERSION).sh altosui/Altos-Linux-$(VERSION).tar.bz2 \
+       teststand/TestStand-Linux-$(VERSION).sh teststand/TestStand-Linux-$(VERSION).tar.bz2 \
        telegps/TeleGPS-Linux-$(VERSION).sh telegps/TeleGPS-Linux-$(VERSION).tar.bz2 \
        micropeak/MicroPeak-Linux-$(VERSION).sh micropeak/MicroPeak-Linux-$(VERSION).tar.bz2
 
 fat_mac = \
        altosui/Altos-Mac-$(VERSION).dmg \
+       teststand/TestStand-Mac-$(VERSION).dmg \
        telegps/TeleGPS-Mac-$(VERSION).dmg \
        micropeak/MicroPeak-Mac-$(VERSION).dmg
 
 fat_windows = \
        altosui/Altos-Windows-$(VERSION_DASH).exe \
+       teststand/TestStand-Windows-$(VERSION_DASH).exe \
        telegps/TeleGPS-Windows-$(VERSION_DASH).exe \
        micropeak/MicroPeak-Windows-$(VERSION_DASH).exe
 
index 7f27dfadc70548ab9d395e1fdb7b52a9052e38d2..7c72567874c80a689ab739e3ca25b9c85f2bd1ba 100644 (file)
@@ -526,6 +526,9 @@ altosuilib/Makefile
 altosui/Makefile
 altosui/Info.plist
 altosui/altos-windows.nsi
+teststand/Makefile
+teststand/Info.plist
+teststand/teststand-windows.nsi
 libaltos/Makefile
 micropeak/Makefile
 micropeak/Info.plist
diff --git a/teststand/Altos.java b/teststand/Altos.java
new file mode 100644 (file)
index 0000000..f6a0306
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+import java.awt.*;
+import libaltosJNI.*;
+
+import org.altusmetrum.altoslib_12.*;
+import org.altusmetrum.altosuilib_12.*;
+
+public class Altos extends AltosUILib {
+
+}
diff --git a/teststand/AltosAscent.java b/teststand/AltosAscent.java
new file mode 100644 (file)
index 0000000..dfc5927
--- /dev/null
@@ -0,0 +1,195 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+import java.util.*;
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import org.altusmetrum.altoslib_12.*;
+import org.altusmetrum.altosuilib_12.*;
+
+public class AltosAscent extends AltosUIFlightTab {
+       JLabel  cur, max;
+
+       class Height extends AltosUIUnitsIndicator {
+
+               public double value(AltosState state, int i) {
+                       if (i == 0)
+                               return state.height();
+                       else
+                               return state.max_height();
+               }
+
+               public Height(Container container, int y) {
+                       super(container, y, AltosConvert.height, "Height", 2, false, 1);
+               }
+       }
+
+       class Speed extends AltosUIUnitsIndicator {
+               public double value(AltosState state, int i) {
+                       if (i == 0)
+                               return state.speed();
+                       else
+                               return state.max_speed();
+               }
+
+               public Speed(Container container, int y) {
+                       super(container, y, AltosConvert.speed, "Speed", 2, false, 1);
+               }
+       }
+
+       class Accel extends AltosUIUnitsIndicator {
+               public boolean hide(double v) { return v == AltosLib.MISSING; }
+
+               public double value(AltosState state, int i) {
+                       if (i == 0)
+                               return state.acceleration();
+                       else
+                               return state.max_acceleration();
+               }
+
+               public Accel(Container container, int y) {
+                       super(container, y, AltosConvert.accel, "Acceleration", 2, false, 1);
+               }
+       }
+
+       class Orient extends AltosUIUnitsIndicator {
+
+               public boolean hide(double v) { return v == AltosLib.MISSING; }
+
+               public double value(AltosState state, int i) {
+                       if (i == 0)
+                               return state.orient();
+                       else
+                               return state.max_orient();
+               }
+
+               public Orient(Container container, int y) {
+                       super(container, y, AltosConvert.orient, "Tilt Angle", 2, false, 1);
+               }
+
+       }
+
+       class Apogee extends AltosUIUnitsIndicator {
+
+               public double value(AltosState state, int i) {
+                       return state.apogee_voltage;
+               }
+
+               public boolean good(double v) { return v >= AltosLib.ao_igniter_good; }
+               public boolean hide(double v) { return v == AltosLib.MISSING; }
+
+               public Apogee (Container container, int y) {
+                       super(container, y, AltosConvert.voltage, "Apogee Igniter Voltage", 1, true, 2);
+               }
+       }
+
+       class Main extends AltosUIUnitsIndicator {
+               public double value(AltosState state, int i) {
+                       return state.main_voltage;
+               }
+
+               public boolean good(double v) { return v >= AltosLib.ao_igniter_good; }
+               public boolean hide(double v) { return v == AltosLib.MISSING; }
+
+               public Main (Container container, int y) {
+                       super(container, y, AltosConvert.voltage, "Main Igniter Voltage", 1, true, 2);
+               }
+       }
+
+       class Lat extends AltosUIUnitsIndicator {
+
+               public boolean hide(AltosState state, int i) {
+                       return state.gps == null || !state.gps.connected;
+               }
+
+               public double value(AltosState state, int i) {
+                       if (state.gps == null)
+                               return AltosLib.MISSING;
+                       if (!state.gps.connected)
+                               return AltosLib.MISSING;
+                       return state.gps.lat;
+               }
+
+               Lat (Container container, int y) {
+                       super (container, y, AltosConvert.latitude, "Latitude", 1, false, 2);
+               }
+       }
+
+       class Lon extends AltosUIUnitsIndicator {
+
+               public boolean hide(AltosState state, int i) {
+                       return state.gps == null || !state.gps.connected;
+               }
+
+               public double value(AltosState state, int i) {
+                       if (state.gps == null)
+                               return AltosLib.MISSING;
+                       if (!state.gps.connected)
+                               return AltosLib.MISSING;
+                       return state.gps.lon;
+               }
+
+               Lon (Container container, int y) {
+                       super (container, y, AltosConvert.longitude, "Longitude", 1, false, 2);
+               }
+       }
+
+       public void font_size_changed(int font_size) {
+               super.font_size_changed(font_size);
+               cur.setFont(AltosUILib.label_font);
+               max.setFont(AltosUILib.label_font);
+       }
+
+       public void labels(GridBagLayout layout, int y) {
+               GridBagConstraints      c;
+
+               cur = new JLabel("Current");
+               cur.setFont(AltosUILib.label_font);
+               c = new GridBagConstraints();
+               c.gridx = 2; c.gridy = y;
+               c.insets = new Insets(Altos.tab_elt_pad, Altos.tab_elt_pad, Altos.tab_elt_pad, Altos.tab_elt_pad);
+               layout.setConstraints(cur, c);
+               add(cur);
+
+               max = new JLabel("Maximum");
+               max.setFont(AltosUILib.label_font);
+               c.gridx = 3; c.gridy = y;
+               layout.setConstraints(max, c);
+               add(max);
+       }
+
+       public String getName() {
+               return "Ascent";
+       }
+
+       public AltosAscent() {
+               int y = 0;
+               labels(layout, y++);
+               add(new Height(this, y++));
+               add(new Speed(this, y++));
+               add(new Accel(this, y++));
+               add(new Orient(this, y++));
+               add(new Lat(this, y++));
+               add(new Lon(this, y++));
+               add(new Apogee(this, y++));
+               add(new Main(this, y++));
+       }
+}
diff --git a/teststand/AltosChannelMenu.java b/teststand/AltosChannelMenu.java
new file mode 100644 (file)
index 0000000..8eb8ea4
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+import java.awt.event.*;
+import javax.swing.*;
+
+public class AltosChannelMenu extends JComboBox<String> implements ActionListener {
+       int                             channel;
+
+       public AltosChannelMenu(int current_channel) {
+
+               channel = current_channel;
+
+               for (int c = 0; c <= 9; c++)
+                       addItem(String.format("Channel %1d (%7.3fMHz)", c, 434.550 + c * 0.1));
+               setSelectedIndex(channel);
+               setMaximumRowCount(10);
+       }
+
+}
diff --git a/teststand/AltosCompanionInfo.java b/teststand/AltosCompanionInfo.java
new file mode 100644 (file)
index 0000000..10fbcd2
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+import java.awt.*;
+import javax.swing.*;
+import org.altusmetrum.altoslib_12.*;
+import org.altusmetrum.altosuilib_12.*;
+
+public class AltosCompanionInfo extends JTable implements AltosFlightDisplay {
+       private AltosFlightInfoTableModel model;
+
+       static final int info_columns = 2;
+       static final int info_rows = 17;
+
+       int desired_row_height() {
+               FontMetrics     infoValueMetrics = getFontMetrics(Altos.table_value_font);
+               return (infoValueMetrics.getHeight() + infoValueMetrics.getLeading()) * 18 / 10;
+       }
+
+       public void font_size_changed(int font_size) {
+               setFont(Altos.table_value_font);
+               setRowHeight(desired_row_height());
+               doLayout();
+       }
+
+       public void units_changed(boolean imperial_units) {
+       }
+
+       public AltosCompanionInfo() {
+               super(new AltosFlightInfoTableModel(info_rows, info_columns));
+               model = (AltosFlightInfoTableModel) getModel();
+               setAutoResizeMode(AUTO_RESIZE_ALL_COLUMNS);
+               setShowGrid(true);
+               font_size_changed(AltosUIPreferences.font_size());
+       }
+
+       public Dimension getPreferredScrollableViewportSize() {
+               return getPreferredSize();
+       }
+
+       public void reset() {
+               model.reset();
+       }
+
+       void info_add_row(int col, String name, String value) {
+               model.addRow(col, name, value);
+       }
+
+       void info_add_row(int col, String name, String format, Object... parameters) {
+               info_add_row (col, name, String.format(format, parameters));
+       }
+
+       void info_finish() {
+               model.finish();
+       }
+
+       public void clear() {
+               model.clear();
+       }
+
+       AltosCompanion  companion;
+
+       public String board_name() {
+               if (companion == null)
+                       return "None";
+               switch (companion.board_id) {
+               case AltosCompanion.board_id_telescience:
+                       return "TeleScience";
+               default:
+                       return String.format("%02x\n", companion.board_id);
+               }
+       }
+
+       public String getName() { return "Companion"; }
+
+       public void show(AltosState state, AltosListenerState listener_state) {
+               if (state == null)
+                       return;
+               if (state.companion != null)
+                       companion = state.companion;
+               reset();
+               info_add_row(0, "Companion board", "%s", board_name());
+               if (companion != null) {
+                       info_add_row(0, "Last Data", "%5d", companion.tick);
+                       info_add_row(0, "Update period", "%5.2f s",
+                                    companion.update_period / 100.0);
+                       info_add_row(0, "Channels", "%3d", companion.channels);
+
+                       for (int i = 0; i < companion.channels; i++)
+                               info_add_row(1, String.format("Channel %2d", i),
+                                            "%6d", companion.companion_data[i]);
+               }
+               info_finish();
+       }
+}
diff --git a/teststand/AltosConfigFC.java b/teststand/AltosConfigFC.java
new file mode 100644 (file)
index 0000000..c81f7db
--- /dev/null
@@ -0,0 +1,315 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+import java.awt.event.*;
+import javax.swing.*;
+import java.io.*;
+import java.util.concurrent.*;
+import java.text.*;
+import org.altusmetrum.altoslib_12.*;
+import org.altusmetrum.altosuilib_12.*;
+
+public class AltosConfigFC implements ActionListener {
+
+       class int_ref {
+               int     value;
+
+               public int get() {
+                       return value;
+               }
+               public void set(int i) {
+                       value = i;
+               }
+               public int_ref(int i) {
+                       value = i;
+               }
+       }
+
+       class string_ref {
+               String  value;
+
+               public String get() {
+                       return value;
+               }
+               public void set(String i) {
+                       value = i;
+               }
+               public string_ref(String i) {
+                       value = i;
+               }
+       }
+
+       JFrame          owner;
+       AltosDevice     device;
+       AltosSerial     serial_line;
+       boolean         remote;
+
+       AltosConfigData data;
+       AltosConfigFCUI config_ui;
+       boolean         serial_started;
+       boolean         made_visible;
+
+       void start_serial() throws InterruptedException, TimeoutException {
+               serial_started = true;
+               if (remote)
+                       serial_line.start_remote();
+       }
+
+       void stop_serial() throws InterruptedException {
+               if (!serial_started)
+                       return;
+               serial_started = false;
+               if (remote)
+                       serial_line.stop_remote();
+       }
+
+       void update_ui() {
+               data.set_values(config_ui);
+               config_ui.set_clean();
+               if (!made_visible) {
+                       made_visible = true;
+                       config_ui.make_visible();
+               }
+       }
+
+       int     pyro;
+
+       final static int        serial_mode_read = 0;
+       final static int        serial_mode_save = 1;
+       final static int        serial_mode_reboot = 2;
+
+       class SerialData implements Runnable {
+               AltosConfigFC   config;
+               int             serial_mode;
+
+               void callback(String in_cmd) {
+                       final String cmd = in_cmd;
+                       Runnable r = new Runnable() {
+                                       public void run() {
+                                               if (cmd.equals("abort")) {
+                                                       abort();
+                                               } else if (cmd.equals("all finished")) {
+                                                       if (serial_line != null)
+                                                               update_ui();
+                                               }
+                                       }
+                               };
+                       SwingUtilities.invokeLater(r);
+               }
+
+               void get_data() {
+                       data = null;
+                       try {
+                               start_serial();
+                               data = new AltosConfigData(config.serial_line);
+                       } catch (InterruptedException ie) {
+                       } catch (TimeoutException te) {
+                               try {
+                                       stop_serial();
+                                       callback("abort");
+                               } catch (InterruptedException ie) {
+                               }
+                       } finally {
+                               try {
+                                       stop_serial();
+                               } catch (InterruptedException ie) {
+                               }
+                       }
+                       callback("all finished");
+               }
+
+               void save_data() {
+                       try {
+                               start_serial();
+                               data.save(serial_line, remote);
+                               if (remote)
+                                       AltosUIPreferences.set_frequency(device.getSerial(),
+                                                                        data.frequency());
+                       } catch (InterruptedException ie) {
+                       } catch (TimeoutException te) {
+                       } finally {
+                               try {
+                                       stop_serial();
+                               } catch (InterruptedException ie) {
+                               }
+                       }
+               }
+
+               void reboot() {
+                       try {
+                               start_serial();
+                               serial_line.printf("r eboot\n");
+                               serial_line.flush_output();
+                       } catch (InterruptedException ie) {
+                       } catch (TimeoutException te) {
+                       } finally {
+                               try {
+                                       stop_serial();
+                                       serial_line.close();
+                               } catch (InterruptedException ie) {
+                               }
+                       }
+               }
+
+               public void run () {
+                       switch (serial_mode) {
+                       case serial_mode_save:
+                               save_data();
+                               /* fall through ... */
+                       case serial_mode_read:
+                               get_data();
+                               break;
+                       case serial_mode_reboot:
+                               reboot();
+                               break;
+                       }
+               }
+
+               public SerialData(AltosConfigFC in_config, int in_serial_mode) {
+                       config = in_config;
+                       serial_mode = in_serial_mode;
+               }
+       }
+
+       void run_serial_thread(int serial_mode) {
+               SerialData      sd = new SerialData(this, serial_mode);
+               Thread          st = new Thread(sd);
+               st.start();
+       }
+
+       void init_ui () throws InterruptedException, TimeoutException {
+               config_ui = new AltosConfigFCUI(owner, remote);
+               config_ui.addActionListener(this);
+               serial_line.set_frame(owner);
+               set_ui();
+       }
+
+       void abort() {
+               if (serial_line != null) {
+                       serial_line.close();
+                       serial_line = null;
+               }
+               JOptionPane.showMessageDialog(owner,
+                                             String.format("Connection to \"%s\" failed",
+                                                           device.toShortString()),
+                                             "Connection Failed",
+                                             JOptionPane.ERROR_MESSAGE);
+               config_ui.setVisible(false);
+       }
+
+       void set_ui() throws InterruptedException, TimeoutException {
+               if (serial_line != null)
+                       run_serial_thread(serial_mode_read);
+               else
+                       update_ui();
+       }
+
+       double frequency() {
+               return AltosConvert.radio_to_frequency(data.radio_frequency,
+                                                      data.radio_setting,
+                                                      data.radio_calibration,
+                                                      data.radio_channel);
+       }
+
+       void save_data() {
+
+               try {
+                       /* bounds check stuff */
+                       if (config_ui.flight_log_max() > data.log_space() / 1024) {
+                               JOptionPane.showMessageDialog(owner,
+                                                             String.format("Requested flight log, %dk, is larger than the available space, %dk.\n",
+                                                                           config_ui.flight_log_max(),
+                                                                           data.log_space() / 1024),
+                                                             "Maximum Flight Log Too Large",
+                                                             JOptionPane.ERROR_MESSAGE);
+                               return;
+                       }
+
+                       /* Pull data out of the UI and stuff back into our local data record */
+
+                       data.get_values(config_ui);
+                       run_serial_thread(serial_mode_save);
+               } catch (AltosConfigDataException ae) {
+                       JOptionPane.showMessageDialog(owner,
+                                                     ae.getMessage(),
+                                                     "Configuration Data Error",
+                                                     JOptionPane.ERROR_MESSAGE);
+               }
+       }
+
+       public void actionPerformed(ActionEvent e) {
+               String  cmd = e.getActionCommand();
+               try {
+                       if (cmd.equals("Save")) {
+                               save_data();
+                       } else if (cmd.equals("Reset")) {
+                               set_ui();
+                       } else if (cmd.equals("Reboot")) {
+                               if (serial_line != null)
+                                       run_serial_thread(serial_mode_reboot);
+                       } else if (cmd.equals("Close")) {
+                               if (serial_line != null)
+                                       serial_line.close();
+                       }
+                       else if (cmd.equals("Accel")) {
+                               if (data.pad_orientation != AltosLib.MISSING) {
+                                       AltosUIAccelCal accel_ui = new AltosUIAccelCal(owner, serial_line, config_ui);
+                                       if (accel_ui != null)
+                                               accel_ui.doit();
+                               }
+                       }
+               } catch (InterruptedException ie) {
+                       abort();
+               } catch (TimeoutException te) {
+                       abort();
+               }
+       }
+
+       public AltosConfigFC(JFrame given_owner) {
+               owner = given_owner;
+
+               device = AltosDeviceUIDialog.show(owner, Altos.product_any);
+               if (device != null) {
+                       try {
+                               serial_line = new AltosSerial(device);
+                               try {
+                                       if (device.matchProduct(Altos.product_basestation))
+                                               remote = true;
+                                       init_ui();
+                               } catch (InterruptedException ie) {
+                                       abort();
+                               } catch (TimeoutException te) {
+                                       abort();
+                               }
+                       } catch (FileNotFoundException ee) {
+                               JOptionPane.showMessageDialog(owner,
+                                                             ee.getMessage(),
+                                                             "Cannot open target device",
+                                                             JOptionPane.ERROR_MESSAGE);
+                       } catch (AltosSerialInUseException si) {
+                               JOptionPane.showMessageDialog(owner,
+                                                             String.format("Device \"%s\" already in use",
+                                                                           device.toShortString()),
+                                                             "Device in use",
+                                                             JOptionPane.ERROR_MESSAGE);
+                       }
+               }
+       }
+}
diff --git a/teststand/AltosConfigFCUI.java b/teststand/AltosConfigFCUI.java
new file mode 100644 (file)
index 0000000..8fcf9ba
--- /dev/null
@@ -0,0 +1,1476 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import java.text.*;
+import org.altusmetrum.altoslib_12.*;
+import org.altusmetrum.altosuilib_12.*;
+
+public class AltosConfigFCUI
+       extends AltosUIDialog
+       implements ActionListener, ItemListener, DocumentListener, AltosConfigValues, AltosUnitsListener
+{
+
+       Container               pane;
+       JLabel                  product_label;
+       JLabel                  version_label;
+       JLabel                  serial_label;
+       JLabel                  main_deploy_label;
+       JLabel                  apogee_delay_label;
+       JLabel                  apogee_lockout_label;
+       JLabel                  frequency_label;
+       JLabel                  radio_calibration_label;
+       JLabel                  radio_frequency_label;
+       JLabel                  radio_enable_label;
+       JLabel                  rate_label;
+       JLabel                  aprs_interval_label;
+       JLabel                  aprs_ssid_label;
+       JLabel                  aprs_format_label;
+       JLabel                  flight_log_max_label;
+       JLabel                  ignite_mode_label;
+       JLabel                  pad_orientation_label;
+       JLabel                  accel_plus_label;
+       JLabel                  accel_minus_label;
+       JLabel                  callsign_label;
+       JLabel                  beep_label;
+       JLabel                  tracker_motion_label;
+       JLabel                  tracker_interval_label;
+
+       public boolean          dirty;
+
+       JFrame                  owner;
+       JLabel                  product_value;
+       JLabel                  version_value;
+       JLabel                  serial_value;
+       JComboBox<String>       main_deploy_value;
+       JComboBox<String>       apogee_delay_value;
+       JComboBox<String>       apogee_lockout_value;
+       AltosUIFreqList         radio_frequency_value;
+       JLabel                  radio_calibration_value;
+       JRadioButton            radio_enable_value;
+       AltosUIRateList         rate_value;
+       JComboBox<String>       aprs_interval_value;
+       JComboBox<Integer>      aprs_ssid_value;
+       JComboBox<String>       aprs_format_value;
+       JComboBox<String>       flight_log_max_value;
+       JComboBox<String>       ignite_mode_value;
+       JComboBox<String>       pad_orientation_value;
+       JTextField              accel_plus_value;
+       JTextField              accel_minus_value;
+       JTextField              callsign_value;
+       JComboBox<String>       beep_value;
+       JComboBox<String>       tracker_motion_value;
+       JComboBox<String>       tracker_interval_value;
+
+       JButton                 pyro;
+       JButton                 accel_cal;
+
+       JButton                 save;
+       JButton                 reset;
+       JButton                 reboot;
+       JButton                 close;
+
+       AltosPyro[]             pyros;
+       double                  pyro_firing_time;
+
+       ActionListener          listener;
+
+       static String[]         main_deploy_values_m = {
+               "100", "150", "200", "250", "300", "350",
+               "400", "450", "500"
+       };
+
+       static String[]         main_deploy_values_ft = {
+               "250", "500", "750", "1000", "1250", "1500",
+               "1750", "2000"
+       };
+
+       static String[]         apogee_delay_values = {
+               "0", "1", "2", "3", "4", "5"
+       };
+
+       static String[]         apogee_lockout_values = {
+               "0", "5", "10", "15", "20"
+       };
+
+       static String[]         ignite_mode_values = {
+               "Dual Deploy",
+               "Redundant Apogee",
+               "Redundant Main",
+       };
+
+       static String[]         aprs_interval_values = {
+               "Disabled",
+               "2",
+               "5",
+               "10"
+       };
+
+       static Integer[]        aprs_ssid_values = {
+               0, 1, 2 ,3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
+       };
+
+       static String[]         beep_values = {
+               "3750",
+               "4000",
+               "4250",
+       };
+
+       static String[]         pad_orientation_values = {
+               "Antenna Up",
+               "Antenna Down",
+       };
+
+       static String[]         tracker_motion_values_m = {
+               "2",
+               "5",
+               "10",
+               "25",
+       };
+
+       static String[]         tracker_motion_values_ft = {
+               "5",
+               "20",
+               "50",
+               "100"
+       };
+
+       static String[]         tracker_interval_values = {
+               "1",
+               "2",
+               "5",
+               "10"
+       };
+
+       /* A window listener to catch closing events and tell the config code */
+       class ConfigListener extends WindowAdapter {
+               AltosConfigFCUI ui;
+
+               public ConfigListener(AltosConfigFCUI this_ui) {
+                       ui = this_ui;
+               }
+
+               public void windowClosing(WindowEvent e) {
+                       ui.actionPerformed(new ActionEvent(e.getSource(),
+                                                          ActionEvent.ACTION_PERFORMED,
+                                                          "Close"));
+               }
+       }
+
+       boolean is_telemini_v1() {
+               String  product = product_value.getText();
+               return product != null && product.startsWith("TeleMini-v1");
+       }
+
+       boolean is_telemini() {
+               String  product = product_value.getText();
+               return product != null && product.startsWith("TeleMini");
+       }
+
+       boolean is_easymini() {
+               String  product = product_value.getText();
+               return product != null && product.startsWith("EasyMini");
+       }
+
+       boolean is_telemetrum() {
+               String  product = product_value.getText();
+               return product != null && product.startsWith("TeleMetrum");
+       }
+
+       void set_radio_enable_tool_tip() {
+               if (radio_enable_value.isVisible())
+                       radio_enable_value.setToolTipText("Enable/Disable telemetry and RDF transmissions");
+               else
+                       radio_enable_value.setToolTipText("Firmware version does not support disabling radio");
+       }
+
+       void set_rate_tool_tip() {
+               if (rate_value.isVisible())
+                       rate_value.setToolTipText("Select telemetry baud rate");
+               else
+                       rate_value.setToolTipText("Firmware version does not support variable telemetry rates");
+       }
+
+       void set_aprs_interval_tool_tip() {
+               if (aprs_interval_value.isVisible())
+                       aprs_interval_value.setToolTipText("Enable APRS and set the interval between APRS reports");
+               else
+                       aprs_interval_value.setToolTipText("Hardware doesn't support APRS");
+       }
+
+       void set_aprs_ssid_tool_tip() {
+               if (aprs_ssid_value.isVisible())
+                       aprs_ssid_value.setToolTipText("Set the APRS SSID (secondary station identifier)");
+               else if (aprs_ssid_value.isVisible())
+                       aprs_ssid_value.setToolTipText("Software version doesn't support setting the APRS SSID");
+               else
+                       aprs_ssid_value.setToolTipText("Hardware doesn't support APRS");
+       }
+
+       void set_aprs_format_tool_tip() {
+               if (aprs_format_value.isVisible())
+                       aprs_format_value.setToolTipText("Set the APRS format (compressed/uncompressed)");
+               else if (aprs_format_value.isVisible())
+                       aprs_format_value.setToolTipText("Software version doesn't support setting the APRS format");
+               else
+                       aprs_format_value.setToolTipText("Hardware doesn't support APRS");
+       }
+
+       void set_flight_log_max_tool_tip() {
+               if (flight_log_max_value.isVisible())
+                       flight_log_max_value.setToolTipText("Size reserved for each flight log (in kB)");
+               else {
+                       if (is_telemini_v1())
+                               flight_log_max_value.setToolTipText("TeleMini-v1 stores only one flight");
+                       else
+                               flight_log_max_value.setToolTipText("Cannot set max value with flight logs in memory");
+               }
+       }
+
+       void set_ignite_mode_tool_tip() {
+               if (ignite_mode_value.isVisible())
+                       ignite_mode_value.setToolTipText("Select when igniters will be fired");
+               else
+                       ignite_mode_value.setToolTipText("Older firmware could not select ignite mode");
+       }
+
+       void set_pad_orientation_tool_tip() {
+               if (pad_orientation_value.isVisible()) {
+                       pad_orientation_value.setToolTipText("How will the computer be mounted in the airframe");
+               } else {
+                       if (is_telemetrum())
+                               pad_orientation_value.setToolTipText("Older TeleMetrum firmware must fly antenna forward");
+                       else if (is_telemini() || is_easymini())
+                               pad_orientation_value.setToolTipText("TeleMini and EasyMini don't care how they are mounted");
+                       else
+                               pad_orientation_value.setToolTipText("Can't select orientation");
+               }
+       }
+
+       void set_accel_tool_tips() {
+               if (accel_plus_value.isVisible()) {
+                       accel_plus_value.setToolTipText("Pad acceleration value in flight orientation");
+                       accel_minus_value.setToolTipText("Upside-down acceleration value");
+               } else {
+                       accel_plus_value.setToolTipText("No accelerometer");
+                       accel_minus_value.setToolTipText("No accelerometer");
+               }
+       }
+
+       void set_beep_tool_tip() {
+               if (beep_value.isVisible())
+                       beep_value.setToolTipText("What frequency the beeper will sound at");
+               else
+                       beep_value.setToolTipText("Older firmware could not select beeper frequency");
+       }
+
+       /* Build the UI using a grid bag */
+       public AltosConfigFCUI(JFrame in_owner, boolean remote) {
+               super (in_owner, "Configure Flight Computer", false);
+
+               owner = in_owner;
+               GridBagConstraints c;
+               int row = 0;
+
+               Insets il = new Insets(4,4,4,4);
+               Insets ir = new Insets(4,4,4,4);
+
+               pane = getContentPane();
+               pane.setLayout(new GridBagLayout());
+
+               /* Product */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               product_label = new JLabel("Product:");
+               pane.add(product_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               product_value = new JLabel("");
+               pane.add(product_value, c);
+               row++;
+
+               /* Version */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               version_label = new JLabel("Software version:");
+               pane.add(version_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               version_value = new JLabel("");
+               pane.add(version_value, c);
+               row++;
+
+               /* Serial */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               serial_label = new JLabel("Serial:");
+               pane.add(serial_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               serial_value = new JLabel("");
+               pane.add(serial_value, c);
+               row++;
+
+               /* Main deploy */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               main_deploy_label = new JLabel(get_main_deploy_label());
+               pane.add(main_deploy_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               main_deploy_value = new JComboBox<String>(main_deploy_values());
+               main_deploy_value.setEditable(true);
+               main_deploy_value.addItemListener(this);
+               pane.add(main_deploy_value, c);
+               main_deploy_value.setToolTipText("Height above pad altitude to fire main charge");
+               row++;
+
+               /* Apogee delay */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               apogee_delay_label = new JLabel("Apogee Delay(s):");
+               pane.add(apogee_delay_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               apogee_delay_value = new JComboBox<String>(apogee_delay_values);
+               apogee_delay_value.setEditable(true);
+               apogee_delay_value.addItemListener(this);
+               pane.add(apogee_delay_value, c);
+               apogee_delay_value.setToolTipText("Delay after apogee before charge fires");
+               row++;
+
+               /* Apogee lockout */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               apogee_lockout_label = new JLabel("Apogee Lockout(s):");
+               pane.add(apogee_lockout_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               apogee_lockout_value = new JComboBox<String>(apogee_lockout_values);
+               apogee_lockout_value.setEditable(true);
+               apogee_lockout_value.addItemListener(this);
+               pane.add(apogee_lockout_value, c);
+               apogee_lockout_value.setToolTipText("Time after boost while apogee detection is locked out");
+               row++;
+
+               /* Frequency */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               radio_frequency_label = new JLabel("Frequency:");
+               pane.add(radio_frequency_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               radio_frequency_value = new AltosUIFreqList();
+               radio_frequency_value.addItemListener(this);
+               pane.add(radio_frequency_value, c);
+               radio_frequency_value.setToolTipText("Telemetry, RDF and packet frequency");
+               row++;
+
+               /* Radio Calibration */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               radio_calibration_label = new JLabel("RF Calibration:");
+               pane.add(radio_calibration_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               radio_calibration_value = new JLabel(String.format("%d", 1186611));
+               pane.add(radio_calibration_value, c);
+               row++;
+
+               /* Radio Enable */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               radio_enable_label = new JLabel("Telemetry/RDF/APRS Enable:");
+               pane.add(radio_enable_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               radio_enable_value = new JRadioButton("Enabled");
+               radio_enable_value.addItemListener(this);
+               pane.add(radio_enable_value, c);
+               set_radio_enable_tool_tip();
+               row++;
+
+               /* Telemetry Rate */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               rate_label = new JLabel("Telemetry baud rate:");
+               pane.add(rate_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               rate_value = new AltosUIRateList();
+               rate_value.addItemListener(this);
+               pane.add(rate_value, c);
+               set_rate_tool_tip();
+               row++;
+
+               /* APRS interval */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               aprs_interval_label = new JLabel("APRS Interval(s):");
+               pane.add(aprs_interval_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               aprs_interval_value = new JComboBox<String>(aprs_interval_values);
+               aprs_interval_value.setEditable(true);
+               aprs_interval_value.addItemListener(this);
+               pane.add(aprs_interval_value, c);
+               set_aprs_interval_tool_tip();
+               row++;
+
+               /* APRS SSID */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               aprs_ssid_label = new JLabel("APRS SSID:");
+               pane.add(aprs_ssid_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               aprs_ssid_value = new JComboBox<Integer>(aprs_ssid_values);
+               aprs_ssid_value.setEditable(false);
+               aprs_ssid_value.addItemListener(this);
+               aprs_ssid_value.setMaximumRowCount(aprs_ssid_values.length);
+               pane.add(aprs_ssid_value, c);
+               set_aprs_ssid_tool_tip();
+               row++;
+
+               /* APRS format */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               aprs_format_label = new JLabel("APRS format:");
+               pane.add(aprs_format_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               aprs_format_value = new JComboBox<String>(AltosLib.ao_aprs_format_name);
+               aprs_format_value.setEditable(false);
+               aprs_format_value.addItemListener(this);
+               aprs_format_value.setMaximumRowCount(AltosLib.ao_aprs_format_name.length);
+               pane.add(aprs_format_value, c);
+               set_aprs_format_tool_tip();
+               row++;
+
+               /* Callsign */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               callsign_label = new JLabel("Callsign:");
+               pane.add(callsign_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               callsign_value = new JTextField(AltosUIPreferences.callsign());
+               callsign_value.getDocument().addDocumentListener(this);
+               pane.add(callsign_value, c);
+               callsign_value.setToolTipText("Callsign reported in telemetry data");
+               row++;
+
+               /* Flight log max */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               flight_log_max_label = new JLabel("Maximum Flight Log Size (kB):");
+               pane.add(flight_log_max_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               flight_log_max_value = new JComboBox<String>();
+               flight_log_max_value.setEditable(true);
+               flight_log_max_value.addItemListener(this);
+               pane.add(flight_log_max_value, c);
+               set_flight_log_max_tool_tip();
+               row++;
+
+               /* Ignite mode */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               ignite_mode_label = new JLabel("Igniter Firing Mode:");
+               pane.add(ignite_mode_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               ignite_mode_value = new JComboBox<String>(ignite_mode_values);
+               ignite_mode_value.setEditable(false);
+               ignite_mode_value.addItemListener(this);
+               pane.add(ignite_mode_value, c);
+               set_ignite_mode_tool_tip();
+               row++;
+
+               /* Pad orientation */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               pad_orientation_label = new JLabel("Pad Orientation:");
+               pane.add(pad_orientation_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               pad_orientation_value = new JComboBox<String>(pad_orientation_values);
+               pad_orientation_value.setEditable(false);
+               pad_orientation_value.addItemListener(this);
+               pane.add(pad_orientation_value, c);
+               set_pad_orientation_tool_tip();
+               row++;
+
+               /* Accel plus */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               accel_plus_label = new JLabel("Accel Plus:");
+               pane.add(accel_plus_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               accel_plus_value = new JTextField(10);
+               accel_plus_value.setEditable(true);
+               accel_plus_value.getDocument().addDocumentListener(this);
+               pane.add(accel_plus_value, c);
+               row++;
+
+               /* Accel minus */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               accel_minus_label = new JLabel("Accel Minus:");
+               pane.add(accel_minus_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               accel_minus_value = new JTextField(10);
+               accel_minus_value.setEditable(true);
+               accel_minus_value.getDocument().addDocumentListener(this);
+               pane.add(accel_minus_value, c);
+               row++;
+               set_accel_tool_tips();
+
+               /* Beeper */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               beep_label = new JLabel("Beeper Frequency:");
+               pane.add(beep_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               beep_value = new JComboBox<String>(beep_values);
+               beep_value.setEditable(true);
+               beep_value.addItemListener(this);
+               pane.add(beep_value, c);
+               set_beep_tool_tip();
+               row++;
+
+               /* Tracker triger horiz distances */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               tracker_motion_label = new JLabel(get_tracker_motion_label());
+               pane.add(tracker_motion_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               tracker_motion_value = new JComboBox<String>(tracker_motion_values());
+               tracker_motion_value.setEditable(true);
+               tracker_motion_value.addItemListener(this);
+               pane.add(tracker_motion_value, c);
+               row++;
+
+               /* Tracker triger vert distances */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               tracker_interval_label = new JLabel("Position Reporting Interval(s):");
+               pane.add(tracker_interval_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               tracker_interval_value = new JComboBox<String>(tracker_interval_values);
+               tracker_interval_value.setEditable(true);
+               tracker_interval_value.addItemListener(this);
+               pane.add(tracker_interval_value, c);
+               set_tracker_tool_tip();
+               row++;
+
+               /* Pyro channels */
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               pyro = new JButton("Configure Pyro Channels");
+               pane.add(pyro, c);
+               pyro.addActionListener(this);
+               pyro.setActionCommand("Pyro");
+               row++;
+
+               /* Accel cal */
+               c = new GridBagConstraints();
+               c.gridx = 5; c.gridy = row;
+               c.gridwidth = 5;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               accel_cal = new JButton("Calibrate Accelerometer");
+               pane.add(accel_cal, c);
+               accel_cal.addActionListener(this);
+               accel_cal.setActionCommand("Accel");
+               row++;
+
+               /* Buttons */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 2;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               save = new JButton("Save");
+               pane.add(save, c);
+               save.addActionListener(this);
+               save.setActionCommand("Save");
+
+               c = new GridBagConstraints();
+               c.gridx = 2; c.gridy = row;
+               c.gridwidth = 2;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.CENTER;
+               c.insets = il;
+               reset = new JButton("Reset");
+               pane.add(reset, c);
+               reset.addActionListener(this);
+               reset.setActionCommand("Reset");
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = row;
+               c.gridwidth = 2;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.CENTER;
+               c.insets = il;
+               reboot = new JButton("Reboot");
+               pane.add(reboot, c);
+               reboot.addActionListener(this);
+               reboot.setActionCommand("Reboot");
+
+               c = new GridBagConstraints();
+               c.gridx = 6; c.gridy = row;
+               c.gridwidth = 2;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_END;
+               c.insets = il;
+               close = new JButton("Close");
+               pane.add(close, c);
+               close.addActionListener(this);
+               close.setActionCommand("Close");
+
+               addWindowListener(new ConfigListener(this));
+               AltosPreferences.register_units_listener(this);
+       }
+
+       /* Once the initial values are set, the config code will show the dialog */
+       public void make_visible() {
+               pack();
+               setLocationRelativeTo(owner);
+               setVisible(true);
+       }
+
+       /* If any values have been changed, confirm before closing */
+       public boolean check_dirty(String operation) {
+               if (dirty) {
+                       Object[] options = { String.format("%s anyway", operation), "Keep editing" };
+                       int i;
+                       i = JOptionPane.showOptionDialog(this,
+                                                        String.format("Configuration modified. %s anyway?", operation),
+                                                        "Configuration Modified",
+                                                        JOptionPane.DEFAULT_OPTION,
+                                                        JOptionPane.WARNING_MESSAGE,
+                                                        null, options, options[1]);
+                       if (i != 0)
+                               return false;
+               }
+               return true;
+       }
+
+       public void set_dirty() {
+               dirty = true;
+               save.setEnabled(true);
+       }
+
+       public void set_clean() {
+               dirty = false;
+               save.setEnabled(false);
+       }
+
+       AltosConfigPyroUI       pyro_ui;
+
+       public void dispose() {
+               if (pyro_ui != null)
+                       pyro_ui.dispose();
+               AltosPreferences.unregister_units_listener(this);
+               super.dispose();
+       }
+
+       /* Listen for events from our buttons */
+       public void actionPerformed(ActionEvent e) {
+               String  cmd = e.getActionCommand();
+
+               if (cmd.equals("Pyro")) {
+                       if (pyro_ui == null && pyros != null)
+                               pyro_ui = new AltosConfigPyroUI(this, pyros, pyro_firing_time);
+                       if (pyro_ui != null)
+                               pyro_ui.make_visible();
+                       return;
+               }
+
+               if (cmd.equals("Close") || cmd.equals("Reboot"))
+                       if (!check_dirty(cmd))
+                               return;
+               listener.actionPerformed(e);
+               if (cmd.equals("Close") || cmd.equals("Reboot")) {
+                       setVisible(false);
+                       dispose();
+               }
+               if (cmd.equals("Save") || cmd.equals("Reset"))
+                       set_clean();
+       }
+
+       /* ItemListener interface method */
+       public void itemStateChanged(ItemEvent e) {
+               set_dirty();
+       }
+
+       /* DocumentListener interface methods */
+       public void changedUpdate(DocumentEvent e) {
+               set_dirty();
+       }
+
+       public void insertUpdate(DocumentEvent e) {
+               set_dirty();
+       }
+
+       public void removeUpdate(DocumentEvent e) {
+               set_dirty();
+       }
+
+       /* Let the config code hook on a listener */
+       public void addActionListener(ActionListener l) {
+               listener = l;
+       }
+
+       /* set and get all of the dialog values */
+       public void set_product(String product) {
+               radio_frequency_value.set_product(product);
+               product_value.setText(product);
+               set_pad_orientation_tool_tip();
+               set_accel_tool_tips();
+               set_flight_log_max_tool_tip();
+       }
+
+       public void set_version(String version) {
+               version_value.setText(version);
+       }
+
+       public void set_serial(int serial) {
+               radio_frequency_value.set_serial(serial);
+               serial_value.setText(String.format("%d", serial));
+       }
+
+       public void set_altitude_32(int altitude_32) {
+       }
+
+       public void set_main_deploy(int new_main_deploy) {
+               if (new_main_deploy != AltosLib.MISSING)
+                       main_deploy_value.setSelectedItem(AltosConvert.height.say(new_main_deploy));
+               main_deploy_value.setVisible(new_main_deploy != AltosLib.MISSING);
+               main_deploy_label.setVisible(new_main_deploy != AltosLib.MISSING);
+       }
+
+       public int main_deploy() throws AltosConfigDataException {
+               String  str = main_deploy_value.getSelectedItem().toString();
+               try {
+                       return (int) (AltosConvert.height.parse_locale(str) + 0.5);
+               } catch (ParseException pe) {
+                       throw new AltosConfigDataException("invalid main deploy height %s", str);
+               }
+       }
+
+       String get_main_deploy_label() {
+               return String.format("Main Deploy Altitude(%s):", AltosConvert.height.parse_units());
+       }
+
+       String[] main_deploy_values() {
+               if (AltosConvert.imperial_units)
+                       return main_deploy_values_ft;
+               else
+                       return main_deploy_values_m;
+       }
+
+       void set_main_deploy_values() {
+               String[]        v = main_deploy_values();
+               while (main_deploy_value.getItemCount() > 0)
+                       main_deploy_value.removeItemAt(0);
+               for (int i = 0; i < v.length; i++)
+                       main_deploy_value.addItem(v[i]);
+               main_deploy_value.setMaximumRowCount(v.length);
+       }
+
+       public void units_changed(boolean imperial_units) {
+               boolean was_dirty = dirty;
+
+               String v = main_deploy_value.getSelectedItem().toString();
+               main_deploy_label.setText(get_main_deploy_label());
+               set_main_deploy_values();
+               try {
+                       int m = (int) (AltosConvert.height.parse_locale(v, !imperial_units) + 0.5);
+                       set_main_deploy(m);
+               } catch (ParseException pe) {
+               }
+
+               if (tracker_motion_value.isVisible()) {
+                       String motion = tracker_motion_value.getSelectedItem().toString();
+                       tracker_motion_label.setText(get_tracker_motion_label());
+                       set_tracker_motion_values();
+                       try {
+                               int m = (int) (AltosConvert.height.parse_locale(motion, !imperial_units) + 0.5);
+                               set_tracker_motion(m);
+                       } catch (ParseException pe) {
+                       }
+               }
+
+               if (!was_dirty)
+                       set_clean();
+       }
+
+       public void set_apogee_delay(int new_apogee_delay) {
+               if (new_apogee_delay != AltosLib.MISSING)
+                       apogee_delay_value.setSelectedItem(Integer.toString(new_apogee_delay));
+               apogee_delay_value.setVisible(new_apogee_delay != AltosLib.MISSING);
+               apogee_delay_label.setVisible(new_apogee_delay != AltosLib.MISSING);
+       }
+
+       private int parse_int(String name, String s, boolean split) throws AltosConfigDataException {
+               String v = s;
+               if (split)
+                       v = s.split("\\s+")[0];
+               try {
+                       return Integer.parseInt(v);
+               } catch (NumberFormatException ne) {
+                       throw new AltosConfigDataException("Invalid %s \"%s\"", name, s);
+               }
+       }
+
+       public int apogee_delay() throws AltosConfigDataException {
+               return parse_int("apogee delay", apogee_delay_value.getSelectedItem().toString(), false);
+       }
+
+       public void set_apogee_lockout(int new_apogee_lockout) {
+               if (new_apogee_lockout != AltosLib.MISSING)
+                       apogee_lockout_value.setSelectedItem(Integer.toString(new_apogee_lockout));
+
+               apogee_lockout_value.setVisible(new_apogee_lockout != AltosLib.MISSING);
+               apogee_lockout_label.setVisible(new_apogee_lockout != AltosLib.MISSING);
+       }
+
+       public int apogee_lockout() throws AltosConfigDataException {
+               return parse_int("apogee lockout", apogee_lockout_value.getSelectedItem().toString(), false);
+       }
+
+       public void set_radio_frequency(double new_radio_frequency) {
+               if (new_radio_frequency != AltosLib.MISSING)
+                       radio_frequency_value.set_frequency(new_radio_frequency);
+               radio_frequency_label.setVisible(new_radio_frequency != AltosLib.MISSING);
+               radio_frequency_value.setVisible(new_radio_frequency != AltosLib.MISSING);
+       }
+
+       public double radio_frequency() {
+               return radio_frequency_value.frequency();
+       }
+
+       public void set_radio_calibration(int new_radio_calibration) {
+               if (new_radio_calibration != AltosLib.MISSING)
+                       radio_calibration_value.setText(String.format("%d", new_radio_calibration));
+               radio_calibration_value.setVisible(new_radio_calibration != AltosLib.MISSING);
+               radio_calibration_label.setVisible(new_radio_calibration != AltosLib.MISSING);
+       }
+
+       public void set_radio_enable(int new_radio_enable) {
+               if (new_radio_enable != AltosLib.MISSING)
+                       radio_enable_value.setSelected(new_radio_enable != 0);
+               radio_enable_label.setVisible(new_radio_enable != AltosLib.MISSING);
+               radio_enable_value.setVisible(new_radio_enable != AltosLib.MISSING);
+               set_radio_enable_tool_tip();
+       }
+
+       public int radio_enable() {
+               if (radio_enable_value.isVisible())
+                       return radio_enable_value.isSelected() ? 1 : 0;
+               else
+                       return AltosLib.MISSING;
+       }
+
+       public void set_telemetry_rate(int new_rate) {
+               if (new_rate != AltosLib.MISSING)
+                       rate_value.set_rate(new_rate);
+               rate_label.setVisible(new_rate != AltosLib.MISSING);
+               rate_value.setVisible(new_rate != AltosLib.MISSING);
+               set_rate_tool_tip();
+       }
+
+       public int telemetry_rate() {
+               return rate_value.rate();
+       }
+
+       public void set_callsign(String new_callsign) {
+               if (new_callsign != null)
+                       callsign_value.setText(new_callsign);
+               callsign_value.setVisible(new_callsign != null);
+               callsign_label.setVisible(new_callsign != null);
+       }
+
+       public String callsign() {
+               if (callsign_value.isVisible())
+                       return callsign_value.getText();
+               return null;
+       }
+
+       int     flight_log_max_limit;
+       int     flight_log_max;
+
+       public String flight_log_max_label(int flight_log_max) {
+               if (flight_log_max_limit != 0) {
+                       int     nflight = flight_log_max_limit / flight_log_max;
+                       String  plural = nflight > 1 ? "s" : "";
+
+                       return String.format("%d (%d flight%s)", flight_log_max, nflight, plural);
+               }
+               return String.format("%d", flight_log_max);
+       }
+
+       public void set_flight_log_max(int new_flight_log_max) {
+               if (new_flight_log_max != AltosLib.MISSING) {
+                       flight_log_max_value.setSelectedItem(flight_log_max_label(new_flight_log_max));
+                       flight_log_max = new_flight_log_max;
+               }
+               flight_log_max_value.setVisible(new_flight_log_max != AltosLib.MISSING);
+               flight_log_max_label.setVisible(new_flight_log_max != AltosLib.MISSING);
+               set_flight_log_max_tool_tip();
+       }
+
+       public void set_flight_log_max_enabled(boolean enable) {
+               flight_log_max_value.setEnabled(enable);
+               set_flight_log_max_tool_tip();
+       }
+
+       public int flight_log_max() throws AltosConfigDataException {
+               if (flight_log_max_value.isVisible())
+                       return parse_int("flight log max", flight_log_max_value.getSelectedItem().toString(), true);
+               return AltosLib.MISSING;
+       }
+
+       public void set_flight_log_max_limit(int new_flight_log_max_limit) {
+               flight_log_max_limit = new_flight_log_max_limit;
+               if (new_flight_log_max_limit != AltosLib.MISSING) {
+                       flight_log_max_value.removeAllItems();
+                       for (int i = 8; i >= 1; i--) {
+                               int     size = flight_log_max_limit / i;
+                               flight_log_max_value.addItem(String.format("%d (%d flights)", size, i));
+                       }
+               }
+               if (flight_log_max != 0)
+                       set_flight_log_max(flight_log_max);
+       }
+
+       public void set_ignite_mode(int new_ignite_mode) {
+               if (new_ignite_mode != AltosLib.MISSING) {
+                       if (new_ignite_mode >= ignite_mode_values.length)
+                               new_ignite_mode = 0;
+                       if (new_ignite_mode < 0) {
+                               ignite_mode_value.setEnabled(false);
+                               new_ignite_mode = 0;
+                       } else {
+                               ignite_mode_value.setEnabled(true);
+                       }
+                       ignite_mode_value.setSelectedIndex(new_ignite_mode);
+               }
+               ignite_mode_value.setVisible(new_ignite_mode != AltosLib.MISSING);
+               ignite_mode_label.setVisible(new_ignite_mode != AltosLib.MISSING);
+
+               set_ignite_mode_tool_tip();
+       }
+
+       public int ignite_mode() {
+               if (ignite_mode_value.isVisible())
+                       return ignite_mode_value.getSelectedIndex();
+               else
+                       return AltosLib.MISSING;
+       }
+
+
+       public void set_pad_orientation(int new_pad_orientation) {
+               if (new_pad_orientation != AltosLib.MISSING) {
+                       if (new_pad_orientation >= pad_orientation_values.length)
+                               new_pad_orientation = 0;
+                       if (new_pad_orientation < 0)
+                               new_pad_orientation = 0;
+                       pad_orientation_value.setSelectedIndex(new_pad_orientation);
+               }
+               pad_orientation_value.setVisible(new_pad_orientation != AltosLib.MISSING);
+               pad_orientation_label.setVisible(new_pad_orientation != AltosLib.MISSING);
+               accel_cal.setVisible(new_pad_orientation != AltosLib.MISSING);
+
+               set_pad_orientation_tool_tip();
+       }
+
+       public int pad_orientation() {
+               if (pad_orientation_value.isVisible())
+                       return pad_orientation_value.getSelectedIndex();
+               else
+                       return AltosLib.MISSING;
+       }
+
+       public void set_accel_cal(int accel_plus, int accel_minus) {
+               if (accel_plus != AltosLib.MISSING) {
+                       accel_plus_value.setText(String.format("%d", accel_plus));
+                       accel_minus_value.setText(String.format("%d", accel_minus));
+               }
+               accel_plus_value.setVisible(accel_plus != AltosLib.MISSING);
+               accel_plus_label.setVisible(accel_plus != AltosLib.MISSING);
+               accel_minus_value.setVisible(accel_minus != AltosLib.MISSING);
+               accel_minus_label.setVisible(accel_minus != AltosLib.MISSING);
+
+               set_accel_tool_tips();
+       }
+
+       public int accel_cal_plus() {
+               if (accel_plus_value.isVisible())
+                       return Integer.parseInt(accel_plus_value.getText());
+               return AltosLib.MISSING;
+       }
+
+       public int accel_cal_minus() {
+               if (accel_minus_value.isVisible())
+                       return Integer.parseInt(accel_minus_value.getText());
+               return AltosLib.MISSING;
+       }
+
+       public void set_beep(int new_beep) {
+               if (new_beep != AltosLib.MISSING) {
+                       int new_freq = (int) Math.floor (AltosConvert.beep_value_to_freq(new_beep) + 0.5);
+                       for (int i = 0; i < beep_values.length; i++)
+                               if (new_beep == AltosConvert.beep_freq_to_value(Integer.parseInt(beep_values[i]))) {
+                                       beep_value.setSelectedIndex(i);
+                                       set_beep_tool_tip();
+                                       return;
+                               }
+                       beep_value.setSelectedItem(String.format("%d", new_freq));
+               }
+               beep_value.setVisible(new_beep != AltosLib.MISSING);
+               beep_label.setVisible(new_beep != AltosLib.MISSING);
+               set_beep_tool_tip();
+       }
+
+       public int beep() {
+               if (beep_value.isVisible())
+                       return AltosConvert.beep_freq_to_value(Integer.parseInt(beep_value.getSelectedItem().toString()));
+               else
+                       return AltosLib.MISSING;
+       }
+
+       String[] tracker_motion_values() {
+               if (AltosConvert.imperial_units)
+                       return tracker_motion_values_ft;
+               else
+                       return tracker_motion_values_m;
+       }
+
+       void set_tracker_motion_values() {
+               String[]        v = tracker_motion_values();
+               while (tracker_motion_value.getItemCount() > 0)
+                       tracker_motion_value.removeItemAt(0);
+               for (int i = 0; i < v.length; i++)
+                       tracker_motion_value.addItem(v[i]);
+               tracker_motion_value.setMaximumRowCount(v.length);
+       }
+
+       String get_tracker_motion_label() {
+               return String.format("Logging Trigger Motion (%s):", AltosConvert.height.parse_units());
+       }
+
+       void set_tracker_tool_tip() {
+               if (tracker_motion_value.isVisible())
+                       tracker_motion_value.setToolTipText("How far the device must move before logging");
+               else
+                       tracker_motion_value.setToolTipText("This device doesn't disable logging when stationary");
+               if (tracker_interval_value.isVisible())
+                       tracker_interval_value.setToolTipText("How often to report GPS position");
+               else
+                       tracker_interval_value.setToolTipText("This device can't configure interval");
+       }
+
+       public void set_tracker_motion(int tracker_motion) {
+               if (tracker_motion != AltosLib.MISSING)
+                       tracker_motion_value.setSelectedItem(AltosConvert.height.say(tracker_motion));
+               tracker_motion_label.setVisible(tracker_motion != AltosLib.MISSING);
+               tracker_motion_value.setVisible(tracker_motion != AltosLib.MISSING);
+       }
+
+       public int tracker_motion() throws AltosConfigDataException {
+               if (tracker_motion_value.isVisible()) {
+                       String str = tracker_motion_value.getSelectedItem().toString();
+                       try {
+                               return (int) (AltosConvert.height.parse_locale(str) + 0.5);
+                       } catch (ParseException pe) {
+                               throw new AltosConfigDataException("invalid tracker motion %s", str);
+                       }
+               }
+               return AltosLib.MISSING;
+       }
+
+       public void set_tracker_interval(int tracker_interval) {
+               if (tracker_interval != AltosLib.MISSING)
+                       tracker_interval_value.setSelectedItem(String.format("%d", tracker_interval));
+               tracker_interval_label.setVisible(tracker_interval != AltosLib.MISSING);
+               tracker_interval_value.setVisible(tracker_interval != AltosLib.MISSING);
+       }
+
+       public int tracker_interval() throws AltosConfigDataException {
+               if (tracker_interval_value.isVisible())
+                       return parse_int ("tracker interval", tracker_interval_value.getSelectedItem().toString(), false);
+               return AltosLib.MISSING;
+       }
+
+       public void set_pyros(AltosPyro[] new_pyros) {
+               pyros = new_pyros;
+               if (pyros != null && pyro_ui != null)
+                       pyro_ui.set_pyros(pyros);
+               pyro.setVisible(pyros != null);
+       }
+
+       public AltosPyro[] pyros() throws AltosConfigDataException {
+               if (pyro_ui != null)
+                       pyros = pyro_ui.get_pyros();
+               return pyros;
+       }
+
+       public void set_pyro_firing_time(double new_pyro_firing_time) {
+               pyro_firing_time = new_pyro_firing_time;
+               if (pyro_firing_time != AltosLib.MISSING && pyro_ui != null)
+                       pyro_ui.set_pyro_firing_time(pyro_firing_time);
+               pyro.setVisible(pyro_firing_time != AltosLib.MISSING);
+       }
+
+       public double pyro_firing_time() throws AltosConfigDataException {
+               if (pyro_ui != null)
+                       pyro_firing_time = pyro_ui.get_pyro_firing_time();
+               return pyro_firing_time;
+       }
+
+       public void set_aprs_interval(int new_aprs_interval) {
+               if (new_aprs_interval != AltosLib.MISSING)
+                       aprs_interval_value.setSelectedItem(Integer.toString(new_aprs_interval));
+               aprs_interval_value.setVisible(new_aprs_interval != AltosLib.MISSING);
+               aprs_interval_label.setVisible(new_aprs_interval != AltosLib.MISSING);
+               set_aprs_interval_tool_tip();
+       }
+
+       public int aprs_interval() throws AltosConfigDataException {
+               if (aprs_interval_value.isVisible()) {
+                       String  s = aprs_interval_value.getSelectedItem().toString();
+
+                       return parse_int("aprs interval", s, false);
+               }
+               return AltosLib.MISSING;
+       }
+
+       public void set_aprs_ssid(int new_aprs_ssid) {
+               if (new_aprs_ssid != AltosLib.MISSING)
+                       aprs_ssid_value.setSelectedItem(new_aprs_ssid);
+               aprs_ssid_value.setVisible(new_aprs_ssid != AltosLib.MISSING);
+               aprs_ssid_label.setVisible(new_aprs_ssid != AltosLib.MISSING);
+               set_aprs_ssid_tool_tip();
+       }
+
+       public int aprs_ssid() throws AltosConfigDataException {
+               if (aprs_ssid_value.isVisible()) {
+                       Integer i = (Integer) aprs_ssid_value.getSelectedItem();
+                       return i;
+               }
+               return AltosLib.MISSING;
+       }
+
+       public void set_aprs_format(int new_aprs_format) {
+               if (new_aprs_format != AltosLib.MISSING)
+                       aprs_format_value.setSelectedIndex(new_aprs_format);
+               aprs_format_value.setVisible(new_aprs_format != AltosLib.MISSING);
+               aprs_format_label.setVisible(new_aprs_format != AltosLib.MISSING);
+               set_aprs_format_tool_tip();
+       }
+
+       public int aprs_format() throws AltosConfigDataException {
+               if (aprs_format_value.isVisible())
+                       return aprs_format_value.getSelectedIndex();
+               return AltosLib.MISSING;
+       }
+}
diff --git a/teststand/AltosConfigPyroUI.java b/teststand/AltosConfigPyroUI.java
new file mode 100644 (file)
index 0000000..eff612e
--- /dev/null
@@ -0,0 +1,464 @@
+/*
+ * Copyright © 2012 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+import java.text.*;
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import org.altusmetrum.altoslib_12.*;
+import org.altusmetrum.altosuilib_12.*;
+
+public class AltosConfigPyroUI
+       extends AltosUIDialog
+       implements ItemListener, DocumentListener, AltosUnitsListener, ActionListener
+{
+       AltosConfigFCUI owner;
+       Container       pane;
+
+       static Insets il = new Insets(4,4,4,4);
+       static Insets ir = new Insets(4,4,4,4);
+
+       static String[] state_names;
+
+       static void make_state_names() {
+               if (state_names == null) {
+
+                       state_names = new String[AltosLib.ao_flight_landed - AltosLib.ao_flight_boost + 1];
+                       for (int state = AltosLib.ao_flight_boost; state <= AltosLib.ao_flight_landed; state++)
+                               state_names[state - AltosLib.ao_flight_boost] = AltosLib.state_name_capital(state);
+               }
+       }
+
+       class PyroItem implements ItemListener, DocumentListener, AltosUnitsListener
+       {
+               public int              flag;
+               public JCheckBox        enable;
+               public JTextField       value;
+               public JComboBox<String>        combo;
+               AltosConfigPyroUI       ui;
+               boolean                 setting;
+
+               public void set_enable(boolean enable) {
+                       if (value != null)
+                               value.setEnabled(enable);
+                       if (combo != null)
+                               combo.setEnabled(enable);
+               }
+
+               public void itemStateChanged(ItemEvent e) {
+                       set_enable(enable.isSelected());
+                       if (!setting)
+                               ui.set_dirty();
+               }
+
+               public void changedUpdate(DocumentEvent e) {
+                       if (!setting)
+                               ui.set_dirty();
+               }
+
+               public void insertUpdate(DocumentEvent e) {
+                       if (!setting)
+                               ui.set_dirty();
+               }
+
+               public void removeUpdate(DocumentEvent e) {
+                       if (!setting)
+                               ui.set_dirty();
+               }
+
+               public void units_changed(boolean imperial_units) {
+                       AltosUnits units = AltosPyro.pyro_to_units(flag);
+
+                       if (units != null) {
+                               try {
+                                       double v = units.parse_locale(value.getText(), !imperial_units);
+                                       set(enabled(), v);
+                               } catch (ParseException pe) {
+                                       set(enabled(), 0.0);
+                               }
+                       }
+               }
+
+               public void set(boolean new_enable, double new_value) {
+                       setting = true;
+                       enable.setSelected(new_enable);
+                       set_enable(new_enable);
+                       if (value != null) {
+                               double  scale = AltosPyro.pyro_to_scale(flag);
+                               double  unit_value = new_value;
+                               AltosUnits units = AltosPyro.pyro_to_units(flag);
+                               if (units != null)
+                                       unit_value = units.parse_value(new_value);
+                               String  format;
+                               if (scale >= 100)
+                                       format = "%6.2f";
+                               else if (scale >= 10)
+                                       format = "%6.1f";
+                               else
+                                       format = "%6.0f";
+                               value.setText(String.format(format, unit_value));
+                       }
+                       if (combo != null)
+                               if (new_value >= AltosLib.ao_flight_boost && new_value <= AltosLib.ao_flight_landed)
+                                       combo.setSelectedIndex((int) new_value - AltosLib.ao_flight_boost);
+                       setting = false;
+               }
+
+               public boolean enabled() {
+                       return enable.isSelected();
+               }
+
+               public double value() throws AltosConfigDataException {
+                       if (value != null) {
+                               AltosUnits units = AltosPyro.pyro_to_units(flag);
+                               try {
+                                       if (units != null)
+                                               return units.parse_locale(value.getText());
+                                       return AltosParse.parse_double_locale(value.getText());
+                               } catch (ParseException e) {
+                                       throw new AltosConfigDataException("\"%s\": %s\n", value.getText(), e.getMessage());
+                               }
+                       }
+                       if (combo != null)
+                               return combo.getSelectedIndex() + AltosLib.ao_flight_boost;
+                       return 0;
+               }
+
+               public PyroItem(AltosConfigPyroUI in_ui, int in_flag, int x, int y) {
+
+                       ui = in_ui;
+                       flag = in_flag;
+
+                       GridBagConstraints      c;
+                       c = new GridBagConstraints();
+                       c.gridx = x; c.gridy = y;
+                       c.gridwidth = 1;
+                       c.fill = GridBagConstraints.NONE;
+                       c.anchor = GridBagConstraints.LINE_START;
+                       c.insets = il;
+                       enable = new JCheckBox();
+                       enable.addItemListener(this);
+                       pane.add(enable, c);
+
+                       if ((flag & AltosPyro.pyro_no_value) == 0) {
+                               c = new GridBagConstraints();
+                               c.gridx = x+1; c.gridy = y;
+                               c.gridwidth = 1;
+                               c.fill = GridBagConstraints.NONE;
+                               c.anchor = GridBagConstraints.LINE_START;
+                               c.insets = il;
+                               if ((flag & AltosPyro.pyro_state_value) != 0) {
+                                       make_state_names();
+                                       combo = new JComboBox<String>(state_names);
+                                       combo.addItemListener(this);
+                                       pane.add(combo, c);
+                               } else {
+                                       value = new JTextField(10);
+                                       value.getDocument().addDocumentListener(this);
+                                       pane.add(value, c);
+                               }
+                       }
+               }
+       }
+
+       class PyroColumn implements AltosUnitsListener {
+               public PyroItem[]       items;
+               public JLabel           label;
+               int                     channel;
+
+               public void set(AltosPyro pyro) {
+                       int row = 0;
+                       for (int flag = 1; flag < AltosPyro.pyro_all; flag <<= 1) {
+                               if ((AltosPyro.pyro_all & flag) != 0) {
+                                       items[row].set((pyro.flags & flag) != 0,
+                                                      pyro.get_value(flag));
+                                       row++;
+                               }
+                       }
+               }
+
+               public AltosPyro get() throws AltosConfigDataException {
+                       AltosPyro       p = new AltosPyro(channel);
+
+                       int row = 0;
+                       for (int flag = 1; flag < AltosPyro.pyro_all; flag <<= 1) {
+                               if ((AltosPyro.pyro_all & flag) != 0) {
+                                       if (items[row].enabled()) {
+                                               try {
+                                               p.flags |= flag;
+                                               p.set_value(flag, items[row].value());
+                                               } catch (AltosConfigDataException ae) {
+                                                       throw new AltosConfigDataException("%s, %s",
+                                                                                          AltosPyro.pyro_to_name(flag),
+                                                                                          ae.getMessage());
+                                               }
+                                       }
+                                       row++;
+                               }
+                       }
+                       return p;
+               }
+
+               public void units_changed(boolean imperial_units) {
+                       int row = 0;
+                       for (int flag = 1; flag < AltosPyro.pyro_all; flag <<= 1) {
+                               if ((AltosPyro.pyro_all & flag) != 0) {
+                                       items[row].units_changed(imperial_units);
+                                       row++;
+                               }
+                       }
+               }
+
+               public PyroColumn(AltosConfigPyroUI ui, int x, int y, int in_channel) {
+
+                       channel = in_channel;
+
+                       int     nrow = 0;
+                       for (int flag = 1; flag < AltosPyro.pyro_all; flag <<= 1)
+                               if ((flag & AltosPyro.pyro_all) != 0)
+                                       nrow++;
+
+                       items = new PyroItem[nrow];
+                       int row = 0;
+
+                       GridBagConstraints      c;
+                       c = new GridBagConstraints();
+                       c.gridx = x; c.gridy = y;
+                       c.gridwidth = 2;
+                       c.fill = GridBagConstraints.NONE;
+                       c.anchor = GridBagConstraints.CENTER;
+                       c.insets = il;
+                       label = new JLabel(String.format("Pyro Channel %c", 'A' + channel));
+                       pane.add(label, c);
+                       y++;
+
+                       for (int flag = 1; flag < AltosPyro.pyro_all; flag <<= 1)
+                               if ((flag & AltosPyro.pyro_all) != 0) {
+                                       items[row] = new PyroItem(ui, flag, x, y + row);
+                                       row++;
+                               }
+               }
+       }
+
+       PyroColumn[]    columns;
+       JLabel[]        labels;
+
+       public void set_pyros(AltosPyro[] pyros) {
+               for (int i = 0; i < pyros.length; i++) {
+                       if (pyros[i].channel < columns.length)
+                               columns[pyros[i].channel].set(pyros[i]);
+               }
+       }
+
+       public AltosPyro[] get_pyros() throws AltosConfigDataException {
+               AltosPyro[]     pyros = new AltosPyro[columns.length];
+               for (int c = 0; c < columns.length; c++) {
+                       try {
+                               pyros[c] = columns[c].get();
+                       } catch (AltosConfigDataException ae) {
+                               throw new AltosConfigDataException ("Channel %c, %s", c + 'A', ae.getMessage());
+                       }
+               }
+               return pyros;
+       }
+
+       JLabel                  pyro_firing_time_label;
+       JComboBox<String>       pyro_firing_time_value;
+
+       static String[]         pyro_firing_time_values = {
+               "0.050", "0.100", "0.250", "0.500", "1.0", "2.0"
+       };
+
+       boolean initializing;
+
+       public void set_pyro_firing_time(double new_pyro_firing_time) {
+               initializing = true;
+               pyro_firing_time_value.setSelectedItem(Double.toString(new_pyro_firing_time));
+               pyro_firing_time_value.setEnabled(new_pyro_firing_time >= 0);
+               initializing = false;
+       }
+
+       public double get_pyro_firing_time() throws AltosConfigDataException {
+               String  v = pyro_firing_time_value.getSelectedItem().toString();
+
+               try {
+                       return AltosParse.parse_double_locale(v);
+               } catch (ParseException e) {
+                       throw new AltosConfigDataException("Invalid pyro firing time \"%s\"", v);
+               }
+       }
+
+       public void set_dirty() {
+               if (!initializing)
+                       owner.set_dirty();
+       }
+
+       public void itemStateChanged(ItemEvent e) {
+               if (!initializing)
+                       owner.set_dirty();
+       }
+
+       public void changedUpdate(DocumentEvent e) {
+               if (!initializing)
+                       owner.set_dirty();
+       }
+
+       public void insertUpdate(DocumentEvent e) {
+               if (!initializing)
+                       owner.set_dirty();
+       }
+
+       public void removeUpdate(DocumentEvent e) {
+               if (!initializing)
+                       owner.set_dirty();
+       }
+
+       public void units_changed(boolean imperial_units) {
+               for (int c = 0; c < columns.length; c++)
+                       columns[c].units_changed(imperial_units);
+               int r = 0;
+               for (int flag = 1; flag <= AltosPyro.pyro_all; flag <<= 1) {
+                       String n = AltosPyro.pyro_to_name(flag);
+                       if (n != null) {
+                               labels[r].setText(n);
+                               r++;
+                       }
+               }
+       }
+
+       /* A window listener to catch closing events and tell the config code */
+       class ConfigListener extends WindowAdapter {
+               AltosConfigPyroUI       ui;
+               AltosConfigFCUI         owner;
+
+               public ConfigListener(AltosConfigPyroUI this_ui, AltosConfigFCUI this_owner) {
+                       ui = this_ui;
+                       owner = this_owner;
+               }
+
+               public void windowClosing(WindowEvent e) {
+                       ui.setVisible(false);
+               }
+       }
+
+       /* Listen for events from our buttons */
+       public void actionPerformed(ActionEvent e) {
+               String  cmd = e.getActionCommand();
+
+               if (cmd.equals("Close"))
+                       setVisible(false);
+       }
+
+       public AltosConfigPyroUI(AltosConfigFCUI in_owner, AltosPyro[] pyros, double pyro_firing_time) {
+
+               super(in_owner, "Configure Pyro Channels", false);
+
+               owner = in_owner;
+
+               GridBagConstraints      c;
+
+               pane = getContentPane();
+               pane.setLayout(new GridBagLayout());
+
+               int     nrow = 0;
+               for (int flag = 1; flag < AltosPyro.pyro_all; flag <<= 1)
+                       if ((flag & AltosPyro.pyro_all) != 0)
+                               nrow++;
+
+               labels = new JLabel[nrow];
+
+               int     row = 1;
+
+               for (int flag = 1; flag <= AltosPyro.pyro_all; flag <<= 1) {
+                       String  n;
+
+                       n = AltosPyro.pyro_to_name(flag);
+                       if (n != null) {
+                               c = new GridBagConstraints();
+                               c.gridx = 0; c.gridy = row;
+                               c.gridwidth = 1;
+                               c.fill = GridBagConstraints.NONE;
+                               c.anchor = GridBagConstraints.LINE_START;
+                               c.insets = il;
+                               JLabel label = new JLabel(n);
+                               pane.add(label, c);
+                               labels[row-1] = label;
+                               row++;
+                       }
+               }
+
+               columns = new PyroColumn[pyros.length];
+
+               for (int i = 0; i < pyros.length; i++) {
+                       columns[i] = new PyroColumn(this, i*2 + 1, 0, i);
+                       columns[i].set(pyros[i]);
+               }
+
+               /* Pyro firing time */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = row;
+               c.gridwidth = 2;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               pyro_firing_time_label = new JLabel("Pyro Firing Time(s):");
+               pane.add(pyro_firing_time_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 2; c.gridy = row;
+               c.gridwidth = 7;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               pyro_firing_time_value = new JComboBox<String>(pyro_firing_time_values);
+               pyro_firing_time_value.setEditable(true);
+               pyro_firing_time_value.addItemListener(this);
+               set_pyro_firing_time(pyro_firing_time);
+               pane.add(pyro_firing_time_value, c);
+               pyro_firing_time_value.setToolTipText("Length of extra pyro channel firing pulse");
+
+               c = new GridBagConstraints();
+               c.gridx = pyros.length*2-1;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.gridwidth = 2;
+               c.gridy = 1000;
+               JButton close = new JButton("Close");
+               pane.add(close, c);
+               close.addActionListener(this);
+               close.setActionCommand("Close");
+
+               addWindowListener(new ConfigListener(this, owner));
+               AltosPreferences.register_units_listener(this);
+       }
+
+       public void dispose() {
+               AltosPreferences.unregister_units_listener(this);
+               super.dispose();
+       }
+
+       public void make_visible() {
+               pack();
+               setVisible(true);
+       }
+}
diff --git a/teststand/AltosConfigTD.java b/teststand/AltosConfigTD.java
new file mode 100644 (file)
index 0000000..f047874
--- /dev/null
@@ -0,0 +1,380 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+import java.awt.event.*;
+import javax.swing.*;
+import java.io.*;
+import java.util.concurrent.*;
+import org.altusmetrum.altoslib_12.*;
+import org.altusmetrum.altosuilib_12.*;
+
+public class AltosConfigTD implements ActionListener {
+
+       class int_ref {
+               int     value;
+
+               public int get() {
+                       return value;
+               }
+               public void set(int i) {
+                       value = i;
+               }
+               public int_ref(int i) {
+                       value = i;
+               }
+       }
+
+       class string_ref {
+               String  value;
+
+               public String get() {
+                       return value;
+               }
+               public void set(String i) {
+                       value = i;
+               }
+               public string_ref(String i) {
+                       value = i;
+               }
+       }
+
+       JFrame          owner;
+       AltosDevice     device;
+       AltosSerial     serial_line;
+       int_ref         serial;
+       int_ref         radio_channel;
+       int_ref         radio_calibration;
+       int_ref         radio_setting;
+       int_ref         radio_frequency;
+       int_ref         telemetry_rate;
+       string_ref      config_version;
+       string_ref      version;
+       string_ref      product;
+       AltosConfigTDUI config_ui;
+       boolean         made_visible;
+
+       boolean get_int(String line, String label, int_ref x) {
+               if (line.startsWith(label)) {
+                       try {
+                               String tail = line.substring(label.length()).trim();
+                               String[] tokens = tail.split("\\s+");
+                               if (tokens.length > 0) {
+                                       int     i = Integer.parseInt(tokens[0]);
+                                       x.set(i);
+                                       return true;
+                               }
+                       } catch (NumberFormatException ne) {
+                       }
+               }
+               return false;
+       }
+
+       boolean get_string(String line, String label, string_ref s) {
+               if (line.startsWith(label)) {
+                       String  quoted = line.substring(label.length()).trim();
+
+                       if (quoted.startsWith("\""))
+                               quoted = quoted.substring(1);
+                       if (quoted.endsWith("\""))
+                               quoted = quoted.substring(0,quoted.length()-1);
+                       s.set(quoted);
+                       return true;
+               } else {
+                       return false;
+               }
+       }
+
+       synchronized void update_ui() {
+               config_ui.set_serial(serial.get());
+               config_ui.set_product(product.get());
+               config_ui.set_version(version.get());
+               config_ui.set_radio_frequency(frequency());
+               config_ui.set_radio_calibration(radio_calibration.get());
+               config_ui.set_telemetry_rate(telemetry_rate.get());
+               config_ui.set_clean();
+               if (!made_visible) {
+                       made_visible = true;
+                       config_ui.make_visible();
+               }
+       }
+
+       void finish_input(String line) {
+               if (line == null) {
+                       abort();
+                       return;
+               }
+               if (line.equals("all finished")) {
+                       if (serial_line != null)
+                               update_ui();
+                       return;
+               }
+       }
+
+       synchronized void process_line(String line) {
+               if (line == null || line.equals("all finished")) {
+                       final String last_line = line;
+                       Runnable r = new Runnable() {
+                                       public void run() {
+                                               finish_input(last_line);
+                                       }
+                               };
+                       SwingUtilities.invokeLater(r);
+               } else {
+                       get_string(line, "Config version", config_version);
+                       get_int(line, "serial-number", serial);
+                       get_int(line, "Radio channel:", radio_channel);
+                       get_int(line, "Radio cal:", radio_calibration);
+                       get_int(line, "Frequency:", radio_frequency);
+                       get_int(line, "Radio setting:", radio_setting);
+                       get_int(line, "Telemetry rate:", telemetry_rate);
+                       get_string(line,"software-version", version);
+                       get_string(line,"product", product);
+               }
+       }
+
+       synchronized void reset_data() {
+               serial.set(0);
+               radio_channel.set(0);
+               radio_setting.set(0);
+               radio_frequency.set(0);
+               radio_calibration.set(1186611);
+               telemetry_rate.set(Altos.ao_telemetry_rate_38400);
+               config_version.set("0.0");
+               version.set("unknown");
+               product.set("unknown");
+       }
+
+       synchronized double frequency() {
+               return AltosConvert.radio_to_frequency(radio_frequency.get(),
+                                                      radio_setting.get(),
+                                                      radio_calibration.get(),
+                                                      radio_channel.get());
+       }
+
+       synchronized void set_frequency(double freq) {
+               int     frequency = radio_frequency.get();
+               int     setting = radio_setting.get();
+
+               if (frequency > 0) {
+                       radio_frequency.set((int) Math.floor (freq * 1000 + 0.5));
+               } else if (setting > 0) {
+                       radio_setting.set(AltosConvert.radio_frequency_to_setting(freq,
+                                                                                 radio_calibration.get()));
+                       radio_channel.set(0);
+               } else {
+                       radio_channel.set(AltosConvert.radio_frequency_to_channel(freq));
+               }
+       }
+
+       synchronized int telemetry_rate() {
+               return telemetry_rate.get();
+       }
+
+       synchronized void set_telemetry_rate(int new_telemetry_rate){
+               int     rate = telemetry_rate.get();
+
+               if (rate >= 0)
+                       telemetry_rate.set(new_telemetry_rate);
+       }
+
+       final static int        serial_mode_read = 0;
+       final static int        serial_mode_save = 1;
+       final static int        serial_mode_reboot = 2;
+
+       SerialData serial_data;
+       Thread serial_thread;
+
+       class SerialData implements Runnable {
+               AltosConfigTD   config;
+               int             serial_mode;
+
+               void get_data() {
+                       try {
+                               boolean been_there = false;
+                               config.reset_data();
+
+                               while (config.serial_line != null) {
+                                       config.serial_line.printf("c s\nf\nv\n");
+                                       while (config.serial_line != null) {
+                                               try {
+                                                       String line = config.serial_line.get_reply(5000);
+                                                       config.process_line(line);
+                                                       if (line != null && line.startsWith("software-version"))
+                                                               break;
+                                               } catch (Exception e) {
+                                                       break;
+                                               }
+                                       }
+                                       if (been_there)
+                                               break;
+                                       if (!config_version.get().equals("0.0"))
+                                               break;
+                                       been_there = true;
+                                       if (config != null && config.serial_line != null) {
+                                               config.serial_line.printf("C\n ");
+                                               config.serial_line.flush_input();
+                                       }
+                               }
+                       } catch (InterruptedException ie) {
+                       }
+                       /*
+                        * This makes sure the displayed frequency respects the limits that the
+                        * available firmware version might place on the actual frequency
+                        */
+                       config.set_frequency(AltosPreferences.frequency(serial.get()));
+                       config.set_telemetry_rate(AltosPreferences.telemetry_rate(serial.get()));
+                       config.process_line("all finished");
+               }
+
+               void save_data() {
+                       double frequency = frequency();
+                       if (frequency != 0)
+                               AltosPreferences.set_frequency(serial.get(),
+                                                              frequency);
+                       AltosPreferences.set_telemetry_rate(serial.get(),
+                                                           telemetry_rate());
+               }
+
+               public void run () {
+                       switch (serial_mode) {
+                       case serial_mode_save:
+                               save_data();
+                               /* fall through ... */
+                       case serial_mode_read:
+                               get_data();
+                               serial_thread = null;
+                               break;
+                       }
+               }
+
+               public SerialData(AltosConfigTD in_config, int in_serial_mode) {
+                       config = in_config;
+                       serial_mode = in_serial_mode;
+               }
+       }
+
+       void run_serial_thread(int serial_mode) {
+               serial_data = new SerialData(this, serial_mode);
+               serial_thread = new Thread(serial_data);
+               serial_thread.start();
+       }
+
+       void abort_serial_thread() {
+               if (serial_thread != null) {
+                       serial_thread.interrupt();
+                       serial_thread = null;
+               }
+       }
+       void init_ui () throws InterruptedException, TimeoutException {
+               config_ui = new AltosConfigTDUI(owner);
+               config_ui.addActionListener(this);
+               serial_line.set_frame(owner);
+               set_ui();
+       }
+
+       void abort() {
+               abort_serial_thread();
+               if (serial_line != null) {
+                       serial_line.close();
+                       serial_line = null;
+               }
+               JOptionPane.showMessageDialog(owner,
+                                             String.format("Connection to \"%s\" failed",
+                                                           device.toShortString()),
+                                             "Connection Failed",
+                                             JOptionPane.ERROR_MESSAGE);
+               config_ui.setVisible(false);
+       }
+
+       void set_ui() throws InterruptedException, TimeoutException {
+               if (serial_line != null)
+                       run_serial_thread(serial_mode_read);
+               else
+                       update_ui();
+       }
+
+       void save_data() {
+               double  freq = config_ui.radio_frequency();
+               set_frequency(freq);
+               int telemetry_rate = config_ui.telemetry_rate();
+               set_telemetry_rate(telemetry_rate);
+               run_serial_thread(serial_mode_save);
+       }
+
+       public void actionPerformed(ActionEvent e) {
+               String  cmd = e.getActionCommand();
+               try {
+                       if (cmd.equals("Save")) {
+                               save_data();
+                       } else if (cmd.equals("Reset")) {
+                               set_ui();
+                       } else if (cmd.equals("Reboot")) {
+                               if (serial_line != null)
+                                       run_serial_thread(serial_mode_reboot);
+                       } else if (cmd.equals("Close")) {
+                               if (serial_line != null)
+                                       serial_line.close();
+                       }
+               } catch (InterruptedException ie) {
+                       abort();
+               } catch (TimeoutException te) {
+                       abort();
+               }
+       }
+
+       public AltosConfigTD(JFrame given_owner) {
+               owner = given_owner;
+
+               serial = new int_ref(0);
+               radio_channel = new int_ref(0);
+               radio_setting = new int_ref(0);
+               radio_frequency = new int_ref(0);
+               radio_calibration = new int_ref(1186611);
+               telemetry_rate = new int_ref(AltosLib.ao_telemetry_rate_38400);
+               config_version = new string_ref("0.0");
+               version = new string_ref("unknown");
+               product = new string_ref("unknown");
+
+               device = AltosDeviceUIDialog.show(owner, Altos.product_basestation);
+               if (device != null) {
+                       try {
+                               serial_line = new AltosSerial(device);
+                               try {
+                                       init_ui();
+                               } catch (InterruptedException ie) {
+                                       abort();
+                               } catch (TimeoutException te) {
+                                       abort();
+                               }
+                       } catch (FileNotFoundException ee) {
+                               JOptionPane.showMessageDialog(owner,
+                                                             ee.getMessage(),
+                                                             "Cannot open target device",
+                                                             JOptionPane.ERROR_MESSAGE);
+                       } catch (AltosSerialInUseException si) {
+                               JOptionPane.showMessageDialog(owner,
+                                                             String.format("Device \"%s\" already in use",
+                                                                           device.toShortString()),
+                                                             "Device in use",
+                                                             JOptionPane.ERROR_MESSAGE);
+                       }
+               }
+       }
+}
diff --git a/teststand/AltosConfigTDUI.java b/teststand/AltosConfigTDUI.java
new file mode 100644 (file)
index 0000000..43da16f
--- /dev/null
@@ -0,0 +1,378 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import org.altusmetrum.altoslib_12.*;
+import org.altusmetrum.altosuilib_12.*;
+
+public class AltosConfigTDUI
+       extends AltosUIDialog
+       implements ActionListener, ItemListener, DocumentListener
+{
+
+       Container       pane;
+       Box             box;
+       JLabel          product_label;
+       JLabel          version_label;
+       JLabel          serial_label;
+       JLabel          frequency_label;
+       JLabel          radio_calibration_label;
+       JLabel          radio_frequency_label;
+       JLabel          rate_label;
+
+       public boolean          dirty;
+
+       JFrame          owner;
+       JLabel          product_value;
+       JLabel          version_value;
+       JLabel          serial_value;
+       AltosUIFreqList radio_frequency_value;
+       JLabel          radio_calibration_value;
+       AltosUIRateList rate_value;
+
+       JButton         save;
+       JButton         reset;
+       JButton         reboot;
+       JButton         close;
+
+       ActionListener  listener;
+
+       /* A window listener to catch closing events and tell the config code */
+       class ConfigListener extends WindowAdapter {
+               AltosConfigTDUI ui;
+
+               public ConfigListener(AltosConfigTDUI this_ui) {
+                       ui = this_ui;
+               }
+
+               public void windowClosing(WindowEvent e) {
+                       ui.actionPerformed(new ActionEvent(e.getSource(),
+                                                          ActionEvent.ACTION_PERFORMED,
+                                                          "Close"));
+               }
+       }
+
+       /* Build the UI using a grid bag */
+       public AltosConfigTDUI(JFrame in_owner) {
+               super (in_owner, "Configure TeleDongle", false);
+
+               owner = in_owner;
+               GridBagConstraints c;
+
+               Insets il = new Insets(4,4,4,4);
+               Insets ir = new Insets(4,4,4,4);
+
+               pane = getContentPane();
+               pane.setLayout(new GridBagLayout());
+
+               /* Product */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = 0;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               product_label = new JLabel("Product:");
+               pane.add(product_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = 0;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               product_value = new JLabel("");
+               pane.add(product_value, c);
+
+               /* Version */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = 1;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               version_label = new JLabel("Software version:");
+               pane.add(version_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = 1;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               version_value = new JLabel("");
+               pane.add(version_value, c);
+
+               /* Serial */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = 2;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               serial_label = new JLabel("Serial:");
+               pane.add(serial_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = 2;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               serial_value = new JLabel("");
+               pane.add(serial_value, c);
+
+               /* Frequency */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = 5;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               radio_frequency_label = new JLabel("Frequency:");
+               pane.add(radio_frequency_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = 5;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               radio_frequency_value = new AltosUIFreqList();
+               radio_frequency_value.addItemListener(this);
+               pane.add(radio_frequency_value, c);
+               radio_frequency_value.setToolTipText("Telemetry, RDF and packet frequency");
+
+               /* Radio Calibration */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = 6;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               radio_calibration_label = new JLabel("RF Calibration:");
+               pane.add(radio_calibration_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = 6;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               radio_calibration_value = new JLabel(String.format("%d", 1186611));
+               pane.add(radio_calibration_value, c);
+
+               /* Telemetry Rate */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = 7;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               c.ipady = 5;
+               rate_label = new JLabel("Telemetry Rate:");
+               pane.add(rate_label, c);
+
+               c = new GridBagConstraints();
+               c.gridx = 4; c.gridy = 7;
+               c.gridwidth = 4;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = ir;
+               c.ipady = 5;
+               rate_value = new AltosUIRateList();
+               pane.add(rate_value, c);
+
+               /* Buttons */
+               c = new GridBagConstraints();
+               c.gridx = 0; c.gridy = 12;
+               c.gridwidth = 2;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_START;
+               c.insets = il;
+               save = new JButton("Save");
+               pane.add(save, c);
+               save.addActionListener(this);
+               save.setActionCommand("Save");
+
+               c = new GridBagConstraints();
+               c.gridx = 2; c.gridy = 12;
+               c.gridwidth = 2;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.CENTER;
+               c.insets = il;
+               reset = new JButton("Reset");
+               pane.add(reset, c);
+               reset.addActionListener(this);
+               reset.setActionCommand("Reset");
+
+               c = new GridBagConstraints();
+               c.gridx = 6; c.gridy = 12;
+               c.gridwidth = 2;
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.LINE_END;
+               c.insets = il;
+               close = new JButton("Close");
+               pane.add(close, c);
+               close.addActionListener(this);
+               close.setActionCommand("Close");
+
+               addWindowListener(new ConfigListener(this));
+       }
+
+       /* Once the initial values are set, the config code will show the dialog */
+       public void make_visible() {
+               pack();
+               setLocationRelativeTo(owner);
+               setVisible(true);
+       }
+
+       /* If any values have been changed, confirm before closing */
+       public boolean check_dirty(String operation) {
+               if (dirty) {
+                       Object[] options = { String.format("%s anyway", operation), "Keep editing" };
+                       int i;
+                       i = JOptionPane.showOptionDialog(this,
+                                                        String.format("Configuration modified. %s anyway?", operation),
+                                                        "Configuration Modified",
+                                                        JOptionPane.DEFAULT_OPTION,
+                                                        JOptionPane.WARNING_MESSAGE,
+                                                        null, options, options[1]);
+                       if (i != 0)
+                               return false;
+               }
+               return true;
+       }
+
+       /* Listen for events from our buttons */
+       public void actionPerformed(ActionEvent e) {
+               String  cmd = e.getActionCommand();
+
+               if (cmd.equals("Close") || cmd.equals("Reboot"))
+                       if (!check_dirty(cmd))
+                               return;
+               listener.actionPerformed(e);
+               if (cmd.equals("Close") || cmd.equals("Reboot")) {
+                       setVisible(false);
+                       dispose();
+               }
+               dirty = false;
+       }
+
+       /* ItemListener interface method */
+       public void itemStateChanged(ItemEvent e) {
+               dirty = true;
+       }
+
+       /* DocumentListener interface methods */
+       public void changedUpdate(DocumentEvent e) {
+               dirty = true;
+       }
+
+       public void insertUpdate(DocumentEvent e) {
+               dirty = true;
+       }
+
+       public void removeUpdate(DocumentEvent e) {
+               dirty = true;
+       }
+
+       /* Let the config code hook on a listener */
+       public void addActionListener(ActionListener l) {
+               listener = l;
+       }
+
+       /* set and get all of the dialog values */
+       public void set_product(String product) {
+               radio_frequency_value.set_product(product);
+               product_value.setText(product);
+       }
+
+       public void set_version(String version) {
+               version_value.setText(version);
+       }
+
+       public void set_serial(int serial) {
+               radio_frequency_value.set_serial(serial);
+               serial_value.setText(String.format("%d", serial));
+       }
+
+       public void set_radio_frequency(double new_radio_frequency) {
+               int i;
+               for (i = 0; i < radio_frequency_value.getItemCount(); i++) {
+                       AltosFrequency  f = (AltosFrequency) radio_frequency_value.getItemAt(i);
+
+                       if (f.close(new_radio_frequency)) {
+                               radio_frequency_value.setSelectedIndex(i);
+                               return;
+                       }
+               }
+               for (i = 0; i < radio_frequency_value.getItemCount(); i++) {
+                       AltosFrequency  f = (AltosFrequency) radio_frequency_value.getItemAt(i);
+
+                       if (new_radio_frequency < f.frequency)
+                               break;
+               }
+               String  description = String.format("%s serial %s",
+                                                   product_value.getText(),
+                                                   serial_value.getText());
+               AltosFrequency  new_frequency = new AltosFrequency(new_radio_frequency, description);
+               AltosPreferences.add_common_frequency(new_frequency);
+               radio_frequency_value.insertItemAt(new_frequency, i);
+               radio_frequency_value.setSelectedIndex(i);
+       }
+
+       public double radio_frequency() {
+               return radio_frequency_value.frequency();
+       }
+
+       public void set_radio_calibration(int calibration) {
+               radio_calibration_value.setText(String.format("%d", calibration));
+       }
+
+       public int telemetry_rate() {
+               return rate_value.getSelectedIndex();
+       }
+
+       public void set_telemetry_rate(int rate) {
+               rate_value.setSelectedIndex(rate);
+       }
+
+       public void set_clean() {
+               dirty = false;
+       }
+}
diff --git a/teststand/AltosConfigureUI.java b/teststand/AltosConfigureUI.java
new file mode 100644 (file)
index 0000000..30c0c3c
--- /dev/null
@@ -0,0 +1,157 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.beans.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import org.altusmetrum.altosuilib_12.*;
+
+public class AltosConfigureUI
+       extends AltosUIConfigure
+       implements DocumentListener
+{
+       AltosVoice      voice;
+
+       public JTextField       callsign_value;
+       public JComboBox<String>        position_value;
+
+       /* DocumentListener interface methods */
+       public void insertUpdate(DocumentEvent e) {
+               changedUpdate(e);
+       }
+
+       public void removeUpdate(DocumentEvent e) {
+               changedUpdate(e);
+       }
+
+       public void changedUpdate(DocumentEvent e) {
+               if (callsign_value != null)
+                       AltosUIPreferences.set_callsign(callsign_value.getText());
+       }
+
+       public void add_voice() {
+
+               /* Voice settings */
+               pane.add(new JLabel("Voice"), constraints(0, 1));
+
+               JRadioButton enable_voice = new JRadioButton("Enable", AltosUIPreferences.voice());
+               enable_voice.addActionListener(new ActionListener() {
+                               public void actionPerformed(ActionEvent e) {
+                                       JRadioButton item = (JRadioButton) e.getSource();
+                                       boolean enabled = item.isSelected();
+                                       AltosUIPreferences.set_voice(enabled);
+                                       if (enabled)
+                                               voice.speak_always("Enable voice.");
+                                       else
+                                               voice.speak_always("Disable voice.");
+                               }
+                       });
+               pane.add(enable_voice, constraints(1, 1));
+               enable_voice.setToolTipText("Enable/Disable all audio in-flight announcements");
+
+               JButton test_voice = new JButton("Test Voice");
+               test_voice.addActionListener(new ActionListener() {
+                               public void actionPerformed(ActionEvent e) {
+                                       voice.speak("That's one small step for man; one giant leap for mankind.");
+                               }
+                       });
+               pane.add(test_voice, constraints(2, 1));
+               test_voice.setToolTipText("Play a stock audio clip to check volume");
+               row++;
+       }
+
+       public void add_callsign() {
+               /* Callsign setting */
+               pane.add(new JLabel("Callsign"), constraints(0, 1));
+
+               callsign_value = new JTextField(AltosUIPreferences.callsign());
+               callsign_value.getDocument().addDocumentListener(this);
+               callsign_value.setToolTipText("Callsign sent in packet mode");
+               pane.add(callsign_value, constraints(1, 2, GridBagConstraints.BOTH));
+               row++;
+       }
+
+       boolean has_bluetooth;
+
+       public void add_bluetooth() {
+               JButton manage_bluetooth = new JButton("Manage Bluetooth");
+               manage_bluetooth.addActionListener(new ActionListener() {
+                               public void actionPerformed(ActionEvent e) {
+                                       AltosBTManage.show(owner, AltosBTKnown.bt_known());
+                               }
+                       });
+               pane.add(manage_bluetooth, constraints(0, 2));
+               /* in the same row as add_frequencies, so don't bump row */
+               has_bluetooth = true;
+       }
+
+       public void add_frequencies() {
+               JButton manage_frequencies = new JButton("Manage Frequencies");
+               manage_frequencies.addActionListener(new ActionListener() {
+                               public void actionPerformed(ActionEvent e) {
+                                       AltosConfigFreqUI.show(owner);
+                               }
+                       });
+               manage_frequencies.setToolTipText("Configure which values are shown in frequency menus");
+               if (has_bluetooth)
+                       pane.add(manage_frequencies, constraints(2, 1));
+               else
+                       pane.add(manage_frequencies, constraints(0, 3));
+               row++;
+       }
+
+       final static String[] position_names = {
+               "Top left",
+               "Top",
+               "Top right",
+               "Left",
+               "Center",
+               "Right",
+               "Bottom left",
+               "Bottom",
+               "Bottom right",
+       };
+
+       public void add_position() {
+               pane.add(new JLabel ("Menu position"), constraints(0, 1));
+
+               position_value = new JComboBox<String>(position_names);
+               position_value.setMaximumRowCount(position_names.length);
+               int position = AltosUIPreferences.position();
+               position_value.setSelectedIndex(position);
+               position_value.addActionListener(new ActionListener() {
+                               public void actionPerformed(ActionEvent e) {
+                                       int     position = position_value.getSelectedIndex();
+                                       AltosUIPreferences.set_position(position);
+                               }
+                       });
+               pane.add(position_value, constraints(1, 2, GridBagConstraints.BOTH));
+               position_value.setToolTipText("Position of main AltosUI window");
+               row++;
+       }
+
+       public AltosConfigureUI(JFrame owner, AltosVoice voice) {
+               super(owner);
+
+               this.voice = voice;
+       }
+}
diff --git a/teststand/AltosDescent.java b/teststand/AltosDescent.java
new file mode 100644 (file)
index 0000000..5d8d8de
--- /dev/null
@@ -0,0 +1,165 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+import java.util.*;
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import org.altusmetrum.altoslib_12.*;
+import org.altusmetrum.altosuilib_12.*;
+
+public class AltosDescent extends AltosUIFlightTab {
+
+       class Height extends AltosUIUnitsIndicator {
+               public double value(AltosState state, int i) { return state.height(); }
+
+               public Height (Container container, int x, int y) {
+                       super (container, x, y, AltosConvert.height, "Height");
+               }
+       }
+
+       class Speed extends AltosUIUnitsIndicator {
+               public double value(AltosState state, int i) { return state.speed(); }
+
+               public Speed (Container container, int x, int y) {
+                       super (container, x, y, AltosConvert.speed, "Speed");
+               }
+       }
+
+       class Lat extends AltosUIUnitsIndicator {
+
+               public boolean hide (AltosState state, int i) { return state.gps == null || !state.gps.connected; }
+
+               public double value(AltosState state, int i) {
+                       if (state.gps == null)
+                               return AltosLib.MISSING;
+                       if (!state.gps.connected)
+                               return AltosLib.MISSING;
+                       return state.gps.lat;
+               }
+
+               public Lat (Container container, int x, int y) {
+                       super (container, x, y, AltosConvert.latitude, "Latitude");
+               }
+       }
+
+       class Lon extends AltosUIUnitsIndicator {
+               public boolean hide (AltosState state, int i) { return state.gps == null || !state.gps.connected; }
+
+               public double value(AltosState state, int i) {
+                       if (state.gps == null)
+                               return AltosLib.MISSING;
+                       if (!state.gps.connected)
+                               return AltosLib.MISSING;
+                       return state.gps.lon;
+               }
+
+               public Lon (Container container, int x, int y) {
+                       super (container, x, y, AltosConvert.longitude, "Longitude");
+               }
+       }
+
+       class Apogee extends AltosUIUnitsIndicator {
+               public boolean hide(double v) { return v == AltosLib.MISSING; }
+               public double value(AltosState state, int i) { return state.apogee_voltage; }
+               public double good() { return AltosLib.ao_igniter_good; }
+
+               public Apogee (Container container, int y) {
+                       super(container, 0, y, 3, AltosConvert.voltage, "Apogee Igniter Voltage", 1, true, 3);
+               }
+       }
+
+       class Main extends AltosUIUnitsIndicator {
+               public boolean hide(double v) { return v == AltosLib.MISSING; }
+               public double value(AltosState state, int i) { return state.main_voltage; }
+               public double good() { return AltosLib.ao_igniter_good; }
+
+               public Main (Container container, int y) {
+                       super(container, 0, y, 3, AltosConvert.voltage, "Main Igniter Voltage", 1, true, 3);
+               }
+       }
+
+       class Distance extends AltosUIUnitsIndicator {
+               public double value(AltosState state, int i) {
+                       if (state.from_pad != null)
+                               return state.from_pad.distance;
+                       else
+                               return AltosLib.MISSING;
+               }
+
+               public Distance(Container container, int x, int y) {
+                       super(container, x, y, AltosConvert.distance, "Ground Distance");
+               }
+       }
+
+       class Range extends AltosUIUnitsIndicator {
+               public double value(AltosState state, int i) {
+                       return state.range;
+               }
+               public Range (Container container, int x, int y) {
+                       super (container, x, y, AltosConvert.distance, "Range");
+               }
+       }
+
+       class Bearing extends AltosUIIndicator {
+               public void show (AltosState state, AltosListenerState listener_state) {
+                       if (state.from_pad != null && state.from_pad.bearing != AltosLib.MISSING) {
+                               show( String.format("%3.0f°", state.from_pad.bearing),
+                                     state.from_pad.bearing_words(
+                                             AltosGreatCircle.BEARING_LONG));
+                       } else {
+                               show("Missing", "Missing");
+                       }
+               }
+               public Bearing (Container container, int x, int y) {
+                       super (container, x, y, 1, "Bearing", 2, false, 1, 2);
+               }
+       }
+
+       class Elevation extends AltosUIIndicator {
+               public void show (AltosState state, AltosListenerState listener_state) {
+                       if (state.elevation == AltosLib.MISSING)
+                               show("Missing");
+                       else
+                               show("%3.0f°", state.elevation);
+               }
+               public Elevation (Container container, int x, int y) {
+                       super (container, x, y, "Elevation", 1, false, 1);
+               }
+       }
+
+       public String getName() {
+               return "Descent";
+       }
+
+       public AltosDescent() {
+               /* Elements in descent display */
+               add(new Speed(this, 0, 0));
+               add(new Height(this, 2, 0));
+               add(new Elevation(this, 0, 1));
+               add(new Range(this, 2, 1));
+               add(new Bearing(this, 0, 2));
+               add(new Distance(this, 0, 3));
+               add(new Lat(this, 0, 4));
+               add(new Lon(this, 2, 4));
+               add(new Apogee(this, 5));
+               add(new Main(this, 6));
+       }
+}
diff --git a/teststand/AltosDevice.java b/teststand/AltosDevice.java
new file mode 100644 (file)
index 0000000..a833dcc
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * Copyright © 2011 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+import libaltosJNI.*;
+
+public interface AltosDevice {
+       public abstract String toString();
+       public abstract String toShortString();
+       public abstract int getSerial();
+       public abstract String getPath();
+       public abstract boolean matchProduct(int product);
+       public abstract String getErrorString();
+       public SWIGTYPE_p_altos_file open();
+}
diff --git a/teststand/AltosFlightStatus.java b/teststand/AltosFlightStatus.java
new file mode 100644 (file)
index 0000000..cbc3fa7
--- /dev/null
@@ -0,0 +1,325 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+import java.awt.*;
+import javax.swing.*;
+import org.altusmetrum.altoslib_12.*;
+import org.altusmetrum.altosuilib_12.*;
+
+public class AltosFlightStatus extends JComponent implements AltosFlightDisplay {
+       GridBagLayout   layout;
+
+       public abstract class FlightValue {
+               JLabel          label;
+               JTextField      value;
+
+               void show() {
+                       label.setVisible(true);
+                       value.setVisible(true);
+               }
+
+               void hide() {
+                       label.setVisible(false);
+                       value.setVisible(false);
+               }
+
+               abstract void show(AltosState state, AltosListenerState listener_state);
+
+               void reset() {
+                       value.setText("");
+               }
+
+               void set_font() {
+                       label.setFont(Altos.status_font);
+                       value.setFont(Altos.status_font);
+               }
+
+               void setVisible(boolean visible) {
+                       label.setVisible(visible);
+                       value.setVisible(visible);
+               }
+
+               public FlightValue (GridBagLayout layout, int x, String text) {
+                       GridBagConstraints      c = new GridBagConstraints();
+                       c.insets = new Insets(5, 5, 5, 5);
+                       c.anchor = GridBagConstraints.CENTER;
+                       c.fill = GridBagConstraints.BOTH;
+                       c.weightx = 1;
+                       c.weighty = 1;
+
+                       label = new JLabel(text);
+                       label.setFont(Altos.status_font);
+                       label.setHorizontalAlignment(SwingConstants.CENTER);
+                       c.gridx = x; c.gridy = 0;
+                       layout.setConstraints(label, c);
+                       add(label);
+
+                       value = new JTextField("");
+                       value.setEditable(false);
+                       value.setFont(Altos.status_font);
+                       value.setHorizontalAlignment(SwingConstants.CENTER);
+                       c.gridx = x; c.gridy = 1;
+                       layout.setConstraints(value, c);
+                       add(value);
+               }
+       }
+
+       class Call extends FlightValue {
+
+               String last_call = "";
+
+               boolean same_call(String call) {
+                       if (last_call == null)
+                               return call == null;
+                       else
+                               return last_call.equals(call);
+               }
+
+               void show(AltosState state, AltosListenerState listener_state) {
+                       if (!same_call(state.cal_data().callsign)) {
+                               show();
+                               value.setText(state.cal_data().callsign);
+                               if (state.cal_data().callsign == null)
+                                       setVisible(false);
+                               else
+                                       setVisible(true);
+                               last_call = state.cal_data().callsign;
+                       }
+               }
+
+               public void reset() {
+                       super.reset();
+                       last_call = "";
+               }
+
+               public Call (GridBagLayout layout, int x) {
+                       super (layout, x, "Callsign");
+               }
+       }
+
+       Call call;
+
+       class Serial extends FlightValue {
+
+               int     last_serial = -1;
+               void show(AltosState state, AltosListenerState listener_state) {
+                       AltosCalData    cal_data = state.cal_data();
+                       if (cal_data.serial != last_serial) {
+                               show();
+                               if (cal_data.serial == AltosLib.MISSING)
+                                       value.setText("none");
+                               else
+                                       value.setText(String.format("%d", cal_data.serial));
+                               last_serial = cal_data.serial;
+                       }
+               }
+
+               public void reset() {
+                       super.reset();
+                       last_serial = -1;
+               }
+
+               public Serial (GridBagLayout layout, int x) {
+                       super (layout, x, "Serial");
+               }
+       }
+
+       Serial serial;
+
+       class Flight extends FlightValue {
+
+               int     last_flight = -1;
+
+               void show(AltosState state, AltosListenerState listener_state) {
+                       AltosCalData cal_data = state.cal_data();
+                       if (cal_data.flight != last_flight) {
+                               show();
+                               if (cal_data.flight == AltosLib.MISSING)
+                                       value.setText("none");
+                               else
+                                       value.setText(String.format("%d", cal_data.flight));
+                               last_flight = cal_data.flight;
+                       }
+               }
+
+               public void reset() {
+                       super.reset();
+                       last_flight = -1;
+               }
+
+               public Flight (GridBagLayout layout, int x) {
+                       super (layout, x, "Flight");
+               }
+       }
+
+       Flight flight;
+
+       class FlightState extends FlightValue {
+
+               int     last_state = -1;
+
+               void show(AltosState state, AltosListenerState listener_state) {
+                       if (state.state() != last_state) {
+                               if (state.state() == AltosLib.ao_flight_stateless)
+                                       hide();
+                               else {
+                                       show();
+                                       value.setText(state.state_name());
+                               }
+                               last_state = state.state();
+                       }
+               }
+
+               public void reset() {
+                       super.reset();
+                       last_state = -1;
+               }
+
+               public FlightState (GridBagLayout layout, int x) {
+                       super (layout, x, "State");
+               }
+       }
+
+       FlightState flight_state;
+
+       class RSSI extends FlightValue {
+
+               int     last_rssi = 10000;
+
+               void show(AltosState state, AltosListenerState listener_state) {
+                       if (state.rssi() != last_rssi) {
+                               show();
+                               value.setText(String.format("%d", state.rssi()));
+                               if (state.rssi == AltosLib.MISSING)
+                                       setVisible(false);
+                               else
+                                       setVisible(true);
+                               last_rssi = state.rssi();
+                       }
+               }
+
+               public void reset() {
+                       super.reset();
+                       last_rssi = 10000;
+               }
+
+               public RSSI (GridBagLayout layout, int x) {
+                       super (layout, x, "RSSI");
+               }
+       }
+
+       RSSI rssi;
+
+       class LastPacket extends FlightValue {
+
+               long    last_secs = -1;
+
+               void show(AltosState state, AltosListenerState listener_state) {
+                       if (listener_state.running) {
+                               long secs = (System.currentTimeMillis() - state.received_time + 500) / 1000;
+                               if (secs != last_secs) {
+                                       value.setText(String.format("%d", secs));
+                                       last_secs = secs;
+                               }
+                       } else {
+                               value.setText("done");
+                       }
+               }
+
+               public void reset() {
+                       super.reset();
+                       last_secs = -1;
+               }
+
+               public LastPacket(GridBagLayout layout, int x) {
+                       super (layout, x, "Age");
+               }
+       }
+
+       LastPacket last_packet;
+
+       public void reset () {
+               call.reset();
+               serial.reset();
+               flight.reset();
+               flight_state.reset();
+               rssi.reset();
+               last_packet.reset();
+       }
+
+       public void font_size_changed(int font_size) {
+               call.set_font();
+               serial.set_font();
+               flight.set_font();
+               flight_state.set_font();
+               rssi.set_font();
+               last_packet.set_font();
+       }
+
+       public void units_changed(boolean imperial_units) {
+       }
+
+       public void show (AltosState state, AltosListenerState listener_state) {
+               call.show(state, listener_state);
+               serial.show(state, listener_state);
+               flight.show(state, listener_state);
+               flight_state.show(state, listener_state);
+               rssi.show(state, listener_state);
+               last_packet.show(state, listener_state);
+               if (!listener_state.running)
+                       stop();
+       }
+
+       public int height() {
+               Dimension d = layout.preferredLayoutSize(this);
+               return d.height;
+       }
+
+       public String getName() { return "Flight Status"; }
+
+       AltosFlightStatusUpdate status_update;
+       javax.swing.Timer       timer;
+
+       public void start(AltosFlightStatusUpdate status_update) {
+               this.status_update = status_update;
+               timer = new javax.swing.Timer(100, status_update);
+               timer.start();
+       }
+
+       public void stop() {
+               if (timer != null) {
+                       timer.stop();
+                       timer = null;
+               }
+       }
+
+       public AltosFlightStatus() {
+               layout = new GridBagLayout();
+
+               setLayout(layout);
+
+               call = new Call(layout, 0);
+               serial = new Serial(layout, 1);
+               flight = new Flight(layout, 2);
+               flight_state = new FlightState(layout, 3);
+               rssi = new RSSI(layout, 4);
+               last_packet = new LastPacket(layout, 5);
+       }
+}
diff --git a/teststand/AltosFlightStatusTableModel.java b/teststand/AltosFlightStatusTableModel.java
new file mode 100644 (file)
index 0000000..63008ad
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+/*
+package teststand;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.filechooser.FileNameExtensionFilter;
+import javax.swing.table.*;
+import java.io.*;
+import java.util.*;
+import java.text.*;
+import java.util.prefs.*;
+import java.util.concurrent.LinkedBlockingQueue;
+import org.altusmetrum.altoslib_12.*;
+
+public class AltosFlightStatusTableModel extends AbstractTableModel {
+       private String[] columnNames = {
+               String.format("Height (%s)", AltosConvert.show_distance_units()),
+               "State",
+               "RSSI (dBm)",
+               String.format("Speed (%s)", AltosConvert.show_speed_unit())
+       };
+       private Object[] data = { 0, "idle", 0, 0 };
+
+       public int getColumnCount() { return columnNames.length; }
+       public int getRowCount() { return 2; }
+       public Object getValueAt(int row, int col) {
+               if (row == 0)
+                       return columnNames[col];
+               return data[col];
+       }
+
+       public void setValueAt(Object value, int col) {
+               data[col] = value;
+               fireTableCellUpdated(1, col);
+       }
+
+       public void setValueAt(Object value, int row, int col) {
+               setValueAt(value, col);
+       }
+
+       public void set(AltosState state) {
+               setValueAt(String.format("%1.0f", AltosConvert.distance(state.height), 0);
+               setValueAt(state.data.state(), 1);
+               setValueAt(state.data.rssi, 2);
+               double speed = state.baro_speed;
+               if (state.ascent)
+                       speed = state.speed;
+               setValueAt(String.format("%1.0f", AltosConvert.speed(speed)), 3);
+       }
+}
+*/
diff --git a/teststand/AltosFlightStatusUpdate.java b/teststand/AltosFlightStatusUpdate.java
new file mode 100644 (file)
index 0000000..aa495c2
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * Copyright © 2012 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+import java.awt.event.*;
+import org.altusmetrum.altoslib_12.*;
+
+public class AltosFlightStatusUpdate implements ActionListener {
+
+       public AltosState               saved_state;
+       public AltosListenerState       saved_listener_state;
+       AltosFlightStatus               flightStatus;
+
+       public void actionPerformed (ActionEvent e) {
+               if (saved_state != null) {
+                       if (saved_listener_state == null)
+                               saved_listener_state = new AltosListenerState();
+                       flightStatus.show(saved_state, saved_listener_state);
+               }
+       }
+
+       public AltosFlightStatusUpdate (AltosFlightStatus in_flightStatus) {
+               flightStatus = in_flightStatus;
+       }
+}
+
diff --git a/teststand/AltosFlightUI.java b/teststand/AltosFlightUI.java
new file mode 100644 (file)
index 0000000..fa0814d
--- /dev/null
@@ -0,0 +1,333 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import java.util.*;
+import java.util.concurrent.*;
+import org.altusmetrum.altoslib_12.*;
+import org.altusmetrum.altosuilib_12.*;
+
+public class AltosFlightUI extends AltosUIFrame implements AltosFlightDisplay {
+       AltosVoice              voice;
+       AltosFlightReader       reader;
+       AltosDisplayThread      thread;
+
+       LinkedList<AltosFlightDisplay> displays;
+
+       JTabbedPane     pane;
+
+       AltosPad        pad;
+       AltosIgnitor    igniter;
+       AltosAscent     ascent;
+       AltosDescent    descent;
+       AltosLanded     landed;
+       AltosCompanionInfo      companion;
+       AltosUIMap      sitemap;
+       boolean         has_map;
+       boolean         has_companion;
+       boolean         has_state;
+       boolean         has_igniter;
+
+       private AltosFlightStatus flightStatus;
+       private AltosInfoTable flightInfo;
+
+       boolean exit_on_close = false;
+
+       JComponent cur_tab = null;
+       JComponent which_tab(AltosState state) {
+               if (state.state() < Altos.ao_flight_boost)
+                       return pad;
+               if (state.state() <= Altos.ao_flight_coast)
+                       return ascent;
+               if (state.state() <= Altos.ao_flight_main)
+                       return descent;
+               if (state.state() == AltosLib.ao_flight_stateless)
+                       return descent;
+               return landed;
+       }
+
+       void stop_display() {
+               if (thread != null && thread.isAlive()) {
+                       thread.interrupt();
+                       try {
+                               thread.join();
+                       } catch (InterruptedException ie) {}
+               }
+               thread = null;
+       }
+
+       void disconnect() {
+               stop_display();
+       }
+
+       public void reset() {
+               for (AltosFlightDisplay d : displays)
+                       d.reset();
+       }
+
+       public void font_size_changed(int font_size) {
+               for (AltosFlightDisplay d : displays)
+                       d.font_size_changed(font_size);
+       }
+
+       public void units_changed(boolean imperial_units) {
+               for (AltosFlightDisplay d : displays)
+                       d.units_changed(imperial_units);
+       }
+
+       AltosFlightStatusUpdate status_update;
+
+       public void show(AltosState state, AltosListenerState listener_state) {
+               status_update.saved_state = state;
+               status_update.saved_listener_state = listener_state;
+
+               if (state == null)
+                       state = new AltosState(new AltosCalData());
+
+               if (state.state() != Altos.ao_flight_startup) {
+                       if (!has_state) {
+                               pane.setTitleAt(0, "Launch Pad");
+                               pane.add(ascent, 1);
+                               pane.add(descent, 2);
+                               pane.add(landed, 3);
+                               has_state = true;
+                       }
+               }
+
+               JComponent tab = which_tab(state);
+               if (tab != cur_tab) {
+                       if (cur_tab == pane.getSelectedComponent())
+                               pane.setSelectedComponent(tab);
+                       cur_tab = tab;
+               }
+
+               if (igniter.should_show(state)) {
+                       if (!has_igniter) {
+                               pane.add("Ignitor", igniter);
+                               has_igniter = true;
+                       }
+               } else {
+                       if (has_igniter) {
+                               pane.remove(igniter);
+                               has_igniter = false;
+                       }
+               }
+
+               if (state.companion != null) {
+                       if (!has_companion) {
+                               pane.add("Companion", companion);
+                               has_companion= true;
+                       }
+               } else {
+                       if (has_companion) {
+                               pane.remove(companion);
+                               has_companion = false;
+                       }
+               }
+
+               if (state.gps != null) {
+                       if (!has_map) {
+                               pane.add("Site Map", sitemap);
+                               has_map = true;
+                       }
+               } else {
+                       if (has_map) {
+                               pane.remove(sitemap);
+                               has_map = false;
+                       }
+               }
+
+               for (AltosFlightDisplay d : displays) {
+                       try {
+                               d.show(state, listener_state);
+                       } catch (Exception e) {
+                               System.out.printf("Exception showing %s\n", d.getName());
+                               e.printStackTrace();
+                       }
+               }
+       }
+
+       public void set_exit_on_close() {
+               exit_on_close = true;
+       }
+
+       Container               bag;
+       AltosUIFreqList         frequencies;
+       AltosUIRateList         rates;
+       AltosUITelemetryList    telemetries;
+       JLabel                  telemetry;
+
+       ActionListener  show_timer;
+
+       public AltosFlightUI(AltosVoice in_voice, AltosFlightReader in_reader, final int serial) {
+               AltosUIPreferences.set_component(this);
+
+               displays = new LinkedList<AltosFlightDisplay>();
+
+               voice = in_voice;
+               reader = in_reader;
+
+               bag = getContentPane();
+               bag.setLayout(new GridBagLayout());
+
+               setTitle(String.format("AltOS %s", reader.name));
+
+               /* Stick channel selector at top of table for telemetry monitoring */
+               if (serial >= 0) {
+                       set_inset(3);
+
+                       // Frequency menu
+                       frequencies = new AltosUIFreqList(AltosUIPreferences.frequency(serial));
+                       frequencies.set_product("Monitor");
+                       frequencies.set_serial(serial);
+                       frequencies.addActionListener(new ActionListener() {
+                                       public void actionPerformed(ActionEvent e) {
+                                               double frequency = frequencies.frequency();
+                                               try {
+                                                       reader.set_frequency(frequency);
+                                               } catch (TimeoutException te) {
+                                               } catch (InterruptedException ie) {
+                                               }
+                                               reader.save_frequency();
+                                       }
+                       });
+                       bag.add (frequencies, constraints(0, 1));
+
+                       // Telemetry rate list
+                       rates = new AltosUIRateList(AltosUIPreferences.telemetry_rate(serial));
+                       rates.addActionListener(new ActionListener() {
+                                       public void actionPerformed(ActionEvent e) {
+                                               int rate = rates.rate();
+                                               try {
+                                                       reader.set_telemetry_rate(rate);
+                                               } catch (TimeoutException te) {
+                                               } catch (InterruptedException ie) {
+                                               }
+                                               reader.save_telemetry_rate();
+                                       }
+                               });
+                       rates.setEnabled(reader.supports_telemetry_rate(AltosLib.ao_telemetry_rate_2400));
+                       bag.add (rates, constraints(1, 1));
+
+                       // Telemetry format list
+                       if (reader.supports_telemetry(Altos.ao_telemetry_standard)) {
+                               telemetries = new AltosUITelemetryList(serial);
+                               telemetries.addActionListener(new ActionListener() {
+                                               public void actionPerformed(ActionEvent e) {
+                                                       int telemetry = telemetries.get_selected();
+                                                       reader.set_telemetry(telemetry);
+                                                       reader.save_telemetry();
+                                               }
+                                       });
+                               bag.add (telemetries, constraints(2, 1));
+                       } else {
+                               String  version;
+
+                               if (reader.supports_telemetry(Altos.ao_telemetry_0_9))
+                                       version = "Telemetry: 0.9";
+                               else if (reader.supports_telemetry(Altos.ao_telemetry_0_8))
+                                       version = "Telemetry: 0.8";
+                               else
+                                       version = "Telemetry: None";
+
+                               telemetry = new JLabel(version);
+                               bag.add (telemetry, constraints(2, 1));
+                       }
+                       next_row();
+               }
+               set_inset(0);
+
+               /* Flight status is always visible */
+               flightStatus = new AltosFlightStatus();
+               displays.add(flightStatus);
+               bag.add(flightStatus, constraints(0, 4, GridBagConstraints.HORIZONTAL));
+               next_row();
+
+               /* The rest of the window uses a tabbed pane to
+                * show one of the alternate data views
+                */
+               pane = new JTabbedPane();
+
+               pad = new AltosPad();
+               displays.add(pad);
+               pane.add("Status", pad);
+
+               igniter = new AltosIgnitor();
+               displays.add(igniter);
+               ascent = new AltosAscent();
+               displays.add(ascent);
+               descent = new AltosDescent();
+               displays.add(descent);
+               landed = new AltosLanded(reader);
+               displays.add(landed);
+
+               flightInfo = new AltosInfoTable();
+               displays.add(flightInfo);
+               pane.add("Table", new JScrollPane(flightInfo));
+
+               companion = new AltosCompanionInfo();
+               displays.add(companion);
+               has_companion = false;
+               has_state = false;
+
+               sitemap = new AltosUIMap();
+               displays.add(sitemap);
+               has_map = false;
+
+               /* Make the tabbed pane use the rest of the window space */
+               bag.add(pane, constraints(0, 4, GridBagConstraints.BOTH));
+
+               setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
+
+               AltosUIPreferences.register_font_listener(this);
+               AltosPreferences.register_units_listener(this);
+
+               status_update = new AltosFlightStatusUpdate(flightStatus);
+
+               flightStatus.start(status_update);
+
+               addWindowListener(new WindowAdapter() {
+                               @Override
+                               public void windowClosing(WindowEvent e) {
+                                       flightStatus.stop();
+                                       disconnect();
+                                       setVisible(false);
+                                       dispose();
+                                       AltosUIPreferences.unregister_font_listener(AltosFlightUI.this);
+                                       AltosPreferences.unregister_units_listener(AltosFlightUI.this);
+                                       if (exit_on_close)
+                                               System.exit(0);
+                               }
+                       });
+
+               pack();
+               setVisible(true);
+
+               thread = new AltosDisplayThread(this, voice, this, reader);
+
+               thread.start();
+       }
+
+       public AltosFlightUI (AltosVoice in_voice, AltosFlightReader in_reader) {
+               this(in_voice, in_reader, -1);
+       }
+}
diff --git a/teststand/AltosGraphUI.java b/teststand/AltosGraphUI.java
new file mode 100644 (file)
index 0000000..6cad982
--- /dev/null
@@ -0,0 +1,150 @@
+/*
+ * Copyright © 2010 Anthony Towns
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+import java.io.*;
+import java.util.ArrayList;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import org.altusmetrum.altoslib_12.*;
+import org.altusmetrum.altosuilib_12.*;
+
+import org.jfree.chart.ChartPanel;
+import org.jfree.chart.JFreeChart;
+import org.jfree.ui.RefineryUtilities;
+
+public class AltosGraphUI extends AltosUIFrame implements AltosFontListener, AltosUnitsListener, AltosFilterListener
+{
+       JTabbedPane             pane;
+       AltosGraph              graph;
+       AltosUIEnable           enable;
+       AltosUIMap              map;
+       AltosFlightStats        stats;
+       AltosFlightStatsTable   statsTable;
+       AltosGPS                gps;
+       boolean                 has_gps;
+
+       void fill_map(AltosFlightSeries flight_series) {
+               boolean                 any_gps = false;
+               AltosGPSTimeValue       gtv_last = null;
+
+               if (flight_series.gps_series != null) {
+                       for (AltosGPSTimeValue gtv : flight_series.gps_series) {
+                               AltosGPS gps = gtv.gps;
+                               if (gps != null &&
+                                   gps.locked &&
+                                   gps.nsat >= 4) {
+                                       if (map == null)
+                                               map = new AltosUIMap();
+                                       map.show(gps, (int) flight_series.value_before(AltosFlightSeries.state_name, gtv.time));
+                                       this.gps = gps;
+                                       gtv_last = gtv;
+                                       has_gps = true;
+                               }
+                       }
+               }
+               if (gtv_last != null) {
+                       int state = (int) flight_series.value_after(AltosFlightSeries.state_name, gtv_last.time);
+                       if (state == AltosLib.ao_flight_landed)
+                               map.show(gtv_last.gps, state);
+               }
+       }
+
+       public void font_size_changed(int font_size) {
+               if (map != null)
+                       map.font_size_changed(font_size);
+               if (statsTable != null)
+                       statsTable.font_size_changed(font_size);
+       }
+
+       public void units_changed(boolean imperial_units) {
+               if (map != null)
+                       map.units_changed(imperial_units);
+               if (enable != null)
+                       enable.units_changed(imperial_units);
+       }
+
+       AltosUIFlightSeries flight_series;
+
+       public void filter_changed(double speed_filter, double accel_filter) {
+               flight_series.set_filter(speed_filter, accel_filter);
+               graph.filter_changed();
+               stats = new AltosFlightStats(flight_series);
+               statsTable.filter_changed(stats);
+       }
+
+       public double speed_filter() {
+               return flight_series.speed_filter_width;
+       }
+
+       public double accel_filter() {
+               return flight_series.accel_filter_width;
+       }
+
+       AltosGraphUI(AltosRecordSet set, File file) throws InterruptedException, IOException {
+               super(file.getName());
+               AltosCalData    cal_data = set.cal_data();
+
+
+               pane = new JTabbedPane();
+
+               flight_series = new AltosUIFlightSeries(cal_data);
+
+               enable = new AltosUIEnable(this);
+
+               set.capture_series(flight_series);
+
+               flight_series.finish();
+
+               stats = new AltosFlightStats(flight_series);
+
+               graph = new AltosGraph(enable, stats, flight_series);
+
+               statsTable = new AltosFlightStatsTable(stats);
+
+               pane.add("Flight Graph", graph.panel);
+               pane.add("Configure Graph", enable);
+               pane.add("Flight Statistics", statsTable);
+
+               has_gps = false;
+               fill_map(flight_series);
+               if (has_gps)
+                       pane.add("Map", map);
+
+               setContentPane (pane);
+
+               AltosUIPreferences.register_font_listener(this);
+               AltosPreferences.register_units_listener(this);
+
+               addWindowListener(new WindowAdapter() {
+                               @Override
+                               public void windowClosing(WindowEvent e) {
+                                       AltosUIPreferences.unregister_font_listener(AltosGraphUI.this);
+                                       AltosPreferences.unregister_units_listener(AltosGraphUI.this);
+                               }
+                       });
+               pack();
+
+               setVisible(true);
+               if (gps != null)
+                       map.centre(gps);
+       }
+}
diff --git a/teststand/AltosIdleMonitorUI.java b/teststand/AltosIdleMonitorUI.java
new file mode 100644 (file)
index 0000000..a6c5fd6
--- /dev/null
@@ -0,0 +1,306 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import java.io.*;
+import java.util.concurrent.*;
+import java.util.Arrays;
+import org.altusmetrum.altoslib_12.*;
+import org.altusmetrum.altosuilib_12.*;
+
+public class AltosIdleMonitorUI extends AltosUIFrame implements AltosFlightDisplay, AltosIdleMonitorListener, DocumentListener {
+       AltosDevice             device;
+       JTabbedPane             pane;
+       AltosPad                pad;
+       AltosInfoTable          flightInfo;
+       AltosFlightStatus       flightStatus;
+       AltosIgnitor            igniter;
+       AltosIdleMonitor        thread;
+       AltosUIMap              sitemap;
+       int                     serial;
+       boolean                 remote;
+       boolean                 has_igniter;
+       boolean                 has_map;
+
+       void stop_display() {
+               if (thread != null) {
+                       try {
+                               thread.abort();
+                       } catch (InterruptedException ie) {
+                       }
+               }
+               thread = null;
+       }
+
+       void disconnect() {
+               stop_display();
+       }
+
+       public void reset() {
+               pad.reset();
+               flightInfo.clear();
+       }
+
+       public void font_size_changed(int font_size) {
+               pad.font_size_changed(font_size);
+               flightInfo.font_size_changed(font_size);
+       }
+
+       public void units_changed(boolean imperial_units) {
+               pad.units_changed(imperial_units);
+               flightInfo.units_changed(imperial_units);
+       }
+
+       AltosFlightStatusUpdate status_update;
+
+       public void show(AltosState state, AltosListenerState listener_state) {
+               status_update.saved_state = state;
+               if (igniter.should_show(state)) {
+                       if (!has_igniter) {
+                               pane.add("Ignitor", igniter);
+                               has_igniter = true;
+                       }
+               } else {
+                       if (has_igniter) {
+                               pane.remove(igniter);
+                               has_igniter = false;
+                       }
+               }
+               if (state.gps != null && state.gps.connected) {
+                       if (!has_map) {
+                               pane.add("Site Map", sitemap);
+                               has_map = true;
+                       }
+               } else {
+                       if (has_map) {
+                               pane.remove(sitemap);
+                               has_map = false;
+                       }
+               }
+
+//             try {
+                       pad.show(state, listener_state);
+                       flightStatus.show(state, listener_state);
+                       flightInfo.show(state, listener_state);
+                       if (has_igniter)
+                               igniter.show(state, listener_state);
+                       if (has_map)
+                               sitemap.show(state, listener_state);
+//             } catch (Exception e) {
+//                     System.out.print("Show exception " + e);
+//             }
+       }
+
+       public void update(final AltosState state, final AltosListenerState listener_state) {
+               Runnable r = new Runnable() {
+                               public void run() {
+                                       show(state, listener_state);
+                               }
+                       };
+               SwingUtilities.invokeLater(r);
+       }
+
+       public void failed() {
+               Runnable r = new Runnable() {
+                               public void run() {
+                                       close();
+                               }
+                       };
+               SwingUtilities.invokeLater(r);
+       }
+
+       public void error(final String reason) {
+               Runnable r = new Runnable() {
+                               public void run() {
+                                       disconnect();
+                                       JOptionPane.showMessageDialog(AltosIdleMonitorUI.this,
+                                                                     reason,
+                                                                     "Error fetching data",
+                                                                     JOptionPane.ERROR_MESSAGE);
+                               }
+                       };
+               SwingUtilities.invokeLater(r);
+       }
+
+       Container       bag;
+       AltosUIFreqList frequencies;
+       JTextField      callsign_value;
+
+       /* DocumentListener interface methods */
+       public void changedUpdate(DocumentEvent e) {
+               if (callsign_value != null) {
+                       String  callsign = callsign_value.getText();
+                       System.out.printf("callsign set to %s\n", callsign);
+                       thread.set_callsign(callsign);
+                       AltosUIPreferences.set_callsign(callsign);
+               }
+       }
+
+       public void insertUpdate(DocumentEvent e) {
+               changedUpdate(e);
+       }
+
+       public void removeUpdate(DocumentEvent e) {
+               changedUpdate(e);
+       }
+
+       void idle_exception(JFrame owner, Exception e) {
+               if (e instanceof FileNotFoundException) {
+                       JOptionPane.showMessageDialog(owner,
+                                                     ((FileNotFoundException) e).getMessage(),
+                                                     "Cannot open target device",
+                                                     JOptionPane.ERROR_MESSAGE);
+               } else if (e instanceof AltosSerialInUseException) {
+                       JOptionPane.showMessageDialog(owner,
+                                                     String.format("Device \"%s\" already in use",
+                                                                   device.toShortString()),
+                                                     "Device in use",
+                                                     JOptionPane.ERROR_MESSAGE);
+               } else if (e instanceof IOException) {
+                       IOException ee = (IOException) e;
+                       JOptionPane.showMessageDialog(owner,
+                                                     device.toShortString(),
+                                                     ee.getLocalizedMessage(),
+                                                     JOptionPane.ERROR_MESSAGE);
+               } else {
+                       JOptionPane.showMessageDialog(owner,
+                                                     String.format("Connection to \"%s\" failed",
+                                                                   device.toShortString()),
+                                                     "Connection Failed",
+                                                     JOptionPane.ERROR_MESSAGE);
+               }
+       }
+
+       private void close() {
+               try {
+                       disconnect();
+               } catch (Exception ex) {
+                       System.out.printf("Exception %s\n", ex.toString());
+                       for (StackTraceElement el : ex.getStackTrace())
+                               System.out.printf("%s\n", el.toString());
+               }
+               setVisible(false);
+               dispose();
+               AltosUIPreferences.unregister_font_listener(AltosIdleMonitorUI.this);
+       }
+
+       public AltosIdleMonitorUI(JFrame in_owner)
+               throws FileNotFoundException, TimeoutException, InterruptedException {
+
+               device = AltosDeviceUIDialog.show(in_owner, Altos.product_any);
+               remote = false;
+               if (!device.matchProduct(Altos.product_altimeter))
+                       remote = true;
+
+               serial = device.getSerial();
+
+               AltosSerial link;
+               try {
+                       link = new AltosSerial(device);
+               } catch (Exception ex) {
+                       idle_exception(in_owner, ex);
+                       return;
+               }
+               link.set_frame(this);
+
+               /* We let the user set the freq/callsign, so don't bother with the cancel dialog */
+               link.set_cancel_enable(false);
+
+               bag = getContentPane();
+               bag.setLayout(new GridBagLayout());
+
+               setTitle(String.format("AltOS %s", device.toShortString()));
+
+               /* Stick frequency selector at top of table for telemetry monitoring */
+               if (remote && serial >= 0) {
+                       set_inset(3);
+
+                       // Frequency menu
+                       frequencies = new AltosUIFreqList(AltosUIPreferences.frequency(serial));
+                       frequencies.addActionListener(new ActionListener() {
+                                       public void actionPerformed(ActionEvent e) {
+                                               double frequency = frequencies.frequency();
+                                               thread.set_frequency(frequency);
+                                               AltosUIPreferences.set_frequency(device.getSerial(),
+                                                                              frequency);
+                                       }
+                       });
+                       bag.add (frequencies, constraints(0, 1));
+                       bag.add (new JLabel("Callsign:"), constraints(1, 1));
+                       /* Add callsign configuration */
+                       callsign_value = new JTextField(AltosUIPreferences.callsign());
+                       callsign_value.getDocument().addDocumentListener(this);
+                       callsign_value.setToolTipText("Callsign sent in packet mode");
+                       bag.add(callsign_value, constraints(2, 1, GridBagConstraints.HORIZONTAL));
+                       next_row();
+               }
+
+               set_inset(0);
+
+               /* Flight status is always visible */
+               flightStatus = new AltosFlightStatus();
+               bag.add(flightStatus, constraints(0, 4, GridBagConstraints.HORIZONTAL));
+
+               next_row();
+
+               /* The rest of the window uses a tabbed pane to
+                * show one of the alternate data views
+                */
+               pane = new JTabbedPane();
+
+               pad = new AltosPad();
+               pane.add("Launch Pad", pad);
+
+               flightInfo = new AltosInfoTable();
+               pane.add("Table", new JScrollPane(flightInfo));
+
+               igniter = new AltosIgnitor();
+
+               sitemap = new AltosUIMap();
+
+               /* Make the tabbed pane use the rest of the window space */
+               bag.add(pane, constraints(0, 4, GridBagConstraints.BOTH));
+
+               setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
+
+               AltosUIPreferences.register_font_listener(this);
+
+               addWindowListener(new WindowAdapter() {
+                               @Override
+                               public void windowClosing(WindowEvent e) {
+                                       close();
+                               }
+                       });
+
+               pack();
+               setVisible(true);
+
+               thread = new AltosIdleMonitor(this, link, (boolean) remote);
+
+               status_update = new AltosFlightStatusUpdate(flightStatus);
+
+               new javax.swing.Timer(100, status_update).start();
+
+               thread.start();
+       }
+}
diff --git a/teststand/AltosIgniteUI.java b/teststand/AltosIgniteUI.java
new file mode 100644 (file)
index 0000000..b78f62b
--- /dev/null
@@ -0,0 +1,477 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import java.io.*;
+import java.text.*;
+import java.util.*;
+import java.util.concurrent.*;
+import org.altusmetrum.altoslib_12.*;
+import org.altusmetrum.altosuilib_12.*;
+
+public class AltosIgniteUI
+       extends AltosUIDialog
+       implements ActionListener
+{
+       AltosDevice     device;
+       JFrame          owner;
+       JLabel          label;
+       JToggleButton   arm;
+       JButton         fire;
+       javax.swing.Timer       timer;
+       JButton         close;
+       ButtonGroup     group;
+       Boolean         opened;
+
+       int             npyro;
+
+       final static int        timeout = 1 * 1000;
+
+       int             time_remaining;
+       boolean         timer_running;
+
+       int             poll_remaining;
+
+       LinkedBlockingQueue<String>     command_queue;
+
+       class Igniter {
+               JRadioButton    button;
+               JLabel          status_label;
+               String          name;
+               int             status;
+
+               void set_status (int status) {
+                       this.status = status;
+                       status_label.setText(String.format("\"%s\"", AltosIgnite.status_string(status)));
+               }
+
+               Igniter(AltosIgniteUI ui, String label, String name, int y) {
+                       Container               pane = getContentPane();
+                       GridBagConstraints      c = new GridBagConstraints();
+                       Insets                  i = new Insets(4,4,4,4);
+
+                       this.name = name;
+                       this.status = AltosIgnite.Unknown;
+
+                       c.gridx = 0;
+                       c.gridy = y;
+                       c.gridwidth = 1;
+                       c.anchor = GridBagConstraints.WEST;
+                       button = new JRadioButton (label);
+                       pane.add(button, c);
+                       button.addActionListener(ui);
+                       button.setActionCommand(name);
+                       group.add(button);
+
+                       c.gridx = 1;
+                       c.gridy = y;
+                       c.gridwidth = 1;
+                       c.anchor = GridBagConstraints.WEST;
+                       status_label = new JLabel("plenty of text");
+                       pane.add(status_label, c);
+
+                       status = AltosIgnite.Unknown;
+               }
+       }
+
+       Igniter igniters[];
+
+       void set_status(String _name, int _status) {
+
+               final String name = _name;
+               final int status = _status;
+               Runnable r = new Runnable() {
+                               public void run() {
+                                       for (int p = 0; p < igniters.length; p++)
+                                               if (name.equals(igniters[p].name))
+                                                       igniters[p].set_status(status);
+                               }
+                       };
+               SwingUtilities.invokeLater(r);
+       }
+
+       class IgniteHandler implements Runnable {
+               AltosIgnite     ignite;
+               JFrame          owner;
+               AltosLink       link;
+
+               void send_exception(Exception e) {
+                       final Exception f_e = e;
+                       Runnable r = new Runnable() {
+                                       public void run() {
+                                               ignite_exception(f_e);
+                                       }
+                               };
+                       SwingUtilities.invokeLater(r);
+               }
+
+               public void run () {
+                       try {
+                               ignite = new AltosIgnite(link,
+                                                        !device.matchProduct(Altos.product_altimeter));
+
+                       } catch (Exception e) {
+                               send_exception(e);
+                               return;
+                       }
+
+                       for (;;) {
+                               Runnable        r;
+
+                               try {
+                                       String          command = command_queue.take();
+                                       String          reply = null;
+
+                                       if (command.equals("get_status")) {
+                                               HashMap<String,Integer> status_map = ignite.status();
+
+                                               for (int p = 0; p < igniters.length; p++) {
+                                                       Integer i = status_map.get(igniters[p].name);
+                                                       if (i != null)
+                                                               set_status(igniters[p].name, i);
+                                               }
+                                               reply = "status";
+                                       } else if (command.equals("get_npyro")) {
+                                               reply = String.format("npyro %d", ignite.npyro());
+                                       } else if (command.equals("quit")) {
+                                               ignite.close();
+                                               break;
+                                       } else {
+                                               ignite.fire(command);
+                                               reply = "fired";
+                                       }
+                                       final String f_reply = reply;
+                                       r = new Runnable() {
+                                                       public void run() {
+                                                               ignite_reply(f_reply);
+                                                       }
+                                               };
+                                       SwingUtilities.invokeLater(r);
+                               } catch (Exception e) {
+                                       send_exception(e);
+                               }
+                       }
+               }
+
+               public IgniteHandler(JFrame in_owner, AltosLink in_link) {
+                       owner = in_owner;
+                       link = in_link;
+               }
+       }
+
+       void ignite_exception(Exception e) {
+               if (e instanceof FileNotFoundException) {
+                       JOptionPane.showMessageDialog(owner,
+                                                     ((FileNotFoundException) e).getMessage(),
+                                                     "Cannot open target device",
+                                                     JOptionPane.ERROR_MESSAGE);
+               } else if (e instanceof AltosSerialInUseException) {
+                       JOptionPane.showMessageDialog(owner,
+                                                     String.format("Device \"%s\" already in use",
+                                                                   device.toShortString()),
+                                                     "Device in use",
+                                                     JOptionPane.ERROR_MESSAGE);
+               } else if (e instanceof IOException) {
+                       IOException ee = (IOException) e;
+                       JOptionPane.showMessageDialog(owner,
+                                                     device.toShortString(),
+                                                     ee.getLocalizedMessage(),
+                                                     JOptionPane.ERROR_MESSAGE);
+               } else {
+                       JOptionPane.showMessageDialog(owner,
+                                                     String.format("Connection to \"%s\" failed",
+                                                                   device.toShortString()),
+                                                     "Connection Failed",
+                                                     JOptionPane.ERROR_MESSAGE);
+               }
+               close();
+       }
+
+       void ignite_reply(String reply) {
+               if (reply.equals("status")) {
+                       set_ignite_status();
+               } else if (reply.equals("fired")) {
+                       fired();
+               } else if (reply.startsWith("npyro")) {
+                       npyro = Integer.parseInt(reply.substring(6));
+                       if (npyro == AltosLib.MISSING)
+                               npyro = 0;
+                       make_ui();
+               }
+       }
+
+       void set_arm_text() {
+               if (arm.isSelected())
+                       arm.setText(String.format("%d", time_remaining));
+               else
+                       arm.setText("Arm");
+       }
+
+       void start_timer() {
+               time_remaining = 10;
+               set_arm_text();
+               timer_running = true;
+       }
+
+       void stop_timer() {
+               time_remaining = 0;
+               fire.setEnabled(false);
+               timer_running = false;
+               arm.setSelected(false);
+               arm.setEnabled(false);
+               set_arm_text();
+       }
+
+       void cancel () {
+               group.clearSelection();
+               fire.setEnabled(false);
+               stop_timer();
+       }
+
+       void send_command(String command) {
+               try {
+                       command_queue.put(command);
+               } catch (Exception ex) {
+                       ignite_exception(ex);
+               }
+       }
+
+       boolean getting_status = false;
+
+       void set_ignite_status() {
+               getting_status = false;
+               poll_remaining = 2;
+               if (!isVisible())
+                       setVisible(true);
+       }
+
+       void poll_ignite_status() {
+               if (poll_remaining > 0) {
+                       --poll_remaining;
+                       return;
+               }
+               if (!getting_status) {
+                       getting_status = true;
+                       send_command("get_status");
+               }
+       }
+
+       boolean firing = false;
+
+       void start_fire(String which) {
+               if (!firing) {
+                       firing = true;
+                       send_command(which);
+               }
+       }
+
+       void fired() {
+               firing = false;
+               cancel();
+       }
+
+       void close() {
+               if (opened) {
+                       send_command("quit");
+               }
+               if (timer != null)
+                       timer.stop();
+               setVisible(false);
+               dispose();
+       }
+
+       void tick_timer() {
+               if (timer_running) {
+                       --time_remaining;
+                       if (time_remaining <= 0)
+                               cancel();
+                       else
+                               set_arm_text();
+               }
+               poll_ignite_status();
+       }
+
+       void fire() {
+               if (arm.isEnabled() && arm.isSelected() && time_remaining > 0) {
+                       String  igniter = "none";
+
+                       for (int p = 0; p < igniters.length; p++)
+                               if (igniters[p].button.isSelected()) {
+                                       igniter = igniters[p].name;
+                                       break;
+                               }
+                       send_command(igniter);
+                       cancel();
+               }
+       }
+
+       public void actionPerformed(ActionEvent e) {
+               String cmd = e.getActionCommand();
+
+               for (int p = 0; p < igniters.length; p++)
+                       if (cmd.equals(igniters[p].name)) {
+                               stop_timer();
+                               arm.setEnabled(true);
+                               break;
+                       }
+
+               if (cmd.equals("arm")) {
+                       if (arm.isSelected()) {
+                               fire.setEnabled(true);
+                               start_timer();
+                       } else
+                               cancel();
+               }
+               if (cmd.equals("fire"))
+                       fire();
+               if (cmd.equals("tick"))
+                       tick_timer();
+               if (cmd.equals("close"))
+                       close();
+       }
+
+       /* A window listener to catch closing events and tell the config code */
+       class ConfigListener extends WindowAdapter {
+               AltosIgniteUI   ui;
+
+               public ConfigListener(AltosIgniteUI this_ui) {
+                       ui = this_ui;
+               }
+
+               public void windowClosing(WindowEvent e) {
+                       ui.actionPerformed(new ActionEvent(e.getSource(),
+                                                          ActionEvent.ACTION_PERFORMED,
+                                                          "close"));
+               }
+       }
+
+       private boolean open() {
+               command_queue = new LinkedBlockingQueue<String>();
+
+               opened = false;
+               device = AltosDeviceUIDialog.show(owner, Altos.product_any);
+               if (device != null) {
+                       try {
+                               AltosSerial     serial = new AltosSerial(device);
+                               serial.set_frame(owner);
+                               IgniteHandler   handler = new IgniteHandler(owner, serial);
+                               Thread          t = new Thread(handler);
+                               t.start();
+                               opened = true;
+                               return true;
+                       } catch (Exception ex) {
+                               ignite_exception(ex);
+                       }
+               }
+               return false;
+       }
+
+       private void make_ui() {
+               group = new ButtonGroup();
+
+               Container               pane = getContentPane();
+
+               GridBagConstraints      c = new GridBagConstraints();
+               Insets                  i = new Insets(4,4,4,4);
+
+               timer = new javax.swing.Timer(timeout, this);
+               timer.setActionCommand("tick");
+               timer_running = false;
+               timer.restart();
+
+               pane.setLayout(new GridBagLayout());
+
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.CENTER;
+               c.insets = i;
+               c.weightx = 0;
+               c.weighty = 0;
+
+               int y = 0;
+
+               c.gridx = 0;
+               c.gridy = y;
+               c.gridwidth = 2;
+               c.anchor = GridBagConstraints.CENTER;
+               label = new JLabel ("Fire Igniter");
+               pane.add(label, c);
+
+               y++;
+
+               igniters = new Igniter[2 + npyro];
+
+               igniters[0] = new Igniter(this, "Apogee", AltosIgnite.Apogee, y++);
+               igniters[1] = new Igniter(this, "Main", AltosIgnite.Main, y++);
+
+               for (int p = 0; p < npyro; p++) {
+                       String  name = String.format("%d", p);
+                       String  label = String.format("%c", 'A' + p);
+                       igniters[2+p] = new Igniter(this, label, name, y++);
+               }
+
+               c.gridx = 0;
+               c.gridy = y;
+               c.gridwidth = 1;
+               c.anchor = GridBagConstraints.CENTER;
+               arm = new JToggleButton ("Arm");
+               pane.add(arm, c);
+               arm.addActionListener(this);
+               arm.setActionCommand("arm");
+               arm.setEnabled(false);
+
+               c.gridx = 1;
+               c.gridy = y;
+               c.gridwidth = 1;
+               c.anchor = GridBagConstraints.CENTER;
+               fire = new JButton ("Fire");
+               fire.setEnabled(false);
+               pane.add(fire, c);
+               fire.addActionListener(this);
+               fire.setActionCommand("fire");
+
+               y++;
+
+               c.gridx = 0;
+               c.gridy = y;
+               c.gridwidth = 2;
+               c.anchor = GridBagConstraints.CENTER;
+               close = new JButton ("Close");
+               pane.add(close, c);
+               close.addActionListener(this);
+               close.setActionCommand("close");
+
+               pack();
+               setLocationRelativeTo(owner);
+
+               addWindowListener(new ConfigListener(this));
+       }
+
+       public AltosIgniteUI(JFrame in_owner) {
+
+               owner = in_owner;
+
+               if (!open())
+                       return;
+
+               send_command("get_npyro");
+       }
+}
diff --git a/teststand/AltosIgnitor.java b/teststand/AltosIgnitor.java
new file mode 100644 (file)
index 0000000..c7ba00d
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * Copyright © 2014 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import org.altusmetrum.altoslib_12.*;
+import org.altusmetrum.altosuilib_12.*;
+
+public class AltosIgnitor extends AltosUIFlightTab {
+
+       public class Ignitor extends AltosUIUnitsIndicator {
+               int             igniter;
+
+               public double value(AltosState state, int i) {
+                       if (state.igniter_voltage == null ||
+                           state.igniter_voltage.length < igniter)
+                               return AltosLib.MISSING;
+                       return state.igniter_voltage[igniter];
+               }
+
+               public double good() { return AltosLib.ao_igniter_good; }
+
+               public Ignitor (AltosUIFlightTab container, int y) {
+                       super(container, y, AltosConvert.voltage, String.format ("%s Voltage", AltosLib.igniter_name(y)), 1, true, 1);
+                       igniter = y;
+               }
+       }
+
+       Ignitor[] igniters;
+
+       public void show(AltosState state, AltosListenerState listener_state) {
+               if (isShowing())
+                       make_igniters(state);
+               super.show(state, listener_state);
+       }
+
+       public boolean should_show(AltosState state) {
+               if (state == null)
+                       return false;
+               if (state.igniter_voltage == null)
+                       return false;
+               return state.igniter_voltage.length > 0;
+       }
+
+       void make_igniters(AltosState state) {
+               int n = (state == null || state.igniter_voltage == null) ? 0 : state.igniter_voltage.length;
+               int old_n = igniters == null ? 0 : igniters.length;
+
+               if (n != old_n) {
+
+                       if (igniters != null) {
+                               for (int i = 0; i < igniters.length; i++) {
+                                       remove(igniters[i]);
+                                       igniters[i].remove(this);
+                                       igniters = null;
+                               }
+                       }
+
+                       if (n > 0) {
+                               setVisible(true);
+                               igniters = new Ignitor[n];
+                               for (int i = 0; i < n; i++) {
+                                       igniters[i] = new Ignitor(this, i);
+                                       add(igniters[i]);
+                               }
+                       } else
+                               setVisible(false);
+               }
+       }
+
+       public String getName() {
+               return "Ignitors";
+       }
+}
diff --git a/teststand/AltosLanded.java b/teststand/AltosLanded.java
new file mode 100644 (file)
index 0000000..c18c0e7
--- /dev/null
@@ -0,0 +1,193 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import java.io.*;
+import org.altusmetrum.altoslib_12.*;
+import org.altusmetrum.altosuilib_12.*;
+
+public class AltosLanded extends AltosUIFlightTab implements ActionListener {
+
+       class Bearing extends AltosUIIndicator {
+               public void show (AltosState state, AltosListenerState listener_state) {
+                       if (state.from_pad != null && state.from_pad.bearing != AltosLib.MISSING) {
+                               show( String.format("%3.0f°", state.from_pad.bearing),
+                                     state.from_pad.bearing_words(
+                                             AltosGreatCircle.BEARING_LONG));
+                       } else {
+                               show("Missing", "Missing");
+                       }
+               }
+               public Bearing (Container container, int y) {
+                       super (container, y, "Bearing", 2);
+               }
+       }
+
+       class Distance extends AltosUIUnitsIndicator {
+               public double value(AltosState state, int i) {
+                       if (state.from_pad != null)
+                               return state.from_pad.distance;
+                       else
+                               return AltosLib.MISSING;
+               }
+
+               public Distance(Container container, int y) {
+                       super(container, y, AltosConvert.distance, "Ground Distance", 2);
+               }
+       }
+
+       class Lat extends AltosUIUnitsIndicator {
+
+               public boolean hide (AltosState state, int i) { return state.gps == null || !state.gps.connected; }
+
+               public double value(AltosState state, int i) {
+                       if (state.gps == null)
+                               return AltosLib.MISSING;
+                       if (!state.gps.connected)
+                               return AltosLib.MISSING;
+                       return state.gps.lat;
+               }
+
+               public Lat (Container container, int y) {
+                       super (container, y, AltosConvert.latitude, "Latitude", 2);
+               }
+       }
+
+       class Lon extends AltosUIUnitsIndicator {
+               public boolean hide (AltosState state, int i) { return state.gps == null || !state.gps.connected; }
+
+               public double value(AltosState state, int i) {
+                       if (state.gps == null)
+                               return AltosLib.MISSING;
+                       if (!state.gps.connected)
+                               return AltosLib.MISSING;
+                       return state.gps.lon;
+               }
+
+               public Lon (Container container, int y) {
+                       super (container, y, AltosConvert.longitude, "Longitude", 2);
+               }
+       }
+
+       class MaxHeight extends AltosUIUnitsIndicator {
+               public double value(AltosState state, int i) { return state.max_height(); }
+
+               public MaxHeight (Container container, int y) {
+                       super (container, y, AltosConvert.height, "Maximum Height", 2);
+               }
+       }
+
+       class MaxSpeed extends AltosUIUnitsIndicator {
+               public double value(AltosState state, int i) { return state.max_speed(); }
+
+               public MaxSpeed (Container container, int y) {
+                       super (container, y, AltosConvert.speed, "Maximum Speed", 2);
+               }
+       }
+
+       class MaxAccel extends AltosUIUnitsIndicator {
+               public double value(AltosState state, int i) { return state.max_acceleration(); }
+
+               public MaxAccel (Container container, int y) {
+                       super (container, y, AltosConvert.speed, "Maximum acceleration", 2);
+               }
+       }
+
+       JButton graph;
+       AltosFlightReader reader;
+
+       public void actionPerformed(ActionEvent e) {
+               String  cmd = e.getActionCommand();
+
+               if (cmd.equals("graph")) {
+                       File    file = reader.backing_file();
+                       if (file != null) {
+                               String  filename = file.getName();
+                               try {
+                                       AltosRecordSet record_set = null;
+                                       FileInputStream in = new FileInputStream(file);
+                                       if (filename.endsWith("eeprom")) {
+                                               record_set = new AltosEepromRecordSet(in);
+                                       } else if (filename.endsWith("telem")) {
+                                               record_set = new AltosTelemetryFile(in);
+                                       } else {
+                                               throw new FileNotFoundException(filename);
+                                       }
+                                       try {
+                                               new AltosGraphUI(record_set, file);
+                                       } catch (InterruptedException ie) {
+                                       } catch (IOException ie) {
+                                       }
+                               } catch (FileNotFoundException fe) {
+                                       JOptionPane.showMessageDialog(null,
+                                                                     fe.getMessage(),
+                                                                     "Cannot open file",
+                                                                     JOptionPane.ERROR_MESSAGE);
+                               } catch (IOException ie) {
+                                       JOptionPane.showMessageDialog(null,
+                                                                     ie.getMessage(),
+                                                                     "Error reading file file",
+                                                                     JOptionPane.ERROR_MESSAGE);
+                               }
+                       }
+               }
+       }
+
+       public String getName() {
+               return "Landed";
+       }
+
+       public void show(AltosState state, AltosListenerState listener_state) {
+               super.show(state, listener_state);
+               if (reader.backing_file() != null)
+                       graph.setEnabled(true);
+       }
+
+       public AltosLanded(AltosFlightReader in_reader) {
+               reader = in_reader;
+
+               /* Elements in descent display */
+               add(new Bearing(this, 0));
+               add(new Distance(this, 1));
+               add(new Lat(this, 2));
+               add(new Lon(this, 3));
+               add(new MaxHeight(this, 4));
+               add(new MaxSpeed(this, 5));
+               add(new MaxAccel(this, 6));
+
+               graph = new JButton ("Graph Flight");
+               graph.setActionCommand("graph");
+               graph.addActionListener(this);
+               graph.setEnabled(false);
+
+               GridBagConstraints      c = new GridBagConstraints();
+
+               c.gridx = 1; c.gridy = 7;
+               c.insets = new Insets(10, 10, 10, 10);
+               c.anchor = GridBagConstraints.WEST;
+               c.weightx = 0;
+               c.weighty = 0;
+               c.fill = GridBagConstraints.VERTICAL;
+               add(graph, c);
+               addHierarchyListener(this);
+       }
+}
diff --git a/teststand/AltosLaunch.java b/teststand/AltosLaunch.java
new file mode 100644 (file)
index 0000000..c77086d
--- /dev/null
@@ -0,0 +1,198 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+import java.io.*;
+import java.util.concurrent.*;
+import java.awt.*;
+import org.altusmetrum.altosuilib_12.*;
+
+public class AltosLaunch {
+       AltosDevice     device;
+       AltosSerial     serial;
+       boolean         serial_started;
+       int             launcher_serial;
+       int             launcher_channel;
+       int             rssi;
+
+       final static int        Unknown = -1;
+       final static int        Good = 0;
+       final static int        Bad = 1;
+
+       int             armed;
+       int             igniter;
+
+       private void start_serial() throws InterruptedException {
+               serial_started = true;
+       }
+
+       private void stop_serial() throws InterruptedException {
+               if (!serial_started)
+                       return;
+               serial_started = false;
+               if (serial == null)
+                       return;
+       }
+
+       class string_ref {
+               String  value;
+
+               public String get() {
+                       return value;
+               }
+               public void set(String i) {
+                       value = i;
+               }
+               public string_ref() {
+                       value = null;
+               }
+       }
+
+       private boolean get_string(String line, String label, string_ref s) {
+               if (line.startsWith(label)) {
+                       String  quoted = line.substring(label.length()).trim();
+
+                       if (quoted.startsWith("\""))
+                               quoted = quoted.substring(1);
+                       if (quoted.endsWith("\""))
+                               quoted = quoted.substring(0,quoted.length()-1);
+                       s.set(quoted);
+                       return true;
+               } else {
+                       return false;
+               }
+       }
+
+       public boolean status() throws InterruptedException, TimeoutException {
+               boolean ok = false;
+               if (serial == null)
+                       return false;
+               string_ref status_name = new string_ref();
+               start_serial();
+               serial.printf("l %d %d\n", launcher_serial, launcher_channel);
+               for (;;) {
+                       String line = serial.get_reply(20000);
+                       if (line == null)
+                               throw new TimeoutException();
+                       if (get_string(line, "Rssi: ", status_name)) {
+                               try {
+                                       rssi = (int) Altos.fromdec(status_name.get());
+                               } catch (NumberFormatException ne) {
+                               }
+                               break;
+                       } else if (get_string(line, "Armed: ", status_name)) {
+                               armed = Good;
+                               String status = status_name.get();
+                               if (status.startsWith("igniter good"))
+                                       igniter = Good;
+                               else if (status.startsWith("igniter bad"))
+                                       igniter = Bad;
+                               else
+                                       igniter = Unknown;
+                               ok = true;
+                       } else if (get_string(line, "Disarmed: ", status_name)) {
+                               armed = Bad;
+                               if (status_name.get().startsWith("igniter good"))
+                                       igniter = Good;
+                               else if (status_name.get().startsWith("igniter bad"))
+                                       igniter = Bad;
+                               else
+                                       igniter = Unknown;
+                               ok = true;
+                       } else if (get_string(line, "Error ", status_name)) {
+                               armed = Unknown;
+                               igniter = Unknown;
+                               ok = false;
+                               break;
+                       }
+               }
+               stop_serial();
+               if (!ok) {
+                       armed = Unknown;
+                       igniter = Unknown;
+               }
+               return ok;
+       }
+
+       public static String status_string(int status) {
+               switch (status) {
+               case Good:
+                       return "good";
+               case Bad:
+                       return "open";
+               }
+               return "unknown";
+       }
+
+       public void arm() {
+               if (serial == null)
+                       return;
+               try {
+                       start_serial();
+                       serial.printf("a %d %d\n", launcher_serial, launcher_channel);
+                       serial.flush_output();
+               } catch (InterruptedException ie) {
+               } finally {
+                       try {
+                               stop_serial();
+                       } catch (InterruptedException ie) {
+                       }
+               }
+       }
+
+       public void fire() {
+               if (serial == null)
+                       return;
+               try {
+                       start_serial();
+                       serial.printf("i %d %d\n", launcher_serial, launcher_channel);
+                       serial.flush_output();
+               } catch (InterruptedException ie) {
+               } finally {
+                       try {
+                               stop_serial();
+                       } catch (InterruptedException ie) {
+                       }
+               }
+       }
+
+       public void close() {
+               try {
+                       stop_serial();
+               } catch (InterruptedException ie) {
+               }
+               serial.close();
+               serial = null;
+       }
+
+       public void set_frame(Frame frame) {
+               serial.set_frame(frame);
+       }
+
+       public void set_remote(int in_serial, int in_channel) {
+               launcher_serial = in_serial;
+               launcher_channel = in_channel;
+       }
+
+       public AltosLaunch(AltosDevice in_device) throws FileNotFoundException, AltosSerialInUseException {
+
+               device = in_device;
+               serial = new AltosSerial(device);
+       }
+}
diff --git a/teststand/AltosLaunchUI.java b/teststand/AltosLaunchUI.java
new file mode 100644 (file)
index 0000000..d038340
--- /dev/null
@@ -0,0 +1,513 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import java.io.*;
+import java.text.*;
+import java.util.concurrent.*;
+import org.altusmetrum.altosuilib_12.*;
+
+class FireButton extends JButton {
+       protected void processMouseEvent(MouseEvent e) {
+               super.processMouseEvent(e);
+               switch (e.getID()) {
+               case MouseEvent.MOUSE_PRESSED:
+                       if (actionListener != null)
+                               actionListener.actionPerformed(new ActionEvent(this, e.getID(), "fire_down"));
+                       break;
+               case MouseEvent.MOUSE_RELEASED:
+                       if (actionListener != null)
+                               actionListener.actionPerformed(new ActionEvent(this, e.getID(), "fire_up"));
+                       break;
+               }
+       }
+
+       public FireButton(String s) {
+               super(s);
+       }
+}
+
+public class AltosLaunchUI
+       extends AltosUIDialog
+       implements ActionListener
+{
+       AltosDevice     device;
+       JFrame          owner;
+       JLabel          label;
+
+       int             radio_channel;
+       JLabel          radio_channel_label;
+       JTextField      radio_channel_text;
+
+       int             launcher_serial;
+       JLabel          launcher_serial_label;
+       JTextField      launcher_serial_text;
+
+       int             launcher_channel;
+       JLabel          launcher_channel_label;
+       JTextField      launcher_channel_text;
+
+       JLabel          armed_label;
+       JLabel          armed_status_label;
+       JLabel          igniter;
+       JLabel          igniter_status_label;
+       JToggleButton   arm;
+       FireButton      fire;
+       javax.swing.Timer       arm_timer;
+       javax.swing.Timer       fire_timer;
+
+       boolean         firing;
+       boolean         armed;
+       int             armed_status;
+       int             igniter_status;
+       int             rssi;
+
+       final static int        arm_timeout = 1 * 1000;
+       final static int        fire_timeout = 250;
+
+       int             armed_count;
+
+       LinkedBlockingQueue<String>     command_queue;
+
+       class LaunchHandler implements Runnable {
+               AltosLaunch     launch;
+               JFrame          owner;
+
+               void send_exception(Exception e) {
+                       final Exception f_e = e;
+                       Runnable r = new Runnable() {
+                                       public void run() {
+                                               launch_exception(f_e);
+                                       }
+                               };
+                       SwingUtilities.invokeLater(r);
+               }
+
+               public void run () {
+                       try {
+                               launch = new AltosLaunch(device);
+                       } catch (Exception e) {
+                               send_exception(e);
+                               return;
+                       }
+                       launch.set_frame(owner);
+                       launch.set_remote(launcher_serial, launcher_channel);
+
+                       for (;;) {
+                               Runnable        r;
+
+                               try {
+                                       String          command = command_queue.take();
+                                       String          reply = null;
+
+                                       if (command.equals("get_status")) {
+                                               launch.status();
+                                               reply = "status";
+                                               armed_status = launch.armed;
+                                               igniter_status = launch.igniter;
+                                               rssi = launch.rssi;
+                                       } else if (command.equals("set_remote")) {
+                                               launch.set_remote(launcher_serial, launcher_channel);
+                                               reply = "remote set";
+                                       } else if (command.equals("arm")) {
+                                               launch.arm();
+                                               reply = "armed";
+                                       } else if (command.equals("fire")) {
+                                               launch.fire();
+                                               reply = "fired";
+                                       } else if (command.equals("quit")) {
+                                               launch.close();
+                                               break;
+                                       } else {
+                                               throw new ParseException(String.format("invalid command %s", command), 0);
+                                       }
+                                       final String f_reply = reply;
+                                       r = new Runnable() {
+                                                       public void run() {
+                                                               launch_reply(f_reply);
+                                                       }
+                                               };
+                                       SwingUtilities.invokeLater(r);
+                               } catch (Exception e) {
+                                       send_exception(e);
+                               }
+                       }
+               }
+
+               public LaunchHandler(JFrame in_owner) {
+                       owner = in_owner;
+               }
+       }
+
+       void launch_exception(Exception e) {
+               if (e instanceof FileNotFoundException) {
+                       JOptionPane.showMessageDialog(owner,
+                                                     ((FileNotFoundException) e).getMessage(),
+                                                     "Cannot open target device",
+                                                     JOptionPane.ERROR_MESSAGE);
+               } else if (e instanceof AltosSerialInUseException) {
+                       JOptionPane.showMessageDialog(owner,
+                                                     String.format("Device \"%s\" already in use",
+                                                                   device.toShortString()),
+                                                     "Device in use",
+                                                     JOptionPane.ERROR_MESSAGE);
+               } else if (e instanceof IOException) {
+                       IOException ee = (IOException) e;
+                       JOptionPane.showMessageDialog(owner,
+                                                     device.toShortString(),
+                                                     ee.getLocalizedMessage(),
+                                                     JOptionPane.ERROR_MESSAGE);
+               } else {
+                       JOptionPane.showMessageDialog(owner,
+                                                     String.format("Connection to \"%s\" failed",
+                                                                   device.toShortString()),
+                                                     "Connection Failed",
+                                                     JOptionPane.ERROR_MESSAGE);
+               }
+               close();
+       }
+
+       void launch_reply(String reply) {
+               if (reply == null)
+                       return;
+               if (reply.equals("remote set"))
+                       poll_launch_status();
+               if (reply.equals("status")) {
+                       set_launch_status();
+               }
+       }
+
+       void set_arm_text() {
+               if (arm.isSelected())
+                       arm.setText(String.format("%d", armed_count));
+               else
+                       arm.setText("Arm");
+       }
+
+       void start_arm_timer() {
+               armed_count = 30;
+               set_arm_text();
+       }
+
+       void stop_arm_timer() {
+               armed_count = 0;
+               armed = false;
+               arm.setSelected(false);
+               fire.setEnabled(false);
+               set_arm_text();
+       }
+
+       void cancel () {
+               fire.setEnabled(false);
+               firing = false;
+               stop_arm_timer();
+       }
+
+       void send_command(String command) {
+               try {
+                       command_queue.put(command);
+               } catch (Exception ex) {
+                       launch_exception(ex);
+               }
+       }
+
+       boolean getting_status = false;
+
+       void set_launch_status() {
+               getting_status = false;
+               armed_status_label.setText(String.format("\"%s\"", AltosLaunch.status_string(armed_status)));
+               igniter_status_label.setText(String.format("\"%s\"", AltosLaunch.status_string(igniter_status)));
+       }
+
+       void poll_launch_status() {
+               if (!getting_status && !firing && !armed) {
+                       getting_status = true;
+                       send_command("get_status");
+               }
+       }
+
+       void fired() {
+               firing = false;
+               cancel();
+       }
+
+       void close() {
+               send_command("quit");
+               arm_timer.stop();
+               setVisible(false);
+               dispose();
+       }
+
+       void tick_arm_timer() {
+               if (armed_count > 0) {
+                       --armed_count;
+                       if (armed_count <= 0) {
+                               armed_count = 0;
+                               cancel();
+                       } else {
+                               if (!firing) {
+                                       send_command("arm");
+                                       set_arm_text();
+                               }
+                       }
+               }
+               poll_launch_status();
+       }
+
+       void arm() {
+               if (arm.isSelected()) {
+                       fire.setEnabled(true);
+                       start_arm_timer();
+                       if (!firing)
+                               send_command("arm");
+                       armed = true;
+               } else
+                       cancel();
+       }
+
+       void fire_more() {
+               if (firing)
+                       send_command("fire");
+       }
+
+       void fire_down() {
+               if (arm.isEnabled() && arm.isSelected() && armed_count > 0) {
+                       firing = true;
+                       fire_more();
+                       fire_timer.restart();
+               }
+       }
+
+       void fire_up() {
+               firing = false;
+               fire_timer.stop();
+       }
+
+       void set_radio() {
+               try {
+                       radio_channel = Integer.parseInt(radio_channel_text.getText());
+               } catch (NumberFormatException ne) {
+                       radio_channel_text.setText(String.format("%d", radio_channel));
+               }
+       }
+
+       void set_serial() {
+               try {
+                       launcher_serial = Integer.parseInt(launcher_serial_text.getText());
+                       AltosUIPreferences.set_launcher_serial(launcher_serial);
+                       send_command("set_remote");
+               } catch (NumberFormatException ne) {
+                       launcher_serial_text.setText(String.format("%d", launcher_serial));
+               }
+       }
+
+       void set_channel() {
+               try {
+                       launcher_channel = Integer.parseInt(launcher_channel_text.getText());
+                       AltosUIPreferences.set_launcher_serial(launcher_channel);
+                       send_command("set_remote");
+               } catch (NumberFormatException ne) {
+                       launcher_channel_text.setText(String.format("%d", launcher_channel));
+               }
+       }
+
+       public void actionPerformed(ActionEvent e) {
+               String cmd = e.getActionCommand();
+               if (cmd.equals("armed") || cmd.equals("igniter")) {
+                       stop_arm_timer();
+               }
+
+               if (cmd.equals("arm"))
+                       arm();
+               if (cmd.equals("tick_arm"))
+                       tick_arm_timer();
+               if (cmd.equals("close"))
+                       close();
+               if (cmd.equals("fire_down"))
+                       fire_down();
+               if (cmd.equals("fire_up"))
+                       fire_up();
+               if (cmd.equals("tick_fire"))
+                       fire_more();
+               if (cmd.equals("new_serial"))
+                       set_serial();
+               if (cmd.equals("new_channel"))
+                       set_channel();
+       }
+
+       /* A window listener to catch closing events and tell the config code */
+       class ConfigListener extends WindowAdapter {
+               AltosLaunchUI   ui;
+
+               public ConfigListener(AltosLaunchUI this_ui) {
+                       ui = this_ui;
+               }
+
+               public void windowClosing(WindowEvent e) {
+                       ui.actionPerformed(new ActionEvent(e.getSource(),
+                                                          ActionEvent.ACTION_PERFORMED,
+                                                          "close"));
+               }
+       }
+
+       private boolean open() {
+               command_queue = new LinkedBlockingQueue<String>();
+
+               device = AltosDeviceUIDialog.show(owner, Altos.product_any);
+               if (device != null) {
+                               LaunchHandler   handler = new LaunchHandler(owner);
+                               Thread          t = new Thread(handler);
+                               t.start();
+                               return true;
+               }
+               return false;
+       }
+
+       public AltosLaunchUI(JFrame in_owner) {
+
+               launcher_channel = AltosUIPreferences.launcher_channel();
+               launcher_serial = AltosUIPreferences.launcher_serial();
+               owner = in_owner;
+               armed_status = AltosLaunch.Unknown;
+               igniter_status = AltosLaunch.Unknown;
+
+               if (!open())
+                       return;
+
+               Container               pane = getContentPane();
+               GridBagConstraints      c = new GridBagConstraints();
+               Insets                  i = new Insets(4,4,4,4);
+
+               arm_timer = new javax.swing.Timer(arm_timeout, this);
+               arm_timer.setActionCommand("tick_arm");
+               arm_timer.restart();
+
+               fire_timer = new javax.swing.Timer(fire_timeout, this);
+               fire_timer.setActionCommand("tick_fire");
+
+               owner = in_owner;
+
+               pane.setLayout(new GridBagLayout());
+
+               c.fill = GridBagConstraints.NONE;
+               c.anchor = GridBagConstraints.CENTER;
+               c.insets = i;
+               c.weightx = 1;
+               c.weighty = 1;
+
+               c.gridx = 0;
+               c.gridy = 0;
+               c.gridwidth = 2;
+               c.anchor = GridBagConstraints.CENTER;
+               label = new JLabel ("Launch Controller");
+               pane.add(label, c);
+
+               c.gridx = 0;
+               c.gridy = 1;
+               c.gridwidth = 1;
+               c.anchor = GridBagConstraints.WEST;
+               launcher_serial_label = new JLabel("Launcher Serial");
+               pane.add(launcher_serial_label, c);
+
+               c.gridx = 1;
+               c.gridy = 1;
+               c.gridwidth = 1;
+               c.anchor = GridBagConstraints.WEST;
+               launcher_serial_text = new JTextField(7);
+               launcher_serial_text.setText(String.format("%d", launcher_serial));
+               launcher_serial_text.setActionCommand("new_serial");
+               launcher_serial_text.addActionListener(this);
+               pane.add(launcher_serial_text, c);
+
+               c.gridx = 0;
+               c.gridy = 2;
+               c.gridwidth = 1;
+               c.anchor = GridBagConstraints.WEST;
+               launcher_channel_label = new JLabel("Launcher Channel");
+               pane.add(launcher_channel_label, c);
+
+               c.gridx = 1;
+               c.gridy = 2;
+               c.gridwidth = 1;
+               c.anchor = GridBagConstraints.WEST;
+               launcher_channel_text = new JTextField(7);
+               launcher_channel_text.setText(String.format("%d", launcher_channel));
+               launcher_channel_text.setActionCommand("new_channel");
+               launcher_channel_text.addActionListener(this);
+               pane.add(launcher_channel_text, c);
+
+               c.gridx = 0;
+               c.gridy = 3;
+               c.gridwidth = 1;
+               c.anchor = GridBagConstraints.WEST;
+               armed_label = new JLabel ("Armed");
+               pane.add(armed_label, c);
+
+               c.gridx = 1;
+               c.gridy = 3;
+               c.gridwidth = 1;
+               c.anchor = GridBagConstraints.WEST;
+               armed_status_label = new JLabel();
+               pane.add(armed_status_label, c);
+
+               c.gridx = 0;
+               c.gridy = 4;
+               c.gridwidth = 1;
+               c.anchor = GridBagConstraints.WEST;
+               igniter = new JLabel ("Igniter");
+               pane.add(igniter, c);
+
+               c.gridx = 1;
+               c.gridy = 4;
+               c.gridwidth = 1;
+               c.anchor = GridBagConstraints.WEST;
+               igniter_status_label = new JLabel();
+               pane.add(igniter_status_label, c);
+
+               c.gridx = 0;
+               c.gridy = 5;
+               c.gridwidth = 1;
+               c.anchor = GridBagConstraints.CENTER;
+               arm = new JToggleButton ("Arm");
+               pane.add(arm, c);
+               arm.addActionListener(this);
+               arm.setActionCommand("arm");
+               arm.setEnabled(true);
+
+               c.gridx = 1;
+               c.gridy = 5;
+               c.gridwidth = 1;
+               c.anchor = GridBagConstraints.CENTER;
+               fire = new FireButton ("Fire");
+               fire.setEnabled(false);
+               pane.add(fire, c);
+               fire.addActionListener(this);
+               fire.setActionCommand("fire");
+
+               pack();
+               setLocationRelativeTo(owner);
+
+               addWindowListener(new ConfigListener(this));
+
+               setVisible(true);
+       }
+}
diff --git a/teststand/AltosLib.jar b/teststand/AltosLib.jar
new file mode 120000 (symlink)
index 0000000..f0a4676
--- /dev/null
@@ -0,0 +1 @@
+../altoslib/AltosLib.jar
\ No newline at end of file
diff --git a/teststand/AltosPad.java b/teststand/AltosPad.java
new file mode 100644 (file)
index 0000000..cf27ad8
--- /dev/null
@@ -0,0 +1,253 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+import java.util.*;
+import org.altusmetrum.altoslib_12.*;
+import org.altusmetrum.altosuilib_12.*;
+
+public class AltosPad extends AltosUIFlightTab {
+
+       class Battery extends AltosUIVoltageIndicator {
+               public double voltage(AltosState state) { return state.battery_voltage; }
+               public double good() { return AltosLib.ao_battery_good; }
+               public Battery (AltosUIFlightTab container, int y) { super(container, y, "Battery Voltage", 2); }
+       }
+
+       class Apogee extends AltosUIVoltageIndicator {
+               public boolean hide(double v) { return v == AltosLib.MISSING; }
+               public double voltage(AltosState state) { return state.apogee_voltage; }
+               public double good() { return AltosLib.ao_igniter_good; }
+               public Apogee (AltosUIFlightTab container, int y) { super(container, y, "Apogee Igniter Voltage", 2); }
+       }
+
+       class Main extends AltosUIVoltageIndicator {
+               public boolean hide(double v) { return v == AltosLib.MISSING; }
+               public double voltage(AltosState state) { return state.main_voltage; }
+               public double good() { return AltosLib.ao_igniter_good; }
+               public Main (AltosUIFlightTab container, int y) { super(container, y, "Main Igniter Voltage", 2); }
+       }
+
+       class LoggingReady extends AltosUIIndicator {
+               public void show (AltosState state, AltosListenerState listener_state) {
+                       AltosCalData    cal_data = state.cal_data();
+                       if (state == null || cal_data.flight == AltosLib.MISSING) {
+                               hide();
+                       } else {
+                               if (cal_data.flight != 0) {
+                                       if (state.state() <= Altos.ao_flight_pad)
+                                               show("Ready to record");
+                                       else if (state.state() < Altos.ao_flight_landed ||
+                                                state.state() == AltosLib.ao_flight_stateless)
+                                               show("Recording data");
+                                       else
+                                               show("Recorded data");
+                               } else
+                                       show("Storage full");
+                               set_lights(cal_data.flight != 0);
+                       }
+               }
+               public LoggingReady (AltosUIFlightTab container, int y) {
+                       super(container, y, "On-board Data Logging", 1, true, 2);
+               }
+       }
+
+       class GPSLocked extends AltosUIIndicator {
+               public void show (AltosState state, AltosListenerState listener_state) {
+                       if (state == null || state.gps == null)
+                               hide();
+                       else {
+                               int     sol = state.gps.nsat;
+                               int     sat = state.gps.cc_gps_sat == null ? 0 : state.gps.cc_gps_sat.length;
+                               show("%d in solution", sol, "%d in view", sat);
+                               set_lights(state.gps.locked && sol >= 4);
+                       }
+               }
+               public GPSLocked (AltosUIFlightTab container, int y) {
+                       super (container, y, "GPS Locked", 2, true, 1);
+               }
+       }
+
+       class GPSReady extends AltosUIIndicator {
+               public void show (AltosState state, AltosListenerState listener_state) {
+                       if (state == null || state.gps == null)
+                               hide();
+                       else {
+                               if (state.gps_ready)
+                                       show("Ready");
+                               else
+                                       show("Waiting %d", state.gps_waiting);
+                               set_lights(state.gps_ready);
+                       }
+               }
+               public GPSReady (AltosUIFlightTab container, int y) {
+                       super (container, y, "GPS Ready", 1, true, 2);
+               }
+       }
+
+       class ReceiverBattery extends AltosUIVoltageIndicator {
+
+               double  last_voltage = AltosLib.MISSING;
+
+               public double voltage(AltosState state) {
+                       return last_voltage;
+               }
+
+               public double good() { return AltosLib.ao_battery_good; }
+
+               public boolean hide(AltosState state, AltosListenerState listener_state, int i) {
+                       return value(state, listener_state, i) == AltosLib.MISSING;
+               }
+
+               public double value(AltosState state, AltosListenerState listener_state, int i) {
+                       if (listener_state == null)
+                               last_voltage = AltosLib.MISSING;
+                       else
+                               last_voltage = listener_state.battery;
+                       return last_voltage;
+               }
+
+               public ReceiverBattery (AltosUIFlightTab container, int y) {
+                       super(container, y, "Receiver Battery", 2);
+               }
+       }
+
+       boolean report_pad(AltosState state) {
+               if (state.state() == AltosLib.ao_flight_stateless ||
+                   state.state() < AltosLib.ao_flight_pad)
+               {
+                       return false;
+               }
+               return true;
+       }
+
+       class PadLat extends AltosUIIndicator {
+
+               double  last_lat = AltosLib.MISSING - 1;
+
+               public void show (AltosState state, AltosListenerState listener_state) {
+                       double lat = AltosLib.MISSING;
+                       String label = null;
+
+                       if (state != null) {
+                               if (report_pad(state)) {
+                                       lat = state.pad_lat;
+                                       label = "Pad Latitude";
+                               } else if (state.gps != null) {
+                                       lat = state.gps.lat;
+                                       label = "Latitude";
+                               }
+                       }
+                       if (lat != last_lat) {
+                               if (lat != AltosLib.MISSING) {
+                                       show(AltosConvert.latitude.show(10, lat));
+                                       set_label(label);
+                               } else
+                                       hide();
+                               last_lat = lat;
+                       }
+               }
+
+               public void reset() {
+                       super.reset();
+                       last_lat = AltosLib.MISSING - 1;
+               }
+
+               public PadLat (AltosUIFlightTab container, int y) {
+                       super (container, y, "Pad Latitude", 1, false, 2);
+               }
+       }
+
+       class PadLon extends AltosUIIndicator {
+
+               double last_lon = AltosLib.MISSING - 1;
+
+               public void show (AltosState state, AltosListenerState listener_state) {
+                       double lon = AltosLib.MISSING;
+                       String label = null;
+
+                       if (state != null) {
+                               if (report_pad(state)) {
+                                       lon = state.pad_lon;
+                                       label = "Pad Longitude";
+                               } else if (state.gps != null) {
+                                       lon = state.gps.lon;
+                                       label = "Longitude";
+                               }
+                       }
+                       if (lon != last_lon) {
+                               if (lon != AltosLib.MISSING) {
+                                       show(AltosConvert.longitude.show(10, lon));
+                                       set_label(label);
+                               } else
+                                       hide();
+                               last_lon = lon;
+                       }
+               }
+
+               public void reset() {
+                       super.reset();
+                       last_lon = AltosLib.MISSING - 1;
+               }
+
+               public PadLon (AltosUIFlightTab container, int y) {
+                       super (container, y, "Pad Longitude", 1, false, 2);
+               }
+       }
+
+       class PadAlt extends AltosUIUnitsIndicator {
+
+               public double value(AltosState state, int i) {
+                       if (report_pad(state))
+                               return state.pad_alt;
+                       else if (state.gps != null)
+                               return state.gps.alt;
+                       else
+                               return state.altitude();
+               }
+
+               public void show (AltosState state, AltosListenerState listener_state) {
+                       String label = "Altitude";
+
+                       if (state != null && report_pad(state))
+                               label = "Pad Altitude";
+                       set_label(label);
+                       super.show(state, listener_state);
+               }
+
+               public PadAlt (AltosUIFlightTab container, int y) {
+                       super (container, y, AltosConvert.height, "Pad Altitude", 1, false, 2);
+               }
+       }
+       public String getName() { return "Pad"; }
+
+       public AltosPad() {
+               int y = 0;
+               add(new Battery(this, y++));
+               add(new ReceiverBattery(this, y++));
+               add(new Apogee(this, y++));
+               add(new Main(this, y++));
+               add(new LoggingReady(this, y++));
+               add(new GPSLocked(this, y++));
+               add(new GPSReady(this, y++));
+               add(new PadLat(this, y++));
+               add(new PadLon(this, y++));
+               add(new PadAlt(this, y++));
+       }
+}
diff --git a/teststand/AltosVersion.java b/teststand/AltosVersion.java
new file mode 100644 (file)
index 0000000..4ff1630
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+ * Copyright © 2011 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+public class AltosVersion {
+       public final static String version = "1.1.9.2";
+}
diff --git a/teststand/Info.plist.in b/teststand/Info.plist.in
new file mode 100644 (file)
index 0000000..8dc797d
--- /dev/null
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
+<plist version="0.9">
+<dict>
+       <key>CFBundleName</key>
+       <string>AltosUI</string>
+       <key>CFBundleVersion</key>
+       <string>@VERSION@</string>
+       <key>CFBundleAllowMixedLocalizations</key>
+       <string>true</string>
+       <key>CFBundleExecutable</key>
+       <string>JavaApplicationStub</string>
+       <key>CFBundleDevelopmentRegion</key>
+       <string>English</string>
+       <key>CFBundlePackageType</key>
+       <string>APPL</string>
+       <key>CFBundleIdentifier</key>
+       <string>org.altusmetrum.altosui</string>
+       <key>CFBundleSignature</key>
+       <string>Altu</string>
+       <key>CFBundleGetInfoString</key>
+       <string>AltOS UI version @VERSION@</string>
+       <key>CFBundleInfoDictionaryVersion</key>
+       <string>6.0</string>
+       <key>CFBundleIconFile</key>
+       <string>altusmetrum-altosui.icns</string>
+       <key>CFBundleDocumentTypes</key>
+       <array>
+         <dict>
+           <key>CFBundleTypeName</key>
+           <string>Telemetry</string>
+           <key>CFBundleTypeIconFile</key>
+           <string>application-vnd.altusmetrum.telemetry.icns</string>
+           <key>CFBundleTypeExtensions</key>
+           <array>
+             <string>telem</string>
+           </array>
+           <key>CFBundleTypeRole</key>
+           <string>Editor</string>
+         </dict>
+         <dict>
+           <key>CFBundleTypeName</key>
+           <string>Eeprom</string>
+           <key>CFBundleTypeIconFile</key>
+           <string>application-vnd.altusmetrum.eeprom.icns</string>
+           <key>CFBundleTypeExtensions</key>
+           <array>
+             <string>eeprom</string>
+           </array>
+           <key>CFBundleTypeRole</key>
+           <string>Editor</string>
+         </dict>
+       </array>
+       <key>Java</key>
+       <dict>
+               <key>MainClass</key>
+               <string>altosui.AltosUI</string>
+               <key>JVMVersion</key>
+               <string>1.5+</string>
+               <key>ClassPath</key>
+               <array>
+                       <string>$JAVAROOT/altosui.jar</string>
+                       <string>$JAVAROOT/freetts.jar</string>
+               </array>
+               <key>VMOptions</key>
+               <array>
+                 <string>-Xms512M</string>
+                 <string>-Xmx512M</string>
+                 <string>-Dosgi.clean=true</string>
+               </array>
+       </dict>
+</dict>
+</plist>
diff --git a/teststand/Instdrv/NSIS/Contrib/InstDrv/Example.nsi b/teststand/Instdrv/NSIS/Contrib/InstDrv/Example.nsi
new file mode 100644 (file)
index 0000000..3ed821e
--- /dev/null
@@ -0,0 +1,84 @@
+#\r
+# InstDrv Example, (c) 2003 Jan Kiszka (Jan Kiszka@web.de)\r
+#\r
+\r
+Name "InstDrv.dll test"\r
+\r
+OutFile "InstDrv-Test.exe"\r
+\r
+ShowInstDetails show\r
+\r
+ComponentText "InstDrv Plugin Usage Example"\r
+\r
+Page components\r
+Page instfiles\r
+\r
+Section "Install a Driver" InstDriver\r
+    InstDrv::InitDriverSetup /NOUNLOAD "{4d36e978-e325-11ce-bfc1-08002be10318}" "IrCOMM2k"\r
+    Pop $0\r
+    DetailPrint "InitDriverSetup: $0"\r
+\r
+    InstDrv::DeleteOemInfFiles /NOUNLOAD\r
+    Pop $0\r
+    DetailPrint "DeleteOemInfFiles: $0"\r
+    StrCmp $0 "00000000" PrintInfNames ContInst1\r
+\r
+  PrintInfNames:\r
+    Pop $0\r
+    DetailPrint "Deleted $0"\r
+    Pop $0\r
+    DetailPrint "Deleted $0"\r
+\r
+  ContInst1:\r
+    InstDrv::CreateDevice /NOUNLOAD\r
+    Pop $0\r
+    DetailPrint "CreateDevice: $0"\r
+\r
+    SetOutPath $TEMP\r
+    File "ircomm2k.inf"\r
+    File "ircomm2k.sys"\r
+\r
+    InstDrv::InstallDriver /NOUNLOAD "$TEMP\ircomm2k.inf"\r
+    Pop $0\r
+    DetailPrint "InstallDriver: $0"\r
+    StrCmp $0 "00000000" PrintReboot ContInst2\r
+\r
+  PrintReboot:\r
+    Pop $0\r
+    DetailPrint "Reboot: $0"\r
+\r
+  ContInst2:\r
+    InstDrv::CountDevices\r
+    Pop $0\r
+    DetailPrint "CountDevices: $0"\r
+SectionEnd\r
+\r
+Section "Uninstall the driver again" UninstDriver\r
+    InstDrv::InitDriverSetup /NOUNLOAD "{4d36e978-e325-11ce-bfc1-08002be10318}" "IrCOMM2k"\r
+    Pop $0\r
+    DetailPrint "InitDriverSetup: $0"\r
+\r
+    InstDrv::DeleteOemInfFiles /NOUNLOAD\r
+    Pop $0\r
+    DetailPrint "DeleteOemInfFiles: $0"\r
+    StrCmp $0 "00000000" PrintInfNames ContUninst1\r
+\r
+  PrintInfNames:\r
+    Pop $0\r
+    DetailPrint "Deleted $0"\r
+    Pop $0\r
+    DetailPrint "Deleted $0"\r
+\r
+  ContUninst1:\r
+    InstDrv::RemoveAllDevices\r
+    Pop $0\r
+    DetailPrint "RemoveAllDevices: $0"\r
+    StrCmp $0 "00000000" PrintReboot ContUninst2\r
+\r
+  PrintReboot:\r
+    Pop $0\r
+    DetailPrint "Reboot: $0"\r
+\r
+  ContUninst2:\r
+    Delete "$SYSDIR\system32\ircomm2k.sys"\r
+SectionEnd
\ No newline at end of file
diff --git a/teststand/Instdrv/NSIS/Contrib/InstDrv/InstDrv-Test.exe b/teststand/Instdrv/NSIS/Contrib/InstDrv/InstDrv-Test.exe
new file mode 100644 (file)
index 0000000..615bae1
Binary files /dev/null and b/teststand/Instdrv/NSIS/Contrib/InstDrv/InstDrv-Test.exe differ
diff --git a/teststand/Instdrv/NSIS/Contrib/InstDrv/InstDrv.c b/teststand/Instdrv/NSIS/Contrib/InstDrv/InstDrv.c
new file mode 100644 (file)
index 0000000..efe866e
--- /dev/null
@@ -0,0 +1,704 @@
+/*\r
+\r
+InstDrv.dll - Installs or Removes Device Drivers\r
+\r
+Copyright © 2003 Jan Kiszka (Jan.Kiszka@web.de)\r
+\r
+This software is provided 'as-is', without any express or implied\r
+warranty. In no event will the authors be held liable for any damages\r
+arising from the use of this software.\r
+\r
+Permission is granted to anyone to use this software for any purpose,\r
+including commercial applications, and to alter it and redistribute\r
+it freely, subject to the following restrictions:\r
+\r
+1. The origin of this software must not be misrepresented; \r
+   you must not claim that you wrote the original software.\r
+   If you use this software in a product, an acknowledgment in the\r
+   product documentation would be appreciated but is not required.\r
+2. Altered versions must be plainly marked as such,\r
+   and must not be misrepresented as being the original software.\r
+3. This notice may not be removed or altered from any distribution.\r
+\r
+*/\r
+\r
+\r
+#include <windows.h>\r
+#include <setupapi.h>\r
+#include <newdev.h>\r
+#include "../exdll/exdll.h"\r
+\r
+\r
+char    paramBuf[1024];\r
+GUID    devClass;\r
+char    hwIdBuf[1024];\r
+int     initialized = 0;\r
+\r
+\r
+\r
+void* memset(void* dst, int val, unsigned int len)\r
+{\r
+    while (len-- > 0)\r
+        *((char *)dst)++ = val;\r
+\r
+    return NULL;\r
+}\r
+\r
+\r
+\r
+void* memcpy(void* dst, const void* src, unsigned int len)\r
+{\r
+    while (len-- > 0)\r
+        *((char *)dst)++ = *((char *)src)++;\r
+\r
+    return NULL;\r
+}\r
+\r
+\r
+\r
+int HexCharToInt(char c)\r
+{\r
+    if ((c >= '0') && (c <= '9'))\r
+        return c - '0';\r
+    else if ((c >= 'a') && (c <= 'f'))\r
+        return c - 'a' + 10;\r
+    else if ((c >= 'A') && (c <= 'F'))\r
+        return c - 'A' + 10;\r
+    else\r
+        return -1;\r
+}\r
+\r
+\r
+\r
+BOOLEAN HexStringToUInt(char* str, int width, void* valBuf)\r
+{\r
+    int i, val;\r
+\r
+\r
+    for (i = width - 4; i >= 0; i -= 4)\r
+    {\r
+        val = HexCharToInt(*str++);\r
+        if (val < 0)\r
+            return FALSE;\r
+        *(unsigned int *)valBuf += val << i;\r
+    }\r
+\r
+    return TRUE;\r
+}\r
+\r
+\r
+\r
+BOOLEAN StringToGUID(char* guidStr, GUID* pGuid)\r
+{\r
+    int i;\r
+\r
+\r
+    memset(pGuid, 0, sizeof(GUID));\r
+\r
+    if (*guidStr++ != '{')\r
+        return FALSE;\r
+\r
+    if (!HexStringToUInt(guidStr, 32, &pGuid->Data1))\r
+        return FALSE;\r
+    guidStr += 8;\r
+\r
+    if (*guidStr++ != '-')\r
+        return FALSE;\r
+\r
+    if (!HexStringToUInt(guidStr, 16, &pGuid->Data2))\r
+        return FALSE;\r
+    guidStr += 4;\r
+\r
+    if (*guidStr++ != '-')\r
+        return FALSE;\r
+\r
+    if (!HexStringToUInt(guidStr, 16, &pGuid->Data3))\r
+        return FALSE;\r
+    guidStr += 4;\r
+\r
+    if (*guidStr++ != '-')\r
+        return FALSE;\r
+\r
+    for (i = 0; i < 2; i++)\r
+    {\r
+        if (!HexStringToUInt(guidStr, 8, &pGuid->Data4[i]))\r
+            return FALSE;\r
+        guidStr += 2;\r
+    }\r
+\r
+    if (*guidStr++ != '-')\r
+        return FALSE;\r
+\r
+    for (i = 2; i < 8; i++)\r
+    {\r
+        if (!HexStringToUInt(guidStr, 8, &pGuid->Data4[i]))\r
+            return FALSE;\r
+        guidStr += 2;\r
+    }\r
+\r
+    if (*guidStr++ != '}')\r
+        return FALSE;\r
+\r
+    return TRUE;\r
+}\r
+\r
+\r
+\r
+DWORD FindNextDevice(HDEVINFO devInfoSet, SP_DEVINFO_DATA* pDevInfoData, DWORD* pIndex)\r
+{\r
+    DWORD   buffersize = 0;\r
+    LPTSTR  buffer     = NULL;\r
+    DWORD   dataType;\r
+    DWORD   result;\r
+\r
+\r
+    while (1)\r
+    {\r
+        if (!SetupDiEnumDeviceInfo(devInfoSet, (*pIndex)++, pDevInfoData))\r
+        {\r
+            result = GetLastError();\r
+            break;\r
+        }\r
+\r
+      GetDeviceRegistryProperty:\r
+        if (!SetupDiGetDeviceRegistryProperty(devInfoSet, pDevInfoData, SPDRP_HARDWAREID,\r
+                                              &dataType, (PBYTE)buffer, buffersize,\r
+                                              &buffersize))\r
+        {\r
+            result = GetLastError();\r
+\r
+            if (result == ERROR_INSUFFICIENT_BUFFER)\r
+            {\r
+                if (buffer != NULL)\r
+                    LocalFree(buffer);\r
+\r
+                buffer = (LPTSTR)LocalAlloc(LPTR, buffersize);\r
+\r
+                if (buffer == NULL)\r
+                    break;\r
+\r
+                goto GetDeviceRegistryProperty;\r
+            }\r
+            else if (result == ERROR_INVALID_DATA)\r
+                continue;   // ignore invalid entries\r
+            else\r
+                break;      // break on other errors\r
+        }\r
+\r
+        if (lstrcmpi(buffer, hwIdBuf) == 0)\r
+        {\r
+            result  = 0;\r
+            break;\r
+        }\r
+    }\r
+\r
+    if (buffer != NULL)\r
+        LocalFree(buffer);\r
+\r
+    return result;\r
+}\r
+\r
+\r
+\r
+DWORD FindFirstDevice(HWND hwndParent, const GUID* pDevClass, const LPTSTR hwId,\r
+                      HDEVINFO* pDevInfoSet, SP_DEVINFO_DATA* pDevInfoData,\r
+                      DWORD *pIndex, DWORD flags)\r
+{\r
+    DWORD   result;\r
+\r
+\r
+    *pDevInfoSet = SetupDiGetClassDevs((GUID*)pDevClass, NULL, hwndParent, flags);\r
+    if (*pDevInfoSet == INVALID_HANDLE_VALUE)\r
+        return GetLastError();\r
+\r
+    pDevInfoData->cbSize = sizeof(SP_DEVINFO_DATA);\r
+    *pIndex = 0;\r
+\r
+    result = FindNextDevice(*pDevInfoSet, pDevInfoData, pIndex);\r
+\r
+    if (result != 0)\r
+        SetupDiDestroyDeviceInfoList(*pDevInfoSet);\r
+\r
+    return result;\r
+}\r
+\r
+\r
+\r
+/*\r
+ * InstDrv::InitDriverSetup devClass drvHWID\r
+ *\r
+ *  devClass    - GUID of the driver's device setup class\r
+ *  drvHWID     - Hardware ID of the supported device\r
+ *\r
+ * Return:\r
+ *  result      - error message, empty on success\r
+ */\r
+void __declspec(dllexport) InitDriverSetup(HWND hwndParent, int string_size, char *variables, stack_t **stacktop)\r
+{\r
+    EXDLL_INIT();\r
+\r
+    /* convert class GUID */\r
+    popstring(paramBuf);\r
+\r
+    if (!StringToGUID(paramBuf, &devClass))\r
+    {\r
+        popstring(paramBuf);\r
+        pushstring("Invalid GUID!");\r
+        return;\r
+    }\r
+\r
+    /* get hardware ID */\r
+    memset(hwIdBuf, 0, sizeof(hwIdBuf));\r
+    popstring(hwIdBuf);\r
+\r
+    initialized = 1;\r
+    pushstring("");\r
+}\r
+\r
+\r
+\r
+/*\r
+ * InstDrv::CountDevices\r
+ *\r
+ * Return:\r
+ *  result      - Number of installed devices the driver supports\r
+ */\r
+void __declspec(dllexport) CountDevices(HWND hwndParent, int string_size, char *variables, stack_t **stacktop)\r
+{\r
+    HDEVINFO            devInfoSet;\r
+    SP_DEVINFO_DATA     devInfoData;\r
+    int                 count = 0;\r
+    char                countBuf[16];\r
+    DWORD               index;\r
+    DWORD               result;\r
+\r
+\r
+    EXDLL_INIT();\r
+\r
+    if (!initialized)\r
+    {\r
+        pushstring("Fatal error!");\r
+        return;\r
+    }\r
+\r
+    result = FindFirstDevice(hwndParent, &devClass, hwIdBuf, &devInfoSet, &devInfoData,\r
+                             &index, DIGCF_PRESENT);\r
+    if (result != 0)\r
+    {\r
+        pushstring("0");\r
+        return;\r
+    }\r
+\r
+    do\r
+    {\r
+        count++;\r
+    } while (FindNextDevice(devInfoSet, &devInfoData, &index) == 0);\r
+\r
+    SetupDiDestroyDeviceInfoList(devInfoSet);\r
+\r
+    wsprintf(countBuf, "%d", count);\r
+    pushstring(countBuf);\r
+}\r
+\r
+\r
+\r
+/*\r
+ * InstDrv::CreateDevice\r
+ *\r
+ * Return:\r
+ *  result      - Windows error code\r
+ */\r
+void __declspec(dllexport) CreateDevice(HWND hwndParent, int string_size, char *variables, stack_t **stacktop)\r
+{\r
+    HDEVINFO            devInfoSet;\r
+    SP_DEVINFO_DATA     devInfoData;\r
+    DWORD               result = 0;\r
+    char                resultBuf[16];\r
+\r
+\r
+    EXDLL_INIT();\r
+\r
+    if (!initialized)\r
+    {\r
+        pushstring("Fatal error!");\r
+        return;\r
+    }\r
+\r
+    devInfoSet = SetupDiCreateDeviceInfoList(&devClass, hwndParent);\r
+    if (devInfoSet == INVALID_HANDLE_VALUE)\r
+    {\r
+        wsprintf(resultBuf, "%08X", GetLastError());\r
+        pushstring(resultBuf);\r
+        return;\r
+    }\r
+\r
+    devInfoData.cbSize = sizeof(SP_DEVINFO_DATA);\r
+    if (!SetupDiCreateDeviceInfo(devInfoSet, hwIdBuf, &devClass, NULL,\r
+                                 hwndParent, DICD_GENERATE_ID, &devInfoData))\r
+    {\r
+        result = GetLastError();\r
+        goto InstallCleanup;\r
+    }\r
+\r
+    if (!SetupDiSetDeviceRegistryProperty(devInfoSet, &devInfoData, SPDRP_HARDWAREID,\r
+                                          hwIdBuf, (lstrlen(hwIdBuf)+2)*sizeof(TCHAR))) \r
+    {\r
+        result = GetLastError();\r
+        goto InstallCleanup;\r
+    }\r
+\r
+    if (!SetupDiCallClassInstaller(DIF_REGISTERDEVICE, devInfoSet, &devInfoData))\r
+        result = GetLastError();\r
+\r
+  InstallCleanup:\r
+    SetupDiDestroyDeviceInfoList(devInfoSet);\r
+\r
+    wsprintf(resultBuf, "%08X", result);\r
+    pushstring(resultBuf);\r
+}\r
+\r
+\r
+\r
+/*\r
+ * InstDrv::InstallDriver infPath\r
+ *\r
+ * Return:\r
+ *  result      - Windows error code\r
+ *  reboot      - non-zero if reboot is required\r
+ */\r
+void __declspec(dllexport) InstallDriver(HWND hwndParent, int string_size, char *variables, stack_t **stacktop)\r
+{\r
+    char    resultBuf[16];\r
+    BOOL    reboot;\r
+\r
+\r
+    EXDLL_INIT();\r
+    popstring(paramBuf);\r
+\r
+    if (!initialized)\r
+    {\r
+        pushstring("Fatal error!");\r
+        return;\r
+    }\r
+\r
+    if (!UpdateDriverForPlugAndPlayDevices(hwndParent, hwIdBuf, paramBuf,\r
+                                           INSTALLFLAG_FORCE, &reboot))\r
+    {\r
+        wsprintf(resultBuf, "%08X", GetLastError());\r
+        pushstring(resultBuf);\r
+    }\r
+    else\r
+    {\r
+        wsprintf(resultBuf, "%d", reboot);\r
+        pushstring(resultBuf);\r
+        pushstring("00000000");\r
+    }\r
+}\r
+\r
+\r
+\r
+/*\r
+ * InstDrv::DeleteOemInfFiles\r
+ *\r
+ * Return:\r
+ *  result      - Windows error code\r
+ *  oeminf      - Path of the deleted devices setup file (oemXX.inf)\r
+ *  oempnf      - Path of the deleted devices setup file (oemXX.pnf)\r
+ */\r
+void __declspec(dllexport) DeleteOemInfFiles(HWND hwndParent, int string_size, char *variables, stack_t **stacktop)\r
+{\r
+    HDEVINFO                devInfo;\r
+    SP_DEVINFO_DATA         devInfoData;\r
+    SP_DRVINFO_DATA         drvInfoData;\r
+    SP_DRVINFO_DETAIL_DATA  drvInfoDetail;\r
+    DWORD                   index;\r
+    DWORD                   result;\r
+    char                    resultBuf[16];\r
+\r
+\r
+    if (!initialized)\r
+    {\r
+        pushstring("Fatal error!");\r
+        return;\r
+    }\r
+\r
+    result = FindFirstDevice(NULL, &devClass, hwIdBuf, &devInfo, &devInfoData, &index, 0);\r
+    if (result != 0)\r
+        goto Cleanup1;\r
+\r
+    if (!SetupDiBuildDriverInfoList(devInfo, &devInfoData, SPDIT_COMPATDRIVER))\r
+    {\r
+        result = GetLastError();\r
+        goto Cleanup2;\r
+    }\r
+\r
+    drvInfoData.cbSize = sizeof(SP_DRVINFO_DATA);\r
+    drvInfoDetail.cbSize = sizeof(SP_DRVINFO_DETAIL_DATA);\r
+\r
+    if (!SetupDiEnumDriverInfo(devInfo, &devInfoData, SPDIT_COMPATDRIVER, 0, &drvInfoData))\r
+    {\r
+        result = GetLastError();\r
+        goto Cleanup3;\r
+    }\r
+\r
+    if (!SetupDiGetDriverInfoDetail(devInfo, &devInfoData, &drvInfoData,\r
+                                    &drvInfoDetail, sizeof(drvInfoDetail), NULL))\r
+    {\r
+        result = GetLastError();\r
+\r
+        if (result != ERROR_INSUFFICIENT_BUFFER)\r
+            goto Cleanup3;\r
+\r
+        result = 0;\r
+    }\r
+\r
+    pushstring(drvInfoDetail.InfFileName);\r
+    if (!DeleteFile(drvInfoDetail.InfFileName))\r
+        result = GetLastError();\r
+    else\r
+    {\r
+        index = lstrlen(drvInfoDetail.InfFileName);\r
+        if (index > 3)\r
+        {\r
+            lstrcpy(drvInfoDetail.InfFileName+index-3, "pnf");\r
+            pushstring(drvInfoDetail.InfFileName);\r
+            if (!DeleteFile(drvInfoDetail.InfFileName))\r
+                result = GetLastError();\r
+        }\r
+    }\r
+\r
+  Cleanup3:\r
+    SetupDiDestroyDriverInfoList(devInfo, &devInfoData, SPDIT_COMPATDRIVER);\r
+\r
+  Cleanup2:\r
+    SetupDiDestroyDeviceInfoList(devInfo);\r
+\r
+  Cleanup1:\r
+    wsprintf(resultBuf, "%08X", result);\r
+    pushstring(resultBuf);\r
+}\r
+\r
+\r
+\r
+/*\r
+ * InstDrv::RemoveAllDevices\r
+ *\r
+ * Return:\r
+ *  result      - Windows error code\r
+ *  reboot      - non-zero if reboot is required\r
+ */\r
+void __declspec(dllexport) RemoveAllDevices(HWND hwndParent, int string_size, char *variables, stack_t **stacktop)\r
+{\r
+    HDEVINFO                devInfo;\r
+    SP_DEVINFO_DATA         devInfoData;\r
+    DWORD                   index;\r
+    DWORD                   result;\r
+    char                    resultBuf[16];\r
+    BOOL                    reboot = FALSE;\r
+    SP_DEVINSTALL_PARAMS    instParams;\r
+\r
+\r
+    EXDLL_INIT();\r
+\r
+    if (!initialized)\r
+    {\r
+        pushstring("Fatal error!");\r
+        return;\r
+    }\r
+\r
+    result = FindFirstDevice(NULL, &devClass, hwIdBuf, &devInfo, &devInfoData, &index, 0);\r
+    if (result != 0)\r
+        goto Cleanup1;\r
+\r
+    do\r
+    {\r
+        if (!SetupDiCallClassInstaller(DIF_REMOVE, devInfo, &devInfoData))\r
+        {\r
+            result = GetLastError();\r
+            break;\r
+        }\r
+\r
+        instParams.cbSize = sizeof(instParams);\r
+        if (!reboot &&\r
+            SetupDiGetDeviceInstallParams(devInfo, &devInfoData, &instParams) &&\r
+            ((instParams.Flags & (DI_NEEDRESTART|DI_NEEDREBOOT)) != 0))\r
+        {\r
+            reboot = TRUE;\r
+        }\r
+\r
+        result = FindNextDevice(devInfo, &devInfoData, &index);\r
+    } while (result == 0);\r
+\r
+    SetupDiDestroyDeviceInfoList(devInfo);\r
+\r
+  Cleanup1:\r
+    if ((result == 0) || (result == ERROR_NO_MORE_ITEMS))\r
+    {\r
+        wsprintf(resultBuf, "%d", reboot);\r
+        pushstring(resultBuf);\r
+        pushstring("00000000");\r
+    }\r
+    else\r
+    {\r
+        wsprintf(resultBuf, "%08X", result);\r
+        pushstring(resultBuf);\r
+    }\r
+}\r
+\r
+\r
+\r
+/*\r
+ * InstDrv::StartSystemService serviceName\r
+ *\r
+ * Return:\r
+ *  result      - Windows error code\r
+ */\r
+void __declspec(dllexport) StartSystemService(HWND hwndParent, int string_size, char *variables, stack_t **stacktop)\r
+{\r
+    SC_HANDLE       managerHndl;\r
+    SC_HANDLE       svcHndl;\r
+    SERVICE_STATUS  svcStatus;\r
+    DWORD           oldCheckPoint;\r
+    DWORD           result;\r
+    char            resultBuf[16];\r
+\r
+\r
+    EXDLL_INIT();\r
+    popstring(paramBuf);\r
+\r
+    managerHndl = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);\r
+    if (managerHndl == NULL)\r
+    {\r
+        result = GetLastError();\r
+        goto Cleanup1;\r
+    }\r
+\r
+    svcHndl = OpenService(managerHndl, paramBuf, SERVICE_START | SERVICE_QUERY_STATUS);\r
+    if (svcHndl == NULL)\r
+    {\r
+        result = GetLastError();\r
+        goto Cleanup2;\r
+    }\r
+\r
+    if (!StartService(svcHndl, 0, NULL) || !QueryServiceStatus(svcHndl, &svcStatus))\r
+    {\r
+        result = GetLastError();\r
+        goto Cleanup3;\r
+    }\r
+\r
+    while (svcStatus.dwCurrentState == SERVICE_START_PENDING)\r
+    {\r
+        oldCheckPoint = svcStatus.dwCheckPoint;\r
+\r
+        Sleep(svcStatus.dwWaitHint);\r
+\r
+        if (!QueryServiceStatus(svcHndl, &svcStatus))\r
+        {\r
+            result = GetLastError();\r
+            break;\r
+        }\r
+\r
+        if (oldCheckPoint >= svcStatus.dwCheckPoint)\r
+        {\r
+            if ((svcStatus.dwCurrentState == SERVICE_STOPPED) &&\r
+                (svcStatus.dwWin32ExitCode != 0))\r
+                result = svcStatus.dwWin32ExitCode;\r
+            else\r
+                result = ERROR_SERVICE_REQUEST_TIMEOUT;\r
+        }\r
+    }\r
+\r
+    if (svcStatus.dwCurrentState == SERVICE_RUNNING)\r
+        result = 0;\r
+\r
+  Cleanup3:\r
+    CloseServiceHandle(svcHndl);\r
+\r
+  Cleanup2:\r
+    CloseServiceHandle(managerHndl);\r
+\r
+  Cleanup1:\r
+    wsprintf(resultBuf, "%08X", result);\r
+    pushstring(resultBuf);\r
+}\r
+\r
+\r
+\r
+/*\r
+ * InstDrv::StopSystemService serviceName\r
+ *\r
+ * Return:\r
+ *  result      - Windows error code\r
+ */\r
+void __declspec(dllexport) StopSystemService(HWND hwndParent, int string_size, char *variables, stack_t **stacktop)\r
+{\r
+    SC_HANDLE       managerHndl;\r
+    SC_HANDLE       svcHndl;\r
+    SERVICE_STATUS  svcStatus;\r
+    DWORD           oldCheckPoint;\r
+    DWORD           result;\r
+    char            resultBuf[16];\r
+\r
+\r
+    EXDLL_INIT();\r
+    popstring(paramBuf);\r
+\r
+    managerHndl = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);\r
+    if (managerHndl == NULL)\r
+    {\r
+        result = GetLastError();\r
+        goto Cleanup1;\r
+    }\r
+\r
+    svcHndl = OpenService(managerHndl, paramBuf, SERVICE_STOP | SERVICE_QUERY_STATUS);\r
+    if (svcHndl == NULL)\r
+    {\r
+        result = GetLastError();\r
+        goto Cleanup2;\r
+    }\r
+\r
+    if (!ControlService(svcHndl, SERVICE_CONTROL_STOP, &svcStatus))\r
+    {\r
+        result = GetLastError();\r
+        goto Cleanup3;\r
+    }\r
+\r
+    while (svcStatus.dwCurrentState == SERVICE_STOP_PENDING)\r
+    {\r
+        oldCheckPoint = svcStatus.dwCheckPoint;\r
+\r
+        Sleep(svcStatus.dwWaitHint);\r
+\r
+        if (!QueryServiceStatus(svcHndl, &svcStatus))\r
+        {\r
+            result = GetLastError();\r
+            break;\r
+        }\r
+\r
+        if (oldCheckPoint >= svcStatus.dwCheckPoint)\r
+        {\r
+            result = ERROR_SERVICE_REQUEST_TIMEOUT;\r
+            break;\r
+        }\r
+    }\r
+\r
+    if (svcStatus.dwCurrentState == SERVICE_STOPPED)\r
+        result = 0;\r
+\r
+  Cleanup3:\r
+    CloseServiceHandle(svcHndl);\r
+\r
+  Cleanup2:\r
+    CloseServiceHandle(managerHndl);\r
+\r
+  Cleanup1:\r
+    wsprintf(resultBuf, "%08X", result);\r
+    pushstring(resultBuf);\r
+}\r
+\r
+\r
+\r
+BOOL WINAPI _DllMainCRTStartup(HANDLE hInst, ULONG ul_reason_for_call, LPVOID lpReserved)\r
+{\r
+    return TRUE;\r
+}\r
diff --git a/teststand/Instdrv/NSIS/Contrib/InstDrv/InstDrv.dsp b/teststand/Instdrv/NSIS/Contrib/InstDrv/InstDrv.dsp
new file mode 100644 (file)
index 0000000..874e66c
--- /dev/null
@@ -0,0 +1,110 @@
+# Microsoft Developer Studio Project File - Name="InstDrv" - Package Owner=<4>\r
+# Microsoft Developer Studio Generated Build File, Format Version 6.00\r
+# ** NICHT BEARBEITEN **\r
+\r
+# TARGTYPE "Win32 (x86) Dynamic-Link Library" 0x0102\r
+\r
+CFG=InstDrv - Win32 Debug\r
+!MESSAGE Dies ist kein gültiges Makefile. Zum Erstellen dieses Projekts mit NMAKE\r
+!MESSAGE verwenden Sie den Befehl "Makefile exportieren" und führen Sie den Befehl\r
+!MESSAGE \r
+!MESSAGE NMAKE /f "InstDrv.mak".\r
+!MESSAGE \r
+!MESSAGE Sie können beim Ausführen von NMAKE eine Konfiguration angeben\r
+!MESSAGE durch Definieren des Makros CFG in der Befehlszeile. Zum Beispiel:\r
+!MESSAGE \r
+!MESSAGE NMAKE /f "InstDrv.mak" CFG="InstDrv - Win32 Debug"\r
+!MESSAGE \r
+!MESSAGE Für die Konfiguration stehen zur Auswahl:\r
+!MESSAGE \r
+!MESSAGE "InstDrv - Win32 Release" (basierend auf  "Win32 (x86) Dynamic-Link Library")\r
+!MESSAGE "InstDrv - Win32 Debug" (basierend auf  "Win32 (x86) Dynamic-Link Library")\r
+!MESSAGE \r
+\r
+# Begin Project\r
+# PROP AllowPerConfigDependencies 0\r
+# PROP Scc_ProjName ""\r
+# PROP Scc_LocalPath ""\r
+CPP=cl.exe\r
+MTL=midl.exe\r
+RSC=rc.exe\r
+\r
+!IF  "$(CFG)" == "InstDrv - Win32 Release"\r
+\r
+# PROP BASE Use_MFC 0\r
+# PROP BASE Use_Debug_Libraries 0\r
+# PROP BASE Output_Dir "Release"\r
+# PROP BASE Intermediate_Dir "Release"\r
+# PROP BASE Target_Dir ""\r
+# PROP Use_MFC 0\r
+# PROP Use_Debug_Libraries 0\r
+# PROP Output_Dir "Release"\r
+# PROP Intermediate_Dir "Release"\r
+# PROP Ignore_Export_Lib 1\r
+# PROP Target_Dir ""\r
+# ADD BASE CPP /nologo /MT /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "INSTDRV_EXPORTS" /YX /FD /c\r
+# ADD CPP /nologo /MT /W3 /GX /O1 /I "C:\Programme\WINDDK\3790\inc\ddk\w2k" /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "INSTDRV_EXPORTS" /FD /c\r
+# SUBTRACT CPP /YX\r
+# ADD BASE MTL /nologo /D "NDEBUG" /mktyplib203 /win32\r
+# ADD MTL /nologo /D "NDEBUG" /mktyplib203 /win32\r
+# ADD BASE RSC /l 0x407 /d "NDEBUG"\r
+# ADD RSC /l 0x407 /d "NDEBUG"\r
+BSC32=bscmake.exe\r
+# ADD BASE BSC32 /nologo\r
+# ADD BSC32 /nologo\r
+LINK32=link.exe\r
+# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /machine:I386\r
+# ADD LINK32 kernel32.lib user32.lib gdi32.lib comdlg32.lib advapi32.lib shell32.lib setupapi.lib newdev.lib /nologo /entry:"_DllMainCRTStartup" /dll /machine:I386 /nodefaultlib /out:"../../Plugins/InstDrv.dll" /libpath:"C:\Programme\WINDDK\3790\lib\w2k\i386" /opt:nowin98\r
+# SUBTRACT LINK32 /pdb:none\r
+\r
+!ELSEIF  "$(CFG)" == "InstDrv - Win32 Debug"\r
+\r
+# PROP BASE Use_MFC 0\r
+# PROP BASE Use_Debug_Libraries 1\r
+# PROP BASE Output_Dir "Debug"\r
+# PROP BASE Intermediate_Dir "Debug"\r
+# PROP BASE Target_Dir ""\r
+# PROP Use_MFC 0\r
+# PROP Use_Debug_Libraries 1\r
+# PROP Output_Dir "Debug"\r
+# PROP Intermediate_Dir "Debug"\r
+# PROP Ignore_Export_Lib 0\r
+# PROP Target_Dir ""\r
+# ADD BASE CPP /nologo /MTd /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "INSTDRV_EXPORTS" /YX /FD /GZ  /c\r
+# ADD CPP /nologo /MTd /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "INSTDRV_EXPORTS" /YX /FD /GZ  /c\r
+# ADD BASE MTL /nologo /D "_DEBUG" /mktyplib203 /win32\r
+# ADD MTL /nologo /D "_DEBUG" /mktyplib203 /win32\r
+# ADD BASE RSC /l 0x407 /d "_DEBUG"\r
+# ADD RSC /l 0x407 /d "_DEBUG"\r
+BSC32=bscmake.exe\r
+# ADD BASE BSC32 /nologo\r
+# ADD BSC32 /nologo\r
+LINK32=link.exe\r
+# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /debug /machine:I386 /pdbtype:sept\r
+# ADD LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /entry:"_DllMainCRTStartup" /dll /debug /machine:I386 /pdbtype:sept\r
+# SUBTRACT LINK32 /nodefaultlib\r
+\r
+!ENDIF \r
+\r
+# Begin Target\r
+\r
+# Name "InstDrv - Win32 Release"\r
+# Name "InstDrv - Win32 Debug"\r
+# Begin Group "Quellcodedateien"\r
+\r
+# PROP Default_Filter "cpp;c;cxx;rc;def;r;odl;idl;hpj;bat"\r
+# Begin Source File\r
+\r
+SOURCE=.\InstDrv.c\r
+# End Source File\r
+# End Group\r
+# Begin Group "Header-Dateien"\r
+\r
+# PROP Default_Filter "h;hpp;hxx;hm;inl"\r
+# End Group\r
+# Begin Group "Ressourcendateien"\r
+\r
+# PROP Default_Filter "ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe"\r
+# End Group\r
+# End Target\r
+# End Project\r
diff --git a/teststand/Instdrv/NSIS/Contrib/InstDrv/InstDrv.dsw b/teststand/Instdrv/NSIS/Contrib/InstDrv/InstDrv.dsw
new file mode 100644 (file)
index 0000000..b3d02f0
--- /dev/null
@@ -0,0 +1,29 @@
+Microsoft Developer Studio Workspace File, Format Version 6.00\r
+# WARNUNG: DIESE ARBEITSBEREICHSDATEI DARF NICHT BEARBEITET ODER GELÖSCHT WERDEN!\r
+\r
+###############################################################################\r
+\r
+Project: "InstDrv"=.\InstDrv.dsp - Package Owner=<4>\r
+\r
+Package=<5>\r
+{{{\r
+}}}\r
+\r
+Package=<4>\r
+{{{\r
+}}}\r
+\r
+###############################################################################\r
+\r
+Global:\r
+\r
+Package=<5>\r
+{{{\r
+}}}\r
+\r
+Package=<3>\r
+{{{\r
+}}}\r
+\r
+###############################################################################\r
+\r
diff --git a/teststand/Instdrv/NSIS/Contrib/InstDrv/Readme.txt b/teststand/Instdrv/NSIS/Contrib/InstDrv/Readme.txt
new file mode 100644 (file)
index 0000000..e5877aa
--- /dev/null
@@ -0,0 +1,141 @@
+InstDrv.dll version 0.2 - Installs or Removes Device Drivers\r
+------------------------------------------------------------\r
+\r
+\r
+The plugin helps you to create NSIS scripts for installing device drivers or\r
+removing them again. It can count installed device instances, create new ones\r
+or delete all supported device. InstDrv works on Windows 2000 or later.\r
+\r
+\r
+\r
+InstDrv::InitDriverSetup devClass drvHWID\r
+Return: result\r
+\r
+To start processing a driver, first call this function. devClass is the GUID\r
+of the device class the driver supports, drvHWID is the device hardware ID. If\r
+you don't know what these terms mean, you may want to take a look at the\r
+Windows DDK. This function returns an empty string on success, otherwise an\r
+error message.\r
+\r
+InitDriverSetup has to be called every time after the plugin dll has been\r
+(re-)loaded, or if you want to switch to a different driver.\r
+\r
+\r
+\r
+InstDrv::CountDevices\r
+Return: number\r
+\r
+This call returns the number of installed and supported devices of the driver.\r
+\r
+\r
+\r
+InstDrv::CreateDevice\r
+Return: result\r
+\r
+To create a new deviced node which the driver has to support, use this\r
+function. You may even call it multiple times for more than one instance. The\r
+return value is the Windows error code (in hex). Use CreateDevice before\r
+installing or updating the driver itself.\r
+\r
+\r
+\r
+InstDrv::InstallDriver infPath\r
+Return: result\r
+        reboot\r
+\r
+InstallDriver installs or updates a device driver as specified in the .inf\r
+setup script. It returns a Windows error code (in hex) and, on success, a flag\r
+signalling if a system reboot is required.\r
+\r
+\r
+\r
+InstDrv::DeleteOemInfFiles\r
+Return: result\r
+        oeminf\r
+        oempnf\r
+\r
+DeleteOemInfFiles tries to clean up the Windows inf directory by deleting the\r
+oemXX.inf and oemXX.pnf files associated with the drivers. It returns a\r
+Windows error code (in hex) and, on success, the names of the deleted files.\r
+This functions requires that at least one device instance is still present.\r
+So, call it before you remove the devices itself. You should also call it\r
+before updating a driver. This avoids that the inf directory gets slowly\r
+messed up with useless old setup scripts (which does NOT really accelerate\r
+Windows). The error code which comes up when no device is installed is\r
+"00000103".\r
+\r
+\r
+\r
+InstDrv::RemoveAllDevices\r
+Return: result\r
+        reboot\r
+\r
+This functions deletes all devices instances the driver supported. It returns\r
+a Windows error code (in hex) and, on success, a flag signalling if the system\r
+needs to be rebooted. You additionally have to remove the driver binaries from\r
+the system paths.\r
+\r
+\r
+\r
+InstDrv::StartSystemService serviceName\r
+Return: result\r
+\r
+Call this function to start the provided system service. The function blocks\r
+until the service is started or the system reported a timeout. The return value\r
+is the Windows error code (in hex).\r
+\r
+\r
+\r
+InstDrv::StopSystemService serviceName\r
+Return: result\r
+\r
+This function tries to stop the provided system service. It blocks until the\r
+service has been shut down or the system reported a timeout. The return value\r
+is the Windows error code (in hex).\r
+\r
+\r
+\r
+Example.nsi\r
+\r
+The example script installs or removes the virtual COM port driver of IrCOMM2k\r
+(2.0.0-alpha8, see www.ircomm2k.de/english). The driver and its setup script\r
+are only included for demonstration purposes, they do not work without the\r
+rest of IrCOMM2k (but they also do not cause any harm).\r
+\r
+\r
+\r
+Building the Source Code\r
+\r
+To build the plugin from the source code, some include files and libraries\r
+which come with the Windows DDK are required.\r
+\r
+\r
+\r
+History\r
+\r
+ 0.2    - fixed bug when calling InitDriverSetup the second time\r
+        - added StartSystemService and StopSystemService\r
+\r
+ 0.1    - first release\r
+\r
+\r
+\r
+License\r
+\r
+Copyright © 2003 Jan Kiszka (Jan.Kiszka@web.de)\r
+\r
+This software is provided 'as-is', without any express or implied\r
+warranty. In no event will the authors be held liable for any damages\r
+arising from the use of this software.\r
+\r
+Permission is granted to anyone to use this software for any purpose,\r
+including commercial applications, and to alter it and redistribute\r
+it freely, subject to the following restrictions:\r
+\r
+1. The origin of this software must not be misrepresented; \r
+   you must not claim that you wrote the original software.\r
+   If you use this software in a product, an acknowledgment in the\r
+   product documentation would be appreciated but is not required.\r
+2. Altered versions must be plainly marked as such,\r
+   and must not be misrepresented as being the original software.\r
+3. This notice may not be removed or altered from any distribution.\r
diff --git a/teststand/Instdrv/NSIS/Contrib/InstDrv/ircomm2k.inf b/teststand/Instdrv/NSIS/Contrib/InstDrv/ircomm2k.inf
new file mode 100644 (file)
index 0000000..ccda1d8
--- /dev/null
@@ -0,0 +1,137 @@
+; IrCOMM2k.inf\r
+;\r
+; Installation file for the Virtual Infrared-COM-Port\r
+;\r
+; (c) Copyright 2001, 2002 Jan Kiszka \r
+;\r
+\r
+[Version]\r
+Signature="$Windows NT$"\r
+Provider=%JK%\r
+Class=Ports\r
+ClassGUID={4d36e978-e325-11ce-bfc1-08002be10318}\r
+;DriverVer=03/26/2002,1.2.1.0\r
+\r
+[DestinationDirs]\r
+IrCOMM2k.Copy2Drivers  = 12\r
+IrCOMM2k.Copy2Winnt    = 10\r
+IrCOMM2k.Copy2System32 = 11\r
+IrCOMM2k.Copy2Help     = 18\r
+\r
+\r
+;\r
+; Driver information\r
+;\r
+\r
+[Manufacturer]\r
+%JK%   = JK.Mfg\r
+\r
+[JK.Mfg]\r
+%JK.DeviceDescIrCOMM% = IrCOMM2k_inst,IrCOMM2k\r
+\r
+\r
+;\r
+; General installation section\r
+;\r
+\r
+[IrCOMM2k_inst]\r
+CopyFiles = IrCOMM2k.Copy2Drivers ;,IrCOMM2k.Copy2System32,IrCOMM2k.Copy2Help,IrCOMM2k.Copy2Winnt\r
+;AddReg    = IrCOMM2k_inst_AddReg\r
+\r
+\r
+;\r
+; File sections\r
+;\r
+\r
+[IrCOMM2k.Copy2Drivers]\r
+ircomm2k.sys,,,2\r
+\r
+;[IrCOMM2k.Copy2System32]\r
+;ircomm2k.exe,,,2\r
+;ircomm2k.dll,,,2\r
+\r
+;[IrCOMM2k.Copy2Help]\r
+;ircomm2k.hlp,,,2\r
+\r
+;[IrCOMM2k.Copy2Winnt]\r
+;IrCOMM2k-Setup.exe,Setup.exe,,2\r
+\r
+\r
+;\r
+; Service Installation\r
+;\r
+\r
+[IrCOMM2k_inst.Services]\r
+AddService = IrCOMM2k,0x00000002,IrCOMM2k_DriverService_Inst,IrCOMM2k_DriverEventLog_Inst\r
+;AddService = IrCOMM2kSvc,,IrCOMM2k_Service_Inst\r
+\r
+[IrCOMM2k_DriverService_Inst]\r
+DisplayName    = %IrCOMM2k.DrvName%\r
+ServiceType    = 1                  ; SERVICE_KERNEL_DRIVER\r
+StartType      = 3                  ; SERVICE_DEMAND_START\r
+ErrorControl   = 0                  ; SERVICE_ERROR_IGNORE\r
+ServiceBinary  = %12%\ircomm2k.sys\r
+\r
+;[IrCOMM2k_Service_Inst]\r
+;DisplayName    = %IrCOMM2k.SvcName%\r
+;Description    = %IrCOMM2k.SvcDesc%\r
+;ServiceType    = 0x00000120         ; SERVICE_WIN32_SHARE_PROCESS, SERVICE_INTERACTIVE_PROCESS\r
+;StartType      = 2                  ; SERVICE_AUTO_START\r
+;ErrorControl   = 0                  ; SERVICE_ERROR_IGNORE\r
+;ServiceBinary  = %11%\ircomm2k.exe\r
+;Dependencies   = IrCOMM2k\r
+;AddReg         = IrCOMM2kSvcAddReg\r
+\r
+\r
+[IrCOMM2k_inst.nt.HW]\r
+AddReg=IrCOMM2kHwAddReg\r
+\r
+[IrCOMM2kHwAddReg]\r
+HKR,,PortSubClass,REG_BINARY,0x00000001\r
+;HKR,,TimeoutScaling,REG_DWORD,0x00000001\r
+;HKR,,StatusLines,REG_DWORD,0x00000000\r
+\r
+;[IrCOMM2k_inst_AddReg]\r
+;HKR,,EnumPropPages32,,"ircomm2k.dll,IrCOMM2kPropPageProvider"\r
+;HKLM,%UNINSTALL_KEY%,DisplayIcon,0x00020000,"%windir%\IrCOMM2k-Setup.exe"\r
+;HKLM,%UNINSTALL_KEY%,DisplayName,,"IrCOMM2k 1.2.1 "\r
+;HKLM,%UNINSTALL_KEY%,DisplayVersion,,"1.2.1"\r
+;HKLM,%UNINSTALL_KEY%,HelpLink,,"http://www.ircomm2k.de"\r
+;HKLM,%UNINSTALL_KEY%,Publisher,,%JK%\r
+;HKLM,%UNINSTALL_KEY%,UninstallString,0x00020000,"%windir%\IrCOMM2k-Setup.exe"\r
+\r
+;[IrCOMM2kSvcAddReg]\r
+;HKR,Parameters,ActiveConnectOnly,REG_DWORD,0x00000000\r
+\r
+\r
+[IrCOMM2k_DriverEventLog_Inst]\r
+AddReg = IrCOMM2k_DriverEventLog_AddReg\r
+\r
+[IrCOMM2k_DriverEventLog_AddReg]\r
+HKR,,EventMessageFile,REG_EXPAND_SZ,"%SystemRoot%\System32\IoLogMsg.dll;%SystemRoot%\System32\drivers\ircomm2k.sys"\r
+HKR,,TypesSupported,REG_DWORD,7\r
+\r
+\r
+[Strings]\r
+\r
+;\r
+; Non-Localizable Strings\r
+;\r
+\r
+REG_SZ         = 0x00000000\r
+REG_MULTI_SZ   = 0x00010000\r
+REG_EXPAND_SZ  = 0x00020000\r
+REG_BINARY     = 0x00000001\r
+REG_DWORD      = 0x00010001\r
+SERVICEROOT    = "System\CurrentControlSet\Services"\r
+UNINSTALL_KEY  = "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\IrCOMM2k"\r
+\r
+;\r
+; Localizable Strings\r
+;\r
+\r
+JK                  = "Jan Kiszka"\r
+JK.DeviceDescIrCOMM = "Virtueller Infrarot-Kommunikationsanschluss"\r
+IrCOMM2k.DrvName    = "Virtueller Infrarot-Kommunikationsanschluss"\r
+;IrCOMM2k.SvcName    = "Virtueller Infrarot-Kommunikationsanschluß, Dienstprogramm"\r
+;IrCOMM2k.SvcDesc    = "Bildet über Infarot einen Kommunikationsanschluß nach."\r
diff --git a/teststand/Instdrv/NSIS/Contrib/InstDrv/ircomm2k.sys b/teststand/Instdrv/NSIS/Contrib/InstDrv/ircomm2k.sys
new file mode 100644 (file)
index 0000000..7882583
Binary files /dev/null and b/teststand/Instdrv/NSIS/Contrib/InstDrv/ircomm2k.sys differ
diff --git a/teststand/Instdrv/NSIS/Includes/java.nsh b/teststand/Instdrv/NSIS/Includes/java.nsh
new file mode 100644 (file)
index 0000000..8e0e208
--- /dev/null
@@ -0,0 +1,181 @@
+!include WordFunc.nsh
+
+; Definitions for Java Detection
+
+!define JAVA_VERSION "6.0"
+
+Function GetFileVersion
+       !define GetFileVersion `!insertmacro GetFileVersionCall`
+       !macro GetFileVersionCall _FILE _RESULT
+               Push `${_FILE}`
+               Call GetFileVersion
+               Pop ${_RESULT}
+       !macroend
+       Exch $0
+       Push $1
+       Push $2
+       Push $3
+       Push $4
+       Push $5
+       Push $6
+       ClearErrors
+       GetDllVersion '$0' $1 $2
+       IfErrors error
+       IntOp $3 $1 >> 16
+       IntOp $3 $3 & 0x0000FFFF
+       IntOp $4 $1 & 0x0000FFFF
+       IntOp $5 $2 >> 16
+       IntOp $5 $5 & 0x0000FFFF
+       IntOp $6 $2 & 0x0000FFFF
+       StrCpy $0 '$3.$4.$5.$6'
+       goto end
+       error:
+       SetErrors
+       StrCpy $0 ''
+       end:
+       Pop $6
+       Pop $5
+       Pop $4
+       Pop $3
+       Pop $2
+       Pop $1
+       Exch $0
+FunctionEnd
+
+Function openLinkNewWindow
+  Push $3
+  Exch
+  Push $2
+  Exch
+  Push $1
+  Exch
+  Push $0
+  Exch
+
+  ReadRegStr $1 HKCU "Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.html\UserChoice" "Progid"
+  IfErrors iexplore
+
+  Goto foundbrowser
+iexplore:
+  StrCpy $1 "IE.AssocFile.HTM"
+
+foundbrowser:  
+
+  StrCpy $2 "\shell\open\command"
+
+  StrCpy $3 $1$2
+
+  ReadRegStr $0 HKCR $3 ""
+
+# Get browser path
+  DetailPrint $0
+  StrCpy $2 '"'
+  StrCpy $1 $0 1
+  StrCmp $1 $2 +2 # if path is not enclosed in " look for space as final char
+  StrCpy $2 ' '
+  StrCpy $3 1
+  loop:
+    StrCpy $1 $0 1 $3
+    DetailPrint $1
+    StrCmp $1 $2 found
+    StrCmp $1 "" found
+    IntOp $3 $3 + 1
+    Goto loop
+  found:
+    StrCpy $1 $0 $3
+    StrCmp $2 " " +2
+      StrCpy $1 '$1"'
+  Pop $0
+  Exec '$1 $0'
+  Pop $0
+  Pop $1
+  Pop $2
+  Pop $3
+FunctionEnd
+!macro _OpenURL URL
+Push "${URL}"
+Call openLinkNewWindow
+!macroend
+!define OpenURL '!insertmacro "_OpenURL"'
+
+Function DoDetectJRE
+
+  DetailPrint "Desired Java version ${JAVA_VERSION}"
+
+  SearchPath $0 javaw.exe
+  IfErrors no
+  
+  DetailPrint "Detected java in $0"
+
+  ${GetFileVersion} "$0" $1
+  IfErrors no
+
+  DetailPrint "Java version $1"
+
+  ${VersionCompare} $1 ${JAVA_VERSION} $2
+  IntCmp $2 1 yes yes old
+
+yes:
+  StrCpy $0 2
+  Goto done
+
+old:
+  StrCpy $0 1
+  Goto done
+
+no:
+  StrCpy $0 0
+  Goto done  
+
+done:
+
+FunctionEnd
+
+var dialog
+var hwnd
+var null
+
+var install
+var quit
+var skip
+
+Function GetJRE
+  ${OpenURL} "java.com"
+  MessageBox MB_OK "Click OK to continue after completing the Java Install."
+FunctionEnd
+
+Function DetectJRE
+
+  Call DoDetectJRE
+
+  IntCmp $0 1 ask_old ask_no yes
+
+ask_no:
+  StrCpy $0 "Cannot find Java. Download and install?"
+  Goto ask
+
+ask_old:
+  StrCpy $0 "Java version appears to be too old. Download and install?"
+  Goto ask
+
+ask:
+  MessageBox MB_YESNOCANCEL $0 IDYES do_java IDNO skip_java
+
+do_java:
+  Call GetJRE
+
+
+skip_java:
+yes:
+
+FunctionEnd
diff --git a/teststand/Instdrv/NSIS/Includes/refresh-sh.nsh b/teststand/Instdrv/NSIS/Includes/refresh-sh.nsh
new file mode 100644 (file)
index 0000000..23d8e5e
--- /dev/null
@@ -0,0 +1,14 @@
+!define SHCNE_ASSOCCHANGED 0x08000000
+!define SHCNF_IDLIST 0
+Function RefreshShellIcons
+  ; By jerome tremblay - april 2003
+  ${DisableX64FSRedirection}
+  System::Call 'shell32.dll::SHChangeNotify(i, i, i, i) v (${SHCNE_ASSOCCHANGED}, ${SHCNF_IDLIST}, 0, 0)'
+FunctionEnd
+
+Function un.RefreshShellIcons
+  ; By jerome tremblay - april 2003
+  ${DisableX64FSRedirection}
+  System::Call 'shell32.dll::SHChangeNotify(i, i, i, i) v (${SHCNE_ASSOCCHANGED}, ${SHCNF_IDLIST}, 0, 0)'
+FunctionEnd
diff --git a/teststand/Instdrv/NSIS/Plugins/InstDrv.dll b/teststand/Instdrv/NSIS/Plugins/InstDrv.dll
new file mode 100644 (file)
index 0000000..482e955
Binary files /dev/null and b/teststand/Instdrv/NSIS/Plugins/InstDrv.dll differ
diff --git a/teststand/Makefile-standalone b/teststand/Makefile-standalone
new file mode 100644 (file)
index 0000000..e5f4163
--- /dev/null
@@ -0,0 +1,191 @@
+.SUFFIXES: .java .class
+
+CLASSPATH=classes:./*:/usr/share/java/*
+CLASSFILES=\
+       Altos.class \
+       AltosChannelMenu.class \
+       AltosConfigFC.class \
+       AltosConfigFCUI.class \
+       AltosConvert.class \
+       AltosCRCException.class \
+       AltosCSV.class \
+       AltosCSVUI.class \
+       AltosDebug.class \
+       AltosEepromDownload.class \
+       AltosEepromMonitor.class \
+       AltosEepromReader.class \
+       AltosEepromRecord.class \
+       AltosFile.class \
+       AltosFlash.class \
+       AltosFlashUI.class \
+       AltosFlightInfoTableModel.class \
+       AltosFlightStatusTableModel.class \
+       AltosGPS.class \
+       AltosGreatCircle.class \
+       AltosHexfile.class \
+       AltosLine.class \
+       AltosInfoTable.class \
+       AltosLog.class \
+       AltosLogfileChooser.class \
+       AltosParse.class \
+       AltosPreferences.class \
+       AltosReader.class \
+       AltosRecord.class \
+       AltosSerialMonitor.class \
+       AltosSerial.class \
+       AltosState.class \
+       AltosStatusTable.class \
+       AltosTelemetry.class \
+       AltosTelemetryReader.class \
+       AltosUI.class \
+       AltosDevice.class \
+       AltosDeviceDialog.class \
+       AltosRomconfig.class \
+       AltosRomconfigUI.class \
+       AltosVoice.class
+
+JAVA_ICONS=\
+       ../icon/altus-metrum-16.png \
+       ../icon/altus-metrum-32.png \
+       ../icon/altus-metrum-48.png \
+       ../icon/altus-metrum-64.png \
+       ../icon/altus-metrum-128.png \
+       ../icon/altus-metrum-256.png
+
+WINDOWS_ICON=../icon/altus-metrum.ico
+
+# where altosui.jar gets installed
+ALTOSLIB=/usr/share/java
+
+# where freetts.jar is to be found
+FREETTSLIB=/usr/share/java
+
+# all of the freetts files
+FREETTSJAR= \
+       $(FREETTSLIB)/cmudict04.jar \
+       $(FREETTSLIB)/cmulex.jar \
+       $(FREETTSLIB)/cmu_time_awb.jar \
+       $(FREETTSLIB)/cmutimelex.jar \
+       $(FREETTSLIB)/cmu_us_kal.jar \
+       $(FREETTSLIB)/en_us.jar \
+       $(FREETTSLIB)/freetts.jar
+
+# The current hex files
+HEXLIB=../src
+HEXFILES = \
+       $(HEXLIB)/telemetrum-v1.0.ihx \
+       $(HEXLIB)/teledongle-v0.2.ihx
+
+JAVAFLAGS=-Xlint:unchecked -Xlint:deprecation
+
+ALTOSUIJAR = altosui.jar
+FATJAR = fat/altosui.jar
+
+OS:=$(shell uname)
+
+LINUX_APP=altosui
+
+DARWIN_ZIP=Altos-Mac.zip
+
+WINDOWS_EXE=Altos-Windows.exe
+
+LINUX_TGZ=Altos-Linux.tgz
+
+all: altosui.jar $(LINUX_APP)
+fat: altosui.jar $(LINUX_APP) $(DARWIN_ZIP) $(WINDOWS_EXE) $(LINUX_TGZ)
+
+$(CLASSFILES):
+
+.java.class:
+       javac -encoding UTF8 -classpath "$(CLASSPATH)" $(JAVAFLAGS) $*.java
+
+altosui.jar: classes/images classes/altosui classes/libaltosJNI $(CLASSFILES) Manifest.txt
+       cd ./classes && jar cfm ../$@ altosui/Manifest.txt images/* altosui/*.class libaltosJNI/*.class
+
+Manifest.txt: Makefile $(CLASSFILES)
+       echo 'Main-Class: altosui.AltosUI' > $@
+       echo "Class-Path: $(FREETTSLIB)/freetts.jar" >> $@
+
+classes/altosui:
+       mkdir -p classes
+       ln -sf .. classes/altosui
+
+classes/libaltosJNI:
+       mkdir -p classes
+       ln -sf ../libaltos/libaltosJNI classes/libaltosJNI
+
+classes/images:
+       mkdir -p classes/images
+       ln -sf ../$(JAVA_ICONS) classes/images
+
+altosui:
+       echo "#!/bin/sh" > $@
+       echo "exec java -Djava.library.path=/usr/lib/altos -jar /usr/share/java/altosui.jar" >> $@
+       chmod +x ./altosui
+
+fat/altosui:
+       echo "#!/bin/sh" > $@
+       echo 'ME=`which "$0"`' >> $@
+       echo 'DIR=`dirname "$ME"`' >> $@
+       echo 'exec java -Djava.library.path="$$DIR" -jar "$$DIR"/altosui.jar' >> $@
+       chmod +x $@
+
+fat/altosui.jar: $(CLASSFILES) $(JAVA_ICONS) fat/classes/Manifest.txt
+       mkdir -p fat/classes
+       test -L fat/classes/altosui || ln -sf ../.. fat/classes/altosui
+       mkdir -p fat/classes/images
+       cp $(JAVA_ICONS) fat/classes/images
+       test -L fat/classes/libaltosJNI || ln -sf ../../libaltos/libaltosJNI fat/classes/libaltosJNI
+       cd ./fat/classes && jar cfm ../../$@ Manifest.txt images/* altosui/*.class libaltosJNI/*.class
+
+fat/classes/Manifest.txt: $(CLASSFILES) Makefile
+       mkdir -p fat/classes
+       echo 'Main-Class: altosui.AltosUI' > $@
+       echo "Class-Path: freetts.jar" >> $@
+
+install: altosui.jar altosui
+       install -m 0644 altosui.jar $(DESTDIR)/usr/share/java/altosui.jar
+       install -m 0644 altosui.1 $(DESTDIR)/usr/share/man/man1/altosui.1
+       install altosui $(DESTDIR)/usr/bin/altosui
+
+clean:
+       rm -f *.class altosui.jar
+       rm -f AltosUI.app/Contents/Resources/Java/*
+       rm -rf classes
+       rm -rf windows linux
+
+distclean:     clean
+       rm -f $(DARWIN_ZIP) $(WINDOWS_EXE) $(LINUX_TGZ)
+       rm -rf darwin fat
+
+FAT_FILES=$(FATJAR) $(FREETTSJAR) $(HEXFILES)
+
+LINUX_FILES=$(FAT_FILES) libaltos/libaltos.so fat/altosui
+$(LINUX_TGZ): $(LINUX_FILES)
+       rm -f $@
+       mkdir -p linux/AltOS
+       rm -f linux/AltOS/*
+       cp $(LINUX_FILES) linux/AltOS
+       cd linux && tar czf ../$@ AltOS
+
+DARWIN_RESOURCES=$(FATJAR) $(FREETTSJAR) libaltos/libaltos.dylib
+DARWIN_EXTRA=$(HEXFILES)
+DARWIN_FILES=$(DARWIN_RESOURCES) $(DARWIN_EXTRA)
+
+$(DARWIN_ZIP): $(DARWIN_FILES)
+       rm -f $@
+       cp -a AltosUI.app darwin/
+       mkdir -p darwin/AltosUI.app/Contents/Resources/Java
+       cp $(DARWIN_RESOURCES) darwin/AltosUI.app/Contents/Resources/Java
+       mkdir -p darwin/AltOS
+       cp $(DARWIN_EXTRA) darwin/AltOS
+       cd darwin && zip -r ../$@ AltosUI.app AltOS
+
+WINDOWS_FILES = $(FAT_FILES) libaltos/altos.dll ../altusmetrum.inf $(WINDOWS_ICON)
+
+$(WINDOWS_EXE): $(WINDOWS_FILES) altos-windows.nsi
+       rm -f $@
+       mkdir -p windows/AltOS
+       rm -f windows/AltOS/*
+       cp $(WINDOWS_FILES) windows/AltOS
+       makensis altos-windows.nsi
diff --git a/teststand/Makefile.am b/teststand/Makefile.am
new file mode 100644 (file)
index 0000000..5ba8fc3
--- /dev/null
@@ -0,0 +1,394 @@
+
+JAVAROOT=classes
+AM_JAVACFLAGS=-target 1.6 -encoding UTF-8 -Xlint:deprecation -Xlint:unchecked -source 6
+
+man_MANS=teststand.1
+
+altoslibdir=$(libdir)/altos
+
+CLASSPATH_ENV=mkdir -p $(JAVAROOT); CLASSPATH="$(JAVAROOT):../altoslib/*:../altosuilib/*:../libaltos:$(JCOMMON)/jcommon.jar:$(JFREECHART)/jfreechart.jar:$(FREETTS)/freetts.jar"
+
+bin_SCRIPTS=teststand
+
+teststand_JAVA = \
+       AltosAscent.java \
+       AltosChannelMenu.java \
+       AltosCompanionInfo.java \
+       AltosConfigFC.java \
+       AltosConfigFCUI.java \
+       AltosConfigPyroUI.java \
+       AltosConfigureUI.java \
+       AltosConfigTD.java \
+       AltosConfigTDUI.java \
+       AltosDescent.java \
+       AltosFlightStatus.java \
+       AltosFlightStatusUpdate.java \
+       AltosFlightUI.java \
+       Altos.java \
+       AltosIdleMonitorUI.java \
+       AltosIgniteUI.java \
+       AltosIgnitor.java \
+       AltosLaunch.java \
+       AltosLaunchUI.java \
+       AltosLanded.java \
+       AltosPad.java \
+       TestStand.java \
+       AltosGraphUI.java
+
+JFREECHART_CLASS= \
+    jfreechart.jar
+
+JCOMMON_CLASS=\
+    jcommon.jar
+
+FREETTS_CLASS= \
+       cmudict04.jar \
+       cmulex.jar \
+       cmu_time_awb.jar \
+       cmutimelex.jar \
+       cmu_us_kal.jar \
+       en_us.jar \
+       freetts.jar
+
+ALTOSLIB_CLASS=\
+       altoslib_$(ALTOSLIB_VERSION).jar
+
+ALTOSUILIB_CLASS=\
+       altosuilib_$(ALTOSUILIB_VERSION).jar
+
+if MULTI_ARCH
+LIBALTOS_LINUX=libaltos32.so libaltos64.so
+else
+LIBALTOS_LINUX=libaltos.so
+endif
+
+LIBALTOS= \
+       $(LIBALTOS_LINUX) \
+       libaltos.dylib \
+       altos.dll \
+       altos64.dll
+
+desktopdir = $(datadir)/applications
+desktop_file = altusmetrum-teststand.desktop
+desktop_SCRIPTS = $(desktop_file)
+
+JAR=teststand.jar
+
+FATJAR=teststand-fat.jar
+
+# Icons
+ICONDIR=../icon
+
+JAVA_ICONS=\
+       $(ICONDIR)/altusmetrum-teststand-16.png \
+       $(ICONDIR)/altusmetrum-teststand-32.png \
+       $(ICONDIR)/altusmetrum-teststand-48.png \
+       $(ICONDIR)/altusmetrum-teststand-64.png \
+       $(ICONDIR)/altusmetrum-teststand-128.png\
+       $(ICONDIR)/altusmetrum-teststand-256.png
+
+# icon base names for jar
+ICONJAR= \
+       -C $(ICONDIR) altusmetrum-teststand-16.png \
+       -C $(ICONDIR) altusmetrum-teststand-32.png \
+       -C $(ICONDIR) altusmetrum-teststand-48.png \
+       -C $(ICONDIR) altusmetrum-teststand-64.png \
+       -C $(ICONDIR) altusmetrum-teststand-128.png\
+       -C $(ICONDIR) altusmetrum-teststand-256.png
+
+WINDOWS_ICONS  =\
+       $(ICONDIR)/altusmetrum-teststand.ico \
+       $(ICONDIR)/altusmetrum-teststand.exe
+       $(ICONDIR)/application-vnd.altusmetrum.eeprom.ico \
+       $(ICONDIR)/application-vnd.altusmetrum.eeprom.exe \
+       $(ICONDIR)/application-vnd.altusmetrum.telemetry.ico \
+       $(ICONDIR)/application-vnd.altusmetrum.telemetry.exe
+
+MACOSX_ICONS   =\
+       $(ICONDIR)/altusmetrum-teststand.icns \
+       $(ICONDIR)/application-vnd.altusmetrum.eeprom.icns \
+       $(ICONDIR)/application-vnd.altusmetrum.telemetry.icns
+
+LINUX_ICONS    =\
+       $(ICONDIR)/altusmetrum-teststand.svg \
+       $(ICONDIR)/application-vnd.altusmetrum.eeprom.svg \
+       $(ICONDIR)/application-vnd.altusmetrum.telemetry.svg
+
+LINUX_MIMETYPE =\
+       $(ICONDIR)/org-altusmetrum-mimetypes.xml
+
+# Firmware
+FIRMWARE_TD_0_2=$(top_srcdir)/src/teledongle-v0.2/teledongle-v0.2-$(VERSION).ihx
+FIRMWARE_TD_3_0=$(top_srcdir)/src/teledongle-v3.0/teledongle-v3.0-$(VERSION).ihx
+FIRMWARE_TD=$(FIRMWARE_TD_0_2) $(FIRMWARE_TD_3_0)
+
+FIRMWARE_TM_1_0=$(top_srcdir)/src/telemetrum-v1.0/telemetrum-v1.0-$(VERSION).ihx
+FIRMWARE_TM_1_1=$(top_srcdir)/src/telemetrum-v1.1/telemetrum-v1.1-$(VERSION).ihx
+FIRMWARE_TM_1_2=$(top_srcdir)/src/telemetrum-v1.2/telemetrum-v1.2-$(VERSION).ihx
+FIRMWARE_TM_2_0=$(top_srcdir)/src/telemetrum-v2.0/telemetrum-v2.0-$(VERSION).ihx
+#FIRMWARE_TM_3_0=$(top_srcdir)/src/telemetrum-v3.0/telemetrum-v3.0-$(VERSION).ihx
+#FIRMWARE_TM=$(FIRMWARE_TM_1_0) $(FIRMWARE_TM_1_1) $(FIRMWARE_TM_1_2) $(FIRMWARE_TM_2_0) $(FIRMWARE_TM_3_0)
+FIRMWARE_TM=$(FIRMWARE_TM_1_0) $(FIRMWARE_TM_1_1) $(FIRMWARE_TM_1_2) $(FIRMWARE_TM_2_0)
+
+FIRMWARE_TELEMINI_1_0=$(top_srcdir)/src/telemini-v1.0/telemini-v1.0-$(VERSION).ihx
+FIRMWARE_TELEMINI_3_0=$(top_srcdir)/src/telemini-v3.0/telemini-v3.0-$(VERSION).ihx
+FIRMWARE_TELEMINI=$(FIRMWARE_TELEMINI_1_0) $(FIRMWARE_TELEMINI_3_0)
+
+FIRMWARE_TBT_1_0=$(top_srcdir)/src/telebt-v1.0/telebt-v1.0-$(VERSION).ihx
+FIRMWARE_TBT_3_0=$(top_srcdir)/src/telebt-v3.0/telebt-v3.0-$(VERSION).ihx
+FIRMWARE_TBT_4_0=$(top_srcdir)/src/telebt-v4.0/telebt-v4.0-$(VERSION).ihx
+FIRMWARE_TBT=$(FIRMWARE_TBT_1_0) $(FIRMWARE_TBT_3_0) $(FIRMWARE_TBT_4_0)
+
+FIRMWARE_TMEGA_1_0=$(top_srcdir)/src/telemega-v1.0/telemega-v1.0-$(VERSION).ihx
+FIRMWARE_TMEGA_2_0=$(top_srcdir)/src/telemega-v2.0/telemega-v2.0-$(VERSION).ihx
+FIRMWARE_TMEGA=$(FIRMWARE_TMEGA_1_0) $(FIRMWARE_TMEGA_2_0)
+
+FIRMWARE_EMINI_1_0=$(top_srcdir)/src/easymini-v1.0/easymini-v1.0-$(VERSION).ihx
+FIRMWARE_EMINI=$(FIRMWARE_EMINI_1_0)
+
+FIRMWARE_EMEGA_1_0=$(top_srcdir)/src/easymega-v1.0/easymega-v1.0-$(VERSION).ihx
+FIRMWARE_EMEGA=$(FIRMWARE_EMEGA_1_0)
+
+FIRMWARE_TGPS_1_0=$(top_srcdir)/src/telegps-v1.0/telegps-v1.0-$(VERSION).ihx
+FIRMWARE_TGPS=$(FIRMWARE_TGPS_1_0)
+
+FIRMWARE=$(FIRMWARE_TM) $(FIRMWARE_TELEMINI) $(FIRMWARE_TD) $(FIRMWARE_TBT) $(FIRMWARE_TMEGA) $(FIRMWARE_EMINI) $(FIRMWARE_TGPS) $(FIRMWARE_EMEGA)
+
+ALTUSMETRUM_DOC=$(top_srcdir)/doc/altusmetrum.pdf
+ALTOS_DOC=$(top_srcdir)/doc/altos.pdf
+TELEMETRY_DOC=$(top_srcdir)/doc/telemetry.pdf
+TEMPLATE_DOC=\
+       $(top_srcdir)/doc/telemetrum-outline.pdf \
+       $(top_srcdir)/doc/easymini-outline.pdf \
+       $(top_srcdir)/doc/telemega-outline.pdf \
+       $(top_srcdir)/doc/telemini-v1-outline.pdf \
+       $(top_srcdir)/doc/telemini-v3-outline.pdf
+
+DOC=$(ALTUSMETRUM_DOC) $(ALTOS_DOC) $(TELEMETRY_DOC) $(TEMPLATE_DOC)
+
+# Distribution targets
+LINUX_DIST=Altos-Linux-$(VERSION).tar.bz2
+LINUX_SH=Altos-Linux-$(VERSION).sh
+MACOSX_DIST=Altos-Mac-$(VERSION).dmg
+WINDOWS_DIST=Altos-Windows-$(VERSION_DASH).exe
+MDWN=$(VERSION).mdwn
+MDWNTMPL=mdwn.tmpl
+
+FAT_FILES=$(FATJAR) $(ALTOSLIB_CLASS) $(ALTOSUILIB_CLASS) $(FREETTS_CLASS) $(JFREECHART_CLASS) $(JCOMMON_CLASS)
+
+LINUX_LIBS=$(LIBALTOS_LINUX)
+
+LINUX_FILES=$(FAT_FILES) $(LINUX_LIBS) $(FIRMWARE) $(DOC) $(desktop_file).in $(LINUX_ICONS) $(LINUX_MIMETYPE)
+LINUX_EXTRA=teststand-fat
+
+MACOSX_INFO_PLIST=Info.plist
+MACOSX_FILES=$(FAT_FILES) libaltos.dylib $(MACOSX_INFO_PLIST) $(DOC) ReadMe-Mac.rtf $(MACOSX_ICONS)
+MACOSX_EXTRA=$(FIRMWARE)
+
+WINDOWS_FILES=$(FAT_FILES) $(FIRMWARE) altos.dll altos64.dll $(top_srcdir)/altusmetrum.inf $(top_srcdir)/altusmetrum.cat $(WINDOWS_ICONS)
+
+all-local: classes/teststand $(JAR) teststand teststand-test teststand-jdb $(MDWN)
+
+clean-local:
+       -rm -rf classes $(JAR) $(FATJAR) \
+               Altos-Linux-*.tar.bz2 Altos-Linux-*.sh Altos-Mac-*.dmg Altos-Windows-*.exe \
+               windows altoslib_*.jar altosuilib_*.jar $(FREETTS_CLASS) \
+               $(JFREECHART_CLASS) $(JCOMMON_CLASS) $(LIBALTOS) Manifest.txt Manifest-fat.txt altos-windows.log altos-windows.nsi \
+               teststand teststand-test teststand-jdb macosx linux *.desktop *.mdwn
+
+EXTRA_DIST = $(desktop_file).in
+
+$(desktop_file): $(desktop_file).in
+       sed -e 's#%bindir%#@bindir@#' -e 's#%icondir%#$(datadir)/icons/hicolor/scalable/apps#' ${srcdir}/$(desktop_file).in > $@
+       chmod +x $@
+
+if FATINSTALL
+
+FATTARGET=$(FATDIR)/AltOS/releases/$(VERSION)
+
+LINUX_SH_TARGET=$(FATTARGET)/$(LINUX_SH)
+MACOSX_DIST_TARGET=$(FATTARGET)/$(MACOSX_DIST)
+WINDOWS_DIST_TARGET=$(FATTARGET)/$(WINDOWS_DIST)
+MDWN_TARGET=$(FATDIR)/AltOS/releases/$(MDWN)
+RELNOTES=release-notes-$(VERSION).html
+RELNOTES_SRC=$(top_builddir)/doc/$(RELNOTES)
+RELNOTES_TARGET=$(FATTARGET)/$(RELNOTES)
+
+fat-install: fat $(LINUX_SH_TARGET) $(MACOSX_DIST_TARGET) $(WINDOWS_DIST_TARGET) $(MDWN_TARGET) $(RELNOTES_TARGET)
+
+$(LINUX_SH_TARGET): $(LINUX_SH)
+       mkdir -p $(FATTARGET)
+       cp -p $< $@
+
+$(MACOSX_DIST_TARGET): $(MACOSX_DIST)
+       mkdir -p $(FATTARGET)
+       cp -p $< $@
+
+$(WINDOWS_DIST_TARGET): $(WINDOWS_DIST)
+       mkdir -p $(FATTARGET)
+       cp -p $< $@
+
+$(MDWN_TARGET): $(MDWN)
+       mkdir -p $(FATTARGET)
+       cp -p $< $@
+
+$(RELNOTES_TARGET): $(RELNOTES_SRC)
+       mkdir -p $(FATTARGET)
+       sh $(top_srcdir)/doc/install-html -d $(FATTARGET) $(RELNOTES_SRC)
+
+endif
+
+$(MDWN): $(MDWNTMPL)
+       sed -e 's/%version%/$(VERSION)/g' -e 's/%version_dash%/$(VERSION_DASH)/g' $(MDWNTMPL) > $@
+
+fat: $(LINUX_DIST) $(LINUX_SH) $(MACOSX_DIST) $(WINDOWS_DIST)
+
+teststanddir=$(datadir)/java
+
+install-teststandJAVA: teststand.jar
+       @$(NORMAL_INSTALL)
+       test -z "$(teststanddir)" || $(MKDIR_P) "$(DESTDIR)$(teststanddir)"
+       echo " $(INSTALL_DATA)" "$<" "'$(DESTDIR)$(teststanddir)/teststand.jar'"; \
+       $(INSTALL_DATA) "$<" "$(DESTDIR)$(teststanddir)"
+
+classes/teststand:
+       mkdir -p classes/teststand
+
+$(JAR): classteststand.stamp Manifest.txt $(JAVA_ICONS) $(ALTOSLIB_CLASS) $(ALTOSUILIB_CLASS)
+       jar cfm $@ Manifest.txt \
+               $(ICONJAR) \
+               -C classes teststand \
+               -C ../libaltos libaltosJNI
+
+$(FATJAR): classteststand.stamp Manifest-fat.txt $(ALTOSLIB_CLASS) $(ALTOSUILIB_CLASS) $(FREETTS_CLASS) $(JFREECHART_CLASS) $(JCOMMON_CLASS) $(LIBALTOS) $(JAVA_ICONS)
+       jar cfm $@ Manifest-fat.txt \
+               $(ICONJAR) \
+               -C classes teststand \
+               -C ../libaltos libaltosJNI
+
+Manifest.txt: Makefile
+       echo 'Main-Class: teststand.TestStand' > $@
+       echo "Class-Path: $(ALTOSLIB_CLASS) $(ALTOSUILIB_CLASS) $(FREETTS)/freetts.jar $(JCOMMON)/jcommon.jar $(JFREECHART)/jfreechart.jar" >> $@
+
+Manifest-fat.txt:
+       echo 'Main-Class: teststand.TestStand' > $@
+       echo "Class-Path: $(ALTOSLIB_CLASS) $(ALTOSUILIB_CLASS) freetts.jar jcommon.jar jfreechart.jar" >> $@
+
+teststand: Makefile
+       echo "#!/bin/sh" > $@
+       echo 'exec java -Djava.library.path="$(altoslibdir)" -jar "$(altosuidir)/teststand.jar" "$$@"' >> $@
+       chmod +x $@
+
+teststand-test: Makefile
+       echo '#!/bin/sh' > $@
+       echo 'dir="$$(dirname $$0)"' >> $@
+       echo 'cd "$$dir"' >> $@
+       echo 'teststand="$$(pwd -P)"' >> $@
+       echo 'altos="$$(dirname $$teststand)"' >> $@
+       echo 'exec java -Djava.library.path="$$altos/libaltos/.libs" -jar "$$teststand/teststand.jar" "$$@"' >> $@
+       chmod +x $@
+
+teststand-jdb: Makefile
+       echo "#!/bin/sh" > $@
+       echo 'exec jdb -classpath "classes:./*:../libaltos:$(FREETTS)/freetts.jar:$(JCOMMON)/jcommon.jar:$(JFREECHART)/jfreechart.jar" -Djava.library.path="../libaltos/.libs" teststand/TestStand "$$@"' >> $@
+       chmod +x $@
+
+libaltos.so: build-libaltos
+       -rm -f "$@"
+       $(LN_S) ../libaltos/.libs/"$@" .
+
+libaltos32.so: build-libaltos
+       -rm -f "$@"
+       $(LN_S) ../libaltos/.libs/"$@" .
+
+libaltos64.so: build-libaltos
+       -rm -f "$@"
+       $(LN_S) ../libaltos/.libs/"$@" .
+
+libaltos.dylib:
+       -rm -f "$@"
+       $(LN_S) ../libaltos/"$@" .
+
+altos.dll: ../libaltos/altos.dll
+       -rm -f "$@"
+       $(LN_S) ../libaltos/"$@" .
+
+altos64.dll: ../libaltos/altos64.dll
+       -rm -f "$@"
+       $(LN_S) ../libaltos/"$@" .
+
+../libaltos/.libs/libaltos64.so: ../libaltos/.libs/libaltos32.so
+
+../libaltos/.libs/libaltos32.so: build-libaltos
+
+../libaltos/.libs/libaltos.so: build-libaltos
+
+../libaltos/altos.dll: build-altos-dll
+
+../libaltos/altos64.dll: build-altos64-dll
+
+build-libaltos:
+       +cd ../libaltos && make libaltos.la
+build-altos-dll:
+       +cd ../libaltos && make altos.dll
+
+build-altos64-dll:
+       +cd ../libaltos && make altos64.dll
+
+$(ALTOSLIB_CLASS):
+       -rm -f "$@"
+       $(LN_S) ../altoslib/"$@" .
+
+$(ALTOSUILIB_CLASS):
+       -rm -f "$@"
+       $(LN_S) ../altosuilib/"$@" .
+
+$(FREETTS_CLASS):
+       -rm -f "$@"
+       $(LN_S) "$(FREETTS)"/"$@" .
+
+$(JFREECHART_CLASS):
+       -rm -f "$@"
+       $(LN_S) "$(JFREECHART)"/"$@" .
+
+$(JCOMMON_CLASS):
+       -rm -f "$@"
+       $(LN_S) "$(JCOMMON)"/"$@" .
+
+$(LINUX_DIST): $(LINUX_FILES) $(LINUX_EXTRA)
+       -rm -f $@
+       -rm -rf linux
+       mkdir -p linux/AltOS
+       cp -p $(LINUX_FILES) linux/AltOS
+       cp -p teststand-fat linux/AltOS/teststand
+       chmod +x linux/AltOS/teststand
+       tar cjf $@ -C linux AltOS
+
+$(LINUX_SH): $(LINUX_DIST) linux-install.sh
+       cat linux-install.sh $(LINUX_DIST) > $@
+       chmod +x $@
+
+$(MACOSX_DIST): $(MACOSX_FILES) $(MACOSX_EXTRA) Makefile
+       -rm -f $@
+       -rm -rf macosx
+       mkdir macosx
+       cp -a TestStand.app macosx/
+       cp -a ReadMe-Mac.rtf macosx/ReadMe.rtf
+       mkdir -p macosx/Doc
+       cp -a $(DOC) macosx/Doc
+       cp -p Info.plist macosx/TestStand.app/Contents
+       mkdir -p macosx/AltOS-$(VERSION) macosx/TestStand.app/Contents/Resources/Java
+       cp -p $(MACOSX_ICONS) macosx/TestStand.app/Contents/Resources
+       cp -p $(FATJAR) macosx/TestStand.app/Contents/Resources/Java/teststand.jar
+       cp -p libaltos.dylib macosx/TestStand.app/Contents/Resources/Java
+       cp -p $(ALTOSLIB_CLASS) macosx/TestStand.app/Contents/Resources/Java
+       cp -p $(ALTOSUILIB_CLASS) macosx/TestStand.app/Contents/Resources/Java
+       cp -p $(FREETTS_CLASS) macosx/TestStand.app/Contents/Resources/Java
+       cp -p $(JFREECHART_CLASS) macosx/TestStand.app/Contents/Resources/Java
+       cp -p $(JCOMMON_CLASS) macosx/TestStand.app/Contents/Resources/Java
+       cp -p $(MACOSX_EXTRA) macosx/AltOS-$(VERSION)
+       genisoimage -D -V AltOS-$(VERSION) -no-pad -r -apple -o $@ macosx
+
+$(WINDOWS_DIST): $(WINDOWS_FILES) altos-windows.nsi Instdrv/NSIS/Includes/java.nsh
+       -rm -f $@
+       makensis -Oaltos-windows.log "-XOutFile $@" "-DVERSION=$(VERSION)" altos-windows.nsi || (cat altos-windows.log && exit 1)
diff --git a/teststand/ReadMe-Mac.rtf b/teststand/ReadMe-Mac.rtf
new file mode 100644 (file)
index 0000000..8a95262
--- /dev/null
@@ -0,0 +1,56 @@
+{\rtf1\ansi\deff3\adeflang1025
+{\fonttbl{\f0\froman\fprq2\fcharset0 Times New Roman;}{\f1\froman\fprq2\fcharset2 Symbol;}{\f2\fswiss\fprq2\fcharset0 Arial;}{\f3\froman\fprq2\fcharset128 Liberation Serif{\*\falt Times New Roman};}{\f4\fswiss\fprq2\fcharset128 Arial;}{\f5\fnil\fprq2\fcharset128 SimSun;}{\f6\fnil\fprq2\fcharset128 Raghindi;}{\f7\fnil\fprq0\fcharset128 Raghindi;}}
+{\colortbl;\red0\green0\blue0;\red128\green128\blue128;}
+{\stylesheet{\s0\snext0\nowidctlpar{\*\hyphen2\hyphlead2\hyphtrail2\hyphmax0}\cf0\kerning1\hich\af5\langfe2052\dbch\af6\afs24\alang1081\loch\f3\fs24\lang1033 Normal;}
+{\*\cs15\snext15 Numbering Symbols;}
+{\s16\sbasedon0\snext17\sb240\sa120\keepn\hich\af5\dbch\af6\afs28\loch\f4\fs28 Heading;}
+{\s17\sbasedon0\snext17\sb0\sa120 Text body;}
+{\s18\sbasedon17\snext18\sb0\sa120\dbch\af7 List;}
+{\s19\sbasedon0\snext19\sb120\sa120\noline\i\dbch\af7\afs24\ai\fs24 Caption;}
+{\s20\sbasedon0\snext20\noline\dbch\af7 Index;}
+}{\*\listtable{\list\listtemplateid1
+{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'00);}{\levelnumbers\'01;}\fi-360\li720}
+{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'01.;}{\levelnumbers\'01;}\fi-360\li1080}
+{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'02.;}{\levelnumbers\'01;}\fi-360\li1440}
+{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'03.;}{\levelnumbers\'01;}\fi-360\li1800}
+{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'04.;}{\levelnumbers\'01;}\fi-360\li2160}
+{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'05.;}{\levelnumbers\'01;}\fi-360\li2520}
+{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'06.;}{\levelnumbers\'01;}\fi-360\li2880}
+{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'07.;}{\levelnumbers\'01;}\fi-360\li3240}
+{\listlevel\levelnfc0\leveljc0\levelstartat1\levelfollow0{\leveltext \'02\'08.;}{\levelnumbers\'01;}\fi-360\li3600}\listid1}
+{\list\listtemplateid2
+{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow0{\leveltext \'00;}{\levelnumbers;}\fi-432\li432}
+{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow0{\leveltext \'00;}{\levelnumbers;}\fi-576\li576}
+{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow0{\leveltext \'00;}{\levelnumbers;}\fi-720\li720}
+{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow0{\leveltext \'00;}{\levelnumbers;}\fi-864\li864}
+{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow0{\leveltext \'00;}{\levelnumbers;}\fi-1008\li1008}
+{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow0{\leveltext \'00;}{\levelnumbers;}\fi-1152\li1152}
+{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow0{\leveltext \'00;}{\levelnumbers;}\fi-1296\li1296}
+{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow0{\leveltext \'00;}{\levelnumbers;}\fi-1440\li1440}
+{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow0{\leveltext \'00;}{\levelnumbers;}\fi-1584\li1584}\listid2}
+}{\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}{\listoverride\listid2\listoverridecount0\ls2}}{\info{\creatim\yr2013\mo1\dy6\hr13\min7}{\revtim\yr0\mo0\dy0\hr0\min0}{\printim\yr0\mo0\dy0\hr0\min0}{\comment LibreOffice}{\vern3500}}\deftab709
+
+{\*\pgdsctbl
+{\pgdsc0\pgdscuse195\pgwsxn12240\pghsxn15840\marglsxn1134\margrsxn1134\margtsxn1134\margbsxn1134\pgdscnxt0 Default;}}
+\formshade\paperh15840\paperw12240\margl1134\margr1134\margt1134\margb1134\sectd\sbknone\sectunlocked1\pgndec\pgwsxn12240\pghsxn15840\marglsxn1134\margrsxn1134\margtsxn1134\margbsxn1134\ftnbj\ftnstart1\ftnrstcont\ftnnar\aenddoc\aftnrstcont\aftnstart1\aftnnrlc
+\pgndec\pard\plain \s0\nowidctlpar{\*\hyphen2\hyphlead2\hyphtrail2\hyphmax0}\cf0\kerning1\hich\af5\langfe2052\dbch\af6\afs24\alang1081\loch\f3\fs24\lang1033{\rtlch \ltrch\loch
+Installing AltOS software for Mac OS X Computers}
+\par \pard\plain \s0\nowidctlpar{\*\hyphen2\hyphlead2\hyphtrail2\hyphmax0}\cf0\kerning1\hich\af5\langfe2052\dbch\af6\afs24\alang1081\loch\f3\fs24\lang1033{\rtlch \ltrch\loch
+}
+\par \pard\plain \s0\nowidctlpar{\*\hyphen2\hyphlead2\hyphtrail2\hyphmax0}\cf0\kerning1\hich\af5\langfe2052\dbch\af6\afs24\alang1081\loch\f3\fs24\lang1033{\rtlch \ltrch\loch
+The AltOS distribution for Mac OS X consists of:}
+\par \pard\plain \s0\nowidctlpar{\*\hyphen2\hyphlead2\hyphtrail2\hyphmax0}\cf0\kerning1\hich\af5\langfe2052\dbch\af6\afs24\alang1081\loch\f3\fs24\lang1033{\rtlch \ltrch\loch
+}
+\par \pard\plain \s0\nowidctlpar{\*\hyphen2\hyphlead2\hyphtrail2\hyphmax0}\cf0\kerning1\hich\af5\langfe2052\dbch\af6\afs24\alang1081\loch\f3\fs24\lang1033{\listtext\pard\plain  1)\tab}\ilvl0\ls1 \li720\ri0\lin720\rin0\fi-360{\rtlch \ltrch\loch
+The AltosUI application}
+\par \pard\plain \s0\nowidctlpar{\*\hyphen2\hyphlead2\hyphtrail2\hyphmax0}\cf0\kerning1\hich\af5\langfe2052\dbch\af6\afs24\alang1081\loch\f3\fs24\lang1033{\listtext\pard\plain  2)\tab}\ilvl0\ls1 \li720\ri0\lin720\rin0\fi-360{\rtlch \ltrch\loch
+Current AltOS firmware for Altus Metrum products}
+\par \pard\plain \s0\nowidctlpar{\*\hyphen2\hyphlead2\hyphtrail2\hyphmax0}\cf0\kerning1\hich\af5\langfe2052\dbch\af6\afs24\alang1081\loch\f3\fs24\lang1033{\rtlch \ltrch\loch
+}
+\par \pard\plain \s0\nowidctlpar{\*\hyphen2\hyphlead2\hyphtrail2\hyphmax0}\cf0\kerning1\hich\af5\langfe2052\dbch\af6\afs24\alang1081\loch\f3\fs24\lang1033{\rtlch \ltrch\loch
+Install the AltosUI application by dragging it to your Applications folder (or wherever else you want to install it).}
+\par \pard\plain \s0\nowidctlpar{\*\hyphen2\hyphlead2\hyphtrail2\hyphmax0}\cf0\kerning1\hich\af5\langfe2052\dbch\af6\afs24\alang1081\loch\f3\fs24\lang1033{\rtlch \ltrch\loch
+}
+\par \pard\plain \s0\nowidctlpar{\*\hyphen2\hyphlead2\hyphtrail2\hyphmax0}\cf0\kerning1\hich\af5\langfe2052\dbch\af6\afs24\alang1081\loch\f3\fs24\lang1033{\rtlch \ltrch\loch
+The AltOS firmware can be used to update your Altus Metrum products to the latest firmware version, you can copy it to your disk if you like, or simply use it directly from the installation disk image.}
+\par }
\ No newline at end of file
diff --git a/teststand/TestStand.app/Contents/MacOS/JavaApplicationStub b/teststand/TestStand.app/Contents/MacOS/JavaApplicationStub
new file mode 100755 (executable)
index 0000000..c661d3e
Binary files /dev/null and b/teststand/TestStand.app/Contents/MacOS/JavaApplicationStub differ
diff --git a/teststand/TestStand.app/Contents/PkgInfo b/teststand/TestStand.app/Contents/PkgInfo
new file mode 100644 (file)
index 0000000..8a43480
--- /dev/null
@@ -0,0 +1 @@
+APPLAM.O
diff --git a/teststand/TestStand.java b/teststand/TestStand.java
new file mode 100644 (file)
index 0000000..3915138
--- /dev/null
@@ -0,0 +1,646 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package teststand;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import java.io.*;
+import java.util.concurrent.*;
+import org.altusmetrum.altoslib_12.*;
+import org.altusmetrum.altosuilib_12.*;
+
+public class TestStand extends AltosUIFrame implements AltosEepromGrapher {
+       public AltosVoice voice = new AltosVoice();
+
+       public static boolean load_library(Frame frame) {
+               if (!Altos.load_library()) {
+                       JOptionPane.showMessageDialog(frame,
+                                                     String.format("No AltOS library in \"%s\"",
+                                                                   System.getProperty("java.library.path","<undefined>")),
+                                                     "Cannot load device access library",
+                                                     JOptionPane.ERROR_MESSAGE);
+                       return false;
+               }
+               return true;
+       }
+
+       void telemetry_window(AltosDevice device) {
+               try {
+                       AltosFlightReader reader = new AltosTelemetryReader(new AltosSerial(device));
+                       if (reader != null)
+                               new AltosFlightUI(voice, reader, device.getSerial());
+               } catch (FileNotFoundException ee) {
+                       JOptionPane.showMessageDialog(TestStand.this,
+                                                     ee.getMessage(),
+                                                     String.format ("Cannot open %s", device.toShortString()),
+                                                     JOptionPane.ERROR_MESSAGE);
+               } catch (AltosSerialInUseException si) {
+                       JOptionPane.showMessageDialog(TestStand.this,
+                                                     String.format("Device \"%s\" already in use",
+                                                                   device.toShortString()),
+                                                     "Device in use",
+                                                     JOptionPane.ERROR_MESSAGE);
+               } catch (IOException ee) {
+                       JOptionPane.showMessageDialog(TestStand.this,
+                                                     String.format ("Unknown I/O error on %s", device.toShortString()),
+                                                     "Unknown I/O error",
+                                                     JOptionPane.ERROR_MESSAGE);
+               } catch (TimeoutException te) {
+                       JOptionPane.showMessageDialog(this,
+                                                     String.format ("Timeout on %s", device.toShortString()),
+                                                     "Timeout error",
+                                                     JOptionPane.ERROR_MESSAGE);
+               } catch (InterruptedException ie) {
+                       JOptionPane.showMessageDialog(this,
+                                                     String.format("Interrupted %s", device.toShortString()),
+                                                     "Interrupted exception",
+                                                     JOptionPane.ERROR_MESSAGE);
+               }
+       }
+
+       public void scan_device_selected(AltosDevice device) {
+               telemetry_window(device);
+       }
+
+       Container       pane;
+       GridBagLayout   gridbag;
+
+       JButton addButton(int x, int y, String label) {
+               GridBagConstraints      c;
+               JButton                 b;
+
+               c = new GridBagConstraints();
+               c.gridx = x; c.gridy = y;
+               c.fill = GridBagConstraints.BOTH;
+               c.weightx = 1;
+               c.weighty = 1;
+               b = new JButton(label);
+
+               //Dimension ps = b.getPreferredSize();
+
+               gridbag.setConstraints(b, c);
+               add(b, c);
+               return b;
+       }
+
+       /* OSXAdapter interfaces */
+       public void macosx_file_handler(String path) {
+               process_graph(new File(path));
+       }
+
+       public void macosx_quit_handler() {
+               System.exit(0);
+       }
+
+       public void macosx_preferences_handler() {
+               ConfigureTestStand();
+       }
+
+       public TestStand() {
+
+               load_library(null);
+
+               register_for_macosx_events();
+
+               AltosUIPreferences.set_component(this);
+
+               pane = getContentPane();
+               gridbag = new GridBagLayout();
+               pane.setLayout(gridbag);
+
+               JButton b;
+
+               b = addButton(0, 0, "Monitor Flight");
+               b.addActionListener(new ActionListener() {
+                                       public void actionPerformed(ActionEvent e) {
+                                               ConnectToDevice();
+                                       }
+                               });
+               b.setToolTipText("Connect to TeleDongle and monitor telemetry");
+               b = addButton(1, 0, "Save Flight Data");
+               b.addActionListener(new ActionListener() {
+                                       public void actionPerformed(ActionEvent e) {
+                                               SaveFlightData();
+                                       }
+                               });
+               b.setToolTipText("Download and/or delete flight data from an altimeter");
+               b = addButton(2, 0, "Replay Flight");
+               b.addActionListener(new ActionListener() {
+                                       public void actionPerformed(ActionEvent e) {
+                                               Replay();
+                                       }
+                               });
+               b.setToolTipText("Watch an old flight in real-time");
+               b = addButton(3, 0, "Graph Data");
+               b.addActionListener(new ActionListener() {
+                                       public void actionPerformed(ActionEvent e) {
+                                               GraphData();
+                                       }
+                               });
+               b.setToolTipText("Present flight data in a graph and table of statistics");
+               b = addButton(4, 0, "Export Data");
+               b.addActionListener(new ActionListener() {
+                                       public void actionPerformed(ActionEvent e) {
+                                               ExportData();
+                                       }
+                               });
+               b.setToolTipText("Convert flight data for a spreadsheet or GoogleEarth");
+               b = addButton(0, 1, "Configure Altimeter");
+               b.addActionListener(new ActionListener() {
+                                       public void actionPerformed(ActionEvent e) {
+                                               ConfigureTeleMetrum();
+                                       }
+                               });
+               b.setToolTipText("Set flight, storage and communication parameters");
+               b = addButton(1, 1, "Configure TestStand");
+               b.addActionListener(new ActionListener() {
+                               public void actionPerformed(ActionEvent e) {
+                                       ConfigureTestStand();
+                               }
+                       });
+               b.setToolTipText("Global TestStand settings");
+
+               b = addButton(2, 1, "Configure Ground Station");
+               b.addActionListener(new ActionListener() {
+                               public void actionPerformed(ActionEvent e) {
+                                       ConfigureTeleDongle();
+                               }
+                       });
+
+               b = addButton(3, 1, "Flash Image");
+               b.addActionListener(new ActionListener() {
+                               public void actionPerformed(ActionEvent e) {
+                                       FlashImage();
+                               }
+                       });
+               b.setToolTipText("Replace the firmware in any AltusMetrum product");
+
+               b = addButton(4, 1, "Fire Igniter");
+               b.addActionListener(new ActionListener() {
+                               public void actionPerformed(ActionEvent e) {
+                                       FireIgniter();
+                               }
+                       });
+               b.setToolTipText("Remote control of igniters for deployment testing");
+               b = addButton(0, 2, "Scan Channels");
+               b.addActionListener(new ActionListener() {
+                               public void actionPerformed(ActionEvent e) {
+                                       ScanChannels();
+                               }
+                       });
+               b.setToolTipText("Find what channel an altimeter is sending telemetry on");
+               b = addButton(1, 2, "Load Maps");
+               b.addActionListener(new ActionListener() {
+                               public void actionPerformed(ActionEvent e) {
+                                       LoadMaps();
+                               }
+                       });
+               b.setToolTipText("Download satellite images for off-line flight monitoring");
+               b = addButton(2, 2, "Monitor Idle");
+               b.addActionListener(new ActionListener() {
+                               public void actionPerformed(ActionEvent e) {
+                                       IdleMonitor();
+                               }
+                       });
+               b.setToolTipText("Check flight readiness of altimeter in idle mode");
+
+//             b = addButton(3, 2, "Launch Controller");
+//             b.addActionListener(new ActionListener() {
+//                             public void actionPerformed(ActionEvent e) {
+//                                     LaunchController();
+//                             }
+//                     });
+
+               b = addButton(4, 2, "Quit");
+               b.addActionListener(new ActionListener() {
+                               public void actionPerformed(ActionEvent e) {
+                                       System.exit(0);
+                               }
+                       });
+               b.setToolTipText("Close all active windows and terminate TestStand");
+
+               setTitle("AltOS");
+
+               pane.doLayout();
+               pane.validate();
+
+               doLayout();
+               validate();
+
+               setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
+               addWindowListener(new WindowAdapter() {
+                       @Override
+                       public void windowClosing(WindowEvent e) {
+                               System.exit(0);
+                       }
+               });
+
+               setLocationByPlatform(false);
+
+               /* Insets aren't set before the window is visible */
+               setVisible(true);
+       }
+
+       private void ConnectToDevice() {
+               AltosDevice     device = AltosDeviceUIDialog.show(TestStand.this,
+                                                               Altos.product_basestation);
+
+               if (device != null)
+                       telemetry_window(device);
+       }
+
+       void ConfigureCallsign() {
+               String  result;
+               result = JOptionPane.showInputDialog(TestStand.this,
+                                                    "Configure Callsign",
+                                                    AltosUIPreferences.callsign());
+               if (result != null)
+                       AltosUIPreferences.set_callsign(result);
+       }
+
+       void ConfigureTeleMetrum() {
+               new AltosConfigFC(TestStand.this);
+       }
+
+       void ConfigureTeleDongle() {
+               new AltosConfigTD(TestStand.this);
+       }
+
+       void FlashImage() {
+               AltosFlashUI.show(TestStand.this);
+       }
+
+       void FireIgniter() {
+               new AltosIgniteUI(TestStand.this);
+       }
+
+       void ScanChannels() {
+               new AltosScanUI(TestStand.this, true);
+       }
+
+       void LoadMaps() {
+               new AltosUIMapPreload(TestStand.this);
+       }
+
+       void LaunchController() {
+               new AltosLaunchUI(TestStand.this);
+       }
+
+       /*
+        * Replay a flight from telemetry data
+        */
+       private void Replay() {
+               AltosDataChooser chooser = new AltosDataChooser(
+                       TestStand.this);
+
+               AltosRecordSet set = chooser.runDialog();
+               if (set != null) {
+                       AltosReplayReader reader = new AltosReplayReader(set, chooser.file());
+                       new AltosFlightUI(voice, reader);
+               }
+       }
+
+       /* Connect to TeleMetrum, either directly or through
+        * a TeleDongle over the packet link
+        */
+
+       public void graph_flights(AltosEepromList flights) {
+               for (AltosEepromLog flight : flights) {
+                       if (flight.graph_selected && flight.file != null) {
+                               process_graph(flight.file);
+                       }
+               }
+       }
+
+       private void SaveFlightData() {
+               new AltosEepromManage(this, this, AltosLib.product_any);
+       }
+
+       private static AltosFlightSeries make_series(AltosRecordSet set) {
+               AltosFlightSeries series = new AltosFlightSeries(set.cal_data());
+               set.capture_series(series);
+               series.finish();
+               return series;
+       }
+
+       /* Load a flight log file and write out a CSV file containing
+        * all of the data in standard units
+        */
+
+       private void ExportData() {
+               AltosDataChooser chooser;
+               chooser = new AltosDataChooser(this);
+               AltosRecordSet set = chooser.runDialog();
+               if (set == null)
+                       return;
+               AltosFlightSeries series = make_series(set);
+               new AltosCSVUI(TestStand.this, series, chooser.file());
+       }
+
+       /* Load a flight log CSV file and display a pretty graph.
+        */
+
+       private void GraphData() {
+               AltosDataChooser chooser;
+               chooser = new AltosDataChooser(this);
+               AltosRecordSet set = chooser.runDialog();
+               if (set == null)
+                       return;
+               try {
+                       new AltosGraphUI(set, chooser.file());
+               } catch (InterruptedException ie) {
+               } catch (IOException ie) {
+               }
+       }
+
+       private void ConfigureTestStand() {
+               new AltosConfigureUI(TestStand.this, voice);
+       }
+
+       private void IdleMonitor() {
+               try {
+                       new AltosIdleMonitorUI(this);
+               } catch (Exception e) {
+               }
+       }
+
+       static AltosWriter open_csv(File file) {
+               try {
+                       return new AltosCSV(file);
+               } catch (FileNotFoundException fe) {
+                       System.out.printf("%s\n", fe.getMessage());
+                       return null;
+               }
+       }
+
+       static AltosWriter open_kml(File file) {
+               try {
+                       return new AltosKML(file);
+               } catch (FileNotFoundException fe) {
+                       System.out.printf("%s\n", fe.getMessage());
+                       return null;
+               }
+       }
+
+       static AltosRecordSet record_set(File input) {
+               try {
+                       return AltosLib.record_set(input);
+               } catch (IOException ie) {
+                       String message = ie.getMessage();
+                       if (message == null)
+                               message = String.format("%s (I/O error)", input.toString());
+                       System.err.printf("%s: %s\n", input.toString(), message);
+               }
+               return null;
+       }
+
+       static final int process_none = 0;
+       static final int process_csv = 1;
+       static final int process_kml = 2;
+       static final int process_graph = 3;
+       static final int process_replay = 4;
+       static final int process_summary = 5;
+       static final int process_oneline = 6;
+
+       static boolean process_csv(File input) {
+               AltosRecordSet set = record_set(input);
+               if (set == null)
+                       return false;
+
+               File output = Altos.replace_extension(input,".csv");
+               System.out.printf("Processing \"%s\" to \"%s\"\n", input, output);
+               if (input.equals(output)) {
+                       System.out.printf("Not processing '%s'\n", input);
+                       return false;
+               } else {
+                       AltosWriter writer = open_csv(output);
+                       if (writer == null)
+                               return false;
+                       AltosFlightSeries series = make_series(set);
+                       writer.write(series);
+                       writer.close();
+               }
+               return true;
+       }
+
+       static boolean process_kml(File input) {
+               AltosRecordSet set = record_set(input);
+               if (set == null)
+                       return false;
+
+               File output = Altos.replace_extension(input,".kml");
+               System.out.printf("Processing \"%s\" to \"%s\"\n", input, output);
+               if (input.equals(output)) {
+                       System.out.printf("Not processing '%s'\n", input);
+                       return false;
+               } else {
+                       AltosWriter writer = open_kml(output);
+                       if (writer == null)
+                               return false;
+                       AltosFlightSeries series = make_series(set);
+                       series.finish();
+                       writer.write(series);
+                       writer.close();
+                       return true;
+               }
+       }
+
+       static AltosReplayReader replay_file(File file) {
+               AltosRecordSet set = record_set(file);
+               if (set == null)
+                       return null;
+               return new AltosReplayReader(set, file);
+       }
+
+       static boolean process_replay(File file) {
+               AltosReplayReader reader = replay_file(file);
+               if (reader == null)
+                       return false;
+               AltosFlightUI flight_ui = new AltosFlightUI(new AltosVoice(), reader);
+               return true;
+       }
+
+       static boolean process_graph(File file) {
+               AltosRecordSet set = record_set(file);
+               if (set == null)
+                       return false;
+               try {
+                       new AltosGraphUI(set, file);
+                       return true;
+               } catch (InterruptedException ie) {
+               } catch (IOException ie) {
+               }
+               return false;
+       }
+
+       static boolean process_summary(File file) {
+               AltosRecordSet set = record_set(file);
+               if (set == null)
+                       return false;
+               System.out.printf("%s:\n", file.toString());
+               AltosFlightSeries series = make_series(set);
+               AltosFlightStats stats = new AltosFlightStats(series);
+               if (stats.serial != AltosLib.MISSING)
+                       System.out.printf("Serial:       %5d\n", stats.serial);
+               if (stats.flight != AltosLib.MISSING)
+                       System.out.printf("Flight:       %5d\n", stats.flight);
+               if (stats.year != AltosLib.MISSING)
+                       System.out.printf("Date:    %04d-%02d-%02d\n",
+                                         stats.year, stats.month, stats.day);
+               if (stats.hour != AltosLib.MISSING)
+                       System.out.printf("Time:      %02d:%02d:%02d UTC\n",
+                                         stats.hour, stats.minute, stats.second);
+               if (stats.max_height != AltosLib.MISSING)
+                       System.out.printf("Max height:  %6.0f m    %6.0f ft\n",
+                                         stats.max_height,
+                                         AltosConvert.meters_to_feet(stats.max_height));
+               if (stats.max_speed != AltosLib.MISSING)
+                       System.out.printf("Max speed:   %6.0f m/s  %6.0f ft/s  %6.4f Mach\n",
+                                         stats.max_speed,
+                                         AltosConvert.meters_to_feet(stats.max_speed),
+                                         AltosConvert.meters_to_mach(stats.max_speed));
+               if (stats.max_acceleration != AltosLib.MISSING) {
+                       System.out.printf("Max accel:   %6.0f m/s² %6.0f ft/s² %6.2f g\n",
+                                         stats.max_acceleration,
+                                         AltosConvert.meters_to_feet(stats.max_acceleration),
+                                         AltosConvert.meters_to_g(stats.max_acceleration));
+               }
+               if (stats.state_speed[Altos.ao_flight_drogue] != AltosLib.MISSING)
+                       System.out.printf("Drogue rate: %6.0f m/s  %6.0f ft/s\n",
+                                         stats.state_speed[Altos.ao_flight_drogue],
+                                         AltosConvert.meters_to_feet(stats.state_speed[Altos.ao_flight_drogue]));
+               if (stats.state_speed[Altos.ao_flight_main] != AltosLib.MISSING)
+                       System.out.printf("Main rate:   %6.0f m/s  %6.0f ft/s\n",
+                                         stats.state_speed[Altos.ao_flight_main],
+                                         AltosConvert.meters_to_feet(stats.state_speed[Altos.ao_flight_main]));
+               if (stats.landed_time != AltosLib.MISSING &&
+                   stats.boost_time != AltosLib.MISSING &&
+                   stats.landed_time > stats.boost_time)
+                       System.out.printf("Flight time: %6.0f s\n",
+                                         stats.landed_time -
+                                         stats.boost_time);
+               System.out.printf("\n");
+               return true;
+       }
+
+       static boolean process_oneline(File file) {
+               AltosRecordSet set = record_set(file);
+               if (set == null)
+                       return false;
+               System.out.printf("%s", file.toString());
+               AltosFlightSeries series = make_series(set);
+               AltosFlightStats stats = new AltosFlightStats(series);
+               if (stats.max_height != AltosLib.MISSING)
+                       System.out.printf(" height  %6.0f m", stats.max_height);
+               if (stats.max_speed != AltosLib.MISSING)
+                       System.out.printf(" speed   %6.0f m/s", stats.max_speed);
+               if (stats.state_enter_speed[AltosLib.ao_flight_drogue] != AltosLib.MISSING)
+                       System.out.printf(" drogue-deploy   %6.0f m/s", stats.state_enter_speed[AltosLib.ao_flight_drogue]);
+               if (stats.max_acceleration != AltosLib.MISSING)
+                       System.out.printf(" accel   %6.0f m/s²", stats.max_acceleration);
+               System.out.printf("\n");
+               return true;
+       }
+
+       public static void help(int code) {
+               System.out.printf("Usage: teststand [OPTION]... [FILE]...\n");
+               System.out.printf("  Options:\n");
+               System.out.printf("    --replay <filename>\t\trelive the glory of past flights \n");
+               System.out.printf("    --graph <filename>\t\tgraph a flight\n");
+               System.out.printf("    --summary <filename>\t\tText summary of a flight\n");
+               System.out.printf("    --oneline <filename>\t\tOne line summary of a flight\n");
+               System.out.printf("    --csv\tgenerate comma separated output for spreadsheets, etc\n");
+               System.out.printf("    --kml\tgenerate KML output for use with Google Earth\n");
+               System.exit(code);
+       }
+
+       public static void main(final String[] args) {
+               int     errors = 0;
+               load_library(null);
+               try {
+                       UIManager.setLookAndFeel(AltosUIPreferences.look_and_feel());
+               } catch (Exception e) {
+               }
+               TestStand teststand = null;
+
+               /* Handle batch-mode */
+               if (args.length == 0) {
+                       teststand = new TestStand();
+                       java.util.List<AltosDevice> devices = AltosUSBDevice.list(Altos.product_basestation);
+                       if (devices != null)
+                               for (AltosDevice device : devices)
+                                       teststand.telemetry_window(device);
+               } else {
+                       int process = process_none;
+                       for (int i = 0; i < args.length; i++) {
+                               if (args[i].equals("--help"))
+                                       help(0);
+                               else if (args[i].equals("--replay"))
+                                       process = process_replay;
+                               else if (args[i].equals("--kml"))
+                                       process = process_kml;
+                               else if (args[i].equals("--csv"))
+                                       process = process_csv;
+                               else if (args[i].equals("--graph"))
+                                       process = process_graph;
+                               else if (args[i].equals("--summary"))
+                                       process = process_summary;
+                               else if (args[i].equals("--oneline"))
+                                       process = process_oneline;
+                               else if (args[i].startsWith("--"))
+                                       help(1);
+                               else {
+                                       File file = new File(args[i]);
+                                       switch (process) {
+                                       case process_none:
+                                               if (teststand == null)
+                                                       teststand = new TestStand();
+                                       case process_graph:
+                                               if (!process_graph(file))
+                                                       ++errors;
+                                               break;
+                                       case process_replay:
+                                               if (!process_replay(file))
+                                                       ++errors;
+                                               break;
+                                       case process_kml:
+                                               if (!process_kml(file))
+                                                       ++errors;
+                                               break;
+                                       case process_csv:
+                                               if (!process_csv(file))
+                                                       ++errors;
+                                               break;
+                                       case process_summary:
+                                               if (!process_summary(file))
+                                                       ++errors;
+                                               break;
+                                       case process_oneline:
+                                               if (!process_oneline(file))
+                                                       ++errors;
+                                               break;
+                                       }
+                               }
+                       }
+               }
+               if (errors != 0)
+                       System.exit(errors);
+       }
+}
diff --git a/teststand/altosui-fat b/teststand/altosui-fat
new file mode 100755 (executable)
index 0000000..95b1c05
--- /dev/null
@@ -0,0 +1,4 @@
+#!/bin/sh
+me=`which "$0"`
+dir=`dirname "$me"`
+exec java -cp "$dir/*" -Djava.library.path="$dir" -jar "$dir"/altosui-fat.jar  "$@"
diff --git a/teststand/altosuilib.jar b/teststand/altosuilib.jar
new file mode 120000 (symlink)
index 0000000..4bfbb6c
--- /dev/null
@@ -0,0 +1 @@
+../altosuilib/altosuilib.jar
\ No newline at end of file
diff --git a/teststand/altusmetrum-teststand.desktop.in b/teststand/altusmetrum-teststand.desktop.in
new file mode 100644 (file)
index 0000000..1128d57
--- /dev/null
@@ -0,0 +1,11 @@
+[Desktop Entry]
+Type=Application
+Version=1.0
+Name=Test Stand UI
+GenericName=Altus Metrum Test Stand
+Comment=Operate and analayze data from Altus Metrum wireless test stand 
+Icon=%icondir%/altusmetrum-teststand.svg
+Exec=%bindir%/teststand %F
+Terminal=false
+MimeType=application/vnd.altusmetrum.telemetry;application/vnd.altusmetrum.eeprom
+Categories=Education;Electronics;Science;
diff --git a/teststand/altusmetrum.jpg b/teststand/altusmetrum.jpg
new file mode 100644 (file)
index 0000000..0402792
Binary files /dev/null and b/teststand/altusmetrum.jpg differ
diff --git a/teststand/linux-install.sh b/teststand/linux-install.sh
new file mode 100644 (file)
index 0000000..2e44c2a
--- /dev/null
@@ -0,0 +1,244 @@
+#!/bin/sh
+
+can_ask=y
+
+finish()
+{
+    if [ "$can_ask" = "y" ]; then
+       echo ""
+       echo -n "Press enter to continue..."
+       read foo
+    fi
+    exit $1
+}
+
+#
+# Make sure we have a terminal to talk to
+#
+
+if tty -s; then
+    :
+else
+    case "$DISPLAY" in
+       "")
+       echo 'No user input available'
+       can_ask=n
+       ;;
+       *)
+       GUESS_XTERMS="x-terminal-emulator xterm rxvt roxterm gnome-terminal dtterm eterm Eterm kvt konsole aterm"
+        for a in $GUESS_XTERMS; do
+            if type $a >/dev/null 2>&1; then
+                XTERM=$a
+                break
+            fi
+        done
+       case "$XTERM" in
+           "")
+           echo 'No terminal emulator available'
+           can_ask=n
+           ;;
+           *)
+           exec "$XTERM" -e "sh '$0'"
+           ;;
+       esac
+       ;;
+    esac
+fi
+
+#
+# Make sure we can run java
+#
+
+echo -n "Checking for java..."
+
+if java -version > /dev/null 2>&1; then
+    echo " found it."
+else
+    echo " java isn't working."
+    echo ""
+    echo "You'll need to install a java runtime system"
+    echo "on this computer before AltOS will work properly."
+    finish 1
+fi
+    
+#
+# Pick an installation target
+# 
+
+if [ '(' -d /opt -a -w /opt ')' -o '(' -d /opt/AltOS -a -w /opt/AltOS ')' ]; then
+    target_default=/opt
+else
+    target_default="$HOME"
+fi
+
+case "$#" in
+0)
+    echo -n "Installation location [default: $target_default] "
+    if [ "$can_ask" = "y" ]; then
+       read target
+    else
+       echo ""
+       target=""
+    fi
+    case "$target" in
+       "")
+       target="$target_default"
+       ;;
+    esac
+    ;;
+*)
+    target="$1"
+    ;;
+esac
+
+target_altos="$target"/AltOS
+
+echo -n "Installing to $target..."
+
+#
+# Make sure the target exists
+#
+mkdir -p "$target_altos"
+
+if [ ! -d "$target_altos" ]; then
+    echo "$target_altos does not exist and cannot be created"
+    finish 1
+fi
+
+if [ ! -w "$target_altos" ]; then
+    echo "$target_altos cannot be written"
+    finish 1
+fi
+
+#
+# Unpack the tar archive appended to the end of this script
+#
+archive_line=`awk '/^__ARCHIVE_BELOW__/ {print NR + 1; exit 0; }' "$0"`
+
+tail -n+$archive_line "$0" | tar xjf - -C "$target"
+
+case $? in
+0)
+    echo " done."
+    ;;
+*)
+    echo "Install failed."
+    finish 1
+    ;;
+esac
+
+#
+# Create the .desktop file by editing the paths
+#
+
+case "$target" in
+/*)
+    target_abs="$target"
+    ;;
+*)
+    target_abs=`pwd`/$target
+    ;;
+esac
+
+BIN="$target_abs"/AltOS
+
+for infile in "$target"/AltOS/*.desktop.in; do
+    desktop="$target"/AltOS/`basename "$infile" .in`
+    rm -f "$desktop"
+    sed -e "s;%bindir%;$BIN;" -e "s;%icondir%;$BIN;" "$infile" > "$desktop"
+    chmod +x "$desktop"
+done
+
+#
+# Install the .desktop file
+#
+
+for desktop in "$target"/AltOS/*.desktop; do
+    case `id -u` in
+       0)
+           xdg-desktop-menu install --mode system "$desktop"
+           ;;
+       *)
+           xdg-desktop-menu install --mode user "$desktop"
+           ;;
+    esac
+done
+
+#
+# Install mime type file
+#
+
+for mimetype in "$target"/AltOS/*-mimetypes.xml; do
+    case `id -u` in
+       0)
+           xdg-mime install --mode system "$mimetype"
+           ;;
+       *)
+           xdg-mime install --mode user "$mimetype"
+           ;;
+    esac
+done
+
+#
+# Install icons
+#
+
+for icon_dir in /usr/share/icons/hicolor/scalable/mimetypes "$HOME/.icons" "$HOME/.kde/share/icons"; do
+    if [ -w "$icon_dir" ]; then
+       cp "$target"/AltOS/*.svg "$icon_dir"
+       update-icon-caches "$icon_dir"
+    fi
+done
+
+
+#
+# Install icon to desktop if desired
+#
+
+if [ -d $HOME/Desktop ]; then
+    default_desktop=n
+    if [ "$can_ask" = "y" ]; then
+       :
+    else
+       default_desktop=y
+    fi
+
+    answered=n
+    while [ "$answered" = "n" ]; do
+       echo -n "Install icons to desktop? [default: $default_desktop] "
+       if [ "$can_ask" = "y" ]; then
+           read do_desktop
+       else
+           echo
+           do_desktop=""
+       fi
+
+       case "$do_desktop" in
+           "")
+           do_desktop=$default_desktop
+           ;;
+       esac
+
+       case "$do_desktop" in
+       [yYnN]*)
+           answered=y
+           ;;
+       esac
+    done
+
+    case "$do_desktop" in
+       [yY]*)
+           echo -n "Installing desktop icons..."
+           for d in "$target"/AltOS/*.desktop; do
+               base=`basename $d`
+               cp --remove-destination "$d" "$HOME/Desktop/"
+           done
+           ;;
+    esac
+
+    echo " done."
+fi
+
+finish 0
+
+__ARCHIVE_BELOW__
diff --git a/teststand/mdwn.tmpl b/teststand/mdwn.tmpl
new file mode 100644 (file)
index 0000000..b64825a
--- /dev/null
@@ -0,0 +1,7 @@
+[[!inline pages="./%version%/release-notes-%version%.html" rss="no" raw="yes" ]]
+
+- Available Files:
+       - [Windows Installer](/AltOS/releases/%version%/Altos-Windows-%version_dash%.exe)
+       - [Mac OS X Package](/AltOS/releases/%version%/Altos-Mac-%version%.dmg)
+       - [Linux](/AltOS/releases/%version%/Altos-Linux-%version%.sh)
+       - [Source Snapshot](http://git.gag.com/?p=fw/altos;a=snapshot;h=refs/tags/%version%;sf=tgz)
diff --git a/teststand/teststand-windows.nsi.in b/teststand/teststand-windows.nsi.in
new file mode 100644 (file)
index 0000000..a684082
--- /dev/null
@@ -0,0 +1,235 @@
+!addplugindir Instdrv/NSIS/Plugins
+!addincludedir Instdrv/NSIS/Includes
+!include x64.nsh
+!include java.nsh
+!include refresh-sh.nsh
+
+!define REG_NAME       "Altus Metrum"
+!define PROG_ID_EEPROM "altusmetrum.teststand.eeprom.1"
+!define FAT_NAME       "teststand-fat.jar"
+!define WIN_APP_ICON   "altusmetrum-teststand.ico"
+!define WIN_APP_EXE    "altusmetrum-teststand.exe"
+!define WIN_EEPROM_EXE "application-vnd.altusmetrum.eeprom.exe"
+
+Name "${REG_NAME} Installer"
+
+; Default install directory
+InstallDir "$PROGRAMFILES\AltusMetrum"
+
+; Tell the installer where to re-install a new version
+InstallDirRegKey HKLM "Software\${REG_NAME}" "Install_Dir"
+
+LicenseText "GNU General Public License Version 2"
+LicenseData "../COPYING"
+
+; Need admin privs for Vista or Win7
+RequestExecutionLevel admin
+
+ShowInstDetails Show
+
+ComponentText "${REG_NAME} Software and Driver Installer"
+
+Function .onInit
+       DetailPrint "Checking host operating system"
+       ${If} ${RunningX64}
+               DetailPrint "Installer running on 64-bit host"
+               SetRegView 64
+               StrCpy $INSTDIR "$PROGRAMFILES64\AltusMetrum"
+               ${DisableX64FSRedirection}
+       ${EndIf}
+FunctionEnd
+
+Function un.onInit
+       DetailPrint "Checking host operating system"
+       ${If} ${RunningX64}
+               DetailPrint "Installer running on 64-bit host"
+               SetRegView 64
+               StrCpy $INSTDIR "$PROGRAMFILES64\AltusMetrum"
+               ${DisableX64FSRedirection}
+       ${EndIf}
+FunctionEnd
+
+; Pages to present
+
+Page license
+Page components
+Page directory
+Page instfiles
+
+UninstPage uninstConfirm
+UninstPage instfiles
+
+; And the stuff to install
+
+Section "Install Driver" InstDriver
+
+       InstDrv::InitDriverSetup /NOUNLOAD {4D36E96D-E325-11CE-BFC1-08002BE10318} AltusMetrumSerial
+       Pop $0
+       DetailPrint "InitDriverSetup: $0"
+       InstDrv::DeleteOemInfFiles /NOUNLOAD
+       InstDrv::CreateDevice /NOUNLOAD
+
+       SetOutPath $INSTDIR
+       File "../altusmetrum.inf"
+       File "../altusmetrum.cat"
+
+       ${DisableX64FSRedirection}
+       IfFileExists $WINDIR\System32\PnPutil.exe 0 nopnp
+               ${DisableX64FSRedirection}
+               nsExec::ExecToLog '"$WINDIR\System32\PnPutil.exe" -i -a "$INSTDIR\altusmetrum.inf"'
+               Goto done
+nopnp:
+               InstDrv::InstallDriver /NOUNLOAD "$INSTDIR\altusmetrum.inf"
+done:
+
+SectionEnd
+
+Section "${REG_NAME} Application"
+       Call DetectJRE
+
+       SetOutPath $INSTDIR
+
+       File "${FAT_NAME}"
+       File "altoslib_@ALTOSLIB_VERSION@.jar"
+       File "altosuilib_@ALTOSUILIB_VERSION@.jar"
+       File "cmudict04.jar"
+       File "cmulex.jar"
+       File "cmu_time_awb.jar"
+       File "cmutimelex.jar"
+       File "cmu_us_kal.jar"
+       File "en_us.jar"
+       File "freetts.jar"
+       File "jfreechart.jar"
+       File "jcommon.jar"
+       File "../icon/${WIN_APP_EXE}"
+
+       File "*.dll"
+
+       File "../icon/${WIN_APP_ICON}"
+
+       CreateShortCut "$SMPROGRAMS\${REG_NAME}.lnk" "$INSTDIR\${WIN_APP_EXE}" "" "$INSTDIR\${WIN_APP_ICON}"
+SectionEnd
+
+Section "${REG_NAME} Desktop Shortcut"
+       CreateShortCut "$DESKTOP\${REG_NAME}.lnk" "$INSTDIR\${WIN_APP_EXE}"  "" "$INSTDIR\${WIN_APP_ICON}"
+SectionEnd
+
+Section "Firmware"
+
+       SetOutPath $INSTDIR
+
+       File "../src/telemetrum-v1.0/telemetrum-v1.0-${VERSION}.ihx"
+       File "../src/telemetrum-v1.1/telemetrum-v1.1-${VERSION}.ihx"
+       File "../src/telemetrum-v1.2/telemetrum-v1.2-${VERSION}.ihx"
+       File "../src/telemetrum-v2.0/telemetrum-v2.0-${VERSION}.ihx"
+;      File "../src/telemetrum-v3.0/telemetrum-v3.0-${VERSION}.ihx"
+       File "../src/telemini-v1.0/telemini-v1.0-${VERSION}.ihx"
+       File "../src/telemini-v3.0/telemini-v3.0-${VERSION}.ihx"
+       File "../src/telegps-v1.0/telegps-v1.0-${VERSION}.ihx"
+       File "../src/teledongle-v0.2/teledongle-v0.2-${VERSION}.ihx"
+       File "../src/teledongle-v3.0/teledongle-v3.0-${VERSION}.ihx"
+       File "../src/telebt-v1.0/telebt-v1.0-${VERSION}.ihx"
+       File "../src/telebt-v3.0/telebt-v3.0-${VERSION}.ihx"
+       File "../src/telebt-v4.0/telebt-v4.0-${VERSION}.ihx"
+       File "../src/telemega-v1.0/telemega-v1.0-${VERSION}.ihx"
+       File "../src/telemega-v2.0/telemega-v2.0-${VERSION}.ihx"
+       File "../src/easymini-v1.0/easymini-v1.0-${VERSION}.ihx"
+       File "../src/easymini-v2.0/easymini-v2.0-${VERSION}.ihx"
+       File "../src/easymega-v1.0/easymega-v1.0-${VERSION}.ihx"
+
+SectionEnd
+
+Section "Documentation"
+
+       SetOutPath $INSTDIR
+
+       File "../doc/altusmetrum.pdf"
+       File "../doc/altos.pdf"
+       File "../doc/telemetry.pdf"
+       File "../doc/telemetrum-outline.pdf"
+       File "../doc/telemega-outline.pdf"
+       File "../doc/easymini-outline.pdf"
+       File "../doc/telemini-v1-outline.pdf"
+       File "../doc/telemini-v3-outline.pdf"
+SectionEnd
+
+Section "File Associations"
+
+       ${DisableX64FSRedirection}
+
+       SetOutPath $INSTDIR
+
+       File "../icon/${WIN_EEPROM_EXE}"
+
+       DeleteRegKey HKCR "${PROG_ID_EEPROM}"
+
+       DeleteRegKey   HKCR ".eeprom\${PROG_ID_EEPROM}"
+       DeleteRegValue HKCR ".eeprom\OpenWithProgids" "${PROG_ID_EEPROM}"
+       DeleteRegKey   HKCR ".telem\${PROG_ID_EEPROM}"
+       DeleteRegValue HKCR ".telem\OpenWithProgids" "${PROG_ID_EEPROM}"
+
+       ; .eeprom elements
+
+       WriteRegStr HKCR "${PROG_ID_EEPROM}"            ""                              "Altus Metrum Log File"
+       WriteRegStr HKCR "${PROG_ID_EEPROM}"            "FriendlyTypeName"              "Altus Metrum Log File"
+       WriteRegStr HKCR "${PROG_ID_EEPROM}\CurVer"     ""                              "${PROG_ID_EEPROM}"
+       WriteRegStr HKCR "${PROG_ID_EEPROM}\DefaultIcon" ""                             '"$INSTDIR\${WIN_EEPROM_EXE}",-101'
+  WriteRegExpandStr HKCR "${PROG_ID_EEPROM}\shell\open\command" ""                     '"$INSTDIR\${WIN_APP_EXE}" "%1"'
+
+       WriteRegStr HKCR ".eeprom"                      ""                              "${PROG_ID_EEPROM}"
+       WriteRegStr HKCR ".eeprom"                      "PerceivedType"                 "Altus Metrum Log File"
+       WriteRegStr HKCR ".eeprom"                      "Content Type"                  "application/vnd.altusmetrum.eeprom"
+
+       WriteRegStr HKCR ".eeprom\OpenWithProgids"      "${PROG_ID_EEPROM}"             ""
+       WriteRegStr HKCR ".eeprom\${PROG_ID_EEPROM}"    ""                              "${REG_NAME}"
+       
+       Call RefreshShellIcons
+SectionEnd
+       
+Section "Uninstaller"
+
+       ; Deal with the uninstaller
+
+       ${DisableX64FSRedirection}
+       SetOutPath $INSTDIR
+
+       ; Write the install path to the registry
+       WriteRegStr HKLM "SOFTWARE\${REG_NAME}" "Install_Dir" "$INSTDIR"
+
+       ; Write the uninstall keys for windows
+       WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${REG_NAME}" "DisplayName" "${REG_NAME}"
+       WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${REG_NAME}" "UninstallString" '"$INSTDIR\uninstall-${REG_NAME}.exe"'
+       WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${REG_NAME}" "NoModify" "1"
+       WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${REG_NAME}" "NoRepair" "1"
+
+       WriteUninstaller "uninstall-${REG_NAME}.exe"
+SectionEnd
+
+Section "Uninstall"
+
+       ${DisableX64FSRedirection}
+
+       DeleteRegKey   HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${REG_NAME}"
+       DeleteRegKey   HKLM "SOFTWARE\${REG_NAME}"
+
+       DetailPrint "Delete uninstall reg entries"
+
+       DeleteRegKey   HKCR "${PROG_ID_EEPROM}"
+
+       DeleteRegKey   HKCR ".eeprom\${PROG_ID_EEPROM}"
+       DeleteRegValue HKCR ".eeprom\OpenWithProgids" "${PROG_ID_EEPROM}"
+
+       DetailPrint "Delete file association reg entries"
+
+       Delete "$INSTDIR\${FAT_NAME}"
+       Delete "$INSTDIR\uninstall-${REG_NAME}.exe"
+
+       Delete "$INSTDIR\${WIN_APP_ICON}"
+       Delete "$INSTDIR\${WIN_APP_EXE}"
+
+       ; Remove shortcuts, if any
+       Delete "$SMPROGRAMS\${REG_NAME}.lnk"
+       Delete "$DESKTOP\${REG_NAME}.lnk"
+       
+       Call un.RefreshShellIcons
+SectionEnd
diff --git a/teststand/teststand.1 b/teststand/teststand.1
new file mode 100644 (file)
index 0000000..0704d32
--- /dev/null
@@ -0,0 +1,39 @@
+.\"
+.\" Copyright © 2018 Bdale Garbee <bdale@gag.com>
+.\"
+.\" This program is free software; you can redistribute it and/or modify
+.\" it under the terms of the GNU General Public License as published by
+.\" the Free Software Foundation; either version 3 of the License, or
+.\" (at your option) any later version.
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU General Public License along
+.\" with this program; if not, write to the Free Software Foundation, Inc.,
+.\" 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+.\"
+.\"
+.TH ALTOSUI 1 "teststand" ""
+.SH NAME
+teststand \- Wireless Test Stand control program
+.SH SYNOPSIS
+.B "teststand"
+.SH DESCRIPTION
+.I teststand
+connects to a TeleDongle or TeleBT device through a USB serial device.
+It provides a menu-oriented
+user interface to initiate, monitor, record and review rocket motor 
+test stand data.
+.SH USAGE
+With a TeleDongle or TeleBT attached, teststand makes it possible to
+control a remote Altus Metrum wireless test stand board.  
+.SH FILES
+All downloaded test stand data files are recorded into a user-specified 
+directory (default ~/TeleMetrum). Files are named using the current date, 
+the serial number of the reported device, the test number recorded in 
+the data and '.eeprom' for eeprom data.
+.SH AUTHOR
+Bdale Garbee