Merge remote branch 'origin/master'
authorKeith Packard <keithp@keithp.com>
Thu, 8 Apr 2010 20:31:23 +0000 (13:31 -0700)
committerKeith Packard <keithp@keithp.com>
Thu, 8 Apr 2010 20:31:23 +0000 (13:31 -0700)
29 files changed:
ao-tools/altosui/.gitignore [new file with mode: 0644]
ao-tools/altosui/AltosConvert.java [new file with mode: 0644]
ao-tools/altosui/AltosDevice.java [new file with mode: 0644]
ao-tools/altosui/AltosDeviceDialog.java [new file with mode: 0644]
ao-tools/altosui/AltosDeviceLinux.java [new file with mode: 0644]
ao-tools/altosui/AltosGPS.java [new file with mode: 0644]
ao-tools/altosui/AltosGreatCircle.java [new file with mode: 0644]
ao-tools/altosui/AltosParse.java [new file with mode: 0644]
ao-tools/altosui/AltosSerial.java [new file with mode: 0644]
ao-tools/altosui/AltosSerialMonitor.java [new file with mode: 0644]
ao-tools/altosui/AltosState.java [new file with mode: 0644]
ao-tools/altosui/AltosTelemetry.java [new file with mode: 0644]
ao-tools/altosui/AltosUI.java [new file with mode: 0644]
ao-tools/altosui/Makefile [new file with mode: 0644]
ao-tools/altosui/Manifest.txt [new file with mode: 0644]
ao-tools/ao-view/aoview_flite.c
src/Makefile
src/ao.h
src/ao_monitor.c
src/ao_pins.h
src/ao_radio.c
src/ao_task.c
src/telemetrum-v0.2/.gitignore [deleted file]
src/telemetrum-v0.2/Makefile [deleted file]
src/telemetrum-v0.2/Makefile.defs [deleted file]
src/telemetrum-v1.0/.gitignore [new file with mode: 0644]
src/telemetrum-v1.0/Makefile [new file with mode: 0644]
src/telemetrum-v1.0/Makefile.defs [new file with mode: 0644]
telemetrum.inf [new file with mode: 0755]

