/* * 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; class AltosUIMonitor implements AltosSerialMonitor { public void data(String data) { System.out.println(data); } } 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(); serialLine.monitor(new AltosUIMonitor()); 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()); } } private void ConnectToDevice() { JFileChooser device_chooser = new JFileChooser(); int returnVal = device_chooser.showOpenDialog(AltosUI.this); if (returnVal == JFileChooser.APPROVE_OPTION) { File file = device_chooser.getSelectedFile(); try { serialLine.connect(file.getCanonicalPath()); } catch (FileNotFoundException ee) { JOptionPane.showMessageDialog(AltosUI.this, file.getName(), "Cannot open serial port", JOptionPane.ERROR_MESSAGE); } catch (NoSuchPortException ee) { JOptionPane.showMessageDialog(AltosUI.this, file.getName(), "No such serial port", JOptionPane.ERROR_MESSAGE); } catch (PortInUseException ee) { JOptionPane.showMessageDialog(AltosUI.this, file.getName(), "Port in use", JOptionPane.ERROR_MESSAGE); } catch (IOException ee) { JOptionPane.showMessageDialog(AltosUI.this, file.getName(), "Unkonwn I/O error", JOptionPane.ERROR_MESSAGE); } } } 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 Thread { FileInputStream replay; String filename; ReplayThread(FileInputStream in, String name) { replay = in; filename = name; } /* Run the replay in a separate thread * so that the UI can update */ public void run() { String line; AltosState state = null; try { while ((line = readline(replay)) != null) { try { AltosTelemetry t = new AltosTelemetry(line); state = new AltosState(t, state); show(state); /* Make it run in realtime after the rocket leaves the pad */ try { if (state.state > AltosTelemetry.ao_flight_pad) Thread.sleep((int) (state.time_change * 1000)); } catch (InterruptedException e) { break; } } catch (ParseException pp) { JOptionPane.showMessageDialog(AltosUI.this, line, "error parsing", JOptionPane.ERROR_MESSAGE); break; } } } catch (IOException ee) { JOptionPane.showMessageDialog(AltosUI.this, filename, "error reading", JOptionPane.ERROR_MESSAGE); } finally { try { replay.close(); } catch (IOException e) {} } } } ReplayThread replay_thread; /* * 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); if (thread != null && replay_thread != null && replay_thread.isAlive()) replay_thread.interrupt(); replay_thread = thread; replay_thread.start(); } 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) { serialLine.close(); } }); 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) { AltosDevice[] devices = AltosDeviceLinux.list(); for (int i = 0; i < devices.length; i++) System.out.printf("Model: %s Serial: %d Tty: %s\n", devices[i].product, devices[i].serial, devices[i].tty); AltosUI altosui = new AltosUI(); altosui.setVisible(true); } }