From: Keith Packard Date: Thu, 8 Apr 2010 18:46:56 +0000 (-0700) Subject: Merge remote branch 'origin/master' into altosui X-Git-Tag: debian/0.6+163+g01e524f~5 X-Git-Url: https://git.gag.com/?p=fw%2Faltos;a=commitdiff_plain;h=447c121fc1ceb878e45718ad1364a5349965a59a;hp=53ca3f98aeb70cb780031fee788de950e4388cf6 Merge remote branch 'origin/master' into altosui --- diff --git a/ao-tools/altosui/.gitignore b/ao-tools/altosui/.gitignore new file mode 100644 index 00000000..59913193 --- /dev/null +++ b/ao-tools/altosui/.gitignore @@ -0,0 +1,2 @@ +*.class +altosui diff --git a/ao-tools/altosui/AltosConvert.java b/ao-tools/altosui/AltosConvert.java new file mode 100644 index 00000000..3be0716c --- /dev/null +++ b/ao-tools/altosui/AltosConvert.java @@ -0,0 +1,245 @@ +/* + * Copyright © 2010 Keith Packard + * + * 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 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 index 00000000..66800c5c --- /dev/null +++ b/ao-tools/altosui/AltosDevice.java @@ -0,0 +1,30 @@ +/* + * Copyright © 2010 Keith Packard + * + * 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 index 00000000..cb1eef8b --- /dev/null +++ b/ao-tools/altosui/AltosDeviceDialog.java @@ -0,0 +1,45 @@ +/* + * Copyright © 2010 Keith Packard + * + * 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 index 00000000..ffc70aff --- /dev/null +++ b/ao-tools/altosui/AltosDeviceLinux.java @@ -0,0 +1,172 @@ +/* + * Copyright © 2010 Keith Packard + * + * 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 devices = new LinkedList(); + + 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 subset = new LinkedList(); + 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 index 00000000..c3b368e2 --- /dev/null +++ b/ao-tools/altosui/AltosGPS.java @@ -0,0 +1,124 @@ +/* + * Copyright © 2010 Keith Packard + * + * 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 index 00000000..878da03e --- /dev/null +++ b/ao-tools/altosui/AltosGreatCircle.java @@ -0,0 +1,66 @@ +/* + * Copyright © 2010 Keith Packard + * + * 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 index 00000000..a60dc694 --- /dev/null +++ b/ao-tools/altosui/AltosParse.java @@ -0,0 +1,75 @@ +/* + * Copyright © 2010 Keith Packard + * + * 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 index 00000000..e4cedde2 --- /dev/null +++ b/ao-tools/altosui/AltosSerial.java @@ -0,0 +1,194 @@ +/* + * Copyright © 2010 Keith Packard + * + * 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 monitor_queue; + LinkedBlockingQueue 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 (); + reply_queue = new LinkedBlockingQueue (); + } + +} + +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 index 00000000..ad0e9295 --- /dev/null +++ b/ao-tools/altosui/AltosSerialMonitor.java @@ -0,0 +1,22 @@ +/* + * Copyright © 2010 Keith Packard + * + * 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 index 00000000..192011d0 --- /dev/null +++ b/ao-tools/altosui/AltosState.java @@ -0,0 +1,175 @@ +/* + * Copyright © 2010 Keith Packard + * + * 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 index 00000000..e13b42e2 --- /dev/null +++ b/ao-tools/altosui/AltosTelemetry.java @@ -0,0 +1,186 @@ +/* + * Copyright © 2010 Keith Packard + * + * 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 states = new HashMap(); + { + 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 index 00000000..7f008f3a --- /dev/null +++ b/ao-tools/altosui/AltosUI.java @@ -0,0 +1,739 @@ +/* + * Copyright © 2010 Keith Packard + * + * 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 rows = new ArrayList(); + + 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 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 index 00000000..fbe0a5eb --- /dev/null +++ b/ao-tools/altosui/Makefile @@ -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 index 00000000..0305fcfb --- /dev/null +++ b/ao-tools/altosui/Manifest.txt @@ -0,0 +1 @@ +Main-Class: altosui.AltosUI diff --git a/src/Makefile b/src/Makefile index e2699ee6..24f562e1 100644 --- a/src/Makefile +++ b/src/Makefile @@ -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 diff --git a/src/ao.h b/src/ao.h index aefefa46..97cb75ae 100644 --- 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 */ }; diff --git a/src/ao_monitor.c b/src/ao_monitor.c index 628b6e67..f2f3fc2e 100644 --- a/src/ao_monitor.c +++ b/src/ao_monitor.c @@ -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 diff --git a/src/ao_pins.h b/src/ao_pins.h index 771d9c7f..e9a265b0 100644 --- a/src/ao_pins.h +++ b/src/ao_pins.h @@ -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 diff --git a/src/ao_radio.c b/src/ao_radio.c index bb19c6a9..0849349e 100644 --- a/src/ao_radio.c +++ b/src/ao_radio.c @@ -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); } diff --git a/src/ao_task.c b/src/ao_task.c index 4a78766e..72c9d7d6 100644 --- a/src/ao_task.c +++ b/src/ao_task.c @@ -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 index 76228093..00000000 --- a/src/telemetrum-v0.2/.gitignore +++ /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 index d8867b19..00000000 --- a/src/telemetrum-v0.2/Makefile +++ /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 index eb11e7dc..00000000 --- a/src/telemetrum-v0.2/Makefile.defs +++ /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 index 00000000..76228093 --- /dev/null +++ b/src/telemetrum-v1.0/.gitignore @@ -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 index 00000000..d8867b19 --- /dev/null +++ b/src/telemetrum-v1.0/Makefile @@ -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 index 00000000..e3cc5eb2 --- /dev/null +++ b/src/telemetrum-v1.0/Makefile.defs @@ -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 index 00000000..4549cfba --- /dev/null +++ b/telemetrum.inf @@ -0,0 +1,42 @@ +; Copyright (C) 2010 Keith Packard (keithp@keithp.com) +; released under GNU General Public License version 2 + +[Version] +Signature = "$Windows NT$" +Class = Ports +ClassGUID = {4d36e978-e325-11ce-bfc1-08002be10318} +Provider = %Mfg% +DriverVer = 30/03/2010,4.0.0.8 + +[Manufacturer] +%Mfg% = Models + +[Models] +%TeleMetrum% = TELEMETRUM, USB\VID_FFFE&PID_000A + +[DestinationDirs] +DefaultDestDir = 12 + +[ControlFlags] +ExcludeFromSelect=USB\VID_0BAF&PID_0303 + +[Strings] +Mfg = "altusmetrum.org" +TeleMetrum = "TeleMetrum/TeleDongle" + +[TELEMETRUM.NT] +include=mdmcpq.inf +CopyFiles=FakeModemCopyFileSection +AddReg=Uninstall + +[TELEMETRUM.NT.HW] +Include=mdmcpq.inf +AddReg=LowerFilterAddReg + +[TELEMETRUM.NT.Services] +Include=mdmcpq.inf +AddService=usbser, 0x00000000, LowerFilter_Service_Inst + +[Uninstall] +HKLM,Software\Microsoft\Windows\CurrentVersion\Uninstall\%TeleMetrum%,DisplayName,,"%TeleMetrum%" +