Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 2 additions & 13 deletions Dev/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,8 @@ <h2>SysID</h2>
</td>
<td style="text-align:left; vertical-align:top; max-width:960px;">
<h2>TelemetryDashboard</h2>
Telemetry Dashboard allows customizable data displays from a MAVLink telemetry stream. Requires a WebSocket server to forward raw binary MAVLink. Attempts to auto connect to MissionPlanner at <code>ws://127.0.0.1:56781</code>. Latest PyMAVLink can also be used eg: <a href="https://github.com/IamPete1/pymavlink/blob/WebSocket_forwarding_example/examples/mavtcpsniff.py">TCP to WebSocket</a>.
This is read only, MAVLink commands are not sent (including stream rate requests).
This is not a GCS replacement.
Telemetry Dashboard is a drone management solution for swarms, multiple or single vehicles to display customisable data from MAVLink telemetry streams. It requires a WebSocket server to forward raw binary MAVLink, eg: <a href="https://github.com/izzy-barr/pymavlink/blob/master/examples/mavtcpsniff.py">TCP to WebSocket</a> modified for any IP address.
It includes Widget & Primary Vehicle Selectors, Vehicle Info Pop-up, colour and icons according to vehicle type. It is read only and not a GCS replacement.
</td>
</tr>

Expand Down Expand Up @@ -119,16 +118,6 @@ <h2>AI Log Analyzer</h2>
</td>
</tr>

<tr>
<td>
<a href="../SCurveTool" style="margin: 20px; display: inline-block;"><img src="../images/SCurveTool_Icon.png" style="width:200px"></a>
</td>
<td style="text-align:left; vertical-align:top; max-width:960px;">
<h2>SCurve Kinematic Tool</h2>
A tool to help understanding s-curve trajectories.
</td>
</tr>

</table>

</body>
Expand Down
15,815 changes: 13,386 additions & 2,429 deletions TelemetryDashboard/Default_Layout.json

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions TelemetryDashboard/Readme.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
## MAVLink Dashboard
## Telemetry Dashboard

This is a display only tool to help visualizing incoming MAVLink telemetry data.
This is a display only tool to help visualise incoming MAVLink telemetry data from multiple vehicles.

This is not a GCS! It should be used in addition to a GCS.

Focus is on flexibility and user customization.

### New features

It now accepts multiple vehicles which you can add with the + button in the Connections tippy. Here you can enter the websocket address, a vehicle name and connect/disconnect/remove as you wish (upon disconnect, any widget with that vehicle selected will remove its content and its name from the options). There is a Primary Vehicle selector which when chosen and 'selected' pressed, sends that vehicle's messages to all widgets in the dashboard. Note, it overrides and removes any vehicles already selected and is primarily designed to speed up the setup time particularly for only using one vehicle.

The code now identified the vehicle type from the MAVLink messages and assigns different icons in the map for the following vehicle types: plane, copter, helicopter, antenna tracker, gcs, blimp, balloon, rocket, rover, boat and sub. If the type has not been identified or is something not on the list, it defaults to a diamond shape.