diff --git a/ao-tools/altosui/.gitignore b/ao-tools/altosui/.gitignore
new file mode 100644 (file)
index 0000000..5991319
--- /dev/null
@@ -0,0 +1,2 @@
+*.class
+altosui
diff --git a/ao-tools/altosui/AltosConvert.java b/ao-tools/altosui/AltosConvert.java
new file mode 100644 (file)
index 0000000..3be0716
--- /dev/null
@@ -0,0 +1,245 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+/*
+ * Sensor data conversion functions
+ */
+package altosui;
+
+public class AltosConvert {
+       /*
+        * Pressure Sensor Model, version 1.1
+        *
+        * written by Holly Grimes
+        *
+        * Uses the International Standard Atmosphere as described in
+        *   "A Quick Derivation relating altitude to air pressure" (version 1.03)
+        *    from the Portland State Aerospace Society, except that the atmosphere
+        *    is divided into layers with each layer having a different lapse rate.
+        *
+        * Lapse rate data for each layer was obtained from Wikipedia on Sept. 1, 2007
+        *    at site <http://en.wikipedia.org/wiki/International_Standard_Atmosphere
+        *
+        * Height measurements use the local tangent plane.  The postive z-direction is up.
+        *
+        * All measurements are given in SI units (Kelvin, Pascal, meter, meters/second^2).
+        *   The lapse rate is given in Kelvin/meter, the gas constant for air is given
+        *   in Joules/(kilogram-Kelvin).
+        */
+
+       static final double GRAVITATIONAL_ACCELERATION = -9.80665;
+       static final double AIR_GAS_CONSTANT            = 287.053;
+       static final double NUMBER_OF_LAYERS            = 7;
+       static final double MAXIMUM_ALTITUDE            = 84852.0;
+       static final double MINIMUM_PRESSURE            = 0.3734;
+       static final double LAYER0_BASE_TEMPERATURE     = 288.15;
+       static final double LAYER0_BASE_PRESSURE        = 101325;
+
+       /* lapse rate and base altitude for each layer in the atmosphere */
+       static final double[] lapse_rate = {
+               -0.0065, 0.0, 0.001, 0.0028, 0.0, -0.0028, -0.002
+       };
+
+       static final int[] base_altitude = {
+               0, 11000, 20000, 32000, 47000, 51000, 71000
+       };
+
+       /* outputs atmospheric pressure associated with the given altitude.
+        * altitudes are measured with respect to the mean sea level
+        */
+       static double
+       cc_altitude_to_pressure(double altitude)
+       {
+               double base_temperature = LAYER0_BASE_TEMPERATURE;
+               double base_pressure = LAYER0_BASE_PRESSURE;
+
+               double pressure;
+               double base; /* base for function to determine pressure */
+               double exponent; /* exponent for function to determine pressure */
+               int layer_number; /* identifies layer in the atmosphere */
+               double delta_z; /* difference between two altitudes */
+
+               if (altitude > MAXIMUM_ALTITUDE) /* FIX ME: use sensor data to improve model */
+                       return 0;
+
+               /* calculate the base temperature and pressure for the atmospheric layer
+                  associated with the inputted altitude */
+               for(layer_number = 0; layer_number < NUMBER_OF_LAYERS - 1 && altitude > base_altitude[layer_number + 1]; layer_number++) {
+                       delta_z = base_altitude[layer_number + 1] - base_altitude[layer_number];
+                       if (lapse_rate[layer_number] == 0.0) {
+                               exponent = GRAVITATIONAL_ACCELERATION * delta_z
+                                       / AIR_GAS_CONSTANT / base_temperature;
+                               base_pressure *= Math.exp(exponent);
+                       }
+                       else {
+                               base = (lapse_rate[layer_number] * delta_z / base_temperature) + 1.0;
+                               exponent = GRAVITATIONAL_ACCELERATION /
+                                       (AIR_GAS_CONSTANT * lapse_rate[layer_number]);
+                               base_pressure *= Math.pow(base, exponent);
+                       }
+                       base_temperature += delta_z * lapse_rate[layer_number];
+               }
+
+               /* calculate the pressure at the inputted altitude */
+               delta_z = altitude - base_altitude[layer_number];
+               if (lapse_rate[layer_number] == 0.0) {
+                       exponent = GRAVITATIONAL_ACCELERATION * delta_z
+                               / AIR_GAS_CONSTANT / base_temperature;
+                       pressure = base_pressure * Math.exp(exponent);
+               }
+               else {
+                       base = (lapse_rate[layer_number] * delta_z / base_temperature) + 1.0;
+                       exponent = GRAVITATIONAL_ACCELERATION /
+                               (AIR_GAS_CONSTANT * lapse_rate[layer_number]);
+                       pressure = base_pressure * Math.pow(base, exponent);
+               }
+
+               return pressure;
+       }
+
+
+/* outputs the altitude associated with the given pressure. the altitude
+   returned is measured with respect to the mean sea level */
+       static double
+       cc_pressure_to_altitude(double pressure)
+       {
+
+               double next_base_temperature = LAYER0_BASE_TEMPERATURE;
+               double next_base_pressure = LAYER0_BASE_PRESSURE;
+
+               double altitude;
+               double base_pressure;
+               double base_temperature;
+               double base; /* base for function to determine base pressure of next layer */
+               double exponent; /* exponent for function to determine base pressure
+                                   of next layer */
+               double coefficient;
+               int layer_number; /* identifies layer in the atmosphere */
+               int delta_z; /* difference between two altitudes */
+
+               if (pressure < 0)  /* illegal pressure */
+                       return -1;
+               if (pressure < MINIMUM_PRESSURE) /* FIX ME: use sensor data to improve model */
+                       return MAXIMUM_ALTITUDE;
+
+               /* calculate the base temperature and pressure for the atmospheric layer
+                  associated with the inputted pressure. */
+               layer_number = -1;
+               do {
+                       layer_number++;
+                       base_pressure = next_base_pressure;
+                       base_temperature = next_base_temperature;
+                       delta_z = base_altitude[layer_number + 1] - base_altitude[layer_number];
+                       if (lapse_rate[layer_number] == 0.0) {
+                               exponent = GRAVITATIONAL_ACCELERATION * delta_z
+                                       / AIR_GAS_CONSTANT / base_temperature;
+                               next_base_pressure *= Math.exp(exponent);
+                       }
+                       else {
+                               base = (lapse_rate[layer_number] * delta_z / base_temperature) + 1.0;
+                               exponent = GRAVITATIONAL_ACCELERATION /
+                                       (AIR_GAS_CONSTANT * lapse_rate[layer_number]);
+                               next_base_pressure *= Math.pow(base, exponent);
+                       }
+                       next_base_temperature += delta_z * lapse_rate[layer_number];
+               }
+               while(layer_number < NUMBER_OF_LAYERS - 1 && pressure < next_base_pressure);
+
+               /* calculate the altitude associated with the inputted pressure */
+               if (lapse_rate[layer_number] == 0.0) {
+                       coefficient = (AIR_GAS_CONSTANT / GRAVITATIONAL_ACCELERATION)
+                               * base_temperature;
+                       altitude = base_altitude[layer_number]
+                               + coefficient * Math.log(pressure / base_pressure);
+               }
+               else {
+                       base = pressure / base_pressure;
+                       exponent = AIR_GAS_CONSTANT * lapse_rate[layer_number]
+                               / GRAVITATIONAL_ACCELERATION;
+                       coefficient = base_temperature / lapse_rate[layer_number];
+                       altitude = base_altitude[layer_number]
+                               + coefficient * (Math.pow(base, exponent) - 1);
+               }
+
+               return altitude;
+       }
+
+       /*
+        * Values for our MP3H6115A pressure sensor
+        *
+        * From the data sheet:
+        *
+        * Pressure range: 15-115 kPa
+        * Voltage at 115kPa: 2.82
+        * Output scale: 27mV/kPa
+        *
+        *
+        * 27 mV/kPa * 2047 / 3300 counts/mV = 16.75 counts/kPa
+        * 2.82V * 2047 / 3.3 counts/V = 1749 counts/115 kPa
+        */
+
+       static final double counts_per_kPa = 27 * 2047 / 3300;
+       static final double counts_at_101_3kPa = 1674.0;
+
+       static double
+       cc_barometer_to_pressure(double count)
+       {
+               return ((count / 16.0) / 2047.0 + 0.095) / 0.009 * 1000.0;
+       }
+
+       static double
+       cc_barometer_to_altitude(double baro)
+       {
+               double Pa = cc_barometer_to_pressure(baro);
+               return cc_pressure_to_altitude(Pa);
+       }
+
+       static final double count_per_mss = 27.0;
+
+       static double
+       cc_accelerometer_to_acceleration(double accel, double ground_accel)
+       {
+               return (ground_accel - accel) / count_per_mss;
+       }
+
+       /* Value for the CC1111 built-in temperature sensor
+        * Output voltage at 0°C = 0.755V
+        * Coefficient = 0.00247V/°C
+        * Reference voltage = 1.25V
+        *
+        * temp = ((value / 32767) * 1.25 - 0.755) / 0.00247
+        *      = (value - 19791.268) / 32768 * 1.25 / 0.00247
+        */
+
+       static double
+       cc_thermometer_to_temperature(double thermo)
+       {
+               return (thermo - 19791.268) / 32728.0 * 1.25 / 0.00247;
+       }
+
+       static double
+       cc_battery_to_voltage(double battery)
+       {
+               return battery / 32767.0 * 5.0;
+       }
+
+       static double
+       cc_ignitor_to_voltage(double ignite)
+       {
+               return ignite / 32767 * 15.0;
+       }
+}
diff --git a/ao-tools/altosui/AltosDevice.java b/ao-tools/altosui/AltosDevice.java
new file mode 100644 (file)
index 0000000..66800c5
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package altosui;
+import java.lang.*;
+import java.util.*;
+
+public class AltosDevice {
+       String  tty;    /* suitable to be passed to AltosSerial.connect */
+       String  manufacturer;
+       String  product;
+       int     serial;
+       int     idProduct;
+       int     idVendor;
+
+}
\ No newline at end of file
diff --git a/ao-tools/altosui/AltosDeviceDialog.java b/ao-tools/altosui/AltosDeviceDialog.java
new file mode 100644 (file)
index 0000000..cb1eef8
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package altosui;
+
+import java.lang.*;
+import java.util.*;
+import javax.swing.*;
+import altosui.AltosDevice;
+import altosui.AltosDeviceLinux;
+
+public class AltosDeviceDialog {
+
+       static AltosDevice show (JFrame frame, String product) {
+               AltosDevice[]   devices = null;
+               if (System.getProperty("os.name").startsWith("Linux"))
+                       devices = AltosDeviceLinux.list(product);
+               if (devices != null & devices.length > 0) {
+                       Object o = JOptionPane.showInputDialog(frame,
+                                                              "Select a device",
+                                                              "Device Selection",
+                                                              JOptionPane.PLAIN_MESSAGE,
+                                                              null,
+                                                              devices,
+                                                              devices[0]);
+                       return (AltosDevice) o;
+               } else {
+                       return null;
+               }
+       }
+}
diff --git a/ao-tools/altosui/AltosDeviceLinux.java b/ao-tools/altosui/AltosDeviceLinux.java
new file mode 100644 (file)
index 0000000..ffc70af
--- /dev/null
@@ -0,0 +1,172 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package altosui;
+import java.lang.*;
+import java.io.*;
+import java.util.*;
+import altosui.AltosDevice;
+
+public class AltosDeviceLinux extends AltosDevice {
+
+       String load_string(File file) {
+               try {
+                       FileInputStream in = new FileInputStream(file);
+                       String result = "";
+                       int c;
+                       try {
+                               while ((c = in.read()) != -1) {
+                                       if (c == '\n')
+                                               break;
+                                       result = result + (char) c;
+                               }
+                               return result;
+                       } catch (IOException ee) {
+                               return "";
+                       }
+               } catch (FileNotFoundException ee) {
+                       return "";
+               }
+       }
+       String load_string(File dir, String name) {
+               return load_string(new File(dir, name));
+       }
+
+       int load_hex(File file) {
+               try {
+                       return Integer.parseInt(load_string(file).trim(), 16);
+               } catch (NumberFormatException ee) {
+                       return -1;
+               }
+       }
+
+       int load_hex(File dir, String name) {
+               return load_hex(new File(dir, name));
+       }
+
+       int load_dec(File file) {
+               try {
+                       return Integer.parseInt(load_string(file).trim());
+               } catch (NumberFormatException ee) {
+                       return -1;
+               }
+       }
+
+       int load_dec(File dir, String name) {
+               return load_dec(new File(dir, name));
+       }
+
+       String usb_tty(File sys_dir) {
+               String base = sys_dir.getName();
+               int num_configs = load_hex(sys_dir, "bNumConfigurations");
+               int num_inters = load_hex(sys_dir, "bNumInterfaces");
+               for (int config = 1; config <= num_configs; config++) {
+                       for (int inter = 0; inter < num_inters; inter++) {
+                               String endpoint_base = String.format("%s:%d.%d",
+                                                                    base, config, inter);
+                               File endpoint_full = new File(sys_dir, endpoint_base);
+
+                               File[] namelist;
+
+                               /* Check for tty:ttyACMx style names */
+                               class tty_colon_filter implements FilenameFilter {
+                                       public boolean accept(File dir, String name) {
+                                               return name.startsWith("tty:");
+                                       }
+                               }
+                               namelist = endpoint_full.listFiles(new tty_colon_filter());
+                               if (namelist != null && namelist.length > 0)
+                                       return new File ("/dev", namelist[0].getName().substring(4)).getPath();
+
+                               /* Check for tty/ttyACMx style names */
+                               class tty_filter implements FilenameFilter {
+                                       public boolean accept(File dir, String name) {
+                                               return name.startsWith("tty");
+                                       }
+                               }
+                               File tty_dir = new File(endpoint_full, "tty");
+                               namelist = tty_dir.listFiles(new tty_filter());
+                               if (namelist != null && namelist.length > 0)
+                                       return new File ("/dev", namelist[0].getName()).getPath();
+                       }
+               }
+               return null;
+       }
+
+       public AltosDeviceLinux (File sys) {
+               sys = sys;
+               manufacturer = load_string(sys, "manufacturer");
+               product = load_string(sys, "product");
+               serial = load_dec(sys, "serial");
+               idProduct = load_hex(sys, "idProduct");
+               idVendor = load_hex(sys, "idVendor");
+               tty = usb_tty(sys);
+       }
+
+       public String toString() {
+               return String.format("%-20s %6d %-15s", product, serial, tty == null ? "" : tty);
+       }
+       static public AltosDeviceLinux[] list() {
+               LinkedList<AltosDeviceLinux> devices = new LinkedList<AltosDeviceLinux>();
+
+               class dev_filter implements FilenameFilter{
+                       public boolean accept(File dir, String name) {
+                               for (int i = 0; i < name.length(); i++) {
+                                       char c = name.charAt(i);
+                                       if (Character.isDigit(c))
+                                               continue;
+                                       if (c == '-')
+                                               continue;
+                                       if (c == '.' && i != 1)
+                                               continue;
+                                       return false;
+                               }
+                               return true;
+                       }
+               }
+
+               File usb_devices = new File("/sys/bus/usb/devices");
+               File[] devs = usb_devices.listFiles(new dev_filter());
+               if (devs != null) {
+                       for (int e = 0; e < devs.length; e++) {
+                               AltosDeviceLinux        dev = new AltosDeviceLinux(devs[e]);
+                               if (dev.idVendor == 0xfffe && dev.tty != null) {
+                                       devices.add(dev);
+                               }
+                       }
+               }
+               AltosDeviceLinux[] foo = new AltosDeviceLinux[devices.size()];
+               for (int e = 0; e < devices.size(); e++)
+                       foo[e] = devices.get(e);
+               return foo;
+       }
+
+       static public AltosDeviceLinux[] list(String model) {
+               AltosDeviceLinux[] devices = list();
+               if (model != null) {
+                       LinkedList<AltosDeviceLinux> subset = new LinkedList<AltosDeviceLinux>();
+                       for (int i = 0; i < devices.length; i++) {
+                               if (devices[i].product.startsWith(model))
+                                       subset.add(devices[i]);
+                       }
+                       devices = new AltosDeviceLinux[subset.size()];
+                       for (int e = 0; e < subset.size(); e++)
+                               devices[e] = subset.get(e);
+               }
+               return devices;
+       }
+}
diff --git a/ao-tools/altosui/AltosGPS.java b/ao-tools/altosui/AltosGPS.java
new file mode 100644 (file)
index 0000000..c3b368e
--- /dev/null
@@ -0,0 +1,124 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package altosui;
+
+import java.lang.*;
+import java.text.*;
+import altosui.AltosParse;
+
+
+public class AltosGPS {
+       public class AltosGPSTime {
+               int year;
+               int month;
+               int day;
+               int hour;
+               int minute;
+               int second;
+
+               public AltosGPSTime(String date, String time) throws ParseException {
+                       String[] ymd = date.split("-");
+                       if (ymd.length != 3)
+                               throw new ParseException("error parsing GPS date " + date + " got " + ymd.length, 0);
+                       year = AltosParse.parse_int(ymd[0]);
+                       month = AltosParse.parse_int(ymd[1]);
+                       day = AltosParse.parse_int(ymd[2]);
+
+                       String[] hms = time.split(":");
+                       if (hms.length != 3)
+                               throw new ParseException("Error parsing GPS time " + time + " got " + hms.length, 0);
+                       hour = AltosParse.parse_int(hms[0]);
+                       minute = AltosParse.parse_int(hms[1]);
+                       second = AltosParse.parse_int(hms[2]);
+               }
+
+               public AltosGPSTime() {
+                       year = month = day = 0;
+                       hour = minute = second = 0;
+               }
+
+       }
+
+       public class AltosGPSSat {
+               int     svid;
+               int     c_n0;
+       }
+
+       int     nsat;
+       boolean gps_locked;
+       boolean gps_connected;
+       AltosGPSTime gps_time;
+       double  lat;            /* degrees (+N -S) */
+       double  lon;            /* degrees (+E -W) */
+       int     alt;            /* m */
+
+       int     gps_extended;   /* has extra data */
+       double  ground_speed;   /* m/s */
+       int     course;         /* degrees */
+       double  climb_rate;     /* m/s */
+       double  hdop;           /* unitless? */
+       int     h_error;        /* m */
+       int     v_error;        /* m */
+
+       AltosGPSSat[] cc_gps_sat;       /* tracking data */
+
+       public AltosGPS(String[] words, int i) throws ParseException {
+               AltosParse.word(words[i++], "GPS");
+               nsat = AltosParse.parse_int(words[i++]);
+               AltosParse.word(words[i++], "sat");
+
+               gps_connected = false;
+               gps_locked = false;
+               lat = lon = 0;
+               alt = 0;
+               if ((words[i]).equals("unlocked")) {
+                       gps_connected = true;
+                       gps_time = new AltosGPSTime();
+                       i++;
+               } else if (words.length >= 40) {
+                       gps_locked = true;
+                       gps_connected = true;
+
+                       gps_time = new AltosGPSTime(words[i], words[i+1]); i += 2;
+                       lat = AltosParse.parse_coord(words[i++]);
+                       lon = AltosParse.parse_coord(words[i++]);
+                       alt = AltosParse.parse_int(AltosParse.strip_suffix(words[i++], "m"));
+                       ground_speed = AltosParse.parse_double(AltosParse.strip_suffix(words[i++], "m/s(H)"));
+                       course = AltosParse.parse_int(AltosParse.strip_suffix(words[i++], "°"));
+                       climb_rate = AltosParse.parse_double(AltosParse.strip_suffix(words[i++], "m/s(V)"));
+                       hdop = AltosParse.parse_double(AltosParse.strip_suffix(words[i++], "(hdop)"));
+                       h_error = AltosParse.parse_int(AltosParse.strip_suffix(words[i++], "(herr)"));
+                       v_error = AltosParse.parse_int(AltosParse.strip_suffix(words[i++], "(verr)"));
+               } else {
+                       gps_time = new AltosGPSTime();
+               }
+               AltosParse.word(words[i++], "SAT");
+               int tracking_channels = 0;
+               if (words[i].equals("not-connected"))
+                       tracking_channels = 0;
+               else
+                       tracking_channels = AltosParse.parse_int(words[i]);
+               i++;
+               cc_gps_sat = new AltosGPS.AltosGPSSat[tracking_channels];
+               for (int chan = 0; chan < tracking_channels; chan++) {
+                       cc_gps_sat[chan] = new AltosGPS.AltosGPSSat();
+                       cc_gps_sat[chan].svid = AltosParse.parse_int(words[i++]);
+                       cc_gps_sat[chan].c_n0 = AltosParse.parse_int(words[i++]);
+               }
+       }
+}
diff --git a/ao-tools/altosui/AltosGreatCircle.java b/ao-tools/altosui/AltosGreatCircle.java
new file mode 100644 (file)
index 0000000..878da03
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package altosui;
+
+import java.lang.Math;
+
+public class AltosGreatCircle {
+       double  distance;
+       double  bearing;
+
+       double sqr(double a) { return a * a; }
+
+       static final double rad = Math.PI / 180;
+       static final double earth_radius = 6371.2 * 1000;       /* in meters */
+
+       AltosGreatCircle (double start_lat, double start_lon,
+                         double end_lat, double end_lon)
+       {
+               double lat1 = rad * start_lat;
+               double lon1 = rad * -start_lon;
+               double lat2 = rad * end_lat;
+               double lon2 = rad * -end_lon;
+
+               double d_lon = lon2 - lon1;
+
+               /* From http://en.wikipedia.org/wiki/Great-circle_distance */
+               double vdn = Math.sqrt(sqr(Math.cos(lat2) * Math.sin(d_lon)) +
+                                      sqr(Math.cos(lat1) * Math.sin(lat2) -
+                                          Math.sin(lat1) * Math.cos(lat2) * Math.cos(d_lon)));
+               double vdd = Math.sin(lat1) * Math.sin(lat2) + Math.cos(lat1) * Math.cos(lat2) * Math.cos(d_lon);
+               double d = Math.atan2(vdn,vdd);
+               double course;
+
+               if (Math.cos(lat1) < 1e-20) {
+                       if (lat1 > 0)
+                               course = Math.PI;
+                       else
+                               course = -Math.PI;
+               } else {
+                       if (d < 1e-10)
+                               course = 0;
+                       else
+                               course = Math.acos((Math.sin(lat2)-Math.sin(lat1)*Math.cos(d)) /
+                                                  (Math.sin(d)*Math.cos(lat1)));
+                       if (Math.sin(lon2-lon1) > 0)
+                               course = 2 * Math.PI-course;
+               }
+               distance = d * earth_radius;
+               bearing = course * 180/Math.PI;
+       }
+}
diff --git a/ao-tools/altosui/AltosParse.java b/ao-tools/altosui/AltosParse.java
new file mode 100644 (file)
index 0000000..a60dc69
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package altosui;
+
+import java.text.*;
+import java.lang.*;
+
+public class AltosParse {
+       static int parse_int(String v) throws ParseException {
+               try {
+                       return Integer.parseInt(v);
+               } catch (NumberFormatException e) {
+                       throw new ParseException("error parsing int " + v, 0);
+               }
+       }
+
+       static int parse_hex(String v) throws ParseException {
+               try {
+                       return Integer.parseInt(v, 16);
+               } catch (NumberFormatException e) {
+                       throw new ParseException("error parsing hex " + v, 0);
+               }
+       }
+
+       static double parse_double(String v) throws ParseException {
+               try {
+                       return Double.parseDouble(v);
+               } catch (NumberFormatException e) {
+                       throw new ParseException("error parsing double " + v, 0);
+               }
+       }
+
+       static double parse_coord(String coord) throws ParseException {
+               String[]        dsf = coord.split("\\D+");
+
+               if (dsf.length != 3) {
+                       throw new ParseException("error parsing coord " + coord, 0);
+               }
+               int deg = parse_int(dsf[0]);
+               int min = parse_int(dsf[1]);
+               int frac = parse_int(dsf[2]);
+
+               double r = deg + (min + frac / 10000.0) / 60.0;
+               if (coord.endsWith("S") || coord.endsWith("W"))
+                       r = -r;
+               return r;
+       }
+
+       static String strip_suffix(String v, String suffix) {
+               if (v.endsWith(suffix))
+                       return v.substring(0, v.length() - suffix.length());
+               return v;
+       }
+
+       static void word(String v, String m) throws ParseException {
+               if (!v.equals(m)) {
+                       throw new ParseException("error matching '" + v + "' '" + m + "'", 0);
+               }
+       }
+}
diff --git a/ao-tools/altosui/AltosSerial.java b/ao-tools/altosui/AltosSerial.java
new file mode 100644 (file)
index 0000000..e4cedde
--- /dev/null
@@ -0,0 +1,194 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+/*
+ * Deal with TeleDongle on a serial port
+ */
+
+package altosui;
+
+import java.lang.*;
+import java.io.*;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.LinkedList;
+import java.util.Iterator;
+import gnu.io.*;
+import altosui.AltosSerialMonitor;
+
+/*
+ * This class reads from the serial port and places each received
+ * line in a queue. Dealing with that queue is left up to other
+ * threads.
+ */
+class AltosSerialReader implements Runnable {
+       InputStream     serial_in;
+       LinkedBlockingQueue<String> monitor_queue;
+       LinkedBlockingQueue<String> reply_queue;
+       Thread input_thread;
+       String line;
+
+       public void run () {
+               int c;
+
+               try {
+                       for (;;) {
+                               c = serial_in.read();
+                               if (Thread.interrupted())
+                                       break;
+                               if (c == -1)
+                                       continue;
+                               if (c == '\r')
+                                       continue;
+                               synchronized(this) {
+                                       if (c == '\n') {
+                                               if (line != "") {
+                                                       if (line.startsWith("VERSION"))
+                                                               monitor_queue.put(line);
+                                                       else
+                                                               reply_queue.put(line);
+                                                       line = "";
+                                               }
+                                       } else {
+                                               line = line + (char) c;
+                                       }
+                               }
+                       }
+               } catch (IOException e) {
+               } catch (InterruptedException e) {
+               }
+       }
+
+       public String get_telem() throws InterruptedException {
+               String s = monitor_queue.take();
+               System.out.println(s);
+               return s;
+       }
+
+       public String get_reply() throws InterruptedException {
+               return reply_queue.take();
+       }
+
+       public void flush () {
+               synchronized(this) {
+                       if (!"VERSION".startsWith(line) && !line.startsWith("VERSION"))
+                               line = "";
+                       reply_queue.clear();
+               }
+       }
+
+       public boolean opened() {
+               return serial_in != null;
+       }
+
+       public void close() {
+               if (serial_in != null) {
+                       try {
+                               serial_in.close();
+                       } catch (IOException e) {
+                       }
+                       serial_in = null;
+               }
+               if (input_thread != null) {
+                       try {
+                               input_thread.interrupt();
+                               input_thread.join();
+                       } catch (InterruptedException e) {
+                       }
+                       input_thread = null;
+               }
+       }
+
+       public void open(File name) throws FileNotFoundException {
+               close();
+               serial_in = new FileInputStream(name);
+               input_thread = new Thread(this);
+               input_thread.start();
+       }
+       public void open(CommPort c) throws IOException {
+               close();
+               try {
+               c.enableReceiveTimeout(1000);   /* icky. the read method cannot be interrupted */
+               } catch (UnsupportedCommOperationException ee) {
+               }
+               serial_in = c.getInputStream();
+               input_thread = new Thread(this);
+               input_thread.start();
+       }
+       public AltosSerialReader () {
+               serial_in = null;
+               input_thread = null;
+               line = "";
+               monitor_queue = new LinkedBlockingQueue<String> ();
+               reply_queue = new LinkedBlockingQueue<String> ();
+       }
+
+}
+
+public class AltosSerial {
+       OutputStream serial_out = null;
+       AltosSerialReader reader = null;
+
+       public String get_telem() throws InterruptedException {
+               return reader.get_telem();
+       }
+
+       CommPort comm_port = null;
+
+       public void close() {
+               try {
+                       serial_out.close();
+               } catch (IOException ee) {
+               }
+               reader.close();
+               if (comm_port != null) {
+                       comm_port.close();
+               }
+       }
+
+       public void open(File serial_name) throws FileNotFoundException {
+               reader.open(serial_name);
+               serial_out = new FileOutputStream(serial_name);
+       }
+
+       public void open(CommPort c) throws IOException {
+               reader.open(c);
+               serial_out = c.getOutputStream();
+       }
+
+       public void connect(String port_name) throws IOException, NoSuchPortException, PortInUseException {
+               comm_port = new RXTXPort(port_name);
+               open(comm_port);
+       }
+
+       void init() {
+               reader = new AltosSerialReader();
+       }
+
+       public AltosSerial() {
+               init();
+       }
+
+       public AltosSerial(File serial_name) throws FileNotFoundException {
+               init();
+               open(serial_name);
+       }
+
+       public AltosSerial(CommPort comm_port) throws IOException {
+               init();
+               open(comm_port);
+       }
+}
diff --git a/ao-tools/altosui/AltosSerialMonitor.java b/ao-tools/altosui/AltosSerialMonitor.java
new file mode 100644 (file)
index 0000000..ad0e929
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package altosui;
+
+public interface AltosSerialMonitor {
+       void data(String data);
+}
diff --git a/ao-tools/altosui/AltosState.java b/ao-tools/altosui/AltosState.java
new file mode 100644 (file)
index 0000000..192011d
--- /dev/null
@@ -0,0 +1,175 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+/*
+ * Track flight state from telemetry data stream
+ */
+
+package altosui;
+
+import altosui.AltosTelemetry;
+import altosui.AltosGPS;
+
+public class AltosState {
+       AltosTelemetry data;
+
+       /* derived data */
+
+       double  report_time;
+
+       double  time_change;
+       int     tick;
+
+       int     state;
+       boolean ascent; /* going up? */
+
+       double  ground_altitude;
+       double  height;
+       double  speed;
+       double  acceleration;
+       double  battery;
+       double  temperature;
+       double  main_sense;
+       double  drogue_sense;
+       double  baro_speed;
+
+       double  max_height;
+       double  max_acceleration;
+       double  max_speed;
+
+       AltosGPS        gps;
+
+       double  pad_lat;
+       double  pad_lon;
+       double  pad_alt;
+       int     npad;
+       int     prev_npad;
+
+       AltosGreatCircle from_pad;
+
+       double  gps_height;
+
+       int     speak_tick;
+       double  speak_altitude;
+
+       static double
+       aoview_time()
+       {
+               return System.currentTimeMillis() / 1000.0;
+       }
+
+       void init (AltosTelemetry cur, AltosState prev_state) {
+               int             i;
+               AltosTelemetry prev;
+               double  accel_counts_per_mss;
+
+               data = cur;
+
+               ground_altitude = AltosConvert.cc_barometer_to_altitude(data.ground_pres);
+               height = AltosConvert.cc_barometer_to_altitude(data.flight_pres) - ground_altitude;
+
+               report_time = aoview_time();
+
+               accel_counts_per_mss = ((data.accel_minus_g - data.accel_plus_g) / 2.0) / 9.80665;
+               acceleration = (data.ground_accel - data.flight_accel) / accel_counts_per_mss;
+               speed = data.flight_vel / (accel_counts_per_mss * 100.0);
+               temperature = AltosConvert.cc_thermometer_to_temperature(data.temp);
+               drogue_sense = AltosConvert.cc_ignitor_to_voltage(data.drogue);
+               main_sense = AltosConvert.cc_ignitor_to_voltage(data.main);
+               battery = AltosConvert.cc_battery_to_voltage(data.batt);
+               tick = data.tick;
+               state = data.state();
+
+               if (prev_state != null) {
+
+                       /* Preserve any existing gps data */
+                       npad = prev_state.npad;
+                       gps = prev_state.gps;
+                       pad_lat = prev_state.pad_lat;
+                       pad_lon = prev_state.pad_lon;
+                       pad_alt = prev_state.pad_alt;
+                       max_height = prev_state.max_height;
+                       max_acceleration = prev_state.max_acceleration;
+                       max_speed = prev_state.max_speed;
+
+                       /* make sure the clock is monotonic */
+                       while (tick < prev_state.tick)
+                               tick += 65536;
+
+                       time_change = (tick - prev_state.tick) / 100.0;
+
+                       /* compute barometric speed */
+
+                       double height_change = height - prev_state.height;
+                       if (time_change > 0)
+                               baro_speed = (prev_state.baro_speed * 3 + (height_change / time_change)) / 4.0;
+                       else
+                               baro_speed = prev_state.baro_speed;
+               } else {
+                       npad = 0;
+                       gps = null;
+                       baro_speed = 0;
+                       time_change = 0;
+               }
+
+               if (state == AltosTelemetry.ao_flight_pad) {
+                       if (data.gps != null && data.gps.gps_locked && data.gps.nsat >= 4) {
+                               npad++;
+                               if (npad > 1) {
+                                       /* filter pad position */
+                                       pad_lat = (pad_lat * 31.0 + data.gps.lat) / 32.0;
+                                       pad_lon = (pad_lon * 31.0 + data.gps.lon) / 32.0;
+                                       pad_alt = (pad_alt * 31.0 + data.gps.alt) / 32.0;
+                               } else {
+                                       pad_lat = data.gps.lat;
+                                       pad_lon = data.gps.lon;
+                                       pad_alt = data.gps.alt;
+                               }
+                       }
+               }
+               ascent = (AltosTelemetry.ao_flight_boost <= state &&
+                         state <= AltosTelemetry.ao_flight_coast);
+
+               /* Only look at accelerometer data on the way up */
+               if (ascent && acceleration > max_acceleration)
+                       max_acceleration = acceleration;
+               if (ascent && speed > max_speed)
+                       max_speed = speed;
+
+               if (height > max_height)
+                       max_height = height;
+               if (data.gps != null) {
+                       if (gps == null || !gps.gps_locked || data.gps.gps_locked)
+                               gps = data.gps;
+                       if (npad > 0 && gps.gps_locked)
+                               from_pad = new AltosGreatCircle(pad_lat, pad_lon, gps.lat, gps.lon);
+               }
+               if (npad > 0) {
+                       gps_height = gps.alt - pad_alt;
+               } else {
+                       gps_height = 0;
+               }
+       }
+
+       public AltosState(AltosTelemetry cur) {
+               init(cur, null);
+       }
+
+       public AltosState (AltosTelemetry cur, AltosState prev) {
+               init(cur, prev);
+       }
+}
diff --git a/ao-tools/altosui/AltosTelemetry.java b/ao-tools/altosui/AltosTelemetry.java
new file mode 100644 (file)
index 0000000..e13b42e
--- /dev/null
@@ -0,0 +1,186 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package altosui;
+
+import java.lang.*;
+import java.text.*;
+import java.util.HashMap;
+import altosui.AltosConvert;
+import altosui.AltosGPS;
+
+/*
+ * Telemetry data contents
+ */
+
+
+/*
+ * The telemetry data stream is a bit of a mess at present, with no consistent
+ * formatting. In particular, the GPS data is formatted for viewing instead of parsing.
+ * However, the key feature is that every telemetry line contains all of the information
+ * necessary to describe the current rocket state, including the calibration values
+ * for accelerometer and barometer.
+ *
+ * GPS unlocked:
+ *
+ * VERSION 2 CALL KB0G SERIAL  51 FLIGHT     2 RSSI  -68 STATUS ff STATE     pad  1001 \
+ *    a: 16032 p: 21232 t: 20284 v: 25160 d:   204 m:   204 fa: 16038 ga: 16032 fv:       0 \
+ *    fp: 21232 gp: 21230 a+: 16049 a-: 16304 GPS  0 sat unlocked SAT 1   15  30
+ *
+ * GPS locked:
+ *
+ * VERSION 2 CALL KB0G SERIAL  51 FLIGHT     2 RSSI  -71 STATUS ff STATE     pad  2504 \
+ *     a: 16028 p: 21220 t: 20360 v: 25004 d:   208 m:   200 fa: 16031 ga: 16032 fv:     330 \
+ *     fp: 21231 gp: 21230 a+: 16049 a-: 16304 \
+ *     GPS  9 sat 2010-02-13 17:16:51 35°20.0803'N 106°45.2235'W  1790m  \
+ *     0.00m/s(H) 0°     0.00m/s(V) 1.0(hdop)     0(herr)     0(verr) \
+ *     SAT 10   29  30  24  28   5  25  21  20  15  33   1  23  30  24  18  26  10  29   2  26
+ */
+
+public class AltosTelemetry {
+       int     version;
+       String  callsign;
+       int     serial;
+       int     flight;
+       int     rssi;
+       int     status;
+       String  state;
+       int     tick;
+       int     accel;
+       int     pres;
+       int     temp;
+       int     batt;
+       int     drogue;
+       int     main;
+       int     flight_accel;
+       int     ground_accel;
+       int     flight_vel;
+       int     flight_pres;
+       int     ground_pres;
+       int     accel_plus_g;
+       int     accel_minus_g;
+       AltosGPS        gps;
+
+       public static final int ao_flight_startup = 0;
+       public static final int ao_flight_idle = 1;
+       public static final int ao_flight_pad = 2;
+       public static final int ao_flight_boost = 3;
+       public static final int ao_flight_fast = 4;
+       public static final int ao_flight_coast = 5;
+       public static final int ao_flight_drogue = 6;
+       public static final int ao_flight_main = 7;
+       public static final int ao_flight_landed = 8;
+       public static final int ao_flight_invalid = 9;
+
+       static HashMap<String,Integer>  states = new HashMap<String,Integer>();
+       {
+               states.put("startup", ao_flight_startup);
+               states.put("idle", ao_flight_idle);
+               states.put("pad", ao_flight_pad);
+               states.put("boost", ao_flight_boost);
+               states.put("fast", ao_flight_fast);
+               states.put("coast", ao_flight_coast);
+               states.put("drogue", ao_flight_drogue);
+               states.put("main", ao_flight_main);
+               states.put("landed", ao_flight_landed);
+               states.put("invalid", ao_flight_invalid);
+       }
+
+       public int state() {
+               if (states.containsKey(state))
+                       return states.get(state);
+               return ao_flight_invalid;
+       }
+
+       public double altitude() {
+               return AltosConvert.cc_pressure_to_altitude(AltosConvert.cc_barometer_to_pressure(pres));
+       }
+
+       public double pad_altitude() {
+               return AltosConvert.cc_pressure_to_altitude(AltosConvert.cc_barometer_to_pressure(ground_pres));
+       }
+
+       public AltosTelemetry(String line) throws ParseException {
+               String[] words = line.split("\\s+");
+
+               int     i = 0;
+
+               AltosParse.word (words[i++], "VERSION");
+               version = AltosParse.parse_int(words[i++]);
+
+               AltosParse.word (words[i++], "CALL");
+               callsign = words[i++];
+
+               AltosParse.word (words[i++], "SERIAL");
+               serial = AltosParse.parse_int(words[i++]);
+
+               AltosParse.word (words[i++], "FLIGHT");
+               flight = AltosParse.parse_int(words[i++]);
+
+               AltosParse.word(words[i++], "RSSI");
+               rssi = AltosParse.parse_int(words[i++]);
+
+               AltosParse.word(words[i++], "STATUS");
+               status = AltosParse.parse_hex(words[i++]);
+
+               AltosParse.word(words[i++], "STATE");
+               state = words[i++];
+
+               tick = AltosParse.parse_int(words[i++]);
+
+               AltosParse.word(words[i++], "a:");
+               accel = AltosParse.parse_int(words[i++]);
+
+               AltosParse.word(words[i++], "p:");
+               pres = AltosParse.parse_int(words[i++]);
+
+               AltosParse.word(words[i++], "t:");
+               temp = AltosParse.parse_int(words[i++]);
+
+               AltosParse.word(words[i++], "v:");
+               batt = AltosParse.parse_int(words[i++]);
+
+               AltosParse.word(words[i++], "d:");
+               drogue = AltosParse.parse_int(words[i++]);
+
+               AltosParse.word(words[i++], "m:");
+               main = AltosParse.parse_int(words[i++]);
+
+               AltosParse.word(words[i++], "fa:");
+               flight_accel = AltosParse.parse_int(words[i++]);
+
+               AltosParse.word(words[i++], "ga:");
+               ground_accel = AltosParse.parse_int(words[i++]);
+
+               AltosParse.word(words[i++], "fv:");
+               flight_vel = AltosParse.parse_int(words[i++]);
+
+               AltosParse.word(words[i++], "fp:");
+               flight_pres = AltosParse.parse_int(words[i++]);
+
+               AltosParse.word(words[i++], "gp:");
+               ground_pres = AltosParse.parse_int(words[i++]);
+
+               AltosParse.word(words[i++], "a+:");
+               accel_plus_g = AltosParse.parse_int(words[i++]);
+
+               AltosParse.word(words[i++], "a-:");
+               accel_minus_g = AltosParse.parse_int(words[i++]);
+
+               gps = new AltosGPS(words, i);
+       }
+}
diff --git a/ao-tools/altosui/AltosUI.java b/ao-tools/altosui/AltosUI.java
new file mode 100644 (file)
index 0000000..7f008f3
--- /dev/null
@@ -0,0 +1,739 @@
+/*
+ * Copyright © 2010 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package altosui;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.filechooser.FileNameExtensionFilter;
+import javax.swing.table.*;
+import java.io.*;
+import java.util.*;
+import java.text.*;
+import java.util.prefs.*;
+import gnu.io.*;
+
+import altosui.AltosSerial;
+import altosui.AltosSerialMonitor;
+import altosui.AltosTelemetry;
+import altosui.AltosState;
+import altosui.AltosDeviceDialog;
+
+class AltosFlightStatusTableModel extends AbstractTableModel {
+       private String[] columnNames = {"Height (m)", "State", "RSSI (dBm)", "Speed (m/s)" };
+       private Object[] data = { 0, "idle", 0, 0 };
+
+       public int getColumnCount() { return columnNames.length; }
+       public int getRowCount() { return 2; }
+       public Object getValueAt(int row, int col) {
+               if (row == 0)
+                       return columnNames[col];
+               return data[col];
+       }
+
+       public void setValueAt(Object value, int col) {
+               data[col] = value;
+               fireTableCellUpdated(1, col);
+       }
+
+       public void setValueAt(Object value, int row, int col) {
+               setValueAt(value, col);
+       }
+
+       public void set(AltosState state) {
+               setValueAt(String.format("%1.0f", state.height), 0);
+               setValueAt(state.data.state, 1);
+               setValueAt(state.data.rssi, 2);
+               double speed = state.baro_speed;
+               if (state.ascent)
+                       speed = state.speed;
+               setValueAt(String.format("%1.0f", speed), 3);
+       }
+}
+
+class AltosFlightInfoTableModel extends AbstractTableModel {
+       private String[] columnNames = {"Field", "Value"};
+
+       class InfoLine {
+               String  name;
+               String  value;
+
+               public InfoLine(String n, String v) {
+                       name = n;
+                       value = v;
+               }
+       }
+
+       private ArrayList<InfoLine> rows = new ArrayList<InfoLine>();
+
+       public int getColumnCount() { return columnNames.length; }
+       public String getColumnName(int col) { return columnNames[col]; }
+
+       public int getRowCount() { return 20; }
+
+       public Object getValueAt(int row, int col) {
+               if (row >= rows.size())
+                       return "";
+               if (col == 0)
+                       return rows.get(row).name;
+               else
+                       return rows.get(row).value;
+       }
+
+       int     current_row = 0;
+       int     prev_num_rows = 0;
+
+       public void resetRow() {
+               current_row = 0;
+       }
+       public void addRow(String name, String value) {
+               if (current_row >= rows.size())
+                       rows.add(current_row, new InfoLine(name, value));
+               else
+                       rows.set(current_row, new InfoLine(name, value));
+               current_row++;
+       }
+       public void finish() {
+               if (current_row > prev_num_rows) {
+                       fireTableRowsInserted(prev_num_rows, current_row - 1);
+                       prev_num_rows = current_row;
+               }
+               fireTableDataChanged();
+       }
+}
+
+public class AltosUI extends JFrame {
+       private int channel = -1;
+
+       private AltosFlightStatusTableModel flightStatusModel;
+       private JTable flightStatus;
+
+       static final int info_columns = 3;
+
+       private AltosFlightInfoTableModel[] flightInfoModel;
+       private JTable[] flightInfo;
+       private AltosSerial serialLine;
+       private Box[] ibox;
+       private Box vbox;
+       private Box hbox;
+
+       private Font statusFont = new Font("SansSerif", Font.BOLD, 24);
+       private Font infoLabelFont = new Font("SansSerif", Font.PLAIN, 14);
+       private Font infoValueFont = new Font("Monospaced", Font.PLAIN, 14);
+
+       public AltosUI() {
+
+               String[] statusNames = { "Height (m)", "State", "RSSI (dBm)", "Speed (m/s)" };
+               Object[][] statusData = { { "0", "pad", "-50", "0" } };
+
+               vbox = Box.createVerticalBox();
+               this.add(vbox);
+
+               flightStatusModel = new AltosFlightStatusTableModel();
+               flightStatus = new JTable(flightStatusModel);
+               flightStatus.setFont(statusFont);
+               TableColumnModel tcm = flightStatus.getColumnModel();
+               for (int i = 0; i < flightStatusModel.getColumnCount(); i++) {
+                       DefaultTableCellRenderer       r = new DefaultTableCellRenderer();
+                       r.setFont(statusFont);
+                       r.setHorizontalAlignment(SwingConstants.CENTER);
+                       tcm.getColumn(i).setCellRenderer(r);
+               }
+
+               FontMetrics     statusMetrics = flightStatus.getFontMetrics(statusFont);
+               int statusHeight = (statusMetrics.getHeight() + statusMetrics.getLeading()) * 15 / 10;
+               flightStatus.setRowHeight(statusHeight);
+               flightStatus.setShowGrid(false);
+
+               vbox.add(flightStatus);
+
+               hbox = Box.createHorizontalBox();
+               vbox.add(hbox);
+
+               flightInfo = new JTable[3];
+               flightInfoModel = new AltosFlightInfoTableModel[3];
+               ibox = new Box[3];
+               FontMetrics     infoValueMetrics = flightStatus.getFontMetrics(infoValueFont);
+               int infoHeight = (infoValueMetrics.getHeight() + infoValueMetrics.getLeading()) * 20 / 10;
+
+               for (int i = 0; i < info_columns; i++) {
+                       ibox[i] = Box.createVerticalBox();
+                       flightInfoModel[i] = new AltosFlightInfoTableModel();
+                       flightInfo[i] = new JTable(flightInfoModel[i]);
+                       flightInfo[i].setFont(infoValueFont);
+                       flightInfo[i].setRowHeight(infoHeight);
+                       flightInfo[i].setShowGrid(true);
+                       ibox[i].add(flightInfo[i].getTableHeader());
+                       ibox[i].add(flightInfo[i]);
+                       hbox.add(ibox[i]);
+               }
+
+               setTitle("AltOS");
+
+               createMenu();
+
+               serialLine = new AltosSerial();
+               int dpi = Toolkit.getDefaultToolkit().getScreenResolution();
+               this.setSize(new Dimension (infoValueMetrics.charWidth('0') * 6 * 20,
+                                           statusHeight * 4 + infoHeight * 17));
+               this.validate();
+               setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
+               addWindowListener(new WindowAdapter() {
+                       @Override
+                       public void windowClosing(WindowEvent e) {
+                               System.exit(0);
+                       }
+               });
+       }
+
+       public void info_reset() {
+               for (int i = 0; i < info_columns; i++)
+                       flightInfoModel[i].resetRow();
+       }
+
+       public void info_add_row(int col, String name, String value) {
+               flightInfoModel[col].addRow(name, value);
+       }
+
+       public void info_add_row(int col, String name, String format, Object value) {
+               flightInfoModel[col].addRow(name, String.format(format, value));
+       }
+
+       public void info_add_row(int col, String name, String format, Object v1, Object v2) {
+               flightInfoModel[col].addRow(name, String.format(format, v1, v2));
+       }
+
+       public void info_add_row(int col, String name, String format, Object v1, Object v2, Object v3) {
+               flightInfoModel[col].addRow(name, String.format(format, v1, v2, v3));
+       }
+
+       public void info_add_deg(int col, String name, double v, int pos, int neg) {
+               int     c = pos;
+               if (v < 0) {
+                       c = neg;
+                       v = -v;
+               }
+               double  deg = Math.floor(v);
+               double  min = (v - deg) * 60;
+
+               flightInfoModel[col].addRow(name, String.format("%3.0f°%08.5f'", deg, min));
+       }
+
+       public void info_finish() {
+               for (int i = 0; i < info_columns; i++)
+                       flightInfoModel[i].finish();
+       }
+
+       static final int MIN_PAD_SAMPLES = 10;
+
+       public void show(AltosState state) {
+               flightStatusModel.set(state);
+
+               info_reset();
+               if (state.npad >= MIN_PAD_SAMPLES)
+                       info_add_row(0, "Ground state", "%s", "ready");
+               else
+                       info_add_row(0, "Ground state", "wait (%d)",
+                                    MIN_PAD_SAMPLES - state.npad);
+               info_add_row(0, "Rocket state", "%s", state.data.state);
+               info_add_row(0, "Callsign", "%s", state.data.callsign);
+               info_add_row(0, "Rocket serial", "%6d", state.data.serial);
+               info_add_row(0, "Rocket flight", "%6d", state.data.flight);
+
+               info_add_row(0, "RSSI", "%6d    dBm", state.data.rssi);
+               info_add_row(0, "Height", "%6.0f    m", state.height);
+               info_add_row(0, "Max height", "%6.0f    m", state.max_height);
+               info_add_row(0, "Acceleration", "%8.1f  m/s²", state.acceleration);
+               info_add_row(0, "Max acceleration", "%8.1f  m/s²", state.max_acceleration);
+               info_add_row(0, "Speed", "%8.1f  m/s", state.ascent ? state.speed : state.baro_speed);
+               info_add_row(0, "Max Speed", "%8.1f  m/s", state.max_speed);
+               info_add_row(0, "Temperature", "%9.2f °C", state.temperature);
+               info_add_row(0, "Battery", "%9.2f V", state.battery);
+               info_add_row(0, "Drogue", "%9.2f V", state.drogue_sense);
+               info_add_row(0, "Main", "%9.2f V", state.main_sense);
+               info_add_row(0, "Pad altitude", "%6.0f    m", state.ground_altitude);
+               if (state.gps == null) {
+                       info_add_row(1, "GPS", "not available");
+               } else {
+                       if (state.data.gps.gps_locked)
+                               info_add_row(1, "GPS", "   locked");
+                       else if (state.data.gps.gps_connected)
+                               info_add_row(1, "GPS", " unlocked");
+                       else
+                               info_add_row(1, "GPS", "  missing");
+                       info_add_row(1, "Satellites", "%6d", state.data.gps.nsat);
+                       info_add_deg(1, "Latitude", state.gps.lat, 'N', 'S');
+                       info_add_deg(1, "Longitude", state.gps.lon, 'E', 'W');
+                       info_add_row(1, "GPS altitude", "%6d", state.gps.alt);
+                       info_add_row(1, "GPS height", "%6.0f", state.gps_height);
+
+                       /* The SkyTraq GPS doesn't report these values */
+                       if (false) {
+                               info_add_row(1, "GPS ground speed", "%8.1f m/s %3d°",
+                                            state.gps.ground_speed,
+                                            state.gps.course);
+                               info_add_row(1, "GPS climb rate", "%8.1f m/s",
+                                            state.gps.climb_rate);
+                               info_add_row(1, "GPS error", "%6d m(h)%3d m(v)",
+                                            state.gps.h_error, state.gps.v_error);
+                       }
+                       info_add_row(1, "GPS hdop", "%8.1f", state.gps.hdop);
+
+                       if (state.npad > 0) {
+                               if (state.from_pad != null) {
+                                       info_add_row(1, "Distance from pad", "%6.0f m", state.from_pad.distance);
+                                       info_add_row(1, "Direction from pad", "%6.0f°", state.from_pad.bearing);
+                               } else {
+                                       info_add_row(1, "Distance from pad", "unknown");
+                                       info_add_row(1, "Direction from pad", "unknown");
+                               }
+                               info_add_deg(1, "Pad latitude", state.pad_lat, 'N', 'S');
+                               info_add_deg(1, "Pad longitude", state.pad_lon, 'E', 'W');
+                               info_add_row(1, "Pad GPS alt", "%6.0f m", state.pad_alt);
+                       }
+                       info_add_row(1, "GPS date", "%04d-%02d-%02d",
+                                      state.gps.gps_time.year,
+                                      state.gps.gps_time.month,
+                                      state.gps.gps_time.day);
+                       info_add_row(1, "GPS time", "  %02d:%02d:%02d",
+                                      state.gps.gps_time.hour,
+                                      state.gps.gps_time.minute,
+                                      state.gps.gps_time.second);
+                       int     nsat_vis = 0;
+                       int     c;
+
+                       if (state.gps.cc_gps_sat == null)
+                               info_add_row(2, "Satellites Visible", "%4d", 0);
+                       else {
+                               info_add_row(2, "Satellites Visible", "%4d", state.gps.cc_gps_sat.length);
+                               for (c = 0; c < state.gps.cc_gps_sat.length; c++) {
+                                       info_add_row(2, "Satellite id,C/N0",
+                                                    "%4d, %4d",
+                                                    state.gps.cc_gps_sat[c].svid,
+                                                    state.gps.cc_gps_sat[c].c_n0);
+                               }
+                       }
+               }
+               info_finish();
+       }
+
+       /* User Preferences */
+       Preferences altosui_preferences = Preferences.userNodeForPackage(this.getClass());
+
+       /* Log directory */
+       private File logdir = null;
+
+       /* logdir preference name */
+       final static String logdirPreference = "LOGDIR";
+
+       /* Default logdir is ~/TeleMetrum */
+       final static String logdirName = "TeleMetrum";
+
+       /* Initialize logdir from preferences */
+       {
+               String logdir_string = altosui_preferences.get(logdirPreference, null);
+               if (logdir_string != null)
+                       logdir = new File(logdir_string);
+               else
+                       /* a hack -- make the file chooser tell us what the default directory
+                        * would be and stick our logdir in a subdirectory of that.
+                        */
+                       logdir = new File(new JFileChooser().getCurrentDirectory(), logdirName);
+       }
+
+       private void set_logdir(File new_logdir) {
+               logdir = new_logdir;
+               System.out.printf("Set logdir to %s\n", logdir.toString());
+               synchronized (altosui_preferences) {
+                       altosui_preferences.put(logdirPreference, logdir.getPath());
+                       try {
+                               altosui_preferences.flush();
+                       } catch (BackingStoreException ee) {
+                               JOptionPane.showMessageDialog(AltosUI.this,
+                                                             altosui_preferences.absolutePath(),
+                                                             "Cannot save prefernces",
+                                                             JOptionPane.ERROR_MESSAGE);
+                       }
+               }
+       }
+
+       private boolean check_dir(File dir) {
+               if (!dir.exists()) {
+                       if (!dir.mkdirs()) {
+                               JOptionPane.showMessageDialog(AltosUI.this,
+                                                             dir.getName(),
+                                                             "Cannot create directory",
+                                                             JOptionPane.ERROR_MESSAGE);
+                               return false;
+                       }
+               } else if (!dir.isDirectory()) {
+                       JOptionPane.showMessageDialog(AltosUI.this,
+                                                     dir.getName(),
+                                                     "Is not a directory",
+                                                     JOptionPane.ERROR_MESSAGE);
+                       return false;
+               }
+               return true;
+       }
+
+       private void PickSerialDevice() {
+               java.util.Enumeration<CommPortIdentifier> port_list = CommPortIdentifier.getPortIdentifiers();
+               while (port_list.hasMoreElements()) {
+                       CommPortIdentifier identifier = port_list.nextElement();
+                       System.out.println("Serial port " + identifier.getName());
+               }
+       }
+
+       class DisplayThread extends Thread {
+               String read() throws InterruptedException { return null; }
+
+               void close() { }
+
+               void update(AltosState state) throws InterruptedException { }
+
+               public void run() {
+                       String          line;
+                       AltosState      state = null;
+
+                       info_reset();
+                       info_finish();
+                       try {
+                               while ((line = read()) != null) {
+                                       try {
+                                               AltosTelemetry  t = new AltosTelemetry(line);
+                                               state = new AltosState(t, state);
+                                               update(state);
+                                               show(state);
+                                       } catch (ParseException pp) {
+                                               System.out.printf("Parse error on %s\n", line);
+                                               System.out.println("exception " + pp);
+                                       }
+                               }
+                       } catch (InterruptedException ee) {
+                       } finally {
+                               close();
+                       }
+               }
+       }
+
+       class DeviceThread extends DisplayThread {
+               AltosSerial     serial;
+
+               String read() throws InterruptedException {
+                       System.out.println("Waiting for telemetry");
+                       String s = serial.get_telem();
+                       System.out.println("Got telemetry " + s);
+                       return s;
+               }
+
+               void close() {
+                       serial.close();
+                       System.out.println("DisplayThread done");
+               }
+
+               public DeviceThread(AltosSerial s) {
+                       serial = s;
+               }
+       }
+
+       private void ConnectToDevice() {
+               AltosDevice     device = AltosDeviceDialog.show(AltosUI.this, "TeleDongle");
+
+               if (device != null) {
+                       try {
+                               serialLine.connect(device.tty);
+                               DeviceThread thread = new DeviceThread(serialLine);
+                               run_display(thread);
+                       } catch (FileNotFoundException ee) {
+                               JOptionPane.showMessageDialog(AltosUI.this,
+                                                             device.tty,
+                                                             "Cannot open serial port",
+                                                             JOptionPane.ERROR_MESSAGE);
+                       } catch (NoSuchPortException ee) {
+                               JOptionPane.showMessageDialog(AltosUI.this,
+                                                             device.tty,
+                                                             "No such serial port",
+                                                             JOptionPane.ERROR_MESSAGE);
+                       } catch (PortInUseException ee) {
+                               JOptionPane.showMessageDialog(AltosUI.this,
+                                                             device.tty,
+                                                             "Port in use",
+                                                             JOptionPane.ERROR_MESSAGE);
+                       } catch (IOException ee) {
+                               JOptionPane.showMessageDialog(AltosUI.this,
+                                                             device.tty,
+                                                             "Unkonwn I/O error",
+                                                             JOptionPane.ERROR_MESSAGE);
+                       }
+               }
+       }
+
+       void DisconnectFromDevice () {
+               stop_display();
+       }
+
+       String readline(FileInputStream s) throws IOException {
+               int c;
+               String  line = "";
+
+               while ((c = s.read()) != -1) {
+                       if (c == '\r')
+                               continue;
+                       if (c == '\n')
+                               return line;
+                       line = line + (char) c;
+               }
+               return null;
+       }
+
+       /*
+        * Open an existing telemetry file and replay it in realtime
+        */
+
+       class ReplayThread extends DisplayThread {
+               FileInputStream replay;
+               String filename;
+
+               ReplayThread(FileInputStream in, String name) {
+                       replay = in;
+                       filename = name;
+               }
+
+               String read() {
+                       try {
+                               return readline(replay);
+                       } catch (IOException ee) {
+                               JOptionPane.showMessageDialog(AltosUI.this,
+                                                             filename,
+                                                             "error reading",
+                                                             JOptionPane.ERROR_MESSAGE);
+                       }
+                       return null;
+               }
+
+               void close () {
+                       try {
+                               replay.close();
+                       } catch (IOException ee) {
+                       }
+               }
+
+               void update(AltosState state) throws InterruptedException {
+                       /* Make it run in realtime after the rocket leaves the pad */
+                       if (state.state > AltosTelemetry.ao_flight_pad)
+                               Thread.sleep((int) (state.time_change * 1000));
+               }
+       }
+
+       Thread          display_thread;
+
+       private void stop_display() {
+               if (display_thread != null && display_thread.isAlive())
+                       display_thread.interrupt();
+               display_thread = null;
+       }
+
+       private void run_display(Thread thread) {
+               stop_display();
+               display_thread = thread;
+               display_thread.start();
+       }
+
+       /*
+        * Replay a flight from telemetry data
+        */
+       private void Replay() {
+               JFileChooser    logfile_chooser = new JFileChooser();
+
+               logfile_chooser.setDialogTitle("Select Telemetry File");
+               logfile_chooser.setFileFilter(new FileNameExtensionFilter("Telemetry file", "telem"));
+               logfile_chooser.setCurrentDirectory(logdir);
+               int returnVal = logfile_chooser.showOpenDialog(AltosUI.this);
+
+               if (returnVal == JFileChooser.APPROVE_OPTION) {
+                       File file = logfile_chooser.getSelectedFile();
+                       if (file == null)
+                               System.out.println("No file selected?");
+                       String  filename = file.getName();
+                       try {
+                               FileInputStream replay = new FileInputStream(file);
+                               ReplayThread    thread = new ReplayThread(replay, filename);
+                               run_display(thread);
+                       } catch (FileNotFoundException ee) {
+                               JOptionPane.showMessageDialog(AltosUI.this,
+                                                             filename,
+                                                             "Cannot open serial port",
+                                                             JOptionPane.ERROR_MESSAGE);
+                       }
+               }
+       }
+
+       /* Connect to TeleMetrum, either directly or through
+        * a TeleDongle over the packet link
+        */
+       private void SaveFlightData() {
+       }
+
+       /* Configure the log directory. This is where all telemetry and eeprom files
+        * will be written to, and where replay will look for telemetry files
+        */
+       private void ConfigureLog() {
+               JFileChooser    logdir_chooser = new JFileChooser();
+
+               logdir_chooser.setDialogTitle("Configure Data Logging Directory");
+               logdir_chooser.setCurrentDirectory(logdir.getParentFile());
+               logdir_chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+
+               if (logdir_chooser.showDialog(AltosUI.this, "Select Directory") == JFileChooser.APPROVE_OPTION) {
+                       File dir = logdir_chooser.getSelectedFile();
+                       if (check_dir(dir))
+                               set_logdir(dir);
+               }
+       }
+
+       /* Create the AltosUI menus
+        */
+       private void createMenu() {
+               JMenuBar menubar = new JMenuBar();
+               JMenu menu;
+               JMenuItem item;
+               JRadioButtonMenuItem radioitem;
+
+               // File menu
+               {
+                       menu = new JMenu("File");
+                       menu.setMnemonic(KeyEvent.VK_F);
+                       menubar.add(menu);
+
+                       item = new JMenuItem("Quit",KeyEvent.VK_Q);
+                       item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q,
+                                                                  ActionEvent.CTRL_MASK));
+                       item.addActionListener(new ActionListener() {
+                                       public void actionPerformed(ActionEvent e) {
+                                               System.exit(0);
+                                       }
+                               });
+                       menu.add(item);
+               }
+
+               // Device menu
+               {
+                       menu = new JMenu("Device");
+                       menu.setMnemonic(KeyEvent.VK_D);
+                       menubar.add(menu);
+
+                       item = new JMenuItem("Connect to Device",KeyEvent.VK_C);
+                       item.addActionListener(new ActionListener() {
+                                       public void actionPerformed(ActionEvent e) {
+                                               ConnectToDevice();
+                                       }
+                               });
+                       menu.add(item);
+
+                       item = new JMenuItem("Disconnect from Device",KeyEvent.VK_D);
+                       item.addActionListener(new ActionListener() {
+                                       public void actionPerformed(ActionEvent e) {
+                                               DisconnectFromDevice();
+                                       }
+                               });
+                       menu.add(item);
+
+                       menu.addSeparator();
+
+                       item = new JMenuItem("Save Flight Data",KeyEvent.VK_S);
+                       item.addActionListener(new ActionListener() {
+                                       public void actionPerformed(ActionEvent e) {
+                                               SaveFlightData();
+                                       }
+                               });
+                       menu.add(item);
+
+                       item = new JMenuItem("Replay",KeyEvent.VK_R);
+                       item.addActionListener(new ActionListener() {
+                                       public void actionPerformed(ActionEvent e) {
+                                               Replay();
+                                       }
+                               });
+                       menu.add(item);
+               }
+               // Log menu
+               {
+                       menu = new JMenu("Log");
+                       menu.setMnemonic(KeyEvent.VK_L);
+                       menubar.add(menu);
+
+                       item = new JMenuItem("New Log",KeyEvent.VK_N);
+                       item.addActionListener(new ActionListener() {
+                                       public void actionPerformed(ActionEvent e) {
+                                       }
+                               });
+                       menu.add(item);
+
+                       item = new JMenuItem("Configure Log",KeyEvent.VK_C);
+                       item.addActionListener(new ActionListener() {
+                                       public void actionPerformed(ActionEvent e) {
+                                               ConfigureLog();
+                                       }
+                               });
+                       menu.add(item);
+               }
+               // Voice menu
+               {
+                       menu = new JMenu("Voice", true);
+                       menu.setMnemonic(KeyEvent.VK_V);
+                       menubar.add(menu);
+
+                       radioitem = new JRadioButtonMenuItem("Enable Voice");
+                       radioitem.addActionListener(new ActionListener() {
+                                       public void actionPerformed(ActionEvent e) {
+                                       }
+                               });
+                       menu.add(radioitem);
+               }
+
+               // Channel menu
+               {
+                       menu = new JMenu("Channel", true);
+                       menu.setMnemonic(KeyEvent.VK_C);
+                       menubar.add(menu);
+                       ButtonGroup group = new ButtonGroup();
+
+                       for (int c = 0; c <= 9; c++) {
+                               radioitem = new JRadioButtonMenuItem(String.format("Channel %1d (%7.3fMHz)", c,
+                                                                                  434.550 + c * 0.1),
+                                                                    c == 0);
+                               radioitem.setActionCommand(String.format("%d", c));
+                               radioitem.addActionListener(new ActionListener() {
+                                               public void actionPerformed(ActionEvent e) {
+                                                       System.out.println("Command: " + e.getActionCommand() + " param: " +
+                                                                          e.paramString());
+                                               }
+                                       });
+                               menu.add(radioitem);
+                               group.add(radioitem);
+                       }
+               }
+
+               this.setJMenuBar(menubar);
+
+       }
+       public static void main(final String[] args) {
+               AltosUI altosui = new AltosUI();
+               altosui.setVisible(true);
+       }
+}
\ No newline at end of file
diff --git a/ao-tools/altosui/Makefile b/ao-tools/altosui/Makefile
new file mode 100644 (file)
index 0000000..fbe0a5e
--- /dev/null
@@ -0,0 +1,34 @@
+.SUFFIXES: .java .class
+
+CLASSPATH=..:/usr/share/java/*
+CLASSFILES=\
+       AltosConvert.class \
+       AltosGPS.class \
+       AltosGreatCircle.class \
+       AltosParse.class \
+       AltosSerialMonitor.class \
+       AltosSerial.class \
+       AltosState.class \
+       AltosTelemetry.class \
+       AltosUI.class \
+       AltosDevice.class \
+       AltosDeviceLinux.class \
+       AltosDeviceDialog.class
+
+JAVAFLAGS=-Xlint:unchecked
+
+all: $(CLASSFILES) altosui altosui.jar
+
+.java.class:
+       javac -cp "$(CLASSPATH)" $(JAVAFLAGS) $*.java
+
+altosui: Makefile
+       (echo '#!/bin/sh'; \
+       echo exec java -cp '"$(CLASSPATH)"' altosui/AltosUI) > $@
+       chmod +x $@
+
+altosui.jar: $(CLASSFILES) Manifest.txt
+       cd .. && jar cfm altosui/$@ altosui/Manifest.txt altosui/*.class
+
+clean:
+       rm -f *.class
diff --git a/ao-tools/altosui/Manifest.txt b/ao-tools/altosui/Manifest.txt
new file mode 100644 (file)
index 0000000..0305fcf
--- /dev/null
@@ -0,0 +1 @@
+Main-Class: altosui.AltosUI
index bc702b0f4cd29275810a574636176378c9ac4a0a..00e6c9deeaae0f3a0acd207f6933f42178a6efe6 100644 (file)
@@ -20,7 +20,7 @@
 #include "aoview.h"
 #include <alsa/asoundlib.h>
 
-cst_voice *register_cmu_us_kal();
+cst_voice *register_cmu_us_kal16();
 static cst_voice *voice;
 
 static FILE *pipe_write;
@@ -118,7 +118,7 @@ aoview_flite_start(void)
 
        if (!once) {
                flite_init();
-               voice = register_cmu_us_kal();
+               voice = register_cmu_us_kal16();
                if (!voice) {
                        perror("register voice");
                        exit(1);
index e2699ee680350e8d9b982393ff72c4aa7bb3ff67..24f562e1062e109002cd6eafc2ba62b2f1dea33a 100644 (file)
@@ -4,7 +4,7 @@
 #
 CC=sdcc
 
-SUBDIRS=telemetrum-v0.2 teledongle-v0.2 telemetrum-v0.1-sky telemetrum-v0.1-sirf teledongle-v0.1 tidongle test
+SUBDIRS=telemetrum-v1.0 teledongle-v0.2 telemetrum-v0.1-sky telemetrum-v0.1-sirf teledongle-v0.1 tidongle test
 
 all: all-recursive
 
index aefefa461390216815c85e0226c5fa2b76448eba..97cb75ae59ca016f7f0c953c094827c134690360 100644 (file)
--- a/src/ao.h
+++ b/src/ao.h
@@ -43,7 +43,7 @@ struct ao_task {
        __xdata void *wchan;            /* current wait channel (NULL if running) */
        uint16_t alarm;                 /* abort ao_sleep time */
        uint8_t stack_count;            /* amount of saved stack */
