└── 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 |
--------------------------------------------------------------------------------