/* openaltimeter -- an open-source altimeter for RC aircraft Copyright (C) 2010-2011 Jony Hudson, Jan Steidl, mycarda http://openaltimeter.org This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.openaltimeter.desktopapp; import java.awt.Dialog.ModalityType; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Iterator; import java.util.List; import java.util.Properties; import java.util.Vector; import java.util.prefs.Preferences; import javax.swing.JDialog; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import javax.swing.filechooser.FileNameExtensionFilter; import org.openaltimeter.Altimeter; import org.openaltimeter.Altimeter.DownloadTimeoutException; import org.openaltimeter.Altimeter.NotAnOpenaltimeterException; import org.openaltimeter.comms.SerialLink; import org.openaltimeter.data.FlightLog; import org.openaltimeter.data.HeightUnits; import org.openaltimeter.data.analysis.DLGFlight; import org.openaltimeter.data.analysis.DLGFlightAnalyser; import org.openaltimeter.desktopapp.MainWindow.DataState; import org.openaltimeter.settings.Settings; public class Controller { public enum ConnectionState {CONNECTED, DISCONNECTED, BUSY} private ConnectionState connectionState; public enum OS { WINDOWS, MAC, LINUX, OTHER }; public OS os; static Controller controller; Altimeter altimeter; MainWindow window; FlightLog flightLog; public String versionNumber = ""; public String firmwareVersionNumber = ""; private HeightUnits hu; public HeightUnits getHeightUnits() { return hu; } public void setHeightUnits(HeightUnits hu) { this.hu = hu; } private Preferences prefs; private static final String PREF_HEIGHT_UNITS = "PREF_HEIGHT_UNITS"; public static void main(String[] args) { try { Controller c = new Controller(); controller = c; c.run(); } catch (Exception e) { e.printStackTrace(); } } public static Controller getController() { return controller; } // this is the application's main method private void run() { // determine what os we're running, as some features are currently os specific os = OS.OTHER; if (System.getProperty("os.name").startsWith("Windows")) os = OS.WINDOWS; if (System.getProperty("os.name").startsWith("Mac")) os = OS.MAC; if (System.getProperty("os.name").startsWith("Linux")) os = OS.LINUX; // load up the version properties file Properties prop = new Properties(); try { prop.load(new FileInputStream("version.properties")); versionNumber = prop.getProperty("version"); System.out.println("OA version: " + versionNumber); firmwareVersionNumber = prop.getProperty("firmware_version"); } catch (Exception e) { // If anything at all goes wrong here we just ignore it and use the default values // The stack trace is suppressed because it's annoying when debugging - the load // always fails. // TODO: this is a limitation of the way things are currently built. The problem // is the lib directory. // e.printStackTrace(); } // load Controller preferences prefs = Preferences.userNodeForPackage(this.getClass()); String s = prefs.get(PREF_HEIGHT_UNITS, "FT"); setHeightUnits(HeightUnits.valueOf(s)); window = new MainWindow(); window.controller = this; window.initialise(); window.setTitle("openaltimeter " + versionNumber + " (" + firmwareVersionNumber + ")"); altimeter = new Altimeter(); window.show(); buildSerialMenu(); Controller.log("Graph hints: drag over area to zoom in, drag up and left to zoom out, click to annotate height, " + "shift-click to annotate vario. Annotations can be cleared from analysis menu.", "help"); } // called by the main window when the app is shutting down. public void appStopping() { savePreferences(); } private void savePreferences() { prefs.put(PREF_HEIGHT_UNITS, getHeightUnits().name()); } private void setConnectionState(ConnectionState state) { connectionState = state; window.setConnectedState(state); } // this is for initialisation code that runs before the GUI is built private void buildSerialMenu() { // build the serial port selection menu Controller.log("Finding serial ports ...", "message"); List<String> serialPorts = SerialLink.getSerialPorts(); Iterator<String> it = serialPorts.iterator(); while (it.hasNext()) window.addCOMMenuItem(it.next()); window.selectFirstCOMItem(); Controller.log("Done.", "message"); } // this is a bit cheezy - it's here to support a smooth upgrade path from old 115200 baud versions of the firmware // to the new 57600 baud versions. If connect fails a first time with a NotAnOpenaltimeterException then it will try // again at a reduced baud rate. This flag holds that state. An upgrade will immediately be offered for the old OA. boolean usingOldOABaudRate = false; // open the serial port and connect to the logger, print some summary information from the altimeter public void connect() { setConnectionState(ConnectionState.BUSY); new Thread( new Runnable() { public void run() { String comPort = window.getSelectedCOMPort(); try { Controller.log("Connecting to serial port " + comPort + " (please wait) ...", "message"); int baudRate = usingOldOABaudRate ? 115200 : 57600; Controller.log(altimeter.connect(comPort, baudRate), "altimeter"); Controller.log("Connected.", "message"); setConnectionState(ConnectionState.CONNECTED); // this is where we test for the firmware versions that are compatible with this // release of the downloader. If they aren't compatible then we try and start the // upgrade process. if (!altimeter.firmwareVersion.equals("V8")) { if (!adviseFirmwareUpgrade()) return; } } catch (NotAnOpenaltimeterException e) { setConnectionState(ConnectionState.DISCONNECTED); if (!usingOldOABaudRate) { Controller.log("Unable to connect, checking for older OA connection (please wait) ...", "error"); usingOldOABaudRate = true; connect(); return; } else { Controller.log("Incorrect reply from device. Check that you've selected the correct serial port, and that " + "the openaltimeter is connected and powered.", "error"); usingOldOABaudRate = false; return; } } catch (Exception e) { Controller.log("Exception opening serial port. Check your serial port settings.", "error"); setConnectionState(ConnectionState.DISCONNECTED); return; } try { Controller.log("Getting log information ...", "message"); Controller.log(altimeter.getFileInfo(), "altimeter"); Controller.log("Done.", "message"); } catch (IOException e) { Controller.log("Unable to get file information from altimeter.", "error"); } } }).start(); } // close the serial port public void disconnect() { if (connectionState != ConnectionState.DISCONNECTED) { altimeter.disconnect(); Controller.log("Disconnected.", "message"); usingOldOABaudRate = false; setConnectionState(ConnectionState.DISCONNECTED); } } // this returns a boolean indicating whether to stay connected to the logger or not. private boolean adviseFirmwareUpgrade() { String[] choices = {"Run firmware update utility ...", "Disconnect", "I'm feeling lucky, leave me connected!"}; int dialogResult = JOptionPane.showOptionDialog( null, "A firmware upgrade is required in order to use this version of the downloader. You can go directly to the firmware\n" + "upgrade dialog by clicking the button below.\n\n" + "Note that the firmware upgrade will delete all data on the logger. If you wish to save any data then you should \n" + "choose \"Disconnect\" below and use an older version of the downloader to save any data before proceeding.\n\n" + "If you're feeling brave, you can try and connect anyway, but things will probably not work well!\n", "Firmware upgrade required ...", JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null, choices, choices[0] ); if (dialogResult == 0) { flashFirmware(); return false; } if (dialogResult == 1) { disconnect(); return false; } if (dialogResult == 2) return true; disconnect(); return false; } public void downloadData() { setConnectionState(ConnectionState.BUSY); new Thread( new Runnable() { public void run() { Controller.log("Downloading altimeter data (please wait) ...", "message"); try { setFlightLog(altimeter.downloadData()); } catch (IOException e) { Controller.log("Communications error while downloading data. Check the serial port.", "error"); } catch (DownloadTimeoutException e) { Controller.log("Download started but did not complete in time. Check the serial port.", "error"); } finally { window.setDataState(DataState.HAVE_DATA); setConnectionState(ConnectionState.CONNECTED); Controller.log("Done.", "message"); } } }).start(); } public void erase() { if (window.showConfirmDialog("Are you sure you want to erase the altimeter's memory?", "Erase ...")) { setConnectionState(ConnectionState.BUSY); new Thread( new Runnable() { public void run() { Controller.log("Erasing altimeter (please wait) ...", "message"); try { log(altimeter.erase(), "altimeter"); } catch (IOException e) { Controller.log("Error sending erase command. Check the serial port.", "error"); } finally { setConnectionState(ConnectionState.CONNECTED); Controller.log("Done.", "message"); } } }).start(); } } public void setFlightLog(FlightLog log) { flightLog = log; window.altimeterChart.resetAnnotations(); window.altimeterChart.setAltitudeData(log.getAltitude(), flightLog.logInterval); window.altimeterChart.setBatteryData(log.getBattery(), flightLog.logInterval); window.altimeterChart.setTemperatureData(log.getTemperature(), flightLog.logInterval); window.altimeterChart.setServoData(log.getServo(), flightLog.logInterval); window.altimeterChart.addEOFAnnotations(log.getEOFIndices(), flightLog.logInterval); } public void saveRaw() { File f = window.showRawSaveDialog(); if (f == null) return; try { FileWriter fw = new FileWriter(f); fw.write(flightLog.rawDataToString()); fw.close(); } catch (IOException e) { window.log("Error writing file. Please check the filename and try again.", "error"); } } public void saveRawSelection(double lower, double upper) { System.out.println("Lower: " + lower + " Upper: " + upper); File f = window.showRawSaveDialog(); if (f == null) return; try { FileWriter fw = new FileWriter(f); fw.write(flightLog.rawDataToString((int) (lower / flightLog.logInterval), (int) (upper / flightLog.logInterval))); fw.close(); } catch (IOException e) { window.log("Error writing file. Please check the filename and try again.", "error"); } } public void loadRawData() { File f = window.showOpenDialog(new FileNameExtensionFilter("Text files", "txt")); if (f == null) return; StringBuilder fileStringBuilder = new StringBuilder(); try { BufferedReader fr = new BufferedReader(new FileReader(f)); String line; while ((line = fr.readLine()) != null) { fileStringBuilder.append(line); fileStringBuilder.append("\n"); } } catch (IOException e) { log("Unable to open file. Please check file is not open elsewhere and try again.", "error"); } FlightLog fl = new FlightLog(); try { fl.fromRawData(fileStringBuilder.toString()); } catch (IOException e) { log("Unable to parse file. Are you sure that this is a raw data file?", "error"); } setFlightLog(fl); window.setDataState(DataState.HAVE_DATA); } public void exit() { window.close(); System.exit(0); } public static void log(String message, String style) { Controller.getController().window.log(message, style); } public static void setProgress(int progress) { Controller.getController().window.updateProgress(progress); } public void altitudePlotSelectedChange(boolean selected) { window.altimeterChart.setAltitudePlotVisible(selected); } public void voltagePlotSelectedChange(boolean selected) { window.altimeterChart.setVoltagePlotVisible(selected); } public void temperaturePlotSelectedChange(boolean selected) { window.altimeterChart.setTemperaturePlotVisible(selected); } public void servoPlotSelectedChange(boolean selected) { window.altimeterChart.setServoPlotVisible(selected); } public void unitSelectedChange(HeightUnits unitsSelected) { setHeightUnits(unitsSelected); window.altimeterChart.setHeightUnits(unitsSelected); } // this is called by the settings menu event handler SettingsDialog settingsDialog; public void runSettingsInterface() { settingsDialog = new SettingsDialog(this); new Thread(new Runnable() { public void run() { loadSettingsFromAltimeter(); try { SwingUtilities.invokeAndWait(new Runnable() { public void run() { settingsDialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); settingsDialog.setModalityType(ModalityType.APPLICATION_MODAL); settingsDialog.setVisible(true); }}); } catch (Exception e) { e.printStackTrace(); } }}).start(); } // a helper method to load the settings from the OA and update the settings dialog private void loadSettingsFromAltimeter() { try { log("Loading settings from altimeter ...", "message"); altimeter.readSettings(); settingsDialog.setSettings(altimeter.settings); log("Done.", "message"); } catch (IOException e) { log("Unable to read settings from altimeter.", "error"); } } //this is called by the settings dialog void saveSettingsToAltimeter() { settingsDialog.enableButtons(false); new Thread(new Runnable() { public void run() { try { log("Writing settings to altimeter ...", "message"); Settings settingsToWrite = settingsDialog.getSettings(); altimeter.settings = settingsToWrite; altimeter.writeSettings(); log("Done.", "message"); log("Rebooting altimeter with new settings ...", "message"); altimeter.disconnect(); String comPort = window.getSelectedCOMPort(); Controller.log("Connecting to serial port " + comPort + " (please wait) ...", "message"); Controller.log(altimeter.connect(comPort, 57600), "altimeter"); Controller.log("Reboot finished.", "message"); Controller.log("Verifying settings ...", "message"); loadSettingsFromAltimeter(); if (!settingsToWrite.equals(altimeter.settings)) { log("Error verifying altimeter settings. Please try saving again.", "error"); settingsDialog.enableButtons(true); return; } } catch (IOException e) { log("Unable to write settings to altimeter.", "error"); e.printStackTrace(); disconnect(); } catch (NotAnOpenaltimeterException e) { log("Incorrect reply from device. Check that you've selected the correct serial port, and that " + "the openaltimeter is connected and powered.", "error"); e.printStackTrace(); disconnect(); } catch (Exception e) { log("Exception opening serial port. Check your serial port settings.", "error"); e.printStackTrace(); disconnect(); } finally { settingsDialog.enableButtons(true); settingsDialog.dispose(); } }}).start(); } // flashes the firmware to a stable version, wipes the settings - putting them back // to default - and erases the log memory. This is supposed to be a "sure-fire" way // of getting the OA back into a workable state. It doesn't depend on there already // being working OA software on the board, only the arduino compatible bootloader. FirmwareDialog firmwareDialog; public void flashFirmware() { firmwareDialog = new FirmwareDialog(this); firmwareDialog.setVisible(true); } public void doFirmwareFlash() { firmwareDialog.enableButtons(false); new Thread(new Runnable() { public void run() { try { if (connectionState == ConnectionState.CONNECTED) { altimeter.disconnect(); setConnectionState(ConnectionState.BUSY); } log("Flashing altimeter firmware ...", "message"); doFirmwareUpload(); log("Done.", "message"); log("Rebooting altimeter ...", "message"); String comPort = window.getSelectedCOMPort(); log("Connecting to serial port " + comPort + " (please wait) ...", "message"); log(altimeter.connect(comPort, 57600), "altimeter"); log("Reboot finished.", "message"); log("Erasing altimeter (please wait) ...", "message"); log(altimeter.erase(), "altimeter"); log("Wiping settings memory ...", "message"); altimeter.wipeSettings(); log("Firmware upgrade done - reconnecting ...", "message"); altimeter.disconnect(); log("Connecting to serial port " + comPort + " (please wait) ...", "message"); log(altimeter.connect(comPort, 57600), "altimeter"); log("Connected.", "message"); setConnectionState(ConnectionState.CONNECTED); } catch (FirmwareFlashException e) { e.printStackTrace(); log("Error flashing firmware.", "error"); setConnectionState(ConnectionState.DISCONNECTED); return; } catch (IOException e) { e.printStackTrace(); log("Error communicating with altimeter.", "error"); altimeter.disconnect(); setConnectionState(ConnectionState.DISCONNECTED); return; } catch (NotAnOpenaltimeterException e) { e.printStackTrace(); log("Incorrect reply from device. Check that you've selected the correct serial port, and that " + "the openaltimeter is connected and powered.", "error"); altimeter.disconnect(); setConnectionState(ConnectionState.DISCONNECTED); return; } catch (Exception e) { e.printStackTrace(); log("Exception communicating with altimeter. Check your serial port settings.", "error"); altimeter.disconnect(); setConnectionState(ConnectionState.DISCONNECTED); return; } finally { firmwareDialog.enableButtons(true); firmwareDialog.dispose(); } runSettingsInterface(); }}).start(); } Properties prop; private void doFirmwareUpload() throws FirmwareFlashException { int exitCode; try { ProcessBuilder pb = new ProcessBuilder(); // this would be where you'd switch based on OS to run the appropriate // firmware upload command. Vector<String> commandLine = new Vector<String>(); if (os == OS.WINDOWS) { // pb.directory(new File("windows_flash")); commandLine.add("windows_flash/avrdude.exe"); commandLine.add("-Cwindows_flash/avrdude.conf"); } if (os == OS.MAC) { commandLine.add("./mac_flash/avrdude"); commandLine.add("-Cmac_flash/avrdude.conf"); } if (os == OS.LINUX) { pb.directory(new File(System.getProperty("user.dir"))); commandLine.add("avrdude"); } commandLine.add("-Ueeprom:w:firmware/blank_eeprom.hex:i"); commandLine.add("-Uflash:w:firmware/firmware.hex:i"); commandLine.add("-q"); commandLine.add("-patmega328p"); commandLine.add("-cstk500v1"); commandLine.add("-P" + window.getSelectedCOMPort()); commandLine.add("-b57600"); commandLine.add("-D"); pb.command(commandLine); Process p = pb.start(); StreamLogPump errorPump = new StreamLogPump(p.getErrorStream(), "error"); StreamLogPump outputPump = new StreamLogPump(p.getInputStream(), "altimeter"); errorPump.start(); outputPump.start(); exitCode = p.waitFor(); } catch (Exception e) { e.printStackTrace(); throw new FirmwareFlashException(); } if (exitCode != 0) { log("avrdude failed with exit code: " + exitCode, "error"); throw new FirmwareFlashException(); } } class StreamLogPump extends Thread { InputStream stream; String type; StreamLogPump(InputStream stream, String type) { this.stream = stream; this.type = type; } public void run() { try { BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); String line; while ((line = reader.readLine()) != null) Controller.log(line, type); } catch (IOException e) { e.printStackTrace(); } } } public void showFirmwareReadme() { try { // seems odd that there isn't a cross platform way to do this! if (os == OS.WINDOWS) Runtime.getRuntime().exec("cmd.exe /c start firmware/readme.txt"); if (os == OS.MAC) Runtime.getRuntime().exec("open firmware/readme.txt"); // this seems cheezy, but I don't know a better way to do it. if (os == OS.LINUX) Controller.log("Not yet working on Linux yet! Please look in the firmware directory.", "error"); //Runtime.getRuntime().exec("vi " + System.getProperty("user.dir") + "/lib/firmware/readme.txt"); } catch (IOException e) { Controller.log("Unable to open firmware readme file.", "error"); e.printStackTrace(); } } public void uploadSelection(double lowerBound, double upperBound) { String dataToUpload = flightLog.rawDataToUploadString((int) (lowerBound / flightLog.logInterval), (int) (upperBound / flightLog.logInterval)); try { altimeter.upload(dataToUpload); } catch (IOException e){ Controller.log("Unable to upload data.", "error"); e.printStackTrace(); } //disconnect(); } @SuppressWarnings("serial") class FirmwareFlashException extends Exception {} public void analyseDLGFlights() { window.altimeterChart.clearDLGAnalysis(); // ask the user which parts of the analysis they wish to carry out DLGAnalysisDialog dialog = new DLGAnalysisDialog(this); dialog.setVisible(true); if (dialog.isSuccessful()) { DLGFlightAnalyser finder = new DLGFlightAnalyser(); List<DLGFlight> flights = finder.findDLGLaunches(flightLog.getAltitude(), flightLog.logInterval); if (dialog.shouldCorrectBaseline()) { // correct the launch heights double[] newAltData = finder.correctDLGBaseline(flightLog.getAltitude(), flights); flightLog.setAltitude(newAltData); // update the plot window.altimeterChart.setAltitudeData(flightLog.getAltitude(), flightLog.logInterval); } // plot the annotations if (dialog.shouldMarkLaunchHeights()) { for (DLGFlight d : flights) window.altimeterChart.addDLGHeightAnnotation(d.launchIndex * flightLog.logInterval, d.launchHeight); } if (dialog.shouldMarkMaxHeights()) { for (DLGFlight d : flights) { if (d.launchHeight != d.maxHeight) window.altimeterChart.addDLGMaxHeightAnnotation(d.maxIndex * flightLog.logInterval, d.maxHeight); } } for (DLGFlight d: flights) window.altimeterChart.addDLGStartAnnotation(d.startIndex * flightLog.logInterval, d.startHeight); if (dialog.shouldShowStatistics()) { // show the analysis results DLGAnalysisResultsWindow resWin = new DLGAnalysisResultsWindow(flights, getHeightUnits()); resWin.setVisible(true); } } } public void clearDLGAnalysis() { window.altimeterChart.clearDLGAnalysis(); // undo the baseline correction flightLog.calculateAltitudes(); window.altimeterChart.setAltitudeData(flightLog.getAltitude(), flightLog.logInterval); } }