-       uint8_t task_id;                /* index in the task array */
+       uint8_t task_id;                /* unique id */
        __code char *name;              /* task name */
        uint8_t stack[AO_STACK_SIZE];   /* saved stack */
 };
index 628b6e6733ba480c0099a2fbbb9566cd48d6b239..f2f3fc2e39650b98482dd44eff84f11881db3bbe 100644 (file)
@@ -80,7 +80,8 @@ ao_set_monitor(uint8_t monitoring)
 {
        ao_monitoring = monitoring;
        ao_wakeup(&ao_monitoring);
-       ao_radio_abort();
+       if (!ao_monitoring)
+               ao_radio_abort();
 }
 
 static void
index 771d9c7f7ac1791113fd5edea2a192f907c6988a..e9a265b0837a4c300f54f1c8e51e94b4b6d1ee54 100644 (file)
@@ -18,7 +18,7 @@
 #ifndef _AO_PINS_H_
 #define _AO_PINS_H_
 
-#if defined(TELEMETRUM_V_0_2)
+#if defined(TELEMETRUM_V_1_0)
        #define HAS_SERIAL_1            1
        #define HAS_ADC                 1
        #define HAS_EEPROM              1
index bb19c6a99f77934c00f8007d9dcb15375f479c78..0849349edb1d10a59208aebad7929c523da414f4 100644 (file)
@@ -317,8 +317,8 @@ ao_radio_idle(void)
 {
        if (RF_MARCSTATE != RF_MARCSTATE_IDLE)
        {
-               RFST = RFST_SIDLE;
                do {
+                       RFST = RFST_SIDLE;
                        ao_yield();
                } while (RF_MARCSTATE != RF_MARCSTATE_IDLE);
        }
index 4a78766e2828664b1687b09f0ea28a02ac987066..72c9d7d61f67216a71436ce9ffd7c4d0989c5bef 100644 (file)
@@ -28,10 +28,19 @@ void
 ao_add_task(__xdata struct ao_task * task, void (*start)(void), __code char *name) __reentrant
 {
        uint8_t __xdata *stack;
+       uint8_t task_id;
+       uint8_t t;
        if (ao_num_tasks == AO_NUM_TASKS)
                ao_panic(AO_PANIC_NO_TASK);
+       for (task_id = 1; task_id != 0; task_id++) {
+               for (t = 0; t < ao_num_tasks; t++)
+                       if (ao_tasks[t]->task_id == task_id)
+                               break;
+               if (t == ao_num_tasks)
+                       break;
+       }
        ao_tasks[ao_num_tasks++] = task;
-       task->task_id = ao_num_tasks;
+       task->task_id = task_id;
        task->name = name;
        /*
         * Construct a stack frame so that it will 'return'
diff --git a/src/telemetrum-v0.2/.gitignore b/src/telemetrum-v0.2/.gitignore
deleted file mode 100644 (file)
index 7622809..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-telemetrum-v0.2
-ao_product.h
diff --git a/src/telemetrum-v0.2/Makefile b/src/telemetrum-v0.2/Makefile
deleted file mode 100644 (file)
index d8867b1..0000000
+++ /dev/null
@@ -1 +0,0 @@
-include ../Makefile.proto
diff --git a/src/telemetrum-v0.2/Makefile.defs b/src/telemetrum-v0.2/Makefile.defs
deleted file mode 100644 (file)
index eb11e7d..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-PROG = telemetrum-v0.2.ihx
-
-SRC = \
-       $(TM_BASE_SRC) \
-       $(FLASH_DRIVER_SRC) \
-       $(SKY_DRIVER_SRC) \
-       $(DBG_SRC)
-
-PRODUCT=TeleMetrum-v0.2
-PRODUCT_DEF=-DTELEMETRUM_V_0_2
diff --git a/src/telemetrum-v1.0/.gitignore b/src/telemetrum-v1.0/.gitignore
new file mode 100644 (file)
index 0000000..7622809
--- /dev/null
@@ -0,0 +1,2 @@
+telemetrum-v0.2
+ao_product.h
diff --git a/src/telemetrum-v1.0/Makefile b/src/telemetrum-v1.0/Makefile
new file mode 100644 (file)
index 0000000..d8867b1
--- /dev/null
@@ -0,0 +1 @@
+include ../Makefile.proto
diff --git a/src/telemetrum-v1.0/Makefile.defs b/src/telemetrum-v1.0/Makefile.defs
new file mode 100644 (file)
index 0000000..e3cc5eb
--- /dev/null
@@ -0,0 +1,10 @@
+PROG = telemetrum-v1.0.ihx
+
+SRC = \
+       $(TM_BASE_SRC) \
+       $(FLASH_DRIVER_SRC) \
+       $(SKY_DRIVER_SRC) \
+       $(DBG_SRC)
+
+PRODUCT=TeleMetrum-v1.0
+PRODUCT_DEF=-DTELEMETRUM_V_1_0
diff --git a/telemetrum.inf b/telemetrum.inf
new file mode 100755 (executable)
index 0000000..4549cfb
--- /dev/null
@@ -0,0 +1,42 @@
+; Copyright (C) 2010 Keith Packard (keithp@keithp.com)\r
+; released under GNU General Public License version 2\r
+\r
+[Version]\r
+Signature      = "$Windows NT$"\r
+Class           = Ports\r
+ClassGUID       = {4d36e978-e325-11ce-bfc1-08002be10318}\r
+Provider       = %Mfg%\r
+DriverVer      = 30/03/2010,4.0.0.8\r
+\r
+[Manufacturer]\r
+%Mfg%           = Models\r
+\r
+[Models]\r
+%TeleMetrum%    = TELEMETRUM, USB\VID_FFFE&PID_000A\r
+\r
+[DestinationDirs]\r
+DefaultDestDir  = 12\r
+\r
+[ControlFlags]\r
+ExcludeFromSelect=USB\VID_0BAF&PID_0303\r
+\r
+[Strings]\r
+Mfg = "altusmetrum.org"\r
+TeleMetrum       = "TeleMetrum/TeleDongle"\r
+\r
+[TELEMETRUM.NT]\r
+include=mdmcpq.inf\r
+CopyFiles=FakeModemCopyFileSection\r
+AddReg=Uninstall\r
+\r
+[TELEMETRUM.NT.HW]\r
+Include=mdmcpq.inf\r
+AddReg=LowerFilterAddReg\r
+\r
+[TELEMETRUM.NT.Services]\r
+Include=mdmcpq.inf\r
+AddService=usbser, 0x00000000, LowerFilter_Service_Inst\r
+\r
+[Uninstall]\r
+HKLM,Software\Microsoft\Windows\CurrentVersion\Uninstall\%TeleMetrum%,DisplayName,,"%TeleMetrum%"\r
+\r