From: Bdale Garbee Date: Sat, 6 May 2017 15:27:41 +0000 (-0700) Subject: first cut of teststand app based on cloning telegps X-Git-Url: https://git.gag.com/?a=commitdiff_plain;h=06e382ab8c12ca0fd2cb37cf93e982c72ae5534d;p=fw%2Faltos first cut of teststand app based on cloning telegps --- diff --git a/teststand/.gitignore b/teststand/.gitignore new file mode 100644 index 00000000..1af9e0f1 --- /dev/null +++ b/teststand/.gitignore @@ -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 index 00000000..b20cf9a6 --- /dev/null +++ b/teststand/Info.plist.in @@ -0,0 +1,61 @@ + + + + + CFBundleName + TestStand + CFBundleVersion + @VERSION@ + CFBundleAllowMixedLocalizations + true + CFBundleExecutable + JavaApplicationStub + CFBundleDevelopmentRegion + English + CFBundlePackageType + APPL + CFBundleIdentifier + org.altusmetrum.teststand + CFBundleSignature + Altu + CFBundleGetInfoString + TestStand version @VERSION@ + CFBundleInfoDictionaryVersion + 6.0 + CFBundleIconFile + altusmetrum-teststand.icns + CFBundleDocumentTypes + + + CFBundleTypeName + Eeprom + CFBundleTypeIconFile + application-vnd.altusmetrum.eeprom.icns + CFBundleTypeExtensions + + eeprom + + CFBundleTypeRole + Editor + + + Java + + MainClass + org.altusmetrum.teststand.TestStand + JVMVersion + 1.5+ + ClassPath + + $JAVAROOT/teststand.jar + $JAVAROOT/freetts.jar + + VMOptions + + -Xms512M + -Xmx512M + -Dosgi.clean=true + + + + diff --git a/teststand/Makefile.am b/teststand/Makefile.am new file mode 100644 index 00000000..72eb338d --- /dev/null +++ b/teststand/Makefile.am @@ -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 index 00000000..48c0de5f --- /dev/null +++ b/teststand/ReadMe-Mac.rtf @@ -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 index 00000000..e8f12590 --- /dev/null +++ b/teststand/TestStand.java @@ -0,0 +1,749 @@ +/* + * Copyright © 2017 Bdale Garbee + * + * 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 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(); + + 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","")), + "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 \t\trelive the glory of past flights \n"); + System.out.printf(" --graph \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 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 index 00000000..b1a3c0e9 --- /dev/null +++ b/teststand/TestStandConfig.java @@ -0,0 +1,298 @@ +/* + * Copyright © 2017 Bdale Garbee + * + * 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 index 00000000..3a22021f --- /dev/null +++ b/teststand/TestStandConfigUI.java @@ -0,0 +1,945 @@ +/* + * Copyright © 2017 Bdale Garbee + * + * 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 aprs_interval_value; + JComboBox aprs_ssid_value; + JComboBox aprs_format_value; + JComboBox flight_log_max_value; + JTextField callsign_value; + JComboBox tracker_motion_value; + JComboBox 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(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(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(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(); + 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(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(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 index 00000000..539b0ebd --- /dev/null +++ b/teststand/TestStandDisplayThread.java @@ -0,0 +1,210 @@ +/* + * Copyright © 2017 Bdale Garbee + * + * 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 index 00000000..b86dab9a --- /dev/null +++ b/teststand/TestStandGraphUI.java @@ -0,0 +1,102 @@ +/* + * Copyright © 2017 Bdale Garbee + * + * 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 index 00000000..7f76fab9 --- /dev/null +++ b/teststand/TestStandInfo.java @@ -0,0 +1,215 @@ +/* + * Copyright © 2017 Bdale Garbee + * + * 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 index 00000000..63edbd36 --- /dev/null +++ b/teststand/TestStandPreferences.java @@ -0,0 +1,121 @@ +/* + * Copyright © 2017 Bdale Garbee + * + * 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 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 index 00000000..e75ab214 --- /dev/null +++ b/teststand/TestStandState.java @@ -0,0 +1,232 @@ +/* + * Copyright © 2017 Bdale Garbee + * + * 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 index 00000000..740dbda8 --- /dev/null +++ b/teststand/TestStandStatus.java @@ -0,0 +1,276 @@ +/* + * Copyright © 2017 Bdale Garbee + * + * 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 index 00000000..2be4cb87 --- /dev/null +++ b/teststand/TestStandStatusUpdate.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2017 Bdale Garbee + * + * 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 index 00000000..c7976f7f --- /dev/null +++ b/teststand/altusmetrum-teststand.desktop.in @@ -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 index 00000000..87491248 --- /dev/null +++ b/teststand/teststand-fat @@ -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 index 00000000..b0b5d6a6 --- /dev/null +++ b/teststand/teststand-windows.nsi.in @@ -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 index 00000000..8d411dcb --- /dev/null +++ b/teststand/teststand.1 @@ -0,0 +1,36 @@ +.\" +.\" Copyright © 2017 Bdale Garbee +.\" +.\" 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