first cut of teststand app based on cloning telegps
authorBdale Garbee <bdale@gag.com>
Sat, 6 May 2017 15:27:41 +0000 (08:27 -0700)
committerBdale Garbee <bdale@gag.com>
Sat, 6 May 2017 15:27:41 +0000 (08:27 -0700)
18 files changed:
teststand/.gitignore [new file with mode: 0644]
teststand/Info.plist.in [new file with mode: 0644]
teststand/Makefile.am [new file with mode: 0644]
teststand/ReadMe-Mac.rtf [new file with mode: 0644]
teststand/TestStand.java [new file with mode: 0644]
teststand/TestStandConfig.java [new file with mode: 0644]
teststand/TestStandConfigUI.java [new file with mode: 0644]
teststand/TestStandDisplayThread.java [new file with mode: 0644]
teststand/TestStandGraphUI.java [new file with mode: 0644]
teststand/TestStandInfo.java [new file with mode: 0644]
teststand/TestStandPreferences.java [new file with mode: 0644]
teststand/TestStandState.java [new file with mode: 0644]
teststand/TestStandStatus.java [new file with mode: 0644]
teststand/TestStandStatusUpdate.java [new file with mode: 0644]
teststand/altusmetrum-teststand.desktop.in [new file with mode: 0644]
teststand/teststand-fat [new file with mode: 0755]
teststand/teststand-windows.nsi.in [new file with mode: 0644]
teststand/teststand.1 [new file with mode: 0644]

