└── Telemetry Viewer ├── .classpath ├── .gitignore ├── .project ├── .settings ├── org.eclipse.core.resources.prefs └── org.eclipse.jdt.core.prefs ├── lib ├── bridj-0.7-20140918.jar ├── commons-math3-3.6.1-javadoc.jar ├── commons-math3-3.6.1.jar ├── gluegen-javadoc.zip ├── gluegen-rt-natives-linux-aarch64.jar ├── gluegen-rt-natives-linux-amd64.jar ├── gluegen-rt-natives-linux-armv6hf.jar ├── gluegen-rt-natives-linux-i586.jar ├── gluegen-rt-natives-macosx-universal.jar ├── gluegen-rt-natives-windows-amd64.jar ├── gluegen-rt-natives-windows-i586.jar ├── gluegen-rt.jar ├── jSerialComm-2.6.2.jar ├── jetty-client-9.4.28.v20200408.jar ├── jetty-http-9.4.28.v20200408.jar ├── jetty-io-9.4.28.v20200408.jar ├── jetty-util-9.4.28.v20200408.jar ├── jogamp-fat-java-src.zip ├── jogl-all-natives-linux-aarch64.jar ├── jogl-all-natives-linux-amd64.jar ├── jogl-all-natives-linux-armv6hf.jar ├── jogl-all-natives-linux-i586.jar ├── jogl-all-natives-macosx-universal.jar ├── jogl-all-natives-windows-amd64.jar ├── jogl-all-natives-windows-i586.jar ├── jogl-all.jar ├── jogl-javadoc.zip ├── miglayout-4.0-javadoc.jar ├── miglayout-4.0-sources.jar ├── miglayout-4.0-swing.jar ├── slf4j-api-1.7.2.jar ├── slf4j-nop-1.7.2.jar ├── turbojpeg.jar ├── webcam-capture-0.3.12-javadoc.jar ├── webcam-capture-0.3.12-sources.jar ├── webcam-capture-0.3.12.jar ├── websocket-api-9.4.28.v20200408.jar ├── websocket-client-9.4.28.v20200408.jar └── websocket-common-9.4.28.v20200408.jar ├── resources └── monkey.stl ├── src ├── AutoScale.java ├── BitfieldEvents.java ├── ChartUtils.java ├── ChartsController.java ├── ColorPickerView.java ├── CommunicationView.java ├── ConfigureView.java ├── Connection.java ├── ConnectionCamera.java ├── ConnectionTelemetry.java ├── ConnectionsController.java ├── DataStructureBinaryView.java ├── DataStructureCsvView.java ├── Dataset.java ├── DatasetsController.java ├── DatasetsInterface.java ├── EventHandler.java ├── LogitechSmoothScrolling.java ├── Main.java ├── NotificationsController.java ├── OpenGL.java ├── OpenGLCameraChart.java ├── OpenGLChartsView.java ├── OpenGLDialChart.java ├── OpenGLFrequencyDomainCache.java ├── OpenGLFrequencyDomainChart.java ├── OpenGLHistogramChart.java ├── OpenGLQuaternionChart.java ├── OpenGLStatisticsChart.java ├── OpenGLTimeDomainChart.java ├── OpenGLTimelineChart.java ├── Plot.java ├── PlotMilliseconds.java ├── PlotSampleCount.java ├── PositionedChart.java ├── SettingsController.java ├── SettingsView.java ├── SharedByteStream.java ├── StorageFloats.java ├── StorageTimestamps.java ├── Theme.java ├── TransmitController.java ├── TransmitView.java ├── Widget.java ├── WidgetCamera.java ├── WidgetCheckbox.java ├── WidgetCombobox.java ├── WidgetDatasets.java ├── WidgetFrequencyDomainType.java ├── WidgetHistogramXaxisType.java ├── WidgetHistogramYaxisType.java ├── WidgetTextfieldInteger.java ├── WidgetTextfieldsOptionalMinMax.java └── WidgetTrigger.java └── test └── StorageTimestampsTest.java /Telemetry Viewer/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /Telemetry Viewer/.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | -------------------------------------------------------------------------------- /Telemetry Viewer/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | Telemetry Viewer 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.jdt.core.javanature 16 | 17 | 18 | -------------------------------------------------------------------------------- /Telemetry Viewer/.settings/org.eclipse.core.resources.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | encoding//src/OpenGLFrequencyDomainChart.java=UTF-8 3 | -------------------------------------------------------------------------------- /Telemetry Viewer/.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled 3 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 4 | org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve 5 | org.eclipse.jdt.core.compiler.compliance=1.8 6 | org.eclipse.jdt.core.compiler.debug.lineNumber=generate 7 | org.eclipse.jdt.core.compiler.debug.localVariable=generate 8 | org.eclipse.jdt.core.compiler.debug.sourceFile=generate 9 | org.eclipse.jdt.core.compiler.problem.assertIdentifier=error 10 | org.eclipse.jdt.core.compiler.problem.enumIdentifier=error 11 | org.eclipse.jdt.core.compiler.source=1.8 12 | -------------------------------------------------------------------------------- /Telemetry Viewer/lib/bridj-0.7-20140918.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/bridj-0.7-20140918.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/commons-math3-3.6.1-javadoc.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/commons-math3-3.6.1-javadoc.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/commons-math3-3.6.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/commons-math3-3.6.1.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/gluegen-javadoc.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/gluegen-javadoc.zip -------------------------------------------------------------------------------- /Telemetry Viewer/lib/gluegen-rt-natives-linux-aarch64.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/gluegen-rt-natives-linux-aarch64.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/gluegen-rt-natives-linux-amd64.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/gluegen-rt-natives-linux-amd64.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/gluegen-rt-natives-linux-armv6hf.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/gluegen-rt-natives-linux-armv6hf.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/gluegen-rt-natives-linux-i586.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/gluegen-rt-natives-linux-i586.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/gluegen-rt-natives-macosx-universal.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/gluegen-rt-natives-macosx-universal.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/gluegen-rt-natives-windows-amd64.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/gluegen-rt-natives-windows-amd64.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/gluegen-rt-natives-windows-i586.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/gluegen-rt-natives-windows-i586.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/gluegen-rt.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/gluegen-rt.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/jSerialComm-2.6.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/jSerialComm-2.6.2.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/jetty-client-9.4.28.v20200408.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/jetty-client-9.4.28.v20200408.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/jetty-http-9.4.28.v20200408.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/jetty-http-9.4.28.v20200408.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/jetty-io-9.4.28.v20200408.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/jetty-io-9.4.28.v20200408.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/jetty-util-9.4.28.v20200408.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/jetty-util-9.4.28.v20200408.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/jogamp-fat-java-src.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/jogamp-fat-java-src.zip -------------------------------------------------------------------------------- /Telemetry Viewer/lib/jogl-all-natives-linux-aarch64.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/jogl-all-natives-linux-aarch64.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/jogl-all-natives-linux-amd64.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/jogl-all-natives-linux-amd64.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/jogl-all-natives-linux-armv6hf.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/jogl-all-natives-linux-armv6hf.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/jogl-all-natives-linux-i586.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/jogl-all-natives-linux-i586.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/jogl-all-natives-macosx-universal.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/jogl-all-natives-macosx-universal.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/jogl-all-natives-windows-amd64.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/jogl-all-natives-windows-amd64.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/jogl-all-natives-windows-i586.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/jogl-all-natives-windows-i586.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/jogl-all.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/jogl-all.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/jogl-javadoc.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/jogl-javadoc.zip -------------------------------------------------------------------------------- /Telemetry Viewer/lib/miglayout-4.0-javadoc.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/miglayout-4.0-javadoc.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/miglayout-4.0-sources.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/miglayout-4.0-sources.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/miglayout-4.0-swing.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/miglayout-4.0-swing.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/slf4j-api-1.7.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/slf4j-api-1.7.2.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/slf4j-nop-1.7.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/slf4j-nop-1.7.2.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/turbojpeg.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/turbojpeg.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/webcam-capture-0.3.12-javadoc.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/webcam-capture-0.3.12-javadoc.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/webcam-capture-0.3.12-sources.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/webcam-capture-0.3.12-sources.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/webcam-capture-0.3.12.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/webcam-capture-0.3.12.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/websocket-api-9.4.28.v20200408.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/websocket-api-9.4.28.v20200408.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/websocket-client-9.4.28.v20200408.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/websocket-client-9.4.28.v20200408.jar -------------------------------------------------------------------------------- /Telemetry Viewer/lib/websocket-common-9.4.28.v20200408.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/TelemetryViewer/c47d907053ecf5c556390fb8bc151ed5ffdb1c3f/Telemetry Viewer/lib/websocket-common-9.4.28.v20200408.jar -------------------------------------------------------------------------------- /Telemetry Viewer/src/AutoScale.java: -------------------------------------------------------------------------------- 1 | import java.util.LinkedList; 2 | import java.util.Queue; 3 | 4 | /** 5 | * Auto-scales an axis. 6 | */ 7 | public class AutoScale { 8 | 9 | public static final int MODE_STICKY = 0; 10 | public static final int MODE_EXPONENTIAL = 1; 11 | 12 | Queue minSequence; 13 | Queue maxSequence; 14 | 15 | float min; 16 | float max; 17 | 18 | int mode; 19 | int frameCount; 20 | float hysteresis; 21 | 22 | /** 23 | * Creates an object that takes the true min/max values for an axis, and outputs new min/max values that re-scale the axis based on some settings. 24 | * 25 | * @param mode MODE_STICKY or MODE_EXPONENTIAL. Sticky mode will only rescale the axis when required, while Exponential mode will constantly rescale to keep things centered. 26 | * @param frameCount How many frames for animating the transition to a new scale. 1 = immediate jump (no animation.) 27 | * @param hysteresis When hitting an existing min/max, re-scale to leave this much extra room (relative to the new range), 28 | * When 1.5*this far from an existing min/max (relative to the new range), re-scale to leave only this much room. 29 | */ 30 | public AutoScale(int mode, int frameCount, float hysteresis) { 31 | 32 | minSequence = new LinkedList(); 33 | maxSequence = new LinkedList(); 34 | 35 | minSequence.add(Float.MAX_VALUE); 36 | maxSequence.add(Float.MIN_VALUE); 37 | 38 | min = Float.MAX_VALUE; 39 | max = Float.MIN_VALUE; 40 | 41 | this.mode = mode; 42 | this.frameCount = frameCount; 43 | this.hysteresis = hysteresis; 44 | 45 | } 46 | 47 | /** 48 | * Updates state with the current min and max values. This method should be called every frame, before calling getMin() or getMax(). 49 | * 50 | * @param newMin Current minimum. 51 | * @param newMax Current maximum. 52 | */ 53 | public void update(float newMin, float newMax) { 54 | 55 | if(mode == MODE_STICKY) { 56 | 57 | float oldMin = minSequence.peek(); 58 | float oldMax = maxSequence.peek(); 59 | float newRange = Math.abs(newMax - newMin); 60 | float idealMin = newMin - (newRange * hysteresis); 61 | float idealMax = newMax + (newRange * hysteresis); 62 | 63 | boolean maxExceededThreshold = newMax > oldMax; 64 | boolean maxFarFromThreshold = newMax < oldMax - 1.5f*hysteresis*newRange; 65 | boolean minExceededThreshold = newMin < oldMin; 66 | boolean minFarFromThreshold = newMin > oldMin + 1.5f*hysteresis*newRange; 67 | 68 | if(maxExceededThreshold) { 69 | maxSequence.clear(); 70 | for(int i = 1; i <= frameCount; i++) { 71 | float delta = (idealMax - newMax) * (float) (Math.sin(Math.PI/2/frameCount*i)); 72 | maxSequence.add(newMax + delta); 73 | } 74 | } else if(maxFarFromThreshold) { 75 | maxSequence.clear(); 76 | for(int i = 1; i <= frameCount; i++) { 77 | float delta = (idealMax - oldMax) * (float) (Math.sin(Math.PI/2/frameCount*i)); 78 | maxSequence.add(oldMax + delta); 79 | } 80 | } 81 | 82 | if(minExceededThreshold) { 83 | minSequence.clear(); 84 | for(int i = 1; i <= frameCount; i++) { 85 | float delta = (idealMin - newMin) * (float) (Math.sin(Math.PI/2/frameCount*i)); 86 | minSequence.add(newMin + delta); 87 | } 88 | } else if(minFarFromThreshold) { 89 | minSequence.clear(); 90 | for(int i = 1; i <= frameCount; i++) { 91 | float delta = (idealMin - oldMin) * (float) (Math.sin(Math.PI/2/frameCount*i)); 92 | minSequence.add(oldMin + delta); 93 | } 94 | } 95 | 96 | } else if(mode == MODE_EXPONENTIAL) { 97 | 98 | float newRange = Math.abs(newMax - newMin); 99 | 100 | if(newMin < min) { 101 | min = newMin; 102 | } else { 103 | float goal = newMin - (newRange * hysteresis); 104 | float error = goal - min; 105 | min = min + error * 2 / (float) frameCount; 106 | } 107 | 108 | if(newMax > max) { 109 | max = newMax; 110 | } else { 111 | float goal = newMax + (newRange * hysteresis); 112 | float error = goal - max; 113 | max = max + error * 2 / (float) frameCount; 114 | } 115 | 116 | } 117 | 118 | } 119 | 120 | /** 121 | * @return The auto-scaled minimum value. 122 | */ 123 | public float getMin() { 124 | 125 | if(mode == MODE_STICKY) { 126 | 127 | if(minSequence.size() > 1) 128 | return minSequence.remove(); 129 | else 130 | return minSequence.peek(); 131 | 132 | } else { 133 | 134 | return min; 135 | 136 | } 137 | 138 | } 139 | 140 | /** 141 | * @return The auto-scaled maximum value. 142 | */ 143 | public float getMax() { 144 | 145 | if(mode == MODE_STICKY) { 146 | 147 | if(maxSequence.size() > 1) 148 | return maxSequence.remove(); 149 | else 150 | return maxSequence.peek(); 151 | 152 | } else { 153 | 154 | return max; 155 | 156 | } 157 | 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/BitfieldEvents.java: -------------------------------------------------------------------------------- 1 | import java.util.ArrayList; 2 | import java.util.List; 3 | import java.util.Map; 4 | import java.util.TreeMap; 5 | import java.util.function.BiFunction; 6 | 7 | public class BitfieldEvents { 8 | 9 | boolean showSampleNumbers; 10 | boolean showTimestamps; 11 | Map edgeMarkers; // key is a sample number, value is an object describing all edge events at that sample number 12 | Map levelMarkers; // key is a bitfield, value is a list of level markers for the chosen states 13 | 14 | /** 15 | * Creates a list of all bitfield events that should be displayed on a chart. 16 | * 17 | * @param showSampleNumbers True if the sample number should be displayed at the top of each marker. 18 | * @param showTimestamps True if the date/time should be displayed at the top of each marker. 19 | * @param datasets Bitfield edges and levels to check for. 20 | * @param minSampleNumber Range of samples numbers to check (inclusive.) 21 | * @param maxSampleNumber Range of samples numbers to check (inclusive.) 22 | */ 23 | public BitfieldEvents(boolean showSampleNumbers, boolean showTimestamps, DatasetsInterface datasets, int minSampleNumber, int maxSampleNumber) { 24 | 25 | this.showSampleNumbers = showSampleNumbers; 26 | this.showTimestamps = showTimestamps; 27 | edgeMarkers = new TreeMap(); 28 | levelMarkers = new TreeMap(); 29 | 30 | if(maxSampleNumber <= minSampleNumber) 31 | return; 32 | 33 | // check for edge events 34 | datasets.forEachEdge(minSampleNumber, maxSampleNumber, (state, eventSampleNumber) -> { 35 | if(edgeMarkers.containsKey(eventSampleNumber)) { 36 | // a marker already exists for this sample number, so append to it 37 | EdgeMarker event = edgeMarkers.get(eventSampleNumber); 38 | event.text.add(state.name); 39 | event.glColors.add(state.glColor); 40 | } else { 41 | // a marker does not exist, so create a new one 42 | edgeMarkers.put(eventSampleNumber, new EdgeMarker(state, eventSampleNumber)); 43 | } 44 | }); 45 | 46 | // check for levels 47 | datasets.forEachLevel(minSampleNumber, maxSampleNumber, (state, range) -> { 48 | LevelMarker marker; 49 | if(levelMarkers.containsKey(state.bitfield)) { 50 | marker = levelMarkers.get(state.bitfield); 51 | } else { 52 | marker = new LevelMarker(state.bitfield); 53 | levelMarkers.put(state.bitfield, marker); 54 | } 55 | marker.ranges.add(new int[] {range[0], range[1]}); 56 | marker.labels.add(state.name); 57 | marker.glColors.add(state.glColor); 58 | }); 59 | 60 | } 61 | 62 | /** 63 | * Calculates the pixelX values for each edge marker, then returns the List of markers. 64 | * 65 | * @param sampleNumberToPixelX Function that takes a Connection and sample number, then returns the corresponding pixelX value. 66 | * @return List of all the edge markers. 67 | */ 68 | public List getEdgeMarkers(BiFunction sampleNumberToPixelX) { 69 | 70 | List list = new ArrayList(edgeMarkers.values()); 71 | for(EdgeMarker marker : list) 72 | marker.pixelX = sampleNumberToPixelX.apply(marker.connection, marker.sampleNumber); 73 | 74 | return list; 75 | 76 | } 77 | 78 | /** 79 | * Calculates the pixelX values for each level marker, then returns the List of markers. 80 | * 81 | * @param sampleNumberToPixelX Function that takes a Connection and sample number, then returns the corresponding pixelX value. 82 | * @return List of all the level markers. 83 | */ 84 | public List getLevelMarkers(BiFunction sampleNumberToPixelX) { 85 | 86 | List list = new ArrayList(levelMarkers.values()); 87 | for(LevelMarker marker : list) 88 | for(int[] range : marker.ranges) 89 | marker.pixelXranges.add(new float[] {sampleNumberToPixelX.apply(marker.bitfield.dataset.connection, range[0]), sampleNumberToPixelX.apply(marker.bitfield.dataset.connection, range[1])}); 90 | 91 | return list; 92 | 93 | } 94 | 95 | /** 96 | * Represents a single *sample number*, and contains all of the bitfield edges that occurred at that sample number. 97 | */ 98 | public class EdgeMarker { 99 | 100 | ConnectionTelemetry connection; 101 | int sampleNumber; 102 | float pixelX; 103 | List text; 104 | List glColors; 105 | 106 | public EdgeMarker(Dataset.Bitfield.State state, int sampleNumber) { 107 | 108 | this.connection = state.dataset.connection; 109 | this.sampleNumber = sampleNumber; 110 | pixelX = 0; 111 | text = new ArrayList(); 112 | glColors = new ArrayList(); 113 | 114 | if(showSampleNumbers) { 115 | text.add("Sample " + sampleNumber); 116 | glColors.add(null); 117 | } 118 | 119 | if(showTimestamps) { 120 | String[] lines = SettingsController.formatTimestampToMilliseconds(connection.datasets.getTimestamp(sampleNumber)).split("\n"); 121 | for(String line : lines) { 122 | text.add(line); 123 | glColors.add(null); 124 | } 125 | } 126 | 127 | text.add(state.name); 128 | glColors.add(state.glColor); 129 | 130 | } 131 | 132 | } 133 | 134 | /** 135 | * Represents a single *bitfield*, and contains all of that its levels that should be displayed on screen. 136 | */ 137 | public class LevelMarker { 138 | 139 | Dataset.Bitfield bitfield; // this object contains a List of all the level markers for this bitfield 140 | List labels; // name of the state 141 | List glColors; // color for the state 142 | List ranges; // sample number range for the state 143 | List pixelXranges; // corresponding pixelX values for those sample number ranges 144 | 145 | public LevelMarker(Dataset.Bitfield bitfield) { 146 | 147 | this.bitfield = bitfield; 148 | labels = new ArrayList(); 149 | glColors = new ArrayList(); 150 | ranges = new ArrayList(); 151 | pixelXranges = new ArrayList(ranges.size()); 152 | 153 | } 154 | 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/ChartsController.java: -------------------------------------------------------------------------------- 1 | import java.awt.Toolkit; 2 | import java.util.ArrayList; 3 | import java.util.Collections; 4 | import java.util.List; 5 | 6 | public class ChartsController { 7 | 8 | private static List charts = Collections.synchronizedList(new ArrayList()); 9 | 10 | private static float dpiScalingFactorJava8 = (int) Math.round((double) Toolkit.getDefaultToolkit().getScreenResolution() / 100.0); // will be reset to 1.0 if using java 9+ 11 | private static float dpiScalingFactorJava9 = 1; // will be updated dynamically if using java 9+ 12 | private static float dpiScalingFactorUser = 1; // may be updated by the user 13 | 14 | /** 15 | * @return The display scaling factor. This takes into account the true DPI scaling requested by the OS, plus the user's modification (if any.) 16 | */ 17 | public static float getDisplayScalingFactor() { 18 | 19 | return dpiScalingFactorUser * dpiScalingFactorJava8 * dpiScalingFactorJava9; 20 | 21 | } 22 | 23 | /** 24 | * @return The display scaling factor for GUI widgets. 25 | */ 26 | public static float getDisplayScalingFactorForGUI() { 27 | 28 | return dpiScalingFactorJava8 * dpiScalingFactorJava9; 29 | 30 | } 31 | 32 | /** 33 | * @return The display scaling factor requested by the user. 34 | */ 35 | public static float getDisplayScalingFactorUser() { 36 | 37 | return dpiScalingFactorUser; 38 | 39 | } 40 | 41 | /** 42 | * @param newFactor The new display scaling factor specified by the user. 43 | */ 44 | public static void setDisplayScalingFactorUser(float newFactor) { 45 | 46 | if(newFactor < 1) newFactor = 1; 47 | if(newFactor > 10) newFactor = 10; 48 | 49 | dpiScalingFactorUser = newFactor; 50 | 51 | } 52 | 53 | /** 54 | * @param newFactor The new display scaling factor specified by the OS if using Java 9+. 55 | */ 56 | public static void setDisplayScalingFactorJava9(float newFactor) { 57 | 58 | if(newFactor == dpiScalingFactorJava9) 59 | return; 60 | 61 | if(newFactor < 1) newFactor = 1; 62 | if(newFactor > 10) newFactor = 10; 63 | 64 | dpiScalingFactorJava9 = newFactor; 65 | dpiScalingFactorJava8 = 1; // only use the Java9 scaling factor 66 | 67 | } 68 | 69 | /** 70 | * @return An array of Strings, one for each possible chart type. 71 | */ 72 | public static String[] getChartTypes() { 73 | 74 | return new String[] { 75 | "Time Domain", 76 | "Frequency Domain", 77 | "Histogram", 78 | "Statistics", 79 | "Dial", 80 | "Quaternion", 81 | "Camera", 82 | "Timeline" 83 | }; 84 | 85 | } 86 | 87 | /** 88 | * Creates a PositionedChart and adds it to the charts list. 89 | * 90 | * @param chartType One of the Strings from Controller.getChartTypes() 91 | * @param x1 The x-coordinate of a bounding-box corner in the OpenGLChartsRegion grid. 92 | * @param y1 The y-coordinate of a bounding-box corner in the OpenGLChartsRegion grid. 93 | * @param x2 The x-coordinate of the opposite bounding-box corner in the OpenGLChartsRegion grid. 94 | * @param y2 The x-coordinate of the opposite bounding-box corner in the OpenGLChartsRegion grid. 95 | * @return That chart, or null if chartType is invalid. 96 | */ 97 | public static PositionedChart createAndAddChart(String chartType, int x1, int y1, int x2, int y2) { 98 | 99 | PositionedChart chart = null; 100 | 101 | if(chartType.equals("Time Domain")) chart = new OpenGLTimeDomainChart(x1, y1, x2, y2); 102 | else if(chartType.equals("Frequency Domain")) chart = new OpenGLFrequencyDomainChart(x1, y1, x2, y2); 103 | else if(chartType.equals("Histogram")) chart = new OpenGLHistogramChart(x1, y1, x2, y2); 104 | else if(chartType.equals("Statistics")) chart = new OpenGLStatisticsChart(x1, y1, x2, y2); 105 | else if(chartType.equals("Dial")) chart = new OpenGLDialChart(x1, y1, x2, y2); 106 | else if(chartType.equals("Quaternion")) chart = new OpenGLQuaternionChart(x1, y1, x2, y2); 107 | else if(chartType.equals("Camera")) chart = new OpenGLCameraChart(x1, y1, x2, y2); 108 | else if(chartType.equals("Timeline")) chart = new OpenGLTimelineChart(x1, y1, x2, y2); 109 | 110 | if(chart != null) 111 | ChartsController.addChart(chart); 112 | 113 | return chart; 114 | 115 | } 116 | 117 | /** 118 | * @param chart New chart to insert and display. 119 | */ 120 | public static void addChart(PositionedChart chart) { 121 | 122 | charts.add(chart); 123 | updateTileOccupancy(null); 124 | 125 | } 126 | 127 | /** 128 | * Reorders the list of charts so the specified chart will be rendered after all other charts. 129 | * 130 | * @param chart The chart to render last. 131 | */ 132 | public static void drawChartLast(PositionedChart chart) { 133 | 134 | if(charts.size() < 2) 135 | return; 136 | 137 | Collections.swap(charts, charts.indexOf(chart), charts.size() - 1); 138 | 139 | } 140 | 141 | /** 142 | * Removes a specific chart. 143 | * 144 | * @param chart Chart to remove. 145 | */ 146 | public static void removeChart(PositionedChart chart) { 147 | 148 | ConfigureView.instance.closeIfUsedFor(chart); 149 | 150 | chart.dispose(); 151 | charts.remove(chart); 152 | updateTileOccupancy(null); 153 | 154 | } 155 | 156 | /** 157 | * Removes all charts. 158 | */ 159 | public static void removeAllCharts() { 160 | 161 | // many a temporary copy of the list because you can't remove from a list that you are iterating over 162 | List list = new ArrayList(charts); 163 | 164 | for(PositionedChart chart : list) 165 | removeChart(chart); 166 | 167 | } 168 | 169 | /** 170 | * @return All charts. 171 | */ 172 | public static List getCharts() { 173 | 174 | return charts; 175 | 176 | } 177 | 178 | /** 179 | * Checks if a region is available in the ChartsRegion. 180 | * 181 | * @param x1 The x-coordinate of a bounding-box corner in the OpenGLChartsRegion grid. 182 | * @param y1 The y-coordinate of a bounding-box corner in the OpenGLChartsRegion grid. 183 | * @param x2 The x-coordinate of the opposite bounding-box corner in the OpenGLChartsRegion grid. 184 | * @param y2 The y-coordinate of the opposite bounding-box corner in the OpenGLChartsRegion grid. 185 | * @return True if available, false if not. 186 | */ 187 | public static boolean gridRegionAvailable(int x1, int y1, int x2, int y2) { 188 | 189 | int topLeftX = x1 < x2 ? x1 : x2; 190 | int topLeftY = y1 < y2 ? y1 : y2; 191 | int bottomRightX = x2 > x1 ? x2 : x1; 192 | int bottomRightY = y2 > y1 ? y2 : y1; 193 | 194 | for(PositionedChart chart : charts) 195 | if(chart.regionOccupied(topLeftX, topLeftY, bottomRightX, bottomRightY)) 196 | return false; 197 | 198 | return true; 199 | 200 | } 201 | 202 | private static boolean[][] tileOccupied = new boolean[SettingsController.getTileColumns()][SettingsController.getTileRows()]; 203 | 204 | /** 205 | * Updates the array that tracks which tiles in the OpenGLChartsRegion are occupied by charts. 206 | * 207 | * @param removingChart If not null, pretend this chart does not exist, so the tiles behind it will be drawn while this chart fades away. 208 | */ 209 | public static void updateTileOccupancy(PositionedChart removingChart) { 210 | 211 | int columns = SettingsController.getTileColumns(); 212 | int rows = SettingsController.getTileRows(); 213 | 214 | tileOccupied = new boolean[columns][rows]; 215 | for(PositionedChart chart : getCharts()) { 216 | for(int x = chart.topLeftX; x <= chart.bottomRightX; x++) 217 | for(int y = chart.topLeftY; y <= chart.bottomRightY; y++) 218 | tileOccupied[x][rows - y - 1] = true; 219 | } 220 | 221 | if(removingChart != null) 222 | for(int x = removingChart.topLeftX; x <= removingChart.bottomRightX; x++) 223 | for(int y = removingChart.topLeftY; y <= removingChart.bottomRightY; y++) 224 | tileOccupied[x][rows - y - 1] = false; 225 | 226 | } 227 | 228 | /** 229 | * @return An array indicating which tiles in the OpenGLChartsRegion are occupied. 230 | */ 231 | public static boolean[][] getTileOccupancy() { 232 | 233 | return tileOccupied; 234 | 235 | } 236 | 237 | } 238 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/ConfigureView.java: -------------------------------------------------------------------------------- 1 | import java.awt.Component; 2 | import java.awt.Dimension; 3 | import java.awt.GridLayout; 4 | import java.awt.event.ActionListener; 5 | import java.awt.event.FocusEvent; 6 | import java.awt.event.FocusListener; 7 | import java.awt.event.KeyEvent; 8 | import java.awt.event.KeyListener; 9 | import java.util.Map; 10 | 11 | import javax.swing.Box; 12 | import javax.swing.JButton; 13 | import javax.swing.JLabel; 14 | import javax.swing.JPanel; 15 | import javax.swing.JScrollPane; 16 | import javax.swing.JTextField; 17 | import javax.swing.JToggleButton; 18 | import javax.swing.SwingUtilities; 19 | import javax.swing.border.EmptyBorder; 20 | 21 | import net.miginfocom.swing.MigLayout; 22 | 23 | @SuppressWarnings("serial") 24 | public class ConfigureView extends JPanel { 25 | 26 | static ConfigureView instance = new ConfigureView(); 27 | 28 | private JPanel widgetsPanel; 29 | private JPanel buttonsPanel; 30 | private JScrollPane scrollableRegion; 31 | private boolean testSizeAgain = true; 32 | 33 | private PositionedChart activeChart = null; 34 | private boolean activeChartIsNew = false; 35 | 36 | /** 37 | * Private constructor to enforce singleton usage. 38 | */ 39 | private ConfigureView() { 40 | 41 | super(); 42 | 43 | widgetsPanel = new JPanel(); 44 | widgetsPanel.setLayout(new MigLayout("hidemode 3, wrap 4, insets" + Theme.padding + " " + Theme.padding / 2 + " " + Theme.padding + " " + Theme.padding + ", gap " + Theme.padding, "[pref][min!][min!][grow]")); 45 | scrollableRegion = new JScrollPane(widgetsPanel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); 46 | scrollableRegion.setBorder(null); 47 | scrollableRegion.getVerticalScrollBar().setUnitIncrement(10); 48 | buttonsPanel = new JPanel(); 49 | buttonsPanel.setLayout(new MigLayout("insets 0", "[33%!][grow][33%!]")); // 3 equal columns 50 | buttonsPanel.setBorder(new EmptyBorder(Theme.padding * 2, Theme.padding, Theme.padding, Theme.padding)); // extra padding above 51 | 52 | setLayout(new MigLayout("wrap 1, insets 0")); // 1 column, no border 53 | add(scrollableRegion, "growx"); 54 | add(buttonsPanel, "growx"); 55 | 56 | setPreferredSize(new Dimension(0, 0)); 57 | 58 | } 59 | 60 | /** 61 | * Calculate the preferred size of this panel, taking into account the width of the vertical scroll bar. 62 | */ 63 | @Override public Dimension getPreferredSize() { 64 | 65 | widgetsPanel.setPreferredSize(null); 66 | Dimension size = widgetsPanel.getPreferredSize(); 67 | 68 | // resize the widgets region if the scrollbar is visible 69 | if(scrollableRegion.getVerticalScrollBar().isVisible()) 70 | size.width += scrollableRegion.getVerticalScrollBar().getPreferredSize().width; 71 | scrollableRegion.setPreferredSize(size); 72 | 73 | buttonsPanel.setPreferredSize(null); 74 | size = buttonsPanel.getPreferredSize(); 75 | int maxButtonWidth = 0; 76 | for(Component c : buttonsPanel.getComponents()) { 77 | int w = c.getPreferredSize().width; 78 | if(w > maxButtonWidth) 79 | maxButtonWidth = w; 80 | } 81 | size.width = 3*maxButtonWidth; 82 | buttonsPanel.setPreferredSize(size); 83 | 84 | // due to the event queue, the scroll bar may be about to appear or disappear, but the above if() can't see the future 85 | // work around this by triggering another getPreferredSize() at the end of the event queue. 86 | if(testSizeAgain) 87 | SwingUtilities.invokeLater(() -> revalidate()); 88 | testSizeAgain = !testSizeAgain; 89 | 90 | return super.getPreferredSize(); 91 | 92 | } 93 | 94 | /** 95 | * Updates this panel with configuration widgets for an existing chart. 96 | * 97 | * @param chart The chart to configure. 98 | */ 99 | public void forExistingChart(PositionedChart chart) { 100 | 101 | activeChart = chart; 102 | activeChartIsNew = false; 103 | 104 | widgetsPanel.removeAll(); 105 | buttonsPanel.removeAll(); 106 | 107 | for(Widget widget : chart.widgets) { 108 | if(widget == null) { 109 | widgetsPanel.add(Box.createVerticalStrut(Theme.padding), "span 4"); 110 | } else { 111 | widget.update(); 112 | for(Map.Entry thing : widget.widgets.entrySet()) 113 | widgetsPanel.add(thing.getKey(), thing.getValue()); 114 | } 115 | } 116 | 117 | JButton doneButton = new JButton("Done"); 118 | doneButton.addActionListener(event -> close()); 119 | buttonsPanel.add(doneButton, "growx, cell 2 0"); 120 | 121 | scrollableRegion.getVerticalScrollBar().setValue(0); 122 | 123 | instance.setPreferredSize(null); 124 | instance.revalidate(); 125 | instance.repaint(); 126 | 127 | } 128 | 129 | /** 130 | * Updates this panel with configuration widgets for a new chart. 131 | * 132 | * @param chart The new chart. 133 | */ 134 | public void forNewChart(PositionedChart chart) { 135 | 136 | activeChart = chart; 137 | activeChartIsNew = true; 138 | 139 | ActionListener chartTypeHandler = event -> { 140 | // replace the chart if a different chart type was selected 141 | JToggleButton clickedButton = (JToggleButton) event.getSource(); 142 | if(!activeChart.toString().equals(clickedButton.getText())) { 143 | int x1 = activeChart.topLeftX; 144 | int y1 = activeChart.topLeftY; 145 | int x2 = activeChart.bottomRightX; 146 | int y2 = activeChart.bottomRightY; 147 | ChartsController.removeChart(activeChart); 148 | PositionedChart newChart = ChartsController.createAndAddChart(clickedButton.getText(), x1, y1, x2, y2); 149 | instance.forNewChart(newChart); 150 | } 151 | }; 152 | 153 | JPanel chartTypePanel = new JPanel(); 154 | chartTypePanel.setLayout(new GridLayout(0, 2, Theme.padding, Theme.padding)); 155 | for(String chartType : ChartsController.getChartTypes()) { 156 | JToggleButton button = new JToggleButton(chartType); 157 | button.setSelected(button.getText().equals(activeChart.toString())); 158 | button.addActionListener(chartTypeHandler); 159 | chartTypePanel.add(button); 160 | } 161 | 162 | widgetsPanel.removeAll(); 163 | widgetsPanel.add(chartTypePanel, "span 4, growx"); 164 | widgetsPanel.add(Box.createVerticalStrut(Theme.padding * 2), "span 4"); 165 | for(Widget widget : activeChart.widgets) { 166 | if(widget == null) 167 | widgetsPanel.add(Box.createVerticalStrut(Theme.padding), "span 4"); 168 | else 169 | for(Map.Entry thing : widget.widgets.entrySet()) 170 | widgetsPanel.add(thing.getKey(), thing.getValue()); 171 | } 172 | 173 | buttonsPanel.removeAll(); 174 | JButton cancelButton = new JButton("Cancel"); 175 | cancelButton.addActionListener(event -> { ChartsController.removeChart(activeChart); close(); }); 176 | JButton doneButton = new JButton("Done"); 177 | doneButton.addActionListener(event -> close()); 178 | buttonsPanel.add(cancelButton, "growx, cell 0 0"); 179 | buttonsPanel.add(doneButton, "growx, cell 2 0"); 180 | 181 | scrollableRegion.getVerticalScrollBar().setValue(0); 182 | 183 | // size the panel as needed 184 | instance.setPreferredSize(null); 185 | instance.revalidate(); 186 | instance.repaint(); 187 | 188 | } 189 | 190 | /** 191 | * Updates this panel with configuration widgets for a Dataset. 192 | * 193 | * @param dataset The dataset to configure. 194 | */ 195 | public void forDataset(Dataset dataset) { 196 | 197 | activeChart = null; 198 | 199 | JTextField nameTextfield = new JTextField(dataset.name, 15); 200 | JButton colorButton = new JButton("\u25B2"); 201 | JTextField unitTextfield = new JTextField(dataset.unit, 15); 202 | 203 | JButton cancelButton = new JButton("Cancel"); 204 | cancelButton.addActionListener(event -> close()); 205 | JButton applyButton = new JButton("Apply"); 206 | applyButton.addActionListener(event -> { 207 | dataset.setNameColorUnit(nameTextfield.getText(), colorButton.getForeground(), unitTextfield.getText()); 208 | close(); 209 | }); 210 | buttonsPanel.removeAll(); 211 | buttonsPanel.add(cancelButton, "growx, cell 0 0"); 212 | buttonsPanel.add(applyButton, "growx, cell 2 0"); 213 | 214 | ActionListener pressEnterToApply = event -> applyButton.doClick(); 215 | 216 | nameTextfield.addActionListener(pressEnterToApply); 217 | nameTextfield.addFocusListener(new FocusListener() { 218 | @Override public void focusLost(FocusEvent e) { nameTextfield.setText(nameTextfield.getText().trim()); } 219 | @Override public void focusGained(FocusEvent e) { nameTextfield.selectAll(); } 220 | }); 221 | 222 | colorButton.setForeground(dataset.color); 223 | colorButton.addActionListener(event -> colorButton.setForeground(ColorPickerView.getColor(nameTextfield.getText(), colorButton.getForeground(), true))); 224 | 225 | unitTextfield.addActionListener(pressEnterToApply); 226 | unitTextfield.addFocusListener(new FocusListener() { 227 | @Override public void focusLost(FocusEvent arg0) { unitTextfield.setText(unitTextfield.getText().trim()); } 228 | @Override public void focusGained(FocusEvent arg0) { unitTextfield.selectAll(); } 229 | }); 230 | unitTextfield.addKeyListener(new KeyListener() { 231 | @Override public void keyReleased(KeyEvent ke) { unitTextfield.setText(unitTextfield.getText().trim()); } 232 | @Override public void keyPressed(KeyEvent ke) { } 233 | @Override public void keyTyped(KeyEvent ke) { } 234 | }); 235 | 236 | widgetsPanel.removeAll(); 237 | widgetsPanel.add(new JLabel("Name: ")); 238 | widgetsPanel.add(nameTextfield, "span 3, growx"); 239 | widgetsPanel.add(new JLabel("Color: ")); 240 | widgetsPanel.add(colorButton, "span 3, growx"); 241 | widgetsPanel.add(new JLabel("Unit: ")); 242 | widgetsPanel.add(unitTextfield, "span 3, growx"); 243 | 244 | scrollableRegion.getVerticalScrollBar().setValue(0); 245 | 246 | instance.setPreferredSize(null); 247 | instance.revalidate(); 248 | instance.repaint(); 249 | 250 | } 251 | 252 | public void redrawIfUsedFor(PositionedChart chart) { 253 | 254 | if(chart != activeChart || activeChart == null) 255 | return; 256 | 257 | if(activeChartIsNew) 258 | forNewChart(chart); 259 | else 260 | forExistingChart(chart); 261 | 262 | } 263 | 264 | /** 265 | * Closes the configuration view if it is being used for a specific chart. 266 | * 267 | * @param chart The chart. 268 | */ 269 | public void closeIfUsedFor(PositionedChart chart) { 270 | 271 | if(activeChart == chart) 272 | close(); 273 | 274 | } 275 | 276 | /** 277 | * Closes the configuration view. 278 | */ 279 | public void close() { 280 | 281 | activeChart = null; 282 | instance.setPreferredSize(new Dimension(0, 0)); 283 | instance.revalidate(); 284 | instance.repaint(); 285 | 286 | } 287 | 288 | } 289 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/Connection.java: -------------------------------------------------------------------------------- 1 | import java.io.PrintWriter; 2 | import java.util.concurrent.atomic.AtomicLong; 3 | import javax.swing.JPanel; 4 | import javax.swing.SwingUtilities; 5 | 6 | /** 7 | * The user establishes one or more Connections, with each Connection providing a stream of data. 8 | * The data might be normal telemetry, or camera images, or ... 9 | * The data might be coming from a live connection, or imported from a file. 10 | * 11 | * This parent class defines some abstract methods that must be implemented by each child class. 12 | * The rest of this class contains fields and methods that are used by some of the child classes. 13 | * Ideally there would be a deeper inheritance structure to divide some of this into even more classes, but for now everything just inherits from Connection. 14 | */ 15 | public abstract class Connection { 16 | 17 | Thread receiverThread; // listens for incoming data 18 | Thread processorThread; // processes the received data 19 | Thread transmitterThread; // sends data 20 | volatile boolean connected = false; 21 | volatile String name = ""; 22 | 23 | /** 24 | * @return A GUI with widgets for controlling this connection. 25 | */ 26 | public abstract JPanel getGui(); 27 | 28 | /** 29 | * Connects and listens for incoming telemetry. 30 | * 31 | * @param showGui If true, show a configuration GUI after establishing the connection. 32 | */ 33 | public abstract void connect(boolean showGui); 34 | 35 | /** 36 | * Configures this connection by reading from a settings file. 37 | * 38 | * @param lines Lines of text from the settings file. 39 | * @throws AssertionError If the settings file does not contain a valid configuration. 40 | */ 41 | public abstract void importSettings(ConnectionsController.QueueOfLines lines) throws AssertionError; 42 | 43 | /** 44 | * Saves the configuration to a settings file. 45 | * 46 | * @param file Destination file. 47 | */ 48 | public abstract void exportSettings(PrintWriter file); 49 | 50 | /** 51 | * Reads just enough from a data file to determine the timestamp of the first item. 52 | * 53 | * @param path Path to the file. 54 | * @return Timestamp for the first item, or Long.MAX_VALUE on error. 55 | */ 56 | public abstract long readFirstTimestamp(String path); 57 | 58 | /** 59 | * Reads the timestamp for a specific sample number or frame number. 60 | * 61 | * @param sampleNumber The sample or frame number. 62 | * @return Corresponding timestamp. 63 | */ 64 | public abstract long getTimestamp(int sampleNumber); 65 | 66 | /** 67 | * @return The number of samples or frames available. 68 | */ 69 | public abstract int getSampleCount(); 70 | 71 | /** 72 | * Removes all samples or frames. This is a non-permanent version of dispose(). 73 | * This does not close the connection, so this code must ensure there is no race condition. 74 | */ 75 | public abstract void removeAllData(); 76 | 77 | /** 78 | * Reads data (samples or images or ...) from a file, instead of a live connection. 79 | * 80 | * @param path Path to the file. 81 | * @param firstTimestamp Timestamp when the first sample from ANY connection was acquired. This is used to allow importing to happen in real time. 82 | * @param beginImportingTimestamp Timestamp when all import threads should begin importing. 83 | * @param completedByteCount Variable to increment as progress is made (this is periodically queried by a progress bar.) 84 | */ 85 | public abstract void importDataFile(String path, long firstTimestamp, long beginImportingTimestamp, AtomicLong completedByteCount); 86 | 87 | /** 88 | * Causes the file import thread to finish importing the file as fast as possible (instead of using a real-time playback speed.) 89 | * If it is already importing as fast as possible, this will instead cancel the process. 90 | */ 91 | public final void finishImporting() { 92 | 93 | if(!ConnectionsController.realtimeImporting) 94 | NotificationsController.showDebugMessage("Importing... Canceled"); 95 | 96 | if(receiverThread != null && receiverThread.isAlive()) 97 | receiverThread.interrupt(); 98 | 99 | } 100 | 101 | /** 102 | * Writes data (samples or images or ...) to a file, so it can be replayed later on. 103 | * 104 | * @param path Path to the file. 105 | * @param completedByteCount Variable to increment as progress is made (this is periodically queried by a progress bar.) 106 | */ 107 | public abstract void exportDataFile(String path, AtomicLong completedByteCount); 108 | 109 | /** 110 | * Permanently closes the connection and removes any cached data in memory or on disk. 111 | */ 112 | public abstract void dispose(); 113 | 114 | /** 115 | * Disconnects from the device and removes any connection-related Notifications. 116 | * This method blocks until disconnected, so it should not be called directly from the receiver thread. 117 | * 118 | * @param errorMessage If not null, show this as a Notification until a connection is attempted. 119 | */ 120 | public void disconnect(String errorMessage) { 121 | 122 | Main.hideConfigurationGui(); 123 | 124 | if(connected) { 125 | 126 | // tell the receiver thread to terminate by setting the boolean AND interrupting the thread because 127 | // interrupting the thread might generate an IOException, but we don't want to report that as an error 128 | connected = false; 129 | if(transmitterThread != null && transmitterThread.isAlive()) { 130 | transmitterThread.interrupt(); 131 | while(transmitterThread.isAlive()); // wait 132 | } 133 | if(receiverThread != null && receiverThread.isAlive()) { 134 | receiverThread.interrupt(); 135 | while(receiverThread.isAlive()); // wait 136 | } 137 | 138 | SwingUtilities.invokeLater(() -> { // invokeLater so this if() fails when importing a layout that has charts 139 | if(ChartsController.getCharts().isEmpty() && !ConnectionsController.telemetryPossible()) 140 | NotificationsController.showHintUntil("Start by connecting to a device or opening a file by using the buttons below.", () -> !ChartsController.getCharts().isEmpty(), true); 141 | }); 142 | 143 | } 144 | 145 | NotificationsController.removeIfConnectionRelated(); 146 | if(errorMessage != null) 147 | NotificationsController.showFailureUntil(errorMessage, () -> false, true); 148 | 149 | CommunicationView.instance.redraw(); 150 | 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/DatasetsInterface.java: -------------------------------------------------------------------------------- 1 | import java.nio.FloatBuffer; 2 | import java.util.ArrayList; 3 | import java.util.HashMap; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.function.BiConsumer; 7 | 8 | /** 9 | * Charts and exportFiles() must access a connection's datasets through a "DatasetsInterface" which automatically manages caching. 10 | * 11 | * All interactions are thread-safe *if* each thread creates its own DatasetsInterface, 12 | * therefore the charts and exportFiles() should each create their own DatasetsInterface. 13 | */ 14 | public class DatasetsInterface { 15 | 16 | public List normalDatasets = new ArrayList<>(); 17 | public List edgeStates = new ArrayList<>(); 18 | public List levelStates = new ArrayList<>(); 19 | public ConnectionTelemetry connection = null; 20 | private Map caches = new HashMap<>(); 21 | 22 | /** 23 | * Sets the normal (non-bitfield) datasets that can be subsequently accessed, replacing any existing ones. 24 | * 25 | * @param newDatasets Normal datasets to use. 26 | */ 27 | public void setNormals(List newDatasets) { 28 | 29 | // update normal datasets 30 | normalDatasets.clear(); 31 | normalDatasets.addAll(newDatasets); 32 | 33 | // update caches 34 | caches.clear(); 35 | normalDatasets.forEach(dataset -> caches.put(dataset, dataset.createCache())); 36 | edgeStates.forEach(state -> caches.put(state.dataset, state.dataset.createCache())); 37 | levelStates.forEach(state -> caches.put(state.dataset, state.dataset.createCache())); 38 | 39 | // update the connection 40 | connection = !normalDatasets.isEmpty() ? normalDatasets.get(0).connection : 41 | !edgeStates.isEmpty() ? edgeStates.get(0).connection : 42 | !levelStates.isEmpty() ? levelStates.get(0).connection : 43 | null; 44 | 45 | } 46 | 47 | /** 48 | * Sets the bitfield edge states that can be subsequently accessed, replacing any existing ones. 49 | * 50 | * @param newEdges Bitfield edge states to use. 51 | */ 52 | public void setEdges(List newEdges) { 53 | 54 | // update edge datasets 55 | edgeStates.clear(); 56 | edgeStates.addAll(newEdges); 57 | 58 | // update caches 59 | caches.clear(); 60 | normalDatasets.forEach(dataset -> caches.put(dataset, dataset.createCache())); 61 | edgeStates.forEach(state -> caches.put(state.dataset, state.dataset.createCache())); 62 | levelStates.forEach(state -> caches.put(state.dataset, state.dataset.createCache())); 63 | 64 | // update the connection 65 | connection = !normalDatasets.isEmpty() ? normalDatasets.get(0).connection : 66 | !edgeStates.isEmpty() ? edgeStates.get(0).connection : 67 | !levelStates.isEmpty() ? levelStates.get(0).connection : 68 | null; 69 | 70 | } 71 | 72 | /** 73 | * Sets the bitfield level states that can be subsequently accessed, replacing any existing ones. 74 | * 75 | * @param newLevels Bitfield level states to use. 76 | */ 77 | public void setLevels(List newLevels) { 78 | 79 | // update edge datasets 80 | levelStates.clear(); 81 | levelStates.addAll(newLevels); 82 | 83 | // update caches 84 | caches.clear(); 85 | normalDatasets.forEach(dataset -> caches.put(dataset, dataset.createCache())); 86 | edgeStates.forEach(state -> caches.put(state.dataset, state.dataset.createCache())); 87 | levelStates.forEach(state -> caches.put(state.dataset, state.dataset.createCache())); 88 | 89 | // update the connection 90 | connection = !normalDatasets.isEmpty() ? normalDatasets.get(0).connection : 91 | !edgeStates.isEmpty() ? edgeStates.get(0).connection : 92 | !levelStates.isEmpty() ? levelStates.get(0).connection : 93 | null; 94 | 95 | } 96 | 97 | /** 98 | * @param datasetN Index of a normal dataset (this is NOT the dataset location, but the setNormals() list index.) 99 | * @return Corresponding normal dataset. 100 | */ 101 | public Dataset getNormal(int datasetN) { 102 | return normalDatasets.get(datasetN); 103 | } 104 | 105 | /** 106 | * @param dataset Dataset to check for. 107 | * @return True if this object references the Dataset (as a normal/edge/level.) 108 | */ 109 | public boolean contains(Dataset dataset) { 110 | return caches.keySet().contains(dataset); 111 | } 112 | 113 | /** 114 | * @return True if any normal datasets have been selected. 115 | */ 116 | public boolean hasNormals() { 117 | return !normalDatasets.isEmpty(); 118 | } 119 | 120 | /** 121 | * @return True if any bitfield edges have been selected. 122 | */ 123 | public boolean hasEdges() { 124 | return !edgeStates.isEmpty(); 125 | } 126 | 127 | /** 128 | * @return True if any bitfield levels have been selected. 129 | */ 130 | public boolean hasLevels() { 131 | return !levelStates.isEmpty(); 132 | } 133 | 134 | /** 135 | * @return True if any normals/edges/levels have been selected. 136 | */ 137 | public boolean hasAnyType() { 138 | return connection != null; 139 | } 140 | 141 | /** 142 | * @return Number of normal datasets that have been selected. 143 | */ 144 | public int normalsCount() { 145 | return normalDatasets.size(); 146 | } 147 | 148 | /** 149 | * Gets a sample as a float32. 150 | * 151 | * @param dataset Dataset. 152 | * @param sampleNumber Sample number. 153 | * @return The sample, as a float32. 154 | */ 155 | public float getSample(Dataset dataset, int sampleNumber) { 156 | 157 | StorageFloats.Cache cache = cacheFor(dataset); 158 | return dataset.getSample(sampleNumber, cache); 159 | 160 | } 161 | 162 | /** 163 | * Gets a sample as a String. 164 | * 165 | * @param dataset Dataset. 166 | * @param sampleNumber Sample number. 167 | * @return The sample, as a String. 168 | */ 169 | public String getSampleAsString(Dataset dataset, int sampleNumber) { 170 | 171 | StorageFloats.Cache cache = cacheFor(dataset); 172 | return dataset.getSampleAsString(sampleNumber, cache); 173 | 174 | } 175 | 176 | /** 177 | * Gets a sequence of samples as a float[]. 178 | * 179 | * @param dataset Dataset. 180 | * @param minSampleNumber First sample number, inclusive. 181 | * @param maxSampleNumber Last sample number, inclusive. 182 | * @return A float[] of the samples. 183 | */ 184 | public float[] getSamplesArray(Dataset dataset, int minSampleNumber, int maxSampleNumber) { 185 | 186 | StorageFloats.Cache cache = cacheFor(dataset); 187 | return dataset.getSamplesArray(minSampleNumber, maxSampleNumber, cache); 188 | 189 | } 190 | 191 | /** 192 | * Gets a sequence of samples as a FloatBuffer. 193 | * 194 | * @param dataset Dataset. 195 | * @param minSampleNumber First sample number, inclusive. 196 | * @param maxSampleNumber Last sample number, inclusive. 197 | * @return A FloatBuffer of the samples. 198 | */ 199 | public FloatBuffer getSamplesBuffer(Dataset dataset, int minSampleNumber, int maxSampleNumber) { 200 | 201 | StorageFloats.Cache cache = cacheFor(dataset); 202 | return dataset.getSamplesBuffer(minSampleNumber, maxSampleNumber, cache); 203 | 204 | } 205 | 206 | /** 207 | * Gets the range (y-axis region) occupied by all of the normal datasets. 208 | * 209 | * @param minSampleNumber Minimum sample number, inclusive. 210 | * @param maxSampleNumber Maximum sample number, inclusive. 211 | * @return The range, as [0] = minY, [1] = maxY. 212 | * If there are no normal datasets, [-1, 1] will be returned. 213 | * If the range is a single value, [value +/- 0.001] will be returned. 214 | */ 215 | public float[] getRange(int minSampleNumber, int maxSampleNumber) { 216 | 217 | float[] minMax = new float[] {Float.MAX_VALUE, -Float.MAX_VALUE}; 218 | 219 | normalDatasets.forEach(dataset -> { 220 | if(!dataset.isBitfield) { 221 | StorageFloats.MinMax range = dataset.getRange((int) minSampleNumber, (int) maxSampleNumber, cacheFor(dataset)); 222 | if(range.min < minMax[0]) 223 | minMax[0] = range.min; 224 | if(range.max > minMax[1]) 225 | minMax[1] = range.max; 226 | } 227 | }); 228 | 229 | if(minMax[0] == Float.MAX_VALUE && minMax[1] == -Float.MAX_VALUE) { 230 | minMax[0] = -1; 231 | minMax[1] = 1; 232 | } else if(minMax[0] == minMax[1]) { 233 | float value = minMax[0]; 234 | minMax[0] = value - 0.001f; 235 | minMax[1] = value + 0.001f; 236 | } 237 | 238 | return minMax; 239 | 240 | } 241 | 242 | /** 243 | * Gets the range (y-axis region) occupied by a specific normal dataset. 244 | * 245 | * @param dataset Dataset to check. 246 | * @param minSampleNumber Minimum sample number, inclusive. 247 | * @param maxSampleNumber Maximum sample number, inclusive. 248 | * @return The range, as [0] = minY, [1] = maxY. 249 | * If there are no normal datasets, [-1, 1] will be returned. 250 | * If the range is a single value, [value +/- 0.001] will be returned. 251 | */ 252 | public float[] getRange(Dataset dataset, int minSampleNumber, int maxSampleNumber) { 253 | 254 | float[] minMax = new float[] {Float.MAX_VALUE, -Float.MAX_VALUE}; 255 | 256 | if(!dataset.isBitfield) { 257 | StorageFloats.MinMax range = dataset.getRange((int) minSampleNumber, (int) maxSampleNumber, cacheFor(dataset)); 258 | if(range.min < minMax[0]) 259 | minMax[0] = range.min; 260 | if(range.max > minMax[1]) 261 | minMax[1] = range.max; 262 | } 263 | 264 | if(minMax[0] == Float.MAX_VALUE && minMax[1] == -Float.MAX_VALUE) { 265 | minMax[0] = -1; 266 | minMax[1] = 1; 267 | } else if(minMax[0] == minMax[1]) { 268 | float value = minMax[0]; 269 | minMax[0] = value - 0.001f; 270 | minMax[1] = value + 0.001f; 271 | } 272 | 273 | return minMax; 274 | 275 | } 276 | 277 | /** 278 | * Iterates over all selected normal datasets. 279 | * 280 | * @param consumer BiConsumer that accepts a dataset and its corresponding samples cache. 281 | */ 282 | public void forEachNormal(BiConsumer consumer) { 283 | for(int i = 0; i < normalDatasets.size(); i++) { 284 | Dataset dataset = normalDatasets.get(i); 285 | StorageFloats.Cache cache = caches.get(dataset); 286 | consumer.accept(dataset, cache); 287 | } 288 | } 289 | 290 | /** 291 | * Checks for any bitfield edge events and iterates over them. 292 | * 293 | * @param minSampleNumber First sample number to check, inclusive. 294 | * @param maxSampleNumber Last sample number to check, inclusive. 295 | * @param consumer BiConsumer that accepts a bitfield state and its corresponding edge event sample number. 296 | */ 297 | public void forEachEdge(int minSampleNumber, int maxSampleNumber, BiConsumer consumer) { 298 | 299 | edgeStates.forEach(state -> { 300 | state.getEdgeEventsBetween(minSampleNumber, maxSampleNumber, cacheFor(state.dataset)).forEach(eventSampleNumber -> { 301 | consumer.accept(state, eventSampleNumber); 302 | }); 303 | }); 304 | 305 | } 306 | 307 | /** 308 | * Checks for any bitfield levels and iterates over them. 309 | * 310 | * @param minSampleNumber First sample number to check, inclusive. 311 | * @param maxSampleNumber Last sample number to check, inclusive. 312 | * @param consumer BiConsumer that accepts a bitfield state and its corresponding sample number range ([0] = start, [1] = end.) 313 | */ 314 | public void forEachLevel(int minSampleNumber, int maxSampleNumber, BiConsumer consumer) { 315 | 316 | levelStates.forEach(state -> { 317 | state.getLevelsBetween(minSampleNumber, maxSampleNumber, cacheFor(state.dataset)).forEach(range -> { 318 | consumer.accept(state, range); 319 | }); 320 | }); 321 | 322 | } 323 | 324 | /** 325 | * @param dataset Dataset (normal/edge/level.) 326 | * @return Corresponding samples cache. 327 | */ 328 | private StorageFloats.Cache cacheFor(Dataset dataset) { 329 | 330 | return caches.get(dataset); 331 | 332 | } 333 | 334 | } -------------------------------------------------------------------------------- /Telemetry Viewer/src/EventHandler.java: -------------------------------------------------------------------------------- 1 | import java.awt.Cursor; 2 | import java.awt.Point; 3 | import java.util.function.Consumer; 4 | 5 | public class EventHandler { 6 | 7 | public Consumer mouseLocationHandler; 8 | public Consumer dragStartedHandler; 9 | public Consumer dragEndedHandler; 10 | boolean forPressEvent = false; 11 | boolean forDragEvent = false; 12 | boolean dragInProgress = false; 13 | PositionedChart chart = null; 14 | Cursor cursor = null; 15 | 16 | /** 17 | * Force usage of the static methods to create an EventHandler. 18 | */ 19 | private EventHandler() { } 20 | 21 | /** 22 | * Called by the Swing MouseListener/MouseMotionListener when the user has clicked or dragged. 23 | * 24 | * @param mouseCoordinates X and Y pixel locations, relative to the chart, with (0,0) at the bottom-left. 25 | */ 26 | public void handleMouseLocation(Point mouseCoordinates) { 27 | mouseLocationHandler.accept(mouseCoordinates); 28 | } 29 | 30 | /** 31 | * Called by the Swing MouseListener when the user has pressed the mouse button. 32 | */ 33 | public void handleDragStarted() { 34 | if(dragStartedHandler != null) 35 | dragStartedHandler.accept(true); 36 | if(forDragEvent) 37 | dragInProgress = true; 38 | } 39 | 40 | /** 41 | * Called by the Swing MouseListener when the user has released the mouse button. 42 | */ 43 | public void handleDragEnded() { 44 | if(dragEndedHandler != null) 45 | dragEndedHandler.accept(true); 46 | if(forDragEvent) 47 | dragInProgress = false; 48 | } 49 | 50 | /** 51 | * Creates an event handler for click events. 52 | * 53 | * @param mouseLocationHandler Will be called if the user clicks. The location will be a dummy (-1,-1) value. 54 | * @return The event handler. 55 | */ 56 | public static EventHandler onPress(Consumer mouseLocationHandler) { 57 | 58 | EventHandler obj = new EventHandler(); 59 | obj.mouseLocationHandler = mouseLocationHandler; 60 | obj.forPressEvent = true; 61 | obj.forDragEvent = false; 62 | obj.dragInProgress = false; 63 | obj.chart = null; 64 | obj.dragStartedHandler = null; 65 | obj.dragEndedHandler = null; 66 | obj.cursor = Theme.clickableCursor; 67 | return obj; 68 | 69 | } 70 | 71 | /** 72 | * Creates an event handler for click events. 73 | * 74 | * @param chart Chart owning this event handler. 75 | * @param mouseLocationHandler Will be called if the user clicks. The location is in pixels, relative to the chart, with (0,0) at the bottom-left. 76 | * @return The event handler. 77 | */ 78 | public static EventHandler onPress(PositionedChart chart, Consumer mouseLocationHandler) { 79 | 80 | EventHandler obj = new EventHandler(); 81 | obj.mouseLocationHandler = mouseLocationHandler; 82 | obj.forPressEvent = true; 83 | obj.forDragEvent = false; 84 | obj.dragInProgress = false; 85 | obj.chart = chart; 86 | obj.dragStartedHandler = null; 87 | obj.dragEndedHandler = null; 88 | obj.cursor = Theme.clickableCursor; 89 | return obj; 90 | 91 | } 92 | 93 | /** 94 | * Creates an event handler for clicking and dragging. 95 | * 96 | * @param dragStartedHandler Will be called when the mouse is pressed. 97 | * @param mouseLocationHandler Will be called when the mouse is pressed or dragged. The location is in pixels, relative to the chart, with (0,0) at the bottom-left. 98 | * @param dragEndedHandler Will be called when the mouse is released. 99 | * @param chart Chart owning this event handler. 100 | * @param cursor Mouse cursor to draw. 101 | * @return The event handler. 102 | */ 103 | public static EventHandler onPressOrDrag(Consumer dragStartedHandler, Consumer mouseLocationHandler, Consumer dragEndedHandler, PositionedChart chart, Cursor cursor) { 104 | 105 | EventHandler obj = new EventHandler(); 106 | obj.mouseLocationHandler = mouseLocationHandler; 107 | obj.forPressEvent = true; 108 | obj.forDragEvent = true; 109 | obj.dragInProgress = false; 110 | obj.chart = chart; 111 | obj.dragStartedHandler = dragStartedHandler; 112 | obj.dragEndedHandler = dragEndedHandler; 113 | obj.cursor = cursor; 114 | return obj; 115 | 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/LogitechSmoothScrolling.java: -------------------------------------------------------------------------------- 1 | import java.net.URI; 2 | import org.eclipse.jetty.websocket.api.Session; 3 | import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; 4 | import org.eclipse.jetty.websocket.api.annotations.WebSocket; 5 | import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; 6 | import org.eclipse.jetty.websocket.client.WebSocketClient; 7 | 8 | /** 9 | * This class allows Logitech mice with high-precision scroll wheels to actually use their high-precision mode. 10 | * 11 | * The Logitech driver seems to rarely expose the high-precision scrolling mode. 12 | * I'm tempted to call this a poorly-written driver, since high-precision scrolling works fine in most programs when using a touchpad or touchscreen. 13 | * 14 | * Logitech offers a Chrome Extension that enables high-precision scrolling for Chrome. Looking at their Javascript code revealed how simple it is: 15 | * 1. Connect to a websocket at 127.0.0.1:59243 (this is a server run by Logitech's SetPoint.exe) 16 | * 2. Any time the window gains focus, you send a JSON text message to the websocket: 17 | * {"hiRes":true,"reason":"content looks good for scrolling"} 18 | * 19 | * It seems that the Logitech driver tracks window focus events, and disables high-precision scrolling for everything except Windows 10 tablet-style programs. 20 | * So just send the above JSON text message every time you gain focus, because if you loose focus, you will be returned to normal scrolling mode. 21 | * 22 | * The Chrome extension can also be reverse engineered without looking at it's Javascript: 23 | * 1. Install Chrome and the "Logitech Smooth Scrolling" Chrome extension. 24 | * 2. Close Chrome. 25 | * 3. Install Wireshark and Npcap. Npcap is needed so Wireshark can monitor the loopback network interface. 26 | * 4. Run Wireshark. 27 | * 5. Capture from "Npcap Loopback Adapter" 28 | * 6. Set the filter to "tcp.port == 59243" 29 | * 7. Open Chrome. 30 | * 8. You should see the connection and subsequent JSON text message. 31 | * 9. If you switch tabs in Chrome, or click on links, or if Chrome loses and then regains focus, you should see the text message being resent. 32 | * 10. Sometimes the message is not resent, and Chrome looses the high-precision scrolling mode. 33 | * This appears to be a bug in the Chrome extension. Making a new tab (or clicking a link) will cause the message to be sent and high-precision scrolling to return. 34 | * 35 | * The WebSocket server is run by Logitech's SetPoint.exe, this can be determined by: 36 | * 1. Open Chrome. 37 | * 2. Open cmd.exe as an administrator 38 | * 3. "netstat -a -b -n" 39 | * 4. You should see something like: 40 | * ... 41 | * [chrome.exe] 42 | * TCP 127.0.0.1:59243 0.0.0.0:0 LISTENING 43 | * [SetPoint.exe] 44 | * TCP 127.0.0.1:59243 127.0.0.1:53459 ESTABLISHED 45 | * ... 46 | */ 47 | @WebSocket(maxTextMessageSize = 64 * 1024) 48 | public class LogitechSmoothScrolling { 49 | 50 | WebSocketClient client; 51 | Session session; 52 | 53 | /** 54 | * Establishes a WebSocket connection to localhost:59243 55 | */ 56 | public LogitechSmoothScrolling() { 57 | 58 | try { 59 | 60 | client = new WebSocketClient(); 61 | client.start(); 62 | client.setConnectTimeout(100); // milliseconds 63 | client.connect(this, new URI("ws://127.0.0.1:59243"), new ClientUpgradeRequest()); 64 | 65 | } catch(Exception e) { 66 | 67 | // e.printStackTrace(); 68 | 69 | } 70 | 71 | } 72 | 73 | /** 74 | * After a successful connection, this method will automatically be called. Smooth scrolling is enabled or disabled here. 75 | * 76 | * @param session The WebSocket session. 77 | */ 78 | @OnWebSocketConnect public void onConnect(Session session) { 79 | 80 | try { 81 | 82 | this.session = session; 83 | if(SettingsController.getSmoothScrolling()) 84 | session.getRemote().sendStringByFuture("{\"hiRes\":true,\"reason\":\"content looks good for scrolling\"}"); 85 | else 86 | session.getRemote().sendStringByFuture("{\"hiRes\":false,\"reason\":\"content looks bad for scrolling\"}"); // made this up but it seems to work 87 | 88 | } catch (Exception e) { 89 | 90 | // e.printStackTrace(); 91 | 92 | } 93 | 94 | } 95 | 96 | /** 97 | * Re-enables or re-disables smooth scrolling. 98 | * This method should be called every time a JFrame gets focus, or whenever the smooth scrolling setting is changed. 99 | * 100 | * If the WebSocket connection has been lost, it will be reestablished. 101 | */ 102 | public void updateScrolling() { 103 | 104 | try { 105 | 106 | if(session != null && session.isOpen()) 107 | onConnect(session); 108 | else 109 | client.connect(this, new URI("ws://127.0.0.1:59243"), new ClientUpgradeRequest()); 110 | 111 | } catch(Exception e) { 112 | 113 | // e.printStackTrace(); 114 | 115 | } 116 | 117 | } 118 | 119 | } -------------------------------------------------------------------------------- /Telemetry Viewer/src/Main.java: -------------------------------------------------------------------------------- 1 | import java.awt.BorderLayout; 2 | import java.awt.Component; 3 | import java.awt.Dimension; 4 | import java.awt.datatransfer.DataFlavor; 5 | import java.awt.dnd.DnDConstants; 6 | import java.awt.dnd.DropTarget; 7 | import java.awt.dnd.DropTargetDropEvent; 8 | import java.awt.event.WindowAdapter; 9 | import java.awt.event.WindowEvent; 10 | import java.awt.event.WindowFocusListener; 11 | import java.io.File; 12 | import java.nio.file.FileAlreadyExistsException; 13 | import java.nio.file.Files; 14 | import java.nio.file.Path; 15 | import java.nio.file.Paths; 16 | import java.util.List; 17 | 18 | import javax.swing.JFrame; 19 | import javax.swing.JOptionPane; 20 | import javax.swing.JPanel; 21 | import javax.swing.SwingUtilities; 22 | import javax.swing.UIManager; 23 | 24 | public class Main { 25 | 26 | static final String versionString = "Telemetry Viewer v0.8"; 27 | static final String versionDate = "2021-07-24"; 28 | 29 | @SuppressWarnings("serial") 30 | static JFrame window = new JFrame(versionString) { 31 | 32 | int dataStructureViewWidth = -1; 33 | 34 | @Override public Dimension getPreferredSize() { 35 | 36 | if(dataStructureViewWidth < 0) 37 | dataStructureViewWidth = Integer.max( new DataStructureCsvView(ConnectionsController.telemetryConnections.get(0)).getPreferredSize().width, 38 | new DataStructureBinaryView(ConnectionsController.telemetryConnections.get(0)).getPreferredSize().width); 39 | 40 | int settingsViewWidth = SettingsView.instance.getPreferredSize().width; 41 | int settingsViewHeight = SettingsView.instance.getPreferredSize().height; 42 | int configureViewWidth = ConfigureView.instance.getPreferredSize().width; 43 | int controlsViewHeight = CommunicationView.instance.getPreferredSize().height; 44 | int controlsViewWidth = CommunicationView.instance.getPreferredSize().width; 45 | 46 | int width = controlsViewWidth; 47 | if(width < dataStructureViewWidth) 48 | width = dataStructureViewWidth; 49 | if(width < settingsViewWidth + configureViewWidth) 50 | width = settingsViewWidth + configureViewWidth; 51 | int height = settingsViewHeight + controlsViewHeight + (8 * Theme.padding); 52 | 53 | return new Dimension(width, height); 54 | 55 | } 56 | 57 | @Override public Dimension getMinimumSize() { 58 | 59 | return getPreferredSize(); 60 | 61 | } 62 | 63 | }; 64 | static LogitechSmoothScrolling mouse = new LogitechSmoothScrolling(); 65 | 66 | /** 67 | * Entry point for the program. 68 | * This just creates and configures the main window. 69 | * 70 | * @param args Command line arguments (not currently used.) 71 | */ 72 | @SuppressWarnings("serial") 73 | public static void main(String[] args) { 74 | 75 | try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch(Exception e){} 76 | 77 | // create the cache folder 78 | Path cacheDir = Paths.get("cache"); 79 | try { Files.createDirectory(cacheDir); } catch(FileAlreadyExistsException e) {} catch(Exception e) { e.printStackTrace(); } 80 | 81 | // populate the window 82 | window.setLayout(new BorderLayout()); 83 | window.add(OpenGLChartsView.instance, BorderLayout.CENTER); 84 | window.add(SettingsView.instance, BorderLayout.WEST); 85 | window.add(CommunicationView.instance, BorderLayout.SOUTH); 86 | window.add(ConfigureView.instance, BorderLayout.EAST); 87 | NotificationsController.showHintUntil("Start by connecting to a device or opening a file by using the buttons below.", () -> false, true); 88 | 89 | window.setSize(window.getPreferredSize()); 90 | window.setMinimumSize(window.getMinimumSize()); 91 | window.setLocationRelativeTo(null); 92 | window.setExtendedState(JFrame.MAXIMIZED_BOTH); 93 | 94 | // support smooth scrolling 95 | window.addWindowFocusListener(new WindowFocusListener() { 96 | @Override public void windowGainedFocus(WindowEvent we) { mouse.updateScrolling(); } 97 | @Override public void windowLostFocus(WindowEvent we) { } 98 | }); 99 | 100 | // allow the user to drag-n-drop settings/CSV/camera files 101 | window.setDropTarget(new DropTarget() { 102 | @Override public void drop(DropTargetDropEvent event) { 103 | try { 104 | event.acceptDrop(DnDConstants.ACTION_LINK); 105 | @SuppressWarnings("unchecked") 106 | List files = (List) event.getTransferable().getTransferData(DataFlavor.javaFileListFlavor); 107 | String[] filepaths = new String[files.size()]; 108 | for(int i = 0; i < files.size(); i++) 109 | filepaths[i] = files.get(i).getAbsolutePath(); 110 | ConnectionsController.importFiles(filepaths); 111 | } catch(Exception e) { 112 | NotificationsController.showFailureUntil("Error while processing files: " + e.getMessage(), () -> false, true); 113 | e.printStackTrace(); 114 | } 115 | } 116 | }); 117 | 118 | // handle window close events 119 | window.addWindowListener(new WindowAdapter() { 120 | @Override public void windowClosing(java.awt.event.WindowEvent windowEvent) { 121 | 122 | // cancel importing 123 | if(ConnectionsController.importing) { 124 | if(ConnectionsController.realtimeImporting) 125 | ConnectionsController.allConnections.forEach(connection -> connection.finishImporting()); // exit real-time 126 | ConnectionsController.allConnections.forEach(connection -> connection.finishImporting()); // abort 127 | } 128 | 129 | // cancel exporting if the user confirms it 130 | if(ConnectionsController.exporting) { 131 | int result = JOptionPane.showConfirmDialog(window, "Exporting in progress. Exit anyway?", "Confirm", JOptionPane.YES_NO_OPTION); 132 | if(result == JOptionPane.YES_OPTION) 133 | ConnectionsController.cancelExporting(); 134 | else 135 | return; // don't close 136 | } 137 | 138 | // close connections and remove their cache files 139 | ConnectionsController.allConnections.forEach(connection -> connection.dispose()); 140 | try { Files.deleteIfExists(cacheDir); } catch(Exception e) { } 141 | 142 | // die 143 | window.dispose(); 144 | System.exit(0); 145 | 146 | } 147 | }); 148 | 149 | // show the window 150 | window.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); // so the windowClosing listener can cancel the close 151 | window.setVisible(true); 152 | 153 | } 154 | 155 | /** 156 | * Hides the charts and settings panels, then shows the data structure screen in the middle of the main window. 157 | * This method is thread-safe. 158 | */ 159 | public static void showConfigurationGui(JPanel gui) { 160 | 161 | SwingUtilities.invokeLater(() -> { 162 | OpenGLChartsView.instance.animator.pause(); 163 | CommunicationView.instance.showSettings(false); 164 | ConfigureView.instance.close(); 165 | window.remove(OpenGLChartsView.instance); 166 | window.add(gui, BorderLayout.CENTER); 167 | window.revalidate(); 168 | window.repaint(); 169 | }); 170 | 171 | } 172 | 173 | /** 174 | * Hides the data structure screen and shows the charts in the middle of the main window. 175 | * This method is thread-safe. 176 | */ 177 | public static void hideConfigurationGui() { 178 | 179 | SwingUtilities.invokeLater(() -> { 180 | // do nothing if already hidden 181 | for(Component c : window.getContentPane().getComponents()) 182 | if(c == OpenGLChartsView.instance) 183 | return; 184 | 185 | // remove the configuration GUI 186 | for(Component c : window.getContentPane().getComponents()) 187 | if(c instanceof DataStructureCsvView || c instanceof DataStructureBinaryView) 188 | window.remove(c); 189 | 190 | window.add(OpenGLChartsView.instance, BorderLayout.CENTER); 191 | window.revalidate(); 192 | window.repaint(); 193 | OpenGLChartsView.instance.animator.resume(); 194 | }); 195 | 196 | } 197 | 198 | } 199 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/NotificationsController.java: -------------------------------------------------------------------------------- 1 | import java.awt.Color; 2 | import java.text.SimpleDateFormat; 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.Date; 6 | import java.util.List; 7 | import java.util.concurrent.atomic.AtomicLong; 8 | import java.util.function.BooleanSupplier; 9 | 10 | import javax.swing.JOptionPane; 11 | 12 | public class NotificationsController { 13 | 14 | private static final SimpleDateFormat timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); 15 | 16 | public static class Notification { 17 | String level; 18 | String[] lines; 19 | float[] glColor; 20 | long creationTimestamp; 21 | 22 | boolean isProgressBar; 23 | AtomicLong currentAmount; 24 | long totalAmount; 25 | 26 | boolean expiresAtTimestamp; // fades away 27 | long expirationTimestamp; 28 | boolean expiresAtEvent; 29 | BooleanSupplier event; 30 | boolean expiresOnConnection; // no fade, immediate removal 31 | 32 | public static Notification forMilliseconds(String level, String[] message, long milliseconds, boolean expiresOnConnection) { 33 | Notification notification = new Notification(); 34 | Color color = level.equals("hint") ? SettingsController.getHintNotificationColor() : 35 | level.equals("warning") ? SettingsController.getWarningNotificationColor() : 36 | level.equals("failure") ? SettingsController.getFailureNotificationColor() : 37 | SettingsController.getVerboseNotificationColor(); 38 | notification.level = level; 39 | notification.lines = message; 40 | notification.glColor = new float[] {color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f, 1.0f}; 41 | notification.creationTimestamp = System.currentTimeMillis(); 42 | notification.isProgressBar = false; 43 | notification.expiresAtTimestamp = true; 44 | notification.expirationTimestamp = notification.creationTimestamp + milliseconds; 45 | notification.expiresAtEvent = false; 46 | notification.expiresOnConnection = expiresOnConnection; 47 | return notification; 48 | } 49 | 50 | public static Notification untilEvent(String level, String[] message, BooleanSupplier isExpired, boolean expiresOnConnection) { 51 | Notification notification = new Notification(); 52 | Color color = level.equals("hint") ? SettingsController.getHintNotificationColor() : 53 | level.equals("warning") ? SettingsController.getWarningNotificationColor() : 54 | level.equals("failure") ? SettingsController.getFailureNotificationColor() : 55 | SettingsController.getVerboseNotificationColor(); 56 | notification.level = level; 57 | notification.lines = message; 58 | notification.glColor = new float[] {color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f, 1.0f}; 59 | notification.creationTimestamp = System.currentTimeMillis(); 60 | notification.isProgressBar = false; 61 | notification.expiresAtTimestamp = false; 62 | notification.expiresAtEvent = true; 63 | notification.event = isExpired; 64 | notification.expiresOnConnection = expiresOnConnection; 65 | return notification; 66 | } 67 | 68 | public static Notification progressBar(String level, String[] message, AtomicLong currentAmount, long totalAmount) { 69 | Notification notification = new Notification(); 70 | Color color = level.equals("hint") ? SettingsController.getHintNotificationColor() : 71 | level.equals("warning") ? SettingsController.getWarningNotificationColor() : 72 | level.equals("failure") ? SettingsController.getFailureNotificationColor() : 73 | SettingsController.getVerboseNotificationColor(); 74 | notification.level = level; 75 | notification.lines = message; 76 | notification.glColor = new float[] {color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f, 1.0f}; 77 | notification.creationTimestamp = System.currentTimeMillis(); 78 | notification.isProgressBar = true; 79 | notification.currentAmount = currentAmount; 80 | notification.totalAmount = totalAmount; 81 | notification.expiresAtTimestamp = false; 82 | notification.expiresAtEvent = false; 83 | notification.expiresOnConnection = false; 84 | return notification; 85 | } 86 | 87 | private Notification() {} 88 | } 89 | private static List notifications = Collections.synchronizedList(new ArrayList()); 90 | 91 | /** 92 | * @return Notifications to show the user. If >5 exist, the oldest non-progress-bar notifications will fade away. 93 | */ 94 | public static List getNotifications() { 95 | 96 | notifications.removeIf(item -> item.expiresAtTimestamp && item.expirationTimestamp + Theme.animationMilliseconds <= System.currentTimeMillis()); 97 | notifications.removeIf(item -> item.expiresAtEvent && item.event.getAsBoolean() == true); 98 | notifications.forEach(item -> { 99 | if(item.isProgressBar && item.currentAmount.get() >= item.totalAmount) { 100 | item.expiresAtTimestamp = true; 101 | item.expirationTimestamp = System.currentTimeMillis() + 2000; // fade away 2 seconds after done 102 | item.lines[0] += " Done"; 103 | item.isProgressBar = false; 104 | NotificationsController.showDebugMessage(item.lines[0]); 105 | } 106 | }); 107 | if(notifications.size() > 5) { 108 | long now = System.currentTimeMillis(); 109 | for(int i = 0; i < notifications.size() - 5; i++) { 110 | Notification n = notifications.get(i); 111 | if(!n.isProgressBar && (!n.expiresAtTimestamp || n.expirationTimestamp > now)) { 112 | n.expiresAtTimestamp = true; 113 | n.expirationTimestamp = now; 114 | } 115 | } 116 | } 117 | 118 | return notifications; 119 | 120 | } 121 | 122 | /** 123 | * Immediately removes all Notifications (without fade away animations) that should expire when connecting or disconnecting. 124 | * This method is thread-safe. 125 | */ 126 | public static void removeIfConnectionRelated() { 127 | 128 | notifications.removeIf(item -> item.expiresOnConnection); 129 | 130 | } 131 | 132 | /** 133 | * Shows a hint message. It will be printed to the console, and if enabled, it will be shown in the GUI until an expiration condition is met. 134 | * This method is thread-safe. 135 | * 136 | * @param message The message to show. 137 | * @param isExpired A BooleanSupplier that returns true when this Notification should be removed. This will be tested periodically. 138 | * @param autoExpire If this Notification should be expired when connecting or disconnecting. 139 | */ 140 | public static void showHintUntil(String message, BooleanSupplier isExpired, boolean autoExpire) { 141 | 142 | System.out.println(timestamp.format(new Date()) + " [HINT ] " + message); 143 | if(SettingsController.getHintNotificationVisibility()) 144 | notifications.add(Notification.untilEvent("hint", message.split("\\R"), isExpired, autoExpire)); 145 | 146 | } 147 | 148 | /** 149 | * Shows a warning message. It will be printed to the console, and if enabled, it will be shown in the GUI for an amount of time. 150 | * This method is thread-safe. 151 | * 152 | * @param message The message to show. 153 | * @param milliseconds How long to show this message for. 154 | * @param autoExpire If this Notification should be expired when connecting or disconnecting. 155 | */ 156 | public static void showWarningForMilliseconds(String message, long milliseconds, boolean autoExpire) { 157 | 158 | System.out.println(timestamp.format(new Date()) + " [WARNING ] " + message); 159 | if(SettingsController.getWarningNotificationVisibility()) 160 | notifications.add(Notification.forMilliseconds("warning", message.split("\\R"), milliseconds, autoExpire)); 161 | 162 | } 163 | 164 | /** 165 | * Shows a failure message. It will be printed to the console, and if enabled, it will be shown in the GUI until an expiration condition is met. 166 | * This method is thread-safe. 167 | * 168 | * @param message The message to show. 169 | * @param isExpired A BooleanSupplier that returns true when this Notification should be removed. This will be tested periodically. 170 | * @param autoExpire If this Notification should be expired when connecting or disconnecting. 171 | */ 172 | public static void showFailureUntil(String message, BooleanSupplier isExpired, boolean autoExpire) { 173 | 174 | System.out.println(timestamp.format(new Date()) + " [FAILURE ] " + message); 175 | if(SettingsController.getFailureNotificationVisibility()) 176 | notifications.add(Notification.untilEvent("failure", message.split("\\R"), isExpired, autoExpire)); 177 | 178 | } 179 | 180 | /** 181 | * Shows a failure message. It will be printed to the console, and if enabled, it will be shown in the GUI for an amount of time. 182 | * This method is thread-safe. 183 | * 184 | * @param message The message to show. 185 | * @param durationSeconds How long to show this message for. 186 | * @param autoExpire If this Notification should be expired when connecting or disconnecting. 187 | */ 188 | public static void showFailureForMilliseconds(String message, long milliseconds, boolean autoExpire) { 189 | 190 | System.out.println(timestamp.format(new Date()) + " [FAILURE ] " + message); 191 | if(SettingsController.getFailureNotificationVisibility()) 192 | notifications.add(Notification.forMilliseconds("failure", message.split("\\R"), milliseconds, autoExpire)); 193 | 194 | } 195 | 196 | /** 197 | * Shows a verbose message. It will be printed to the console, and if enabled, it will be shown in the GUI for a number of seconds. 198 | * This method is thread-safe. 199 | * 200 | * @param message The message to show. 201 | * @param milliseconds How long to show this message for. 202 | * @param autoExpire If this Notification should be expired when connecting or disconnecting. 203 | */ 204 | public static void showVerboseForMilliseconds(String message, long milliseconds, boolean autoExpire) { 205 | 206 | System.out.println(timestamp.format(new Date()) + " [VERBOSE ] " + message); 207 | if(SettingsController.getVerboseNotificationVisibility()) 208 | notifications.add(Notification.forMilliseconds("verbose", message.split("\\R"), milliseconds, autoExpire)); 209 | 210 | } 211 | 212 | /** 213 | * Shows a debug message. It will be printed to the console, and if enabled, it will be shown in the GUI for 10 seconds. 214 | * This method is thread-safe. 215 | * 216 | * @param message The message to show. 217 | */ 218 | public static void showDebugMessage(String message) { 219 | 220 | System.out.println(timestamp.format(new Date()) + " [DEBUG ] " + message); 221 | 222 | } 223 | 224 | /** 225 | * Shows a progress bar with message. The message will be printed to the console as a verbose log, and a progress bar will be shown in the GUI. 226 | * This method is thread-safe. 227 | * 228 | * @param message The message to show. 229 | */ 230 | public static AtomicLong showProgressBar(String message, long totalAmount) { 231 | 232 | System.out.println(timestamp.format(new Date()) + " [HINT ] " + message); 233 | 234 | AtomicLong currentAmount = new AtomicLong(0); 235 | notifications.add(Notification.progressBar("hint", message.split("\\R"), currentAmount, totalAmount)); 236 | 237 | return currentAmount; 238 | 239 | } 240 | 241 | public static void showCriticalFault(String message) { 242 | 243 | System.out.println(timestamp.format(new Date()) + " [CRITICAL] " + message); 244 | JOptionPane.showMessageDialog(null, "CRITICAL FAULT

