├── .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 | Teleplot logo 2 | 3 | # Teleplot 4 | 5 | A ridiculously simple tool to plot telemetry data from a running program and trigger function calls. 6 | 7 | ![](images/preview.jpg) 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 | ![](images/wandercraft-logo.png) 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 | ![](images/preview-vscode.png) 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 | ![](images/wandercraft.png) -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------