diff --git a/teststand/.gitignore b/teststand/.gitignore
new file mode 100644 (file)
index 0000000..1af9e0f
--- /dev/null
@@ -0,0 +1,27 @@
+windows/
+linux/
+macosx/
+fat/
+Manifest.txt
+Manifest-fat.txt
+AltosVersion.java
+Info.plist
+libaltosJNI
+classes
+telegps
+telegps-test
+telegps-jdb
+classtelegps.stamp
+telegps-windows.nsi
+TeleGPS-Linux-*.tar.bz2
+TeleGPS-Linux-*.sh
+TeleGPS-Mac-*.zip
+TeleGPS-Windows-*.exe
+*.desktop
+telegps-windows.log
+*.dll
+*.dylib
+*.so
+*.jar
+*.class
+*.dmg
diff --git a/teststand/Info.plist.in b/teststand/Info.plist.in
new file mode 100644 (file)
index 0000000..b20cf9a
--- /dev/null
@@ -0,0 +1,61 @@
+<?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>TestStand</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.teststand</string>
+       <key>CFBundleSignature</key>
+       <string>Altu</string>
+       <key>CFBundleGetInfoString</key>
+       <string>TestStand version @VERSION@</string>
+       <key>CFBundleInfoDictionaryVersion</key>
+       <string>6.0</string>
+       <key>CFBundleIconFile</key>
+       <string>altusmetrum-teststand.icns</string>
+       <key>CFBundleDocumentTypes</key>
+       <array>
+         <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>org.altusmetrum.teststand.TestStand</string>
+               <key>JVMVersion</key>
+               <string>1.5+</string>
+               <key>ClassPath</key>
+               <array>
+                       <string>$JAVAROOT/teststand.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/Makefile.am b/teststand/Makefile.am
new file mode 100644 (file)
index 0000000..72eb338
--- /dev/null
@@ -0,0 +1,324 @@
+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=".:classes:../altoslib/*:../altosuilib/*:../libaltos:$(JCOMMON)/jcommon.jar:$(JFREECHART)/jfreechart.jar:$(FREETTS)/freetts.jar"
+
+bin_SCRIPTS=teststand
+
+teststanddir=$(datadir)/java
+
+teststand_JAVA= \
+       TestStand.java \
+       TestStandStatus.java \
+       TestStandStatusUpdate.java \
+       TestStandInfo.java \
+       TestStandState.java \
+       TestStandConfig.java \
+       TestStandConfigUI.java \
+       TestStandPreferences.java \
+       TestStandGraphUI.java \
+       TestStandDisplayThread.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
+
+JAR=teststand.jar
+
+FATJAR=teststand-fat.jar
+
+if MULTI_ARCH
+LIBALTOS_LINUX=libaltos32.so libaltos64.so
+else
+LIBALTOS_LINUX=libaltos.so
+endif
+
+LIBALTOS= \
+       $(LIBALTOS_LINUX) \
+       libaltos.dylib \
+       altos64.dll \
+       altos.dll
+
+ALTOSLIB_CLASS=\
+       altoslib_$(ALTOSLIB_VERSION).jar
+
+ALTOSUILIB_CLASS=\
+       altosuilib_$(ALTOSUILIB_VERSION).jar
+
+# Icons
+ICONDIR=$(top_srcdir)/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  =\
+       ../icon/altusmetrum-teststand.ico \
+       ../icon/altusmetrum-teststand.exe
+       ../icon/application-vnd.altusmetrum.eeprom.ico \
+       ../icon/application-vnd.altusmetrum.eeprom.exe 
+
+MACOSX_ICONS   =\
+       ../icon/altusmetrum-teststand.icns \
+       ../icon/application-vnd.altusmetrum.eeprom.icns
+
+LINUX_ICONS    =\
+       $(ICONDIR)/altusmetrum-altosui.svg \
+       $(ICONDIR)/application-vnd.altusmetrum.eeprom.svg 
+
+LINUX_MIMETYPE =\
+       $(ICONDIR)/org-altusmetrum-mimetypes.xml
+
+# Firmware
+FIRMWARE_TD_3_0=$(top_srcdir)/src/teledongle-v3.0/teledongle-v3.0-$(VERSION).ihx
+FIRMWARE_TD=$(FIRMWARE_TD_3_0)
+
+FIRMWARE_TBT_3_0=$(top_srcdir)/src/telebt-v3.0/telebt-v3.0-$(VERSION).ihx
+FIRMWARE_TBT=$(FIRMWARE_TBT_3_0)
+
+FIRMWARE_TS_1_0=$(top_srcdir)/src/telefiretwo-v1.0/telefiretwo-v1.0-$(VERSION).ihx
+FIRMWARE_TS=$(FIRMWARE_TS_1_0)
+
+FIRMWARE=$(FIRMWARE_TS) $(FIRMWARE_TD) $(FIRMWARE_TBT)
+
+desktopdir = $(datadir)/applications
+desktop_file = altusmetrum-teststand.desktop
+desktop_SCRIPTS = $(desktop_file)
+
+all-local: teststand-test teststand-jdb $(JAR)
+
+clean-local:
+       -rm -rf classes $(JAR) $(FATJAR) \
+               TestStand-Linux-*.tar.bz2 TestStand-Mac-*.dmg TestStand-Windows-*.exe \
+               altoslib_*.jar altosuilib_*.jar \
+               $(JFREECHART_CLASS) $(JCOMMON_CLASS) $(FREETTS_CLASS) $(LIBALTOS) Manifest.txt Manifest-fat.txt \
+               teststand teststand-test teststand-jdb macosx linux windows teststand-windows.log \
+               teststand-windows.nsi *.desktop
+
+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 $@
+
+LINUX_DIST=TestStand-Linux-$(VERSION).tar.bz2
+LINUX_SH=TestStand-Linux-$(VERSION).sh
+MACOSX_DIST=TestStand-Mac-$(VERSION).dmg
+WINDOWS_DIST=TestStand-Windows-$(VERSION_DASH).exe
+
+TELEGPS_DOC=$(top_srcdir)/doc/teststand.pdf
+
+DOC=$(TELEGPS_DOC)
+
+FAT_FILES=$(FATJAR) $(ALTOSLIB_CLASS) $(ALTOSUILIB_CLASS) $(FREETTS_CLASS) $(JFREECHART_CLASS) $(JCOMMON_CLASS)
+
+LINUX_FILES=$(FAT_FILES) $(LIBALTOS_LINUX) $(FIRMWARE) $(DOC) $(desktop_file).in $(LINUX_ICONS) $(LINUX_MIMETYPE)
+LINUX_EXTRA=teststand-fat $(desktop_file).in
+
+MACOSX_INFO_PLIST=Info.plist
+MACOSX_README=ReadMe-Mac.rtf
+MACOSX_FILES=$(FAT_FILES) libaltos.dylib $(MACOSX_INFO_PLIST) $(MACOSX_README) $(DOC) $(MACOSX_ICONS)
+MACOSX_EXTRA=$(FIRMWARE)
+
+WINDOWS_FILES=$(FAT_FILES) altos.dll altos64.dll $(top_srcdir)/altusmetrum.inf $(top_srcdir)/altusmetrum.cat $(DOC) $(WINDOWS_ICONS)
+
+if FATINSTALL
+
+FATTARGET=$(FATDIR)/$(VERSION)
+
+LINUX_DIST_TARGET=$(FATTARGET)/$(LINUX_DIST)
+LINUX_SH_TARGET=$(FATTARGET)/$(LINUX_SH)
+MACOSX_DIST_TARGET=$(FATTARGET)/$(MACOSX_DIST)
+WINDOWS_DIST_TARGET=$(FATTARGET)/$(WINDOWS_DIST)
+
+fat: $(LINUX_DIST_TARGET) $(LINUX_SH_TARGET) $(MACOSX_DIST_TARGET) $(WINDOWS_DIST_TARGET)
+
+$(LINUX_DIST_TARGET): $(LINUX_DIST)
+       mkdir -p $(FATTARGET)
+       cp -p $< $@
+
+$(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 $< $@
+
+else
+fat: $(LINUX_DIST) $(LINUX_SH) $(MACOSX_DIST) $(WINDOWS_DIST)
+endif
+
+teststand: Makefile
+       echo "#!/bin/sh" > $@
+       echo 'exec java  -Djava.library.path="$(altoslibdir)" -jar "$(teststanddir)/teststand.jar" "$$@"' >> $@
+       chmod +x $@
+
+teststand-jdb: Makefile
+       echo "#!/bin/sh" > $@
+       echo 'exec jdb -classpath "classes:./*:../libaltos:$(JCOMMON)/jcommon.jar:$(JFREECHART)/jfreechart.jar" -Djava.library.path="../libaltos/.libs" org.altusmetrum.teststand.TestStand "$$@"' >> $@
+       chmod +x $@
+
+teststand-test: Makefile
+       echo "#!/bin/sh" > $@
+       echo 'exec java -Djava.library.path="../libaltos/.libs" -jar teststand.jar "$$@"' >> $@
+       chmod +x $@
+
+install-teststandJAVA: teststand.jar
+       @$(NORMAL_INSTALL)
+       test -z "$(teststanddir)" || $(MKDIR_P) "$(DESTDIR)$(teststanddir)"
+       echo " $(INSTALL_DATA)" "$<" "'$(DESTDIR)$(teststanddir)/teststand.jar'"; \
+       $(INSTALL_DATA) "$<" "$(DESTDIR)$(teststanddir)"
+
+$(JAR): classteststand.stamp Manifest.txt $(JAVA_ICONS) $(ALTOSLIB_CLASS) $(ALTOSUILIB_CLASS)
+       jar cfm $@ Manifest.txt \
+               $(ICONJAR) \
+               -C classes org \
+               -C ../libaltos libaltosJNI
+
+$(FATJAR): classteststand.stamp Manifest-fat.txt $(ALTOSLIB_CLASS) $(ALTOSUILIB_CLASS) $(JFREECHART_CLASS) $(JCOMMON_CLASS) $(JAVA_ICONS)
+       jar cfm $@ Manifest-fat.txt \
+               $(ICONJAR) \
+               -C classes org \
+               -C ../libaltos libaltosJNI
+
+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/TestStand
+       cp -p $(LINUX_FILES) linux/TestStand
+       cp -p teststand-fat linux/TestStand/teststand
+       chmod +x linux/TestStand/teststand
+       tar cjf $@ -C linux TestStand
+
+$(LINUX_SH): $(LINUX_DIST) $(srcdir)/../altosui/linux-install.sh
+       sed 's/AltOS/TestStand/g' $(srcdir)/../altosui/linux-install.sh | cat - $(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 $(MACOSX_README) 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 TestStand-$(VERSION) -no-pad -r -apple -o $@ macosx
+
+$(WINDOWS_DIST): $(WINDOWS_FILES) teststand-windows.nsi
+       -rm -f $@
+       makensis -Oteststand-windows.log "-XOutFile $@" "-DVERSION=$(VERSION)" teststand-windows.nsi || (cat teststand-windows.log && exit 1)
+
+Manifest.txt: Makefile
+       echo 'Main-Class: org.altusmetrum.teststand.TestStand' > $@
+       echo "Class-Path: $(ALTOSLIB_CLASS) $(ALTOSUILIB_CLASS) $(FREETTS)/freetts.jar $(JCOMMON)/jcommon.jar $(JFREECHART)/jfreechart.jar" >> $@
+
+Manifest-fat.txt:
+       echo 'Main-Class: org.altusmetrum.teststand.TestStand' > $@
+       echo "Class-Path: $(ALTOSLIB_CLASS) $(ALTOSUILIB_CLASS) freetts.jar jcommon.jar jfreechart.jar" >> $@
+
diff --git a/teststand/ReadMe-Mac.rtf b/teststand/ReadMe-Mac.rtf
new file mode 100644 (file)
index 0000000..48c0de5
--- /dev/null
@@ -0,0 +1,19 @@
+{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf510
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\margl1440\margr1440\vieww10800\viewh8400\viewkind0
+\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural
+
+\f0\fs24 \cf0 Installing TestStand software for Mac OS X computers\
+\
+There are two files included in the Mac OS X distribution:\
+\
+ 1) The TestStand application\
+\
+ 2) The FTDI device drivers\
+\
+As with most Mac OS X applications, install TestStand by dragging it from the distribution disk image to a suitable place on your computer.\
+\
+To communicate with the TestStand serial adapter, you need to installed the FTDI device drivers, which is done by double-clicking on the FTDIUSBSerialDriver disk image. Inside that is the FTDI USB Serial Driver package. Double click on that and it will guide you through the installation process.\
+\
+Thanks for choosing AltusMetrum products!}
diff --git a/teststand/TestStand.java b/teststand/TestStand.java
new file mode 100644 (file)
index 0000000..e8f1259
--- /dev/null
@@ -0,0 +1,749 @@
+/*
+ * Copyright © 2017 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 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 org.altusmetrum.teststand;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import java.io.*;
+import java.util.concurrent.*;
+import java.util.*;
+import java.text.*;
+import org.altusmetrum.altoslib_11.*;
+import org.altusmetrum.altosuilib_11.*;
+
+public class TestStand
+       extends AltosUIFrame
+       implements AltosFlightDisplay, AltosFontListener, AltosUnitsListener, ActionListener
+{
+
+       static String[] teststand_icon_names = {
+               "/altusmetrum-teststand-16.png",
+               "/altusmetrum-teststand-32.png",
+               "/altusmetrum-teststand-48.png",
+               "/altusmetrum-teststand-64.png",
+               "/altusmetrum-teststand-128.png",
+               "/altusmetrum-teststand-256.png"
+       };
+
+       static { set_icon_names(teststand_icon_names); }
+
+       static AltosVoice       voice;
+
+       static AltosVoice voice() {
+               if (voice == null)
+                       voice = new AltosVoice();
+               return voice;
+       }
+
+       AltosFlightReader       reader;
+       TestStandDisplayThread  thread;
+       boolean                 idle_mode;
+
+       JMenuBar                menu_bar;
+
+       JMenu                   file_menu;
+       JMenu                   monitor_menu;
+       JMenu                   device_menu;
+       AltosUIFreqList         frequencies;
+       ActionListener          frequency_listener;
+       AltosUIRateList         rates;
+       ActionListener          rate_listener;
+
+       Container               bag;
+
+       TestStandStatus         teststand_status;
+       TestStandStatusUpdate   status_update;
+
+       JTabbedPane             pane;
+
+       AltosUIMap              map;
+       TestStandInfo           gps_info;
+       TestStandState          gps_state;
+       AltosInfoTable          info_table;
+
+       LinkedList<AltosFlightDisplay>  displays;
+
+       /* File menu */
+       final static String     new_command = "new";
+       final static String     graph_command = "graph";
+       final static String     export_command = "export";
+       final static String     load_maps_command = "loadmaps";
+       final static String     preferences_command = "preferences";
+       final static String     close_command = "close";
+       final static String     exit_command = "exit";
+
+       static final String[][] file_menu_entries = new String[][] {
+               { "Graph Data",         graph_command },
+               { "Export Data",        export_command },
+               { "Load Maps",          load_maps_command },
+               { "Preferences",        preferences_command },
+               { "Close",              close_command },
+               { "Exit",               exit_command },
+       };
+
+       /* Monitor menu */
+       final static String     connect_command = "connect";
+       final static String     disconnect_command = "disconnect";
+       final static String     scan_command = "scan";
+
+       static final String[][] monitor_menu_entries = new String[][] {
+               { "Connect Device",     connect_command },
+               { "Disconnect",         disconnect_command },
+               { "Scan Channels",      scan_command },
+       };
+
+       /* Device menu */
+       final static String     download_command = "download";
+       final static String     configure_command = "configure";
+       final static String     flash_command = "flash";
+
+       static final String[][] device_menu_entries = new String[][] {
+               { "Download Data",      download_command },
+               { "Configure Device",   configure_command },
+               { "Flash Device",       flash_command },
+       };
+
+       void stop_display() {
+               if (thread != null && thread.isAlive()) {
+                       thread.interrupt();
+                       try {
+                               thread.join();
+                       } catch (InterruptedException ie) {}
+               }
+               thread = null;
+       }
+
+       public void reset() {
+               for (AltosFlightDisplay display : displays)
+                       display.reset();
+       }
+
+       public void font_size_changed(int font_size) {
+               for (AltosFlightDisplay display : displays)
+                       display.font_size_changed(font_size);
+       }
+
+       public void units_changed(boolean imperial_units) {
+               for (AltosFlightDisplay display : displays)
+                       display.units_changed(imperial_units);
+       }
+
+       public void show(AltosState state, AltosListenerState listener_state) {
+               try {
+                       status_update.saved_state = state;
+                       status_update.saved_listener_state = listener_state;
+
+                       if (state == null)
+                               state = new AltosState();
+
+                       int i = 0;
+                       for (AltosFlightDisplay display : displays) {
+                               display.show(state, listener_state);
+                               i++;
+                       }
+               } catch (Exception ex) {
+                       System.out.printf("Exception %s\n", ex.toString());
+                       for (StackTraceElement e : ex.getStackTrace())
+                               System.out.printf("%s\n", e.toString());
+               }
+       }
+
+       void preferences() {
+               new TestStandPreferences(this, voice());
+       }
+
+       void load_maps() {
+               new AltosUIMapPreload(this);
+       }
+
+       void disconnect() {
+               setTitle("TestStand");
+               stop_display();
+               teststand_status.stop();
+
+               teststand_status.disable_receive();
+               disable_frequency_menu();
+               disable_rate_menu();
+       }
+
+       void connect_flight(AltosDevice device) {
+               try {
+                       AltosFlightReader       reader = new AltosTelemetryReader(new AltosSerial(device));
+                       set_reader(reader, device, false);
+               } catch (FileNotFoundException ee) {
+                       JOptionPane.showMessageDialog(this,
+                                                     ee.getMessage(),
+                                                     String.format ("Cannot open %s", device.toShortString()),
+                                                     JOptionPane.ERROR_MESSAGE);
+               } catch (AltosSerialInUseException si) {
+                       JOptionPane.showMessageDialog(this,
+                                                     String.format("Device \"%s\" already in use",
+                                                                   device.toShortString()),
+                                                     "Device in use",
+                                                     JOptionPane.ERROR_MESSAGE);
+               } catch (IOException ee) {
+                       JOptionPane.showMessageDialog(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);
+               }
+       }
+
+       void connect_idle(AltosDevice device) {
+               try {
+                               AltosFlightReader       reader = new AltosIdleReader(new AltosSerial(device), false);
+                       set_reader(reader, device, true);
+               } catch (FileNotFoundException ee) {
+                       JOptionPane.showMessageDialog(this,
+                                                     ee.getMessage(),
+                                                     String.format ("Cannot open %s", device.toShortString()),
+                                                     JOptionPane.ERROR_MESSAGE);
+               } catch (AltosSerialInUseException si) {
+                       JOptionPane.showMessageDialog(this,
+                                                     String.format("Device \"%s\" already in use",
+                                                                   device.toShortString()),
+                                                     "Device in use",
+                                                     JOptionPane.ERROR_MESSAGE);
+               } catch (IOException ee) {
+                       JOptionPane.showMessageDialog(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);
+               }
+       }
+
+       void connect(AltosDevice device) {
+               if (reader != null)
+                       disconnect();
+               if (device.matchProduct(AltosLib.product_basestation))
+                       connect_flight(device);
+               else
+                       connect_idle(device);
+       }
+
+       void connect() {
+               AltosDevice     device = AltosDeviceUIDialog.show(this,
+                                                                 AltosLib.product_any);
+               if (device == null)
+                       return;
+               connect(device);
+       }
+
+       public void scan_device_selected(AltosDevice device) {
+               connect(device);
+       }
+
+       void scan() {
+               new AltosScanUI(this, false);
+       }
+
+       void download(){
+               new AltosEepromManage(this, AltosLib.product_any);
+       }
+
+       void configure() {
+               new TestStandConfig(this);
+       }
+
+       void export() {
+               AltosDataChooser chooser;
+               chooser = new AltosDataChooser(this);
+               AltosStateIterable states = chooser.runDialog();
+               if (states == null)
+                       return;
+               new AltosCSVUI(this, states, chooser.file());
+       }
+
+       void graph() {
+               AltosDataChooser chooser;
+               chooser = new AltosDataChooser(this);
+               AltosStateIterable states = chooser.runDialog();
+               if (states == null)
+                       return;
+               try {
+                       new TestStandGraphUI(states, chooser.file());
+               } catch (InterruptedException ie) {
+               } catch (IOException ie) {
+               }
+       }
+
+       void flash() {
+               AltosFlashUI.show(this);
+       }
+
+       public void actionPerformed(ActionEvent ev) {
+
+               /* File menu */
+               if (preferences_command.equals(ev.getActionCommand())) {
+                       preferences();
+                       return;
+               }
+               if (load_maps_command.equals(ev.getActionCommand())) {
+                       load_maps();
+                       return;
+               }
+               if (close_command.equals(ev.getActionCommand())) {
+                       close();
+                       return;
+               }
+               if (exit_command.equals(ev.getActionCommand()))
+                       System.exit(0);
+
+               /* Monitor menu */
+               if (connect_command.equals(ev.getActionCommand())) {
+                       connect();
+                       return;
+               }
+               if (disconnect_command.equals(ev.getActionCommand())) {
+                       disconnect();
+                       return;
+               }
+               if (scan_command.equals(ev.getActionCommand())) {
+                       scan();
+                       return;
+               }
+
+               /* Device menu */
+               if (download_command.equals(ev.getActionCommand())) {
+                       download();
+                       return;
+               }
+               if (configure_command.equals(ev.getActionCommand())) {
+                       configure();
+                       return;
+               }
+               if (export_command.equals(ev.getActionCommand())) {
+                       export();
+                       return;
+               }
+               if (graph_command.equals(ev.getActionCommand())) {
+                       graph();
+                       return;
+               }
+               if (flash_command.equals(ev.getActionCommand())) {
+                       flash();
+                       return;
+               }
+       }
+
+       void enable_frequency_menu(int serial, final AltosFlightReader reader) {
+
+               if (frequency_listener != null)
+                       disable_frequency_menu();
+
+               frequency_listener = 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();
+                               }
+                       };
+
+               frequencies.addActionListener(frequency_listener);
+               frequencies.set_product("Monitor");
+               frequencies.set_serial(serial);
+               frequencies.set_frequency(AltosUIPreferences.frequency(serial));
+               frequencies.setEnabled(true);
+
+       }
+
+       void disable_frequency_menu() {
+               if (frequency_listener != null) {
+                       frequencies.removeActionListener(frequency_listener);
+                       frequencies.setEnabled(false);
+                       frequency_listener = null;
+               }
+
+       }
+
+       void enable_rate_menu(int serial, final AltosFlightReader reader) {
+
+               if (rate_listener != null)
+                       disable_rate_menu();
+
+               rate_listener = 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.addActionListener(rate_listener);
+               rates.set_product("Monitor");
+               rates.set_serial(serial);
+               rates.set_rate(AltosUIPreferences.telemetry_rate(serial));
+               rates.setEnabled(reader.supports_telemetry_rate(AltosLib.ao_telemetry_rate_2400));
+       }
+
+       void disable_rate_menu() {
+               if (rate_listener != null) {
+                       rates.removeActionListener(rate_listener);
+                       rates.setEnabled(false);
+                       rate_listener = null;
+               }
+
+       }
+
+       public void set_reader(AltosFlightReader reader, AltosDevice device, boolean idle_mode) {
+               this.idle_mode = idle_mode;
+               status_update = new TestStandStatusUpdate(teststand_status);
+
+               teststand_status.start(status_update);
+
+               setTitle(String.format("TestStand %s", reader.name));
+               thread = new TestStandDisplayThread(this, voice(), this, reader);
+               thread.start();
+
+               if (device != null) {
+                       if (idle_mode) {
+                               disable_frequency_menu();
+                               disable_rate_menu();
+                       } else {
+                               enable_frequency_menu(device.getSerial(), reader);
+                               enable_rate_menu(device.getSerial(), reader);
+                       }
+               }
+       }
+
+       static int      number_of_windows;
+
+       static public void add_window() {
+               ++number_of_windows;
+       }
+
+       static public void subtract_window() {
+               --number_of_windows;
+               if (number_of_windows == 0)
+                       System.exit(0);
+       }
+
+       private void close() {
+               disconnect();
+               AltosUIPreferences.unregister_font_listener(this);
+               AltosPreferences.unregister_units_listener(this);
+               setVisible(false);
+               dispose();
+               subtract_window();
+       }
+
+       private void add_menu(JMenu menu, String label, String action) {
+               JMenuItem       item = new JMenuItem(label);
+               menu.add(item);
+               item.addActionListener(this);
+               item.setActionCommand(action);
+       }
+
+
+       private JMenu make_menu(String label, String[][] items) {
+               JMenu   menu = new JMenu(label);
+               for (int i = 0; i < items.length; i++) {
+                       if (MAC_OS_X) {
+                               if (items[i][1].equals("exit"))
+                                       continue;
+                               if (items[i][1].equals("preferences"))
+                                       continue;
+                       }
+                       add_menu(menu, items[i][0], items[i][1]);
+               }
+               menu_bar.add(menu);
+               return menu;
+       }
+
+       /* 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() {
+               preferences();
+       }
+
+       public TestStand() {
+
+               AltosUIPreferences.set_component(this);
+
+               register_for_macosx_events();
+
+               reader = null;
+
+               bag = getContentPane();
+               bag.setLayout(new GridBagLayout());
+
+               setTitle("TestStand");
+
+               menu_bar = new JMenuBar();
+               setJMenuBar(menu_bar);
+
+               file_menu = make_menu("File", file_menu_entries);
+               monitor_menu = make_menu("Monitor", monitor_menu_entries);
+               device_menu = make_menu("Device", device_menu_entries);
+
+               set_inset(3);
+               frequencies = new AltosUIFreqList();
+               frequencies.setEnabled(false);
+               bag.add(frequencies, constraints (0, 1));
+
+               rates = new AltosUIRateList();
+               rates.setEnabled(false);
+               bag.add(rates, constraints(1, 1));
+               next_row();
+               set_inset(0);
+
+               displays = new LinkedList<AltosFlightDisplay>();
+
+               int serial = -1;
+
+               /* TestStand status is always visible */
+               teststand_status = new TestStandStatus();
+               bag.add(teststand_status, constraints(0, 3, GridBagConstraints.HORIZONTAL));
+               next_row();
+
+               displays.add(teststand_status);
+
+
+               /* The rest of the window uses a tabbed pane to
+                * show one of the alternate data views
+                */
+               pane = new JTabbedPane();
+
+               /* Make the tabbed pane use the rest of the window space */
+               bag.add(pane, constraints(0, 3, GridBagConstraints.BOTH));
+
+               map = new AltosUIMap();
+               pane.add(map.getName(), map);
+               displays.add(map);
+
+               gps_info = new TestStandInfo();
+               pane.add(gps_info.getName(), gps_info);
+               displays.add(gps_info);
+
+               gps_state = new TestStandState();
+               pane.add(gps_state.getName(), gps_state);
+               displays.add(gps_state);
+
+               info_table = new AltosInfoTable();
+               pane.add("Table", info_table);
+               displays.add(info_table);
+
+               setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
+
+               AltosUIPreferences.register_font_listener(this);
+               AltosPreferences.register_units_listener(this);
+
+               addWindowListener(new WindowAdapter() {
+                               @Override
+                               public void windowClosing(WindowEvent e) {
+                                       close();
+                               }
+                       });
+
+               pack();
+               setVisible(true);
+
+               add_window();
+       }
+
+       public TestStand(AltosFlightReader reader, boolean idle_mode) {
+               this();
+               set_reader(reader, null, idle_mode);
+       }
+
+       public TestStand(AltosDevice device) {
+               this();
+               connect(device);
+       }
+
+       static AltosStateIterable record_iterable(File file) {
+               FileInputStream in;
+               try {
+                       in = new FileInputStream(file);
+               } catch (Exception e) {
+                       System.out.printf("Failed to open file '%s'\n", file);
+                       return null;
+               }
+               if (file.getName().endsWith("telem"))
+                       return new AltosTelemetryFile(in);
+               else
+                       return new AltosEepromFile(in);
+       }
+
+       static AltosReplayReader replay_file(File file) {
+               AltosStateIterable states = record_iterable(file);
+               if (states == null)
+                       return null;
+               return new AltosReplayReader(states.iterator(), file);
+       }
+
+       static boolean process_graph(File file) {
+               AltosStateIterable states = record_iterable(file);
+               if (states == null)
+                       return false;
+               try {
+                       new TestStandGraphUI(states, file);
+               } catch (Exception e) {
+                       return false;
+               }
+               return true;
+       }
+
+       static boolean process_replay(File file) {
+               AltosReplayReader new_reader = replay_file(file);
+               if (new_reader == null)
+                       return false;
+
+               new TestStand(new_reader, true);
+               return true;
+       }
+
+       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_cat = 6;
+
+       public static boolean load_library(Frame frame) {
+               if (!AltosUILib.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;
+       }
+
+       public static void help(int code) {
+               System.out.printf("Usage: altosui [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("    --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(String[] args) {
+               int     errors = 0;
+
+               load_library(null);
+               try {
+                       UIManager.setLookAndFeel(AltosUIPreferences.look_and_feel());
+               } catch (Exception e) {
+               }
+
+               boolean any_created = false;
+
+
+               /* Handle batch-mode */
+               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("--cat"))
+                               process = process_cat;
+                       else if (args[i].startsWith("--"))
+                               help(1);
+                       else {
+                               File file = new File(args[i]);
+                               switch (process) {
+                               case process_none:
+                               case process_graph:
+                                       if (!process_graph(file))
+                                               ++errors;
+                                       break;
+                               case process_replay:
+                                       if (!process_replay(file))
+                                               ++errors;
+                                       any_created = true;
+                                       break;
+                               case process_kml:
+                                       ++errors;
+                                       break;
+                               case process_csv:
+                                       ++errors;
+                                       break;
+                               case process_summary:
+                                       ++errors;
+                                       break;
+                               case process_cat:
+                                       ++errors;
+                               }
+                       }
+               }
+               if (errors != 0)
+                       System.exit(errors);
+               if (number_of_windows == 0) {
+                       java.util.List<AltosDevice> devices = AltosUSBDevice.list(AltosLib.product_basestation);
+                       if (devices != null)
+                               for (AltosDevice device : devices) {
+                                       new TestStand(device);
+                                       any_created = true;
+                               }
+                       if (number_of_windows == 0)
+                               new TestStand();
+               }
+       }
+}
diff --git a/teststand/TestStandConfig.java b/teststand/TestStandConfig.java
new file mode 100644 (file)
index 0000000..b1a3c0e
--- /dev/null
@@ -0,0 +1,298 @@
+/*
+ * Copyright © 2017 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 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 org.altusmetrum.teststand;
+
+import java.awt.event.*;
+import javax.swing.*;
+import java.io.*;
+import java.util.concurrent.*;
+import java.text.*;
+import org.altusmetrum.altoslib_11.*;
+import org.altusmetrum.altosuilib_11.*;
+
+public class TestStandConfig 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;
+
+       AltosConfigData data;
+       TestStandConfigUI       config_ui;
+       boolean         serial_started;
+       boolean         made_visible;
+
+       void start_serial() throws InterruptedException, TimeoutException {
+               serial_started = true;
+       }
+
+       void stop_serial() throws InterruptedException {
+               if (!serial_started)
+                       return;
+               serial_started = false;
+       }
+
+       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 {
+               TestStandConfig 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, false);
+                       } 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(TestStandConfig 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 TestStandConfigUI(owner);
+               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();
+                       }
+               } catch (InterruptedException ie) {
+                       abort();
+               } catch (TimeoutException te) {
+                       abort();
+               }
+       }
+
+       public TestStandConfig(JFrame given_owner) {
+               owner = given_owner;
+
+               device = AltosDeviceUIDialog.show(owner, AltosLib.product_any);
+               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/TestStandConfigUI.java b/teststand/TestStandConfigUI.java
new file mode 100644 (file)
index 0000000..3a22021
--- /dev/null
@@ -0,0 +1,945 @@
+/*
+ * Copyright © 2017 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 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 org.altusmetrum.teststand;
+
+import java.text.*;
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import org.altusmetrum.altoslib_11.*;
+import org.altusmetrum.altosuilib_11.*;
+
+public class TestStandConfigUI
+       extends AltosUIDialog
+       implements ActionListener, ItemListener, DocumentListener, AltosConfigValues, AltosUnitsListener
+{
+
+       Container               pane;
+       JLabel                  product_label;
+       JLabel                  version_label;
+       JLabel                  serial_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                  callsign_label;
+       JLabel                  tracker_motion_label;
+       JLabel                  tracker_interval_label;
+
+       public boolean          dirty;
+
+       JFrame                  owner;
+       JLabel                  product_value;
+       JLabel                  version_value;
+       JLabel                  serial_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;
+       JTextField              callsign_value;
+       JComboBox<String>       tracker_motion_value;
+       JComboBox<String>       tracker_interval_value;
+
+       JButton                 save;
+       JButton                 reset;
+       JButton                 reboot;
+       JButton                 close;
+
+       ActionListener          listener;
+
+       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[]         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 {
+               TestStandConfigUI       ui;
+
+               public ConfigListener(TestStandConfigUI this_ui) {
+                       ui = this_ui;
+               }
+
+               public void windowClosing(WindowEvent e) {
+                       ui.actionPerformed(new ActionEvent(e.getSource(),
+                                                          ActionEvent.ACTION_PERFORMED,
+                                                          "Close"));
+               }
+       }
+
+       public void set_pyros(AltosPyro[] new_pyros) {
+       }
+
+       public AltosPyro[] pyros() {
+               return null;
+       }
+
+       public void set_pyro_firing_time(double new_pyro_firing_time) {
+       }
+
+       public double pyro_firing_time() {
+               return -1;
+       }
+
+       boolean is_telemetrum() {
+               String  product = product_value.getText();
+               return product != null && product.startsWith("TestStand");
+       }
+
+       void set_radio_enable_tool_tip() {
+               if (radio_enable_value.isEnabled())
+                       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.isEnabled())
+                       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.isEnabled())
+                       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.isEnabled())
+                       aprs_ssid_value.setToolTipText("Set the APRS SSID (secondary station identifier)");
+               else if (aprs_ssid_value.isEnabled())
+                       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.isEnabled())
+                       aprs_format_value.setToolTipText("Set the APRS format (compressed/uncompressed)");
+               else if (aprs_format_value.isEnabled())
+                       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.isEnabled())
+                       flight_log_max_value.setToolTipText("Size reserved for each flight log (in kB)");
+               else
+                       flight_log_max_value.setToolTipText("Cannot set max value with flight logs in memory");
+       }
+
+       /* Build the UI using a grid bag */
+       public TestStandConfigUI(JFrame in_owner) {
+               super (in_owner, "Configure Device", 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++;
+
+               /* 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 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++;
+
+               /* 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++;
+
+               /* 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;
+       }
+
+       void set_dirty() {
+               dirty = true;
+               save.setEnabled(true);
+       }
+
+       public void set_clean() {
+               dirty = false;
+               save.setEnabled(false);
+       }
+
+       public void 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("Close") || cmd.equals("Reboot"))
+                       if (!check_dirty(cmd))
+                               return;
+               listener.actionPerformed(e);
+               if (cmd.equals("Close") || cmd.equals("Reboot")) {
+                       setVisible(false);
+                       dispose();
+               }
+               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;
+       }
+
+       public void units_changed(boolean imperial_units) {
+               boolean was_dirty = dirty;
+
+               if (tracker_motion_value.isEnabled()) {
+                       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();
+       }
+
+       /* 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_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) {
+       }
+
+       public int main_deploy() {
+               return -1;
+       }
+
+       public void set_apogee_delay(int new_apogee_delay) { }
+
+       public int apogee_delay() {
+               return -1;
+       }
+
+       public void set_apogee_lockout(int new_apogee_lockout) { }
+
+       public int apogee_lockout() { return -1; }
+
+       public void set_radio_frequency(double new_radio_frequency) {
+               radio_frequency_value.set_frequency(new_radio_frequency);
+       }
+
+       public double radio_frequency() {
+               return radio_frequency_value.frequency();
+       }
+
+       public void set_radio_calibration(int new_radio_calibration) {
+               radio_calibration_value.setVisible(new_radio_calibration >= 0);
+               if (new_radio_calibration < 0)
+                       radio_calibration_value.setText("Disabled");
+               else
+                       radio_calibration_value.setText(String.format("%d", new_radio_calibration));
+       }
+
+       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 void set_radio_enable(int new_radio_enable) {
+               if (new_radio_enable >= 0) {
+                       radio_enable_value.setSelected(new_radio_enable > 0);
+                       radio_enable_value.setEnabled(true);
+               } else {
+                       radio_enable_value.setSelected(true);
+                       radio_enable_value.setVisible(radio_frequency() > 0);
+                       radio_enable_value.setEnabled(false);
+               }
+               set_radio_enable_tool_tip();
+       }
+
+       public int radio_enable() {
+               if (radio_enable_value.isEnabled())
+                       return radio_enable_value.isSelected() ? 1 : 0;
+               else
+                       return -1;
+       }
+
+       public void set_telemetry_rate(int new_rate) {
+               rate_value.set_rate(new_rate);
+       }
+
+       public int telemetry_rate() {
+               return rate_value.rate();
+       }
+
+       public void set_callsign(String new_callsign) {
+               callsign_value.setVisible(new_callsign != null);
+               callsign_value.setText(new_callsign);
+       }
+
+       public String callsign() {
+               return callsign_value.getText();
+       }
+
+       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) {
+               flight_log_max_value.setSelectedItem(flight_log_max_label(new_flight_log_max));
+               flight_log_max = new_flight_log_max;
+               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 {
+               return parse_int("flight log max", flight_log_max_value.getSelectedItem().toString(), true);
+       }
+
+       public void set_flight_log_max_limit(int new_flight_log_max_limit) {
+               flight_log_max_limit = new_flight_log_max_limit;
+               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) { }
+       public int ignite_mode() { return -1; }
+
+
+       public void set_pad_orientation(int new_pad_orientation) { }
+       public int pad_orientation() { return -1; }
+
+       public void set_beep(int new_beep) { }
+
+       public int beep() { return -1; }
+
+       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.isEnabled())
+                       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.isEnabled())
+                       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 < 0) {
+                       tracker_motion_value.setEnabled(false);
+               } else {
+                       tracker_motion_value.setEnabled(true);
+                       tracker_motion_value.setSelectedItem(AltosConvert.height.say(tracker_motion));
+               }
+       }
+
+       public int tracker_motion() throws AltosConfigDataException {
+               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);
+               }
+       }
+
+       public void set_tracker_interval(int tracker_interval) {
+               if (tracker_interval< 0) {
+                       tracker_interval_value.setEnabled(false);
+               } else {
+                       tracker_interval_value.setEnabled(true);
+                       tracker_interval_value.setSelectedItem(String.format("%d", tracker_interval));
+               }
+       }
+
+       public int tracker_interval() throws AltosConfigDataException {
+               return parse_int ("tracker interval", tracker_interval_value.getSelectedItem().toString(), false);
+       }
+
+       public void set_aprs_interval(int new_aprs_interval) {
+               String  s;
+
+               if (new_aprs_interval <= 0)
+                       s = "Disabled";
+               else
+                       s = Integer.toString(new_aprs_interval);
+               aprs_interval_value.setSelectedItem(s);
+               aprs_interval_value.setVisible(new_aprs_interval >= 0);
+               set_aprs_interval_tool_tip();
+       }
+
+       public int aprs_interval() throws AltosConfigDataException {
+               String  s = aprs_interval_value.getSelectedItem().toString();
+
+               if (s.equals("Disabled"))
+                       return 0;
+               return parse_int("aprs interval", s, false);
+       }
+
+       public void set_aprs_ssid(int new_aprs_ssid) {
+               aprs_ssid_value.setSelectedItem(Math.max(0,new_aprs_ssid));
+               aprs_ssid_value.setVisible(new_aprs_ssid >= 0);
+               set_aprs_ssid_tool_tip();
+       }
+
+       public int aprs_ssid() throws AltosConfigDataException {
+               Integer i = (Integer) aprs_ssid_value.getSelectedItem();
+               return i;
+       }
+
+       public void set_aprs_format(int new_aprs_format) {
+               aprs_format_value.setVisible(new_aprs_format >= 0);
+               aprs_format_label.setVisible(new_aprs_format >= 0);
+
+               aprs_format_value.setSelectedIndex(Math.max(0,new_aprs_format));
+               set_aprs_format_tool_tip();
+       }
+
+       public int aprs_format() throws AltosConfigDataException {
+               return aprs_format_value.getSelectedIndex();
+       }
+}
diff --git a/teststand/TestStandDisplayThread.java b/teststand/TestStandDisplayThread.java
new file mode 100644 (file)
index 0000000..539b0eb
--- /dev/null
@@ -0,0 +1,210 @@
+/*
+ * Copyright © 2017 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 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 org.altusmetrum.teststand;
+
+import java.awt.*;
+import javax.swing.*;
+import java.io.*;
+import java.text.*;
+import org.altusmetrum.altoslib_11.*;
+import org.altusmetrum.altosuilib_11.*;
+
+public class TestStandDisplayThread extends Thread {
+
+       Frame                   parent;
+       IdleThread              idle_thread;
+       AltosVoice              voice;
+       AltosFlightReader       reader;
+       AltosState              old_state, state;
+       AltosListenerState      listener_state;
+       AltosFlightDisplay      display;
+
+       synchronized void show_safely() {
+               final AltosState my_state = state;
+               final AltosListenerState my_listener_state = listener_state;
+               Runnable r = new Runnable() {
+                               public void run() {
+                                       try {
+                                               display.show(my_state, my_listener_state);
+                                       } catch (Exception ex) {
+                                       }
+                               }
+                       };
+               SwingUtilities.invokeLater(r);
+       }
+
+       void reading_error_internal() {
+               JOptionPane.showMessageDialog(parent,
+                                             String.format("Error reading from \"%s\"", reader.name),
+                                             "Telemetry Read Error",
+                                             JOptionPane.ERROR_MESSAGE);
+       }
+
+       void reading_error_safely() {
+               Runnable r = new Runnable() {
+                               public void run() {
+                                       try {
+                                               reading_error_internal();
+                                       } catch (Exception ex) {
+                                       }
+                               }
+                       };
+               SwingUtilities.invokeLater(r);
+       }
+
+       class IdleThread extends Thread {
+
+               boolean started;
+               int     report_interval;
+               long    report_time;
+
+               public synchronized void report(boolean last) {
+                       if (state == null)
+                               return;
+
+                       if (state.height() != AltosLib.MISSING) {
+                               if (state.from_pad != null) {
+                                       voice.speak("Height %s, bearing %s %d, elevation %d, range %s, .\n",
+                                                   AltosConvert.height.say(state.gps_height()),
+                                                   state.from_pad.bearing_words(
+                                                           AltosGreatCircle.BEARING_VOICE),
+                                                   (int) (state.from_pad.bearing + 0.5),
+                                                   (int) (state.elevation + 0.5),
+                                                   AltosConvert.distance.say(state.range));
+                               } else {
+                                       voice.speak("Height %s.\n",
+                                                   AltosConvert.height.say(state.height()));
+                               }
+                       }
+               }
+
+               long now () {
+                       return System.currentTimeMillis();
+               }
+
+               void set_report_time() {
+                       report_time = now() + report_interval;
+               }
+
+               public void run () {
+                       try {
+                               for (;;) {
+                                       if (reader.has_monitor_battery()) {
+                                               listener_state.battery = reader.monitor_battery();
+                                               show_safely();
+                                       }
+                                       set_report_time();
+                                       for (;;) {
+                                               voice.drain();
+                                               synchronized (this) {
+                                                       long    sleep_time = report_time - now();
+                                                       if (sleep_time <= 0)
+                                                               break;
+                                                       wait(sleep_time);
+                                               }
+                                       }
+
+                                       report(false);
+                               }
+                       } catch (InterruptedException ie) {
+                               try {
+                                       voice.drain();
+                               } catch (InterruptedException iie) { }
+                       }
+               }
+
+               public synchronized void notice(boolean spoken) {
+                       if (old_state != null && old_state.state() != state.state()) {
+                               report_time = now();
+                               this.notify();
+                       } else if (spoken)
+                               set_report_time();
+               }
+
+               public IdleThread() {
+                       report_interval = 10000;
+               }
+       }
+
+       synchronized boolean tell() {
+               boolean ret = false;
+               if (old_state == null || old_state.gps_ready != state.gps_ready) {
+                       if (state.gps_ready) {
+                               voice.speak("GPS ready");
+                               ret = true;
+                       }
+                       else if (old_state != null) {
+                               voice.speak("GPS lost");
+                               ret = true;
+                       }
+               }
+               old_state = state;
+               return ret;
+       }
+
+       public void run() {
+               boolean         interrupted = false;
+               boolean         told;
+
+               idle_thread = new IdleThread();
+               idle_thread.start();
+
+               try {
+                       for (;;) {
+                               try {
+                                       state = reader.read();
+                                       if (state == null) {
+                                               listener_state.running = false;
+                                               break;
+                                       }
+                                       reader.update(state);
+                                       show_safely();
+                                       told = tell();
+                                       idle_thread.notice(told);
+                               } catch (ParseException pp) {
+                                       System.out.printf("Parse error: %d \"%s\"\n", pp.getErrorOffset(), pp.getMessage());
+                               } catch (AltosCRCException ce) {
+                                       ++listener_state.crc_errors;
+                                       show_safely();
+                               }
+                       }
+               } catch (InterruptedException ee) {
+                       interrupted = true;
+               } catch (IOException ie) {
+                       reading_error_safely();
+               } finally {
+                       if (!interrupted)
+                               idle_thread.report(true);
+                       reader.close(interrupted);
+                       idle_thread.interrupt();
+                       try {
+                               idle_thread.join();
+                       } catch (InterruptedException ie) {}
+               }
+       }
+
+       public TestStandDisplayThread(Frame in_parent, AltosVoice in_voice, AltosFlightDisplay in_display, AltosFlightReader in_reader) {
+               listener_state = new AltosListenerState();
+               parent = in_parent;
+               voice = in_voice;
+               display = in_display;
+               reader = in_reader;
+               display.reset();
+       }
+}
diff --git a/teststand/TestStandGraphUI.java b/teststand/TestStandGraphUI.java
new file mode 100644 (file)
index 0000000..b86dab9
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * Copyright © 2017 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 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 org.altusmetrum.teststand;
+
+import java.io.*;
+import java.util.ArrayList;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import java.io.*;
+import java.util.concurrent.*;
+import java.util.*;
+import org.altusmetrum.altoslib_11.*;
+import org.altusmetrum.altosuilib_11.*;
+
+import org.jfree.chart.ChartPanel;
+import org.jfree.chart.JFreeChart;
+import org.jfree.ui.RefineryUtilities;
+
+public class TestStandGraphUI extends AltosUIFrame
+{
+       JTabbedPane             pane;
+       AltosGraph              graph;
+       AltosUIEnable           enable;
+       AltosUIMap              map;
+       AltosState              state;
+       AltosFlightStats        stats;
+       AltosGraphDataSet       graphDataSet;
+       AltosFlightStatsTable   statsTable;
+
+       void fill_map(AltosStateIterable states) {
+               for (AltosState state : states) {
+                       if (state.gps != null && state.gps.locked && state.gps.nsat >= 4)
+                               map.show(state, null);
+               }
+       }
+
+       private void close() {
+               setVisible(false);
+               dispose();
+               TestStand.subtract_window();
+       }
+
+       TestStandGraphUI(AltosStateIterable states, File file) throws InterruptedException, IOException {
+               super(file.getName());
+               state = null;
+
+               pane = new JTabbedPane();
+
+               enable = new AltosUIEnable();
+               stats = new AltosFlightStats(states);
+               graphDataSet = new AltosGraphDataSet(states);
+               graph = new AltosGraph(enable, stats, graphDataSet);
+               statsTable = new AltosFlightStatsTable(stats);
+
+               map = new AltosUIMap();
+
+               pane.add("Graph", graph.panel);
+               pane.add("Configure Graph", enable);
+               pane.add("Statistics", statsTable);
+               fill_map(states);
+               pane.add("Map", map);
+
+               setContentPane (pane);
+
+               addWindowListener(new WindowAdapter() {
+                               @Override
+                               public void windowClosing(WindowEvent e) {
+                                       close();
+                               }
+                       });
+
+               pack();
+
+               setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
+
+               TestStand.add_window();
+
+               setVisible(true);
+
+               if (state != null)
+                       map.centre(state);
+
+       }
+}
diff --git a/teststand/TestStandInfo.java b/teststand/TestStandInfo.java
new file mode 100644 (file)
index 0000000..7f76fab
--- /dev/null
@@ -0,0 +1,215 @@
+/*
+ * Copyright © 2017 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 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 org.altusmetrum.teststand;
+
+import java.util.*;
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import org.altusmetrum.altoslib_11.*;
+import org.altusmetrum.altosuilib_11.*;
+
+public class TestStandInfo extends AltosUIFlightTab {
+
+       JLabel                          cur, max;
+
+       abstract class Value extends AltosUIUnitsIndicator {
+               public abstract void show(AltosState state, AltosListenerState listener_state);
+
+               public Value (Container container, int y, AltosUnits units, String text) {
+                       super(container, y, units, text, 1, false, 2);
+               }
+       }
+
+       abstract class DualValue extends AltosUIUnitsIndicator {
+               public DualValue (Container container, int y, AltosUnits units, String text) {
+                       super(container, y, units, text, 2, false, 1);
+               }
+       }
+
+       abstract class ValueHold extends DualValue {
+               public void reset() {
+                       super.reset();
+               }
+               public ValueHold (Container container, int y, AltosUnits units, String text) {
+                       super(container, y, units, text);
+               }
+       }
+
+       class Altitude extends ValueHold {
+               public double value(AltosState state, int i) {
+                       if (i == 0)
+                               return state.altitude();
+                       else
+                               return state.max_altitude();
+               }
+
+               public Altitude (Container container, int y) {
+                       super (container, y, AltosConvert.height, "Altitude");
+               }
+       }
+
+       class AscentRate extends ValueHold {
+               public double value(AltosState state, int i) {
+                       if (i == 0)
+                               return state.gps_ascent_rate();
+                       else
+                               return state.max_gps_ascent_rate();
+               }
+               public AscentRate (Container container, int y) {
+                       super (container, y, AltosConvert.speed, "Ascent Rate");
+               }
+       }
+
+       class GroundSpeed extends ValueHold {
+               public double value(AltosState state, int i) {
+                       if (i == 0)
+                               return state.gps_ground_speed();
+                       else
+                               return state.max_gps_ground_speed();
+               }
+               public GroundSpeed (Container container, int y) {
+                       super (container, y, AltosConvert.speed, "Ground Speed");
+               }
+       }
+
+       class Course extends AltosUIIndicator {
+
+               public void show (AltosState state, AltosListenerState listener_state) {
+                       double  course = state.gps_course();
+                       if (course == AltosLib.MISSING)
+                               show("Missing", "Missing");
+                       else
+                               show( String.format("%3.0f°", course),
+                                     AltosConvert.bearing_to_words(
+                                             AltosConvert.BEARING_LONG,
+                                             course));
+               }
+               public Course (Container container, int y) {
+                       super (container, y, "Course", 2, false, 1);
+               }
+       }
+
+       class Lat extends AltosUIIndicator {
+
+               String pos(double p, String pos, String neg) {
+                       String  h = pos;
+                       if (p < 0) {
+                               h = neg;
+                               p = -p;
+                       }
+                       int deg = (int) Math.floor(p);
+                       double min = (p - Math.floor(p)) * 60.0;
+                       return String.format("%s %4d° %9.6f", h, deg, min);
+               }
+
+               public void show (AltosState state, AltosListenerState listener_state) {
+                       if (state.gps != null && state.gps.connected && state.gps.lat != AltosLib.MISSING)
+                               show(pos(state.gps.lat,"N", "S"));
+                       else
+                               show("Missing");
+               }
+               public Lat (Container container, int y) {
+                       super (container, y, "Latitude", 1, false, 2);
+               }
+       }
+
+       class Lon extends AltosUIIndicator {
+
+               String pos(double p, String pos, String neg) {
+                       String  h = pos;
+                       if (p < 0) {
+                               h = neg;
+                               p = -p;
+                       }
+                       int deg = (int) Math.floor(p);
+                       double min = (p - Math.floor(p)) * 60.0;
+                       return String.format("%s %4d° %9.6f", h, deg, min);
+               }
+
+               public void show (AltosState state, AltosListenerState listener_state) {
+                       if (state.gps != null && state.gps.connected && state.gps.lon != AltosLib.MISSING)
+                               show(pos(state.gps.lon,"E", "W"));
+                       else
+                               show("Missing");
+               }
+               public Lon (Container container, int y) {
+                       super (container, y, "Longitude", 1, false, 2);
+               }
+       }
+
+       class GPSLocked extends AltosUIIndicator {
+
+               public void show (AltosState state, AltosListenerState listener_state) {
+                       if (state == null || state.gps == null)
+                               hide();
+                       else {
+                               int soln = state.gps.nsat;
+                               int nsat = state.gps.cc_gps_sat != null ? state.gps.cc_gps_sat.length : 0;
+                               show("%4d in solution", soln,
+                                    "%4d in view", nsat);
+                               set_lights(state.gps.locked && soln >= 4);
+                       }
+               }
+               public GPSLocked (Container container, int y) {
+                       super (container, y, "GPS Locked", 2, true, 1);
+               }
+       }
+
+       public void font_size_changed(int font_size) {
+               cur.setFont(AltosUILib.label_font);
+               max.setFont(AltosUILib.label_font);
+               super.font_size_changed(font_size);
+       }
+
+       public void labels(Container container, int y) {
+               GridBagLayout           layout = (GridBagLayout)(container.getLayout());
+               GridBagConstraints      c;
+
+               cur = new JLabel("Current");
+               cur.setFont(AltosUILib.label_font);
+               c = new GridBagConstraints();
+               c.gridx = 2; c.gridy = y;
+               c.insets = new Insets(AltosUILib.tab_elt_pad, AltosUILib.tab_elt_pad, AltosUILib.tab_elt_pad, AltosUILib.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 "Location";
+       }
+
+       public TestStandInfo() {
+               int y = 0;
+               labels(this, y++);
+               add(new Altitude(this, y++));
+               add(new GroundSpeed(this, y++));
+               add(new AscentRate(this, y++));
+               add(new Course(this, y++));
+               add(new Lat(this, y++));
+               add(new Lon(this, y++));
+               add(new GPSLocked(this, y++));
+       }
+}
diff --git a/teststand/TestStandPreferences.java b/teststand/TestStandPreferences.java
new file mode 100644 (file)
index 0000000..63edbd3
--- /dev/null
@@ -0,0 +1,121 @@
+/*
+ * Copyright © 2017 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 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 org.altusmetrum.teststand;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.beans.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import org.altusmetrum.altosuilib_11.*;
+
+public class TestStandPreferences
+       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++;
+       }
+
+       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 */
+       }
+
+       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");
+               pane.add(manage_frequencies, constraints(2, 1));
+               row++;
+       }
+
+       public TestStandPreferences(JFrame owner, AltosVoice voice) {
+               super(owner, "TestStand Preferences", "Configure TestStand");
+
+               this.voice = voice;
+       }
+}
diff --git a/teststand/TestStandState.java b/teststand/TestStandState.java
new file mode 100644 (file)
index 0000000..e75ab21
--- /dev/null
@@ -0,0 +1,232 @@
+/*
+ * Copyright © 2017 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 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 org.altusmetrum.teststand;
+
+import java.util.*;
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import org.altusmetrum.altoslib_11.*;
+import org.altusmetrum.altosuilib_11.*;
+
+public class TestStandState extends AltosUIFlightTab {
+
+       JLabel  cur, max;
+
+       abstract class Value extends AltosUIUnitsIndicator {
+               public Value (Container container, int y, AltosUnits units, String text) {
+                       super(container, y, units, text, 1, false, 2);
+               }
+       }
+
+       abstract class DualValue extends AltosUIUnitsIndicator {
+               public DualValue (Container container, int y, AltosUnits units, String text) {
+                       super(container, y, units, text, 2, false, 1);
+               }
+       }
+
+       abstract class ValueHold extends DualValue {
+               public ValueHold (Container container, int y, AltosUnits units, String text) {
+                       super(container, y, units, text);
+               }
+       }
+
+       class Height extends ValueHold {
+               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");
+               }
+       }
+
+       class Speed extends ValueHold {
+               public double value(AltosState state, int i) {
+                       if (i == 0)
+                               return state.gps_speed();
+                       else
+                               return state.max_gps_speed();
+               }
+
+               public Speed(Container container, int y) {
+                       super(container, y, AltosConvert.speed, "Speed");
+               }
+       }
+
+       class Distance extends Value {
+               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, "Distance");
+               }
+       }
+
+       class Range extends Value {
+               public double value(AltosState state, int i) {
+                       return state.range;
+               }
+               public Range (Container container, int y) {
+                       super (container, 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 y) {
+                       super (container, y, "Bearing", 2, false, 1);
+               }
+       }
+
+       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 y) {
+                       super (container, y, "Elevation", 1, false, 2);
+               }
+       }
+
+       class FirmwareVersion extends AltosUIIndicator {
+               public void show(AltosState state, AltosListenerState listener_state) {
+                       if (state.firmware_version == null)
+                               show("Missing");
+                       else
+                               show(state.firmware_version);
+               }
+
+               public FirmwareVersion(Container container, int y) {
+                       super(container, y, "Firmware Version", 1, false, 2);
+               }
+       }
+
+       class FlightLogMax extends AltosUIIndicator {
+               public void show(AltosState state, AltosListenerState listener_state) {
+                       int storage = state.flight_log_max;
+                       if (storage == AltosLib.MISSING)
+                               storage = state.log_space >> 10;
+                       if (storage == AltosLib.MISSING)
+                               show("Missing");
+                       else
+                               show(String.format("%dkB", storage));
+               }
+
+               public FlightLogMax(Container container, int y) {
+                       super(container, y, "Flight Log Storage", 1, false, 2);
+               }
+       }
+
+       class BatteryVoltage extends AltosUIVoltageIndicator {
+               public double voltage(AltosState state) {
+                       return state.battery_voltage;
+               }
+
+               public double good() {
+                       return AltosLib.ao_battery_good;
+               }
+
+               public BatteryVoltage(Container container, int y) {
+                       super(container, y, "Battery Voltage", 2);
+               }
+       }
+
+       class ReceiverBattery extends AltosUIVoltageIndicator {
+
+               public double voltage(AltosState state) { return AltosLib.MISSING; }
+
+               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)
+                               return AltosLib.MISSING;
+                       return listener_state.battery;
+               }
+
+               public ReceiverBattery (AltosUIFlightTab container, int y) {
+                       super(container, y, "Receiver Battery", 2);
+               }
+       }
+
+       public void labels(Container container, int y) {
+               GridBagLayout           layout = (GridBagLayout)(container.getLayout());
+               GridBagConstraints      c;
+
+               cur = new JLabel("Current");
+               cur.setFont(AltosUILib.label_font);
+               c = new GridBagConstraints();
+               c.gridx = 2; c.gridy = y;
+               c.insets = new Insets(AltosUILib.tab_elt_pad, AltosUILib.tab_elt_pad, AltosUILib.tab_elt_pad, AltosUILib.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 void font_size_changed(int font_size) {
+               cur.setFont(AltosUILib.label_font);
+               max.setFont(AltosUILib.label_font);
+               super.font_size_changed(font_size);
+       }
+
+       public String getName() {
+               return "Status";
+       }
+
+       public TestStandState() {
+               int y = 0;
+               labels(this, y++);
+               add(new Height(this, y++));
+               add(new Speed(this, y++));
+               add(new Distance(this, y++));
+               add(new Range(this, y++));
+               add(new Bearing(this, y++));
+               add(new Elevation(this, y++));
+               add(new FirmwareVersion(this, y++));
+               add(new FlightLogMax(this, y++));
+               add(new BatteryVoltage(this, y++));
+               add(new ReceiverBattery(this, y++));
+       }
+}
diff --git a/teststand/TestStandStatus.java b/teststand/TestStandStatus.java
new file mode 100644 (file)
index 0000000..740dbda
--- /dev/null
@@ -0,0 +1,276 @@
+/*
+ * Copyright © 2017 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 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 org.altusmetrum.teststand;
+
+import java.awt.*;
+import javax.swing.*;
+import org.altusmetrum.altoslib_11.*;
+import org.altusmetrum.altosuilib_11.*;
+
+public class TestStandStatus extends JComponent implements AltosFlightDisplay {
+       GridBagLayout   layout;
+
+       public class Value {
+               JLabel          label;
+               JTextField      value;
+
+               void show(AltosState state, AltosListenerState listener_state) {}
+
+               void reset() {
+                       value.setText("");
+               }
+
+               void set_font() {
+                       label.setFont(AltosUILib.status_font);
+                       value.setFont(AltosUILib.status_font);
+               }
+
+               void setVisible(boolean visible) {
+                       label.setVisible(visible);
+                       value.setVisible(visible);
+               }
+
+               public Value (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(AltosUILib.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(AltosUILib.status_font);
+                       value.setHorizontalAlignment(SwingConstants.CENTER);
+                       c.gridx = x; c.gridy = 1;
+                       layout.setConstraints(value, c);
+                       add(value);
+               }
+       }
+
+       class Call extends Value {
+               String  call;
+
+               void show(AltosState state, AltosListenerState listener_state) {
+                       if (state.callsign != call) {
+                               value.setText(state.callsign);
+                               call = state.callsign;
+                       }
+                       if (state.callsign == null)
+                               setVisible(false);
+                       else
+                               setVisible(true);
+               }
+
+               public void reset() {
+                       super.reset();
+                       call = "";
+               }
+
+               public Call (GridBagLayout layout, int x) {
+                       super (layout, x, "Callsign");
+               }
+       }
+
+       Call call;
+
+       class Serial extends Value {
+               int     serial = -1;
+               void show(AltosState state, AltosListenerState listener_state) {
+                       if (state.serial != serial) {
+                               if (state.serial == AltosLib.MISSING)
+                                       value.setText("none");
+                               else
+                                       value.setText(String.format("%d", state.serial));
+                               serial = state.serial;
+                       }
+               }
+
+               public void reset() {
+                       super.reset();
+                       serial = -1;
+               }
+
+               public Serial (GridBagLayout layout, int x) {
+                       super (layout, x, "Serial");
+               }
+       }
+
+       Serial serial;
+
+       class Flight extends Value {
+
+               int     last_flight = -1;
+
+               void show(AltosState state, AltosListenerState listener_state) {
+                       if (state.flight != last_flight) {
+                               if (state.flight == AltosLib.MISSING)
+                                       value.setText("none");
+                               else
+                                       value.setText(String.format("%d", state.flight));
+                               last_flight = state.flight;
+                       }
+               }
+
+               public void reset() {
+                       super.reset();
+                       last_flight = -1;
+               }
+
+               public Flight (GridBagLayout layout, int x) {
+                       super (layout, x, "Flight");
+               }
+       }
+
+       Flight flight;
+
+       class RSSI extends Value {
+               int     rssi = 10000;
+
+               void show(AltosState state, AltosListenerState listener_state) {
+                       int     new_rssi = state.rssi();
+
+                       if (new_rssi != rssi) {
+                               value.setText(String.format("%d", new_rssi));
+                               if (state.rssi == AltosLib.MISSING)
+                                       setVisible(false);
+                               else
+                                       setVisible(true);
+                               rssi = new_rssi;
+                       }
+               }
+
+               public void reset() {
+                       super.reset();
+                       rssi = 10000;
+               }
+
+               public RSSI (GridBagLayout layout, int x) {
+                       super (layout, x, "RSSI");
+               }
+       }
+
+       RSSI rssi;
+
+       class LastPacket extends Value {
+
+               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");
+                       }
+               }
+
+               void reset() {
+                       super.reset();
+                       last_secs = -1;
+               }
+
+               void disable() {
+                       value.setText("");
+               }
+
+               public LastPacket(GridBagLayout layout, int x) {
+                       super (layout, x, "Age");
+               }
+       }
+
+       LastPacket last_packet;
+
+       public void disable_receive() {
+               last_packet.disable();
+       }
+
+       public void reset () {
+               call.reset();
+               serial.reset();
+               flight.reset();
+               rssi.reset();
+               last_packet.reset();
+       }
+
+       public void font_size_changed(int font_size) {
+               call.set_font();
+               serial.set_font();
+               flight.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);
+               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;
+       }
+
+       TestStandStatusUpdate   status_update;
+       javax.swing.Timer       timer;
+
+       public void start(TestStandStatusUpdate 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 TestStandStatus() {
+               layout = new GridBagLayout();
+
+               setLayout(layout);
+
+               call = new Call(layout, 0);
+               serial = new Serial(layout, 1);
+               flight = new Flight(layout, 2);
+               rssi = new RSSI(layout, 4);
+               last_packet = new LastPacket(layout, 5);
+       }
+}
diff --git a/teststand/TestStandStatusUpdate.java b/teststand/TestStandStatusUpdate.java
new file mode 100644 (file)
index 0000000..2be4cb8
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * Copyright © 2017 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 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 org.altusmetrum.teststand;
+
+import java.awt.event.*;
+import org.altusmetrum.altoslib_11.*;
+
+public class TestStandStatusUpdate implements ActionListener {
+
+       public AltosState               saved_state;
+       public AltosListenerState       saved_listener_state;
+       TestStandStatus                 status;
+
+       public void actionPerformed (ActionEvent e) {
+               if (saved_state != null) {
+                       if (saved_listener_state == null)
+                               saved_listener_state = new AltosListenerState();
+                       status.show(saved_state, saved_listener_state);
+               }
+       }
+
+       public TestStandStatusUpdate (TestStandStatus in_status) {
+               status = in_status;
+       }
+}
+
diff --git a/teststand/altusmetrum-teststand.desktop.in b/teststand/altusmetrum-teststand.desktop.in
new file mode 100644 (file)
index 0000000..c7976f7
--- /dev/null
@@ -0,0 +1,10 @@
+[Desktop Entry]
+Type=Application
+Name=TestStand
+GenericName=TestStand monitor, download and analysis
+Comment=View and log data from TestStand tracking devices
+Icon=%icondir%/altusmetrum-teststand.svg
+Exec=%bindir%/teststand %F
+Terminal=false
+MimeType=application/vnd.altusmetrum.eeprom
+Categories=Education;Electronics;Science;
diff --git a/teststand/teststand-fat b/teststand/teststand-fat
new file mode 100755 (executable)
index 0000000..8749124
--- /dev/null
@@ -0,0 +1,4 @@
+#!/bin/sh
+me=`which "$0"`
+dir=`dirname "$me"`
+exec java -cp "$dir/*" -Djava.library.path="$dir" -jar "$dir"/telegps-fat.jar  "$@"
diff --git a/teststand/teststand-windows.nsi.in b/teststand/teststand-windows.nsi.in
new file mode 100644 (file)
index 0000000..b0b5d6a
--- /dev/null
@@ -0,0 +1,239 @@
+!addplugindir ../altosui/Instdrv/NSIS/Plugins
+!addincludedir ../altosui/Instdrv/NSIS/Includes
+!include x64.nsh
+!include java.nsh
+!include refresh-sh.nsh
+
+!define REG_NAME       "TeleGPS"
+!define PROG_ID_TELEM  "altusmetrum.telegps.telem.1"
+!define PROG_ID_EEPROM "altusmetrum.telegps.eeprom.1"
+!define FAT_NAME       "telegps-fat.jar"
+!define WIN_APP_ICON   "altusmetrum-telegps.ico"
+!define WIN_APP_EXE    "altusmetrum-telegps.exe"
+!define WIN_TELEM_EXE  "application-vnd.altusmetrum.telemetry.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 "TeleGPS, TeleDongle and TeleBT Firmware"
+
+       SetOutPath $INSTDIR
+
+       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"
+
+SectionEnd
+
+Section "Documentation"
+
+       SetOutPath $INSTDIR
+
+       File "../doc/telegps.pdf"
+       File "../doc/altos.pdf"
+       File "../doc/telemetry.pdf"
+SectionEnd
+
+Section "File Associations"
+
+       ${DisableX64FSRedirection}
+
+       SetOutPath $INSTDIR
+
+       File "../icon/${WIN_TELEM_EXE}"
+       File "../icon/${WIN_EEPROM_EXE}"
+
+       DeleteRegKey HKCR "${PROG_ID_TELEM}"
+       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}"
+       
+       ; .telem elements
+       
+       WriteRegStr HKCR "${PROG_ID_TELEM}"             ""                              "Altus Metrum Telemetry File"
+       WriteRegStr HKCR "${PROG_ID_TELEM}"             "FriendlyTypeName"              "Altus Metrum Telemetry File"
+       WriteRegStr HKCR "${PROG_ID_TELEM}\CurVer"      ""                              "${PROG_ID_TELEM}"
+       WriteRegStr HKCR "${PROG_ID_TELEM}\DefaultIcon" ""                              '"$INSTDIR\${WIN_TELEM_EXE}",-101'
+  WriteRegExpandStr HKCR "${PROG_ID_TELEM}\shell\open\command" ""                      '"$INSTDIR\${WIN_APP_EXE}" "%1"'
+
+       WriteRegStr HKCR ".telem"                       ""                              "${PROG_ID_TELEM}"
+       WriteRegStr HKCR ".telem"                       "PerceivedType"                 "Altus Metrum Telemetry File"
+       WriteRegStr HKCR ".telem"                       "Content Type"                  "application/vnd.altusmetrum.telemetry"
+
+       WriteRegStr HKCR ".telem\OpenWithProgids"       "${PROG_ID_TELEM}"              ""
+       WriteRegStr HKCR ".telem\${PROG_ID_TELEM}"      ""                              "${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 "${PROG_ID_TELEM}"
+
+       DeleteRegKey   HKCR ".eeprom\${PROG_ID_EEPROM}"
+       DeleteRegValue HKCR ".eeprom\OpenWithProgids" "${PROG_ID_EEPROM}"
+
+       DeleteRegKey   HKCR ".telem\${PROG_ID_TELEM}"
+       DeleteRegValue HKCR ".telem\OpenWithProgids" "${PROG_ID_TELEM}"
+
+       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..8d411dc
--- /dev/null
@@ -0,0 +1,36 @@
+.\"
+.\" Copyright © 2017 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 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.
+.\"
+.\"
+.TH TESTSTAND 1 "teststand" ""
+.SH NAME
+teststand \- Rocket motor test stand control and data analysis
+.SH SYNOPSIS
+.B "teststand"
+.SH DESCRIPTION
+.I teststand
+connects to a TeleDongle or TeleBT device through a USB serial interface.
+It provides a menu-oriented
+user interface to control a rocket motor test stand, colleting and helping
+analyze data about each test.
+.SH FILES
+All data log files are recorded into a user-specified directory
+(default ~/AltusMetrum). Files are named using the current date, the serial
+number of the reporting device, the test number recorded in the data
+and the suffix '.eeprom'.
+.SH AUTHOR
+Bdale Garbee