If you continue to use the software it may crash or become unresponsive.
The error message is below, but more details may have been printed to the console.

" + message + "
", "CRITICAL FAULT", JOptionPane.ERROR_MESSAGE); 245 | 246 | } 247 | 248 | } 249 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/OpenGLCameraChart.java: -------------------------------------------------------------------------------- 1 | import com.jogamp.opengl.GL2ES3; 2 | import com.jogamp.opengl.GL3; 3 | 4 | /** 5 | * Displays images from a camera. 6 | * 7 | * User settings: 8 | * Camera to use. 9 | * Rotation and mirroring. 10 | * A label can be displayed. 11 | */ 12 | public class OpenGLCameraChart extends PositionedChart { 13 | 14 | ConnectionCamera camera = null; 15 | long previousFrameTimestamp = 0; 16 | 17 | // image region on screen 18 | int[] texHandle; 19 | boolean mirrorX; 20 | boolean mirrorY; 21 | boolean rotateClockwise; 22 | float xDisplayLeft; 23 | float xDisplayRight; 24 | float yDisplayTop; 25 | float yDisplayBottom; 26 | float displayWidth; 27 | float displayHeight; 28 | 29 | // label 30 | boolean showLabel; 31 | float labelWidth; 32 | float xLabelLeft; 33 | float xLabelRight; 34 | float yLabelBaseline; 35 | float yLabelTop; 36 | 37 | // control widgets 38 | WidgetCamera cameraWidget; 39 | WidgetCheckbox mirrorXwidget; 40 | WidgetCheckbox mirrorYwidget; 41 | WidgetCheckbox rotatewidget; 42 | WidgetCheckbox labelWidget; 43 | 44 | @Override public String toString() { 45 | 46 | return "Camera"; 47 | 48 | } 49 | 50 | public OpenGLCameraChart(int x1, int y1, int x2, int y2) { 51 | 52 | super(x1, y1, x2, y2); 53 | 54 | cameraWidget = new WidgetCamera(cameraName -> { 55 | for(ConnectionCamera c : ConnectionsController.cameraConnections) 56 | if(c.name.equals(cameraName)) 57 | camera = c; 58 | }); 59 | 60 | mirrorXwidget = new WidgetCheckbox("Mirror X-Axis \u2194", 61 | false, 62 | mirror -> mirrorX = mirror); 63 | 64 | mirrorYwidget = new WidgetCheckbox("Mirror Y-Axis \u2195", 65 | false, 66 | mirror -> mirrorY = mirror); 67 | 68 | rotatewidget = new WidgetCheckbox("Rotate Clockwise \u21B7", 69 | false, 70 | rotate -> rotateClockwise = rotate); 71 | 72 | labelWidget = new WidgetCheckbox("Show Label", 73 | true, 74 | newShowLabel -> showLabel = newShowLabel); 75 | 76 | widgets = new Widget[5]; 77 | widgets[0] = cameraWidget; 78 | widgets[1] = mirrorXwidget; 79 | widgets[2] = mirrorYwidget; 80 | widgets[3] = rotatewidget; 81 | widgets[4] = labelWidget; 82 | 83 | } 84 | 85 | @Override public EventHandler drawChart(GL2ES3 gl, float[] chartMatrix, int width, int height, long nowTimestamp, int lastSampleNumber, double zoomLevel, int mouseX, int mouseY) { 86 | 87 | // get the image 88 | ConnectionCamera.GLframe f = null; 89 | if(camera == null) 90 | f = new ConnectionCamera.GLframe(null, true, 1, 1, "[select a camera]", 0); 91 | else if(OpenGLChartsView.instance.isLiveView() && !ConnectionsController.importing) 92 | f = camera.getLiveImage(); 93 | else { 94 | long lastTimestamp = OpenGLChartsView.instance.isLiveView() ? ConnectionsController.getLastTimestamp() : OpenGLChartsView.instance.pausedTimestamp; 95 | f = camera.getImageAtOrBeforeTimestamp(lastTimestamp); 96 | } 97 | 98 | // calculate x and y positions of everything 99 | xDisplayLeft = Theme.tilePadding; 100 | xDisplayRight = width - Theme.tilePadding; 101 | displayWidth = xDisplayRight - xDisplayLeft; 102 | yDisplayBottom = Theme.tilePadding; 103 | yDisplayTop = height - Theme.tilePadding; 104 | displayHeight = yDisplayTop - yDisplayBottom; 105 | 106 | if(showLabel) { 107 | labelWidth = OpenGL.largeTextWidth(gl, f.label); 108 | yLabelBaseline = Theme.tilePadding; 109 | yLabelTop = yLabelBaseline + OpenGL.largeTextHeight; 110 | xLabelLeft = (width / 2f) - (labelWidth / 2f); 111 | xLabelRight = xLabelLeft + labelWidth; 112 | 113 | yDisplayBottom = yLabelTop + Theme.tickTextPadding + Theme.tilePadding; 114 | displayHeight = yDisplayTop - yDisplayBottom; 115 | } 116 | 117 | // maintain the image aspect ratio, so it doesn't stretch 118 | float desiredAspectRatio = rotateClockwise ? (float) f.height / (float) f.width : (float) f.width / (float) f.height; 119 | float currentAspectRatio = displayWidth / displayHeight; 120 | if(currentAspectRatio != desiredAspectRatio) { 121 | if(desiredAspectRatio > currentAspectRatio) { 122 | // need to make image shorter 123 | float desiredHeight = displayWidth / desiredAspectRatio; 124 | float delta = displayHeight - desiredHeight; 125 | yDisplayTop -= delta / 2; 126 | yDisplayBottom += delta / 2; 127 | displayHeight = yDisplayTop - yDisplayBottom; 128 | } else { 129 | // need to make image narrower 130 | float desiredWidth = displayHeight * desiredAspectRatio; 131 | float delta = displayWidth - desiredWidth; 132 | xDisplayLeft += delta / 2; 133 | xDisplayRight -= delta / 2; 134 | displayWidth = xDisplayRight - xDisplayLeft; 135 | } 136 | } 137 | 138 | // draw the image 139 | if(texHandle == null) { 140 | texHandle = new int[1]; 141 | OpenGL.createTexture(gl, texHandle, f.width, f.height, f.isBgr ? GL3.GL_BGR : GL3.GL_RGB, GL3.GL_UNSIGNED_BYTE, true); 142 | OpenGL.writeTexture (gl, texHandle, f.width, f.height, f.isBgr ? GL3.GL_BGR : GL3.GL_RGB, GL3.GL_UNSIGNED_BYTE, f.buffer); 143 | previousFrameTimestamp = f.timestamp; 144 | } else if(f.timestamp != previousFrameTimestamp) { 145 | // only replace the texture if a new image is available 146 | OpenGL.writeTexture(gl, texHandle, f.width, f.height, f.isBgr ? GL3.GL_BGR : GL3.GL_RGB, GL3.GL_UNSIGNED_BYTE, f.buffer); 147 | previousFrameTimestamp = f.timestamp; 148 | } 149 | 150 | if(!mirrorX && !mirrorY) OpenGL.drawTexturedBox(gl, texHandle, false, xDisplayLeft, yDisplayTop, displayWidth, -displayHeight, 0, rotateClockwise); 151 | else if( mirrorX && !mirrorY) OpenGL.drawTexturedBox(gl, texHandle, false, xDisplayRight, yDisplayTop, -displayWidth, -displayHeight, 0, rotateClockwise); 152 | else if(!mirrorX && mirrorY) OpenGL.drawTexturedBox(gl, texHandle, false, xDisplayLeft, yDisplayBottom, displayWidth, displayHeight, 0, rotateClockwise); 153 | else if( mirrorX && mirrorY) OpenGL.drawTexturedBox(gl, texHandle, false, xDisplayRight, yDisplayBottom, -displayWidth, displayHeight, 0, rotateClockwise); 154 | 155 | // draw the label, on top of a background quad, if there is room 156 | if(showLabel && labelWidth < width - Theme.tilePadding * 2) { 157 | OpenGL.drawQuad2D(gl, Theme.tileShadowColor, xLabelLeft - Theme.tickTextPadding, yLabelBaseline - Theme.tickTextPadding, xLabelRight + Theme.tickTextPadding, yLabelTop + Theme.tickTextPadding); 158 | OpenGL.drawLargeText(gl, f.label, (int) xLabelLeft, (int) yLabelBaseline, 0); 159 | } 160 | 161 | return null; 162 | 163 | } 164 | 165 | @Override public void disposeGpu(GL2ES3 gl) { 166 | 167 | super.disposeGpu(gl); 168 | if(texHandle != null) 169 | gl.glDeleteTextures(1, texHandle, 0); 170 | texHandle = null; 171 | 172 | } 173 | 174 | } 175 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/OpenGLDialChart.java: -------------------------------------------------------------------------------- 1 | import com.jogamp.opengl.GL2ES3; 2 | import com.jogamp.opengl.GL3; 3 | 4 | /** 5 | * Renders a dial showing the value of the most recent sample. 6 | * 7 | * User settings: 8 | * Dataset to visualize. 9 | * Dial minimum value can be fixed or autoscaled. 10 | * Dial maximum value can be fixed or autoscaled. 11 | * Sample count (this is used for autoscaling and for statistics.) 12 | * Current reading label can be displayed. 13 | * Dataset label can be displayed. 14 | * Dial minimum and maximum labels can be displayed. 15 | * Statistics (mean and standard deviation) can be displayed. 16 | */ 17 | public class OpenGLDialChart extends PositionedChart { 18 | 19 | final int dialResolution = 400; // how many quads to draw 20 | final float dialThickness = 0.4f; // percentage of the radius 21 | float dialMin; 22 | float dialMax; 23 | 24 | // plot region 25 | float xPlotLeft; 26 | float xPlotRight; 27 | float plotWidth; 28 | float yPlotTop; 29 | float yPlotBottom; 30 | float plotHeight; 31 | 32 | // min max labels 33 | boolean showMinMaxLabels; 34 | float yMinMaxLabelsBaseline; 35 | float yMinMaxLabelsTop; 36 | String minLabel; 37 | String maxLabel; 38 | float minLabelWidth; 39 | float maxLabelWidth; 40 | float xMinLabelLeft; 41 | float xMaxLabelLeft; 42 | 43 | // reading label 44 | boolean showReadingLabel; 45 | String readingLabel; 46 | float readingLabelWidth; 47 | float xReadingLabelLeft; 48 | float yReadingLabelBaseline; 49 | float yReadingLabelTop; 50 | float readingLabelRadius; 51 | 52 | // dataset label 53 | boolean showDatasetLabel; 54 | String datasetLabel; 55 | float datasetLabelWidth; 56 | float yDatasetLabelBaseline; 57 | float yDatasetLabelTop; 58 | float xDatasetLabelLeft; 59 | float datasetLabelRadius; 60 | 61 | // control widgets 62 | WidgetDatasets datasetWidget; 63 | WidgetTextfieldsOptionalMinMax minMaxWidget; 64 | WidgetCheckbox showReadingLabelWidget; 65 | WidgetCheckbox showDatasetLabelWidget; 66 | WidgetCheckbox showMinMaxLabelsWidget; 67 | 68 | @Override public String toString() { 69 | 70 | return "Dial"; 71 | 72 | } 73 | 74 | public OpenGLDialChart(int x1, int y1, int x2, int y2) { 75 | 76 | super(x1, y1, x2, y2); 77 | 78 | datasetWidget = new WidgetDatasets(newDatasets -> datasets.setNormals(newDatasets), 79 | null, 80 | null, 81 | null, 82 | false, 83 | new String[] {"Dataset"}); 84 | 85 | minMaxWidget = new WidgetTextfieldsOptionalMinMax("Dial", 86 | false, 87 | -1, 88 | 1, 89 | -Float.MAX_VALUE, 90 | Float.MAX_VALUE, 91 | (newAutoscaleMin, newManualMin) -> dialMin = newManualMin, 92 | (newAutoscaleMax, newManualMax) -> dialMax = newManualMax); 93 | 94 | showReadingLabelWidget = new WidgetCheckbox("Show Reading Label", 95 | true, 96 | newShowReadingLabel -> showReadingLabel = newShowReadingLabel); 97 | 98 | showDatasetLabelWidget = new WidgetCheckbox("Show Dataset Label", 99 | true, 100 | newShowDatasetLabel -> showDatasetLabel = newShowDatasetLabel); 101 | 102 | showMinMaxLabelsWidget = new WidgetCheckbox("Show Min/Max Labels", 103 | true, 104 | newShowMinMaxLabels -> showMinMaxLabels = newShowMinMaxLabels); 105 | 106 | widgets = new Widget[7]; 107 | widgets[0] = datasetWidget; 108 | widgets[1] = null; 109 | widgets[2] = minMaxWidget; 110 | widgets[3] = null; 111 | widgets[4] = showDatasetLabelWidget; 112 | widgets[5] = showReadingLabelWidget; 113 | widgets[6] = showMinMaxLabelsWidget; 114 | 115 | } 116 | 117 | @Override public EventHandler drawChart(GL2ES3 gl, float[] chartMatrix, int width, int height, long endTimestamp, int endSampleNumber, double zoomLevel, int mouseX, int mouseY) { 118 | 119 | EventHandler handler = null; 120 | 121 | // sanity check 122 | if(datasets.normalsCount() != 1) 123 | return handler; 124 | 125 | // get the sample 126 | int lastSampleNumber = endSampleNumber; 127 | int trueLastSampleNumber = datasets.connection.getSampleCount() - 1; 128 | if(lastSampleNumber > trueLastSampleNumber) 129 | lastSampleNumber = trueLastSampleNumber; 130 | Dataset dataset = datasets.getNormal(0); 131 | float sample = lastSampleNumber > 0 ? datasets.getSample(dataset, lastSampleNumber) : 0; 132 | 133 | // calculate x and y positions of everything 134 | xPlotLeft = Theme.tilePadding; 135 | xPlotRight = width - Theme.tilePadding; 136 | plotWidth = xPlotRight - xPlotLeft; 137 | yPlotTop = height - Theme.tilePadding; 138 | yPlotBottom = Theme.tilePadding; 139 | plotHeight = yPlotTop - yPlotBottom; 140 | 141 | if(showMinMaxLabels) { 142 | yMinMaxLabelsBaseline = Theme.tilePadding; 143 | yMinMaxLabelsTop = yMinMaxLabelsBaseline + OpenGL.smallTextHeight; 144 | minLabel = ChartUtils.formattedNumber(dialMin, 6); 145 | maxLabel = ChartUtils.formattedNumber(dialMax, 6); 146 | minLabelWidth = OpenGL.smallTextWidth(gl, minLabel); 147 | maxLabelWidth = OpenGL.smallTextWidth(gl, maxLabel); 148 | 149 | yPlotBottom = yMinMaxLabelsTop + Theme.tickTextPadding; 150 | plotHeight = yPlotTop - yPlotBottom; 151 | } 152 | 153 | float xCircleCenter = plotWidth / 2 + Theme.tilePadding; 154 | float yCircleCenter = yPlotBottom; 155 | float circleOuterRadius = Float.min(plotHeight, plotWidth / 2); 156 | float circleInnerRadius = circleOuterRadius * (1 - dialThickness); 157 | 158 | // stop if the dial is too small 159 | if(circleOuterRadius < 0) 160 | return handler; 161 | 162 | if(showReadingLabel && lastSampleNumber >= 0) { 163 | readingLabel = ChartUtils.formattedNumber(sample, 6) + " " + dataset.unit; 164 | readingLabelWidth = OpenGL.largeTextWidth(gl, readingLabel); 165 | xReadingLabelLeft = xCircleCenter - (readingLabelWidth / 2); 166 | yReadingLabelBaseline = yPlotBottom; 167 | yReadingLabelTop = yReadingLabelBaseline + OpenGL.largeTextHeight; 168 | readingLabelRadius = (float) Math.sqrt((readingLabelWidth / 2) * (readingLabelWidth / 2) + (yReadingLabelTop - yCircleCenter) * (yReadingLabelTop - yCircleCenter)); 169 | 170 | if(readingLabelRadius + Theme.tickTextPadding < circleInnerRadius) 171 | OpenGL.drawLargeText(gl, readingLabel, (int) xReadingLabelLeft, (int) yReadingLabelBaseline, 0); 172 | } 173 | 174 | if(showMinMaxLabels && lastSampleNumber >= 0) { 175 | xMinLabelLeft = xCircleCenter - circleOuterRadius; 176 | xMaxLabelLeft = xCircleCenter + circleOuterRadius - maxLabelWidth; 177 | 178 | if(xMinLabelLeft + minLabelWidth + Theme.tickTextPadding < xMaxLabelLeft - Theme.tickTextPadding) { 179 | OpenGL.drawSmallText(gl, minLabel, (int) xMinLabelLeft, (int) yMinMaxLabelsBaseline, 0); 180 | OpenGL.drawSmallText(gl, maxLabel, (int) xMaxLabelLeft, (int) yMinMaxLabelsBaseline, 0); 181 | } 182 | } 183 | 184 | if(showDatasetLabel && lastSampleNumber >= 0) { 185 | datasetLabel = dataset.name; 186 | datasetLabelWidth = OpenGL.largeTextWidth(gl, datasetLabel); 187 | yDatasetLabelBaseline = showReadingLabel ? yReadingLabelTop + Theme.tickTextPadding + Theme.legendTextPadding : yPlotBottom; 188 | yDatasetLabelTop = yDatasetLabelBaseline + OpenGL.largeTextHeight; 189 | xDatasetLabelLeft = xCircleCenter - (datasetLabelWidth / 2); 190 | datasetLabelRadius = (float) Math.sqrt((datasetLabelWidth / 2) * (datasetLabelWidth / 2) + (yDatasetLabelTop - yCircleCenter) * (yDatasetLabelTop - yCircleCenter)) + Theme.legendTextPadding; 191 | 192 | if(datasetLabelRadius + Theme.tickTextPadding < circleInnerRadius) { 193 | float xMouseoverLeft = xDatasetLabelLeft - Theme.legendTextPadding; 194 | float xMouseoverRight = xDatasetLabelLeft + datasetLabelWidth + Theme.legendTextPadding; 195 | float yMouseoverBottom = yDatasetLabelBaseline - Theme.legendTextPadding; 196 | float yMouseoverTop = yDatasetLabelTop + Theme.legendTextPadding; 197 | if(mouseX >= xMouseoverLeft && mouseX <= xMouseoverRight && mouseY >= yMouseoverBottom && mouseY <= yMouseoverTop) { 198 | OpenGL.drawQuad2D(gl, Theme.legendBackgroundColor, xMouseoverLeft, yMouseoverBottom, xMouseoverRight, yMouseoverTop); 199 | OpenGL.drawQuadOutline2D(gl, Theme.tickLinesColor, xMouseoverLeft, yMouseoverBottom, xMouseoverRight, yMouseoverTop); 200 | handler = EventHandler.onPress(event -> ConfigureView.instance.forDataset(dataset)); 201 | } 202 | OpenGL.drawLargeText(gl, datasetLabel, (int) xDatasetLabelLeft, (int) yDatasetLabelBaseline, 0); 203 | } 204 | } 205 | 206 | // draw the dial 207 | float dialPercentage = (sample - dialMin) / (dialMax - dialMin); 208 | OpenGL.buffer.rewind(); 209 | for(float angle = 0; angle < Math.PI; angle += Math.PI / dialResolution) { 210 | 211 | float x1 = -1f * circleOuterRadius * (float) Math.cos(angle) + xCircleCenter; // top-left 212 | float y1 = circleOuterRadius * (float) Math.sin(angle) + yCircleCenter; 213 | float x2 = -1f * circleOuterRadius * (float) Math.cos(angle + Math.PI / dialResolution) + xCircleCenter; // top-right 214 | float y2 = circleOuterRadius * (float) Math.sin(angle + Math.PI / dialResolution) + yCircleCenter; 215 | float x4 = -1f * circleOuterRadius * (1 - dialThickness) * (float) Math.cos(angle) + xCircleCenter; // bottom-left 216 | float y4 = circleOuterRadius * (1 - dialThickness) * (float) Math.sin(angle) + yCircleCenter; 217 | float x3 = -1f * circleOuterRadius * (1 - dialThickness) * (float) Math.cos(angle + Math.PI / dialResolution) + xCircleCenter; // bottom-right 218 | float y3 = circleOuterRadius * (1 - dialThickness) * (float) Math.sin(angle + Math.PI / dialResolution) + yCircleCenter; 219 | 220 | float[] color = angle >= Math.PI * dialPercentage ? Theme.plotBackgroundColor : dataset.glColor; 221 | OpenGL.buffer.put(x1); OpenGL.buffer.put(y1); OpenGL.buffer.put(color); 222 | OpenGL.buffer.put(x2); OpenGL.buffer.put(y2); OpenGL.buffer.put(color); 223 | OpenGL.buffer.put(x4); OpenGL.buffer.put(y4); OpenGL.buffer.put(color); 224 | 225 | OpenGL.buffer.put(x4); OpenGL.buffer.put(y4); OpenGL.buffer.put(color); 226 | OpenGL.buffer.put(x2); OpenGL.buffer.put(y2); OpenGL.buffer.put(color); 227 | OpenGL.buffer.put(x3); OpenGL.buffer.put(y3); OpenGL.buffer.put(color); 228 | 229 | } 230 | OpenGL.buffer.rewind(); 231 | OpenGL.drawTrianglesXYRGBA(gl, GL3.GL_TRIANGLES, OpenGL.buffer, 6 * dialResolution); 232 | 233 | return handler; 234 | 235 | } 236 | 237 | } 238 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/OpenGLQuaternionChart.java: -------------------------------------------------------------------------------- 1 | import java.nio.FloatBuffer; 2 | import java.util.Arrays; 3 | 4 | import com.jogamp.opengl.GL2ES3; 5 | import com.jogamp.opengl.GL3; 6 | import com.jogamp.opengl.math.Quaternion; 7 | 8 | /** 9 | * Renders a 3D object that is rotated based on a quaternion. 10 | * 11 | * User settings: 12 | * Four quaternion datasets. 13 | * The quaternion (as text) can be displayed. 14 | */ 15 | public class OpenGLQuaternionChart extends PositionedChart { 16 | 17 | FloatBuffer shape; // triangles: x1,y1,z1,u1,v1,w1,... 18 | 19 | // plot region 20 | float xPlotLeft; 21 | float xPlotRight; 22 | float plotWidth; 23 | float yPlotTop; 24 | float yPlotBottom; 25 | float plotHeight; 26 | 27 | // text label 28 | boolean showTextLabel; 29 | String textLabel; 30 | float yTextLabelBaseline; 31 | float yTextLabelTop; 32 | float xTextLabelLeft; 33 | float xTextLabelRight; 34 | 35 | // control widgets 36 | WidgetDatasets datasetsWidget; 37 | WidgetCheckbox showTextLabelWidget; 38 | 39 | @Override public String toString() { 40 | 41 | return "Quaternion"; 42 | 43 | } 44 | 45 | public OpenGLQuaternionChart(int x1, int y1, int x2, int y2) { 46 | 47 | super(x1, y1, x2, y2); 48 | 49 | duration = 1; 50 | 51 | shape = ChartUtils.getShapeFromAsciiStl(getClass().getResourceAsStream("monkey.stl")); 52 | 53 | // create the control widgets and event handlers 54 | datasetsWidget = new WidgetDatasets(newDatasets -> datasets.setNormals(newDatasets), 55 | null, 56 | null, 57 | null, 58 | false, 59 | new String[] {"Q0", "Q1", "Q2", "Q3"}); 60 | 61 | showTextLabelWidget = new WidgetCheckbox("Show Text Label", 62 | true, 63 | newShowTextLabel -> showTextLabel = newShowTextLabel); 64 | 65 | widgets = new Widget[3]; 66 | 67 | widgets[0] = datasetsWidget; 68 | widgets[1] = null; 69 | widgets[2] = showTextLabelWidget; 70 | 71 | } 72 | 73 | @Override public EventHandler drawChart(GL2ES3 gl, float[] chartMatrix, int width, int height, long endTimestamp, int endSampleNumber, double zoomLevel, int mouseX, int mouseY) { 74 | 75 | // sanity check 76 | if(datasets.normalsCount() != 4) 77 | return null; 78 | 79 | // determine which sample to use 80 | int lastSampleNumber = datasets.connection.getSampleCount() - 1; 81 | if(endSampleNumber < lastSampleNumber) 82 | lastSampleNumber = endSampleNumber; 83 | 84 | // get the quaternion values 85 | float[] q = new float[4]; 86 | for(int i = 0; i < 4; i++) { 87 | Dataset dataset = datasets.getNormal(i); 88 | q[i] = lastSampleNumber < 0 ? 0 : datasets.getSample(dataset, lastSampleNumber); 89 | } 90 | 91 | // calculate x and y positions of everything 92 | xPlotLeft = Theme.tilePadding; 93 | xPlotRight = width - Theme.tilePadding; 94 | plotWidth = xPlotRight - xPlotLeft; 95 | yPlotBottom = Theme.tilePadding; 96 | yPlotTop = height - Theme.tilePadding; 97 | plotHeight = yPlotTop - yPlotBottom; 98 | 99 | if(showTextLabel) { 100 | textLabel = String.format("Quaternion (%+1.3f,%+1.3f,%+1.3f,%+1.3f)", q[0], q[1], q[2], q[3]); 101 | yTextLabelBaseline = Theme.tilePadding; 102 | yTextLabelTop = yTextLabelBaseline + OpenGL.largeTextHeight; 103 | xTextLabelLeft = (width / 2f) - (OpenGL.largeTextWidth(gl, textLabel) / 2f); 104 | xTextLabelRight = xTextLabelLeft + OpenGL.largeTextWidth(gl, textLabel); 105 | 106 | yPlotBottom = yTextLabelTop + Theme.tickTextPadding; 107 | yPlotTop = height - Theme.tilePadding; 108 | plotHeight = yPlotTop - yPlotBottom; 109 | } 110 | 111 | // make the plot square so it doesn't stretch the 3D shape 112 | if(plotWidth > plotHeight) { 113 | float delta = plotWidth - plotHeight; 114 | xPlotLeft += delta / 2; 115 | xPlotRight -= delta / 2; 116 | plotWidth = xPlotRight - xPlotLeft; 117 | } else if(plotHeight > plotWidth) { 118 | float delta = plotHeight - plotWidth; 119 | yPlotBottom += delta / 2; 120 | yPlotTop -= delta / 2; 121 | plotHeight = yPlotTop - yPlotBottom; 122 | } 123 | 124 | float[] quatMatrix = new float[16]; 125 | new Quaternion(q[1], q[2], q[3], q[0]).toMatrix(quatMatrix, 0); // x,y,z,w 126 | 127 | // adjust the modelview matrix to map the vertices' local space (-1 to +1) into chart space 128 | // x = x * (plotWidth / 2) + (plotWidth / 2) 129 | // y = y * (plotHeight / 2) + (plotHeight / 2) 130 | // z = z * (plotHeight / 2) + (plotHeight / 2) 131 | float[] modelMatrix = Arrays.copyOf(chartMatrix, 16); 132 | OpenGL.translateMatrix(modelMatrix, (plotWidth/2f) + xPlotLeft, (plotHeight/2f) + yPlotBottom, (plotHeight/2f)); 133 | OpenGL.scaleMatrix (modelMatrix, (plotWidth/2f), (plotHeight/2f), (plotHeight/2f)); 134 | 135 | // rotate the camera 136 | OpenGL.rotateMatrix(modelMatrix, 180, 0, 0, 1); 137 | OpenGL.rotateMatrix(modelMatrix, 90, 1, 0, 0); 138 | 139 | // apply the quaternion rotation 140 | OpenGL.multiplyMatrix(modelMatrix, quatMatrix); 141 | 142 | // invert direction of x-axis 143 | OpenGL.rotateMatrix(modelMatrix, 180, 1, 0, 0); 144 | 145 | // swap x and z axes 146 | OpenGL.rotateMatrix(modelMatrix, 90, 0, 0, 1); 147 | 148 | // draw the monkey 149 | OpenGL.useMatrix(gl, modelMatrix); 150 | OpenGL.drawTrianglesXYZUVW(gl, GL3.GL_TRIANGLES, shape.position(0), shape.capacity() / 6); 151 | 152 | OpenGL.useMatrix(gl, chartMatrix); 153 | 154 | // draw the text, on top of a background quad, if there is room 155 | if(showTextLabel && OpenGL.largeTextWidth(gl, textLabel) < width - Theme.tilePadding * 2) { 156 | OpenGL.drawQuad2D(gl, Theme.tileShadowColor, xTextLabelLeft - Theme.tickTextPadding, yTextLabelBaseline - Theme.tickTextPadding, xTextLabelRight + Theme.tickTextPadding, yTextLabelTop + Theme.tickTextPadding); 157 | OpenGL.drawLargeText(gl, textLabel, (int) xTextLabelLeft, (int) yTextLabelBaseline, 0); 158 | } 159 | 160 | return null; 161 | 162 | } 163 | 164 | } 165 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/Plot.java: -------------------------------------------------------------------------------- 1 | import java.util.Map; 2 | 3 | import com.jogamp.opengl.GL2ES3; 4 | 5 | public abstract class Plot { 6 | 7 | DatasetsInterface datasets; 8 | long maxSampleNumber; 9 | long minSampleNumber; 10 | long plotSampleCount; 11 | long plotMaxX; // sample number or unix timestamp 12 | long plotMinX; // sample number or unix timestamp 13 | long plotDomain; // sample count or milliseconds 14 | float samplesMinY; // of the samples, not necessarily of the plot 15 | float samplesMaxY; // of the samples, not necessarily of the plot 16 | String xAxisTitle = ""; 17 | BitfieldEvents events; 18 | boolean cachedMode; 19 | 20 | /** 21 | * Step 1: (Required) Calculate the domain and range of the plot. 22 | * 23 | * @param endTimestamp Timestamp corresponding with the right edge of a time-domain plot. NOTE: this might be in the future! 24 | * @param endSampleNumber Sample number corresponding with the right edge of a time-domain plot. NOTE: this sample might not exist yet! 25 | * @param zoomLevel Current zoom level. 1.0 = no zoom. 26 | * @param datasets Normal/edge/level datasets to acquire from. 27 | * @param timestampCache Place to cache timestamps. 28 | * @param duration The sample count, before applying the zoom factor. 29 | * @param cachedMode True to enable the cache. 30 | * @param showTimestamps True if the x-axis shows timestamps, false if the x-axis shows sample count or elapsed time. 31 | */ 32 | abstract void initialize(long endTimestamp, long endSampleNumber, double zoomLevel, DatasetsInterface datasets, StorageTimestamps.Cache timestampsCache, long duration, boolean cachedMode, boolean showTimestamps); 33 | 34 | /** 35 | * Step 2: Get the required range, assuming you want to see all samples on screen. 36 | * 37 | * @return The minimum and maximum Y-axis values. 38 | */ 39 | final StorageFloats.MinMax getRange() { return new StorageFloats.MinMax(samplesMinY, samplesMaxY); } 40 | 41 | /** 42 | * Step 3: Get the x-axis title. 43 | * 44 | * @return The x-axis title. 45 | */ 46 | final String getTitle() { return xAxisTitle; } 47 | 48 | /** 49 | * Step 4: Get the x-axis divisions. 50 | * 51 | * @param gl The OpenGL context. 52 | * @param plotWidth The width of the plot region, in pixels. 53 | * @return A Map where each value is a string to draw on screen, and each key is the pixelX location for it (0 = left edge of the plot) 54 | */ 55 | abstract Map getXdivisions(GL2ES3 gl, float plotWidth); 56 | 57 | /** 58 | * Step 5: Acquire the samples. 59 | * If you will call draw(), you must call this before it. 60 | * 61 | * @param plotMinY Y-axis value at the bottom of the plot. 62 | * @param plotMaxY Y-axis value at the top of the plot. 63 | * @param plotWidth Width of the plot region, in pixels. 64 | * @param plotHeight Height of the plot region, in pixels. 65 | */ 66 | final void acquireSamples(float plotMinY, float plotMaxY, int plotWidth, int plotHeight) { 67 | 68 | if(plotSampleCount < 2) 69 | return; 70 | 71 | if(cachedMode) 72 | acquireSamplesCachedMode(plotMinY, plotMaxY, plotWidth, plotHeight); 73 | else 74 | acquireSamplesNonCachedMode(plotMinY, plotMaxY, plotWidth, plotHeight); 75 | 76 | } 77 | abstract void acquireSamplesCachedMode (float plotMinY, float plotMaxY, int plotWidth, int plotHeight); 78 | abstract void acquireSamplesNonCachedMode(float plotMinY, float plotMaxY, int plotWidth, int plotHeight); 79 | 80 | /** 81 | * Step 6: Render the plot on screen. 82 | * 83 | * @param gl The OpenGL context. 84 | * @param chartMatrix The current 4x4 matrix. 85 | * @param xPlotLeft Bottom-left corner location, in pixels. 86 | * @param yPlotBottom Bottom-left corner location, in pixels. 87 | * @param plotWidth Width of the plot region, in pixels. 88 | * @param plotHeight Height of the plot region, in pixels. 89 | * @param plotMinY Y-axis value at the bottom of the plot. 90 | * @param plotMaxY Y-axis value at the top of the plot. 91 | */ 92 | final void draw(GL2ES3 gl, float[] chartMatrix, int xPlotLeft, int yPlotBottom, int plotWidth, int plotHeight, float plotMinY, float plotMaxY) { 93 | 94 | if(plotSampleCount < 2) 95 | return; 96 | 97 | if(cachedMode) 98 | drawCachedMode(gl, chartMatrix, xPlotLeft, yPlotBottom, plotWidth, plotHeight, plotMinY, plotMaxY); 99 | else 100 | drawNonCachedMode(gl, chartMatrix, xPlotLeft, yPlotBottom, plotWidth, plotHeight, plotMinY, plotMaxY); 101 | 102 | } 103 | abstract void drawCachedMode (GL2ES3 gl, float[] chartMatrix, int xPlotLeft, int yPlotBottom, int plotWidth, int plotHeight, float plotMinY, float plotMaxY); 104 | abstract void drawNonCachedMode(GL2ES3 gl, float[] chartMatrix, int xPlotLeft, int yPlotBottom, int plotWidth, int plotHeight, float plotMinY, float plotMaxY); 105 | 106 | /** 107 | * Step 7: Check if a tooltip should be drawn for the mouse's current location. 108 | * 109 | * @param mouseX The mouse's location along the x-axis, in pixels (0 = left edge of the plot) 110 | * @param plotWidth Width of the plot region, in pixels. 111 | * @return An object indicating if the tooltip should be drawn, for what sample number, with what label, and at what location on screen. 112 | */ 113 | abstract TooltipInfo getTooltip(int mouseX, float plotWidth); 114 | 115 | /** 116 | * Gets the horizontal location, relative to the plot, for a sample number. 117 | * 118 | * @param sampleNumber The sample number. 119 | * @param plotWidth Width of the plot region, in pixels. 120 | * @return Corresponding horizontal location on the plot, in pixels, with 0 = left edge of the plot. 121 | */ 122 | abstract float getPixelXforSampleNumber(long sampleNumber, float plotWidth); 123 | 124 | /** 125 | * @return Domain (interval of x-axis values) of the plot. 126 | */ 127 | final long getPlotDomain() { return plotDomain; } 128 | 129 | static class TooltipInfo { 130 | 131 | boolean draw; 132 | int sampleNumber; 133 | String label; 134 | float pixelX; 135 | 136 | TooltipInfo(boolean draw, long sampleNumber, String label, float pixelX) { 137 | this.draw = draw; 138 | this.sampleNumber = (int) sampleNumber; 139 | this.label = label; 140 | this.pixelX = pixelX; 141 | } 142 | 143 | } 144 | 145 | abstract public void freeResources(GL2ES3 gl); 146 | 147 | } 148 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/PositionedChart.java: -------------------------------------------------------------------------------- 1 | import java.util.ArrayList; 2 | import java.util.List; 3 | import com.jogamp.opengl.GL2ES3; 4 | import com.jogamp.opengl.GL3; 5 | 6 | public abstract class PositionedChart { 7 | 8 | // grid coordinates, not pixels 9 | int topLeftX; 10 | int topLeftY; 11 | int bottomRightX; 12 | int bottomRightY; 13 | 14 | int duration; 15 | boolean sampleCountMode; 16 | 17 | Widget[] widgets; 18 | DatasetsInterface datasets = new DatasetsInterface(); 19 | 20 | public PositionedChart(int x1, int y1, int x2, int y2) { 21 | 22 | topLeftX = x1 < x2 ? x1 : x2; 23 | topLeftY = y1 < y2 ? y1 : y2; 24 | bottomRightX = x2 > x1 ? x2 : x1; 25 | bottomRightY = y2 > y1 ? y2 : y1; 26 | sampleCountMode = true; 27 | 28 | } 29 | 30 | public boolean regionOccupied(int startX, int startY, int endX, int endY) { 31 | 32 | if(endX < startX) { 33 | int temp = startX; 34 | startX = endX; 35 | endX = temp; 36 | } 37 | if(endY < startY) { 38 | int temp = startY; 39 | startY = endY; 40 | endY = temp; 41 | } 42 | 43 | for(int x = startX; x <= endX; x++) 44 | for(int y = startY; y <= endY; y++) 45 | if(x >= topLeftX && x <= bottomRightX && y >= topLeftY && y <= bottomRightY) 46 | return true; 47 | 48 | return false; 49 | 50 | } 51 | 52 | long cpuStartNanoseconds; 53 | long cpuStopNanoseconds; 54 | double previousCpuMilliseconds; 55 | double previousGpuMilliseconds; 56 | double cpuMillisecondsAccumulator; 57 | double gpuMillisecondsAccumulator; 58 | int count; 59 | final int SAMPLE_COUNT = 60; 60 | double averageCpuMilliseconds; 61 | double averageGpuMilliseconds; 62 | int[] gpuQueryHandles; 63 | long[] gpuTimes = new long[2]; 64 | String line1; 65 | String line2; 66 | 67 | public final EventHandler draw(GL2ES3 gl, float[] chartMatrix, int width, int height, long nowTimestamp, int lastSampleNumber, double zoomLevel, int mouseX, int mouseY) { 68 | 69 | boolean openGLES = OpenGLChartsView.instance.openGLES; 70 | if(!openGLES && gpuQueryHandles == null) { 71 | gpuQueryHandles = new int[2]; 72 | gl.glGenQueries(2, gpuQueryHandles, 0); 73 | gl.glQueryCounter(gpuQueryHandles[0], GL3.GL_TIMESTAMP); // insert both queries to prevent a warning on the first time they are read 74 | gl.glQueryCounter(gpuQueryHandles[1], GL3.GL_TIMESTAMP); 75 | } 76 | 77 | // if benchmarking, calculate CPU/GPU time for the *previous frame* 78 | // GPU benchmarking is not possible with OpenGL ES 79 | if(SettingsController.getBenchmarking()) { 80 | previousCpuMilliseconds = (cpuStopNanoseconds - cpuStartNanoseconds) / 1000000.0; 81 | if(!openGLES) { 82 | gl.glGetQueryObjecti64v(gpuQueryHandles[0], GL3.GL_QUERY_RESULT, gpuTimes, 0); 83 | gl.glGetQueryObjecti64v(gpuQueryHandles[1], GL3.GL_QUERY_RESULT, gpuTimes, 1); 84 | } 85 | previousGpuMilliseconds = (gpuTimes[1] - gpuTimes[0]) / 1000000.0; 86 | if(count < SAMPLE_COUNT) { 87 | cpuMillisecondsAccumulator += previousCpuMilliseconds; 88 | gpuMillisecondsAccumulator += previousGpuMilliseconds; 89 | count++; 90 | } else { 91 | averageCpuMilliseconds = cpuMillisecondsAccumulator / 60.0; 92 | averageGpuMilliseconds = gpuMillisecondsAccumulator / 60.0; 93 | cpuMillisecondsAccumulator = 0; 94 | gpuMillisecondsAccumulator = 0; 95 | count = 0; 96 | } 97 | 98 | // start timers for *this frame* 99 | cpuStartNanoseconds = System.nanoTime(); 100 | if(!openGLES) 101 | gl.glQueryCounter(gpuQueryHandles[0], GL3.GL_TIMESTAMP); 102 | } 103 | 104 | // draw the chart 105 | EventHandler handler = drawChart(gl, chartMatrix, width, height, nowTimestamp, lastSampleNumber, zoomLevel, mouseX, mouseY); 106 | 107 | // if benchmarking, draw the CPU/GPU benchmarks over this chart 108 | // GPU benchmarking is not possible with OpenGL ES 109 | if(SettingsController.getBenchmarking()) { 110 | // stop timers for *this frame* 111 | cpuStopNanoseconds = System.nanoTime(); 112 | if(!openGLES) 113 | gl.glQueryCounter(gpuQueryHandles[1], GL3.GL_TIMESTAMP); 114 | 115 | // show times of *previous frame* 116 | line1 = String.format("CPU = %.3fms (Average = %.3fms)", previousCpuMilliseconds, averageCpuMilliseconds); 117 | line2 = !openGLES ? String.format("GPU = %.3fms (Average = %.3fms)", previousGpuMilliseconds, averageGpuMilliseconds) : 118 | "GPU = unknown"; 119 | float textHeight = 2 * OpenGL.smallTextHeight + Theme.tickTextPadding; 120 | float textWidth = Float.max(OpenGL.smallTextWidth(gl, line1), OpenGL.smallTextWidth(gl, line2)); 121 | OpenGL.drawBox(gl, Theme.neutralColor, Theme.tileShadowOffset, 0, textWidth + Theme.tickTextPadding*2, textHeight + Theme.tickTextPadding*2); 122 | OpenGL.drawSmallText(gl, line1, (int) (Theme.tickTextPadding + Theme.tileShadowOffset), (int) (2 * Theme.tickTextPadding + OpenGL.smallTextHeight), 0); 123 | OpenGL.drawSmallText(gl, line2, (int) (Theme.tickTextPadding + Theme.tileShadowOffset), (int) Theme.tickTextPadding, 0); 124 | } 125 | 126 | return handler; 127 | 128 | } 129 | 130 | /** 131 | * Draws the chart on screen. 132 | * 133 | * @param gl The OpenGL context. 134 | * @param chartMatrix The 4x4 matrix to use. 135 | * @param width Width of the chart, in pixels. 136 | * @param height Height of the chart, in pixels. 137 | * @param endTimestamp Timestamp corresponding with the right edge of a time-domain plot. NOTE: this might be in the future! 138 | * @param endSampleNumber Sample number corresponding with the right edge of a time-domain plot. NOTE: this sample might not exist yet! 139 | * @param zoomLevel Requested zoom level. 140 | * @param mouseX Mouse's x position, in pixels, relative to the chart. 141 | * @param mouseY Mouse's y position, in pixels, relative to the chart. 142 | * @return An EventHandler if the mouse is over something that can be clicked or dragged. 143 | */ 144 | public abstract EventHandler drawChart(GL2ES3 gl, float[] chartMatrix, int width, int height, long endTimestamp, int endSampleNumber, double zoomLevel, int mouseX, int mouseY); 145 | 146 | public final void importChart(ConnectionsController.QueueOfLines lines) { 147 | 148 | for(Widget widget : widgets) 149 | if(widget != null) 150 | widget.importState(lines); 151 | 152 | } 153 | 154 | final public List exportChart() { 155 | 156 | List lines = new ArrayList(); 157 | 158 | for(Widget widget : widgets) 159 | if(widget != null) 160 | for(String line : widget.exportState()) 161 | lines.add(line); 162 | 163 | return lines; 164 | 165 | } 166 | 167 | public abstract String toString(); 168 | 169 | /** 170 | * Schedules the chart to be disposed. 171 | * Non-GPU resources (cache files, etc.) will be released immediately. 172 | * GPU resources will be released the next time the OpenGLChartsRegion is drawn. (the next vsync, if it's on screen.) 173 | */ 174 | final public void dispose() { 175 | 176 | disposeNonGpu(); 177 | OpenGLChartsView.instance.chartsToDispose.add(this); 178 | 179 | } 180 | 181 | /** 182 | * Charts that create cache files or other non-GPU resources must dispose of them when this method is called. 183 | * The chart may be drawn after this call, so the chart must be able to automatically regenerate any needed caches. 184 | */ 185 | public void disposeNonGpu() { 186 | 187 | } 188 | 189 | /** 190 | * Charts that create any OpenGL FBOs/textures/etc. must dispose of them when this method is called. 191 | * The chart may be drawn after this call, so the chart must be able to automatically regenerate any needed FBOs/textures/etc. 192 | * 193 | * @param gl The OpenGL context. 194 | */ 195 | public void disposeGpu(GL2ES3 gl) { 196 | 197 | if(gpuQueryHandles != null) { 198 | gl.glDeleteQueries(2, gpuQueryHandles, 0); 199 | gpuQueryHandles = null; 200 | } 201 | 202 | } 203 | 204 | } -------------------------------------------------------------------------------- /Telemetry Viewer/src/SettingsView.java: -------------------------------------------------------------------------------- 1 | import java.awt.Dimension; 2 | import java.awt.event.ActionListener; 3 | import java.awt.event.FocusEvent; 4 | import java.awt.event.FocusListener; 5 | import java.util.ArrayList; 6 | import java.util.Hashtable; 7 | import java.util.List; 8 | 9 | import javax.swing.JButton; 10 | import javax.swing.JCheckBox; 11 | import javax.swing.JComboBox; 12 | import javax.swing.JLabel; 13 | import javax.swing.JPanel; 14 | import javax.swing.JScrollPane; 15 | import javax.swing.JSlider; 16 | import javax.swing.JTextField; 17 | import net.miginfocom.swing.MigLayout; 18 | 19 | /** 20 | * The SettingsView and SettingsController classes form the MVC that manage GUI-related settings. 21 | * Settings can be changed when the user interacts with the GUI or opens a Settings file. 22 | * This class is the GUI that is optionally shown at the left side of the screen. 23 | * The transmit GUIs are also drawn here because it's convenient to have them on the left side of the screen. 24 | */ 25 | @SuppressWarnings("serial") 26 | public class SettingsView extends JPanel { 27 | 28 | public static SettingsView instance = new SettingsView(); 29 | 30 | private boolean isVisible = true; 31 | private JScrollPane scrollablePanel; 32 | private JPanel panel; 33 | private List txGuis = new ArrayList(); 34 | 35 | JTextField tileColumnsTextfield; 36 | JTextField tileRowsTextfield; 37 | 38 | JComboBox timeFormatCombobox; 39 | JCheckBox timeFormat24hoursCheckbox; 40 | 41 | JCheckBox hintNotificationsCheckbox; 42 | JButton hintNotificationsColorButton; 43 | JCheckBox warningNotificationsCheckbox; 44 | JButton warningNotificationsColorButton; 45 | JCheckBox failureNotificationsCheckbox; 46 | JButton failureNotificationsColorButton; 47 | JCheckBox verboseNotificationsCheckbox; 48 | JButton verboseNotificationsColorButton; 49 | 50 | JCheckBox showTooltipsCheckbox; 51 | JCheckBox enableSmoothScrollingCheckbox; 52 | JSlider antialiasingLevelSlider; 53 | JCheckBox showFpsCheckbox; 54 | JCheckBox showBenchmarksCheckbox; 55 | 56 | /** 57 | * Private constructor to enforce singleton usage. 58 | */ 59 | private SettingsView() { 60 | 61 | super(); 62 | 63 | setLayout(new MigLayout("wrap 1, insets 0, filly")); // 1 column, no border 64 | panel = new JPanel(); 65 | panel.setLayout(new MigLayout("hidemode 3, wrap 2, insets" + Theme.padding + " " + Theme.padding + " " + Theme.padding + " 0, gap " + Theme.padding)); 66 | scrollablePanel = new JScrollPane(panel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); 67 | scrollablePanel.setBorder(null); 68 | scrollablePanel.getVerticalScrollBar().setUnitIncrement(10); 69 | add(scrollablePanel, "grow"); 70 | 71 | // tile columns and rows 72 | tileColumnsTextfield = new JTextField(Integer.toString(SettingsController.getTileColumns())); 73 | ActionListener updateTileColumns = event -> { 74 | try { 75 | SettingsController.setTileColumns(Integer.parseInt(tileColumnsTextfield.getText().trim())); 76 | } catch(Exception e) { 77 | tileColumnsTextfield.setText(Integer.toString(SettingsController.getTileColumns())); 78 | } 79 | }; 80 | tileColumnsTextfield.addActionListener(updateTileColumns); 81 | tileColumnsTextfield.addFocusListener(new FocusListener() { 82 | @Override public void focusLost(FocusEvent fe) { updateTileColumns.actionPerformed(null); } 83 | @Override public void focusGained(FocusEvent fe) { tileColumnsTextfield.selectAll();} 84 | }); 85 | 86 | tileRowsTextfield = new JTextField(Integer.toString(SettingsController.getTileRows())); 87 | ActionListener updateTileRows = event -> { 88 | try { 89 | SettingsController.setTileRows(Integer.parseInt(tileRowsTextfield.getText().trim())); 90 | } catch(Exception e) { 91 | tileRowsTextfield.setText(Integer.toString(SettingsController.getTileRows())); 92 | } 93 | }; 94 | tileRowsTextfield.addActionListener(updateTileRows); 95 | tileRowsTextfield.addFocusListener(new FocusListener() { 96 | @Override public void focusLost(FocusEvent fe) { updateTileRows.actionPerformed(null); } 97 | @Override public void focusGained(FocusEvent fe) { tileRowsTextfield.selectAll(); } 98 | }); 99 | 100 | // time format 101 | timeFormatCombobox = new JComboBox(SettingsController.getTimeFormats()); 102 | String format = SettingsController.getTimeFormat(); 103 | for(int i = 0; i < timeFormatCombobox.getItemCount(); i++) 104 | if(timeFormatCombobox.getItemAt(i).equals(format)) 105 | timeFormatCombobox.setSelectedIndex(i); 106 | timeFormatCombobox.addActionListener(event -> SettingsController.setTimeFormat(timeFormatCombobox.getSelectedItem().toString())); 107 | timeFormat24hoursCheckbox = new JCheckBox("Show 24-Hour Time", SettingsController.getTimeFormat24hours()); 108 | timeFormat24hoursCheckbox.addActionListener(event -> SettingsController.setTimeFormat24hours(timeFormat24hoursCheckbox.isSelected())); 109 | 110 | // notifications 111 | hintNotificationsCheckbox = new JCheckBox("Show Hint Notifications", SettingsController.getHintNotificationVisibility()); 112 | hintNotificationsCheckbox.addActionListener(event -> SettingsController.setHintNotificationVisibility(hintNotificationsCheckbox.isSelected())); 113 | hintNotificationsColorButton = new JButton("\u25B2"); 114 | hintNotificationsColorButton.setForeground(SettingsController.getHintNotificationColor()); 115 | hintNotificationsColorButton.addActionListener(event -> SettingsController.setHintNotificationColor(ColorPickerView.getColor("Hint Notifications", SettingsController.getHintNotificationColor(), false))); 116 | 117 | warningNotificationsCheckbox = new JCheckBox("Show Warning Notifications", SettingsController.getWarningNotificationVisibility()); 118 | warningNotificationsCheckbox.addActionListener(event -> SettingsController.setWarningNotificationVisibility(warningNotificationsCheckbox.isSelected())); 119 | warningNotificationsColorButton = new JButton("\u25B2"); 120 | warningNotificationsColorButton.setForeground(SettingsController.getWarningNotificationColor()); 121 | warningNotificationsColorButton.addActionListener(event -> SettingsController.setWarningNotificationColor(ColorPickerView.getColor("Warning Notifications", SettingsController.getWarningNotificationColor(), false))); 122 | 123 | failureNotificationsCheckbox = new JCheckBox("Show Failure Notifications", SettingsController.getFailureNotificationVisibility()); 124 | failureNotificationsCheckbox.addActionListener(event -> SettingsController.setFailureNotificationVisibility(failureNotificationsCheckbox.isSelected())); 125 | failureNotificationsColorButton = new JButton("\u25B2"); 126 | failureNotificationsColorButton.setForeground(SettingsController.getFailureNotificationColor()); 127 | failureNotificationsColorButton.addActionListener(event -> SettingsController.setFailureNotificationColor(ColorPickerView.getColor("Failure Notifications", SettingsController.getFailureNotificationColor(), false))); 128 | 129 | verboseNotificationsCheckbox = new JCheckBox("Show Verbose Notifications", SettingsController.getVerboseNotificationVisibility()); 130 | verboseNotificationsCheckbox.addActionListener(event -> SettingsController.setVerboseNotificationVisibility(verboseNotificationsCheckbox.isSelected())); 131 | verboseNotificationsColorButton = new JButton("\u25B2"); 132 | verboseNotificationsColorButton.setForeground(SettingsController.getVerboseNotificationColor()); 133 | verboseNotificationsColorButton.addActionListener(event -> SettingsController.setVerboseNotificationColor(ColorPickerView.getColor("Verbose Notifications", SettingsController.getVerboseNotificationColor(), false))); 134 | 135 | // tooltips 136 | showTooltipsCheckbox = new JCheckBox("Show Plot Tooltips", SettingsController.getTooltipVisibility()); 137 | showTooltipsCheckbox.addActionListener(event -> SettingsController.setTooltipVisibility(showTooltipsCheckbox.isSelected())); 138 | 139 | // logitech smooth scrolling 140 | enableSmoothScrollingCheckbox = new JCheckBox("Enable Logitech Smooth Scrolling", SettingsController.getSmoothScrolling()); 141 | enableSmoothScrollingCheckbox.addActionListener(event -> SettingsController.setSmoothScrolling(enableSmoothScrollingCheckbox.isSelected())); 142 | 143 | // FPS 144 | showFpsCheckbox = new JCheckBox("Show FPS and Period", SettingsController.getFpsVisibility()); 145 | showFpsCheckbox.addActionListener(event -> SettingsController.setFpsVisibility(showFpsCheckbox.isSelected())); 146 | 147 | // benchmarking 148 | showBenchmarksCheckbox = new JCheckBox("Show Benchmarks", SettingsController.getBenchmarking()); 149 | showBenchmarksCheckbox.addActionListener(event -> SettingsController.setBenchmarking(showBenchmarksCheckbox.isSelected())); 150 | 151 | // antialiasing 152 | antialiasingLevelSlider = new JSlider(0, 5, (int) (Math.log(SettingsController.getAntialiasingLevel()) / Math.log(2))); 153 | Hashtable labels = new Hashtable(); 154 | labels.put(0, new JLabel("1")); 155 | labels.put(1, new JLabel("2")); 156 | labels.put(2, new JLabel("4")); 157 | labels.put(3, new JLabel("8")); 158 | labels.put(4, new JLabel("16")); 159 | labels.put(5, new JLabel("32")); 160 | antialiasingLevelSlider.setLabelTable(labels); 161 | antialiasingLevelSlider.setMajorTickSpacing(1); 162 | antialiasingLevelSlider.setPaintTicks(true); 163 | antialiasingLevelSlider.setPaintLabels(true); 164 | antialiasingLevelSlider.addChangeListener(event -> SettingsController.setAntialiasingLevel((int) Math.pow(2, antialiasingLevelSlider.getValue()))); 165 | 166 | // actually populating the panel is done in setVisible() because the transmit GUIs may change when connections change 167 | // that also means that setVisible() must be called any time a connection is added/removed/connected/disconnected 168 | 169 | setVisible(false); 170 | 171 | } 172 | 173 | /** 174 | * Shows or hides this panel, and repopulates the panel to ensure everything is in sync. 175 | * 176 | * @param visible True or false. 177 | */ 178 | @Override public void setVisible(boolean visible) { 179 | 180 | // remove all swing widgets 181 | panel.removeAll(); 182 | txGuis.clear(); 183 | 184 | // repopulate the panel 185 | panel.add(new JLabel("Tile Columns: ")); 186 | panel.add(tileColumnsTextfield, "grow x"); 187 | panel.add(new JLabel("Tile Rows: ")); 188 | panel.add(tileRowsTextfield, "grow x, gapbottom " + 4*Theme.padding); 189 | 190 | panel.add(new JLabel("Time Format: ")); 191 | panel.add(timeFormatCombobox); 192 | panel.add(timeFormat24hoursCheckbox, "span 2, grow x, gapbottom " + 4*Theme.padding); 193 | 194 | panel.add(hintNotificationsCheckbox, "split 2, span 2, grow x"); 195 | panel.add(hintNotificationsColorButton); 196 | panel.add(warningNotificationsCheckbox, "split 2, span 2, grow x"); 197 | panel.add(warningNotificationsColorButton); 198 | panel.add(failureNotificationsCheckbox, "split 2, span 2, grow x"); 199 | panel.add(failureNotificationsColorButton); 200 | panel.add(verboseNotificationsCheckbox, "split 2, span 2, grow x"); 201 | panel.add(verboseNotificationsColorButton, "gapbottom " + 4*Theme.padding); 202 | 203 | panel.add(showTooltipsCheckbox, "span 2, grow x"); 204 | panel.add(enableSmoothScrollingCheckbox, "span 2, grow x"); 205 | panel.add(showFpsCheckbox, "span 2, grow x"); 206 | panel.add(showBenchmarksCheckbox, "span 2, grow x"); 207 | 208 | panel.add(new JLabel("Antialiasing: ")); 209 | panel.add(antialiasingLevelSlider, "width 1, grow x, gapbottom " + 4*Theme.padding); // shrink it horizontally 210 | 211 | // if visible, also repopulate the panel with transmit GUIs 212 | if(visible) 213 | ConnectionsController.telemetryConnections.forEach(connection -> { 214 | JPanel txGui = connection.getTransmitPanel(); 215 | if(txGui != null) { 216 | panel.add(txGui, "growx, span 2, gapbottom " + 4*Theme.padding); 217 | txGuis.add(txGui); 218 | } 219 | }); 220 | 221 | isVisible = visible; 222 | revalidate(); 223 | repaint(); 224 | 225 | } 226 | 227 | /** 228 | * @return True if the panel is visible. 229 | */ 230 | @Override public boolean isVisible() { 231 | 232 | return isVisible; 233 | 234 | } 235 | 236 | /** 237 | * Ensures this panel is sized correctly. 238 | */ 239 | @Override public Dimension getPreferredSize() { 240 | 241 | txGuis.forEach(gui -> gui.setVisible(false)); 242 | panel.setPreferredSize(null); 243 | scrollablePanel.setPreferredSize(null); 244 | Dimension emptySize = scrollablePanel.getPreferredSize(); 245 | txGuis.forEach(gui -> gui.setVisible(true)); 246 | Dimension fullSize = scrollablePanel.getPreferredSize(); 247 | Dimension size = new Dimension(Integer.max(emptySize.width, fullSize.width), emptySize.height); 248 | 249 | if(!isVisible) { 250 | 251 | size.width = 0; 252 | return size; 253 | 254 | } else { 255 | 256 | // resize the widgets region if the scrollbar is visible 257 | if(scrollablePanel.getVerticalScrollBar().isVisible()) { 258 | size.width += scrollablePanel.getVerticalScrollBar().getPreferredSize().width; 259 | scrollablePanel.setPreferredSize(size); 260 | size.width += Theme.padding; 261 | } 262 | 263 | return size; 264 | 265 | } 266 | 267 | } 268 | 269 | } 270 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/SharedByteStream.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Inspired by PipedOutputStream/PipedInputStream, but optimized for my use cases. 3 | * This is a thread-safe way to share a buffer of telemetry packets between two threads (one reader and one writer.) 4 | * 5 | * This class supports two different packet modes: CSV (text) and binary packets. 6 | * In CSV mode, a single ring buffer is used. The reader receives a COPY of each line of text. This isn't very efficient, but I don't expect people to use CSV mode for massive data streams. 7 | * In binary mode, two ping-pong buffers are used. The reader receives the ORIGINAL buffer, along with a corresponding offset and byte count. This is much more efficient. 8 | */ 9 | public class SharedByteStream { 10 | 11 | private boolean ready; 12 | private int packetByteCount; 13 | 14 | private byte[][] buffer; // [0 or 1][byteN] 15 | private int bufferSize; 16 | private int[] writeIndex; // [0 or 1] 17 | private int[] readIndex; // [0 or 1] 18 | private int[] occupiedSize; // [0 or 1] 19 | private boolean writeIntoA; 20 | 21 | private ConnectionTelemetry connection; 22 | 23 | /** 24 | * Creates a placeholder for sharing data between one reading thread and one writing thread. 25 | * Before data can be written or read, the setPacketSize() method must be called. 26 | */ 27 | public SharedByteStream(ConnectionTelemetry connection) { 28 | 29 | ready = false; 30 | this.connection = connection; 31 | 32 | } 33 | 34 | /** 35 | * Prepares the buffers to receive data. 36 | * 37 | * @param byteCount Number of bytes per packet (binary mode), or 0 for CSV mode. 38 | */ 39 | public synchronized void setPacketSize(int byteCount) { 40 | 41 | if(byteCount == 0) { 42 | 43 | // CSV mode 44 | bufferSize = 8388608; // 8MB each 45 | buffer = new byte[2][bufferSize]; 46 | writeIndex = new int[] {0, 0}; 47 | readIndex = new int[] {0, 0}; 48 | occupiedSize = new int[] {0, 0}; 49 | 50 | writeIntoA = true; 51 | packetByteCount = 0; 52 | 53 | } else { 54 | 55 | // binary mode 56 | bufferSize = 8388608 + byteCount - 1; // 8MB each + enough room to prepend an incomplete packet 57 | buffer = new byte[2][bufferSize]; 58 | writeIndex = new int[] {byteCount - 1, byteCount - 1}; 59 | readIndex = new int[] {byteCount - 1, byteCount - 1}; 60 | occupiedSize = new int[] {0, 0}; 61 | 62 | writeIntoA = true; 63 | packetByteCount = byteCount; 64 | 65 | } 66 | 67 | ready = true; 68 | 69 | } 70 | 71 | /** 72 | * Appends bytes to the buffer. 73 | * 74 | * @param bytes Data to write. 75 | * @param byteCount Amount of data. 76 | * @throws InterruptedException If the thread is interrupted while waiting for free space in the buffer. 77 | */ 78 | public synchronized void write(byte[] bytes, int byteCount) throws InterruptedException { 79 | 80 | // ignore if the buffers are not ready 81 | if(!ready) 82 | return; 83 | 84 | int writeBuffer = writeIntoA ? 0 : 1; 85 | 86 | // wait for free space if necessary 87 | int availableBufferSpace = bufferSize; 88 | if(packetByteCount != 0) 89 | availableBufferSpace -= packetByteCount - 1; 90 | while(occupiedSize[writeBuffer] + byteCount > availableBufferSpace) { 91 | notifyAll(); 92 | wait(1); 93 | writeBuffer = writeIntoA ? 0 : 1; 94 | } 95 | 96 | // write into the buffer 97 | int startIndex = writeIndex[writeBuffer]; 98 | int endIndex = (writeIndex[writeBuffer] + byteCount - 1) % bufferSize; 99 | if(endIndex >= startIndex) { 100 | // no need to wrap around the ring buffer 101 | System.arraycopy(bytes, 0, buffer[writeBuffer], startIndex, byteCount); 102 | writeIndex[writeBuffer] += byteCount; 103 | occupiedSize[writeBuffer] += byteCount; 104 | } else { 105 | // must wrap around the ring buffer 106 | int firstByteCount = bufferSize - writeIndex[writeBuffer]; 107 | int secondByteCount = byteCount - firstByteCount; 108 | System.arraycopy(bytes, 0, buffer[writeBuffer], startIndex, firstByteCount); 109 | System.arraycopy(bytes, firstByteCount, buffer[writeBuffer], 0, secondByteCount); 110 | writeIndex[writeBuffer] = (endIndex + 1) % bufferSize; 111 | occupiedSize[writeBuffer] += byteCount; 112 | } 113 | 114 | // inform reading thread that new data is available 115 | notifyAll(); 116 | 117 | } 118 | 119 | /** 120 | * Blocks until at least one packet is available. 121 | * 122 | * @return The buffer to read from. 123 | */ 124 | private synchronized int awaitPacket() throws InterruptedException { 125 | 126 | int readBuffer = writeIntoA ? 1 : 0; 127 | int writeBuffer = writeIntoA ? 0 : 1; 128 | 129 | // if this buffer contains <1 complete packet, 130 | // prepend the remaining bytes to the other buffer, 131 | // then wait for the other buffer to contain >=1 packet 132 | // then swap buffers 133 | int remainingByteCount = occupiedSize[readBuffer]; 134 | if(remainingByteCount < packetByteCount) { 135 | if(remainingByteCount > 0) { 136 | for(int i = 0; i < remainingByteCount; i++) { 137 | int writeBufferIndex = packetByteCount - 1 - remainingByteCount + i; 138 | buffer[writeBuffer][writeBufferIndex] = buffer[readBuffer][readIndex[readBuffer]]; 139 | readIndex[readBuffer]++; 140 | } 141 | readIndex[writeBuffer] = packetByteCount - 1 - remainingByteCount; 142 | occupiedSize[writeBuffer] += remainingByteCount; 143 | } 144 | 145 | while(occupiedSize[writeBuffer] < packetByteCount) { 146 | notifyAll(); 147 | wait(1); 148 | } 149 | 150 | writeIndex[readBuffer] = packetByteCount - 1; 151 | readIndex[readBuffer] = packetByteCount - 1; 152 | occupiedSize[readBuffer] = 0; 153 | 154 | writeIntoA = !writeIntoA; 155 | readBuffer = writeIntoA ? 1 : 0; 156 | } 157 | 158 | return readBuffer; 159 | 160 | } 161 | 162 | /** 163 | * Reads at least one binary packet from the buffer. 164 | * 165 | * The returned packets are guaranteed to have correct sync words and valid checksums (if using checksums.) 166 | * This method will provide all of the currently available packets, or stop early if a loss of sync or bad checksum is detected. 167 | * Stopping early is intentional, so that error messages can be printed in the correct order even if packets are processed by parallel threads. 168 | * 169 | * @param syncWord The first byte that marks the beginning of each telemetry packet. 170 | * @param syncWordByteCount Byte count of the sync word. 171 | * @return A BufferObject containing the buffer, offset and length. 172 | * @throws InterruptedException If the thread is interrupted while waiting for at least one packet to arrive. 173 | */ 174 | public PacketsBuffer readPackets(byte syncWord, int syncWordByteCount) throws InterruptedException { 175 | 176 | int readBuffer = awaitPacket(); 177 | 178 | // align with the sync word 179 | boolean lostSync = false; 180 | if(syncWordByteCount > 0) 181 | while(buffer[readBuffer][readIndex[readBuffer]] != syncWord) { 182 | lostSync = true; 183 | readIndex[readBuffer] = (readIndex[readBuffer] + 1) % bufferSize; 184 | occupiedSize[readBuffer]--; 185 | readBuffer = awaitPacket(); 186 | } 187 | 188 | // show an error message if sync was lost, unless this is the first packet (because we may have connected in the middle of a packet) 189 | if(lostSync && connection.getSampleCount() > 0) 190 | NotificationsController.showFailureForMilliseconds("Lost sync with the telemetry packet stream.", 5000, true); 191 | 192 | // stop at the first loss of sync or failed checksum 193 | int packetCount = occupiedSize[readBuffer] / packetByteCount; 194 | int index = readIndex[readBuffer]; 195 | int skipCorruptByteCount = 0; 196 | for(int i = 0; i < packetCount; i++) { 197 | if(syncWordByteCount > 0 && buffer[readBuffer][index] != syncWord) { 198 | packetCount = i; 199 | skipCorruptByteCount = 1; 200 | break; 201 | } 202 | if(!connection.datasets.checksumPassed(buffer[readBuffer], index, packetByteCount)) { 203 | packetCount = i; 204 | skipCorruptByteCount = packetByteCount; 205 | break; 206 | } 207 | index += packetByteCount; 208 | } 209 | 210 | // prepare buffer 211 | PacketsBuffer packets = new PacketsBuffer(); 212 | packets.buffer = buffer[readBuffer]; 213 | packets.offset = readIndex[readBuffer]; 214 | packets.count = packetCount; 215 | 216 | // update state 217 | int byteCount = packetCount * packetByteCount + skipCorruptByteCount; 218 | readIndex[readBuffer] = (readIndex[readBuffer] + byteCount) % bufferSize; 219 | occupiedSize[readBuffer] -= byteCount; 220 | 221 | return packets; 222 | 223 | } 224 | 225 | /** 226 | * Reads one line of text from the buffer. 227 | * 228 | * @return The text, without a CR/LF. 229 | * @throws InterruptedException 230 | */ 231 | public synchronized String readLine() throws InterruptedException { 232 | 233 | StringBuilder text = new StringBuilder(16 * connection.datasets.getCount()); 234 | 235 | // skip past any line terminators 236 | while(true) { 237 | 238 | // wait for data if necessary 239 | while(occupiedSize[0] < 1) { 240 | notifyAll(); 241 | wait(1); 242 | } 243 | 244 | // read from buffer 245 | byte b = buffer[0][readIndex[0]]; 246 | if(b == '\r' || b == '\n') { 247 | readIndex[0] = (readIndex[0] + 1) % bufferSize; 248 | occupiedSize[0]--; 249 | } else { 250 | break; 251 | } 252 | 253 | } 254 | 255 | // build up the line of text 256 | while(true) { 257 | 258 | // wait for data if necessary 259 | while(occupiedSize[0] < 1) { 260 | notifyAll(); 261 | wait(1); 262 | } 263 | 264 | // read from buffer 265 | byte b = buffer[0][readIndex[0]]; 266 | readIndex[0] = (readIndex[0] + 1) % bufferSize; 267 | occupiedSize[0]--; 268 | if(b != '\r' && b != '\n') { 269 | text.append((char) b); 270 | } else { 271 | break; 272 | } 273 | 274 | } 275 | 276 | return text.toString(); 277 | 278 | } 279 | 280 | public static class PacketsBuffer { 281 | byte[] buffer; 282 | int offset; 283 | int count; 284 | } 285 | 286 | } 287 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/Theme.java: -------------------------------------------------------------------------------- 1 | import java.awt.Color; 2 | import java.awt.Cursor; 3 | import java.awt.Font; 4 | import java.awt.Insets; 5 | 6 | import javax.swing.JPanel; 7 | import javax.swing.JToggleButton; 8 | import javax.swing.border.Border; 9 | import javax.swing.border.EmptyBorder; 10 | 11 | import com.jogamp.opengl.GL2ES3; 12 | 13 | 14 | /** 15 | * All GUI-related colors, element spacing, and fonts are managed by this class. 16 | * Colors are specified in float[4]{r,g,b,a} format. 17 | * Element spacing is specified in true pixels (pre-multiplied by the display scaling factor.) 18 | */ 19 | public class Theme { 20 | 21 | // general swing and other settings 22 | public static Color jpanelColor = new JPanel().getBackground(); 23 | public static int padding = Integer.parseInt(System.getProperty("java.version").split("\\.")[0]) >= 9 ? 5 : (int) (5 * ChartsController.getDisplayScalingFactor()); 24 | public static String removeSymbol = "\uD83D\uDDD9"; 25 | public static Color defaultDatasetColor = Color.RED; 26 | public static long defaultChartDurationMilliseconds = 10_000; 27 | public static Cursor defaultCursor = new Cursor(Cursor.DEFAULT_CURSOR); 28 | public static Cursor clickableCursor = new Cursor(Cursor.HAND_CURSOR); 29 | public static Cursor upDownCursor = new Cursor(Cursor.N_RESIZE_CURSOR); 30 | public static Cursor leftRigthCursor = new Cursor(Cursor.E_RESIZE_CURSOR); 31 | public static Border narrowButtonBorder; 32 | static { 33 | JToggleButton temp = new JToggleButton("_"); 34 | Insets insets = temp.getBorder().getBorderInsets(temp); 35 | narrowButtonBorder = new EmptyBorder(insets.top, Integer.max(insets.top, insets.bottom), insets.bottom, Integer.max(insets.top, insets.bottom)); 36 | } 37 | 38 | // general openGL 39 | public static float lineWidth = 1.0f; 40 | public static float pointWidth = 3.0f; 41 | public static long animationMilliseconds = 300; 42 | public static double animationMillisecondsDouble = 300.0; 43 | 44 | // charts region 45 | public static float[] tileColor = new float[] {0.8f, 0.8f, 0.8f, 1.0f}; 46 | public static float[] tileShadowColor = new float[] {0.7f, 0.7f, 0.7f, 1.0f}; 47 | public static float[] tileSelectedColor = new float[] {0.5f, 0.5f, 0.5f, 1.0f}; 48 | public static float tilePadding = 5.0f; 49 | public static float tileShadowOffset = tilePadding / 2; 50 | public static float[] neutralColor = new float[] {jpanelColor.getRed() / 255.0f, jpanelColor.getGreen() / 255.0f, jpanelColor.getBlue() / 255.0f, 1.0f}; 51 | public static float[] transparentNeutralColor = new float[] {neutralColor[0], neutralColor[1], neutralColor[2], 0.7f}; 52 | 53 | // plot region 54 | public static float[] plotOutlineColor = new float[] {0.0f, 0.0f, 0.0f, 1.0f}; 55 | public static float[] plotBackgroundColor = neutralColor; 56 | public static float[] divisionLinesColor = new float[] {0.7f, 0.7f, 0.7f, 1.0f}; 57 | public static float[] divisionLinesFadedColor = new float[] {0.7f, 0.7f, 0.7f, 0.0f}; 58 | 59 | // tooltips and markers in the plot region 60 | public static float[] tooltipBackgroundColor = new float[] {1, 1, 1, 1}; 61 | public static float[] tooltipBorderColor = new float[] {0.0f, 0.0f, 0.0f, 1.0f}; 62 | public static float[] markerBorderColor = new float[] {0.6f, 0.6f, 0.6f, 1.0f}; 63 | public static float[] tooltipVerticalBarColor = new float[] {0.0f, 0.0f, 0.0f, 1.0f}; 64 | public static float tooltipTextPadding = 5.0f; 65 | 66 | // tick marks surrounding the plot region 67 | public static float[] tickLinesColor = new float[] {0.0f, 0.0f, 0.0f, 1.0f}; 68 | public static float tickLength = 6.0f; 69 | public static float tickTextPadding = 3.0f; 70 | 71 | // legend 72 | public static float[] legendBackgroundColor = tileShadowColor; 73 | public static float legendTextPadding = 5.0f; 74 | public static float legendNamesPadding = 25.0f; 75 | 76 | // fonts 77 | public static Font smallFont = new Font("Geneva", Font.PLAIN, 12); 78 | public static Font mediumFont = new Font("Geneva", Font.BOLD, 14); 79 | public static Font largeFont = new Font("Geneva", Font.BOLD, 18); 80 | 81 | /** 82 | * This method must be called when the OpenGL context is initialized, 83 | * and any time the display scaling factor changes. 84 | * 85 | * @param gl The OpenGL context. 86 | * @param displayScalingFactor The display scaling factor. 87 | */ 88 | public static void initialize(GL2ES3 gl, float displayScalingFactor) { 89 | 90 | lineWidth = 1.0f * displayScalingFactor; 91 | pointWidth = 3.0f * displayScalingFactor; 92 | 93 | tilePadding = 5.0f * displayScalingFactor; 94 | tileShadowOffset = tilePadding / 2; 95 | 96 | tooltipTextPadding = 5.0f * displayScalingFactor; 97 | 98 | tickLength = 6.0f * displayScalingFactor; 99 | tickTextPadding = 3.0f * displayScalingFactor; 100 | 101 | legendTextPadding = 5.0f * displayScalingFactor; 102 | legendNamesPadding = 25.0f * displayScalingFactor; 103 | 104 | smallFont = new Font("Geneva", Font.PLAIN, (int) (12.0 * displayScalingFactor)); 105 | mediumFont = new Font("Geneva", Font.BOLD, (int) (14.0 * displayScalingFactor)); 106 | largeFont = new Font("Geneva", Font.BOLD, (int) (18.0 * displayScalingFactor)); 107 | OpenGL.updateFontTextures(gl); 108 | 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/TransmitView.java: -------------------------------------------------------------------------------- 1 | import java.awt.event.ActionListener; 2 | import java.awt.event.FocusEvent; 3 | import java.awt.event.FocusListener; 4 | import java.awt.event.KeyEvent; 5 | import java.awt.event.KeyListener; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | import javax.swing.JButton; 10 | import javax.swing.JCheckBox; 11 | import javax.swing.JComboBox; 12 | import javax.swing.JPanel; 13 | import javax.swing.JTextField; 14 | import javax.swing.SwingConstants; 15 | import net.miginfocom.swing.MigLayout; 16 | 17 | @SuppressWarnings("serial") 18 | public class TransmitView extends JPanel { 19 | 20 | TransmitController controller; 21 | 22 | JComboBox typeCombobox; 23 | JTextField dataTextfield; 24 | JCheckBox appendCRcheckbox; 25 | JCheckBox appendLFcheckbox; 26 | JCheckBox repeatCheckbox; 27 | JTextField repeatMillisecondsTextfield; 28 | JButton saveButton; 29 | JButton transmitButton; 30 | List savedPacketButtons; 31 | 32 | /** 33 | * A JPanel of widgets that lets the user transmit data. 34 | * Data may be entered in text, hex or binary form. 35 | * Packets may be saved, appearing as buttons that can be clicked to transmit. 36 | */ 37 | public TransmitView(TransmitController controller) { 38 | 39 | this.controller = controller; 40 | 41 | setLayout(new MigLayout("hidemode 3, fillx, wrap 2, insets " + Theme.padding + ", gap " + Theme.padding)); 42 | 43 | // which data format the user will provide 44 | typeCombobox = new JComboBox(new String[] {"Text", "Hex", "Bin"}); 45 | typeCombobox.setSelectedItem(controller.getTransmitType()); 46 | typeCombobox.addActionListener(event -> controller.setTransmitType(typeCombobox.getSelectedItem().toString())); 47 | 48 | // data provided by the user 49 | dataTextfield = new JTextField(controller.getTransmitText(), 10); 50 | dataTextfield.addKeyListener(new KeyListener() { 51 | @Override public void keyReleased(KeyEvent e) { controller.setTransmitText(dataTextfield.getText(), e); } 52 | @Override public void keyTyped(KeyEvent e) { } 53 | @Override public void keyPressed(KeyEvent e) { } 54 | }); 55 | dataTextfield.addFocusListener(new FocusListener() { 56 | @Override public void focusLost(FocusEvent e) { controller.setTransmitText(dataTextfield.getText(), null); } 57 | @Override public void focusGained(FocusEvent e) { } 58 | }); 59 | dataTextfield.addActionListener(event -> { controller.setTransmitText(dataTextfield.getText(), null); transmitButton.doClick(); }); 60 | 61 | // for text mode, \r or \n can be automatically appended 62 | appendCRcheckbox = new JCheckBox("CR", controller.getAppendCR()); 63 | appendCRcheckbox.addActionListener(event -> controller.setAppendCR(appendCRcheckbox.isSelected())); 64 | appendLFcheckbox = new JCheckBox("LF", controller.getAppendLF()); 65 | appendLFcheckbox.addActionListener(event -> controller.setAppendLF(appendLFcheckbox.isSelected())); 66 | 67 | // transmitted data can be automatically repeated every n milliseconds 68 | repeatCheckbox = new JCheckBox("Repeat", controller.getRepeats()); 69 | repeatCheckbox.addActionListener(event -> controller.setRepeats(repeatCheckbox.isSelected())); 70 | repeatMillisecondsTextfield = new JTextField(controller.getRepititionInterval() + " ms", 7); 71 | ActionListener millisecondsHandler = event -> { 72 | try { 73 | String text = repeatMillisecondsTextfield.getText().trim(); 74 | if(text.endsWith("ms")) 75 | text = text.substring(0, text.length() - 2).trim(); 76 | int milliseconds = Integer.parseInt(text); 77 | controller.setRepititionInterval(milliseconds); 78 | } catch(Exception e) { 79 | controller.setRepititionInterval(0); 80 | } 81 | }; 82 | repeatMillisecondsTextfield.addFocusListener(new FocusListener() { 83 | @Override public void focusLost(FocusEvent fe) { millisecondsHandler.actionPerformed(null); } 84 | @Override public void focusGained(FocusEvent fe) { repeatMillisecondsTextfield.selectAll(); } 85 | }); 86 | repeatMillisecondsTextfield.addActionListener(millisecondsHandler); 87 | 88 | // packets can be saved to JButtons 89 | savedPacketButtons = new ArrayList(); 90 | saveButton = new JButton("Save"); 91 | saveButton.addActionListener(event -> { 92 | TransmitController.SavedPacket data = new TransmitController.SavedPacket(); 93 | String mode = controller.getTransmitType(); 94 | boolean textMode = mode.equals("Text"); 95 | boolean hexMode = mode.equals("Hex"); 96 | data.bytes = textMode ? ChartUtils.convertTextStringToBytes(controller.getTransmitText(), controller.getAppendLF(), controller.getAppendCR()) : 97 | hexMode ? ChartUtils.convertHexStringToBytes(controller.getTransmitText()) : 98 | ChartUtils.convertBinStringToBytes(controller.getTransmitText()); 99 | data.label = textMode ? "Text: " + ChartUtils.convertBytesToTextString(data.bytes, true) : 100 | hexMode ? "Hex: " + ChartUtils.convertBytesToHexString(data.bytes) : 101 | "Bin: " + ChartUtils.convertBytesToBinString(data.bytes); 102 | controller.savePacket(data); 103 | controller.setTransmitText("", null); 104 | }); 105 | 106 | // transmit button for sending the data once 107 | transmitButton = new JButton("Transmit"); 108 | transmitButton.addActionListener(event -> { 109 | String mode = controller.getTransmitType(); 110 | boolean textMode = mode.equals("Text"); 111 | boolean hexMode = mode.equals("Hex"); 112 | byte[] bytes = textMode ? ChartUtils.convertTextStringToBytes(controller.getTransmitText(), controller.getAppendLF(), controller.getAppendCR()) : 113 | hexMode ? ChartUtils.convertHexStringToBytes(controller.getTransmitText()) : 114 | ChartUtils.convertBinStringToBytes(controller.getTransmitText()); 115 | controller.connection.transmit(bytes); 116 | }); 117 | 118 | add(typeCombobox, "grow x"); 119 | add(dataTextfield, "grow x"); 120 | add(appendCRcheckbox, "span 2, split 4"); 121 | add(appendLFcheckbox); 122 | add(repeatCheckbox); 123 | add(repeatMillisecondsTextfield, "grow x"); 124 | add(saveButton, "span 2, split 2, grow x"); 125 | add(transmitButton, "grow x"); 126 | 127 | } 128 | 129 | /** 130 | * Redraws the list of saved packets. 131 | * 132 | * @param savedPackets The current list of saved packets. 133 | */ 134 | public void redrawSavedPackets(List savedPackets) { 135 | 136 | for(JButton button : savedPacketButtons) 137 | remove(button); 138 | savedPacketButtons.clear(); 139 | 140 | for(TransmitController.SavedPacket packet : savedPackets) { 141 | JButton sendButton = new JButton(); 142 | sendButton.setText(packet.label); 143 | sendButton.setHorizontalAlignment(SwingConstants.LEFT); 144 | sendButton.addActionListener(clicked -> controller.connection.transmit(packet.bytes)); 145 | JButton removeButton = new JButton(Theme.removeSymbol); 146 | removeButton.setBorder(Theme.narrowButtonBorder); 147 | removeButton.addActionListener(click -> controller.removePacket(packet)); 148 | savedPacketButtons.add(sendButton); 149 | savedPacketButtons.add(removeButton); 150 | if(controller.connection.packetType == ConnectionTelemetry.PacketType.TC66) { 151 | add(sendButton, "span 2, grow x"); 152 | } else { 153 | add(sendButton, "span 2, split 2, grow x, width 1:1:"); // setting min/pref width to 1px to ensure this button doesn't widen the panel 154 | add(removeButton); 155 | } 156 | } 157 | 158 | revalidate(); 159 | repaint(); 160 | 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/Widget.java: -------------------------------------------------------------------------------- 1 | import java.awt.Component; 2 | import java.util.LinkedHashMap; 3 | import java.util.Map; 4 | 5 | public abstract class Widget { 6 | 7 | Map widgets; // key = Swing widget, value = MigLayout component constraints 8 | 9 | public Widget() { 10 | 11 | widgets = new LinkedHashMap(); 12 | 13 | } 14 | 15 | /** 16 | * Optional method that will be called whenever outside state has changed. 17 | */ 18 | public void update() { 19 | 20 | } 21 | 22 | /** 23 | * Updates the widget and chart based on a settings file. 24 | * 25 | * @param lines A queue of remaining lines from the layout file. 26 | */ 27 | public abstract void importState(ConnectionsController.QueueOfLines lines); 28 | 29 | /** 30 | * Saves the current state to one or more lines of text. 31 | * 32 | * @return A String[] where each element is a line of text. 33 | */ 34 | public abstract String[] exportState(); 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/WidgetCamera.java: -------------------------------------------------------------------------------- 1 | import java.util.function.Consumer; 2 | 3 | import javax.swing.JComboBox; 4 | import javax.swing.JLabel; 5 | 6 | public class WidgetCamera extends Widget { 7 | 8 | JLabel namesLabel; 9 | JComboBox namesCombobox; 10 | Consumer handler; 11 | 12 | String selectedCamera; 13 | 14 | /** 15 | * A widget that lets the user pick a camera and resolution from drop-down lists. 16 | * 17 | * @param eventHandler Will be notified when the chosen options change. 18 | */ 19 | public WidgetCamera(Consumer eventHandler) { 20 | 21 | super(); 22 | 23 | handler = eventHandler; 24 | update(); 25 | 26 | 27 | } 28 | 29 | @Override public void update() { 30 | 31 | widgets.clear(); 32 | 33 | namesLabel = new JLabel("Camera: "); 34 | namesCombobox = new JComboBox(); 35 | for(ConnectionCamera camera : ConnectionsController.cameraConnections) 36 | if(camera.connected || camera.getSampleCount() > 0) 37 | namesCombobox.addItem(camera.name); 38 | if(namesCombobox.getItemCount() == 0) { 39 | namesCombobox.addItem("[No cameras available]"); 40 | namesCombobox.setEnabled(false); 41 | } 42 | for(int i = 0; i < namesCombobox.getItemCount(); i++) 43 | if(namesCombobox.getItemAt(i).equals(selectedCamera)) 44 | namesCombobox.setSelectedIndex(i); 45 | namesCombobox.addActionListener(event -> { 46 | selectedCamera = namesCombobox.getSelectedItem().toString(); 47 | handler.accept(selectedCamera); 48 | }); 49 | 50 | widgets.put(namesLabel, ""); 51 | widgets.put(namesCombobox, "span 3, growx"); 52 | 53 | handler.accept(namesCombobox.getSelectedItem().toString()); 54 | 55 | } 56 | 57 | /** 58 | * Updates the widget and chart based on settings from a layout file. 59 | * 60 | * @param lines A queue of remaining lines from the layout file. 61 | */ 62 | @Override public void importState(ConnectionsController.QueueOfLines lines) { 63 | 64 | // update the GUI 65 | String name = ChartUtils.parseString(lines.remove(), "camera name = %s"); 66 | int cameraN = -1; 67 | for(int i = 0; i < namesCombobox.getItemCount(); i++) 68 | if(namesCombobox.getItemAt(i).equals(name)) 69 | cameraN = i; 70 | if(cameraN >= 0) { 71 | namesCombobox.setSelectedIndex(cameraN); 72 | } else { 73 | namesCombobox.addItem(name); 74 | namesCombobox.setSelectedItem(name); 75 | } 76 | 77 | } 78 | 79 | /** 80 | * Saves the current state to one or more lines of text. 81 | * 82 | * @return A String[] where each element is a line of text. 83 | */ 84 | @Override public String[] exportState() { 85 | 86 | return new String[] { "camera name = " + namesCombobox.getSelectedItem().toString()}; 87 | 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/WidgetCheckbox.java: -------------------------------------------------------------------------------- 1 | import java.util.function.Consumer; 2 | 3 | import javax.swing.JCheckBox; 4 | import javax.swing.JLabel; 5 | 6 | public class WidgetCheckbox extends Widget { 7 | 8 | String label; 9 | JCheckBox checkbox; 10 | Consumer handler; 11 | 12 | /** 13 | * A widget that lets the user check or uncheck a checkbox. 14 | * 15 | * @param labelText Label to show at the right of the checkbox. 16 | * @param isChecked If the checkbox should default to checked. 17 | * @param eventHandler Will be notified when the checkbox changes. 18 | */ 19 | public WidgetCheckbox(String labelText, boolean isChecked, Consumer eventHandler) { 20 | 21 | super(); 22 | 23 | label = labelText; 24 | handler = eventHandler; 25 | 26 | checkbox = new JCheckBox(label); 27 | checkbox.setSelected(isChecked); 28 | checkbox.addActionListener(event -> handler.accept(checkbox.isSelected())); 29 | 30 | widgets.put(new JLabel(""), ""); 31 | widgets.put(checkbox, "span 3, growx"); 32 | 33 | handler.accept(checkbox.isSelected()); 34 | 35 | } 36 | 37 | /** 38 | * Updates the widget and chart based on settings from a layout file. 39 | * 40 | * @param lines A queue of remaining lines from the layout file. 41 | */ 42 | @Override public void importState(ConnectionsController.QueueOfLines lines) { 43 | 44 | // parse the text 45 | boolean checked = ChartUtils.parseBoolean(lines.remove(), label.trim().toLowerCase() + " = %b"); 46 | 47 | // update the widget 48 | checkbox.setSelected(checked); 49 | 50 | // update the chart 51 | handler.accept(checkbox.isSelected()); 52 | 53 | } 54 | 55 | /** 56 | * Saves the current state to one or more lines of text. 57 | * 58 | * @return A String[] where each element is a line of text. 59 | */ 60 | @Override public String[] exportState() { 61 | 62 | return new String[] { 63 | label.trim().toLowerCase() + " = " + checkbox.isSelected() 64 | }; 65 | 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/WidgetCombobox.java: -------------------------------------------------------------------------------- 1 | import java.util.function.Consumer; 2 | 3 | import javax.swing.JComboBox; 4 | import javax.swing.JLabel; 5 | 6 | public class WidgetCombobox extends Widget { 7 | 8 | String label; 9 | JComboBox combobox; 10 | Consumer handler; 11 | 12 | /** 13 | * A widget that lets the user select one option from a combobox. 14 | * 15 | * @param textLabel Label to show at the left of the combobox. 16 | * @param values A String[] of options to show in the combobox. 17 | * @param eventHandler Will be notified when the combobox selection changes. 18 | */ 19 | public WidgetCombobox(String textLabel, String[] values, Consumer eventHandler) { 20 | 21 | super(); 22 | 23 | label = textLabel; 24 | handler = eventHandler; 25 | 26 | combobox = new JComboBox(values); 27 | combobox.addActionListener(event -> eventHandler.accept(combobox.getSelectedItem().toString())); 28 | 29 | widgets.put(new JLabel(label + ": "), ""); 30 | widgets.put(combobox, "span 3, growx"); 31 | 32 | eventHandler.accept(combobox.getSelectedItem().toString()); 33 | 34 | } 35 | 36 | /** 37 | * Updates the widget and chart based on settings from a layout file. 38 | * 39 | * @param lines A queue of remaining lines from the layout file. 40 | */ 41 | @Override public void importState(ConnectionsController.QueueOfLines lines) { 42 | 43 | // parse the text 44 | String text = ChartUtils.parseString(lines.remove(), label.trim().toLowerCase() + " = %s"); 45 | 46 | // update the widget 47 | boolean found = false; 48 | for(int i = 0; i < combobox.getItemCount(); i++) { 49 | if(combobox.getItemAt(i).equals(text)) { 50 | combobox.setSelectedIndex(i); 51 | found = true; 52 | break; 53 | } 54 | } 55 | 56 | if(!found) 57 | throw new AssertionError("Invalid option."); 58 | 59 | // update the chart 60 | handler.accept(combobox.getSelectedItem().toString()); 61 | 62 | } 63 | 64 | /** 65 | * Saves the current state to one or more lines of text. 66 | * 67 | * @return A String[] where each element is a line of text. 68 | */ 69 | @Override public String[] exportState() { 70 | 71 | return new String[] { 72 | label.trim().toLowerCase() + " = " + combobox.getSelectedItem().toString() 73 | }; 74 | 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/WidgetFrequencyDomainType.java: -------------------------------------------------------------------------------- 1 | import java.awt.event.FocusEvent; 2 | import java.awt.event.FocusListener; 3 | import java.util.function.Consumer; 4 | 5 | import javax.swing.ButtonGroup; 6 | import javax.swing.JLabel; 7 | import javax.swing.JTextField; 8 | import javax.swing.JToggleButton; 9 | 10 | public class WidgetFrequencyDomainType extends Widget { 11 | 12 | final int waveformRowCountUpperLimit = 1000; 13 | final int waveformRowCountLowerLimit = 2; 14 | final int waveformRowCountDefault = 60; 15 | final int dftCountUpperLimit = 100; 16 | final int dftCountLowerLimit = 2; 17 | final int dftCountDefault = 20; 18 | 19 | JToggleButton singleMode; 20 | JToggleButton multipleMode; 21 | JToggleButton waterfallMode; 22 | 23 | JLabel dftCountLabel; 24 | JTextField dftCountTextfield; 25 | JLabel rowCountLabel; 26 | JTextField rowCountTextfield; 27 | 28 | Consumer modeHandler; 29 | Consumer dftCountHandler; 30 | Consumer rowCountHandler; 31 | 32 | /** 33 | * A widget that lets the user specify the mode for a frequency domain chart. 34 | * 35 | * The mode can be "Single" or "Multiple" or "Waterfall". 36 | * For the waterfall type, the user specifies a DFT count. 37 | * For the multiple type, the user specifies a DFT count and row count. 38 | * 39 | * @param modeEventHandler Will be notified when the value changes. 40 | * @param dftCountEventHandler Will be notified when the value changes. 41 | * @param rowCountEventHandler Will be notified when the value changes. 42 | */ 43 | public WidgetFrequencyDomainType(Consumer modeEventHandler, Consumer dftCountEventHandler, Consumer rowCountEventHandler) { 44 | 45 | super(); 46 | 47 | singleMode = new JToggleButton("Single", true); 48 | singleMode.setBorder(Theme.narrowButtonBorder); 49 | singleMode.addActionListener(event -> sanityCheck()); 50 | multipleMode = new JToggleButton("Multiple", false); 51 | multipleMode.setBorder(Theme.narrowButtonBorder); 52 | multipleMode.addActionListener(event -> sanityCheck()); 53 | waterfallMode = new JToggleButton("Waterfall", false); 54 | waterfallMode.setBorder(Theme.narrowButtonBorder); 55 | waterfallMode.addActionListener(event -> sanityCheck()); 56 | ButtonGroup group = new ButtonGroup(); 57 | group.add(singleMode); 58 | group.add(multipleMode); 59 | group.add(waterfallMode); 60 | 61 | dftCountLabel = new JLabel("DFT Count: "); 62 | dftCountTextfield = new JTextField(Integer.toString(dftCountDefault)); 63 | dftCountTextfield.addFocusListener(new FocusListener() { 64 | @Override public void focusLost(FocusEvent fe) { sanityCheck(); } 65 | @Override public void focusGained(FocusEvent fe) { dftCountTextfield.selectAll(); } 66 | }); 67 | dftCountTextfield.addActionListener(event -> sanityCheck()); 68 | 69 | rowCountLabel = new JLabel("Row Count: "); 70 | rowCountTextfield = new JTextField(Integer.toString(waveformRowCountDefault)); 71 | rowCountTextfield.addFocusListener(new FocusListener() { 72 | @Override public void focusLost(FocusEvent fe) { sanityCheck(); } 73 | @Override public void focusGained(FocusEvent fe) { rowCountTextfield.selectAll(); } 74 | }); 75 | rowCountTextfield.addActionListener(event -> sanityCheck()); 76 | 77 | modeHandler = modeEventHandler; 78 | dftCountHandler = dftCountEventHandler; 79 | rowCountHandler = rowCountEventHandler; 80 | 81 | widgets.put(new JLabel("Mode: "), ""); 82 | widgets.put(singleMode, "span 3, split 3, growx"); 83 | widgets.put(multipleMode, "growx"); 84 | widgets.put(waterfallMode, "growx"); 85 | widgets.put(dftCountLabel, ""); 86 | widgets.put(dftCountTextfield, "span 3, growx"); 87 | widgets.put(rowCountLabel, ""); 88 | widgets.put(rowCountTextfield, "span 3, growx"); 89 | 90 | sanityCheck(); 91 | 92 | } 93 | 94 | /** 95 | * Ensures the DFT count and waveform row count are within the allowed ranges. 96 | * Shows or hides widgets based on the selected chart mode. 97 | * Notifies all handlers. 98 | */ 99 | public void sanityCheck() { 100 | 101 | // DFT count 102 | try { 103 | int dftCount = Integer.parseInt(dftCountTextfield.getText().trim()); 104 | if(dftCount > dftCountUpperLimit) dftCount = dftCountUpperLimit; 105 | if(dftCount < dftCountLowerLimit) dftCount = dftCountLowerLimit; 106 | dftCountTextfield.setText(Integer.toString(dftCount)); 107 | dftCountHandler.accept(dftCount); 108 | } catch(Exception e) { 109 | dftCountTextfield.setText(Integer.toString(dftCountDefault)); 110 | dftCountHandler.accept(dftCountDefault); 111 | } 112 | 113 | // waveform row count 114 | try { 115 | int count = Integer.parseInt(rowCountTextfield.getText().trim()); 116 | if(count > waveformRowCountUpperLimit) count = waveformRowCountUpperLimit; 117 | if(count < waveformRowCountLowerLimit) count = waveformRowCountLowerLimit; 118 | rowCountTextfield.setText(Integer.toString(count)); 119 | rowCountHandler.accept(count); 120 | } catch(Exception e) { 121 | rowCountTextfield.setText(Integer.toString(waveformRowCountDefault)); 122 | rowCountHandler.accept(waveformRowCountDefault); 123 | } 124 | 125 | // show/hide widgets as needed 126 | boolean multipleViewMode = multipleMode.isSelected(); 127 | boolean waterfallViewMode = waterfallMode.isSelected(); 128 | dftCountLabel.setVisible(multipleViewMode || waterfallViewMode); 129 | dftCountTextfield.setVisible(multipleViewMode || waterfallViewMode); 130 | rowCountLabel.setVisible(multipleViewMode); 131 | rowCountTextfield.setVisible(multipleViewMode); 132 | 133 | modeHandler.accept(singleMode.isSelected() ? "Single" : 134 | multipleMode.isSelected() ? "Multiple" : 135 | "Waterfall"); 136 | 137 | } 138 | 139 | /** 140 | * Updates the widget and chart based on settings from a layout file. 141 | * 142 | * @param lines A queue of remaining lines from the layout file. 143 | */ 144 | @Override public void importState(ConnectionsController.QueueOfLines lines) { 145 | 146 | // parse the text 147 | String mode = ChartUtils.parseString (lines.remove(), "mode = %s"); 148 | int dftCount = ChartUtils.parseInteger(lines.remove(), "dft count = %d"); 149 | int waveformRowCount = ChartUtils.parseInteger(lines.remove(), "waveform view row count = %d"); 150 | 151 | if(!mode.equals("Single") && !mode.equals("Multiple") && !mode.equals("Waterfall")) 152 | throw new AssertionError("Invalid Frequency Domain Chart mode."); 153 | 154 | // update the widgets 155 | if(mode.equals("Single")) 156 | singleMode.setSelected(true); 157 | else if(mode.equals("Multiple")) 158 | multipleMode.setSelected(true); 159 | else if(mode.equals("Waterfall")) 160 | waterfallMode.setSelected(true); 161 | dftCountTextfield.setText(Integer.toString(dftCount)); 162 | rowCountTextfield.setText(Integer.toString(waveformRowCount)); 163 | 164 | // update the chart 165 | sanityCheck(); 166 | 167 | } 168 | 169 | /** 170 | * Saves the current state to one or more lines of text. 171 | * 172 | * @return A String[] where each element is a line of text. 173 | */ 174 | @Override public String[] exportState() { 175 | 176 | String mode = singleMode.isSelected() ? "Single" : 177 | multipleMode.isSelected() ? "Multiple" : 178 | "Waterfall"; 179 | 180 | return new String[] { 181 | "mode = " + mode, 182 | "dft count = " + dftCountTextfield.getText(), 183 | "waveform view row count = " + rowCountTextfield.getText() 184 | }; 185 | 186 | } 187 | 188 | } 189 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/WidgetHistogramXaxisType.java: -------------------------------------------------------------------------------- 1 | import java.awt.event.FocusEvent; 2 | import java.awt.event.FocusListener; 3 | import java.util.function.BiConsumer; 4 | 5 | import javax.swing.Box; 6 | import javax.swing.BoxLayout; 7 | import javax.swing.JCheckBox; 8 | import javax.swing.JComboBox; 9 | import javax.swing.JLabel; 10 | import javax.swing.JPanel; 11 | import javax.swing.JTextField; 12 | 13 | public class WidgetHistogramXaxisType extends Widget { 14 | 15 | JLabel axisTypeLabel; 16 | JComboBox axisTypeCombobox; 17 | JLabel maxLabel; 18 | JLabel minLabel; 19 | JLabel centerLabel; 20 | JCheckBox maxCheckbox; 21 | JCheckBox minCheckbox; 22 | JTextField maxTextfield; 23 | JTextField minTextfield; 24 | JTextField centerTextfield; 25 | JPanel maxPanel; 26 | JPanel minPanel; 27 | float upperLimit; 28 | float lowerLimit; 29 | float defaultMax; 30 | float defaultMin; 31 | float defaultCenter; 32 | BiConsumer minHandler; 33 | BiConsumer maxHandler; 34 | BiConsumer centerHandler; 35 | 36 | /** 37 | * A widget that lets the user specify the x-axis type for a histogram. 38 | * 39 | * The axis can be "normal" where the minimum/maximum values can either be autoscaled or specified. 40 | * Or the axis can be "locked center" where the user specifies the center value. 41 | * In the locked-center mode, the minimum/maximum will autoscale as needed, but in a way that keeps the center of the axis at the specified value. 42 | * 43 | * @param defaultMin Default value for "normal" axis minimum. 44 | * @param defaultMax Default value for "normal" axis maximum. 45 | * @param defaultCenter Default value for "locked center" axis center. 46 | * @param lowerLimit Minimum allowed value. 47 | * @param upperLimit Maximum allowed value. 48 | * @param minEventHandler Will be notified when the minimum changes. 49 | * @param maxEventHandler Will be notified when the maximum changes. 50 | * @param centerEventHandler Will be notified when the center changes. 51 | */ 52 | public WidgetHistogramXaxisType(float defaultMin, float defaultMax, float defaultCenter, float lowerLimit, float upperLimit, BiConsumer minEventHandler, BiConsumer maxEventHandler, BiConsumer centerEventHandler) { 53 | 54 | super(); 55 | 56 | axisTypeLabel = new JLabel("X-Axis Type: "); 57 | axisTypeCombobox = new JComboBox(new String[] {"Normal", "Locked Center"}); 58 | maxLabel = new JLabel("X-Axis Maximum: "); 59 | minLabel = new JLabel("X-Axis Minimum: "); 60 | centerLabel = new JLabel("X-Axis Center: "); 61 | maxCheckbox = new JCheckBox("Automatic"); 62 | minCheckbox = new JCheckBox("Automatic"); 63 | maxCheckbox.setSelected(true); 64 | minCheckbox.setSelected(true); 65 | maxTextfield = new JTextField(Float.toString(defaultMax)); 66 | minTextfield = new JTextField(Float.toString(defaultMin)); 67 | maxTextfield.setEnabled(false); 68 | minTextfield.setEnabled(false); 69 | centerTextfield = new JTextField(Float.toString(defaultCenter)); 70 | 71 | this.upperLimit = upperLimit; 72 | this.lowerLimit = lowerLimit; 73 | this.defaultMax = defaultMax; 74 | this.defaultMin = defaultMin; 75 | this.defaultCenter = defaultCenter; 76 | this.minHandler = minEventHandler; 77 | this.maxHandler = maxEventHandler; 78 | this.centerHandler = centerEventHandler; 79 | 80 | maxCheckbox.addActionListener(event -> sanityCheck()); 81 | 82 | minCheckbox.addActionListener(event -> sanityCheck()); 83 | 84 | maxTextfield.addFocusListener(new FocusListener() { 85 | @Override public void focusLost(FocusEvent fe) { sanityCheck(); } 86 | @Override public void focusGained(FocusEvent fe) { maxTextfield.selectAll(); } 87 | }); 88 | maxTextfield.addActionListener(event -> sanityCheck()); 89 | 90 | minTextfield.addFocusListener(new FocusListener() { 91 | @Override public void focusLost(FocusEvent fe) { sanityCheck(); } 92 | @Override public void focusGained(FocusEvent fe) { minTextfield.selectAll(); } 93 | }); 94 | minTextfield.addActionListener(event -> sanityCheck()); 95 | 96 | centerTextfield.addFocusListener(new FocusListener() { 97 | @Override public void focusLost(FocusEvent fe) { sanityCheck(); } 98 | @Override public void focusGained(FocusEvent fe) { centerTextfield.selectAll(); } 99 | }); 100 | centerTextfield.addActionListener(event -> sanityCheck()); 101 | 102 | axisTypeCombobox.addActionListener(event -> sanityCheck()); 103 | 104 | maxPanel = new JPanel(); 105 | maxPanel.setLayout(new BoxLayout(maxPanel, BoxLayout.X_AXIS)); 106 | maxPanel.add(maxCheckbox); 107 | maxPanel.add(Box.createHorizontalStrut(10)); 108 | maxPanel.add(maxTextfield); 109 | 110 | minPanel = new JPanel(); 111 | minPanel.setLayout(new BoxLayout(minPanel, BoxLayout.X_AXIS)); 112 | minPanel.add(minCheckbox); 113 | minPanel.add(Box.createHorizontalStrut(10)); 114 | minPanel.add(minTextfield); 115 | 116 | widgets.put(axisTypeLabel, ""); 117 | widgets.put(axisTypeCombobox, "span 3, growx"); 118 | widgets.put(maxLabel, ""); 119 | widgets.put(maxPanel, "span 3, growx"); 120 | widgets.put(minLabel, ""); 121 | widgets.put(minPanel, "span 3, growx"); 122 | widgets.put(centerLabel, ""); 123 | widgets.put(centerTextfield, "span 3, growx"); 124 | 125 | sanityCheck(); 126 | 127 | } 128 | 129 | /** 130 | * Ensures the min and max values are within the allowed range, and that minimum < maximum. 131 | * Ensures the center value is within the allowed range. 132 | * Shows/hides/disables widgets based on the selected axis type and autoscale selections. 133 | * 134 | * Notifies all handlers. 135 | */ 136 | public void sanityCheck() { 137 | 138 | // sanity check the min and max 139 | try { 140 | 141 | // clip to limits 142 | float max = Float.parseFloat(maxTextfield.getText().trim()); 143 | float min = Float.parseFloat(minTextfield.getText().trim()); 144 | if(max > upperLimit) max = upperLimit; 145 | if(max < lowerLimit) max = lowerLimit; 146 | if(min > upperLimit) min = upperLimit; 147 | if(min < lowerLimit) min = lowerLimit; 148 | 149 | // ensure min < max 150 | if(min == max) { 151 | if(max == upperLimit) 152 | min = Math.nextDown(min); 153 | else 154 | max = Math.nextUp(max); 155 | } else if(min > max) { 156 | float temp = max; 157 | max = min; 158 | min = temp; 159 | } 160 | 161 | // update textfields 162 | maxTextfield.setText(Float.toString(max)); 163 | minTextfield.setText(Float.toString(min)); 164 | maxHandler.accept(maxCheckbox.isSelected(), max); 165 | minHandler.accept(minCheckbox.isSelected(), min); 166 | 167 | } catch(Exception e) { 168 | 169 | // one of the textfields doesn't contain a valid number, so reset both to defaults 170 | maxTextfield.setText(Float.toString(defaultMax)); 171 | minTextfield.setText(Float.toString(defaultMin)); 172 | maxHandler.accept(maxCheckbox.isSelected(), defaultMax); 173 | minHandler.accept(minCheckbox.isSelected(), defaultMin); 174 | 175 | } 176 | 177 | // sanity check the center value 178 | try { 179 | 180 | // clip to limits 181 | float center = Float.parseFloat(centerTextfield.getText().trim()); 182 | if(center > upperLimit) center = upperLimit; 183 | if(center < lowerLimit) center = lowerLimit; 184 | centerTextfield.setText(Float.toString(center)); 185 | centerHandler.accept(axisTypeCombobox.getSelectedItem().toString().equals("Locked Center"), center); 186 | 187 | } catch(Exception e) { 188 | 189 | // not a valid number, so reset to default 190 | centerTextfield.setText(Float.toString(defaultCenter)); 191 | centerHandler.accept(axisTypeCombobox.getSelectedItem().toString().equals("Locked Center"), defaultCenter); 192 | 193 | } 194 | 195 | // disable textboxes for autoscaled values 196 | minTextfield.setEnabled(!minCheckbox.isSelected()); 197 | maxTextfield.setEnabled(!maxCheckbox.isSelected()); 198 | 199 | // redraw depending on the axis type 200 | if(axisTypeCombobox.getSelectedItem().toString().equals("Normal")) { 201 | 202 | maxLabel.setVisible(true); 203 | maxPanel.setVisible(true); 204 | minLabel.setVisible(true); 205 | minPanel.setVisible(true); 206 | centerLabel.setVisible(false); 207 | centerTextfield.setVisible(false); 208 | 209 | } else if(axisTypeCombobox.getSelectedItem().toString().equals("Locked Center")) { 210 | 211 | maxLabel.setVisible(false); 212 | maxPanel.setVisible(false); 213 | minLabel.setVisible(false); 214 | minPanel.setVisible(false); 215 | centerLabel.setVisible(true); 216 | centerTextfield.setVisible(true); 217 | 218 | } 219 | 220 | } 221 | 222 | /** 223 | * Updates the widget and chart based on settings from a layout file. 224 | * 225 | * @param lines A queue of remaining lines from the layout file. 226 | */ 227 | @Override public void importState(ConnectionsController.QueueOfLines lines) { 228 | 229 | // parse the text 230 | boolean xAxisIsCentered = ChartUtils.parseBoolean(lines.remove(), "x-axis is centered = %b"); 231 | float xCenterValue = ChartUtils.parseFloat (lines.remove(), "x-axis center value = %f"); 232 | boolean xAutoscaleMin = ChartUtils.parseBoolean(lines.remove(), "x-axis autoscale minimum = %b"); 233 | float manualMinX = ChartUtils.parseFloat (lines.remove(), "x-axis manual minimum = %f"); 234 | boolean xAutoscaleMax = ChartUtils.parseBoolean(lines.remove(), "x-axis autoscale maximum = %b"); 235 | float manualMaxX = ChartUtils.parseFloat (lines.remove(), "x-axis manual maximum = %f"); 236 | 237 | // update the widget 238 | axisTypeCombobox.setSelectedItem(xAxisIsCentered ? "Locked Center" : "Normal"); 239 | centerTextfield.setText(Float.toString(xCenterValue)); 240 | minCheckbox.setSelected(xAutoscaleMin); 241 | minTextfield.setText(Float.toString(manualMinX)); 242 | maxCheckbox.setSelected(xAutoscaleMax); 243 | maxTextfield.setText(Float.toString(manualMaxX)); 244 | 245 | // update the chart 246 | sanityCheck(); 247 | 248 | } 249 | 250 | /** 251 | * Saves the current state to one or more lines of text. 252 | * 253 | * @return A String[] where each element is a line of text. 254 | */ 255 | @Override public String[] exportState() { 256 | 257 | return new String[] { 258 | "x-axis is centered = " + (axisTypeCombobox.getSelectedIndex() == 1), 259 | "x-axis center value = " + centerTextfield.getText(), 260 | "x-axis autoscale minimum = " + minCheckbox.isSelected(), 261 | "x-axis manual minimum = " + minTextfield.getText(), 262 | "x-axis autoscale maximum = " + maxCheckbox.isSelected(), 263 | "x-axis manual maximum = " + maxTextfield.getText() 264 | }; 265 | 266 | } 267 | 268 | } 269 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/WidgetHistogramYaxisType.java: -------------------------------------------------------------------------------- 1 | import java.awt.event.FocusEvent; 2 | import java.awt.event.FocusListener; 3 | import java.util.function.BiConsumer; 4 | 5 | import javax.swing.Box; 6 | import javax.swing.BoxLayout; 7 | import javax.swing.JCheckBox; 8 | import javax.swing.JComboBox; 9 | import javax.swing.JLabel; 10 | import javax.swing.JPanel; 11 | import javax.swing.JTextField; 12 | 13 | public class WidgetHistogramYaxisType extends Widget { 14 | 15 | JLabel axisTypeLabel; 16 | JComboBox axisTypeCombobox; 17 | JLabel maxLabel; 18 | JLabel minLabel; 19 | JCheckBox maxCheckbox; 20 | JCheckBox minCheckbox; 21 | JTextField maxTextfield; 22 | JTextField minTextfield; 23 | 24 | float relativeFrequencyUpperLimit; 25 | float relativeFrequencyLowerLimit; 26 | float relativeFrequencyDefaultMaximum; 27 | float relativeFrequencyDefaultMinimum; 28 | 29 | float frequencyUpperLimit; 30 | float frequencyLowerLimit; 31 | float frequencyDefaultMaximum; 32 | float frequencyDefaultMinimum; 33 | 34 | BiConsumer axisTypeHandler; 35 | BiConsumer minHandler; 36 | BiConsumer maxHandler; 37 | 38 | /** 39 | * A widget that lets the user specify the y-axis type for a histogram. 40 | * 41 | * The axis can show relative frequency, frequency, or both. 42 | * If both are shown, the left axis will be relative frequency and the right axis will be frequency. 43 | * 44 | * The minimum/maximum values can either be autoscaled or specified. 45 | * 46 | * @param relativeFrequencyDefaultMinimum Default value for axis minimum. 47 | * @param relativeFrequencyDefaultMaximum Default value for axis maximum. 48 | * @param relativeFrequencyLowerLimit Minimum allowed value. 49 | * @param relativeFrequencyUpperLimit Maximum allowed value. 50 | * @param frequencyDefaultMinimum Default value for axis minimum. 51 | * @param frequencyDefaultMaximum Default value for axis maximum. 52 | * @param frequencyLowerLimit Minimum allowed value. 53 | * @param frequencyUpperLimit Maximum allowed value. 54 | * @param axisTypeEventHandler Will be notified when the axis type (relative frequency, frequency, or both) changes. 55 | * @param minEventHandler Will be notified when the minimum changes. 56 | * @param maxEventHandler Will be notified when the maximum changes. 57 | */ 58 | public WidgetHistogramYaxisType(float relativeFrequencyDefaultMinimum, float relativeFrequencyDefaultMaximum, float relativeFrequencyLowerLimit, float relativeFrequencyUpperLimit, float frequencyDefaultMinimum, float frequencyDefaultMaximum, float frequencyLowerLimit, float frequencyUpperLimit, BiConsumer axisTypeEventHandler, BiConsumer minEventHandler, BiConsumer maxEventHandler) { 59 | 60 | super(); 61 | 62 | axisTypeLabel = new JLabel("Y-Axis Type: "); 63 | axisTypeCombobox = new JComboBox(new String[] {"Relative Frequency", "Frequency", "Both"}); 64 | maxLabel = new JLabel("Relative Frequency Maximum: "); 65 | minLabel = new JLabel("Relative Frequency Minimum: "); 66 | maxCheckbox = new JCheckBox("Automatic"); 67 | minCheckbox = new JCheckBox("Zero"); 68 | maxCheckbox.setSelected(true); 69 | minCheckbox.setSelected(true); 70 | maxTextfield = new JTextField(Float.toString(relativeFrequencyDefaultMaximum)); 71 | minTextfield = new JTextField(Float.toString(relativeFrequencyDefaultMinimum)); 72 | maxTextfield.setEnabled(false); 73 | minTextfield.setEnabled(false); 74 | 75 | this.relativeFrequencyUpperLimit = relativeFrequencyUpperLimit; 76 | this.relativeFrequencyLowerLimit = relativeFrequencyLowerLimit; 77 | this.relativeFrequencyDefaultMaximum = relativeFrequencyDefaultMaximum; 78 | this.relativeFrequencyDefaultMinimum = relativeFrequencyDefaultMinimum; 79 | this.frequencyUpperLimit = frequencyUpperLimit; 80 | this.frequencyLowerLimit = frequencyLowerLimit; 81 | this.frequencyDefaultMaximum = frequencyDefaultMaximum; 82 | this.frequencyDefaultMinimum = frequencyDefaultMinimum; 83 | this.axisTypeHandler = axisTypeEventHandler; 84 | this.minHandler = minEventHandler; 85 | this.maxHandler = maxEventHandler; 86 | 87 | axisTypeCombobox.addActionListener(event -> sanityCheck()); 88 | 89 | maxCheckbox.addActionListener(event -> sanityCheck()); 90 | 91 | minCheckbox.addActionListener(event -> sanityCheck()); 92 | 93 | maxTextfield.addFocusListener(new FocusListener() { 94 | @Override public void focusLost(FocusEvent fe) { sanityCheck(); } 95 | @Override public void focusGained(FocusEvent fe) { maxTextfield.selectAll(); } 96 | }); 97 | maxTextfield.addActionListener(event -> sanityCheck()); 98 | 99 | minTextfield.addFocusListener(new FocusListener() { 100 | @Override public void focusLost(FocusEvent fe) { sanityCheck(); } 101 | @Override public void focusGained(FocusEvent fe) { minTextfield.selectAll(); } 102 | }); 103 | minTextfield.addActionListener(event -> sanityCheck()); 104 | 105 | widgets.put(axisTypeLabel, ""); 106 | widgets.put(axisTypeCombobox, "span 3, growx"); 107 | 108 | JPanel maxPanel = new JPanel(); 109 | maxPanel.setLayout(new BoxLayout(maxPanel, BoxLayout.X_AXIS)); 110 | maxPanel.add(maxCheckbox); 111 | maxPanel.add(Box.createHorizontalStrut(Theme.padding)); 112 | maxPanel.add(maxTextfield); 113 | widgets.put(maxLabel, ""); 114 | widgets.put(maxPanel, "span 3, growx"); 115 | 116 | JPanel minPanel = new JPanel(); 117 | minPanel.setLayout(new BoxLayout(minPanel, BoxLayout.X_AXIS)); 118 | minPanel.add(minCheckbox); 119 | minPanel.add(Box.createHorizontalStrut(Theme.padding)); 120 | minPanel.add(minTextfield); 121 | widgets.put(minLabel, ""); 122 | widgets.put(minPanel, "span 3, growx"); 123 | 124 | sanityCheck(); 125 | 126 | } 127 | 128 | /** 129 | * Ensures the min and max values are within the allowed range, and that minimum < maximum. 130 | * Renames/disables the min and max textboxes depending on the axis type and autoscaling. 131 | * 132 | * Notifies all handlers. 133 | */ 134 | public void sanityCheck() { 135 | 136 | String axisType = axisTypeCombobox.getSelectedItem().toString(); 137 | 138 | // rename labels depending on axis type 139 | if(axisType.equals("Relative Frequency") || axisType.equals("Both")) { 140 | maxLabel.setText("Relative Frequency Maximum: "); 141 | minLabel.setText("Relative Frequency Minimum: "); 142 | } else if(axisType.equals("Frequency")) { 143 | maxLabel.setText("Frequency Maximum: "); 144 | minLabel.setText("Frequency Minimum: "); 145 | } 146 | 147 | axisTypeHandler.accept(axisType.equals("Relative Frequency") || axisType.equals("Both"), axisType.equals("Frequency") || axisType.equals("Both")); 148 | 149 | // sanity check the min and max 150 | try { 151 | 152 | float max = Float.parseFloat(maxTextfield.getText().trim()); 153 | float min = Float.parseFloat(minTextfield.getText().trim()); 154 | 155 | if(maxLabel.getText().equals("Frequency Maximum: ")) { 156 | max = (float) Math.floor(max); 157 | min = (float) Math.floor(min); 158 | } 159 | 160 | // clip to limits 161 | if(maxLabel.getText().equals("Relative Frequency Maximum: ")) { 162 | if(max > relativeFrequencyUpperLimit) max = relativeFrequencyUpperLimit; 163 | if(max < relativeFrequencyLowerLimit) max = relativeFrequencyLowerLimit; 164 | if(min > relativeFrequencyUpperLimit) min = relativeFrequencyUpperLimit; 165 | if(min < relativeFrequencyLowerLimit) min = relativeFrequencyLowerLimit; 166 | } else { 167 | if(max > frequencyUpperLimit) max = frequencyUpperLimit; 168 | if(max < frequencyLowerLimit) max = frequencyLowerLimit; 169 | if(min > frequencyUpperLimit) min = frequencyUpperLimit; 170 | if(min < frequencyLowerLimit) min = frequencyLowerLimit; 171 | } 172 | 173 | // ensure min < max 174 | if(maxLabel.getText().equals("Relative Frequency Maximum: ")) { 175 | if(min == max) { 176 | if(max == relativeFrequencyUpperLimit) 177 | min = Math.nextDown(min); 178 | else 179 | max = Math.nextUp(max); 180 | } else if(min > max) { 181 | float temp = max; 182 | max = min; 183 | min = temp; 184 | } 185 | } else { 186 | if(min == max) { 187 | if(max == frequencyUpperLimit) 188 | min--; 189 | else 190 | max++; 191 | } else if(min > max) { 192 | float temp = max; 193 | max = min; 194 | min = temp; 195 | } 196 | } 197 | 198 | // update textfields 199 | maxTextfield.setText(Float.toString(max)); 200 | minTextfield.setText(Float.toString(min)); 201 | maxHandler.accept(maxCheckbox.isSelected(), max); 202 | minHandler.accept(minCheckbox.isSelected(), min); 203 | 204 | } catch(Exception e) { 205 | 206 | // one of the textfields doesn't contain a valid number, so reset both to defaults 207 | if(axisType.equals("Relative Frequency") || axisType.equals("Both")) { 208 | maxTextfield.setText(Float.toString(relativeFrequencyDefaultMaximum)); 209 | minTextfield.setText(Float.toString(relativeFrequencyDefaultMinimum)); 210 | maxHandler.accept(maxCheckbox.isSelected(), relativeFrequencyDefaultMaximum); 211 | minHandler.accept(minCheckbox.isSelected(), relativeFrequencyDefaultMinimum); 212 | } else { 213 | maxTextfield.setText(Float.toString(frequencyDefaultMaximum)); 214 | minTextfield.setText(Float.toString(frequencyDefaultMinimum)); 215 | maxHandler.accept(maxCheckbox.isSelected(), frequencyDefaultMaximum); 216 | minHandler.accept(minCheckbox.isSelected(), frequencyDefaultMinimum); 217 | } 218 | 219 | } 220 | 221 | // disable textboxes for autoscaled values 222 | maxTextfield.setEnabled(!maxCheckbox.isSelected()); 223 | minTextfield.setEnabled(!minCheckbox.isSelected()); 224 | 225 | } 226 | 227 | /** 228 | * Updates the widget and chart based on settings from a layout file. 229 | * 230 | * @param lines A queue of remaining lines from the layout file. 231 | */ 232 | @Override public void importState(ConnectionsController.QueueOfLines lines) { 233 | 234 | // parse the text 235 | boolean yAxisShowsRelativeFrequency = ChartUtils.parseBoolean(lines.remove(), "y-axis shows relative frequency = %b"); 236 | boolean yAxisShowsFrequency = ChartUtils.parseBoolean(lines.remove(), "y-axis shows frequency = %b"); 237 | boolean yMinimumIsZero = ChartUtils.parseBoolean(lines.remove(), "y-axis minimum is zero = %b"); 238 | boolean yAutoscaleMax = ChartUtils.parseBoolean(lines.remove(), "y-axis autoscale maximum = %b"); 239 | float manualMinY = ChartUtils.parseFloat (lines.remove(), "y-axis manual minimum = %f"); 240 | float manualMaxY = ChartUtils.parseFloat (lines.remove(), "y-axis manual maximum = %f"); 241 | 242 | // update the widget 243 | String type = (yAxisShowsRelativeFrequency && yAxisShowsFrequency) ? "Both" : yAxisShowsRelativeFrequency ? "Relative Frequency" : "Frequency"; 244 | for(int i = 0; i < axisTypeCombobox.getItemCount(); i++) 245 | if(axisTypeCombobox.getItemAt(i).toString().equals(type)) 246 | axisTypeCombobox.setSelectedIndex(i); 247 | 248 | minCheckbox.setSelected(yMinimumIsZero); 249 | minTextfield.setText(Float.toString(manualMinY)); 250 | 251 | maxCheckbox.setSelected(yAutoscaleMax); 252 | maxTextfield.setText(Float.toString(manualMaxY)); 253 | 254 | // update the chart 255 | sanityCheck(); 256 | 257 | } 258 | 259 | /** 260 | * Saves the current state to one or more lines of text. 261 | * 262 | * @return A String[] where each element is a line of text. 263 | */ 264 | @Override public String[] exportState() { 265 | 266 | return new String[] { 267 | "y-axis shows relative frequency = " + (axisTypeCombobox.getSelectedIndex() == 0 || axisTypeCombobox.getSelectedIndex() == 2), 268 | "y-axis shows frequency = " + (axisTypeCombobox.getSelectedIndex() == 1 || axisTypeCombobox.getSelectedIndex() == 2), 269 | "y-axis minimum is zero = " + minCheckbox.isSelected(), 270 | "y-axis autoscale maximum = " + maxCheckbox.isSelected(), 271 | "y-axis manual minimum = " + minTextfield.getText(), 272 | "y-axis manual maximum = " + maxTextfield.getText() 273 | }; 274 | 275 | } 276 | 277 | } 278 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/WidgetTextfieldInteger.java: -------------------------------------------------------------------------------- 1 | import java.awt.event.FocusEvent; 2 | import java.awt.event.FocusListener; 3 | import java.util.function.Consumer; 4 | 5 | import javax.swing.JLabel; 6 | import javax.swing.JTextField; 7 | import javax.swing.SwingUtilities; 8 | 9 | public class WidgetTextfieldInteger extends Widget { 10 | 11 | String label; 12 | JTextField textfield; 13 | Consumer handler; 14 | int defaultValue; 15 | int lowerLimit; 16 | int upperLimit; 17 | 18 | /** 19 | * A widget that lets the user specify an integer with a textfield. 20 | * 21 | * @param textLabel Label to show at the left of the textfield. 22 | * @param defaultValue Default value. 23 | * @param lowerLimit Minimum allowed value. 24 | * @param upperLimit Maximum allowed value. 25 | * @param eventHandler Will be notified when the textfield changes. 26 | */ 27 | public WidgetTextfieldInteger(String textLabel, int defaultValue, int lowerLimit, int upperLimit, Consumer eventHandler) { 28 | 29 | super(); 30 | 31 | label = textLabel; 32 | handler = eventHandler; 33 | this.defaultValue = defaultValue; 34 | this.lowerLimit = lowerLimit; 35 | this.upperLimit = upperLimit; 36 | 37 | textfield = new JTextField(Integer.toString(defaultValue)); 38 | textfield.addFocusListener(new FocusListener() { 39 | @Override public void focusLost(FocusEvent fe) { sanityCheck(); } 40 | @Override public void focusGained(FocusEvent fe) { textfield.selectAll(); } 41 | }); 42 | textfield.addActionListener(event -> sanityCheck()); 43 | 44 | widgets.put(new JLabel(label + ": "), ""); 45 | widgets.put(textfield, "span 3, growx"); 46 | 47 | SwingUtilities.invokeLater(() -> sanityCheck()); // invokeLater to ensure the chart has finish constructing before notifying it 48 | 49 | } 50 | 51 | /** 52 | * Ensures the number is within the allowed range, then notifies the handlers. 53 | */ 54 | public void sanityCheck() { 55 | 56 | try { 57 | 58 | int i = Integer.parseInt(textfield.getText().trim()); 59 | if(i < lowerLimit) 60 | i = lowerLimit; 61 | else if(i > upperLimit) 62 | i = upperLimit; 63 | 64 | textfield.setText(Integer.toString(i)); 65 | handler.accept(i); 66 | 67 | } catch(Exception e) { 68 | 69 | textfield.setText(Integer.toString(defaultValue)); 70 | handler.accept(defaultValue); 71 | 72 | } 73 | 74 | } 75 | 76 | /** 77 | * Updates the widget and chart based on settings from a layout file. 78 | * 79 | * @param lines A queue of remaining lines from the layout file. 80 | */ 81 | @Override public void importState(ConnectionsController.QueueOfLines lines) { 82 | 83 | // parse the text 84 | int number = ChartUtils.parseInteger(lines.remove(), label.trim().toLowerCase() + " = %d"); 85 | 86 | // update the widget 87 | textfield.setText(Integer.toString(number)); 88 | 89 | // update the chart 90 | sanityCheck(); 91 | 92 | } 93 | 94 | /** 95 | * Saves the current state to one or more lines of text. 96 | * 97 | * @return A String[] where each element is a line of text. 98 | */ 99 | @Override public String[] exportState() { 100 | 101 | return new String[] { 102 | label.trim().toLowerCase() + " = " + textfield.getText() 103 | }; 104 | 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /Telemetry Viewer/src/WidgetTextfieldsOptionalMinMax.java: -------------------------------------------------------------------------------- 1 | import java.awt.event.FocusEvent; 2 | import java.awt.event.FocusListener; 3 | import java.util.function.BiConsumer; 4 | 5 | import javax.swing.Box; 6 | import javax.swing.BoxLayout; 7 | import javax.swing.JCheckBox; 8 | import javax.swing.JLabel; 9 | import javax.swing.JPanel; 10 | import javax.swing.JTextField; 11 | 12 | public class WidgetTextfieldsOptionalMinMax extends Widget { 13 | 14 | String prefix; 15 | JCheckBox maxCheckbox; 16 | JCheckBox minCheckbox; 17 | JTextField maxTextfield; 18 | JTextField minTextfield; 19 | float upperLimit; 20 | float lowerLimit; 21 | float defaultMax; 22 | float defaultMin; 23 | BiConsumer minHandler; 24 | BiConsumer maxHandler; 25 | 26 | /** 27 | * A widget that lets the user make minimum and maximum values be fixed or autoscaled. 28 | * 29 | * @param labelPrefix Text to show before the "Minimum" or "Maximum" label. 30 | * @param allowAutoscale True to allow autoscaling, false to require specific min/max values. 31 | * @param defaultMin Default value for minimum. 32 | * @param defaultMax Default value for maximum. 33 | * @param lowerLimit Minimum allowed value. 34 | * @param upperLimit Maximum allowed value. 35 | * @param minEventHandler Will be notified when the minimum changes. 36 | * @param maxEventHandler Will be notified when the maximum changes. 37 | */ 38 | public WidgetTextfieldsOptionalMinMax(String labelPrefix, boolean allowAutoscale, float defaultMin, float defaultMax, float lowerLimit, float upperLimit, BiConsumer minEventHandler, BiConsumer maxEventHandler) { 39 | 40 | super(); 41 | 42 | prefix = labelPrefix; 43 | this.upperLimit = upperLimit; 44 | this.lowerLimit = lowerLimit; 45 | this.defaultMax = defaultMax; 46 | this.defaultMin = defaultMin; 47 | minHandler = minEventHandler; 48 | maxHandler = maxEventHandler; 49 | 50 | maxTextfield = new JTextField(Float.toString(defaultMax)); 51 | maxTextfield.setEnabled(false); 52 | maxTextfield.addFocusListener(new FocusListener() { 53 | @Override public void focusLost(FocusEvent fe) { sanityCheck(); } 54 | @Override public void focusGained(FocusEvent fe) { maxTextfield.selectAll(); } 55 | }); 56 | maxTextfield.addActionListener(event -> sanityCheck()); 57 | 58 | maxCheckbox = new JCheckBox("Automatic", allowAutoscale); 59 | maxCheckbox.addActionListener(event -> sanityCheck()); 60 | 61 | minTextfield = new JTextField(Float.toString(defaultMin)); 62 | minTextfield.setEnabled(false); 63 | minTextfield.addFocusListener(new FocusListener() { 64 | @Override public void focusLost(FocusEvent fe) { sanityCheck(); } 65 | @Override public void focusGained(FocusEvent fe) { minTextfield.selectAll(); } 66 | }); 67 | minTextfield.addActionListener(event -> sanityCheck()); 68 | 69 | minCheckbox = new JCheckBox("Automatic", allowAutoscale); 70 | minCheckbox.addActionListener(event -> sanityCheck()); 71 | 72 | JPanel maxPanel = new JPanel(); 73 | maxPanel.setLayout(new BoxLayout(maxPanel, BoxLayout.X_AXIS)); 74 | if(allowAutoscale) { 75 | maxPanel.add(maxCheckbox); 76 | maxPanel.add(Box.createHorizontalStrut(Theme.padding)); 77 | } 78 | maxPanel.add(maxTextfield); 79 | 80 | widgets.put(new JLabel(labelPrefix + " Maximum: "), ""); 81 | widgets.put(maxPanel, "span 3, growx"); 82 | 83 | JPanel minPanel = new JPanel(); 84 | minPanel.setLayout(new BoxLayout(minPanel, BoxLayout.X_AXIS)); 85 | if(allowAutoscale) { 86 | minPanel.add(minCheckbox); 87 | minPanel.add(Box.createHorizontalStrut(Theme.padding)); 88 | } 89 | minPanel.add(minTextfield); 90 | 91 | widgets.put(new JLabel(labelPrefix + " Minimum: "), ""); 92 | widgets.put(minPanel, "span 3, growx"); 93 | 94 | sanityCheck(); 95 | 96 | } 97 | 98 | /** 99 | * Ensures that both values are within the allowed range, and that minimum < maximum. 100 | * Disables min/max textfields if they are autoscaled. 101 | * 102 | * Notifies all handlers. 103 | */ 104 | public void sanityCheck() { 105 | 106 | try { 107 | 108 | float min = Float.parseFloat(minTextfield.getText().trim()); 109 | float max = Float.parseFloat(maxTextfield.getText().trim()); 110 | 111 | // clip to limits 112 | if(min > upperLimit) min = upperLimit; 113 | if(min < lowerLimit) min = lowerLimit; 114 | if(max > upperLimit) max = upperLimit; 115 | if(max < lowerLimit) max = lowerLimit; 116 | 117 | // ensure min < max 118 | if(min == max) { 119 | if(max == upperLimit) 120 | min = Math.nextDown(min); 121 | else 122 | max = Math.nextUp(max); 123 | } else if(min > max) { 124 | float temp = max; 125 | max = min; 126 | min = temp; 127 | } 128 | 129 | // update textfields 130 | minTextfield.setText(Float.toString(min)); 131 | maxTextfield.setText(Float.toString(max)); 132 | 133 | minHandler.accept(minCheckbox.isSelected(), min); 134 | maxHandler.accept(maxCheckbox.isSelected(), max); 135 | 136 | } catch(Exception e) { 137 | 138 | // one of the textfields doesn't contain a valid number, so reset both to defaults 139 | minTextfield.setText(Float.toString(defaultMin)); 140 | maxTextfield.setText(Float.toString(defaultMax)); 141 | 142 | minHandler.accept(minCheckbox.isSelected(), defaultMin); 143 | maxHandler.accept(maxCheckbox.isSelected(), defaultMax); 144 | 145 | } 146 | 147 | maxTextfield.setEnabled(!maxCheckbox.isSelected()); 148 | minTextfield.setEnabled(!minCheckbox.isSelected()); 149 | 150 | } 151 | 152 | /** 153 | * Updates the widget and chart based on settings from a layout file. 154 | * 155 | * @param lines A queue of remaining lines from the layout file. 156 | */ 157 | @Override public void importState(ConnectionsController.QueueOfLines lines) { 158 | 159 | // parse the text 160 | boolean autoscaleMin = ChartUtils.parseBoolean(lines.remove(), "autoscale " + prefix.trim().toLowerCase() + " minimum = %b"); 161 | float manualMin = ChartUtils.parseFloat (lines.remove(), "manual " + prefix.trim().toLowerCase() + " minimum = %f"); 162 | boolean autoscaleMax = ChartUtils.parseBoolean(lines.remove(), "autoscale " + prefix.trim().toLowerCase() + " maximum = %b"); 163 | float manualMax = ChartUtils.parseFloat (lines.remove(), "manual " + prefix.trim().toLowerCase() + " maximum = %f"); 164 | 165 | // update the widget 166 | minCheckbox.setSelected(autoscaleMin); 167 | minTextfield.setText(Float.toString(manualMin)); 168 | maxCheckbox.setSelected(autoscaleMax); 169 | maxTextfield.setText(Float.toString(manualMax)); 170 | 171 | // update the chart 172 | sanityCheck(); 173 | 174 | } 175 | 176 | /** 177 | * Saves the current state to one or more lines of text. 178 | * 179 | * @return A String[] where each element is a line of text. 180 | */ 181 | @Override public String[] exportState() { 182 | 183 | return new String[] { 184 | "autoscale " + prefix.trim().toLowerCase() + " minimum = " + minCheckbox.isSelected(), 185 | "manual " + prefix.trim().toLowerCase() + " minimum = " + minTextfield.getText(), 186 | "autoscale " + prefix.trim().toLowerCase() + " maximum = " + maxCheckbox.isSelected(), 187 | "manual " + prefix.trim().toLowerCase() + " maximum = " + maxTextfield.getText() 188 | }; 189 | 190 | } 191 | 192 | } 193 | -------------------------------------------------------------------------------- /Telemetry Viewer/test/StorageTimestampsTest.java: -------------------------------------------------------------------------------- 1 | // TODO: update this test and write more tests 2 | 3 | //import static org.junit.jupiter.api.Assertions.*; 4 | // 5 | //import java.nio.FloatBuffer; 6 | //import java.nio.file.FileAlreadyExistsException; 7 | //import java.nio.file.Files; 8 | //import java.nio.file.Paths; 9 | //import java.util.ArrayList; 10 | //import java.util.List; 11 | //import java.util.Random; 12 | //import java.util.stream.Stream; 13 | // 14 | //import org.junit.jupiter.api.AfterEach; 15 | //import org.junit.jupiter.api.BeforeEach; 16 | //import org.junit.jupiter.api.DisplayName; 17 | //import org.junit.jupiter.params.ParameterizedTest; 18 | //import org.junit.jupiter.params.provider.Arguments; 19 | //import org.junit.jupiter.params.provider.MethodSource; 20 | 21 | class StorageTimestampsTest { 22 | 23 | // /** 24 | // * Possible tests: 25 | // * 26 | // * - Can't read sample(s) from empty. 27 | // * - Can't read sample(s) in excess of existing data. 28 | // * - Can't read negative sample numbers. 29 | // * - getFirstTimestamp() is correct. 30 | // * - Sample(s) match inserted data. 31 | // * - Flushing to disk at different points does not cause problems. 32 | // * - Flushing to disk frees up memory. 33 | // * - Writing from multiple threads works correctly. 34 | // * 35 | // */ 36 | // 37 | // final int SAMPLE_COUNT = 16 * (int) Math.pow(2, 20); // 16M 38 | // 39 | // static int[] riskyNumbers() { 40 | // return new int[] { 41 | // 1, 42 | // StorageFloats.BLOCK_SIZE - 1, 43 | // StorageFloats.BLOCK_SIZE, 44 | // StorageFloats.BLOCK_SIZE + 1, 45 | // StorageFloats.SLOT_SIZE - 1, 46 | // StorageFloats.SLOT_SIZE, 47 | // StorageFloats.SLOT_SIZE + 1, 48 | // 3*StorageFloats.SLOT_SIZE - 1, 49 | // 3*StorageFloats.SLOT_SIZE, 50 | // 3*StorageFloats.SLOT_SIZE + 1, 51 | // 4*StorageFloats.SLOT_SIZE - 1, 52 | // 4*StorageFloats.SLOT_SIZE, 53 | // 4*StorageFloats.SLOT_SIZE + 1, 54 | // }; 55 | // } 56 | // 57 | // ConnectionTelemetry connection; 58 | // StorageTimestamps DUT; 59 | // StorageTimestamps.Cache cache; 60 | // long[] timestamps; 61 | // 62 | // @BeforeEach 63 | // void prepare() { 64 | // 65 | // try { Files.createDirectory(Paths.get("cache")); } catch(FileAlreadyExistsException e) {} catch(Exception e) { e.printStackTrace(); } 66 | // connection = new ConnectionTelemetry("Demo Mode"); 67 | // DUT = new StorageTimestamps(connection); 68 | // cache = DUT.createCache(); 69 | // 70 | // timestamps = new long[SAMPLE_COUNT]; 71 | // Random rng = new Random(); 72 | // for(int i = 0; i < SAMPLE_COUNT; i++) 73 | // timestamps[i] = rng.nextLong(); 74 | // 75 | // for(int sampleN = 0; sampleN < SAMPLE_COUNT; sampleN++) 76 | // DUT.appendTimestamp(timestamps[sampleN]); 77 | // 78 | // } 79 | // 80 | // @DisplayName(value = "Individual Timestamps") 81 | // @ParameterizedTest(name = "Flushing after every {0} samples") 82 | // void individualTimestamps() { 83 | // 84 | // for(int sampleN = 0; sampleN < SAMPLE_COUNT; sampleN += 16) { 85 | // assertTrue(DUT.getTimestamp(sampleN) == timestamps[sampleN]); 86 | // } 87 | // 88 | // } 89 | // 90 | // @DisplayName(value = "Timestamp Blocks, Varying Block Sizes / Offsets") 91 | // @ParameterizedTest(name = "Reading {0} bytes at offset {1}") 92 | // @MethodSource("riskyNumbersPair") 93 | // void blocksOfTimestamps(int blockSize, int offset) { 94 | // 95 | // FloatBuffer buffer = DUT.getTampstamps(offset, offset + blockSize - 1, cache, 0); 96 | // for(int i = 0; i < blockSize; i++) 97 | // assertTrue(buffer.get(i) == (float) timestamps[offset + i]); 98 | // 99 | // } 100 | // 101 | // static Stream riskyNumbersPair() { 102 | // 103 | // List list = new ArrayList(); 104 | // for(int x : riskyNumbers()) 105 | // for(int y : riskyNumbers()) 106 | // list.add(Arguments.of(x, y)); 107 | // 108 | // return list.stream(); 109 | // 110 | // } 111 | // 112 | // @AfterEach 113 | // void deleteCacheFiles() { 114 | // 115 | // DUT.dispose(); 116 | // connection.dispose(); 117 | // 118 | // } 119 | 120 | } 121 | --------------------------------------------------------------------------------