package org.openaltimeter.data.analysis; import java.util.ArrayList; import java.util.List; import org.openaltimeter.data.FlightLog; // This class scans flight logs and finds the DLG launches. All work is done in meters. // The algorithm looks kind of horrible - that's because it's a direct port of the C++ algorithm // that runs on the OA. The OA algorithm is written to run in real time, with a flash memory // store holding the history. public class DLGFlightAnalyser { public List<DLGFlight> findDLGLaunches(double[] altData, double logInterval) { ArrayList<DLGFlight> flights = new ArrayList<DLGFlight>(); for (int i = 0; i < altData.length; i++) updateHeightMonitor(altData, i, logInterval, flights); return flights; } // -- launch detector // these parameters tune the launch detector // this is the rate of climb that is considered a launch. It's measured in // m/s. static final double LAUNCH_CLIMB_THRESHOLD = 3.0; // this is how long the climb intervals have to exceed the launch climb rate // to trigger the launch detector, measured in ms static final int LAUNCH_CLIMB_TIME = 1500; // this is how many samples to seek back after the launch was detected to // find the minimum height static final int LAUNCH_SEEKBACK_SAMPLES = 20; // this is how long the launch window is, in ms. The launch height will be // measured in this window. static final int LAUNCH_WINDOW_TIME = 5000; // the height at which the launch detector re-arms. Measured in meters. static final double LAUNCH_DETECTOR_REARM_HEIGHT = 8.0; double currentHeight = 0; double maxHeight = 0; // The overall maximum height of the flight. private void updateHeightMonitor(double[] altData, int index, double logInterval, ArrayList<DLGFlight> flights) { currentHeight = altData[index]; if (currentHeight > maxHeight) { maxHeight = currentHeight; maxHeightIndex = index; } updateDLGHeightMonitor(altData, index, logInterval, flights); } // DLG specific height functions and variables. This is broken out from the // main height monitor to make the firmware // easier to customise. int launchCount = 0; // A launch is defined as N successive periods with // more than a certain climb rate. // This keeps track of how long we've been climbing. int launchWindowCount = 0; // Used to track launch height separate from max // height. double lastHeight = 0; // Used for measuring climb rates. boolean launched = false; // This indicates whether we're in flight or not. // Launch detector is disabled in flight. double maxLaunchHeight = 0; double launchWindowEndHeight = 0; // It's useful to know what height was // attained a few seconds after launch // to optimise push over. double launchHeight = 0; int launchIndex = 0; int maxHeightIndex = 0; int maxLaunchHeightIndex = 0; private void updateDLGHeightMonitor(double[] altData, int index, double logInterval, ArrayList<DLGFlight> flights) { // We monitor the height data looking for a "launch". This is a number // of samples that climb consistently at greater than a given rate. if (!launched) { if (currentHeight - lastHeight > (LAUNCH_CLIMB_THRESHOLD * logInterval)) launchCount++; else launchCount = 0; lastHeight = currentHeight; if (launchCount >= (LAUNCH_CLIMB_TIME / (logInterval * 1000))) { // we've just detected a launch - disable the launch detector launched = true; launchCount = 0; launchIndex = 0; maxHeightIndex = 0; maxLaunchHeightIndex = 0; // When we detect a launch we do a few things: we reset the // launch height to the lowest height // in the few seconds before the launch; // we start a countdown which defines the "launch window"; we // reset the maximum heights. // -- reset launch height double newLaunchHeight = (double) Double.MAX_VALUE; for (int i = LAUNCH_SEEKBACK_SAMPLES; i > 0; i--) { int seekbackIndex = index - i; // have we reached the start of the array? if (seekbackIndex >= 0) { // we stop if we encounter a file boundary. // unlikely to happen, but worth checking for. if (altData[seekbackIndex] == FlightLog.PRESSURE_EMPTY_DATA) break; if (altData[seekbackIndex] < newLaunchHeight) { newLaunchHeight = altData[seekbackIndex]; // -- save the launch index launchIndex = seekbackIndex; } } } // -- save the launch height launchHeight = newLaunchHeight; // -- time the launch window launchWindowCount = (int) (LAUNCH_WINDOW_TIME / (logInterval * 1000)) + 1; // -- reset max heights maxHeight = currentHeight; maxHeightIndex = index; maxLaunchHeight = currentHeight; maxLaunchHeightIndex = index; launchWindowEndHeight = 0; } } else { // The launch detector is disabled after a launch, so that it can't // retrigger in flight. It is reset by either the logger's altitude // coming below a certain threshold, or a height output function // being commanded by the user (on the basis that this should always // happen on the ground - the latter is implemented to stop the // logger getting stuck should the ground-level pressure change // dramatically during // a flight.) // Here we check for the former. if (currentHeight < LAUNCH_DETECTOR_REARM_HEIGHT) { launched = false; // store the previous launch details if (maxLaunchHeight != 0) { DLGFlight fl = new DLGFlight(); fl.launchHeight = maxLaunchHeight; fl.launchIndex = maxLaunchHeightIndex; fl.launchWindowEndHeight = launchWindowEndHeight; fl.maxHeight = maxHeight; fl.maxIndex = maxHeightIndex; fl.startHeight = launchHeight; fl.startIndex = launchIndex; flights.add(fl); } } } // if we're in the launch window we need to track the maximum altitude. if (launchWindowCount > 0) { if (currentHeight > maxLaunchHeight) { maxLaunchHeight = currentHeight; maxLaunchHeightIndex = index; } // if this is the end of the launch window then we record the height if (launchWindowCount == 1) launchWindowEndHeight = currentHeight; launchWindowCount--; } } // Adjusts each flight so that the launch is at zero height. This function // modifies both the flightlog and the flights. public double[] correctDLGBaseline(double[] altData, List<DLGFlight> flights) { double[] newAltData = new double[altData.length]; if (flights.size() > 1) { // assemble the list of offsets. The correction is applied in the // simplest way possible - each flight is offset by a constant // amount such that it starts at zero altitude. This could be modified to // use a smooth interpolation. double[] offsets = new double[altData.length]; // the first segment from the start of the file to the start of the // second flight. for (int i = 0; i < flights.get(1).startIndex; i++) offsets[i] = flights.get(0).startHeight; // the middle segment is from the start of the second flight, to the // start of the last flight. if (flights.size() > 3) { for (int j = 1; j < flights.size() - 2; j++) { for (int i = flights.get(j).startIndex; i < flights.get(j + 1).startIndex; i++) offsets[i] = flights.get(j).startHeight; } } // the last segement is from the start of the last flight to the // end. for (int i = flights.get(flights.size() - 1).startIndex; i < offsets.length; i++) offsets[i] = flights.get(flights.size() - 1).startHeight; // subtract the offsets for (int i = 0; i < offsets.length; i++) newAltData[i] = altData[i] - offsets[i]; // correct the flight information for (DLGFlight flight : flights) { flight.launchHeight -= flight.startHeight; flight.launchWindowEndHeight -= flight.startHeight; flight.maxHeight -= flight.startHeight; flight.startHeight = 0; } } return newAltData; } }