When Edit is enabled (found in the Settings tippy), you can now select and deselect vehicles at each widget. The map and graphs accept multiple vehicles, all the rest only allow one vehicle's data. Each vehicle is assigned a colour at random for easy identification across the dashboard and can be changed by clicking on the vehicle in the map which brings up a vehicle info popup (also new feature) where you can see the vehicle's websocket, name, coordinates, colour and MAVLink Inspector which has been moved inside here (it is still a widget which can be added and assigned a vehicle).
2 changes: 1 addition & 1 deletion TelemetryDashboard/SandBoxWidgets/Attitude.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"components": []
},
"form_content": {},
"sandbox": "// Import Gauges from https://github.com/teocci/js-module-flight-indicators\n// Add ccs with link tag\nconst ccs = document.createElement('link')\nccs.rel = \"stylesheet\"\nccs.href = \"https://unpkg.com/flight-indicators-js@1.0.5/css/flight-indicators.css\"\ndocument.body.appendChild(ccs)\n\nlet attitude\nimport(\"https://unpkg.com/flight-indicators-js@1.0.5/esm/module-flight-indicators.mjs\").then((mod) => {\n const FlightIndicators = mod.default\n\n attitude = new FlightIndicators(\n div,\n FlightIndicators.TYPE_ATTITUDE\n )\n\n // This is a dirty hack to switch to remote copy's of images\n let images = div.querySelectorAll(\"img\")\n for (const image of images) {\n let src = image.src\n\n var lastIndex = src.lastIndexOf(\"/img/\")\n image.src = \"https://unpkg.com/flight-indicators-js@1.0.5\" + src.substr(lastIndex)\n\n // Hide box is broken, hide manually\n // see: https://github.com/teocci/js-module-flight-indicators/pull/1\n if (src.endsWith(\"fi_box.svg\")) {\n image.style.display = \"none\"\n }\n }\n\n resize()\n})\n\n// Remove margin and border to give more room\ndiv.style.margin = 0\ndiv.style.border = 0\ndiv.style.padding = 0\n\n// Center gauge\ndiv.style.display = \"flex\"\ndiv.style.justifyContent = \"center\"\ndiv.style.alignItems = \"center\"\n\nfunction resize() {\n\n if (attitude == null) {\n return\n }\n\n // Get width and height of widget\n const width = div.offsetWidth\n const height = div.offsetHeight\n\n const max_size = Math.min(width, height)\n attitude.resize(max_size)\n}\n\n// Watch for size changes\nnew ResizeObserver(() => { resize() }).observe(div)\n\nconst ATTITUDE_id = 30\n\n// Runtime function\nhandle_msg = function(msg) {\n\n if (msg._id != ATTITUDE_id) {\n return\n }\n\n if (attitude == null) {\n return\n }\n\n function rad2deg(rad) {\n return rad * (180.0 / Math.PI)\n }\n\n // Roll is backwards for some reason...\n attitude.updateRoll(-rad2deg(msg.roll))\n\n attitude.updatePitch(rad2deg(msg.pitch))\n}\n",
"sandbox": "// Import Gauges from https://github.com/teocci/js-module-flight-indicators\n// Add css with link tag\nconst css = document.createElement('link')\ncss.rel = \"stylesheet\"\ncss.href = \"https://unpkg.com/flight-indicators-js@1.0.5/css/flight-indicators.css\"\ndocument.body.appendChild(css)\n\nlet attitude\nimport(\"https://unpkg.com/flight-indicators-js@1.0.5/esm/module-flight-indicators.mjs\").then((mod) => {\n const FlightIndicators = mod.default\n\n attitude = new FlightIndicators(\n div,\n FlightIndicators.TYPE_ATTITUDE\n )\n\n // This is a dirty hack to switch to remote copy's of images\n let images = div.querySelectorAll(\"img\")\n for (const image of images) {\n let src = image.src\n\n var lastIndex = src.lastIndexOf(\"/img/\")\n image.src = \"https://unpkg.com/flight-indicators-js@1.0.5\" + src.substr(lastIndex)\n\n // Hide box is broken, hide manually\n // see: https://github.com/teocci/js-module-flight-indicators/pull/1\n if (src.endsWith(\"fi_box.svg\")) {\n image.style.display = \"none\"\n }\n }\n\n resize()\n})\n\n// Remove margin and border to give more room\ndiv.style.margin = 0\ndiv.style.border = 0\ndiv.style.padding = 0\n\n// Center gauge\ndiv.style.display = \"flex\"\ndiv.style.justifyContent = \"center\"\ndiv.style.alignItems = \"center\"\n\nfunction resize() {\n\n if (attitude == null) {\n return\n }\n\n // Get width and height of widget\n const width = div.offsetWidth\n const height = div.offsetHeight\n\n const max_size = Math.min(width, height)\n attitude.resize(max_size)\n}\n\n// Watch for size changes\nnew ResizeObserver(() => { resize() }).observe(div)\n\nconst ATTITUDE_id = 30\nlet selected = null //IB add\n\n// Runtime function\nhandle_msg = function(msg) {\n\n if (msg._id != ATTITUDE_id) {\n return\n }\n\n selected = msg._vehicleID //IB add\n\n if (attitude == null) {\n return\n }\n\n function rad2deg(rad) {\n return rad * (180.0 / Math.PI)\n }\n\n // Roll is backwards for some reason...\n attitude.updateRoll(-rad2deg(msg.roll))\n\n attitude.updatePitch(rad2deg(msg.pitch))\n}\n\n//IB listen for vehicle remove\nparent.addEventListener('vehicleRemoveAttitude gauge', e => {\n const vehicleID = e.detail.vehicleID\n if (vehicleID == selected) {\n // Reset angle\n attitude.updateRoll(0)\n attitude.updatePitch(0)\n resize()\n } \n})\n",
"about" : {
"name": "Attitude gauge",
"info": "Attitude gauge example built using the Sandbox widget. Reads ATTITUDE MAVLink message."
Expand Down
75 changes: 2 additions & 73 deletions TelemetryDashboard/SandBoxWidgets/Graph.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,76 +165,6 @@
"spellcheck": true,
"truncateMultipleSpaces": false
},
{
"label": "color",
"tooltip": "Text color",
"key": "color",
"type": "color",
"input": true,
"tableView": false,
"widget": {
"type": "input"
},
"inputType": "color",
"mask": false,
"data": "#000000",
"id": "emqmow4a",
"placeholder": "",
"prefix": "",
"customClass": "",
"suffix": "",
"multiple": false,
"defaultValue": null,
"protected": false,
"unique": false,
"persistent": true,
"hidden": false,
"clearOnHide": true,
"refreshOn": "",
"redrawOn": "",
"modalEdit": false,
"dataGridLabel": false,
"labelPosition": "top",
"description": "",
"errorLabel": "",
"hideLabel": false,
"tabindex": "",
"disabled": false,
"autofocus": false,
"dbIndex": false,
"customDefaultValue": "",
"calculateValue": "",
"calculateServer": false,
"attributes": {},
"validateOn": "change",
"validate": {
"required": false,
"custom": "",
"customPrivate": false,
"strictDateValidation": false,
"multiple": false,
"unique": false
},
"conditional": {
"show": null,
"when": null,
"eq": ""
},
"overlay": {
"style": "",
"left": "",
"top": "",
"width": "",
"height": ""
},
"allowCalculateOverride": false,
"encrypted": false,
"showCharCount": false,
"showWordCount": false,
"properties": {},
"allowMultipleMasks": false,
"addons": []
},
{
"label": "Message",
"tooltip": "Message to look for",
Expand Down Expand Up @@ -1690,7 +1620,7 @@
},
{
"label": "Field",
"tooltip": "Mesage feild to display",
"tooltip": "Message field to display",
"MAVLinkMsgSelect": "message",
"defaultValue": "groundspeed",
"key": "field",
Expand Down Expand Up @@ -1945,10 +1875,9 @@
"field": "groundspeed",
"scaleFactor": 1,
"time": 60,
"color": "#000000",
"periodS": 60
},
"sandbox": "// Include potly\nconst script = document.createElement(\"script\")\nscript.src = \"https://cdn.plot.ly/plotly-2.35.0.min.js\"\ndocument.body.appendChild(script)\n\n// Setup layout\nconst plot_layout = { \n title: { text: options.title },\n legend: { itemclick: false, itemdoubleclick: false }, \n margin: { b: 50, l: 65, r: 50, t: 50 },\n xaxis: { title: { text: \"time (s)\" }, range: [-options.time, 0], zeroline: false, showline: true, mirror: true },\n yaxis: { title: { text: options.label }, zeroline: false, showline: true, mirror: true }\n}\n\nconst plot_data = [\n { mode: 'lines', x: [], y:[], line: { color: options.color } }\n]\n\ndata = {\n time: [],\n value: []\n}\n\nlet plot_created = false\n\n// Update plot\nfunction update_data() {\n\n // Calculate time since sample\n const now = Date.now()\n const len = data.time.length\n const dt = new Array(len)\n for (let i = 0; i<len; i++) {\n dt[i] = (now - data.time[i]) / -1000.0\n }\n\n // See if there is any data to discard\n const last = dt.findLastIndex((x) => -x > options.time)\n if (last != -1) {\n data.time.splice(0, last)\n data.value.splice(0, last)\n dt.splice(0, last)\n }\n\n // Update plot\n plot_data[0].x = dt\n plot_data[0].y = data.value\n\n // Make sure plotly is loaded\n if (window.Plotly !== undefined) {\n if (!plot_created) {\n replot()\n }\n Plotly.redraw(div)\n }\n}\n\n\nfunction replot() {\n // Clear plot and redraw to cope with change in size or options\n plot_layout.title.text = options.title\n plot_layout.xaxis.range[0] = -options.time\n plot_layout.yaxis.title.text = options.label\n plot_data[0].line.color = options.color\n\n if (window.Plotly !== undefined) {\n Plotly.purge(div)\n Plotly.newPlot(div, plot_data, plot_layout, {displaylogo: false})\n plot_created = true\n }\n}\n\n// Watch for size changes\nnew ResizeObserver(() => { replot() }).observe(div)\n\n// Runtime function\nhandle_msg = function (msg) {\n\n // Check message ID\n if (msg._id != options.message) {\n return\n }\n\n // Check for field\n if (!(options.field in msg)) {\n throw new Error(\"No field \" + options.field + \" in \" + msg._name)\n }\n\n let value = msg[options.field]\n value *= options.scaleFactor\n\n // Add data\n data.value.push(value)\n data.time.push(Date.now())\n\n // Plot\n update_data()\n}\n\n// Add 10Hz update plot\nsetInterval(update_data, 100)\n\n// Optional function to allow run-time update of options\nhandle_options = function (new_options) {\n options = new_options\n\n replot()\n}\n",
"sandbox": "// Include Plotly\nconst script = document.createElement(\"script\")\nscript.src = \"https://cdn.plot.ly/plotly-2.35.0.min.js\"\ndocument.body.appendChild(script)\n\n// Setup layout\nconst plot_layout = {\n title: { text: options.title },\n legend: { itemclick: false, itemdoubleclick: false },\n margin: { b: 50, l: 65, r: 50, t: 50 },\n xaxis: {\n title: { text: \"time (s)\" },\n range: [-options.time, 0],\n zeroline: false,\n showline: true,\n mirror: true\n },\n yaxis: {\n title: { text: options.label },\n zeroline: false,\n showline: true,\n mirror: true\n }\n}\n\nconst plot_data = [] //IB change for multi vehicle\n\nlet vehicle_data = {} //IB add\nlet plot_created = false\n\n//IB moved plot_data and vehicle_data into function for multi vehicle purposes\nfunction graph_vehicle_init(id, colour, vehicleID) {\n\n const trace = {\n mode: \"lines\",\n x: [],\n y: [],\n line: { color: colour }, //IB change colour to vehicle colour property\n name: parent.vehicleMap.get(vehicleID).name //IB label according to user-inputted vehicle name\n }\n\n plot_data.push(trace)\n\n vehicle_data[id] = {\n time: [],\n value: [],\n trace_index: plot_data.length - 1\n }\n\n replot()\n}\n\n// Update plot\nfunction update_data() {\n\n //IB move inside for loop for each vehicle and updated variable names\n for (const id in vehicle_data) {\n \n // Calculate time since sample\n const v = vehicle_data[id] //IB add\n const now = Date.now()\n const len = v.time.length\n const dt = new Array(len)\n\n for (let i = 0; i < len; i++) {\n dt[i] = (now - v.time[i]) / -1000.0 \n }\n\n // See if there is any data to discard\n const last = dt.findLastIndex((x) => -x > options.time) \n\n if (last !== -1) {\n v.time.splice(0, last) \n v.value.splice(0, last) \n dt.splice(0, last) \n }\n\n // Update plot data\n plot_data[v.trace_index].x = dt \n plot_data[v.trace_index].y = v.value \n\n }\n \n\n // Make sure Plotly is loaded\n if (window.Plotly !== undefined) {\n if (!plot_created) {\n replot() \n }\n Plotly.redraw(div) \n }\n}\n\nfunction replot() {\n // Clear plot and redraw to cope with change in size or options\n plot_layout.title.text = options.title \n plot_layout.xaxis.range[0] = -options.time \n plot_layout.yaxis.title.text = options.label \n\n if (window.Plotly !== undefined) {\n Plotly.purge(div) \n Plotly.newPlot(div, plot_data, plot_layout, { displaylogo: false }) \n plot_created = true \n }\n}\n\n//IB change plotline colour\nfunction change_colour(colour, id) {\n // Change colour, clear plot and redraw\n plot_data[vehicle_data[id].trace_index].line.color = colour \n if (window.Plotly !== undefined) {\n Plotly.purge(div) \n Plotly.newPlot(div, plot_data, plot_layout, { displaylogo: false }) \n plot_created = true \n }\n}\n\n// Watch for size changes\nnew ResizeObserver(() => {\n replot() \n}).observe(div) \n\n// Runtime function\nhandle_msg = function (msg) {\n\n // Check message ID\n if (msg._id !== options.message) {\n return \n }\n\n // Check for field\n if (!(options.field in msg)) {\n throw new Error(\"No field \" + options.field + \" in \" + msg._name) \n }\n\n const id = msg._vehicleID //IB add\n\n //IB initiate new trace for new vehicle\n if (vehicle_data[id] == null) {\n graph_vehicle_init(id, msg._colour, msg._vehicleID)\n } else if (plot_data[vehicle_data[id].trace_index].line.color !== msg._colour) {\n change_colour(msg._colour, id)\n }\n\n let value = msg[options.field] \n value *= options.scaleFactor \n\n // Add data\n vehicle_data[id].value.push(value) \n vehicle_data[id].time.push(Date.now()) \n\n // Plot\n update_data() \n} \n\n// Add 10 Hz update plot\nsetInterval(update_data, 100) \n\n// Optional function to allow run-time update of options\nhandle_options = function (new_options) {\n options = new_options \n replot() \n} ",
"about" : {
"name": "Graph",
"info": "Graph example built using the Sandbox widget. User customizable plot options."
Expand Down
Loading