├── .github
└── workflows
│ └── node.js.yml
├── LICENSE.txt
├── README.md
├── clients
├── MPU6050_HID_tests
│ ├── .theia
│ │ └── launch.json
│ └── MPU6050_HID_tests.ino
├── bash
│ └── sample.sh
├── cpp
│ ├── Telecmd.h
│ ├── Teleplot.h
│ └── sample.cpp
└── python
│ ├── full_test.py
│ ├── imu.py
│ ├── main.py
│ ├── mickey.py
│ ├── test_log.py
│ └── xy.py
├── doc
├── README.md
└── UMLClassDiagram.groovy
├── images
├── logo-color.png
├── logo-color.svg
├── logo.svg
├── preview-vscode.png
├── preview.jpg
├── wandercraft-logo.png
└── wandercraft.png
├── server
├── .dockerignore
├── Dockerfile
├── docker-compose.yml
├── main.js
├── package.json
└── www
│ ├── classes
│ ├── 3d
│ │ ├── Shape3D.js
│ │ └── World.js
│ ├── DataSerie.js
│ ├── Telemetry.js
│ ├── communication
│ │ ├── connection
│ │ │ ├── Connection.js
│ │ │ ├── ConnectionTeleplotVSCode.js
│ │ │ └── ConnectionTeleplotWebsocket.js
│ │ ├── data
│ │ │ ├── DataInput.js
│ │ │ ├── DataInputSerial.js
│ │ │ └── DataInputUDP.js
│ │ └── serverMessageReading.js
│ └── view
│ │ ├── logs
│ │ ├── Log.js
│ │ └── LogConsole.js
│ │ └── widgets
│ │ ├── ChartWidget.js
│ │ ├── DataWidget.js
│ │ ├── SingleValueWidget.js
│ │ └── Widget3D.js
│ ├── components
│ ├── 3dComponent
│ │ ├── 3dComponent.css
│ │ └── 3dComponent.js
│ ├── singleValue
│ │ ├── single-value.css
│ │ └── singleValue.js
│ ├── uPlot
│ │ └── uplot-component.js
│ └── vueResponsiveText
│ │ ├── vue-responsive-text.css
│ │ └── vue-responsive-text.js
│ ├── constants.js
│ ├── favicon.ico
│ ├── images
│ └── logo-color.svg
│ ├── index.html
│ ├── lib
│ ├── hyperlist
│ │ └── hyperlist.js
│ ├── three
│ │ ├── InfiniteGrid.js
│ │ ├── OrbitControls.js
│ │ ├── three.js
│ │ ├── three.min.js
│ │ └── three.module.js
│ ├── uPlot
│ │ ├── uPlot.min.css
│ │ └── uplot.iife.js
│ └── vue.js
│ ├── main.js
│ ├── style-dark.css
│ ├── style.css
│ └── utils
│ ├── import_export
│ ├── fileManagement.js
│ ├── layout.js
│ └── session.js
│ ├── javascriptUtils.js
│ ├── stats
│ └── computeStats.js
│ └── view
│ ├── cursor.js
│ ├── initializeAppView.js
│ ├── paint.js
│ └── updateView.js
└── vscode
├── .eslintrc.js
├── .vscode
├── extensions.json
├── launch.json
├── settings.json
└── tasks.json
├── LICENSE.txt
├── README.md
├── images
├── logo-color.png
├── preview-vscode.png
└── wandercraft.png
├── package.json
├── src
└── extension.ts
└── tsconfig.json
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3 |
4 | name: Server binaries
5 |
6 | on:
7 | push:
8 | branches: [ "main" ]
9 | pull_request:
10 | branches: [ "main" ]
11 |
12 | jobs:
13 | build:
14 | if: ${{ false }} # disable for now
15 | runs-on: ubuntu-18.04
16 | strategy:
17 | matrix:
18 | include:
19 | - arch: armv7
20 | distro: ubuntu18.04
21 | base: arm32v7/node:18-buster
22 | target: node18-linuxstatic-armv7
23 | #node-version: [14.x]
24 | #pkg-target: ["node14-windows-x64", "node14-linux-x64", "node14-linux-armv7"]
25 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
26 |
27 | steps:
28 | - uses: actions/checkout@v3
29 | - uses: uraimo/run-on-arch-action@v2
30 | name: Build artifact
31 | id: build
32 | with:
33 | #arch: ${{ matrix.arch }}
34 | #distro: ${{ matrix.distro }}
35 | base_image: ${{ matrix.base }}
36 | githubToken: ${{ github.token }}
37 | setup: |
38 | mkdir -p "${PWD}/artifacts"
39 | dockerRunArgs: |
40 | --volume "${PWD}/artifacts:/artifacts"
41 | env: | # YAML, but pipe character is necessary
42 | artifact_name: git-${{ matrix.distro }}_${{ matrix.arch }}
43 | shell: /bin/sh
44 | install: |
45 | case "${{ matrix.distro }}" in
46 | ubuntu*|jessie|stretch|buster|bullseye)
47 | apt-get update -q -y
48 | apt-get install -q -y git
49 | ;;
50 | fedora*)
51 | dnf -y update
52 | dnf -y install git which
53 | ;;
54 | alpine*)
55 | apk update
56 | apk add git
57 | ;;
58 | esac
59 | run: |
60 | uname -a
61 | cd server
62 | npm i -g pkg
63 | pkg --targets ${{ matrix.target }} --out-path /artifacts/ .
64 |
65 | - name: Show the artifact
66 | run: |
67 | ls -al "${PWD}/artifacts"
68 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Alexandre Brehmer
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Teleplot
4 |
5 | A ridiculously simple tool to plot telemetry data from a running program and trigger function calls.
6 |
7 | 
8 |
9 | `echo "myData:4" | nc -u -w0 127.0.0.1 47269`
10 |
11 | # Test it online
12 |
13 | **Go to [teleplot.fr](https://teleplot.fr)**
14 |
15 | # Supports
16 |
17 | Teleplot project received the generous technical support of [Wandercraft](https://www.wandercraft.eu/).
18 |
19 | 
20 |
21 | # Start the server
22 |
23 | ## As a binary
24 | ```bash
25 | cd server
26 | npm i
27 | sudo npm run-script make
28 | ./build/teleplot
29 | ```
30 |
31 | > Current target is x64 and configurable in `package.json -> pkg/targets`
32 |
33 | ## Using node
34 | ```bash
35 | cd server
36 | npm i
37 | node main.js
38 | ```
39 |
40 | Open your navigator at [127.0.0.1:8080](127.0.0.1:8080)
41 |
42 | ## Using docker
43 | ```bash
44 | cd server
45 | docker build -t teleplot .
46 | docker run -d -p 8080:8080 -p 47269:47269/udp teleplot
47 | ```
48 |
49 | Open your navigator at [127.0.0.1:8080](127.0.0.1:8080)
50 |
51 | ## Using docker-compose
52 | ```bash
53 | cd server
54 | docker-compose build
55 | docker-compose up
56 | ```
57 |
58 | Open your navigator at [127.0.0.1:8080](127.0.0.1:8080)
59 |
60 | # Telemetry Format
61 |
62 | A telemetry gets published by sending a text-based UDP packet on the port `47269`. As it's a trivial thing to do on the vast majority of languages, it makes it very easy to publish from anywhere.
63 |
64 | The telemetry format is inspired by `statsd` and *to some extents* compatible with it.
65 |
66 | The expected format is `A:B:C§D|E` where:
67 | - **A** is the name of the telemetry variable (be kind and avoid **`:|`** special chars in it!)
68 | - **B** is **optional** and represents the timestamp in milliseconds (`1627551892437`). If omitted, like in `myValue:1234|g`, the reception timestamp will be used, wich will create some precision loss due to the networking.
69 | - **C** is either the integer or floating point value to be plotted or a text format value to be displayed.
70 | - **D** is **optional** and is the unit of the telemetry ( please avoid **`,;:|.`** special chars in it!)
71 | - **E** is containing flags that carry information on how to read and display the data.
72 |
73 | Examples:
74 | - `myValue:1234`
75 | - `myValue:1234|`
76 | - `myValue:12.34e+2`
77 | - `myValue:1627551892437:1234`
78 | - `myValue:hello|t`
79 | - `myValue:1234§km²`
80 |
81 | ### Plot XY rather than time-based
82 |
83 | Using the `xy` flag, and providing a value in both **B** and **C** field, teleplot will display an YX line chart.
84 |
85 | - `trajectory:12.3:45.67|xy`
86 |
87 | A timestamp can be associated with the xy point by adding an extra `:1627551892437` after the **C** field.
88 |
89 | - `trajectoryTimestamped:1:1:1627551892437;2:2:1627551892448;3:3:1627551892459|xy`
90 |
91 | The YX line chart will only be displayed when at least two values have been received.
92 |
93 | > Using `clr` flag when sending a telemetry will clear the previous values. This is useful when streaming cyclic data like lidar points.
94 |
95 | ### Publishing text format telemetries
96 | - Using the `t` flag and giving a text telemetry (with or without timestamp), teleplot will display a text chart.
97 |
98 | - `motor_4_state:Turned On|t`
99 | - `motor_4_state:1627551892437:Off|t`
100 |
101 | ### Publishing multiple points
102 | /!\ here, many values will be received at the same time by teleplot, therefore you must precise their timestamps.
103 |
104 | Multiple values of a single telemetry can be sent in a single packet if separated by a `;`
105 |
106 | - `trajectory:1:1;2:2;3:3;4:4|xy`
107 | - `myValue:1627551892444:1;1627551892555:2;1627551892666:3`
108 | - `myValue:1627551892444:1;1627551892555:2;1627551892666:3§rad`
109 | - `state:1627551892444:state_a;1627551892555:state_b|t`
110 |
111 | ### Publishing multiple telemetries
112 |
113 | Multiple telemetries can be sent in a single packet if separated by a `\n`
114 |
115 | ```
116 | myValue:1234
117 | mySecondValue:1234:m/s
118 | myThirdValue:1627551892437:1234
119 | state:state_a|t
120 | trajectory:1:1;2:2;3:3;4:4|xy
121 | trajectoryTimestamped:1:1:1627551892437;2:2:1627551892448;3:3:1627551892459|xy
122 | ```
123 |
124 | > Notice that your data needs to fit in a single UPD packet whick can be limited to 512(Internet), 1432(Intranets) or 8932(Jumbo frames) Bytes depending on the network.
125 |
126 | ### Prevent auto-plot of telemetry
127 |
128 | By default, teleplot will display all the incoming telemetry as a chart, while this is handy for new user with small amount of data, this might not be desired with lots of data.
129 | The `np` (for no-plot) flag can be used to prevent this behavior:
130 | - `myValue:1627551892437:1234|np`
131 | - `trajectory:12.3:45.67|xy,np`
132 |
133 | ### Publishing 3D telemetries
134 |
135 | To send 3D shapes to teleplot, use this syntax : `3D|A:B:C|E`, where
136 | **A** is the name of the shape telemetry
137 | **B** is the timestamp of the telemetry and is **optional**
138 | **C** is a text representing the shape
139 | **E** is containing flags and is **optional**
140 |
141 | ### Writing **A** (the name of the shape telemetry)
142 |
143 | if **A** contains a comma, the text after the comma will be considered as a 'widget label', if multiple telemtries are sent with the same
144 | widget label, they will automatically be displayed on the same widget.
145 | The text before the comma will be considered as the name of the shape telemetry.
146 |
147 | If **A** doesn't contain a comma, its whole text will be considered as the name of the shape telemetry.
148 |
149 | #### Writing **C** (the text representing the shape)
150 |
151 | **C** is built as a concatenation of the shape properties followed by their values, the whole with colons in between.
152 |
153 | LIST OF PROPERTIES :
154 |
155 | - "shape" or "S" => the shape type (either "cube" or "sphere" for the moment).
156 |
157 | - "position" or "P" => the position of the center of the sphere in a cartesian coordinate system
158 | 1st argument : x, 2nd argument : y, 3rd argument : z
159 |
160 | - "rotation" or "R" => the rotation ( in radian ) of the shape using Euler angles ( please avoid this method and use a quaternion instead )
161 | 1st argument : the rotation around the x axis, 2nd argument : the rotation around the y axis, 3rd argument : the rotation around the z axis
162 |
163 | - "quaternion" or "Q" => the rotation of the shape using a quaternion
164 | 1st argument : x coordinate, 2nd argument : y coordinate, 3rd argument : z coordinate, 4th argument : w coordinate
165 |
166 | - "color" or "C" => the color of the shape, ex : "blue", "#2ecc71" ...
167 |
168 | - "opacity" or "O" => the opacity of the shape, float between 0 and 1, 0 being fully transparent and 1 fully opaque ( set to 1 by default )
169 |
170 | === Sphere only ===
171 |
172 | - "precision" or "PR" => the number of rectangles used to draw a sphere (the bigger the more precise, by default = 15)
173 |
174 | - "radius" or "RA"=> the radius of the sphere
175 |
176 | === Cube only ===
177 |
178 | - "height" or "H" => the height of the cube ( Y axis )
179 | - "width" or "W" => the width of the cube ( X axis )
180 | - "depth" or "D" => the depth of the cube ( Z axis )
181 |
182 |
183 |
184 | If you don't want to send all the arguments of a certain property, you still have to add colons ( ex : position::-1:1, here the x position is not given but colons are still present )
185 |
186 | for unspecified properties, teleplot will use the ones from the previous shape state.
187 |
188 | If it is the first time teleplot receives data of a certain shape, the property "shape" (cube or sphere) has to be given and
189 | missing properties will be replaced by default ones.
190 |
191 | Also, the shape, color and precision properties can not be changed later on.
192 |
193 | #### Some examples
194 | Creating a simple sphere and cube :
195 |
196 | cube without timestamp :
197 | - `3D|mySimpleCube:S:cube:P:1:1:1:R:0:0:0:W:2:H:2:D:2:C:#2ecc71`
198 |
199 | sphere with timestamp :
200 | - `3D|mySimpleSphere:1627551892437:S:sphere:P::2::RA:2:C:red`
201 |
202 | Creating a cube that grows and rotates :
203 |
204 | In the first request we send, we need to specify the shape:
205 | - `3D|my_super_cube:S:cube:W:1:D:1:H:1:C:blue`
206 |
207 | Then we can specify only properties that change :
208 | - `3D|my_super_cube:W:1.2:R::0.2:`
209 | - `3D|my_super_cube:W:1.4:R::0.4:`
210 | - `3D|my_super_cube:W:1.6:R::0.6:`
211 |
212 | Creating a simple sphere and cube and display them on the same widget by default :
213 |
214 | cube with widget label 'widget0' :
215 | - `3D|myCube,widget0:S:cube`
216 |
217 | sphere with same widget label :
218 | - `3D|mySphere,widget0:S:sphere`
219 |
220 |
221 | /!\ Despite the examples above, it might be a better idea to send every property everytime, as if teleplot refreshes or if it wasn't lauched
222 | before you sent certain properties, it will not have any way to be aware of the properties you sent previously.
223 | Therefore, it will have to use default values, or may not display anything at all, if it is not informed of the shape type for instance.
224 |
225 | /!\ If you send shapes at a too high frequency, teleplot will quickly be overloaded, also teleplot can only draw 3D shapes at a maximum speed
226 | of 50-60 frames per second, therefore it is not recommended to send more than 60 shapes per seconds.
227 |
228 | # Publish telemetries
229 |
230 | ## Bash
231 |
232 | ```bash
233 | echo "myValue:1234" | nc -u -w0 127.0.0.1 47269
234 | ```
235 |
236 | ## C++
237 |
238 | Copy `Teleplot.h` (from `clients/cpp`) in your project and use its object.
239 | ```cpp
240 | #include
241 | #include "Teleplot.h"
242 | Teleplot teleplot("127.0.0.1");
243 |
244 | int main(int argc, char* argv[])
245 | {
246 | for(float i=0;i<1000;i+=0.1)
247 | {
248 | // Use instanciated object
249 | teleplot.update("sin", sin(i));
250 | teleplot.update("cos", cos(i), 10); // Limit at 10Hz
251 |
252 | // Use static localhost object
253 | Teleplot::localhost().update("tan", tan(i));
254 |
255 | usleep(10000);
256 | }
257 | return 0;
258 | }
259 | ```
260 |
261 | ## Python
262 |
263 | ```python
264 | import socket
265 | import math
266 | import time
267 |
268 | teleplotAddr = ("127.0.0.1",47269)
269 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
270 |
271 | def sendTelemetry(name, value):
272 | now = time.time() * 1000
273 | msg = name+":"+str(now)+":"+str(value)
274 | sock.sendto(msg.encode(), teleplotAddr)
275 |
276 | i=0
277 | while i < 1000:
278 |
279 | sendTelemetry("sin", math.sin(i))
280 | sendTelemetry("cos", math.cos(i))
281 |
282 | i+=0.1
283 | time.sleep(0.01)
284 | ```
285 |
286 | ## Not listed?
287 |
288 | You just need to send a UDP packet with the proper text in it. Open your web browser, search for `my_language send UDP packet`, and copy-paste the first sample you find before editing it with the following options:
289 |
290 | - address: `127.0.0.1`
291 | - port: `47269`
292 | - your test message: `myValue:1234|g`
293 |
294 | # Remote function calls
295 |
296 | Remote function calls is an optional feature that opens an UDP socket between the program and the Teleplot server to pull the list of registered functions and call them.
297 |
298 | # Register a function
299 |
300 | ## C++
301 |
302 | Copy `Telecmd.h` (from `clients/cpp`) in your project and use its object.
303 | `Telecmd.h` and `Teleplot.h` can be used at the same time.
304 |
305 | ```cpp
306 | #include "Telecmd.h"
307 |
308 | int main(int argc, char* argv[])
309 | {
310 | bool keepRunning = true;
311 |
312 | Telecmd::localhost().registerCmd("sayHello",[](std::string params){
313 | std::cout << "Hello " << params << std::endl;
314 | });
315 |
316 | Telecmd::localhost().registerCmd("stop",[&](std::string){
317 | std::cout << "Stopping..." << std::endl;
318 | keepRunning = false
319 | });
320 |
321 | // Main program loop
322 | while(keepRunning){
323 | Telecmd::localhost().run();
324 | }
325 | return 0;
326 | }
327 | ```
328 |
329 | ## Call a function
330 |
331 | Functions can be called from the Teleplot interface and will be auto-discovered, however, they can also be triggered by a simple UDP packet:
332 |
333 | - With a string as parameter: `echo "|sayHello|world|" | nc -u -w0 127.0.0.1 47268`
334 | - Without parameters: `echo "|stop|" | nc -u -w0 127.0.0.1 47268`
335 |
336 | ## Send a text log
337 |
338 | Along with telemetries, you can also send text logs to be display in a console-like manner:
339 |
340 | `echo ">:Hello world" | nc -u -w0 127.0.0.1 47269`
341 |
342 | By adding a millisecond timestamp to your log, you can sync them with the charts.
343 |
344 | `echo ">1627551892437:Hello world" | nc -u -w0 127.0.0.1 47269`
345 |
--------------------------------------------------------------------------------
/clients/MPU6050_HID_tests/.theia/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | "version": "0.2.0",
5 | "configurations": [
6 |
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/clients/MPU6050_HID_tests/MPU6050_HID_tests.ino:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include "Adafruit_AHRS_Mahony.h"
4 | #include
5 | #include
6 |
7 | Adafruit_MPU6050 mpu;
8 | Adafruit_Mahony filter;
9 |
10 | float offset_gx = 0.07;
11 | float offset_gy = -0.1;
12 | float offset_gz = 0.02;
13 |
14 |
15 | void setup(void) {
16 | Serial.begin(921600);
17 |
18 | // Try to initialize!
19 | if (!mpu.begin()) {
20 | Serial.println("Failed to find MPU6050 chip");
21 | while (1) {
22 | delay(10);
23 | }
24 | }
25 | Serial.println("MPU6050 Found!");
26 |
27 | mpu.setAccelerometerRange(MPU6050_RANGE_8_G);
28 | mpu.setGyroRange(MPU6050_RANGE_500_DEG);
29 |
30 | mpu.setFilterBandwidth(MPU6050_BAND_5_HZ);
31 | filter.begin(5);
32 | }
33 |
34 | void loop() {
35 | /* Get new sensor events with the readings */
36 | sensors_event_t a, g, temp;
37 | mpu.getEvent(&a, &g, &temp);
38 | g.gyro.x -= offset_gx;
39 | g.gyro.y -= offset_gy;
40 | g.gyro.z -= offset_gz;
41 |
42 | /* Estimate pose */
43 | filter.updateIMU( g.gyro.x
44 | , g.gyro.y
45 | , g.gyro.z
46 | , a.acceleration.x
47 | , a.acceleration.y
48 | , a.acceleration.z);
49 |
50 | /* Print out the values */
51 | Serial.print("a.x:"); Serial.print(a.acceleration.x); Serial.println("|np");
52 | Serial.print("a.y:"); Serial.print(a.acceleration.y); Serial.println("|np");
53 | Serial.print("a.z:"); Serial.print(a.acceleration.z); Serial.println("|np");
54 | Serial.print("g.x:"); Serial.print(g.gyro.x); Serial.println("|np");
55 | Serial.print("g.y:"); Serial.print(g.gyro.y); Serial.println("|np");
56 | Serial.print("g.z:"); Serial.print(g.gyro.z); Serial.println("|np");
57 | Serial.print("temperature:"); Serial.print(temp.temperature); Serial.println("|np");
58 |
59 | Serial.print("pose.roll:"); Serial.print( filter.getRoll()); Serial.println("|np");
60 | Serial.print("pose.pitch:"); Serial.print(filter.getPitch()); Serial.println("|np");
61 | Serial.print("pose.yaw:"); Serial.print( filter.getYaw()); Serial.println("|np");
62 | // 3D
63 |
64 |
65 | Serial.print("3D|IMU:R:");
66 | Serial.print((filter.getPitch() * M_PI /180.f));
67 | Serial.print(":");
68 | Serial.print((filter.getYaw() * M_PI /180.f));
69 | Serial.print(":");
70 | Serial.print((-filter.getRoll() * M_PI /180.f ));
71 | Serial.println(":S:cube:W:4:H:1.5:D:3:C:grey|g");
72 |
73 | //delay(1);
74 | }
--------------------------------------------------------------------------------
/clients/bash/sample.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | echo "myData:1234|g" | nc -u -w0 127.0.0.1 47269
--------------------------------------------------------------------------------
/clients/cpp/Telecmd.h:
--------------------------------------------------------------------------------
1 | // Telecmd
2 | // Source: https://github.com/nesnes/teleplot
3 |
4 | #ifndef TELECMD_H
5 | #define TELECMD_H
6 |
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 | #include
14 | #include
15 |
16 | #include
17 | #include
18 | #include
19 | #include
20 |
21 | //#define TELECMD_DISABLE // Would prevent telecmd from doing anything, useful for production builds
22 |
23 | #define TELECMD_INPUT_BUFFER_SIZE 1024
24 | class Telecmd {
25 | public:
26 | Telecmd(std::string address) : address_(address)
27 | {
28 | #ifdef TELECMD_DISABLE
29 | return ;
30 | #endif
31 | // Create UDP socket
32 | sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
33 | memset(&serv_, 0, sizeof(serv_));
34 | memset(&client_, 0, sizeof(client_));
35 | serv_.sin_family = AF_INET; // IPv4
36 | serv_.sin_addr.s_addr = htonl(INADDR_ANY);
37 | serv_.sin_port = htons(47268);
38 |
39 | // Set addr reuse
40 | uint8_t yes = 1;
41 | setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR, (char*) &yes, sizeof(yes));
42 | setsockopt(sockfd_, SOL_SOCKET, SO_REUSEPORT, (const char*)&yes, sizeof(yes));
43 |
44 | // Set socket timeout
45 | struct timeval timeout;
46 | timeout.tv_sec = 0;
47 | timeout.tv_usec = 100;
48 | setsockopt(sockfd_, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout));
49 |
50 | // Create Answer UDP socket
51 | sockfdOut_ = socket(AF_INET, SOCK_DGRAM, 0);
52 | servOut_.sin_family = AF_INET;
53 | servOut_.sin_port = htons(47269);
54 | servOut_.sin_addr.s_addr = inet_addr(address_.c_str());
55 |
56 | // Listen to UDP socket
57 | if ( bind(sockfd_, (const struct sockaddr *)&serv_, sizeof(serv_)) >= 0 ) {
58 | serverReady_ = true;
59 | }
60 | else {
61 | std::cout << "Telecmd init failed" <= TELECMD_INPUT_BUFFER_SIZE) return;
78 | std::string cmd(inputBuffer_, n);
79 |
80 | // Perform requested command
81 | if(cmd.rfind("|_telecmd_list_cmd|", 0) == 0){
82 | sendCommandList();
83 | }
84 | else {
85 | parseFunctionCall(cmd);
86 | }
87 | }
88 |
89 | void registerCmd(std::string name, std::function func){
90 | functionMap_[name] = func;
91 | }
92 |
93 | private:
94 | void sendCommandList(){
95 | std::string cmdList = "|";
96 | for (auto const& registeredCmd : functionMap_)
97 | {
98 | cmdList += registeredCmd.first + "|";
99 | }
100 | sendto(sockfdOut_, cmdList.c_str(), cmdList.size(), MSG_CONFIRM, (const struct sockaddr *) &servOut_, sizeof(servOut_));
101 | }
102 |
103 | void parseFunctionCall(std::string const& cmd) {
104 | try {
105 | if(cmd.size() == 0 || cmd[0] != '|') return;
106 | // Function Name
107 | size_t nameEnd = cmd.find("|", 1);
108 | if(nameEnd == std::string::npos) return;
109 | std::string name = cmd.substr(1, nameEnd-1);
110 | // Function Params
111 | std::string params = "";
112 | size_t paramStart = nameEnd+1;
113 | if(paramStart> functionMap_;
138 | };
139 |
140 | #endif
--------------------------------------------------------------------------------
/clients/cpp/sample.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include "Teleplot.h"
3 | #include
4 |
5 | Teleplot teleplot("127.0.0.1", 47269);
6 |
7 | int main(int argc, char* argv[])
8 | {
9 | float i = 0;
10 | int state_arr_length = 3;
11 | std::string state_arr[state_arr_length] = {"standing", "sitting", "walking"};
12 |
13 | int heights_arr_length = 6; double heights_arr[heights_arr_length] = {20, 5, 8, 4, 1, 2};
14 |
15 | for (;;)
16 | {
17 | // Use instanciated object
18 | teleplot.update("sin", sin(i), "km²");
19 | teleplot.update("cos", cos(i), "", 10); // Limit at 10Hz
20 | teleplot.update("state", state_arr[rand()%state_arr_length], "", 0, "t");
21 |
22 | teleplot.update3D(
23 | ShapeTeleplot("mysquare", "cube")
24 | .setCubeProperties(heights_arr[rand()%heights_arr_length])
25 | .setPos(sin(i)*10, cos(i)*10)
26 | );
27 |
28 | // Use static localhost object
29 | Teleplot::localhost().update("tan", tan(i), "");
30 |
31 | usleep(10000);
32 |
33 | i+=0.1;
34 | }
35 | return 0;
36 | }
--------------------------------------------------------------------------------
/clients/python/full_test.py:
--------------------------------------------------------------------------------
1 | import socket
2 | import math
3 | import time
4 | import random
5 | import threading
6 |
7 | teleplotAddr = ("127.0.0.1",47269)
8 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
9 |
10 | def format_unit(unit):
11 | if (unit == None or unit == ""):
12 | return ""
13 | return ("§" + unit)
14 |
15 | def sendTelemetry(name, value, unit, now):
16 |
17 | flags = ""
18 | if (type(value) is str):
19 | flags += "t"
20 |
21 | msg = name+":"+str(now)+":"+str(value)+format_unit(unit)+"|"+flags
22 | if (now == None):
23 | msg = name+":"+str(value)+format_unit(unit)+"|"+flags
24 |
25 | sock.sendto(msg.encode(), teleplotAddr)
26 |
27 | def sendTelemetryXY(name, x, y, x1, y1, unit):
28 |
29 | now = time.time() * 1000
30 | msg = name+":"+str(x)+":"+str(y)+":"+str(now)+";" +str(x1)+":"+str(y1)+":"+str(now)+format_unit(unit)+"|xy"
31 |
32 | sock.sendto(msg.encode(), teleplotAddr)
33 |
34 |
35 | def sendMultipleTelemTest():
36 | msg = "myValue:1627551892444:1;1627551892555:2;1627551892666:3\n\
37 | mySecondValue:1627551892444:1;1627551892555:2;1627551892666:3§rad\n\
38 | myThirdValue:3.2323e+4|\n\
39 | state:state_a|t\n\
40 | state2:1627551892444:state_a;1627551892555:state_b|t\n\
41 | trajectory:1:1;2:2;3:3;4:4|xy\n\
42 | myvariable1,chart52:45\n\
43 | myvariable2,chart52:151\n\
44 | myvariable3,chart50:97\n\
45 | myvariable4,chart52:0.454"
46 |
47 |
48 | sock.sendto(msg.encode(), teleplotAddr)
49 |
50 | def basicTest():
51 | th0 = threading.Thread(target=basicTestSubBefore)
52 | th = threading.Thread(target=basicTestSub)
53 |
54 | th0.start()
55 |
56 | time.sleep(1)
57 | th.start()
58 |
59 | def basicTestSubBefore():
60 | i=0
61 | while True:
62 | now = time.time() * 1000
63 |
64 | sendTelemetry("cos_before", math.cos(i), "", now)
65 | i+=0.1
66 | time.sleep(0.01)
67 |
68 | def basicTestSub():
69 | i=0
70 | currentRobotState = "standing"
71 | while True:
72 |
73 | now = time.time() * 1000
74 |
75 |
76 | sendTelemetry("sin_unit", math.sin(i), "my_weird@ unit $", now)
77 | sendTelemetry("cos_no_time", math.cos(i), "", None)
78 |
79 | # for i in range(0, 1):
80 | sendTelemetry("cos_time_var_0,mysuper_widget", math.cos(i), "", now)
81 | sendTelemetry("cos_time_var_1,mysuper_widget", 2*math.cos(i), "", now)
82 | # sendTelemetry("cos_time_var_2", math.cos(i), "", now)
83 | # sendTelemetry("cos_time_var_3", math.cos(i), "", now)
84 | # sendTelemetry("cos_time_var_4", math.cos(i), "", now)
85 | # sendTelemetry("cos_time_var_5", math.cos(i), "", now)
86 | # sendTelemetry("cos_time_var_6", math.cos(i), "", now)
87 | # sendTelemetry("cos_time_var_7", math.cos(i), "", now)
88 | # sendTelemetry("cos_time_var_8", math.cos(i), "", now)
89 | # sendTelemetry("cos_time_var_9", math.cos(i), "", now)
90 |
91 | sendTelemetry("cos_no_time_unit", math.cos(i), "kilos", None)
92 | sendTelemetry("cos", math.cos(i), "", now)
93 | # sendLog("cos(i) : "+str(math.cos(i)), None)
94 | sendLog("cos(i) : "+str(math.cos(i)), now)
95 |
96 | sendTelemetryXY("XY_", math.sin(i),math.cos(i), math.sin(i+0.1), math.cos(i+0.1), "km²")
97 |
98 | if (random.randint(0, 1000) >= 950 ):
99 | if (currentRobotState == "standing") :
100 | currentRobotState = "sitted"
101 | else :
102 | currentRobotState = "standing"
103 |
104 | sendTelemetry("robot_state", currentRobotState ,"", now)
105 | sendTelemetry("robot_state_no_time", currentRobotState ,"", None)
106 | sendTelemetry("robot_state_no_time_unit", currentRobotState ,"km/h", None)
107 | sendTelemetry("robot_state_unit", currentRobotState ,"m²", now)
108 |
109 |
110 | i+=0.1
111 | time.sleep(0.01)
112 |
113 | def sendLog(mstr, now):
114 | timestamp = ""
115 | if (now != None):
116 | timestamp = str(now)
117 |
118 | msg = (">"+timestamp+":I am a log, linked to : "+mstr+", useless text : blablablablablablablablablablablablablablablablablablabla")
119 | sock.sendto(msg.encode(), teleplotAddr)
120 |
121 | def testThreeD():
122 | th1 = threading.Thread(target=testThreeD_sub)
123 | th2 = threading.Thread(target=testThreeDHighRate_sub)
124 | th3 = threading.Thread(target=testThreeDMAnyShapesSameWidget)
125 | th4 = threading.Thread(target=testThreeDRotatingSpheres)
126 | th1.start()
127 | th2.start()
128 | th3.start()
129 | th4.start()
130 |
131 | def testThreeD_sub():
132 | sphereRadius = 3
133 | cube1Depth = 7
134 | cube2Rot = 0
135 |
136 | while True:
137 |
138 | msg1 = '3D|mycube1:S:cube:O:0.2:C:blue:W:5:H:4:D:'+str(cube1Depth)
139 | msg2 = '3D|mysphere,widget0:RA:'+str(sphereRadius)+':S:sphere:O:0.4'
140 | msg3 = '3D|mycube2,widget0:S:cube:R:'+ str(cube2Rot) +':::C:green'
141 |
142 | randomNb = random.randint(0, 100)
143 |
144 | if ( randomNb >= 1 and randomNb <= 10):
145 | sphereRadius += 1
146 | elif (randomNb >= 11 and randomNb <= 21):
147 | sphereRadius = max(sphereRadius-1, 1)
148 | elif (randomNb >= 22 and randomNb <= 32):
149 | cube1Depth += 1
150 | elif (randomNb >= 33 and randomNb <= 43):
151 | cube1Depth = max(cube1Depth-1, 1)
152 | elif (randomNb >= 44 and randomNb <= 54):
153 | cube2Rot -= 0.1
154 | elif (randomNb >= 65 and randomNb <= 85):
155 | cube2Rot += 0.1
156 |
157 | sock.sendto(msg1.encode(), teleplotAddr)
158 | sock.sendto(msg2.encode(), teleplotAddr)
159 | sock.sendto(msg3.encode(), teleplotAddr)
160 |
161 | time.sleep(0.1)
162 |
163 | def testThreeDHighRate_sub():
164 |
165 | i = 0
166 | while True:
167 | msg4 = '3D|mysphere3:RA:1:S:sphere:O:0.4:P:'+str(math.sin(i)*2)+':'+str(math.cos(i)*2)+':1'
168 | sock.sendto(msg4.encode(), teleplotAddr)
169 |
170 | i+=0.1
171 | time.sleep(0.01) #1kHz
172 |
173 |
174 |
175 |
176 | def testThreeDRotatingSpheres():
177 | class MySphere:
178 | def __init__(self):
179 | self.xoffset = random.randint(-25,25)
180 | self.yoffset = random.randint(-25,25)
181 | self.zoffset = random.randint(-25,25)
182 | self.xdistance = random.randint(-50,50)
183 | self.ydistance = random.randint(-50,50)
184 | self.zdistance = random.randint(-50,50)
185 | self.zIsCos = True
186 | if (random.randint(0,1) == 0):
187 | self.zIsCos = False
188 |
189 | self.radius = random.randint(1,8)
190 |
191 | self.loopcount = 0
192 | self.totalspeed = random.randint(1,5)/100
193 | self.move()
194 |
195 | def move(self):
196 | self.x = math.sin(self.loopcount) * self.xdistance + self.xoffset
197 | self.y = math.cos(self.loopcount) * self.ydistance + self.yoffset
198 | if (self.zIsCos) :
199 | self.z = math.cos(self.loopcount) * self.zdistance + self.zoffset
200 | else :
201 | self.z = math.sin(self.loopcount) * self.zdistance + self.zoffset
202 |
203 | self.loopcount += self.totalspeed
204 |
205 |
206 | numberOfSpheres = 20
207 |
208 | mycolors = ["green", "red", "blue", "grey", "purple", "yellow", "orange", "brown", "black", "white"]
209 | mySpheres = []
210 | myMessages = [""] * numberOfSpheres
211 |
212 | for i in range (numberOfSpheres):
213 | mySpheres.append(MySphere())
214 |
215 |
216 | while True:
217 |
218 | for i in range (numberOfSpheres):
219 | currSphere = mySpheres[i]
220 |
221 | myMessages[i] = '3D|mySphere_'+ str(i) +',myFavWidget2:S:sphere:O:0.5:C:' + str(mycolors[i%(len(mycolors))]) +':RA:'+ str(currSphere.radius) +':P:'+ str(currSphere.x) +':'+ str(currSphere.y) +':'+ str(currSphere.z)
222 |
223 | # print(myMessages[i])
224 | sock.sendto(myMessages[i].encode(), teleplotAddr)
225 |
226 | currSphere.move()
227 |
228 | time.sleep(0.017)
229 |
230 |
231 |
232 | def testThreeDMAnyShapesSameWidget():
233 | class MyCube:
234 | def __init__(self):
235 | self.rx = random.randint(0, 7)
236 | self.ry = random.randint(0, 7)
237 | self.rz = random.randint(0, 7)
238 | self.x = random.randint(0, 50)-25
239 | self.y = random.randint(0, 50)-25
240 | self.z = random.randint(0, 50)-25
241 | self.height = 5
242 | self.depth = 5
243 | self.width = 5
244 |
245 | def shuffle(self, chaosFactor):
246 | maxNb = 120
247 | randomNb = random.randint(0, maxNb)
248 |
249 | the_change_rot = 0.01
250 | the_change_mov = 0.01
251 | move_proba = (maxNb/2) * chaosFactor
252 |
253 | if (randomNb <= move_proba):
254 | # if(random.randint(0, 1) == 0):
255 | # self.rx+=the_change_rot
256 | # if(random.randint(0, 1) == 1):
257 | # self.rx-=the_change_rot
258 | # if(random.randint(0, 1) == 0):
259 | # self.ry+=the_change_rot
260 | # if(random.randint(0, 1) == 1):
261 | # self.ry-=the_change_rot
262 | # if(random.randint(0, 1) == 0):
263 | self.rz+=the_change_rot
264 | self.rx-=the_change_rot
265 | self.ry+=2*the_change_rot
266 | # if(random.randint(0, 1) == 1):
267 | # self.rz-=the_change_rot
268 | # if(random.randint(0, 1) == 0):
269 | # self.x+=the_change_mov
270 | # if(random.randint(0, 1) == 1):
271 | # self.x-=the_change_mov
272 | # if(random.randint(0, 1) == 0):
273 | # self.y+=the_change_mov
274 | # if(random.randint(0, 1) == 1):
275 | # self.y-=the_change_mov
276 | # if(random.randint(0, 1) == 0):
277 | # self.z+=the_change_mov
278 | # if(random.randint(0, 1) == 1):
279 | # self.z-=the_change_mov
280 |
281 | numberOfCubes = 20
282 |
283 | CubeChaosFactor = 10
284 |
285 | mycolors = ["green", "red", "blue", "grey", "purple", "yellow", "orange", "brown", "black", "white"]
286 | myCubes = []
287 | myMessages = [""] * numberOfCubes
288 |
289 | for i in range (numberOfCubes):
290 | myCubes.append(MyCube())
291 |
292 |
293 | while True:
294 |
295 | for i in range (numberOfCubes):
296 | currCube = myCubes[i]
297 |
298 | myMessages[i] = '3D|myCube_'+ str(i) +',myFavWidget:S:cube:O:0.5:C:' + str(mycolors[i%(len(mycolors))]) +':W:'+ str(currCube.width) +':H:'+ str(currCube.height) +':D:'+ str(currCube.depth) +':P:'+ str(currCube.x) +':'+ str(currCube.y) +':'+ str(currCube.z) +':R:'+ str(currCube.rx) +':'+ str(currCube.ry) +':'+ str(currCube.rz)
299 |
300 |
301 | # print(myMessages[i])
302 | sock.sendto(myMessages[i].encode(), teleplotAddr)
303 |
304 | currCube.shuffle(CubeChaosFactor)
305 |
306 | time.sleep(0.01)
307 |
308 |
309 | sendMultipleTelemTest()
310 | basicTest()
311 | testThreeD()
312 |
313 |
314 |
--------------------------------------------------------------------------------
/clients/python/imu.py:
--------------------------------------------------------------------------------
1 | from serial import Serial
2 | import socket
3 |
4 | teleplotAddr = ("127.0.0.1",47269)
5 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
6 |
7 | with Serial('/dev/ttyACM0', 921600) as ser:
8 | while True:
9 | line = ser.readline().decode("utf-8")
10 | # print(line)
11 |
12 | sock.sendto(line.encode(), teleplotAddr)
--------------------------------------------------------------------------------
/clients/python/main.py:
--------------------------------------------------------------------------------
1 | import socket
2 | import math
3 | import time
4 |
5 | teleplotAddr = ("127.0.0.1",47269)
6 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
7 |
8 | def sendTelemetry(name, value):
9 | now = time.time() * 1000
10 | msg = name+":"+str(now)+":"+str(value)+"|g"
11 | sock.sendto(msg.encode(), teleplotAddr)
12 |
13 | i=0
14 | while True:
15 |
16 | sendTelemetry("sin", math.sin(i))
17 | sendTelemetry("cos", math.cos(i))
18 |
19 | i+=0.1
20 | time.sleep(0.01)
--------------------------------------------------------------------------------
/clients/python/mickey.py:
--------------------------------------------------------------------------------
1 | import socket
2 | import math
3 | import time
4 | import random
5 | import threading
6 |
7 | teleplotAddr = ("127.0.0.1",47269)
8 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
9 |
10 | sphere1rad = 5
11 | sphere1x = 0
12 | sphere1y = 0
13 | sphere1z = 0
14 |
15 | sphere1xOffset = -8
16 | sphere1yOffset = 8
17 | sphere1zOffset = 0
18 |
19 |
20 |
21 | sphere2rad = 5
22 | sphere2x = 0
23 | sphere2y = 0
24 | sphere2z = 0
25 |
26 | sphere2xOffset = 8
27 | sphere2yOffset = 8
28 | sphere2zOffset = 0
29 |
30 |
31 |
32 | sphere3rad = 10
33 | sphere3x = 0
34 | sphere3y = 0
35 | sphere3z = 0
36 |
37 | sphere3xOffset = 0
38 | sphere3yOffset = 0
39 | sphere3zOffset = 0
40 |
41 |
42 | i = 0
43 |
44 | while True:
45 |
46 | totalXOffset = math.sin(i)*30 + math.cos(6*i)*10
47 | totalYOffset = math.cos(i)*30 + math.sin(6*i)*10
48 |
49 | sphere1x = sphere1xOffset + totalXOffset
50 | sphere1y = sphere1yOffset + totalYOffset
51 | sphere1z = sphere1zOffset
52 |
53 | sphere2x = sphere2xOffset + totalXOffset
54 | sphere2y = sphere2yOffset + totalYOffset
55 | sphere2z = sphere2zOffset
56 |
57 | sphere3x = sphere3xOffset + totalXOffset
58 | sphere3y = sphere3yOffset + totalYOffset
59 | sphere3z = sphere3zOffset
60 |
61 |
62 |
63 |
64 | msg1 = '3D|sphere1,widget0:S:sphere:RA:'+ str(sphere1rad)+':P:'+ str(sphere1x) +':'+ str(sphere1y) +':'+ str(sphere1z) + ':C:black:O:1'
65 | msg2 = '3D|sphere2,widget0:S:sphere:RA:'+ str(sphere2rad)+':P:'+ str(sphere2x) +':'+ str(sphere2y) +':'+ str(sphere2z) + ':C:black:O:1'
66 | msg3 = '3D|sphere3,widget0:S:sphere:RA:'+ str(sphere3rad)+':P:'+ str(sphere3x) +':'+ str(sphere3y) +':'+ str(sphere3z) + ':C:black:O:1'
67 |
68 | sock.sendto(msg1.encode(), teleplotAddr)
69 | sock.sendto(msg2.encode(), teleplotAddr)
70 | sock.sendto(msg3.encode(), teleplotAddr)
71 |
72 |
73 | time.sleep(0.08)
74 | i+=0.1
75 |
--------------------------------------------------------------------------------
/clients/python/test_log.py:
--------------------------------------------------------------------------------
1 | import socket
2 | import math
3 | import time
4 | import random
5 | import threading
6 |
7 | teleplotAddr = ("127.0.0.1",47269)
8 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
9 |
10 | def sendTelemetry(name, value, now):
11 |
12 | flags = ""
13 | if (type(value) is str):
14 | flags += "t"
15 |
16 | msg = name+":"+str(now)+":"+str(value)+"|"+flags
17 | if (now == None):
18 | msg = name+":"+str(value)+"|"+flags
19 |
20 | sock.sendto(msg.encode(), teleplotAddr)
21 |
22 |
23 |
24 | def basicTest():
25 | th = threading.Thread(target=basicTestSub)
26 | th.start()
27 |
28 | def basicTestSub():
29 | i=0
30 | while True:
31 |
32 | now = time.time() * 1000
33 |
34 |
35 | sendTelemetry("cos", math.cos(i), now)
36 | sendLog("cos(i) : "+str(math.cos(i)), now)
37 |
38 |
39 | i+=0.1
40 | time.sleep(0.01)
41 |
42 | def sendLog(mstr, now):
43 | timestamp = ""
44 | if (now != None):
45 | timestamp = str(now)
46 |
47 | msg = (">"+timestamp+":I am a log, linked to : "+mstr)
48 | sock.sendto(msg.encode(), teleplotAddr)
49 |
50 |
51 |
52 | basicTest()
53 |
54 |
55 |
--------------------------------------------------------------------------------
/clients/python/xy.py:
--------------------------------------------------------------------------------
1 | import socket
2 | import math
3 | import time
4 |
5 | teleplotAddr = ("127.0.0.1",47269)
6 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
7 |
8 | def sendTelemetryXY(name, valueX, valueY, clear=False):
9 | now = time.time() * 1000
10 | msg = name+":"+str(valueX)+":"+str(valueY)+":"+str(now)+"|xy"+("clr" if clear else "")
11 | sock.sendto(msg.encode(), teleplotAddr)
12 |
13 | i=0
14 | while True:
15 |
16 | # Clear telemetry when completing a circle
17 | clear = False
18 | if( i > 2 * math.pi):
19 | i = 0
20 | clear=True
21 |
22 | sendTelemetryXY("circle", math.sin(i), math.cos(i), clear)
23 | i+=0.1
24 | time.sleep(0.01)
--------------------------------------------------------------------------------
/doc/README.md:
--------------------------------------------------------------------------------
1 | # UML Class Diagram
2 |
3 | The UMLClassDiagram.groovy file contains the class diagram of the project in a PlantUML format.
4 | This file can easily be visualized with online editors such as plantuml.com or planttext.com for instance.
--------------------------------------------------------------------------------
/doc/UMLClassDiagram.groovy:
--------------------------------------------------------------------------------
1 | @startuml
2 |
3 | abstract class Connection {
4 | {static} ConnectionCount : Number
5 | name : String
6 | id : String
7 | type : String
8 | connected : boolean
9 | inputs : DataInput []
10 |
11 | void connect(void)
12 | void removeInput(input : DataInput)
13 | }
14 |
15 | class ConnectionTeleplotVSCode {
16 | vscode : boolean
17 | udp : DataInputUDP
18 | supportSerial : boolean
19 |
20 | void connect(void)
21 | void disconnect(void)
22 | void sendServerCommand(command : {id : String, cmd : String, text : String})
23 | void sendCommand(command : String)
24 | void updateCMDList(void)
25 | void createInput(type : String)
26 | }
27 |
28 | class ConnectionTeleplotWebSocket {
29 | socket : WebSocket
30 | adress : String
31 | port : String
32 | udp : DataInputUDP
33 |
34 | void connect(_address : String, _port : Number)
35 | void disconnect(void)
36 | void sendServerCommand(command : {id : String, cmd : String, text : String})
37 | void updateCMDList(void)
38 | void createInput(type : String)
39 | }
40 |
41 | abstract class DataInput {
42 | {static} DataInputCount : Number
43 | connection : Connection
44 | name : String
45 | id : String
46 | type : String
47 | connected : boolean
48 | }
49 |
50 | class DataInputSerial {
51 | port : Number
52 | baudrate : Number
53 | portList : Portinfo []
54 | textToSend : String
55 | endlineToSend : String
56 |
57 | void connect(void)
58 | void disconnect(void)
59 | void onMessage(msg : { id : Number, input : DataInput, cmd : String, list : Portinfo []} )
60 | void listPorts(void)
61 | void sendCommand(void)
62 | void updateCMDList(void)
63 | void sendText(text : String, lineEndings : String)
64 |
65 | }
66 |
67 | class DataInputUDP {
68 | adress : String
69 | port : Number
70 |
71 | void connect(void)
72 | void disconnect(void)
73 | void onMessage(msg : String)
74 | void sendCOmmand(command : String)
75 | void updateCMDList(void)
76 |
77 | }
78 |
79 | class ChartWidget {
80 | isXY : boolean
81 | data : Number [][][]
82 | options : { title : String, width : Number, height : Number, scales : Object, series : DataSerie [], focus : Object, cursor : Object, legend : Object}
83 | forceUpdate : boolean
84 |
85 | void destroy(void)
86 | void addSerie(DataSerie)
87 | void update(void)
88 |
89 | }
90 |
91 | class DataSerie {
92 | {static} DataSerieIdCount : Number
93 | name : String
94 | id : String
95 | sourceNames : String []
96 | formula : String
97 | initialized : boolean
98 | dataIdx : Number
99 | data : Number [][]
100 | pendingData : Number [][]
101 | options : { _serie : String, stroke : String, fill : String, paths : Fun}
102 | value : Number
103 | stats : {min : Number, max : Number, sum : Number, mean : Number, med : Number, stedv : Number}
104 |
105 | void destroy(void)
106 | void addSource(name : String)
107 | void update(void)
108 | void updateStats(void)
109 | void applyTimeWindow(void)
110 |
111 | }
112 |
113 | abstract class DataWidget {
114 | {static} widgetCount : Number
115 | - {static} widgetBeingResized : DataWidget
116 | series : DataSerie []
117 | type : String
118 | id : String
119 | gridPos : {h : Number, w : Number, x : Number, y : Number}
120 | - initialCursorPos : Number
121 | - initialCursorYPos : Number
122 | - initialHeight : Number
123 | - initialWidth : Number
124 | - isResized : Number
125 |
126 | void isUsingSource(name : String)
127 | DataSerie [] _getSourceList(void)
128 | void updateStats(void)
129 | }
130 |
131 |
132 |
133 | Connection <|-- ConnectionTeleplotVSCode
134 | Connection <|-- ConnectionTeleplotWebSocket
135 |
136 | Connection "1" <--> "0..*" DataInput
137 |
138 | DataWidget --> "0..*" DataSerie
139 |
140 | DataInput <|-- DataInputUDP
141 | DataInput <|-- DataInputSerial
142 |
143 | DataWidget <|-- ChartWidget
144 |
145 | @enduml
146 |
147 | /'
148 | Portinfo doc : https://serialport.io/docs/api-bindings-cpp#list
149 | '/
--------------------------------------------------------------------------------
/images/logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nesnes/teleplot/40caddd5aac92674f007b959a4d8a4771df83d96/images/logo-color.png
--------------------------------------------------------------------------------
/images/logo-color.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
16 |
39 |
41 |
46 |
49 |
56 |
61 |
66 |
72 |
78 |
85 |
89 |
93 |
97 |
101 |
105 |
109 |
113 |
117 |
118 |
124 |
129 |
134 |
135 |
136 |
137 |
--------------------------------------------------------------------------------
/images/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
16 |
39 |
41 |
46 |
49 |
56 |
61 |
66 |
72 |
78 |
85 |
89 |
93 |
97 |
101 |
105 |
109 |
113 |
117 |
118 |
124 |
129 |
134 |
135 |
136 |
137 |
--------------------------------------------------------------------------------
/images/preview-vscode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nesnes/teleplot/40caddd5aac92674f007b959a4d8a4771df83d96/images/preview-vscode.png
--------------------------------------------------------------------------------
/images/preview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nesnes/teleplot/40caddd5aac92674f007b959a4d8a4771df83d96/images/preview.jpg
--------------------------------------------------------------------------------
/images/wandercraft-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nesnes/teleplot/40caddd5aac92674f007b959a4d8a4771df83d96/images/wandercraft-logo.png
--------------------------------------------------------------------------------
/images/wandercraft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nesnes/teleplot/40caddd5aac92674f007b959a4d8a4771df83d96/images/wandercraft.png
--------------------------------------------------------------------------------
/server/.dockerignore:
--------------------------------------------------------------------------------
1 | **/node_modules
--------------------------------------------------------------------------------
/server/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:12.18.3
2 |
3 | RUN mkdir -p /teleplot
4 | WORKDIR /teleplot
5 |
6 | # Setup node envs
7 | ARG NODE_ENV
8 | ENV NODE_ENV $NODE_ENV
9 |
10 | # Install dependencies
11 | COPY . /teleplot/
12 | RUN npm install && npm cache clean --force
13 |
14 | # Expose required ports
15 | EXPOSE 8080
16 | EXPOSE 47269
17 |
18 | # Start
19 | ENTRYPOINT [ "node", "main.js" ]
20 |
--------------------------------------------------------------------------------
/server/docker-compose.yml:
--------------------------------------------------------------------------------
1 | teleplot-server:
2 | build: .
3 | ports:
4 | - 47269:47269/udp
5 | - 8080:8080
6 |
--------------------------------------------------------------------------------
/server/main.js:
--------------------------------------------------------------------------------
1 | const UDP_PORT = 47269;
2 | const CMD_UDP_PORT = 47268;
3 | const HTTP_WS_PORT = 8080;
4 | const requestDelay = 50;// every requestDelay milliseconds, we send a websocket packet
5 |
6 | const { Console } = require('console');
7 | const udp = require('dgram');
8 | var express = require('express');
9 | var app = express();
10 | var expressWs = require('express-ws')(app);
11 |
12 | //Setup file server
13 | app.use(express.static(__dirname + '/www'))
14 |
15 | //Setup websocket server
16 | app.ws('/', (ws, req)=>{
17 | ws.on('message', function(msgStr) {
18 | try {
19 | let msg = JSON.parse(msgStr);
20 | udpServer.send(msg.cmd, CMD_UDP_PORT);
21 | }
22 | catch(e){}
23 | });
24 | });
25 | app.listen(HTTP_WS_PORT);
26 |
27 | // Setup UDP server
28 | var udpServer = udp.createSocket('udp4');
29 | udpServer.bind(UDP_PORT);
30 |
31 |
32 | // packets are being grouped up in this string and sent all together later on ( we have just added a line break between each )
33 | let groupedUpPacket = "";
34 |
35 | // Relay UDP packets to Websocket
36 | udpServer.on('message',function(msg,info){
37 | groupedUpPacket += ("\n" + msg.toString());
38 | });
39 |
40 |
41 | // every requestDelay ms, we send the packets (no need to send them at a higher frequency as it will just slow teleplot)
42 | setInterval(()=>{
43 | if(groupedUpPacket != "")
44 | {
45 | expressWs.getWss().clients.forEach((client)=>{
46 | client.send(JSON.stringify({data: groupedUpPacket, fromSerial:false, timestamp: new Date().getTime()}), { binary: false });
47 | });
48 | }
49 |
50 | groupedUpPacket = "";
51 | }, requestDelay);
52 |
53 | console.log("Teleplot server started");
54 | console.log(`Open your browser at 127.0.0.1:${HTTP_WS_PORT}`);
55 | console.log(`Send telemetry with a "key:value" UDP packet to 127.0.0.1:${UDP_PORT}`);
56 | console.log(`Example:`);
57 | console.log(`\t BASH: echo "myData:1234" | nc -u -w0 127.0.0.1 ${UDP_PORT}`);
58 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "teleplot",
3 | "version": "0.1.0",
4 | "description": "",
5 | "main": "main.js",
6 | "bin": "main.js",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "start": "node main.js",
10 | "make": "npm i -g pkg && pkg . && chmod -R 777 build"
11 | },
12 | "author": "",
13 | "license": "MIT",
14 | "dependencies": {
15 | "express": "^4.17.1",
16 | "express-ws": "^5.0.2"
17 | },
18 | "pkg": {
19 | "scripts": "main.js",
20 | "assets": "www/**",
21 | "targets": ["node14-windows-x64", "node14-linux-x64", "node14-linux-armv7" ],
22 | "outputPath": "build"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/server/www/classes/3d/Shape3D.js:
--------------------------------------------------------------------------------
1 | class Shape3D
2 | {
3 | constructor ()
4 | {
5 | this.name = undefined; //ex : "my_cube_0"
6 | this.type = undefined; // String, ex : "cube"
7 | this.three_object = null;
8 | this.default_material = undefined;
9 | this.color = undefined;
10 | this.opacity = undefined;
11 |
12 | this.position = undefined; // Object containing x, y, and z, its three coordonates
13 | this.rotation = undefined; // Object containing x, y, and z, its three rotations
14 | this.quaternion = undefined; // Object containing x, y, z and w, its three quaternion coordinates
15 |
16 | this.radius = undefined; // Number, the radius of the sphere
17 | this.precision = undefined; // Number, the precision of the sphere
18 |
19 | this.height = undefined; // Number, the height of the cube
20 | this.width = undefined; // Number, the width of the cube
21 | this.depth = undefined; // Number, the depth of the cube
22 |
23 | }
24 |
25 | initializeFromRawShape(key, rawShape)
26 | {
27 | // rawShape is of type String, ex :
28 | // R::3.14:P:1:2:-1:S:cube:W:5:H:4:D:3:C:red
29 |
30 | this.name = key;
31 |
32 |
33 |
34 | function readCurrentProperty (rawShape, i) {
35 |
36 | function readPropertyValues (rawShape, i, propertiesCount) {
37 | i++; // we skip the ':'
38 | let propertiesValues = [undefined];
39 | let propCounter = 0;
40 | while (propCounter < propertiesCount)
41 | {
42 | if (i >= rawShape.length || rawShape[i] == ":")
43 | {
44 | propCounter ++;
45 |
46 | if (rawShape[i] == ":" && propCounter < propertiesCount)
47 | propertiesValues.push(undefined);
48 | }
49 | else
50 | {
51 | if (propertiesValues[propCounter] == undefined)
52 | propertiesValues[propCounter] = rawShape[i];
53 | else
54 | propertiesValues[propCounter] += rawShape[i];
55 | }
56 |
57 | i++;
58 | }
59 |
60 | return [propertiesValues, i];
61 | }
62 | function getPropertyInfo(currentProperty) {
63 | switch (currentProperty)
64 | {
65 | case "shape":
66 | case "S":
67 | return [1, "type"];
68 | case "opacity":
69 | case "O":
70 | return [1, "opacity"];
71 | case "quaternion":
72 | case "Q":
73 | return [4, "quaternion"];
74 | case "position":
75 | case "P":
76 | return [3, "position"];
77 | case "rotation":
78 | case "R":
79 | return [3, "rotation"];
80 | case "precision":
81 | case "PR":
82 | return [1, "precision"];
83 | case "radius":
84 | case "RA":
85 | return [1, "radius"];
86 | case "color":
87 | case "C":
88 | return [1, "color"];
89 | case "height" :
90 | case "H":
91 | return [1, "height"];
92 | case "width":
93 | case "W":
94 | return [1, "width"];
95 | case "depth":
96 | case "D":
97 | return [1, "depth"];
98 | default :
99 | throw new Error("Invalid shape property : " + currentProperty);
100 | }
101 | }
102 |
103 |
104 | if (rawShape[i] == ":")
105 | i++;
106 |
107 | let currentProperty = "";
108 |
109 | while (rawShape[i] != ":")
110 | {
111 | currentProperty += rawShape[i];
112 | i++;
113 | }
114 |
115 | let propertiesCount = 0;
116 | [propertiesCount, currentProperty] = getPropertyInfo(currentProperty);
117 |
118 | [propertyValues, i] = readPropertyValues(rawShape, i, propertiesCount);
119 |
120 | return [i, currentProperty, propertyValues];
121 | }
122 |
123 | this.position = {};
124 | this.rotation = {};
125 |
126 | let i = 0;
127 | let currentProperty = "";
128 | let propertyValues = [];
129 |
130 | while (i 0 ) // otherwise we want rendering to stop
4 | requestAnimationFrame(drawAllWords);
5 |
6 | for (let i = 0; i world.setRendererSize(entries[0].contentRect)).observe(this.containerDiv);
92 | }
93 |
94 | initializeOrbitControls(camera, renderer)
95 | {
96 | return new OrbitControls( camera, renderer.domElement );
97 | }
98 |
99 | destructor() {
100 | if (this.resize_obs != undefined)
101 | this.resize_obs.unobserve(this.containerDiv);
102 | }
103 |
104 | setRendererSize(container)
105 | {
106 | this.camera.aspect = container.width / container.height;
107 | this.camera.updateProjectionMatrix();
108 |
109 | this.renderer.setSize(container.width, container.height);
110 | this.renderer.setPixelRatio(window.devicePixelRatio);
111 | }
112 |
113 | updateToNewShape(old_shape, new_shape)
114 | {
115 | let myMesh = old_shape.three_object;
116 | if ( myMesh == null)
117 | {
118 | throw new Error("error myMesh shouldn't be null");
119 | }
120 |
121 |
122 | if (new_shape.type == "cube")// in this case we can just rescale it
123 | {
124 | myMesh.scale.set( new_shape.width, new_shape.height, new_shape.depth);
125 | }
126 | else if (new_shape.type == "sphere")
127 | {
128 | myMesh.scale.set(new_shape.radius, new_shape.radius, new_shape.radius);
129 | }
130 |
131 | if (myMesh.position != undefined && new_shape.position != undefined)
132 | {
133 | myMesh.position.y = new_shape.position.z;
134 | myMesh.position.z = new_shape.position.y;
135 | myMesh.position.x = new_shape.position.x;
136 | }
137 |
138 | if (new_shape.quaternion == undefined)
139 | {
140 | myMesh.rotation.x = new_shape.rotation.x;
141 | myMesh.rotation.y = new_shape.rotation.z;
142 | myMesh.rotation.z = new_shape.rotation.y;
143 | }
144 | else
145 | buildMeshFromQuaternion(myMesh, new_shape);
146 |
147 |
148 | }
149 |
150 | // shapeId is the idx of the shape in this._3Dshapes or the idx at which it should be if it is the first time
151 | // shape3d is the new shape (either a totaly new one or an update of a previous one)
152 | setObject(shapeId, shape3d)
153 | {
154 |
155 | if (shapeId < this._3Dshapes.length)
156 | {
157 | this.updateToNewShape(this._3Dshapes[shapeId], shape3d);
158 | }
159 | else
160 | {
161 | let shape_cp = (new Shape3D()).initializeFromShape3D(shape3d);
162 |
163 | buildThreeObject(shape_cp);
164 | this._3Dshapes.push(shape_cp);
165 |
166 | // as we can't share the same mesh between multiple scenes, we are making
167 | // a copy of it just before adding it to the scene, otherwise their might be two scenes trying to use the same mesh
168 | this.scene.add(shape_cp.three_object);
169 | }
170 |
171 | }
172 |
173 | unsetObject(idx)
174 | {
175 | if(idx>=0 && idx=1?this.sourceNames[0]:undefined;
40 | let telemetry = telemName!=undefined?app.telemetries[telemName]:undefined;
41 | if (telemetry != undefined)
42 | this._values = telemetry.values;
43 | }
44 |
45 | return this._values;
46 | }
47 |
48 | set values(new_values)
49 | {
50 | this._values = new_values;
51 | }
52 |
53 | formatDetails3D(strToTrim)
54 | {
55 | let nb = Number(strToTrim);
56 | if (!nb.isNaN)
57 | {
58 | if (nb == 0)
59 | return "0"
60 |
61 | return nb.toPrecision(2).toString();
62 | }
63 | return strToTrim;
64 | }
65 |
66 | updateDetails3D()
67 | {
68 | if (this.type != "3D")
69 | return;
70 |
71 | this.details_3d_formatted.position.x = this.formatDetails3D(this._values[0].position.x);
72 | this.details_3d_formatted.position.y = this.formatDetails3D(this._values[0].position.y);
73 | this.details_3d_formatted.position.z = this.formatDetails3D(this._values[0].position.z);
74 |
75 | if (this._values[0].quaternion == undefined)
76 | {
77 | this.details_3d_formatted.rotation.x = this.formatDetails3D(this._values[0].rotation.x);
78 | this.details_3d_formatted.rotation.y = this.formatDetails3D(this._values[0].rotation.y);
79 | this.details_3d_formatted.rotation.z = this.formatDetails3D(this._values[0].rotation.z);
80 | }
81 | else
82 | {
83 | this.details_3d_formatted.quaternion.x = this.formatDetails3D(this._values[0].quaternion.x);
84 | this.details_3d_formatted.quaternion.y = this.formatDetails3D(this._values[0].quaternion.y);
85 | this.details_3d_formatted.quaternion.z = this.formatDetails3D(this._values[0].quaternion.z);
86 | this.details_3d_formatted.quaternion.w = this.formatDetails3D(this._values[0].quaternion.w);
87 | }
88 |
89 |
90 | }
91 |
92 | updateFormattedValues()
93 | {
94 | if ((this.type != "number" && this.type != "xy") || this.values == undefined || this.values[0] == undefined)
95 | this.values_formatted = "";
96 |
97 | if (this.type == "xy" && this.values[1] != undefined && typeof(this.values[0]) == 'number')
98 | this.values_formatted = ((this.values[0].toFixed(4)) + " " +(this.values[1].toFixed(4)));
99 | else if (this.type == "number" && typeof(this.values[0]) == 'number')
100 | {
101 | this.values_formatted = (this.values[0].toFixed(4));
102 | }
103 |
104 | this.updateNameColor();
105 | this.updateDetails3D();
106 | }
107 |
108 | updateNameColor()
109 | {
110 | if (this.type == "3D" && this.values[0] != undefined)
111 | this.name_color = this.values[0].color;
112 |
113 | this.name_color = "black";
114 | }
115 |
116 | destroy(){
117 | for(let name of this.sourceNames){
118 | onTelemetryUnused(name);
119 | }
120 | this.sourceNames.length = 0;
121 | this.onSerieChanged = undefined;
122 | }
123 |
124 | addSource(name){
125 | this.sourceNames.push(name);
126 | onTelemetryUsed(name);
127 | }
128 |
129 | update(){ // this function is called from its widget, on updateView()
130 |
131 | this.applyTimeWindow();
132 | // no formula, simple data reference
133 | if(this.formula=="" && this.sourceNames.length==1 && app.telemetries[this.sourceNames[0]]){ // in this case our data serie matches a simple telemetry
134 |
135 | let isXY = app.telemetries[this.sourceNames[0]].type=="xy";
136 | this.data[0] = app.telemetries[this.sourceNames[0]].data[0];
137 | this.data[1] = app.telemetries[this.sourceNames[0]].data[1];
138 | if(isXY) this.data[2] = app.telemetries[this.sourceNames[0]].data[2];
139 | this.pendingData[0] = app.telemetries[this.sourceNames[0]].pendingData[0];
140 | this.pendingData[1] = app.telemetries[this.sourceNames[0]].pendingData[1];
141 | if(isXY) this.pendingData[2] = app.telemetries[this.sourceNames[0]].pendingData[2];
142 |
143 | if (this.onSerieChanged != undefined) // we want to notify that our serie may have changed
144 | this.onSerieChanged();
145 |
146 | this.updateFormattedValues();
147 | }
148 | else if (this.formula != "" && this.sourceNames.length>=1)
149 | {
150 | // TODO
151 | }
152 | }
153 |
154 | updateStats(){
155 | this.stats = computeStats(this.data);
156 | }
157 |
158 | applyTimeWindow(){
159 | if(parseFloat(app.viewDuration)<=0) return;
160 | for(let key of this.sourceNames) {
161 | if(app.telemetries[key] == undefined) continue;
162 | let d = app.telemetries[key].data;
163 | let timeIdx = 0;
164 | if(app.telemetries[key].type=="xy") timeIdx = 2;
165 | let latestTimestamp = d[timeIdx][d[timeIdx].length-1];
166 | let minTimestamp = latestTimestamp - parseFloat(app.viewDuration);
167 | let minIdx = findClosestLowerByIdx(d[timeIdx], minTimestamp);
168 | if(d[timeIdx][minIdx]0)telem.usageCount--;
212 | }
--------------------------------------------------------------------------------
/server/www/classes/Telemetry.js:
--------------------------------------------------------------------------------
1 | class Telemetry{
2 | constructor(_name, unit = undefined, type = "number"){
3 | this.type = type; // either "number", "text", "3D" or "xy"
4 | this.name = _name;
5 | this.unit = ( unit != "" ) ? unit : undefined;
6 | this.usageCount = 0;
7 |
8 |
9 | // contains either one (if !xy) or two values (if xy).
10 | this.values = [];
11 |
12 | this.data = [[],[]]; // data[0] contains the timestamps and data[1] contains the values corresponding to each timestamp
13 | this.pendingData = [[],[]];
14 |
15 | if(this.type == "xy")
16 | {
17 | // in this case, this.data and this.pending data contain 3 arrays, the two first for x and y values and the last one for the timestamp
18 | this.data.push([]);
19 | this.pendingData.push([]);
20 | }
21 |
22 | // this is what will be displayed on the left pannel next to the telem name,
23 | // it is either the current value of the telem (number), or its text or the type of the shape ...
24 | this.values_formatted = "";
25 |
26 |
27 | if (this.type == "3D")
28 | this.setShapeTypeDelay();
29 | }
30 |
31 | clearData()
32 | {
33 | this.values.length = 0;
34 | for(let arr of this.data) { arr.length = 0; }
35 | for(let arr of this.pendingData) { arr.length = 0; }
36 | this.values_formatted = "";
37 | }
38 |
39 | setShapeTypeDelay()
40 | {
41 | setTimeout(()=>{
42 | if (!this.setShapeType())
43 | this.setShapeTypeDelay();
44 | }, 50);
45 | }
46 |
47 | setShapeType()
48 | {
49 | let res0 = this.data[1][this.data[1].length-1];
50 |
51 | if (res0 != undefined)
52 | {
53 | let res1 = res0.type;
54 | if (res1 != undefined)
55 | {
56 | this.values_formatted = res1;
57 | return true;
58 | }
59 | }
60 | return false;
61 | }
62 |
63 | iniFromTelem(telem)
64 | {
65 | this.type = telem.type;
66 | this.name = telem.name;
67 | this.unit = telem.unit;
68 | this.usageCount = telem.usageCount;
69 | this.values = telem.values;
70 | this.data = telem.data;
71 | this.pendingData = telem.pendingData;
72 |
73 | return this;
74 | }
75 |
76 | updateFormattedValues() {
77 |
78 | if ((this.type == "number" || this.type == "xy") && this.values[0] != undefined && typeof(this.values[0])=='number')
79 | {
80 | let res = this.values[0].toFixed(4);
81 |
82 | if (this.type=="xy" && this.values.length == 2)
83 | res += (" " + this.values[1].toFixed(4));
84 |
85 | this.values_formatted = res;
86 | }
87 | else if (this.type == "text")
88 | {
89 | this.values_formatted = this.values[0];
90 | }
91 | else if (this.type != "3D")
92 | // if equals 3D, then values_formatted contains the name of the shape and has already been set at instanciation,
93 | // otherwise, it means we haven't been able to get te good text to show so we just return ""
94 | {
95 | this.values_formatted = "";
96 | }
97 |
98 | }
99 | }
--------------------------------------------------------------------------------
/server/www/classes/communication/connection/Connection.js:
--------------------------------------------------------------------------------
1 | var ConnectionCount = 0; // counts the number of Connections instanciated, it is useful for Connections ids
2 | class Connection{
3 | constructor(){
4 | if (this.constructor === Connection)
5 | {
6 | throw new Error("Connection is an abstract class, it should only be inherited and never instanciated !");
7 | }
8 | this.name = "";
9 | this.id = "connection-"+ConnectionCount++;
10 | this.type = "";
11 | this.connected = false;
12 | this.inputs = [];
13 | }
14 |
15 | connect(){
16 |
17 | }
18 |
19 | removeInput(input){
20 | for(let i=0;i {
20 | let msg = message.data;
21 | if("id" in msg){
22 | for(let input of this.inputs){
23 | if(input.id == msg.id){
24 | input.onMessage(msg);
25 | break;
26 | }
27 | }
28 | }
29 | else{
30 | if("data" in msg) {
31 | parseData(msg); //update server so it keeps track of connection IDs when forwarding data
32 | }
33 | else if("cmd" in msg) {
34 | //nope
35 | }
36 | }
37 | });
38 | this.vscode.postMessage({ cmd: "listSerialPorts"});
39 | //Report UDP input as connected
40 | this.udp.connected = true;
41 | this.connected = true;
42 | return true;
43 | }
44 |
45 | disconnect() {
46 | for(let input of this.inputs){
47 | input.disconnect();
48 | }
49 | this.connected = false;
50 | }
51 |
52 | sendServerCommand(command) {
53 | this.vscode.postMessage(command);
54 | }
55 |
56 | sendCommand(command) {
57 | for(let input of this.inputs){
58 | input.sendCommand(command);
59 | }
60 | }
61 |
62 | updateCMDList() {
63 | for(let input of this.inputs){
64 | input.updateCMDList();
65 | }
66 | }
67 |
68 | createInput(type) {
69 | if(type=="serial") {
70 | let serialIn = new DataInputSerial(this, "Serial");
71 | this.inputs.push(serialIn);
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/server/www/classes/communication/connection/ConnectionTeleplotWebsocket.js:
--------------------------------------------------------------------------------
1 | class ConnectionTeleplotWebsocket extends Connection{
2 | constructor(){
3 | super();
4 | this.name=""
5 | this.type = "teleplot-websocket";
6 | this.inputs = [];
7 | this.socket = null;
8 | this.address = "";
9 | this.port = "";
10 | this.udp = new DataInputUDP(this, "UDP");
11 | this.udp.address = "";
12 | this.udp.port = UDPport;
13 | this.inputs.push(this.udp);
14 | }
15 |
16 | connect(_address, _port){
17 | this.name = _address+":"+_port;
18 | this.address = _address;
19 | this.port = _port;
20 | this.udp.address = this.address;
21 | this.socket = new WebSocket("ws://"+this.address+":"+this.port);
22 | this.socket.onopen = (event) => {
23 | this.udp.connected = true;
24 | this.connected = true;
25 | this.sendServerCommand({ cmd: "listSerialPorts"});
26 | };
27 | this.socket.onclose = (event) => {
28 | this.udp.connected = false;
29 | this.connected = false;
30 | for(let input of this.inputs){
31 | input.disconnect();
32 | }
33 | setTimeout(()=>{
34 | this.connect(this.address, this.port);
35 | }, 2000);
36 | };
37 | this.socket.onmessage = (msgWS) => {
38 | let msg = JSON.parse(msgWS.data);
39 | if("id" in msg){
40 | for(let input of this.inputs){
41 | if(input.id == msg.id){
42 | input.onMessage(msg);
43 | break;
44 | }
45 | }
46 | }
47 | else{
48 | this.udp.onMessage(msg);
49 | }
50 | };
51 | return true;
52 | }
53 |
54 | disconnect(){
55 | if(this.socket){
56 | this.socket.close();
57 | this.socket = null;
58 | }
59 | }
60 |
61 | sendServerCommand(command){
62 | if(this.socket) this.socket.send(JSON.stringify(command));
63 | }
64 |
65 | updateCMDList(){
66 | for(let input of this.inputs){
67 | input.updateCMDList();
68 | }
69 | }
70 |
71 | createInput(type) {
72 | if(type=="serial") {
73 | let serialIn = new DataInputSerial(this, "Serial");
74 | this.inputs.push(serialIn);
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/server/www/classes/communication/data/DataInput.js:
--------------------------------------------------------------------------------
1 | var DataInputCount = 0;
2 | class DataInput{
3 | constructor(_connection, _name){
4 | if (this.constructor === DataInput)
5 | {
6 | throw new Error("DataInput is an abstract class, it should only be inherited and never instanciated !");
7 | }
8 | this.connection = _connection;
9 | this.name = _name;
10 | this.id = "data-input-"+DataInputCount++;
11 | this.type = "";
12 | this.connected = false;
13 | }
14 | }
--------------------------------------------------------------------------------
/server/www/classes/communication/data/DataInputSerial.js:
--------------------------------------------------------------------------------
1 | class DataInputSerial extends DataInput{
2 | constructor(_connection, _name) {
3 | super(_connection, _name);
4 | this.port = null;
5 | this.baudrate = 115200;
6 | this.type = "serial";
7 | this.portList = [];
8 | this.listPorts();
9 | this.textToSend = "";
10 | this.endlineToSend = "";
11 | }
12 |
13 | connect(){
14 | let baud = parseInt(this.baudrate);
15 | this.connection.sendServerCommand({ id: this.id, cmd: "connectSerialPort", port: this.port, baud: baud})
16 | }
17 |
18 | disconnect(){
19 | this.connection.sendServerCommand({ id: this.id, cmd: "disconnectSerialPort"})
20 | }
21 |
22 | onMessage(msg){
23 | if("data" in msg) {
24 | msg.input = this;
25 | parseData(msg);
26 | }
27 | else if("cmd" in msg) {
28 | if(msg.cmd == "serialPortList"){
29 | this.portList.length = 0;
30 | for(let serial of msg.list){
31 | if( serial.locationId
32 | || serial.serialNumber
33 | || serial.pnpId
34 | || serial.vendorId
35 | || serial.productId ){
36 | this.portList.push(serial);
37 | }
38 | }
39 | }
40 | else if(msg.cmd == "serialPortConnect"){
41 | this.connected = true;
42 | }
43 | else if(msg.cmd == "serialPortDisconnect"){
44 | this.connected = false;
45 | }
46 | }
47 | }
48 |
49 | listPorts(){
50 | this.connection.sendServerCommand({ id: this.id, cmd: "listSerialPorts"});
51 | }
52 |
53 | sendCommand(){
54 | //nope
55 | }
56 |
57 | updateCMDList(){
58 | //nope
59 | }
60 |
61 | sendText(text, lineEndings) {
62 | let escape = lineEndings.replace("\\n","\n");
63 | escape = escape.replace("\\r","\r");
64 | this.connection.sendServerCommand({ id: this.id, cmd: "sendToSerial", text: text+escape});
65 | }
66 | }
--------------------------------------------------------------------------------
/server/www/classes/communication/data/DataInputUDP.js:
--------------------------------------------------------------------------------
1 | class DataInputUDP extends DataInput{
2 | constructor(_connection, _name) {
3 | super(_connection, _name);
4 | this.type = "UDP";
5 | this.address = "";
6 | this.port = UDPport;
7 | }
8 |
9 | connect(){}
10 | disconnect(){}
11 |
12 | onMessage(msg){
13 | if("data" in msg) {
14 | msg.input = this;
15 | parseData(msg);
16 | }
17 | else if("cmd" in msg) {
18 | //nope
19 | }
20 | }
21 |
22 | sendCommand(command){
23 | this.connection.sendServerCommand({ id: this.id, cmd: command});
24 | }
25 |
26 | updateCMDList(){
27 | this.sendCommand("|_telecmd_list_cmd|");
28 | }
29 | }
--------------------------------------------------------------------------------
/server/www/classes/communication/serverMessageReading.js:
--------------------------------------------------------------------------------
1 | //parses the message we received from the server
2 |
3 | function parseData(msgIn){
4 |
5 | if(app.isViewPaused) return; // Do not buffer incomming data while paused
6 | let now = new Date().getTime();
7 |
8 |
9 | let fromSerial = msgIn.fromSerial || (msgIn.input && msgIn.input.type=="serial");
10 | if(fromSerial) now = msgIn.timestamp;
11 |
12 | now/=1000; // we convert timestamp in seconds for uPlot to work
13 | //parse msg
14 | let msgList = (""+msgIn.data).split("\n");
15 |
16 | for(let msg of msgList){
17 | try{
18 | // Inverted logic on serial port for usability
19 | if(fromSerial && msg.startsWith(">")) msg = msg.substring(1);// remove '>' to consider as variable
20 | else if(fromSerial && !msg.startsWith(">")) msg = ">:"+msg;// add '>' to consider as log
21 |
22 | // Command
23 | if(msg.startsWith("|"))
24 | parseCommandList(msg);
25 | // Log
26 | else if(msg.startsWith(">"))
27 | parseLog(msg, now);
28 | // 3D
29 | else if (msg.substring(0,3) == "3D|")
30 | parse3D(msg, now);
31 | // Data
32 | else
33 | parseVariablesData(msg, now);
34 | }
35 | catch(e){console.log(e)}
36 | }
37 | }
38 |
39 | function parseCommandList(msg) // a String containing a list of commands, ex : "|sayHello|world|"
40 | {
41 | let cmdList = msg.split("|");
42 | for(let cmd of cmdList){
43 | if(cmd.length==0) continue;
44 | if(cmd.startsWith("_")) continue;
45 | if(app.commands[cmd] == undefined){
46 | let newCmd = {
47 | name: cmd
48 | };
49 | Vue.set(app.commands, cmd, newCmd);
50 | }
51 | }
52 | if(!app.cmdAvailable && Object.entries(app.commands).length>0) app.cmdAvailable = true;
53 |
54 | }
55 |
56 | // msg : a String containing a log message, ex : ">:Hello world"
57 | // now : a Number representing a timestamp
58 | function parseLog(msg, now)
59 | {
60 |
61 | let logStart = msg.indexOf(":")+1;
62 |
63 | let logText = msg.substr(logStart);
64 | let logTimestamp = (parseFloat(msg.substr(1, logStart-2)))/1000; // /1000 to convert to seconds
65 | if(isNaN(logTimestamp) || !isFinite(logTimestamp)) logTimestamp = now;
66 |
67 | logBuffer.push(new Log(logTimestamp, logText));
68 | }
69 |
70 |
71 | function isTextFormatTelemetry(msg)
72 | {
73 | return (Array.from(msg)).some((mchar) => ((mchar < '0' || mchar > '9') && mchar!='-' && mchar!=':' && mchar!='.' && mchar!=';' && mchar!= ',' && mchar!= '§'));
74 | }
75 |
76 | // msg : a String containing data of a variable, ex : "myValue:1627551892437:1234|g"
77 | // now : a Number representing a timestamp
78 | function parseVariablesData(msg, now)
79 | {
80 | if(!msg.includes(':')) return;
81 |
82 | let startIdx = msg.indexOf(':');
83 |
84 | let keyAndWidgetLabel = msg.substr(0,msg.indexOf(':'));
85 |
86 | if(keyAndWidgetLabel.substring(0, 6) === "statsd") return;
87 |
88 | let [name, widgetLabel] = separateWidgetAndLabel(keyAndWidgetLabel);
89 |
90 | let endIdx = msg.lastIndexOf('|');
91 | if (endIdx == -1) endIdx = msg.length;
92 |
93 | let flags = msg.substr(endIdx+1);
94 |
95 | let isTextFormatTelem = flags.includes('t');
96 |
97 | let unit = "";
98 | let unitIdx = msg.indexOf('§');
99 | if (unitIdx!=-1)
100 | {
101 | unit = msg.substring(unitIdx+1, endIdx);
102 | endIdx = unitIdx;
103 | }
104 |
105 | // Extract values array
106 | let values = msg.substring(startIdx+1, endIdx).split(';')
107 | let xArray = [];
108 | let yArray = [];
109 | let zArray = [];
110 | for(let value of values)
111 | {
112 | /* All possibilities :
113 |
114 | Number timestamp :
115 | [1627551892437, 1234]
116 | Number no timestamp :
117 | [1234]
118 |
119 | Text timestamp :
120 | [1627551892437, Turned On]
121 | Text no timestamp :
122 | [Turned On]
123 |
124 | xy timestamp :
125 | [1, 1, 1627551892437]
126 | xy no timestamp :
127 | [1, 1]
128 | */
129 |
130 | if(value.length==0) continue;
131 | let dims = value.split(":");
132 |
133 | if(dims.length == 1){
134 | xArray.push(now);
135 | yArray.push(isTextFormatTelem?dims[0]:parseFloat(dims[0]));
136 | }
137 | else if(dims.length == 2){
138 | let v1 = parseFloat(dims[0]);
139 | if (!flags.includes("xy")) // in this case, v1 is the timestamp that we convert to seconds
140 | v1/=1000;
141 |
142 | xArray.push(v1);
143 | yArray.push(isTextFormatTelem?dims[1]:parseFloat(dims[1]));
144 | zArray.push(now);
145 | }
146 | else if(dims.length == 3){
147 | xArray.push(parseFloat(dims[0]));
148 | yArray.push(parseFloat(dims[1]));
149 | zArray.push(parseFloat(dims[2])/1000);// this one is the timestamp we convert to seconds
150 | }
151 |
152 | }
153 | //console.log("name : "+name+", xArray : "+xArray+", yArray : "+yArray+", zArray : "+zArray+", unit : "+unit+", flags : "+flags);
154 | if(xArray.length>0){
155 | appendData(name, xArray, yArray, zArray, unit, flags, isTextFormatTelem?"text":"number", widgetLabel);
156 | }
157 | }
158 |
159 | function separateWidgetAndLabel(keyAndWidgetLabel)
160 | {
161 | //keyAndWidgetLabel ex : "mysquare0,the_chart541"
162 | //keyAndWidgetLabel ex2 : "mysquare0"
163 |
164 | let marray = keyAndWidgetLabel.split(',');
165 | let key = marray[0];
166 |
167 | let label = marray.length > 1 ? marray[1] : undefined;
168 |
169 | return [key, label]
170 | }
171 |
172 | function parse3D(msg, now)
173 | {
174 | //3D|myData1:R::3.14:P:1:2:-1:S:cube:W:5:H:4:D:3:C:red|g
175 |
176 | let firstPipeIdx = msg.indexOf("|");
177 | let startIdx = msg.indexOf(':') +1;
178 | let endIdx = msg.lastIndexOf("|");
179 | if (endIdx <= firstPipeIdx) endIdx = msg.length;// in this case the last pipe is not given ( there are no flags )
180 | let keyAndWidgetLabel = msg.substring(firstPipeIdx+1, startIdx-1);
181 |
182 | let [key, widgetLabel] = separateWidgetAndLabel(keyAndWidgetLabel);
183 |
184 | let values = msg.substring(startIdx, endIdx).split(';')
185 |
186 | let flags = msg.substr(endIdx+1);
187 |
188 | for (let value of values)
189 | {
190 | if (value == "")
191 | continue;
192 |
193 | let valueStartIdx = 0;
194 | let timestamp;
195 | if (isLetter(value[0]))
196 | {
197 | timestamp = now;
198 | }
199 | else
200 | {
201 | let trueStartIdx = value.indexOf(':');
202 |
203 | timestamp = (value.substring(0, trueStartIdx))/1000;// we divise by 1000 to get timestamp in seconds
204 |
205 | valueStartIdx = trueStartIdx+1;
206 | }
207 |
208 | let rawShape = value.substring(valueStartIdx, value.length);
209 |
210 |
211 | let shape3D;
212 | try { shape3D = new Shape3D().initializeFromRawShape(key, rawShape);}
213 | catch(e) { throw new Error("Error invalid shape text given : "+rawShape)};
214 |
215 | appendData(key, [timestamp], [shape3D], [], "", flags, "3D", widgetLabel)
216 | }
217 | }
218 |
219 | function getWidgetAccordingToLabel(widgetLabel, widgetType, isXY = false)
220 | {
221 | if (widgetLabel != undefined)
222 | {
223 | for (let i = 0; i arr[idx] = elem); }
294 | else { valuesZ.forEach((elem, idx, arr)=>arr[idx] = elem); }
295 |
296 | // Flush data into buffer (to be flushed by updateView)
297 |
298 | telemBuffer[key].data[0].push(...valuesX);
299 | telemBuffer[key].data[1].push(...valuesY);
300 | telemBuffer[key].values.length = 0;
301 |
302 |
303 | if(app.telemetries[key].type=="xy")
304 | {
305 | telemBuffer[key].values.push(valuesX[valuesX.length-1]);
306 | telemBuffer[key].values.push(valuesY[valuesY.length-1]);
307 |
308 | telemBuffer[key].data[2].push(...valuesZ);
309 | }
310 | else
311 | {
312 | telemBuffer[key].values.push(valuesY[valuesY.length-1]);
313 |
314 | if (app.telemetries[key].type=="3D")
315 | {
316 | let prevShapeIdx = app.telemetries[key].data[1].length -1;
317 |
318 | let newShape = telemBuffer[key].values[0];
319 |
320 | if (prevShapeIdx >= 0) // otherwise, it means that there ain't any previous shape
321 | {
322 | let shapeJustBefore = app.telemetries[key].data[1][prevShapeIdx];
323 |
324 | newShape.fillUndefinedWith(shapeJustBefore);// fills undefined properties of the new shape with the previous ones.
325 | }
326 | else if (newShape.type != undefined)
327 | {
328 | newShape.fillUndefinedWithDefaults();
329 | }
330 | else
331 | {
332 | throw new Error("no type given for the shape ( cube, or sphere ... should be passed )");
333 | }
334 | }
335 | }
336 | return;
337 | }
338 |
--------------------------------------------------------------------------------
/server/www/classes/view/logs/Log.js:
--------------------------------------------------------------------------------
1 | class Log
2 | {
3 | constructor(timestamp, text)
4 | {
5 | this.timestamp = timestamp;
6 | this.text = text;
7 | }
8 | }
--------------------------------------------------------------------------------
/server/www/classes/view/logs/LogConsole.js:
--------------------------------------------------------------------------------
1 | var logConsoleInstance = null;
2 | var lastLogHoveredTimestamp = 0; // the most recent timestamp at wich a log was hovered
3 | var logIndexToHighlight = -1; // the index of the log to be highlited
4 | // Log Console is of type singleton, it should never be instanciated with its own constructor but with getInstance()
5 | class LogConsole
6 | {
7 | constructor()
8 | {
9 | this.startIdx = 0; // where to start in app.logs
10 | this.endIdx = app.logs.length;
11 |
12 | this.container = document.getElementById("log-container-div");
13 |
14 | this.isBeingScrolled = false;
15 | this.container.addEventListener('mousedown', ()=>{this.isBeingScrolled = true; }, false);
16 | this.container.addEventListener('mouseup', ()=>{this.isBeingScrolled = false; }, false);
17 |
18 | this.scroller = undefined;
19 | this.config = undefined;
20 |
21 | this.hyperlist = undefined;
22 | this.autoScrollToEnd = true;
23 | this.containerHeight = 500;// the height of the console log
24 | this.itemHeight = 20; // has to be the same size as .vrow height in style.css
25 | }
26 |
27 | static getInstance()
28 | {
29 | if (logConsoleInstance == null)
30 | logConsoleInstance = new LogConsole();
31 |
32 | return logConsoleInstance
33 | }
34 |
35 | static reboot()
36 | {
37 | LogConsole.getInstance().logsUpdated(0,0);
38 | }
39 |
40 | getHyperListConfig()
41 | {
42 | let mstartIdx = this.startIdx;
43 |
44 | function onMouseLeaveLog(el)
45 | {
46 | if (el != null && el != undefined)
47 | el.classList.remove('log-vue-selected');
48 |
49 | logCursor.remove();
50 | };
51 |
52 | return {
53 | height : this.containerHeight,
54 | itemHeight: this.itemHeight,
55 | total: (this.endIdx - this.startIdx),
56 |
57 | generate(rowIdx) {
58 | let el = document.createElement('div')
59 | let currIdx = rowIdx + mstartIdx;
60 | let currLog = app.logs[currIdx];
61 |
62 | if (currLog == undefined) return;
63 |
64 | el.innerHTML = currLog.text;
65 |
66 |
67 | el.addEventListener("mouseover", function () {
68 |
69 | lastLogHoveredTimestamp = new Date().getTime();
70 |
71 | el.classList.add('log-vue-selected');
72 | logCursor.pub(currLog);
73 |
74 |
75 | // normaly, we should not have to do that ( with setTimeout... ) and the call to mouseLeaveLog() from mouseleave event listener should be enough
76 | // however, in some cases, mousleave event is not triggered, so we are also doing that to make sure onMouseleaveLog() is called everytime
77 |
78 | let maxDuration = 150; // this is the maximal duration possible between 2 mouseover events
79 | let maxTimeoutImprecision = 100; // we consider that this is the maximum imprecision the setTimeout function should have
80 |
81 | setTimeout(()=>{
82 | let currentTime = new Date().getTime();
83 |
84 |
85 | // if the last log hovered timestamp exeeds the max duration betwee, 2 mouseover events,
86 | // it means that the cursor has left the log
87 | if ((currentTime - lastLogHoveredTimestamp) >= maxDuration)
88 | {
89 | onMouseLeaveLog(el);
90 | }
91 | }, maxTimeoutImprecision + maxDuration)
92 |
93 | });
94 |
95 | el.addEventListener("mouseleave", function () {
96 | onMouseLeaveLog(el);
97 | });
98 |
99 | if (logIndexToHighlight == currIdx)
100 | {
101 | el.classList.add('log-vue-selected');
102 | }
103 | return el;
104 | }
105 | };
106 | }
107 |
108 | goToLog(logIdx)
109 | {
110 | this.autoScrollToEnd = false;
111 |
112 | logIdx = Math.max(logIdx, this.startIdx);
113 | logIdx = Math.min(logIdx, this.endIdx);
114 |
115 | logIndexToHighlight = logIdx;
116 |
117 | // we do -(containerHeight/2) as we want to let some space before the current log
118 | this.container.scrollTop = this.itemHeight * (logIdx) - (this.containerHeight/2);
119 | }
120 |
121 | untrackLog()
122 | {
123 | logIndexToHighlight = -1;
124 | this.autoScrollToEnd = true;
125 | }
126 |
127 | logsUpdated(startIdx, endIdx) {
128 |
129 | this.startIdx = startIdx;
130 | this.endIdx = endIdx
131 |
132 | if (this.hyperlist == undefined)
133 | {
134 | // let mscroller = document.createElement('div');
135 |
136 | this.hyperlist = HyperList.create(this.container, this.getHyperListConfig());
137 |
138 | // this.scroller = mscroller;
139 |
140 | }
141 | else
142 | {
143 | this.hyperlist.refresh(this.container, this.getHyperListConfig());
144 |
145 | if (this.autoScrollToEnd && !app.isViewPaused && !this.isBeingScrolled)
146 | {
147 | // this.logIndexToHighlighting = -1;
148 | this.container.scrollTop = app.logs.length * this.itemHeight;// this is always greater than the div height, so it will scroll to the end
149 | }
150 | }
151 | }
152 |
153 | }
--------------------------------------------------------------------------------
/server/www/classes/view/widgets/ChartWidget.js:
--------------------------------------------------------------------------------
1 | /*
2 | This class represents a chart
3 | */
4 |
5 | class ChartWidget extends DataWidget{
6 | constructor(_isXY=false) {
7 | super();
8 | this.type = "chart";
9 | this.isXY = _isXY;
10 | this.data = []; // this is what contains the data ready for uplot
11 | this.data_available_xy = false; // this tells wheter this.data is ready for uplot for xy chart or not
12 | this.options = {
13 | title: "",
14 | width: undefined,
15 | height: undefined,
16 | scales: { x: { time: true }, y:{} },
17 | series: [ {} ],
18 | focus: { alpha: 1.0, },
19 | cursor: {
20 | lock: false,
21 | focus: { prox: 16, },
22 | sync: { key: window.cursorSync.key, setSeries: true }
23 | },
24 | legend: { show: false }
25 | }
26 | if(this.isXY) {
27 | this.data.push(null);
28 | this.options.mode = 2;
29 | delete this.options.cursor;
30 | this.options.scales.x.time = false;
31 | }
32 | this.forceUpdate = true;
33 |
34 | updateWidgetSize_(this);
35 | }
36 | destroy(){
37 | for(let s of this.series) s.destroy();
38 | }
39 |
40 | addSerie(_serie){
41 | _serie.options._serie = _serie.name;
42 | _serie.options._id = _serie.id;
43 | // Search available color
44 | for(let i=this.series.length; i>=0; i--) {
45 | let candidateColor = ColorPalette.getColor(i).toString();
46 | let alreadyUsed = this.options.series.findIndex((o)=>o.stroke==candidateColor) >= 0;
47 | if(alreadyUsed) continue;
48 | _serie.options.stroke = ColorPalette.getColor(i).toString();
49 | _serie.options.fill = ColorPalette.getColor(i, 0.1).toString();
50 | break;
51 | }
52 | if(this.isXY) _serie.options.paths = drawXYPoints;
53 | this.options.series.push(_serie.options);
54 | _serie.dataIdx = this.data.length;
55 | this.data.push([]);
56 | this.series.push(_serie);
57 | this.forceUpdate = true;
58 | }
59 |
60 | removeSerie(_serie){
61 | let idx = this.series.findIndex((s)=>s.id==_serie.id);
62 | if(idx>=0) this.series.splice(idx, 1);
63 |
64 | let idxOption = this.options.series.findIndex((o)=>o._id==_serie.id);
65 | if(idxOption>=0) this.options.series.splice(idxOption, 1);
66 |
67 | this.forceUpdate = true;
68 | this.update();
69 | }
70 |
71 | update(){
72 | // Update each series
73 | for(let s of this.series) s.update();
74 | if(app.isViewPaused && !this.forceUpdate) return;
75 |
76 | if(this.isXY){
77 | if(this.forceUpdate) {
78 | this.data.length = 0;
79 | this.data.push(null);
80 | for(let s of this.series){
81 | s.dataIdx = this.data.length;
82 | this.data.push(s.data);
83 | }
84 | this.id += "-" //DUMMY way to force update
85 | // triggerChartResize();
86 | this.forceUpdate = false;
87 | }
88 | else {
89 | for(let s of this.series) {
90 | if(s.pendingData[0].length==0) continue;
91 | for(let i=0;i {return (el != null && el.length == 1) };
98 |
99 | this.data_available_xy = (this.data.length>=2 && this.data[1] != null && this.data[1] != undefined
100 | && !(this.data[1].some(elIsAlone)));
101 | }
102 | else if(this.data[0].length==0 || this.forceUpdate && !this.updatedForZeroLength) {
103 | //Create data with common x axis
104 | let dataList = [];
105 | for(let s of this.series) dataList.push(s.data);
106 | this.data.length = 0;
107 | this.data = uPlot.join(dataList)
108 | this.id += "-" //DUMMY way to force update
109 | // triggerChartResize();
110 | this.forceUpdate = false;
111 | this.updatedForZeroLength = true; // Avoid constant update of widget with no data
112 | }
113 | else {
114 | //Iterate on all series, adding timestamps and values
115 | let dataList = [];
116 | for(let s of this.series) dataList.push(s.data);
117 | this.data.length = 0;
118 | this.data.push(...uPlot.join(dataList));
119 | if(this.data[0].length>0) this.updatedForZeroLength = false;
120 | }
121 | }
122 | }
--------------------------------------------------------------------------------
/server/www/classes/view/widgets/DataWidget.js:
--------------------------------------------------------------------------------
1 | var widgetCount = 0; // counts the number of DataWidgets instanciated, it is useful for its ids
2 |
3 | /*
4 | if the resize button of a certain widget is clicked
5 | then, widgetBeingResized will contain a reference to this particular widget
6 | otherwise, it is equal to null
7 | */
8 | var widgetBeingResized = null;
9 |
10 | window.addEventListener('mouseup', widgetOnMouseUp, false);
11 | window.addEventListener('mousemove', widgetOnMouseMove, false)
12 |
13 | class DataWidget{
14 | constructor() {
15 | if (this.constructor === DataWidget)
16 | {
17 | throw new Error("DataWidget is an abstract class, it should only be inherited and never instanciated !");
18 | }
19 |
20 | // usefull to identify a chart ( we can send multiples shapes to be displayed in the same chart by precising their chart label )
21 | this.label = undefined;
22 |
23 | this.options = {
24 | width: undefined,
25 | height: undefined,
26 | }
27 |
28 | this.series = []; // DataSerie
29 | this.id = "widget-chart-" + widgetCount;
30 | widgetCount ++;
31 | this.gridPos = {h:6, w:6, x:0, y:0};
32 | // the X and y coordonates of the cursor when the user drags the resize button :
33 | this.initialCursorXPos = undefined;
34 | this.initialCursorYPos = undefined;
35 | // the height and width of the dataWidget just before the user resizes it :
36 | this.initialHeight = undefined;
37 | this.initialWidth = undefined;
38 | this.isResized = false;
39 | }
40 |
41 | isUsingSource(name){
42 | for(let s of this.series)
43 | if(s.sourceNames.includes(name)) return true;
44 | return false;
45 | }
46 |
47 | _getSourceList(){
48 | let sourceList = {};
49 | for(let s of this.series)
50 | for(let n of s.sourceNames)
51 | sourceList[n] = app.telemetries[n];
52 | return sourceList;
53 | }
54 |
55 | updateStats(){
56 | for(let s of this.series)
57 | s.updateStats();
58 | }
59 | }
60 |
61 | function onMouseDownOnResizeButton_(event, widget)
62 | {
63 | widget.initialCursorXPos = event.pageX;
64 | widget.initialCursorYPos = event.pageY;
65 | widget.initialHeight = widget.gridPos.h;
66 | widget.initialWidth = widget.gridPos.w;
67 | widgetBeingResized = widget;
68 | widget.isResized = true;
69 | }
70 |
71 | function widgetOnMouseUp()
72 | {
73 | if(widgetBeingResized)
74 | {
75 | widgetBeingResized.isResized = false;
76 | updateWidgetSize_(widgetBeingResized);
77 | widgetBeingResized = null;
78 | }
79 | }
80 |
81 | function widgetOnMouseMove(event)
82 | {
83 | if (widgetBeingResized)
84 | {
85 | // the div element containing our widgets
86 | var widgetContainerDiv = document.getElementById("widget-container-div");
87 |
88 | let minWidgetWidth = Math.round(widgetContainerDiv.clientWidth/12);
89 | let minWidgetHeight = 50;
90 |
91 | let heightExtension = (event.pageY - widgetBeingResized.initialCursorYPos);
92 | let widthExtension = (event.pageX - widgetBeingResized.initialCursorXPos);
93 |
94 | widgetBeingResized.gridPos.h = widgetBeingResized.initialHeight + Math.round(heightExtension/minWidgetHeight);
95 | widgetBeingResized.gridPos.w = widgetBeingResized.initialWidth + Math.round(widthExtension/minWidgetWidth);
96 |
97 | }
98 | }
99 |
100 | function updateWidgetSize_(widget){
101 | const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
102 | widget.gridPos.w = clamp(widget.gridPos.w, 2, 12);
103 | widget.gridPos.h = clamp(widget.gridPos.h, 2, 20);
104 | widget.options.height = (widget.gridPos.h-1)*50;
105 | widget.forceUpdate = true;
106 | triggerChartResize();
107 | }
108 |
109 | var chartResizeTimeout = null;
110 | function triggerChartResize(){
111 | if(chartResizeTimeout) clearTimeout(chartResizeTimeout);
112 | chartResizeTimeout = setTimeout(()=>{
113 | window.dispatchEvent(new Event('resize'));
114 | }, 100);
115 | }
--------------------------------------------------------------------------------
/server/www/classes/view/widgets/SingleValueWidget.js:
--------------------------------------------------------------------------------
1 | class SingleValueWidget extends DataWidget{
2 | constructor(containsTextFormat=false) {
3 | super();
4 |
5 | this.type = "single_value_number";
6 | this.singlevalue = [0]; // type : array of Number, the value of the widget ( so the average, the max or the min ... according to widgetMode )
7 | // should contain two numbers if represents a xy serie, one otherwise.
8 | this.precision_mode = 0; // either 0 (default), 1 (good) or 2 (very good)
9 |
10 | if (containsTextFormat)
11 | this.type = "single_value_text"
12 |
13 | }
14 |
15 | addSerie(serie)
16 | {
17 | serie.options.stroke = ColorPalette.getColor(0).toString(); // we take the first color of the ColorPalette, so 0
18 | serie.options.fill = ColorPalette.getColor(0, 0.1).toString();
19 |
20 | if (this.series.length != 0)
21 | throw new Error("SingleValueWidget should contain only one serie");
22 | this.series.push(serie);
23 | }
24 |
25 | destroy(){
26 | if (this.series.length == 1)
27 | this.series[0].destroy();
28 | }
29 |
30 |
31 | trimNumberAccordingToPrecision(nb)
32 | {
33 |
34 | let significant_digits = 21;
35 | switch (this.precision_mode)
36 | {
37 | case 0 : // default
38 | significant_digits = 3;
39 | break;
40 | case 1 : // good precision
41 | significant_digits = 7;
42 | break;
43 | case 2 : // maximal precision
44 | significant_digits = 21;
45 | break;
46 | }
47 | return nb.toPrecision(significant_digits);
48 | }
49 |
50 | // updates this.singleValue according to the last value of the serie,
51 | // and also write it in a string format ready to be displayed
52 | updateSingleValue(currentSerie)
53 | {
54 | if (currentSerie == undefined || currentSerie.values[0] == undefined)
55 | return;
56 |
57 | this.singlevalue.length = 0;
58 |
59 |
60 | if (this.type == "single_value_text")
61 | {
62 | this.singlevalue.push(currentSerie.values[0])
63 | }
64 | else if (currentSerie.type=="xy" && currentSerie.values[1] != undefined)
65 | {
66 | this.singlevalue.push(this.trimNumberAccordingToPrecision(currentSerie.values[0]))
67 | this.singlevalue.push(this.trimNumberAccordingToPrecision(currentSerie.values[1]))
68 | }
69 | else if (currentSerie.type=="number")
70 | {
71 | this.singlevalue.push(this.trimNumberAccordingToPrecision(currentSerie.values[0]))
72 | }
73 | }
74 |
75 | update(){
76 | let currentSerie = this.series[0];
77 | currentSerie.update();
78 |
79 | this.updateSingleValue(currentSerie);
80 |
81 | }
82 |
83 |
84 | changeValuePrecision()
85 | {
86 | this.precision_mode = (this.precision_mode + 1) % 3;
87 | }
88 |
89 | }
90 |
--------------------------------------------------------------------------------
/server/www/classes/view/widgets/Widget3D.js:
--------------------------------------------------------------------------------
1 | class Widget3D extends DataWidget{
2 | constructor() {
3 | super();
4 | this.type = "widget3D";
5 | this.worldId = undefined; // the id of the world instance linked to this widget
6 | this.onNewSerieAdded = undefined;
7 | this.onSerieRemoved = undefined;
8 | }
9 |
10 | addSerie(serie)
11 | {
12 | this.series.push(serie);
13 |
14 | if (this.onNewSerieAdded != undefined)
15 | this.onNewSerieAdded();
16 | }
17 |
18 | removeSerie(serie)
19 | {
20 | let idx = this.series.findIndex((s)=>s.id==serie.id);
21 | if(idx>=0){
22 | if (this.onSerieRemoved != undefined)
23 | this.onSerieRemoved(idx);
24 | this.series[idx].destroy();
25 | this.series.splice(idx, 1);
26 | }
27 |
28 | this.update();
29 | }
30 |
31 | destroy(){
32 | for(let s of this.series) s.destroy();
33 |
34 |
35 | // we remove the world linked to this widget from worlds, so it will be removed by the garbage collector
36 | if (this.worldId != undefined)
37 | {
38 |
39 | let i = 0;
40 | let found = false;
41 |
42 | while (i < worlds.length && !found)
43 | {
44 | if (worlds[i].id == this.worldId)
45 | {
46 | worlds.splice(i, 1);
47 | found = true;
48 | }
49 |
50 | i++;
51 | }
52 | }
53 | }
54 |
55 | update(){
56 | for(let s of this.series) s.update();
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/server/www/components/3dComponent/3dComponent.css:
--------------------------------------------------------------------------------
1 | .comp-3d-container
2 | {
3 | width: 100%;
4 | height: calc(100% - 50px);
5 | position: absolute;
6 | top: 50px;
7 | overflow: hidden;
8 | }
--------------------------------------------------------------------------------
/server/www/components/3dComponent/3dComponent.js:
--------------------------------------------------------------------------------
1 | Vue.component('comp-3d', {
2 | name: 'comp-3d',
3 | props: {
4 | series: {type: Array, required: true},
5 | widget: {type: Object, required: true},
6 | },
7 | data() {
8 | return { world : undefined};
9 | },
10 |
11 | mounted() {
12 | this.initializeWorld();
13 | },
14 | methods: {
15 | initializeWorld()
16 | {
17 | let containerDiv = this.$refs.div_3d_container;
18 | this.world = new World(containerDiv);
19 | worlds.push(this.world);
20 | this.widget.worldId = this.world.id;
21 |
22 | this.reDrawShape(-1);// passing -1 means to redraw everything
23 |
24 | this.setUpSeriesObserver();
25 |
26 | // if worlds.length == 1, then we are in the first 3dComponent, so we launch the drawAllWorlds function, but we just need to launch this function once for
27 | // all worlds
28 | if (worlds.length == 1) drawAllWords();
29 | },
30 | setUpSeriesObserver()
31 | {
32 | this.widget.onNewSerieAdded = () => {
33 | let newSerieIdx = this.series.length-1;
34 | let newSerie = this.series[newSerieIdx];
35 | this.reDrawShape(newSerieIdx);
36 |
37 | newSerie.onSerieChanged = () => this.reDrawShape(this.series.findIndex((s)=>s.id==newSerie.id));
38 | };
39 |
40 | this.widget.onSerieRemoved = (idx) => {
41 | this.world.unsetObject(idx)
42 | }
43 |
44 |
45 |
46 | for (let serie of this.series) {
47 | serie.onSerieChanged = () => this.reDrawShape(this.series.findIndex((s)=>s.id==serie.id));
48 | }
49 |
50 | },
51 | reDrawShape(serieId) // -1 means redraw everything, otherwise we pass the serie shape index in this.series
52 | {
53 | if (serieId == -1)
54 | {
55 | for (let i = 0; i < this.series.length; i++)
56 | {
57 | let currSerie = this.series[i];
58 |
59 | if (currSerie.values[0] != undefined)
60 | {
61 | this.world.setObject(i, currSerie.values[0]);
62 | }
63 | }
64 | }
65 | else
66 | {
67 | let currSerie = this.series[serieId];
68 |
69 | if (currSerie == undefined)
70 | throw new Error("trying to acces an index that is invalid : i = " + serieId);
71 |
72 | if (currSerie.values[0] != undefined)
73 | {
74 | this.world.setObject(serieId, currSerie.values[0]);
75 | }
76 | }
77 | },
78 | },
79 | template:'\
80 | \
81 |
',
82 | });
--------------------------------------------------------------------------------
/server/www/components/singleValue/single-value.css:
--------------------------------------------------------------------------------
1 | .single-value-container
2 | {
3 | width: 100%;
4 | height: 100%;
5 | position: absolute;
6 | top: 0;
7 | overflow: hidden;
8 | }
9 |
10 | .single-value-telem-div, .single-value-unit-div, .single-value-value-div
11 | {
12 | display: flex;
13 | align-items: center;
14 | width: 100%;
15 | position: absolute;
16 | }
17 |
18 | .single-value-telem-div
19 | {
20 | width: 100%;
21 | top: 5%;
22 | height: 15%;
23 | z-index: 2;
24 | }
25 |
26 | .single-value-value-div
27 | {
28 | top: 15%;
29 | height: 73%;
30 | width: 100%;
31 | cursor: pointer;
32 | user-select: none;
33 |
34 | display:flex;
35 | flex-direction: column;
36 | }
37 |
38 | .single-value-unit-div
39 | {
40 | height: 15%;
41 | width: 50%;
42 | left: 25%;
43 | top: 82%;
44 | }
45 |
46 | .value1-solo{
47 | height: 100%;
48 | width: 100%;
49 | display: flex;
50 | }
51 |
52 | .value1-2-duo{
53 | width:100%;
54 | height:50%;
55 | display:flex;
56 | }
57 |
--------------------------------------------------------------------------------
/server/www/components/singleValue/singleValue.js:
--------------------------------------------------------------------------------
1 | Vue.component('single-value', {
2 | name: 'single-value',
3 | props: {
4 | widget: {type: Object, required: true},
5 | },
6 | computed: {
7 | telem() { return this.widget.series[0].name; },
8 | value1()
9 | {
10 | // if the widget is of type single_value_text, this.widget.singlevalue[0] is going to contain some text,
11 | // and if the widget is of type single_value_number, it is going to contain some number (in a string format)
12 | return this.widget.singlevalue[0];
13 | },
14 | value2()
15 | {
16 | if (this.widget.type =="single_value_text" || this.widget.singlevalue.length <= 1)
17 | return undefined;
18 | else
19 | return this.widget.singlevalue[1];
20 | },
21 | unit() { return this.widget.series[0].unit; },
22 | getWidgetTitle()
23 | {
24 | if (this.widget.type =="single_value_text")
25 | return "";
26 | else
27 | return "Click to change precision";
28 | }
29 |
30 | },
31 | methods: {
32 | onContainerResized()
33 | {
34 |
35 | if (this.$refs.telem_responsive_text == undefined)
36 | return;
37 |
38 | this.$refs.telem_responsive_text.triggerTextResize();
39 | this.$refs.value_responsive_text1.triggerTextResize();
40 | if (this.$refs.value_responsive_text2 != undefined)
41 | this.$refs.value_responsive_text2.triggerTextResize();
42 | this.$refs.unit_responsive_text.triggerTextResize();
43 | }
44 | },
45 | mounted() {
46 |
47 | const resizeObserverForSingleValue = new ResizeObserver((entries) => {
48 |
49 | this.onContainerResized();
50 | });
51 |
52 | resizeObserverForSingleValue.observe(this.$refs.single_value_container_ref);
53 |
54 | },
55 | updated() {
56 |
57 | // doing that at every updated() call might be a bit expensive,
58 | // but singleValueComponents are way less expensive than plots anyway
59 | this.onContainerResized();
60 | },
61 | unmounted(){
62 | resizeObserverForSingleValue.unobserve(singleValueContainer);
63 | },
64 | template:'\
65 | \
66 |
\
67 | {{telem}} \
68 |
\
69 |
\
70 |
\
71 | {{value1}} \
72 |
\
73 |
\
74 | {{value2}} \
75 |
\
76 |
\
77 |
\
78 | {{unit}} \
79 |
\
80 |
',
81 | });
82 |
83 |
--------------------------------------------------------------------------------
/server/www/components/uPlot/uplot-component.js:
--------------------------------------------------------------------------------
1 | Vue.component('uplot-vue', {
2 | name: 'uplot-vue',
3 | props: {
4 | options: {type: Object, required: true},
5 | data: {type: Array, required: true}, // this is what contains the data ready for uplot
6 | target: {
7 | validator(target) {
8 | return target == null || target instanceof HTMLElement || typeof target === 'function';
9 | },
10 | default: undefined,
11 | required: false
12 | }
13 | },
14 | data() {
15 | // eslint-disable-next-line
16 | return {_chart: null, div_: null, width_:0, height_:0};
17 | },
18 | watch: {
19 | options(options, prevOptions) {
20 | if (!this._chart) {
21 | this._destroy();
22 | this._create();
23 | } else if (this.width_ != options.width || this.height_ != options.height) {
24 | this._chart.setSize({width: options.width, height: options.height});
25 | }
26 | this.width_ = options.width;
27 | this.height_ = options.height;
28 | },
29 | target() {
30 | this._destroy();
31 | this._create();
32 | },
33 | data(data, prevData) {
34 | if (!this._chart) {
35 | this._create();
36 | } else if ((0 in data)) {
37 | this._chart.setData(data);
38 | }
39 | this._resize();
40 | }
41 | },
42 | mounted() {
43 | this._create();
44 | this._resize();
45 | },
46 | beforeUnmount() {
47 | this._destroy();
48 | },
49 | beforeDestroy() {
50 | this._destroy();
51 | },
52 | methods: {
53 | _destroy() {
54 | if (this._chart) {
55 | this.$emit('delete', this._chart);
56 | this._chart.destroy();
57 | this._chart = null;
58 | }
59 | },
60 | _create() {
61 |
62 | this.div_ = this.$props.target || this.$refs.targetRef;
63 | this._chart = new uPlot(this.$props.options, this.$props.data, this.div_);
64 | if(this.$props.options.cursor && "sync" in this.$props.options.cursor) window.cursorSync.sub(this._chart);
65 | this.width_ = this.$props.options.width;
66 | this.height_ = this.$props.options.height;
67 | this.$emit('create', this._chart);
68 | window.addEventListener("resize", e => { this._resize(); });
69 |
70 | },
71 | _resize() {
72 | if(!this._chart) return;
73 | let parentWidth = this.div_.offsetWidth;
74 | if(parentWidth != this.width_ || this.$props.options.height != this.height_){
75 | this.width_ = parentWidth;
76 | this.height_ = this.$props.options.height;
77 | this._chart.setSize({width: this.width_, height: this.height_});
78 | }
79 | }
80 | },
81 | render(h) {
82 | return this.$props.target ? null : (Vue.createVNode ? Vue.createVNode : h)('div', {
83 | ref: 'targetRef'
84 | });
85 | }
86 | });
87 |
88 |
--------------------------------------------------------------------------------
/server/www/components/vueResponsiveText/vue-responsive-text.css:
--------------------------------------------------------------------------------
1 | .responsive-text-wrapper-value, .responsive-text-wrapper-telem, .responsive-text-wrapper-unit {
2 | margin: auto;
3 | }
4 |
5 | .responsive-text-wrapper-telem, .responsive-text-wrapper-unit {
6 | color: #505252;
7 | }
8 |
9 | .responsive-text-wrapper-value{
10 | font-weight: 100;
11 | font-family: sans-serif;
12 | }
--------------------------------------------------------------------------------
/server/www/components/vueResponsiveText/vue-responsive-text.js:
--------------------------------------------------------------------------------
1 | function getNodeSize(node)// node : the html element we want to measure
2 | {
3 | const nodeStyles = window.getComputedStyle(node, null);
4 |
5 | let convertToFloat = (nb_str) => { return nb_str?parseFloat(nb_str):0 };
6 |
7 | return {
8 | width : (node.offsetWidth - convertToFloat(nodeStyles.borderLeftWidth) - convertToFloat(nodeStyles.borderRightWidth)
9 | - convertToFloat(nodeStyles.paddingLeft) - convertToFloat(nodeStyles.paddingRight)),
10 |
11 | height : (node.offsetHeight - convertToFloat(nodeStyles.borderTopHeight) - convertToFloat(nodeStyles.borderBottomHeight)
12 | - convertToFloat(nodeStyles.paddingTop) - convertToFloat(nodeStyles.paddingBottom))
13 | }
14 |
15 | }
16 |
17 | Vue.component('vue-responsive-text', {
18 | name: 'vue-responsive-text',
19 | props: {
20 | isTelem: {type: Boolean, required: false},
21 | isUnit: {type: Boolean, required: false},
22 | isValue: {type: Boolean, required: false},
23 | },
24 | data() {
25 | return {
26 | scale: 1,
27 | currentWidth: null,
28 | maxWidth: null,
29 | currentHeight: null,
30 | maxHeight: null,
31 | };
32 | },
33 | computed: {
34 | scaleStyle() {
35 | const scaleValue = `scale(${this.scale}, ${this.scale})`;
36 | return {
37 | msTransform: scaleValue,
38 | WebkitTransform: scaleValue,
39 | OTransform: scaleValue,
40 | MozTransform: scaleValue,
41 | transform: scaleValue
42 | };
43 | },
44 | },
45 | methods: {
46 | updateScale(currentWidth, maxWidth, currentHeight, maxHeight) {
47 | this.scale = Math.min(maxWidth / currentWidth, maxHeight/ currentHeight);
48 | },
49 | updateNodeWidth() {
50 | let parentNodeSize = getNodeSize(this.$el.parentElement);
51 | let thisNodeSize = getNodeSize(this.$el);
52 |
53 | this.currentWidth = thisNodeSize.width;
54 | this.maxWidth = parentNodeSize.width;
55 | this.currentHeight = thisNodeSize.height;
56 | this.maxHeight = parentNodeSize.height;
57 | },
58 | triggerTextResize()
59 | {
60 | this.updateNodeWidth();
61 | this.updateScale(this.currentWidth, this.maxWidth, this.currentHeight, this.maxHeight);
62 | }
63 | },
64 |
65 | template:' \
68 | \
69 | '
70 | });
71 |
72 |
--------------------------------------------------------------------------------
/server/www/constants.js:
--------------------------------------------------------------------------------
1 | // the number of frames per second for the widgets ( = the number of times the function updateView() will be called per second)
2 | const widgetFPS = 15;
3 |
4 | // the UDP port on which the server will try to connect
5 | const UDPport = 47269;
6 |
7 | const RedXAxis = "#e74c3c"
8 | const GreenYAxis = "#3ba30b"
9 | // const BlueZAxis = "#2980b9"
10 |
11 | const GridHeplerColor = "#cccccc"
--------------------------------------------------------------------------------
/server/www/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nesnes/teleplot/40caddd5aac92674f007b959a4d8a4771df83d96/server/www/favicon.ico
--------------------------------------------------------------------------------
/server/www/lib/three/InfiniteGrid.js:
--------------------------------------------------------------------------------
1 | // Author: Fyrestar https://mevedia.com (https://github.com/Fyrestar/THREE.InfiniteGridHelper)
2 |
3 | THREE.InfiniteGridHelper = function InfiniteGridHelper( size1, size2, color, distance, axes = 'xzy' ) {
4 |
5 | color = color || new THREE.Color( 'white' );
6 | size1 = size1 || 10;
7 | size2 = size2 || 100;
8 |
9 | distance = distance || 8000;
10 |
11 |
12 |
13 | const planeAxes = axes.substr( 0, 2 );
14 |
15 | const geometry = new THREE.PlaneBufferGeometry( 2, 2, 1, 1 );
16 |
17 | const material = new THREE.ShaderMaterial( {
18 |
19 | side: THREE.DoubleSide,
20 |
21 | uniforms: {
22 | uSize1: {
23 | value: size1
24 | },
25 | uSize2: {
26 | value: size2
27 | },
28 | uColor: {
29 | value: color
30 | },
31 | uDistance: {
32 | value: distance
33 | }
34 | },
35 | transparent: true,
36 | vertexShader: `
37 |
38 | varying vec3 worldPosition;
39 |
40 | uniform float uDistance;
41 |
42 | void main() {
43 |
44 | vec3 pos = position.${axes} * uDistance;
45 | pos.${planeAxes} += cameraPosition.${planeAxes};
46 |
47 | worldPosition = pos;
48 |
49 | gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
50 |
51 | }
52 | `,
53 |
54 |
55 | fragmentShader: `
56 |
57 | varying vec3 worldPosition;
58 |
59 | uniform float uSize1;
60 | uniform float uSize2;
61 | uniform vec3 uColor;
62 | uniform float uDistance;
63 |
64 |
65 |
66 | float getGrid(float size) {
67 |
68 | vec2 r = worldPosition.${planeAxes} / size;
69 |
70 |
71 | vec2 grid = abs(fract(r - 0.5) - 0.5) / fwidth(r);
72 | float line = min(grid.x, grid.y);
73 |
74 |
75 | return 1.0 - min(line, 1.0);
76 | }
77 |
78 | void main() {
79 |
80 |
81 | float d = 1.0 - min(distance(cameraPosition.${planeAxes}, worldPosition.${planeAxes}) / uDistance, 1.0);
82 |
83 | float g1 = getGrid(uSize1);
84 | float g2 = getGrid(uSize2);
85 |
86 |
87 | gl_FragColor = vec4(uColor.rgb, mix(g2, g1, g1) * pow(d, 3.0));
88 | gl_FragColor.a = mix(0.5 * gl_FragColor.a, gl_FragColor.a, g2);
89 |
90 | if ( gl_FragColor.a <= 0.0 ) discard;
91 |
92 |
93 | }
94 |
95 | `,
96 |
97 | extensions: {
98 | derivatives: true
99 | }
100 |
101 | } );
102 |
103 |
104 | THREE.Mesh.call( this, geometry, material );
105 |
106 | this.frustumCulled = false;
107 |
108 | };
109 |
110 | THREE.InfiniteGridHelper.prototype = {
111 | ...THREE.Mesh.prototype,
112 | ...THREE.Object3D.prototype,
113 | ...THREE.EventDispatcher.prototype
114 | };
115 |
116 | if ( parseInt( THREE.REVISION ) > 126 ) {
117 |
118 | class InfiniteGridHelper extends THREE.Mesh {
119 |
120 | constructor ( size1, size2, color, distance, axes = 'xzy' ) {
121 |
122 |
123 | color = color || new THREE.Color( 'white' );
124 | size1 = size1 || 10;
125 | size2 = size2 || 100;
126 |
127 | distance = distance || 8000;
128 |
129 |
130 |
131 | const planeAxes = axes.substr( 0, 2 );
132 |
133 | const geometry = new THREE.PlaneBufferGeometry( 2, 2, 1, 1 );
134 |
135 | const material = new THREE.ShaderMaterial( {
136 |
137 | side: THREE.DoubleSide,
138 |
139 | uniforms: {
140 | uSize1: {
141 | value: size1
142 | },
143 | uSize2: {
144 | value: size2
145 | },
146 | uColor: {
147 | value: color
148 | },
149 | uDistance: {
150 | value: distance
151 | }
152 | },
153 | transparent: true,
154 | vertexShader: `
155 |
156 | varying vec3 worldPosition;
157 |
158 | uniform float uDistance;
159 |
160 | void main() {
161 |
162 | vec3 pos = position.${axes} * uDistance;
163 | pos.${planeAxes} += cameraPosition.${planeAxes};
164 |
165 | worldPosition = pos;
166 |
167 | gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
168 |
169 | }
170 | `,
171 |
172 |
173 | fragmentShader: `
174 |
175 | varying vec3 worldPosition;
176 |
177 | uniform float uSize1;
178 | uniform float uSize2;
179 | uniform vec3 uColor;
180 | uniform float uDistance;
181 |
182 |
183 |
184 | float getGrid(float size) {
185 |
186 | vec2 r = worldPosition.${planeAxes} / size;
187 |
188 |
189 | vec2 grid = abs(fract(r - 0.5) - 0.5) / fwidth(r);
190 | float line = min(grid.x, grid.y);
191 |
192 |
193 | return 1.0 - min(line, 1.0);
194 | }
195 |
196 | void main() {
197 |
198 |
199 | float d = 1.0 - min(distance(cameraPosition.${planeAxes}, worldPosition.${planeAxes}) / uDistance, 1.0);
200 |
201 | float g1 = getGrid(uSize1);
202 | float g2 = getGrid(uSize2);
203 |
204 |
205 | gl_FragColor = vec4(uColor.rgb, mix(g2, g1, g1) * pow(d, 3.0));
206 | gl_FragColor.a = mix(0.5 * gl_FragColor.a, gl_FragColor.a, g2);
207 |
208 | if ( gl_FragColor.a <= 0.0 ) discard;
209 |
210 |
211 | }
212 |
213 | `,
214 |
215 | extensions: {
216 | derivatives: true
217 | }
218 |
219 | } );
220 |
221 | super( geometry, material );
222 |
223 | this.frustumCulled = false;
224 |
225 | }
226 |
227 | }
228 |
229 | Object.assign( InfiniteGridHelper.prototype, THREE.InfiniteGridHelper.prototype );
230 |
231 | THREE.InfiniteGridHelper = InfiniteGridHelper;
232 |
233 | }
--------------------------------------------------------------------------------
/server/www/lib/uPlot/uPlot.min.css:
--------------------------------------------------------------------------------
1 | .uplot, .uplot *, .uplot *::before, .uplot *::after {box-sizing: border-box;}.uplot {font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";line-height: 1.5;width: min-content;}.u-title {text-align: center;font-size: 18px;font-weight: bold;}.u-wrap {position: relative;user-select: none;}.u-over, .u-under {position: absolute;}.u-under {overflow: hidden;}.uplot canvas {display: block;position: relative;width: 100%;height: 100%;}.u-axis {position: absolute;}.u-legend {font-size: 14px;margin: auto;text-align: center;}.u-inline {display: block;}.u-inline * {display: inline-block;}.u-inline tr {margin-right: 16px;}.u-legend th {font-weight: 600;}.u-legend th > * {vertical-align: middle;display: inline-block;}.u-legend .u-marker {width: 1em;height: 1em;margin-right: 4px;background-clip: padding-box !important;}.u-inline.u-live th::after {content: ":";vertical-align: middle;}.u-inline:not(.u-live) .u-value {display: none;}.u-series > * {padding: 4px;}.u-series th {cursor: pointer;}.u-legend .u-off > * {opacity: 0.3;}.u-select {background: rgba(0,0,0,0.07);position: absolute;pointer-events: none;}.u-cursor-x, .u-cursor-y {position: absolute;left: 0;top: 0;pointer-events: none;will-change: transform;z-index: 100;}.u-hz .u-cursor-x, .u-vt .u-cursor-y {height: 100%;border-right: 1px dashed #607D8B;}.u-hz .u-cursor-y, .u-vt .u-cursor-x {width: 100%;border-bottom: 1px dashed #607D8B;}.u-cursor-pt {position: absolute;top: 0;left: 0;border-radius: 50%;border: 0 solid;pointer-events: none;will-change: transform;z-index: 100;/*this has to be !important since we set inline "background" shorthand */background-clip: padding-box !important;}.u-axis.u-off, .u-select.u-off, .u-cursor-x.u-off, .u-cursor-y.u-off, .u-cursor-pt.u-off {display: none;}
--------------------------------------------------------------------------------
/server/www/main.js:
--------------------------------------------------------------------------------
1 | // Init Vue
2 |
3 | var vscode = null;
4 | if("acquireVsCodeApi" in window) vscode = acquireVsCodeApi();
5 |
6 | var app = initializeAppView();
7 |
8 | //Init refresh rate
9 | setInterval(updateView, 1000 / widgetFPS);
10 |
11 |
12 | if(vscode){
13 | let conn = new ConnectionTeleplotVSCode();
14 | conn.connect();
15 | app.connections.push(conn);
16 | }
17 | else {
18 | let conn = new ConnectionTeleplotWebsocket();
19 | let addr = window.location.hostname;
20 | let port = window.location.port;
21 | conn.connect(addr, port);
22 | app.connections.push(conn);
23 |
24 | // Parse url params
25 | let params = new URLSearchParams(window.location.search);
26 |
27 | // Open layout from url
28 | let layout = params.get("layout")
29 | if (layout) {
30 | fetch(layout).then(res => res.blob()).then(blob => {
31 | importLayoutJSON({target:{files:[blob]}});
32 | });
33 | }
34 | }
35 |
36 |
37 | setInterval(()=>{
38 | for(let conn of app.connections){
39 | conn.updateCMDList();
40 | }
41 | }, 3000);
42 |
43 |
--------------------------------------------------------------------------------
/server/www/style-dark.css:
--------------------------------------------------------------------------------
1 | .dark-style.main-body {
2 | background-color: #262627;
3 | color: white;
4 | }
5 |
6 | .dark-style .var-vue-name {
7 | background-color: #3e3e42;
8 | color: white;
9 | }
10 |
11 | .dark-style .serie-name {
12 | color: white;
13 | }
14 |
15 | .dark-style .serie-value {
16 | color: white;
17 | }
18 |
19 | .dark-style .style-button {
20 | color: white;
21 | }
22 |
23 | .dark-style .widget-vue {
24 | background-color: #2d2d30;
25 | }
26 |
27 | .dark-style .serie-stat-value {
28 | font-size: 11px;
29 | color: #b1b1b1;
30 | }
--------------------------------------------------------------------------------
/server/www/style.css:
--------------------------------------------------------------------------------
1 | html {
2 | font-family: Verdana, Geneva, Tahoma, sans-serif;
3 | font-size: 12px;
4 | }
5 |
6 | button {
7 | background-color: #ecf0f1;
8 | border: 1px solid #bdc3c7;
9 | border-radius: 3px;
10 | color: #2c3e50;
11 | padding: 0.5em 0.5em;
12 | text-align: center;
13 | text-decoration: none;
14 | display: inline-block;
15 | cursor: pointer;
16 | font-size: 12px;
17 | }
18 |
19 | .top-banner {
20 | display: flex;
21 | flex-direction: row;
22 | flex-wrap: wrap;
23 | background-color: #2980b9;
24 | padding-top: 0px;
25 | padding-bottom: 0px;
26 | padding-left: 10px;
27 | padding-right: 10px;
28 | color: white;
29 | justify-content: space-between;
30 | align-items: center;
31 | min-height: 50px;
32 | }
33 |
34 | .top-banner-left {
35 | display: flex;
36 | flex-direction: row;
37 | align-items: center;
38 | height: 100%;
39 | justify-content: flex-start;
40 | width: 100%;
41 | flex: 1;
42 | }
43 |
44 | .top-banner-title {
45 | display: block;
46 | font-size: 30px;
47 | }
48 |
49 | #session-json-input {
50 | position: absolute;
51 | width: 0px;
52 | left: 0px;
53 | opacity: 0;
54 | }
55 |
56 | .header-button {
57 | background-color: #216490;
58 | padding: 5px 10px 5px 10px;
59 | color: white;
60 | cursor: pointer;
61 | border-radius: 50px;
62 | border: 0px;
63 | font-size: 11px;
64 | margin: 1px;
65 | line-height: 15px;
66 | }
67 |
68 | .play-pause-button {
69 | width: 34px;
70 | height: 34px;
71 | font-size: 16px;
72 | margin-right: 10px;
73 | }
74 |
75 | .play-pause-button-paused {
76 | background-color: #2ecc71;
77 | }
78 |
79 | .style-button {
80 | color: #2d2d30;
81 | text-decoration: underline;
82 | cursor: pointer;
83 | margin-left: 0.25em;
84 | }
85 |
86 | body {
87 | margin: 0px;
88 | padding: 0px;
89 | }
90 |
91 | .main-body {
92 | background-color: #ecf0f1;
93 | display: flex;
94 | flex-direction: row;
95 | flex-wrap: wrap;
96 | }
97 | .left-col {
98 | min-width: min(250px, max(100px, 20%));
99 | }
100 | .center-col {
101 | min-width: min(400px, 50%);
102 | /*Thanks to this padding, the zone to drop new widget goes even under already existant widgets*/
103 | padding-bottom: 200px;
104 | flex: 3;
105 | }
106 | .right-col {
107 | min-width: min(350px, max(250px, 30%));
108 | flex: 1;
109 | margin-right: 10px;
110 | }
111 |
112 | .vrow {
113 | height: 20px;
114 | padding-left: 10px;
115 | padding-right: 10px;
116 | color: #ecf0f1;
117 | cursor: pointer;
118 | white-space: nowrap;
119 | z-index: 100;
120 | margin-left: 4px;
121 | }
122 |
123 | .log-vue-selected {
124 | background-color: #27ae60;
125 | }
126 |
127 | .log-container {
128 | /* direction:rtl; we want the scroll bar to be on the left */
129 | background-color: #2c3e50;
130 | }
131 |
132 | .right-reduce-btn {
133 | width: 60px;
134 | margin-top: 0;
135 | margin-right: -10px;
136 | padding: 2px;
137 | background-color: #2980b9;
138 | font-weight: bold;
139 | color: white;
140 | border: 0px;
141 | border-radius: 0 0 0 5px;
142 | align-self: flex-end;
143 | }
144 |
145 | .help-container {
146 | display: flex;
147 | flex-direction: column;
148 | align-items: center;
149 | font-size: 16px;
150 | color: #7f8c8d;
151 | }
152 |
153 | .help-container img {
154 | display: block;
155 | width: 300px;
156 | margin-top: 100px;
157 | }
158 | .help-container span {
159 | display: block;
160 | margin-top: 100px;
161 | }
162 |
163 | .telem-container {
164 | display: flex;
165 | flex-direction: row;
166 | flex-wrap: wrap;
167 | justify-content: center;
168 | align-content: stretch;
169 | align-items: center;
170 | padding: 5px;
171 | }
172 |
173 | .telem-vue {
174 | flex: 1 1;
175 | align-self: auto;
176 | min-width: max(300px, 45%);
177 | box-shadow: 0px 0px 8px rgba(0,0,0,0.2);
178 | border-radius: 3px;
179 | margin: 5px;
180 | }
181 |
182 | .telem-vue-header {
183 | display: flex;
184 | flex-direction: row;
185 | justify-content: space-around;
186 | flex-wrap: wrap;
187 | }
188 |
189 | .telem-vue-title {
190 | display: block;
191 | font-size: 22px;
192 | text-align: center;
193 | }
194 |
195 | .var-container {
196 | display: flex;
197 | flex-direction: column;
198 | }
199 |
200 | .var-vue {
201 | margin: 2px 0px 2px 5px;
202 | font-size: 13px;
203 | display: flex;
204 | flex-direction: row;
205 | justify-content: space-between;
206 | cursor: grab;
207 | overflow: hidden;
208 | }
209 |
210 | .var-vue-name {
211 | color: black;
212 | max-width: 200px;
213 | word-break: break-all;
214 | border: 1px solid #2ecc71;
215 | flex: 1;
216 | padding: 0px 0px 1px 3px;
217 | border-radius: 5px 0px 0px 5px;
218 | text-align: center;
219 | position: relative;
220 | }
221 |
222 | .var-vue-value {
223 | background-color: #2ecc71;
224 | padding: 1px 0px 1px 0px;
225 | color: white;
226 | width: 60px;
227 | text-align: center;
228 | display: flex;
229 | justify-content: center;
230 | align-items: center;
231 | border-radius: 0px 5px 5px 0px;
232 | }
233 |
234 | .symbol-text-format-telem{
235 | position: absolute;
236 | left: 3px;
237 | color: #2ecc71;
238 |
239 | }
240 |
241 | .symbol-3d-telem{
242 | position: absolute;
243 | left: 3px;
244 | color: #2ecc71;
245 | }
246 |
247 | .cmd-container {
248 | display: flex;
249 | flex-direction: column;
250 | }
251 |
252 | .cmd-container-title {
253 | font-size: 18px;
254 | margin-top: 10px;
255 | }
256 |
257 | .cmd-vue {
258 | padding-top: 10px;
259 | }
260 |
261 | /* the rows of the logs pannel */
262 |
263 | .left-rounded-btn {
264 | border-radius: 50%;
265 | padding-right: 0px;
266 | display: block;
267 | margin-left: -20px;
268 | width: 60px;
269 | height: 60px;
270 | background-color: #2980b9;
271 | font-weight: bold;
272 | color: white;
273 | border: 0px;
274 | margin-top: 5px;
275 | }
276 |
277 | .right-rounded-btn {
278 | border-radius: 50%;
279 | padding-left: 0px;
280 | display: block;
281 | margin-right: -20px;
282 | width: 60px;
283 | height: 60px;
284 | background-color: #2980b9;
285 | font-weight: bold;
286 | color: white;
287 | border: 0px;
288 | margin-top: 5px;
289 | }
290 |
291 | .left-reduce-btn {
292 | width: 60px;
293 | margin-top: 0;
294 | padding: 2px;
295 | background-color: #2980b9;
296 | font-weight: bold;
297 | color: white;
298 | border: 0px;
299 | border-radius: 0 0 5px 0;
300 | }
301 |
302 |
303 | .send-text-container {
304 | display: flex;
305 | flex-direction: row;
306 | }
307 |
308 |
309 | .widget-container {
310 | padding: 5px;
311 | display:grid;
312 | grid-template-columns: repeat(12,calc(100% / 12));
313 | grid-auto-rows: 50px;
314 | }
315 |
316 | .widget-vue {
317 | flex: 1 1;
318 | align-self: auto;
319 | min-width: max(150px, 45%);
320 | box-shadow: 0px 0px 8px rgba(0,0,0,0.15);
321 | border-radius: 3px;
322 | margin: 3px;
323 | border: 1px solid transparent;
324 | position: relative;
325 | }
326 |
327 | .widget-drag-over {
328 | border: 1px solid #009aff;
329 | }
330 |
331 | .widget-vue-header {
332 | font-size: 12px;
333 | padding: 5px;
334 | display: flex;
335 | flex-direction: row;
336 | justify-content: space-around;
337 | }
338 |
339 | .serie-name {
340 | color: black;
341 | }
342 |
343 | .serie-header-container {
344 | height: 40px;
345 | width: 100%;
346 | z-index: 110;
347 | overflow-y: auto;
348 | white-space: nowrap;
349 | text-align: center;
350 | }
351 | .serie-header {
352 | display: inline-block;
353 | margin-left: 7px;
354 | margin-right: 7px;
355 | }
356 |
357 |
358 | .serie-color {
359 | display: inline-block;
360 | border-width: 2px;
361 | border-style: solid;
362 | margin-right: 3px;
363 | }
364 |
365 | .serie-name, .serie-unit {
366 | margin-right: 3px;
367 | margin-left: 3px;
368 | }
369 |
370 | .serie-value {
371 | margin-left: 3px;
372 | border-radius: 3px;
373 | color: black;
374 | padding: 0px 4px;
375 | }
376 | .serie-header-small {
377 | font-size: 8px;
378 | }
379 |
380 | .serie-header-small .serie-value {
381 | margin-left: 0px;
382 | padding: 0px 1px;
383 | }
384 |
385 | .serie-remove {
386 | color: #bdc3c7;
387 | padding: 0px;
388 | margin: -5px;
389 | font-size: 1.2em;
390 | cursor: pointer;
391 | }
392 |
393 | .widget-option-container {
394 | display: flex;
395 | flex-direction: row;
396 | }
397 |
398 | .widget-option{
399 | color: #bdc3c7;
400 | margin-left: 4px;
401 | margin-right: 4px;
402 | cursor: pointer;
403 | z-index: 200;
404 |
405 | /* user can't highlight the element ( useful for when resizing ) */
406 | user-select: none;
407 | }
408 |
409 | .widget-option-resize{
410 | display: inline-block;
411 | z-index: 1;
412 | padding: 1.5em;
413 | position: absolute;
414 | right: 10px;
415 | bottom: 10px;
416 | color: #bdc3c7;
417 | cursor: nwse-resize;
418 |
419 | /* negative margin, to make the clickable zone bigger */
420 | margin: -2em;
421 |
422 | /* user can't highlight the element ( useful for when resizing ) */
423 | user-select: none;
424 | }
425 |
426 | .new-chart-drop-zone, .new-chart-drop-zone-over {
427 | margin: 5px;
428 | padding: 5px;
429 | border: 2px dashed #3498db;
430 | border-radius: 5px;
431 | color: #3498db;
432 | text-align: center;
433 |
434 | border-bottom: 1px dashed #3498db;
435 | margin-bottom: 0px;
436 | }
437 |
438 | .last-value-drop-zone, .last-value-drop-zone-over {
439 | margin: 5px;
440 | padding: 5px;
441 | border: 2px dashed #3498db;
442 | border-radius: 5px;
443 | color: #3498db;
444 | text-align: center;
445 |
446 | border-top: 1px dashed #3498db;
447 | margin-top: 0px;
448 | }
449 |
450 | .new-chart-drop-zone-over, .last-value-drop-zone-over {
451 | background-color: rgb(255, 255, 0,0.1);
452 | }
453 |
454 | .serie-name-container {
455 | text-align: center;
456 | }
457 |
458 | .serie-stat-container, .serie-3d-details-container {
459 | display: flex;
460 | flex-direction: row;
461 | align-items: center;
462 | flex-wrap: wrap;
463 | }
464 |
465 | .details-3d-x {
466 | /* this text color should match RedXAxis in constants.js */
467 | color: #e74c3c;
468 | }
469 |
470 | .details-3d-y {
471 | color: #3ba30b;
472 | }
473 |
474 | .details-3d-z {
475 | /* this text color should match RedZAxis in constants.js */
476 | color: #2980b9;
477 | }
478 |
479 | .serie-stat, .serie-3d-details {
480 | display: inline-block;
481 | margin: 1px 1px 1px 2px;
482 | line-height: 0.7em;
483 | z-index: 100;
484 | }
485 |
486 | .serie-stat-value {
487 | font-size: 11px;
488 | color: #2c3e50;
489 | }
490 | .serie-stat-name {
491 | font-size: 9px;
492 | color: #7f8c8d;
493 | }
494 |
495 |
496 |
497 | .connection-container {
498 | display: flex;
499 | flex-direction: row;
500 | height: 100%;
501 | flex-wrap: wrap;
502 | }
503 |
504 | .connection {
505 | display: flex;
506 | flex-direction: column;
507 | margin: 2px 2px 2px 5px;
508 | }
509 |
510 | .connection-header {
511 | font-size: 11px;
512 | background-color: #216490;
513 | padding: 0px 5px;
514 | border-radius: 3px 3px 0px 0px;
515 | display: flex;
516 | flex-direction: row;
517 | justify-content: space-between;
518 | }
519 |
520 | .connection-name {
521 |
522 | }
523 |
524 | .connection-button-container {
525 | cursor: pointer;
526 | }
527 |
528 | .connection-button {
529 | margin-left: 10px;
530 | }
531 |
532 | .connection-input-container {
533 | display: flex;
534 | flex-direction: row;
535 | flex-wrap: wrap;
536 | background-color: #2164909e;
537 | border-radius: 0px 0px 5px 5px;
538 | overflow: hidden;
539 | }
540 |
541 | .connection-input {
542 | display: flex;
543 | flex-direction: column;
544 | font-size: 12px;
545 | padding-left: 5px;
546 | padding-right: 5px;
547 | border-width: 2px 2px 2px 2px;
548 | border-color: #216490;
549 | border-style: solid;
550 | }
551 |
552 | .connection-input-header {
553 | display: flex;
554 | flex-direction: row;
555 | justify-content: space-between;
556 | }
557 |
558 | .connection-input-name-container {
559 | display: flex;
560 | flex-direction: row;
561 | }
562 |
563 | .connection-input-name {
564 | font-size: 12px;
565 | }
566 |
567 | .connection-input-status-not-connected {
568 | background-color: #e74c3c;
569 | padding: 1px 5px 1px 5px;
570 | color: white;
571 | font-size: 11px;
572 | border-radius: 50px;
573 | }
574 |
575 | .connection-serial-input {
576 | display: flex;
577 | flex-direction: row;
578 | }
579 |
580 | .connection-serial-input-params {
581 | display: flex;
582 | flex-direction: row;
583 | }
584 |
585 | .connection-serial-input-params > select {
586 | font-size: 11px;
587 | }
588 |
589 | .connection-serial-buttons {
590 | display: flex;
591 | flex-direction: row;
592 | }
593 |
594 | .connection-serial-buttons > button {
595 | padding: 0px 5px;
596 | margin-left: 2px;
597 | }
598 |
599 | .connection-new-container {
600 | display: flex;
601 | flex-direction: row;
602 | font-size: 12px;
603 | margin-left: 10px;
604 | align-items: center;
605 | justify-content: center;
606 | }
607 |
608 | .connection-new-plus-button {
609 | background-color: #216490;
610 | color: white;
611 | border: 0;
612 | height: 30px;
613 | width: 30px;
614 | border-radius: 15px;
615 | }
616 |
617 |
618 | .pre-defined-connection-create-button {
619 | background-color: #216490;
620 | color: white;
621 | border: 0;
622 | height: 30px;
623 | border-radius: 15px;
624 | margin-right: 5px;
625 | }
626 |
627 | .flex-row {
628 | display: flex;
629 | flex-direction: row;
630 | }
631 |
632 | .flex-col {
633 | display: flex;
634 | flex-direction: column;
635 | }
636 |
637 | .uplot-vue {
638 | /* this is done to raise a bit the uplot vue */
639 | margin-top: -8px;
640 | }
641 |
--------------------------------------------------------------------------------
/server/www/utils/import_export/fileManagement.js:
--------------------------------------------------------------------------------
1 | function saveFile(content, filename) {
2 | if(vscode){
3 | vscode.postMessage({ cmd: "saveFile", file: {
4 | name: filename,
5 | content: content
6 | }});
7 | }
8 | else {
9 | var element = document.createElement('a');
10 | element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(content));
11 | element.setAttribute('download', filename);
12 | element.style.display = 'none';
13 | document.body.appendChild(element);
14 | element.click();
15 | document.body.removeChild(element);
16 | }
17 | }
18 |
19 | function buildFileName(fileContentType, fileExtension)
20 | {
21 | let fileName = "teleplot_";
22 |
23 | if (fileContentType === "layout")
24 | fileName += "layout_";
25 |
26 | let now = new Date();
27 |
28 | fileName += (now.getFullYear() + "-" + (now.getMonth()+1) + "-" + now.getDate() + "_" + now.getHours() + "-" + now.getMinutes());
29 | fileName += ( "." + fileExtension );
30 |
31 | return fileName;
32 | }
--------------------------------------------------------------------------------
/server/www/utils/import_export/layout.js:
--------------------------------------------------------------------------------
1 | function exportLayout() {
2 | let obj = {
3 | widgets: [],
4 | viewDuration: app.viewDuration
5 | };
6 | for(let w of widgets) {
7 | let widget = {
8 | type: w.type,
9 | gridPos: w.gridPos,
10 | series: [],
11 | precision_mode: w.precision_mode
12 | };
13 | for(let s of w.series) {
14 | let serie = {
15 | name: s.name,
16 | sourceNames: s.sourceNames,
17 | formula: s.formula,
18 | options: s.options,
19 | unit: s.unit,
20 | type : s.type,
21 | }
22 | widget.series.push(serie);
23 | }
24 | obj.widgets.push(widget);
25 | }
26 | let content = JSON.stringify(obj, null, 3);
27 | let filename = buildFileName("layout", "json");
28 | saveFile(content, filename);
29 | }
30 |
31 | function deleteCurrentWidgets()
32 | {
33 | for(let w of widgets) w.destroy();
34 |
35 | widgets.length = 0;
36 | Vue.set(app, 'widgets', widgets);
37 | }
38 |
39 | function importLayoutJSON(event) {
40 | var file = event.target.files[0];
41 | if (!file) {
42 | return;
43 | }
44 | var reader = new FileReader();
45 | reader.onload = function(e) {
46 | try{
47 | let content = JSON.parse(e.target.result);
48 | if("viewDuration" in content) app.viewDuration = content.viewDuration;
49 |
50 | deleteCurrentWidgets();
51 | for(let w of content.widgets){
52 |
53 | let newSeries = []
54 | let isWidgetXY = false;
55 | for(let s of w.series)
56 | {
57 | let serie = new DataSerie(s.name, s.unit, s.type);
58 | for(let sn of s.sourceNames){
59 | serie.addSource(sn);
60 | }
61 | if (serie.type == "xy") isWidgetXY = true;
62 |
63 | newSeries.push(serie);
64 | }
65 |
66 | let widget = undefined;
67 |
68 | if(w.type == "chart")
69 | {
70 | widget = new ChartWidget(isWidgetXY);
71 |
72 | for(let s of newSeries)
73 | widget.addSerie(s);
74 | }
75 |
76 | else if (w.type == "single_value_text")
77 | {
78 | widget = new SingleValueWidget(true);
79 | widget.addSerie(newSeries[0]);
80 | }
81 | else if (w.type == "single_value_number")
82 | {
83 | widget = new SingleValueWidget(false);
84 | widget.precision_mode = w.precision_mode
85 | widget.addSerie(newSeries[0]);
86 | }
87 | else if (w.type == "widget3D")
88 | {
89 | widget = new Widget3D();
90 | for (let s of newSeries)
91 | widget.addSerie(s);
92 | }
93 | else throw new Error("widget type "+w.type+" is not supported");
94 |
95 |
96 |
97 | widget.gridPos = w.gridPos;
98 | setTimeout(()=>{updateWidgetSize_(widget)}, 100);
99 | widgets.push(widget);
100 | }
101 | app.leftPanelVisible = false; // hide telemetry list
102 | }
103 | catch(e) {
104 | alert("Importation failed: "+e.toString());
105 | }
106 | };
107 | reader.readAsText(file);
108 | }
--------------------------------------------------------------------------------
/server/www/utils/import_export/session.js:
--------------------------------------------------------------------------------
1 | function exportSessionJSON() {
2 | let savedObj = JSON.stringify({
3 | telemetries: app.telemetries,
4 | logs: app.logs,
5 | dataAvailable: app.dataAvailable,
6 | logAvailable: app.logAvailable
7 | }, null, 3);
8 |
9 | saveFile(savedObj, buildFileName("session", "json"));
10 | }
11 |
12 | // dataSerie is of type DataSerie, we check that unit exists for this dataSerie
13 | // if it does, we return it between parentheses.
14 | function getFormatedSerieUnit(dataSerie)
15 | {
16 | if (dataSerie.unit != undefined)
17 | {
18 | return " ("+dataSerie.unit.replace(app.csvCellSeparator, "_").replace(app.csvDecimalSeparator, "_") + ")"
19 | }
20 |
21 | return "";
22 | }
23 |
24 | function exportSessionCSV() {
25 |
26 | let csv = "timestamp(ms)"+app.csvCellSeparator;
27 | let dataList = [];
28 | for(let key in app.telemetries) {
29 | let telemetry = app.telemetries[key];
30 | if (telemetry.type != "3D") // 3D not supported
31 | {
32 | csv += (key + getFormatedSerieUnit(telemetry) + app.csvCellSeparator);
33 | dataList.push(telemetry.data);
34 | }
35 | }
36 | csv += "\n";
37 | let joinedData = uPlot.join(dataList);
38 |
39 | for(let i=0;i0)
70 | {
71 | app.logAvailable = true;
72 | LogConsole.getInstance().logsUpdated(0, app.logs.length);
73 | }
74 |
75 | // we rebuilt the telemetries and the shapes from our file
76 | for (let [telemName, telem] of Object.entries(savedObj.telemetries))
77 | {
78 | if (app.telemetries[telemName] == undefined)
79 | {
80 | let newTelem = (new Telemetry(telemName)).iniFromTelem(telem);
81 | if (newTelem.type == "3D")
82 | {
83 | for (let i = 0; i < newTelem.data[0].length ; i++)
84 | {
85 | let newShape = (new Shape3D()).initializeFromShape3D(newTelem.data[1][i]);
86 | newTelem.data[1][i] = newShape;
87 | }
88 |
89 | newTelem.values[0] = newTelem.data[1][newTelem.data[1].length - 1];
90 |
91 | }
92 | Vue.set(app.telemetries, telemName, newTelem);
93 | }
94 | }
95 | }
96 | catch(e) {
97 | alert("Importation failed: "+e.toString());
98 | }
99 | };
100 | reader.readAsText(file);
101 | }
--------------------------------------------------------------------------------
/server/www/utils/javascriptUtils.js:
--------------------------------------------------------------------------------
1 | function my_copyArray(src_array)
2 | {
3 | if (src_array == undefined) return undefined;
4 |
5 | let destArray = [];
6 | for (let i = 0; i < src_array.length; i++)
7 | {
8 | let copyEl;
9 |
10 | if (typeof(src_array[i]) == Object)
11 | copyEl = JSON.parse(JSON.stringify(src_array[i]));
12 | else
13 | copyEl = src_array[i];
14 |
15 | destArray.push(copyEl);
16 |
17 | }
18 |
19 | return destArray;
20 | }
21 |
22 | function isLetter(c)
23 | {
24 | return ((c>='a' && c<='z') || (c>='A' && c<='Z'));
25 | }
26 |
--------------------------------------------------------------------------------
/server/www/utils/stats/computeStats.js:
--------------------------------------------------------------------------------
1 | function computeStats(data) {
2 | let decPrecision = 4;
3 |
4 | let mToFixed = (nb) => {
5 | if (typeof(nb) == 'number')
6 | return nb.toFixed(decPrecision);
7 |
8 | return nb.toString();
9 | }
10 |
11 | let stats = {
12 | min:"0",
13 | max:"0",
14 | sum:"0",
15 | mean:"0",
16 | med:"0",
17 | stdev:"0",
18 | };
19 | let values = data[1];
20 |
21 | //Find min/max indexes from timestampWindow
22 | let minIdx = 0, maxIdx = data[1].length;
23 |
24 | if(timestampWindow.min !=0 && timestampWindow.max != 0)
25 | {
26 | minIdx = findClosestLowerByIdx(data[0], timestampWindow.min) + 1;
27 | maxIdx = findClosestLowerByIdx(data[0], timestampWindow.max);
28 | if(maxIdx<=minIdx || maxIdx>=data[0].length) return stats;
29 |
30 |
31 | if (!app.isViewPaused)
32 | maxIdx = values.length;
33 |
34 | values = data[1].slice(minIdx, maxIdx);
35 | }
36 | if(values.length==0) return stats;
37 |
38 | // Sort
39 | let arr = values.sort(function compareFn(a, b) { return a-b});
40 | if(arr.length==0) return stats;
41 |
42 | // Min, Max
43 | stats.min = mToFixed(arr[0]);
44 | stats.max = mToFixed(arr[arr.length-1]);
45 |
46 | // Sum, Mean
47 | let sum = 0;
48 | for(let i=0;i{
56 | timestampWindow.min = self.scales.x._min;
57 | timestampWindow.max = self.scales.x._max;
58 | }, 10);
59 | return true;
60 | }});
61 |
62 |
63 | function findClosestLowerByIdx(values, mouseX) {
64 |
65 | let from = 0;
66 | let to = values.length - 1;
67 | let idx;
68 |
69 | while (from <= to) {
70 | idx = Math.floor((from + to) / 2);
71 |
72 | let isLowerLast = values[idx] <= mouseX && idx == values.length-1;
73 | let isClosestLower = (idx+1 < values.length-1) && (values[idx] <= mouseX ) && (values[idx+1] > mouseX );
74 | if (isClosestLower || isLowerLast) {
75 | return idx;
76 | }
77 | else {
78 | if (values[idx] > mouseX ) to = idx - 1;
79 | else from = idx + 1;
80 | }
81 | }
82 | return 0;
83 | }
84 |
85 |
86 | function findClosestTimestampToCursor(mlist, timeStampMouseX, islog=false) {
87 |
88 | function getTimestamp(i)
89 | {
90 | if (!islog) // if !islog, mlist is a list of timestamps, we do mlist[i] to get timestamp i
91 | {
92 | return mlist[i];
93 | }
94 | if (islog) // if islog, mlist contains a list of log, we do mlist[i]["timestamp"] to get timestamp i
95 | return mlist[i]["timestamp"];
96 |
97 | }
98 |
99 | let from = 0;
100 | let to = mlist.length - 1;
101 | let idx;
102 |
103 | function isCloserThan(anchorTimestamp, timestamp1, timestamp2)
104 | {
105 | let timeDiff = (time1, time2) => { return Math.abs(time1 - time2); }
106 |
107 | return timeDiff(anchorTimestamp, timestamp1) < timeDiff(anchorTimestamp, timestamp2);
108 | }
109 |
110 | while (from <= to) {
111 | idx = Math.floor((from + to) / 2);
112 |
113 | let isCursorOnTheLeft = timeStampMouseX < getTimestamp(idx);
114 |
115 | if (isCursorOnTheLeft)
116 | {
117 | let currentTimestamp = getTimestamp(idx);
118 | let timestampJustBefore = getTimestamp(idx>0?idx-1:0);
119 |
120 | if (isCloserThan(timeStampMouseX, currentTimestamp, timestampJustBefore))
121 | return idx; // we have found the closest timestamp
122 | }
123 | else // cursor is on the right
124 | {
125 | let currentTimestamp = getTimestamp(idx);
126 | let maxIdx = mlist.length-1;
127 | let timestampJustAfter = getTimestamp(idxw.id==widget.id);
134 | if(idx>=0) this.widgets.splice(idx, 1);
135 | triggerChartResize();
136 | },
137 | onDropInNewChart: function(e, prepend=true){
138 | e.preventDefault();
139 | e.stopPropagation();
140 | this.newChartDropZoneOver = false;
141 | let telemetryName = e.dataTransfer.getData("telemetryName");
142 |
143 | let chart = undefined;
144 |
145 | if (this.telemetries[telemetryName].type == "text")
146 | {
147 | chart = new SingleValueWidget(true);
148 | serie = getSerieInstanceFromTelemetry(telemetryName);
149 | chart.addSerie(serie);
150 |
151 | }
152 | else if (this.telemetries[telemetryName].type == "3D")
153 | {
154 | chart = new Widget3D();
155 | chart.addSerie(getSerieInstanceFromTelemetry(telemetryName));
156 | }
157 | else
158 | {
159 | chart = new ChartWidget(this.telemetries[telemetryName].type=="xy");
160 | chart.addSerie(getSerieInstanceFromTelemetry(telemetryName));
161 | }
162 |
163 | if(prepend) widgets.unshift(chart);
164 | else widgets.push(chart);
165 | },
166 |
167 | onDropInLastValue: function(e, prepend=true){
168 | e.preventDefault();
169 | e.stopPropagation();
170 | this.lastValueDropZoneOver = false;
171 |
172 | let telemetryName = e.dataTransfer.getData("telemetryName");
173 |
174 | if (this.telemetries[telemetryName].type == "3D")
175 | {
176 | this.onDropInNewChart(e, prepend);
177 | return;
178 | }
179 |
180 | let chart = new SingleValueWidget(this.telemetries[telemetryName].type == "text");
181 | let serie = getSerieInstanceFromTelemetry(telemetryName);
182 | chart.addSerie(serie);
183 | if(prepend) widgets.unshift(chart);
184 | else widgets.push(chart);
185 | },
186 | onNewChartDragOver: function(e){
187 | e.preventDefault();
188 | this.newChartDropZoneOver = true;
189 | },
190 | onLastValueDragOver: function(e){
191 | e.preventDefault();
192 | this.lastValueDropZoneOver = true;
193 | },
194 | onNewChartDragLeave: function(e){
195 | e.preventDefault();
196 | this.newChartDropZoneOver = false;
197 | },
198 | onLastValueDragLeave: function(e){
199 | e.preventDefault();
200 | this.lastValueDropZoneOver = false;
201 | },
202 | onMouseDownOnResizeButton: function(event, widget){
203 | onMouseDownOnResizeButton_(event, widget);
204 | },
205 | createConnection: function(address_=undefined, port_=undefined){
206 | let conn = new ConnectionTeleplotWebsocket();
207 | let addr = address_ || this.newConnectionAddress;
208 | let port = port_ || 8080;
209 | if(addr.includes(":")) {
210 | port = parseInt(addr.split(":")[1]);
211 | addr = addr.split(":")[0];
212 | }
213 | conn.connect(addr, port);
214 | this.connections.push(conn);
215 | this.creatingConnection = false;
216 | this.newConnectionAddress = "";
217 | },
218 | removeConnection: function(conn){
219 | for(let i=0;i str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
233 | let rule = this.telemetryFilterString.split("*").map(escapeRegex).join(".*");
234 | rule = "^" + rule + "$";
235 | let regex = new RegExp(rule, 'i');
236 | return regex.test(text);
237 | },
238 | updateWidgetSize: function(widget){
239 | updateWidgetSize_(widget);
240 | },
241 | isWidgetSmallOnGrid: function(widget){
242 | if(widget.gridPos.w < 3) return true;
243 | if(widget.gridPos.w < 5 && widget.series.length > 1) return true;
244 | return false;
245 | },
246 | shouldShowRightPanelButton: function(){
247 | if(this.rightPanelVisible) return false;
248 | if(this.cmdAvailable || this.logAvailable) return true;
249 | // Show with connected serial inputs
250 | for(let conn of this.connections){
251 | if(conn.connected){
252 | for(let input of conn.inputs){
253 | if(input.type == "serial" && input.connected){
254 | return true;
255 | }
256 | }
257 | }
258 | }
259 | return false;
260 | }
261 | }
262 | })
263 | }
--------------------------------------------------------------------------------
/server/www/utils/view/paint.js:
--------------------------------------------------------------------------------
1 | function rgba(r,g,b,a){
2 | return {r,g,b,a, toString: function(){ return `rgba(${this.r},${this.g},${this.b},${this.a})`}};
3 | }
4 |
5 | var ColorPalette = {
6 | colors: [
7 | rgba(231, 76, 60,1.0), //red
8 | rgba(52, 152, 219,1.0), //blue
9 | rgba(46, 204, 113,1.0), //green
10 | rgba(155, 89, 182,1.0), //violet
11 | rgba(241, 196, 15,1.0), //yellow
12 | rgba(26, 188, 156,1.0), //turquoise
13 | rgba(230, 126, 34,1.0), //orange
14 | rgba(52, 73, 94,1.0), //blueish grey
15 | rgba(127, 140, 141,1.0), //gray
16 | rgba(192, 57, 43,1.0), //dark red
17 | rgba(41, 128, 185,1.0), //darkblue
18 | rgba(39, 174, 96,1.0), //darkgreen
19 | rgba(142, 68, 173,1.0), // darkviolet
20 | rgba(211, 84, 0,1.0), //darkorange
21 | rgba(44, 62, 80,1.0), //blueish darkgrey
22 | rgba(0, 0, 0,1.0), //black
23 | ],
24 | getColor: function(index, alpha=1.0, strColor = undefined) {
25 | let color = undefined;
26 |
27 | if (index == undefined)
28 | color = Object.assign({}, rgba(44, 62, 80,1.0));
29 | else
30 | color = Object.assign({}, this.colors[index % this.colors.length]);
31 |
32 | color.a = alpha;
33 | return color;
34 | }
35 | }
36 |
37 |
38 | const drawXYPoints = (u, seriesIdx, idx0, idx1) => {
39 |
40 | const size = 5 * devicePixelRatio;
41 | uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
42 | let d = u.data[seriesIdx];
43 | u.ctx.fillStyle = series.stroke();
44 | let deg360 = 2 * Math.PI;
45 |
46 | let p = new Path2D();
47 | for (let i = Math.max(d[0].length - 2000, 0); i < d[0].length; i++) {
48 | let xVal = d[0][i];
49 | let yVal = d[1][i];
50 | if (xVal >= scaleX.min && xVal <= scaleX.max && yVal >= scaleY.min && yVal <= scaleY.max) {
51 |
52 | let cx = valToPosX(xVal, scaleX, xDim, xOff);
53 | let cy = valToPosY(yVal, scaleY, yDim, yOff);
54 |
55 | p.moveTo(cx + size/2, cy);
56 | arc(p, cx, cy, size/2, 0, deg360);
57 |
58 | }
59 | }
60 | u.ctx.fill(p);
61 | });
62 | return null;
63 | };
--------------------------------------------------------------------------------
/server/www/utils/view/updateView.js:
--------------------------------------------------------------------------------
1 |
2 | var lastUpdateViewTimestamp = 0;
3 | function updateView() {
4 | // Clear Telemetries pendingData
5 | for(let key in app.telemetries) {
6 | if (app.telemetries[key].pendingData != undefined)
7 | {
8 | app.telemetries[key].pendingData[0].length = 0;
9 | app.telemetries[key].pendingData[1].length = 0;
10 | if(app.telemetries[key].type=="xy") app.telemetries[key].pendingData[2].length = 0;
11 | }
12 | }
13 | // Flush Telemetry buffer into app model
14 | let dataSum = 0;
15 | if(!app.isViewPaused){
16 | for(let key in telemBuffer) {
17 |
18 | if(telemBuffer[key].data[0].length == 0) continue; // nothing to flush
19 | dataSum += telemBuffer[key].data[0].length;
20 | app.telemetries[key].data[0].push(...telemBuffer[key].data[0]);
21 | app.telemetries[key].data[1].push(...telemBuffer[key].data[1]);
22 | if(app.telemetries[key].type=="xy") app.telemetries[key].data[2].push(...telemBuffer[key].data[2]);
23 | if (app.telemetries[key].pendingData != undefined)
24 | {
25 | app.telemetries[key].pendingData[0].push(...telemBuffer[key].data[0]);
26 | app.telemetries[key].pendingData[1].push(...telemBuffer[key].data[1]);
27 | if(app.telemetries[key].type=="xy") app.telemetries[key].pendingData[2].push(...telemBuffer[key].data[2]);
28 | }
29 |
30 | telemBuffer[key].data[0].length = 0;
31 | telemBuffer[key].data[1].length = 0;
32 | if(app.telemetries[key].type=="xy") telemBuffer[key].data[2].length = 0;
33 |
34 | app.telemetries[key].values.length = 0;
35 |
36 | if (telemBuffer[key].values.length > 0)
37 | app.telemetries[key].values.push(telemBuffer[key].values[0]);
38 |
39 | if (telemBuffer[key].values.length > 1)
40 | app.telemetries[key].values.push(telemBuffer[key].values[1]);
41 |
42 | // this has to be done every time telem.values get modified, it is usefull for the html to display
43 | //the good text in the telemetry pannel
44 | app.telemetries[key].updateFormattedValues();
45 |
46 | }
47 | }
48 |
49 | //Clear older data from viewDuration
50 | if(parseFloat(app.viewDuration)>0)
51 | {
52 | for(let key in app.telemetries) {
53 | let data = app.telemetries[key].data;
54 | let timeIdx = 0;
55 | if(app.telemetries[key].type=="xy") timeIdx = 2;
56 | let latestTimestamp = data[timeIdx][data[timeIdx].length-1];
57 | let minTimestamp = latestTimestamp - parseFloat(app.viewDuration);
58 | let minIdx = findClosestLowerByIdx(data[timeIdx], minTimestamp);
59 | if(data[timeIdx][minIdx]0) app.dataAvailable = true;
73 |
74 | // Logs
75 | var logSum = logBuffer.length;
76 | if(!app.isViewPaused && logBuffer.length>0) {
77 | app.logs.push(...logBuffer);//append log to list
78 | logBuffer.length = 0;
79 |
80 | }
81 |
82 | if (app.logs.length>0)
83 | {
84 | app.logAvailable = true;
85 | LogConsole.getInstance().logsUpdated(0, app.logs.length);
86 | }
87 |
88 |
89 | // Stats
90 | let now = new Date().getTime();
91 | if(lastUpdateViewTimestamp==0) lastUpdateViewTimestamp = now;
92 | let diff = now - lastUpdateViewTimestamp
93 | if(diff>0){
94 | app.telemRate = app.telemRate*0.8 + (1000/diff*dataSum)*0.2;
95 | app.logRate = app.logRate *0.8 + (1000/diff*logSum)*0.2;
96 | }
97 | lastUpdateViewTimestamp = now;
98 | }
--------------------------------------------------------------------------------
/vscode/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /**@type {import('eslint').Linter.Config} */
2 | // eslint-disable-next-line no-undef
3 | module.exports = {
4 | root: true,
5 | parser: '@typescript-eslint/parser',
6 | plugins: [
7 | '@typescript-eslint',
8 | ],
9 | extends: [
10 | 'eslint:recommended',
11 | 'plugin:@typescript-eslint/recommended',
12 | ],
13 | ignorePatterns: [
14 | 'media'
15 | ],
16 | rules: {
17 | 'semi': [2, "always"],
18 | '@typescript-eslint/no-unused-vars': 0,
19 | '@typescript-eslint/no-explicit-any': 0,
20 | '@typescript-eslint/explicit-module-boundary-types': 0,
21 | '@typescript-eslint/no-non-null-assertion': 0,
22 | }
23 | };
--------------------------------------------------------------------------------
/vscode/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
4 |
5 | // List of extensions which should be recommended for users of this workspace.
6 | "recommendations": [
7 | "dbaeumer.vscode-eslint"
8 | ]
9 | }
--------------------------------------------------------------------------------
/vscode/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that compiles the extension and then opens it inside a new window
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | {
6 | "version": "0.2.0",
7 | "configurations": [
8 | {
9 | "name": "Run Extension",
10 | "type": "extensionHost",
11 | "request": "launch",
12 | "runtimeExecutable": "${execPath}",
13 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"],
14 | "outFiles": ["${workspaceFolder}/out/**/*.js"],
15 | "preLaunchTask": "pre_launch"
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/vscode/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.insertSpaces": false
3 | }
--------------------------------------------------------------------------------
/vscode/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | // See https://go.microsoft.com/fwlink/?LinkId=733558
2 | // for the documentation about the tasks.json format
3 | {
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "label": "npm_watch",
8 | "type": "npm",
9 | "script": "watch",
10 | "problemMatcher": "$tsc-watch",
11 | "isBackground": true,
12 | "presentation": {
13 | "reveal": "never"
14 | },
15 | "group": {
16 | "kind": "build",
17 | "isDefault": true
18 | }
19 | },
20 | {
21 | "label": "cp_media",
22 | "type": "shell",
23 | "options": {
24 | "cwd": "${workspaceFolder}"
25 | },
26 | "command": "rm -rf ./media/; mkdir media; cp -r ../server/www/* ./media/",
27 | "presentation": {
28 | "reveal": "always"
29 | },
30 | "group": {
31 | "kind": "build",
32 | "isDefault": true
33 | }
34 | },
35 | {
36 | "label": "pre_launch",
37 | "dependsOn": [
38 | "cp_media",
39 | "npm_watch"
40 | ]
41 | }
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/vscode/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Alexandre Brehmer
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/vscode/README.md:
--------------------------------------------------------------------------------
1 | # Teleplot for VSCode
2 |
3 | Plots telemetry sent over **Serial** or **UDP Packets**.
4 |
5 | 
6 |
7 | ## From an Arduino (Serial)
8 |
9 | ```c++
10 | #include
11 |
12 | void setup() {
13 | Serial.begin(115200);
14 | // Print log
15 | Serial.println("setup");
16 | }
17 |
18 | float i=0;
19 | void loop() {
20 | i+=0.1;
21 |
22 | // Print log
23 | Serial.print("loop");
24 | Serial.println(i);
25 |
26 | // Plot a sinus
27 | Serial.print(">sin:");
28 | Serial.println(sin(i));
29 |
30 | // Plot a cosinus
31 | Serial.print(">cos:");
32 | Serial.println(cos(i));
33 |
34 | delay(50);
35 | }
36 | ```
37 |
38 | Every **serial** message formated `>varName:1234\n` will be ploted in teleplot. Other messages will be printed in the teleplot console.
39 |
40 | Serial port needs to be selected and connected at the top-left on Teleplot.
41 |
42 | > This format is **specific** to **Serial** messages to enhance ease of use on microcontrollers.
43 |
44 |
45 | ## From any program (UDP)
46 |
47 | Teleplot listen to UDP packects on port `47269`, allowing any type of software to post telemetry messages.
48 |
49 | - `varName:1234|g` adds or update the `varName` variable value on Teleplot *plots*.
50 | - `varName:1627551892437:1234|g` does the same but specifies the value's timestamp in milliseconds for more accurate ploting.
51 | - `varName:1627551892444:1;1627551892555:2;1627551892666:3|g` does the same as above but publishes multiple values in a single packet.
52 |
53 | > For more details on the format and additional telemetry types (like text or 3D shapes), check the [Teleplot README](https://github.com/nesnes/teleplot)
54 |
55 | ### Bash
56 | ```bash
57 | echo "myValue:1234|g" | nc -u -w0 127.0.0.1 47269
58 | ```
59 |
60 | ### C++
61 | Grab `clients/cpp/Teleplot.h` from the [Teleplot repository](https://github.com/nesnes/teleplot).
62 |
63 | ```c++
64 | #include
65 | #include "Teleplot.h"
66 | Teleplot teleplot("127.0.0.1");
67 |
68 | int main(int argc, char* argv[])
69 | {
70 | for(float i=0;i<1000;i+=0.1)
71 | {
72 | // Use instanciated object
73 | teleplot.update("sin", sin(i));
74 | teleplot.update("cos", cos(i), 10); // Limit at 10Hz
75 |
76 | // Use static localhost object
77 | Teleplot::localhost().update("tan", tan(i));
78 |
79 | usleep(10000);
80 | }
81 | return 0;
82 | }
83 | ```
84 |
85 | ## Python
86 |
87 | ```python
88 | import socket
89 | import math
90 | import time
91 |
92 | teleplotAddr = ("127.0.0.1",47269)
93 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
94 |
95 | def sendTelemetry(name, value):
96 | now = time.time() * 1000
97 | msg = name+":"+str(now)+":"+str(value)+"|g"
98 | sock.sendto(msg.encode(), teleplotAddr)
99 |
100 | i=0
101 | while i < 1000:
102 |
103 | sendTelemetry("sin", math.sin(i))
104 | sendTelemetry("cos", math.cos(i))
105 |
106 | i+=0.1
107 | time.sleep(0.01)
108 | ```
109 |
110 | ## Not listed?
111 |
112 | You just need to send a UDP packet with the proper text in it. Open your web browser, search for `my_language send UDP packet`, and copy-paste the first sample you find before editing it with the following options:
113 |
114 | - address: `127.0.0.1`
115 | - port: `47269`
116 | - your test message: `myValue:1234|g`
117 |
118 | ## Supports
119 |
120 | Teleplot project received the generous technical support of [Wandercraft](https://www.wandercraft.eu/).
121 |
122 | 
--------------------------------------------------------------------------------
/vscode/images/logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nesnes/teleplot/40caddd5aac92674f007b959a4d8a4771df83d96/vscode/images/logo-color.png
--------------------------------------------------------------------------------
/vscode/images/preview-vscode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nesnes/teleplot/40caddd5aac92674f007b959a4d8a4771df83d96/vscode/images/preview-vscode.png
--------------------------------------------------------------------------------
/vscode/images/wandercraft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nesnes/teleplot/40caddd5aac92674f007b959a4d8a4771df83d96/vscode/images/wandercraft.png
--------------------------------------------------------------------------------
/vscode/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "teleplot",
3 | "description": "Teleplot - Ridiculously-simple telemetry viewer.",
4 | "version": "1.1.3",
5 | "publisher": "alexnesnes",
6 | "private": true,
7 | "license": "MIT",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/nesnes/teleplot"
11 | },
12 | "engines": {
13 | "vscode": "^1.47.0"
14 | },
15 | "categories": [
16 | "Other"
17 | ],
18 | "activationEvents": [
19 | "onStartupFinished",
20 | "onCommand:teleplot.start",
21 | "onWebviewPanel:teleplot"
22 | ],
23 | "main": "./out/extension.js",
24 | "icon": "images/logo-color.png",
25 | "contributes": {
26 | "commands": [
27 | {
28 | "command": "teleplot.start",
29 | "title": "Start teleplot session",
30 | "category": "teleplot",
31 | "icon": {
32 | "light": "images/logo-color.svg",
33 | "dark": "images/logo-color.svg"
34 | }
35 | }
36 | ]
37 | },
38 | "scripts": {
39 | "vscode:prepublish": "echo Dont forget to run extension first so source files are updated by pre_launch task && npm run compile",
40 | "compile": "tsc -p ./",
41 | "rebuild:native-modules": "node ./node_modules/electron-rebuild/lib/cli.js --version 1.4.13",
42 | "lint": "eslint . --ext .ts,.tsx",
43 | "watch": "tsc -w -p ./"
44 | },
45 | "dependencies": {
46 | "dgram": "^1.0.1",
47 | "node-usb-native": "^0.0.20",
48 | "serialport": "^10.4.0",
49 | "vsce": "^2.9.2"
50 | },
51 | "devDependencies": {
52 | "@types/node": "^12.12.0",
53 | "@types/vscode": "^1.47.0",
54 | "@types/vscode-webview": "^1.57.0",
55 | "@typescript-eslint/eslint-plugin": "^4.16.0",
56 | "@typescript-eslint/parser": "^4.16.0",
57 | "electron-prebuilt": "1.4.13",
58 | "electron-rebuild": "^3.2.3",
59 | "eslint": "^7.21.0",
60 | "typescript": "^4.4.3"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/vscode/src/extension.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import * as path from 'path';
3 | import * as fs from 'fs';
4 | import { ReadlineParser } from 'serialport';
5 | const { SerialPort } = require('serialport')//require('node-usb-native');
6 | const Readline = require('@serialport/parser-readline')
7 |
8 | const UDP_PORT = 47269;
9 | const CMD_UDP_PORT = 47268;
10 |
11 | const udp = require('dgram');
12 |
13 | var serials : any = {};
14 | var udpServer : any = null;
15 | var currentPanel:vscode.WebviewPanel;
16 | var _disposables: vscode.Disposable[] = [];
17 | var statusBarIcon:any;
18 |
19 | export function activate(context: vscode.ExtensionContext) {
20 | context.subscriptions.push(
21 | vscode.commands.registerCommand('teleplot.start', () => {
22 | startTeleplotServer();
23 |
24 | const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined;
25 | // If we already have a panel, show it.
26 | if (currentPanel) {
27 | currentPanel.reveal(column);
28 | return;
29 | }
30 |
31 | // Otherwise, create a new panel.
32 | const panel = vscode.window.createWebviewPanel('teleplot', 'Teleplot', column || vscode.ViewColumn.One,
33 | {
34 | enableScripts: true,
35 | localResourceRoots: [vscode.Uri.file(path.join(context.extensionPath, 'media'))],
36 | retainContextWhenHidden: true,
37 | enableCommandUris: true
38 | }
39 | );
40 | currentPanel = panel;
41 |
42 | fs.readFile(path.join(context.extensionPath, 'media', 'index.html') ,(err,data) => {
43 | if(err) {console.error(err)}
44 | let rawHTML = data.toString();
45 | // Replace all urls
46 | const srcList = rawHTML.match(/src\=\"(.*)\"/g);
47 | const hrefList = rawHTML.match(/href\=\"(.*)\"/g);
48 | if(srcList != null && hrefList != null) {
49 | for(let src of [...srcList, ...hrefList]) {
50 | // Extract url only
51 | let url = src.split("\"")[1];
52 | const extensionURI = vscode.Uri.joinPath(context.extensionUri, "./media/"+url)
53 | const webURI = panel.webview.asWebviewUri(extensionURI);
54 | const toReplace = src.replace(url, webURI.toString())
55 | console.log(url, extensionURI ,webURI )
56 | rawHTML = rawHTML.replace(src, toReplace)
57 | }
58 | }
59 | // Set default color style to dark
60 |
61 | const teleplotStyle = rawHTML.match(/(.*)_teleplot_default_color_style(.*)/g);
62 | if(teleplotStyle != null) {
63 | rawHTML = rawHTML.replace(teleplotStyle.toString(), 'var _teleplot_default_color_style = "dark";');
64 | }
65 |
66 | panel.webview.html = rawHTML;
67 | });
68 |
69 | panel.onDidDispose(() => {
70 | if(udpServer) {
71 | udpServer.close();
72 | udpServer = null;
73 | }
74 | while(_disposables.length) {
75 | const x = _disposables.pop();
76 | if(x) x.dispose();
77 | }
78 | _disposables.length = 0;
79 | for(let s in serials){
80 | serials[s].close();
81 | serials[s] = null;
82 | }
83 | (currentPanel as any) = null;
84 | }, null, _disposables);
85 |
86 | panel.webview.onDidReceiveMessage( message => {
87 | if("data" in message) {
88 | var udpClient = udp.createSocket('udp4');
89 | udpClient.send(message.data, 0, message.data.length, CMD_UDP_PORT, ()=> {
90 | udpClient.close();
91 | });
92 | }
93 | else if("cmd" in message) {
94 | runCmd(message);
95 | }
96 | }, null, _disposables);
97 | })
98 | );
99 |
100 | statusBarIcon = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
101 | statusBarIcon.command = 'teleplot.start';
102 | statusBarIcon.text = "$(graph-line) Teleplot"
103 | context.subscriptions.push(statusBarIcon);
104 | statusBarIcon.show();
105 | }
106 |
107 | function startTeleplotServer(){
108 | // Setup UDP server
109 | udpServer = udp.createSocket('udp4');
110 | udpServer.bind(UDP_PORT);
111 | // Relay UDP packets to webview
112 | udpServer.on('message',function(msg:any,info:any){
113 | currentPanel.webview.postMessage({data: msg.toString(), fromSerial:false, timestamp: new Date().getTime()});
114 | });
115 | }
116 |
117 | var dataBuffer = "";
118 | function runCmd(msg:any){
119 | let id = ("id" in msg)?msg.id:"";
120 | if(msg.cmd == "listSerialPorts"){
121 | SerialPort.list().then((ports:any) => {
122 | currentPanel.webview.postMessage({id, cmd: "serialPortList", list: ports});
123 | });
124 | }
125 | else if(msg.cmd == "connectSerialPort"){
126 | if(serials[id]) { //Already exists
127 | serials[id].close();
128 | delete serials[id];
129 | }
130 | serials[id] = new SerialPort({baudRate: msg.baud, path: msg.port}, function(err: any) {
131 | if(err) {
132 | console.log("erroror");
133 | currentPanel.webview.postMessage({id, cmd: "serialPortError", port: msg.port, baud: msg.baud});
134 | }
135 | else {
136 | console.log("open");
137 | currentPanel.webview.postMessage({id, cmd: "serialPortConnect", port: msg.port, baud: msg.baud});
138 | }
139 | })
140 |
141 | const parser = serials[id].pipe(new ReadlineParser({ delimiter: '\n' }));
142 | parser.on('data', function(data:any) {
143 | currentPanel.webview.postMessage({id, data: data.toString(), fromSerial:true, timestamp: new Date().getTime()});
144 | })
145 | serials[id].on('close', function(err:any) {
146 | currentPanel.webview.postMessage({id, cmd: "serialPortDisconnect"});
147 | })
148 | }
149 | else if(msg.cmd == "sendToSerial"){
150 | serials[id].write(msg.text);
151 | }
152 | else if(msg.cmd == "disconnectSerialPort"){
153 | serials[id].close();
154 | delete serials[id];
155 | }
156 | else if(msg.cmd == "saveFile"){
157 | try {
158 | exportDataWithConfirmation(path.join(msg.file.name), { JSON: ["json"] }, msg.file.content);
159 | } catch (error) {
160 | void vscode.window.showErrorMessage("Couldn't write file: " + error);
161 | }
162 | }
163 | }
164 |
165 | function exportDataWithConfirmation(fileName: string, filters: { [name: string]: string[] }, data: string): void {
166 | void vscode.window.showSaveDialog({
167 | defaultUri: vscode.Uri.file(fileName),
168 | filters,
169 | }).then((uri: vscode.Uri | undefined) => {
170 | if (uri) {
171 | const value = uri.fsPath;
172 | fs.writeFile(value, data, (error:any) => {
173 | if (error) {
174 | void vscode.window.showErrorMessage("Could not write to file: " + value + ": " + error.message);
175 | } else {
176 | void vscode.window.showInformationMessage("Saved " + value );
177 | }
178 | });
179 | }
180 | });
181 | }
--------------------------------------------------------------------------------
/vscode/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es2019",
5 | "lib": ["ES2019"],
6 | "outDir": "out",
7 | "sourceMap": true,
8 | "strict": true,
9 | "rootDir": "src"
10 | },
11 | "exclude": ["node_modules", ".vscode-test"]
12 | }
13 |
--------------------------------------------------------------------------------