diff --git a/dashboards/PROD/PROD_DRD_Cascadia.json b/dashboards/PROD/PROD_DRD_Cascadia.json new file mode 100644 index 00000000..0e9cd3e1 --- /dev/null +++ b/dashboards/PROD/PROD_DRD_Cascadia.json @@ -0,0 +1,1475 @@ +{ + "title": "PROD: DRD - Cascadia", + "uid": "cascadia-prod-drd", + "tags": [ + "cascadia", + "prod" + ], + "description": "Driver dashboard: link health, faults, pedal/drive inputs and the motor command sent to MDI.", + "editable": true, + "graphTooltip": 1, + "fiscalYearStartMonth": 0, + "liveNow": false, + "refresh": "1s", + "schemaVersion": 38, + "version": 1, + "style": "dark", + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "templating": { + "list": [] + }, + "annotations": { + "list": [] + }, + "links": [ + { + "title": "Cascadia boards", + "type": "dashboards", + "tags": [ + "cascadia", + "prod" + ], + "asDropdown": false, + "includeVars": false, + "keepTime": true, + "icon": "external link", + "targetBlank": false + } + ], + "panels": [ + { + "type": "stat", + "title": "DRD: Fault banner", + "description": "Red: a fault bit is set now (names listed). Orange: something is wrong that is not a fault bit: DRD link lost / rebooted; Speed data timeout; DriveState data timeout; SOC data timeout; Voltage data timeout; Current data timeout; Accel data timeout; Drive state INVALID. Blank when everything is normal.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "import \"array\"\nimport \"strings\"\nst = from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\")\n |> filter(fn: (r) => r[\"class\"] == \"DRDDiagnostics\")\n |> filter(fn: (r) => r[\"_field\"] == \"ADCOutOfRange\" or r[\"_field\"] == \"ADCMismatch\" or r[\"_field\"] == \"DRDWatchdogReset\" or r[\"_field\"] == \"MotorCommFault\" or r[\"_field\"] == \"DRDMCUOverTemp\" or r[\"_field\"] == \"DRDMCUUnderVoltage\")\n |> aggregateWindow(every: v.windowPeriod, fn: max, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> toInt()\n |> duplicate(column: \"_value\", as: \"now\")\n |> cumulativeSum(columns: [\"_value\"])\n |> map(fn: (r) => ({_time: r._time, _field: r._field, _value: if r.now > 0 then 2 else if r._value > 0 then 1 else 0}))\n |> last()\n |> group()\nact = st |> filter(fn: (r) => r._value == 2) |> findColumn(fn: (key) => true, column: \"_field\")\nwl = from(bucket: \"CAN_prod\")\n |> range(start: -1h)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\" and r[\"class\"] == \"DRDTimeSinceBootup\")\n |> last()\n |> map(fn: (r) => ({r with _value:\n if float(v: uint(v: now()) - uint(v: r._time)) / 1000000000.0 > 3.0 then \"DRD link lost\"\n else if float(v: r._value) < 60.0 then \"DRD rebooted \" + string(v: int(v: r._value)) + \" s ago\"\n else \"\"}))\n |> keep(columns: [\"_value\"])\nw0 = from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\" and r[\"class\"] == \"DRDDiagnostics\" and r[\"_field\"] == \"SpeedTimeout\")\n |> last()\n |> map(fn: (r) => ({r with _value: if r._value == 1 then \"Speed data timeout\" + \"\" else \"\"}))\n |> keep(columns: [\"_value\"])\nw1 = from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\" and r[\"class\"] == \"DRDDiagnostics\" and r[\"_field\"] == \"DriveStateTimeout\")\n |> last()\n |> map(fn: (r) => ({r with _value: if r._value == 1 then \"DriveState data timeout\" + \"\" else \"\"}))\n |> keep(columns: [\"_value\"])\nw2 = from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\" and r[\"class\"] == \"DRDDiagnostics\" and r[\"_field\"] == \"SOCTimeout\")\n |> last()\n |> map(fn: (r) => ({r with _value: if r._value == 1 then \"SOC data timeout\" + \"\" else \"\"}))\n |> keep(columns: [\"_value\"])\nw3 = from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\" and r[\"class\"] == \"DRDDiagnostics\" and r[\"_field\"] == \"VoltageTimeout\")\n |> last()\n |> map(fn: (r) => ({r with _value: if r._value == 1 then \"Voltage data timeout\" + \"\" else \"\"}))\n |> keep(columns: [\"_value\"])\nw4 = from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\" and r[\"class\"] == \"DRDDiagnostics\" and r[\"_field\"] == \"CurrentTimeout\")\n |> last()\n |> map(fn: (r) => ({r with _value: if r._value == 1 then \"Current data timeout\" + \"\" else \"\"}))\n |> keep(columns: [\"_value\"])\nw5 = from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\" and r[\"class\"] == \"DRDDiagnostics\" and r[\"_field\"] == \"AccelTimeout\")\n |> last()\n |> map(fn: (r) => ({r with _value: if r._value == 1 then \"Accel data timeout\" + \"\" else \"\"}))\n |> keep(columns: [\"_value\"])\nw6 = from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\" and r[\"class\"] == \"DRDDiagnostics\" and r[\"_field\"] == \"DriveState\")\n |> last()\n |> map(fn: (r) => ({r with _value: if r._value == 0 then \"Drive state INVALID\" + \"\" else \"\"}))\n |> keep(columns: [\"_value\"])\nwarn = union(tables: [wl, w0, w1, w2, w3, w4, w5, w6]) |> group() |> filter(fn: (r) => r._value != \"\") |> findColumn(fn: (key) => true, column: \"_value\")\nf = if length(arr: act) > 0 then \"ACTIVE FAULT: \" + strings.joinStr(arr: act, v: \", \") else \"\"\nw = if length(arr: warn) > 0 then \"WARNING: \" + strings.joinStr(arr: warn, v: \", \") else \"\"\nmsg = if f != \"\" and w != \"\" then f + \" | \" + w else f + w\narray.from(rows: [{_value: msg}]) |> yield(name: \"banner\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [ + { + "type": "regex", + "options": { + "pattern": "^ACTIVE FAULT.*", + "result": { + "color": "#ff1a1a", + "index": 0 + } + } + }, + { + "type": "regex", + "options": { + "pattern": "^WARNING.*", + "result": { + "color": "#ff9830", + "index": 1 + } + } + }, + { + "type": "special", + "options": { + "match": "empty", + "result": { + "text": " ", + "color": "transparent", + "index": 2 + } + } + } + ] + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/.*/", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "textMode": "value", + "text": { + "valueSize": 28 + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 0, + "w": 24, + "h": 3 + }, + "id": 1 + }, + { + "type": "row", + "title": "Health & faults", + "collapsed": false, + "panels": [], + "gridPos": { + "x": 0, + "y": 3, + "w": 24, + "h": 1 + }, + "id": 2 + }, + { + "type": "stat", + "title": "DRD: Link (s since last frame)", + "description": "Time since the last DRDTimeSinceBootup frame reached InfluxDB (looks back 1 h). Red after 3 s: the board, CAN or the radio link is down.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: -1h)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\")\n |> filter(fn: (r) => r[\"class\"] == \"DRDTimeSinceBootup\")\n |> last()\n |> map(fn: (r) => ({_time: r._time, _field: \"Last frame\", _value: float(v: uint(v: now()) - uint(v: r._time)) / 1000000000.0}))\n |> yield(name: \"age\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "s", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "#ff1a1a", + "value": 3.0 + } + ] + }, + "mappings": [], + "decimals": 1 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "textMode": "auto" + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 4, + "w": 4, + "h": 5 + }, + "id": 3 + }, + { + "type": "stat", + "title": "DRD: Uptime (s)", + "description": "DRDTimeSinceBootUp: seconds since boot. A drop to ~0 means the board reset.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\")\n |> filter(fn: (r) => r[\"class\"] == \"DRDTimeSinceBootup\")\n |> filter(fn: (r) => r[\"_field\"] == \"DRDTimeSinceBootUp\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"last\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "s", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [], + "decimals": 0 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "textMode": "auto" + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 4, + "y": 4, + "w": 4, + "h": 5 + }, + "id": 4 + }, + { + "type": "stat", + "title": "DRD: MCU Vdd (mV)", + "description": "DRDMCUSupplyVoltage: MCU supply. Red below 2000 mV, where firmware sets the undervoltage flag. 0 means the ADC read failed.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\")\n |> filter(fn: (r) => r[\"class\"] == \"DRDMCUHealth\")\n |> filter(fn: (r) => r[\"_field\"] == \"DRDMCUSupplyVoltage\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "suffix: mV", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "#ff1a1a", + "value": 0 + }, + { + "color": "transparent", + "value": 2000 + } + ] + }, + "mappings": [], + "decimals": 0 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "textMode": "auto" + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 8, + "y": 4, + "w": 4, + "h": 5 + }, + "id": 5 + }, + { + "type": "stat", + "title": "DRD: MCU temperature (\u00b0C)", + "description": "DRDMCUTemperature: MCU die temperature. Red at 125 \u00b0C, where firmware sets the overtemperature flag.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\")\n |> filter(fn: (r) => r[\"class\"] == \"DRDMCUHealth\")\n |> filter(fn: (r) => r[\"_field\"] == \"DRDMCUTemperature\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "celsius", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "#ff1a1a", + "value": 125 + } + ] + }, + "mappings": [], + "decimals": 0 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "textMode": "auto" + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 12, + "y": 4, + "w": 4, + "h": 5 + }, + "id": 6 + }, + { + "type": "stat", + "title": "DRD: Drive state", + "description": "DriveState as DRD sees it.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\")\n |> filter(fn: (r) => r[\"class\"] == \"DRDDiagnostics\")\n |> filter(fn: (r) => r[\"_field\"] == \"DriveState\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"last\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "INVALID", + "color": "#ff1a1a", + "index": 0 + }, + "1": { + "text": "FORWARD", + "color": "transparent", + "index": 1 + }, + "2": { + "text": "CRUISE", + "color": "transparent", + "index": 2 + }, + "3": { + "text": "PARK", + "color": "transparent", + "index": 3 + }, + "4": { + "text": "REVERSE", + "color": "transparent", + "index": 4 + } + } + }, + { + "type": "range", + "options": { + "from": -1000000000000.0, + "to": 1000000000000.0, + "result": { + "text": "UNKNOWN", + "color": "#ff9830", + "index": 5 + } + } + } + ] + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "textMode": "auto" + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 16, + "y": 4, + "w": 4, + "h": 5 + }, + "id": 7 + }, + { + "type": "stat", + "title": "DRD: SOC (%)", + "description": "State of charge shown to the driver (can read up to 104 %, SOC_MAX = 1.04).", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\")\n |> filter(fn: (r) => r[\"class\"] == \"DRDDiagnostics\")\n |> filter(fn: (r) => r[\"_field\"] == \"SOC\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"last\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "percent", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [], + "decimals": 0, + "min": 0, + "max": 104 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "textMode": "auto" + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 20, + "y": 4, + "w": 4, + "h": 5 + }, + "id": 8 + }, + { + "type": "state-timeline", + "title": "DRD: Faults", + "description": "DRDDiagnostics fault bits. Per row: OK, FAULT (red) while set, CLEARED (yellow) once it has faulted in the time range shown. Zoom the time range past an old fault to clear the yellow.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\")\n |> filter(fn: (r) => r[\"class\"] == \"DRDDiagnostics\")\n |> filter(fn: (r) => r[\"_field\"] == \"ADCOutOfRange\" or r[\"_field\"] == \"ADCMismatch\" or r[\"_field\"] == \"DRDWatchdogReset\" or r[\"_field\"] == \"MotorCommFault\" or r[\"_field\"] == \"DRDMCUOverTemp\" or r[\"_field\"] == \"DRDMCUUnderVoltage\")\n |> aggregateWindow(every: v.windowPeriod, fn: max, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> toInt()\n |> duplicate(column: \"_value\", as: \"now\")\n |> cumulativeSum(columns: [\"_value\"])\n |> map(fn: (r) => ({_time: r._time, _field: r._field, _value: if r.now > 0 then 2 else if r._value > 0 then 1 else 0}))\n |> yield(name: \"fault_state\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "color": { + "mode": "fixed", + "fixedColor": "#8e8e8e" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#8e8e8e", + "value": null + } + ] + }, + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "OK", + "color": "#8e8e8e", + "index": 0 + }, + "1": { + "text": "CLEARED", + "color": "#f2cc0c", + "index": 1 + }, + "2": { + "text": "FAULT", + "color": "#ff1a1a", + "index": 2 + } + } + }, + { + "type": "range", + "options": { + "from": -1000000000000.0, + "to": 1000000000000.0, + "result": { + "text": "UNKNOWN", + "color": "#ff9830", + "index": 3 + } + } + } + ], + "custom": { + "fillOpacity": 80, + "lineWidth": 0, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "showValue": "auto", + "mergeValues": true, + "alignValue": "left", + "rowHeight": 0.85, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 9, + "w": 12, + "h": 6 + }, + "id": 9 + }, + { + "type": "state-timeline", + "title": "DRD: Cyclic data timeouts", + "description": "A cyclic input DRD displays stopped arriving, so the LCD shows stale data.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\")\n |> filter(fn: (r) => r[\"class\"] == \"DRDDiagnostics\")\n |> filter(fn: (r) => r[\"_field\"] == \"SpeedTimeout\" or r[\"_field\"] == \"DriveStateTimeout\" or r[\"_field\"] == \"SOCTimeout\" or r[\"_field\"] == \"VoltageTimeout\" or r[\"_field\"] == \"CurrentTimeout\" or r[\"_field\"] == \"AccelTimeout\")\n |> aggregateWindow(every: v.windowPeriod, fn: max, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"max\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "color": { + "mode": "fixed", + "fixedColor": "#8e8e8e" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#8e8e8e", + "value": null + } + ] + }, + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "OK", + "color": "#8e8e8e", + "index": 0 + }, + "1": { + "text": "TIMEOUT", + "color": "#ff9830", + "index": 1 + } + } + }, + { + "type": "range", + "options": { + "from": -1000000000000.0, + "to": 1000000000000.0, + "result": { + "text": "UNKNOWN", + "color": "#ff9830", + "index": 2 + } + } + } + ], + "custom": { + "fillOpacity": 80, + "lineWidth": 0, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "showValue": "auto", + "mergeValues": true, + "alignValue": "left", + "rowHeight": 0.85, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 12, + "y": 9, + "w": 12, + "h": 6 + }, + "id": 10 + }, + { + "type": "row", + "title": "Driver inputs & motor command", + "collapsed": false, + "panels": [], + "gridPos": { + "x": 0, + "y": 15, + "w": 24, + "h": 1 + }, + "id": 11 + }, + { + "type": "timeseries", + "title": "DRD: Throttle & regen command (DAC counts)", + "description": "MotorCommand DAC values sent to MDI (0 to 1023; MDI caps throttle at 920).", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\")\n |> filter(fn: (r) => r[\"class\"] == \"MotorCommand\")\n |> filter(fn: (r) => r[\"_field\"] == \"ThrottleDAC\" or r[\"_field\"] == \"RegenDAC\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "suffix: counts", + "color": { + "mode": "palette-classic" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [], + "decimals": 0, + "min": 0, + "max": 1023, + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 0, + "showPoints": "never", + "spanNulls": true, + "axisPlacement": "auto", + "axisLabel": "", + "thresholdsStyle": { + "mode": "off" + }, + "stacking": { + "mode": "none", + "group": "A" + }, + "lineInterpolation": "linear", + "gradientMode": "none", + "barAlignment": 0, + "scaleDistribution": { + "type": "linear" + }, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "calcs": [ + "lastNotNull" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 16, + "w": 12, + "h": 8 + }, + "id": 12 + }, + { + "type": "state-timeline", + "title": "DRD: Driver state", + "description": "Drive mode and driver inputs. Blue marks the less common or active state.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\")\n |> filter(fn: (r) => r[\"class\"] == \"DRDDiagnostics\")\n |> filter(fn: (r) => r[\"_field\"] == \"DriveState\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"last\")", + "refId": "A" + }, + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\")\n |> filter(fn: (r) => r[\"class\"] == \"MotorCommand\")\n |> filter(fn: (r) => r[\"_field\"] == \"Direction\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"last\")", + "refId": "B" + }, + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\")\n |> filter(fn: (r) => r[\"class\"] == \"MotorCommand\")\n |> filter(fn: (r) => r[\"_field\"] == \"EcoModeOn\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"last\")", + "refId": "C" + }, + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\")\n |> filter(fn: (r) => r[\"class\"] == \"MotorCommand\")\n |> filter(fn: (r) => r[\"_field\"] == \"SpeedUnits\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"last\")", + "refId": "D" + }, + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\")\n |> filter(fn: (r) => r[\"class\"] == \"DRDDiagnostics\")\n |> filter(fn: (r) => r[\"_field\"] == \"BrakePressed\" or r[\"_field\"] == \"RegenEnabled\")\n |> aggregateWindow(every: v.windowPeriod, fn: max, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"max\")", + "refId": "E" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "color": { + "mode": "fixed", + "fixedColor": "#8e8e8e" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#8e8e8e", + "value": null + } + ] + }, + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "INVALID", + "color": "#ff1a1a", + "index": 0 + }, + "1": { + "text": "FORWARD", + "color": "#8e8e8e", + "index": 1 + }, + "2": { + "text": "CRUISE", + "color": "#5794f2", + "index": 2 + }, + "3": { + "text": "PARK", + "color": "#464c54", + "index": 3 + }, + "4": { + "text": "REVERSE", + "color": "#5794f2", + "index": 4 + } + } + }, + { + "type": "range", + "options": { + "from": -1000000000000.0, + "to": 1000000000000.0, + "result": { + "text": "UNKNOWN", + "color": "#ff9830", + "index": 5 + } + } + } + ], + "custom": { + "fillOpacity": 80, + "lineWidth": 0, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "type": "value", + "options": { + "0": { + "text": "REVERSE", + "color": "#5794f2", + "index": 0 + }, + "1": { + "text": "FORWARD", + "color": "#464c54", + "index": 1 + } + } + }, + { + "type": "range", + "options": { + "from": -1000000000000.0, + "to": 1000000000000.0, + "result": { + "text": "UNKNOWN", + "color": "#ff9830", + "index": 2 + } + } + } + ] + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "C" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "type": "value", + "options": { + "0": { + "text": "POWER", + "color": "#5794f2", + "index": 0 + }, + "1": { + "text": "ECO", + "color": "#464c54", + "index": 1 + } + } + }, + { + "type": "range", + "options": { + "from": -1000000000000.0, + "to": 1000000000000.0, + "result": { + "text": "UNKNOWN", + "color": "#ff9830", + "index": 2 + } + } + } + ] + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "D" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "type": "value", + "options": { + "0": { + "text": "MPH", + "color": "#5794f2", + "index": 0 + }, + "1": { + "text": "KPH", + "color": "#464c54", + "index": 1 + } + } + }, + { + "type": "range", + "options": { + "from": -1000000000000.0, + "to": 1000000000000.0, + "result": { + "text": "UNKNOWN", + "color": "#ff9830", + "index": 2 + } + } + } + ] + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "E" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "type": "value", + "options": { + "0": { + "text": "OFF", + "color": "#464c54", + "index": 0 + }, + "1": { + "text": "ON", + "color": "#5794f2", + "index": 1 + } + } + }, + { + "type": "range", + "options": { + "from": -1000000000000.0, + "to": 1000000000000.0, + "result": { + "text": "UNKNOWN", + "color": "#ff9830", + "index": 2 + } + } + } + ] + } + ] + } + ] + }, + "options": { + "showValue": "auto", + "mergeValues": true, + "alignValue": "left", + "rowHeight": 0.85, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 12, + "y": 16, + "w": 12, + "h": 8 + }, + "id": 13 + }, + { + "type": "row", + "title": "Pedal sensor & SOC", + "collapsed": false, + "panels": [], + "gridPos": { + "x": 0, + "y": 24, + "w": 24, + "h": 1 + }, + "id": 14 + }, + { + "type": "timeseries", + "title": "DRD: Throttle pedal raw ADC (counts)", + "description": "Both throttle sensor channels. They should track each other; a gap means ADCMismatch.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\")\n |> filter(fn: (r) => r[\"class\"] == \"DRDDiagnostics\")\n |> filter(fn: (r) => r[\"_field\"] == \"RawADC1\" or r[\"_field\"] == \"RawADC2\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "suffix: counts", + "color": { + "mode": "palette-classic" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [], + "decimals": 0, + "min": 0, + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 0, + "showPoints": "never", + "spanNulls": true, + "axisPlacement": "auto", + "axisLabel": "", + "thresholdsStyle": { + "mode": "off" + }, + "stacking": { + "mode": "none", + "group": "A" + }, + "lineInterpolation": "linear", + "gradientMode": "none", + "barAlignment": 0, + "scaleDistribution": { + "type": "linear" + }, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "calcs": [ + "lastNotNull" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 25, + "w": 12, + "h": 7 + }, + "id": 15 + }, + { + "type": "timeseries", + "title": "DRD: SOC (%)", + "description": "SOC as displayed on the driver LCD.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\")\n |> filter(fn: (r) => r[\"class\"] == \"DRDDiagnostics\")\n |> filter(fn: (r) => r[\"_field\"] == \"SOC\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"last\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "percent", + "color": { + "mode": "palette-classic" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [], + "decimals": 0, + "min": 0, + "max": 104, + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 0, + "showPoints": "never", + "spanNulls": true, + "axisPlacement": "auto", + "axisLabel": "", + "thresholdsStyle": { + "mode": "off" + }, + "stacking": { + "mode": "none", + "group": "A" + }, + "lineInterpolation": "linear", + "gradientMode": "none", + "barAlignment": 0, + "scaleDistribution": { + "type": "linear" + }, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "calcs": [ + "lastNotNull" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 12, + "y": 25, + "w": 12, + "h": 7 + }, + "id": 16 + }, + { + "type": "row", + "title": "Raw / diagnostic (bench, bug reports)", + "collapsed": true, + "panels": [ + { + "type": "table", + "title": "DRD: All signals (latest raw values)", + "description": "Every signal this board sent in the time range, decoded with the DBC: latest value and when it arrived. Use for bench checks and when filing a bug.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"DRD\")\n |> last()\n |> toFloat()\n |> keep(columns: [\"_time\", \"class\", \"_field\", \"_value\"])\n |> group()\n |> sort(columns: [\"class\", \"_field\"])\n |> rename(columns: {class: \"Message\", _field: \"Signal\", _value: \"Latest value\", _time: \"Received\"})\n |> yield(name: \"raw\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "custom": { + "align": "auto", + "filterable": true + }, + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#8e8e8e", + "value": null + } + ] + }, + "mappings": [] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Received" + }, + "properties": [ + { + "id": "unit", + "value": "dateTimeAsLocal" + } + ] + } + ] + }, + "options": { + "showHeader": true, + "cellHeight": "sm", + "footer": { + "show": false + }, + "sortBy": [ + { + "displayName": "Message", + "desc": false + } + ] + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 33, + "w": 24, + "h": 14 + }, + "id": 18 + } + ], + "gridPos": { + "x": 0, + "y": 32, + "w": 24, + "h": 1 + }, + "id": 17 + } + ] +} diff --git a/dashboards/PROD/PROD_MDI_Cascadia.json b/dashboards/PROD/PROD_MDI_Cascadia.json new file mode 100644 index 00000000..8e06a49b --- /dev/null +++ b/dashboards/PROD/PROD_MDI_Cascadia.json @@ -0,0 +1,949 @@ +{ + "title": "PROD: MDI - Cascadia", + "uid": "cascadia-prod-mdi", + "tags": [ + "cascadia", + "prod" + ], + "description": "Motor driver interface: link health, faults and the motor RTD temperature.", + "editable": true, + "graphTooltip": 1, + "fiscalYearStartMonth": 0, + "liveNow": false, + "refresh": "1s", + "schemaVersion": 38, + "version": 1, + "style": "dark", + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "templating": { + "list": [] + }, + "annotations": { + "list": [] + }, + "links": [ + { + "title": "Cascadia boards", + "type": "dashboards", + "tags": [ + "cascadia", + "prod" + ], + "asDropdown": false, + "includeVars": false, + "keepTime": true, + "icon": "external link", + "targetBlank": false + } + ], + "panels": [ + { + "type": "stat", + "title": "MDI: Fault banner", + "description": "Red: a fault bit is set now (names listed). Orange: something is wrong that is not a fault bit: MDI link lost / rebooted; Motor RTD not reading; Motor temperature. Blank when everything is normal.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "import \"array\"\nimport \"strings\"\nst = from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"MDI\")\n |> filter(fn: (r) => r[\"class\"] == \"MDIDiagnostics\")\n |> filter(fn: (r) => r[\"_field\"] == \"MDIWatchdogReset\" or r[\"_field\"] == \"MDIVoltageOverThreshold\" or r[\"_field\"] == \"MDIMotorOverTemp\" or r[\"_field\"] == \"MDIRTDFault\" or r[\"_field\"] == \"MDIRTDCommError\")\n |> aggregateWindow(every: v.windowPeriod, fn: max, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> toInt()\n |> duplicate(column: \"_value\", as: \"now\")\n |> cumulativeSum(columns: [\"_value\"])\n |> map(fn: (r) => ({_time: r._time, _field: r._field, _value: if r.now > 0 then 2 else if r._value > 0 then 1 else 0}))\n |> last()\n |> group()\nact = st |> filter(fn: (r) => r._value == 2) |> findColumn(fn: (key) => true, column: \"_field\")\nwl = from(bucket: \"CAN_prod\")\n |> range(start: -1h)\n |> filter(fn: (r) => r[\"_measurement\"] == \"MDI\" and r[\"class\"] == \"MDITimeSinceBootup\")\n |> last()\n |> map(fn: (r) => ({r with _value:\n if float(v: uint(v: now()) - uint(v: r._time)) / 1000000000.0 > 3.0 then \"MDI link lost\"\n else if float(v: r._value) < 60.0 then \"MDI rebooted \" + string(v: int(v: r._value)) + \" s ago\"\n else \"\"}))\n |> keep(columns: [\"_value\"])\nw0 = from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"MDI\" and r[\"class\"] == \"MDIMotorTemperature\" and r[\"_field\"] == \"RTDReadSuccess\")\n |> last()\n |> map(fn: (r) => ({r with _value: if r._value == 0 then \"Motor RTD not reading\" + \"\" else \"\"}))\n |> keep(columns: [\"_value\"])\nw1 = from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"MDI\" and r[\"class\"] == \"MDIMotorTemperature\" and r[\"_field\"] == \"MotorTemperature\")\n |> last()\n |> map(fn: (r) => ({r with _value: if r._value >= 95 and r._value < 100 then \"Motor temperature\" + \": \" + string(v: int(v: r._value)) + \" \u00b0C\" else \"\"}))\n |> keep(columns: [\"_value\"])\nwarn = union(tables: [wl, w0, w1]) |> group() |> filter(fn: (r) => r._value != \"\") |> findColumn(fn: (key) => true, column: \"_value\")\nf = if length(arr: act) > 0 then \"ACTIVE FAULT: \" + strings.joinStr(arr: act, v: \", \") else \"\"\nw = if length(arr: warn) > 0 then \"WARNING: \" + strings.joinStr(arr: warn, v: \", \") else \"\"\nmsg = if f != \"\" and w != \"\" then f + \" | \" + w else f + w\narray.from(rows: [{_value: msg}]) |> yield(name: \"banner\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [ + { + "type": "regex", + "options": { + "pattern": "^ACTIVE FAULT.*", + "result": { + "color": "#ff1a1a", + "index": 0 + } + } + }, + { + "type": "regex", + "options": { + "pattern": "^WARNING.*", + "result": { + "color": "#ff9830", + "index": 1 + } + } + }, + { + "type": "special", + "options": { + "match": "empty", + "result": { + "text": " ", + "color": "transparent", + "index": 2 + } + } + } + ] + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/.*/", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "textMode": "value", + "text": { + "valueSize": 28 + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 0, + "w": 24, + "h": 3 + }, + "id": 1 + }, + { + "type": "row", + "title": "Health & faults", + "collapsed": false, + "panels": [], + "gridPos": { + "x": 0, + "y": 3, + "w": 24, + "h": 1 + }, + "id": 2 + }, + { + "type": "stat", + "title": "MDI: Link (s since last frame)", + "description": "Time since the last MDITimeSinceBootup frame reached InfluxDB (looks back 1 h). Red after 3 s: the board, CAN or the radio link is down.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: -1h)\n |> filter(fn: (r) => r[\"_measurement\"] == \"MDI\")\n |> filter(fn: (r) => r[\"class\"] == \"MDITimeSinceBootup\")\n |> last()\n |> map(fn: (r) => ({_time: r._time, _field: \"Last frame\", _value: float(v: uint(v: now()) - uint(v: r._time)) / 1000000000.0}))\n |> yield(name: \"age\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "s", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "#ff1a1a", + "value": 3.0 + } + ] + }, + "mappings": [], + "decimals": 1 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "textMode": "auto" + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 4, + "w": 6, + "h": 5 + }, + "id": 3 + }, + { + "type": "stat", + "title": "MDI: Uptime (s)", + "description": "MDITimeSinceBootUp: seconds since boot. A drop to ~0 means the board reset.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"MDI\")\n |> filter(fn: (r) => r[\"class\"] == \"MDITimeSinceBootup\")\n |> filter(fn: (r) => r[\"_field\"] == \"MDITimeSinceBootUp\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"last\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "s", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [], + "decimals": 0 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "textMode": "auto" + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 6, + "y": 4, + "w": 6, + "h": 5 + }, + "id": 4 + }, + { + "type": "stat", + "title": "MDI: Motor temperature (\u00b0C)", + "description": "RTD motor temperature. Orange from 95 \u00b0C (overtemp clears here), red at 100 \u00b0C (firmware sets MDIMotorOverTemp).", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"MDI\")\n |> filter(fn: (r) => r[\"class\"] == \"MDIMotorTemperature\")\n |> filter(fn: (r) => r[\"_field\"] == \"MotorTemperature\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "celsius", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "#ff9830", + "value": 95 + }, + { + "color": "#ff1a1a", + "value": 100 + } + ] + }, + "mappings": [], + "decimals": 0 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "textMode": "auto" + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 12, + "y": 4, + "w": 6, + "h": 5 + }, + "id": 5 + }, + { + "type": "stat", + "title": "MDI: RTD read", + "description": "RTDReadSuccess (worst in window). FAILED means the temperature above is not valid.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"MDI\")\n |> filter(fn: (r) => r[\"class\"] == \"MDIMotorTemperature\")\n |> filter(fn: (r) => r[\"_field\"] == \"RTDReadSuccess\")\n |> aggregateWindow(every: v.windowPeriod, fn: min, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"min\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "FAILED", + "color": "#ff1a1a", + "index": 0 + }, + "1": { + "text": "OK", + "color": "transparent", + "index": 1 + } + } + }, + { + "type": "range", + "options": { + "from": -1000000000000.0, + "to": 1000000000000.0, + "result": { + "text": "UNKNOWN", + "color": "#ff9830", + "index": 2 + } + } + } + ] + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "textMode": "auto" + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 18, + "y": 4, + "w": 6, + "h": 5 + }, + "id": 6 + }, + { + "type": "state-timeline", + "title": "MDI: Faults", + "description": "MDIDiagnostics bits. MDIVoltageOverThreshold is defined but not set by current MDI firmware, so it always reads OK. Per row: OK, FAULT (red) while set, CLEARED (yellow) once it has faulted in the time range shown. Zoom the time range past an old fault to clear the yellow.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"MDI\")\n |> filter(fn: (r) => r[\"class\"] == \"MDIDiagnostics\")\n |> filter(fn: (r) => r[\"_field\"] == \"MDIWatchdogReset\" or r[\"_field\"] == \"MDIVoltageOverThreshold\" or r[\"_field\"] == \"MDIMotorOverTemp\" or r[\"_field\"] == \"MDIRTDFault\" or r[\"_field\"] == \"MDIRTDCommError\")\n |> aggregateWindow(every: v.windowPeriod, fn: max, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> toInt()\n |> duplicate(column: \"_value\", as: \"now\")\n |> cumulativeSum(columns: [\"_value\"])\n |> map(fn: (r) => ({_time: r._time, _field: r._field, _value: if r.now > 0 then 2 else if r._value > 0 then 1 else 0}))\n |> yield(name: \"fault_state\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "color": { + "mode": "fixed", + "fixedColor": "#8e8e8e" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#8e8e8e", + "value": null + } + ] + }, + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "OK", + "color": "#8e8e8e", + "index": 0 + }, + "1": { + "text": "CLEARED", + "color": "#f2cc0c", + "index": 1 + }, + "2": { + "text": "FAULT", + "color": "#ff1a1a", + "index": 2 + } + } + }, + { + "type": "range", + "options": { + "from": -1000000000000.0, + "to": 1000000000000.0, + "result": { + "text": "UNKNOWN", + "color": "#ff9830", + "index": 3 + } + } + } + ], + "custom": { + "fillOpacity": 80, + "lineWidth": 0, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "showValue": "auto", + "mergeValues": true, + "alignValue": "left", + "rowHeight": 0.85, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 9, + "w": 24, + "h": 6 + }, + "id": 7 + }, + { + "type": "row", + "title": "Motor temperature", + "collapsed": false, + "panels": [], + "gridPos": { + "x": 0, + "y": 15, + "w": 24, + "h": 1 + }, + "id": 8 + }, + { + "type": "timeseries", + "title": "MDI: Motor temperature (\u00b0C)", + "description": "Trend against the overtemp band (95 \u00b0C clear / 100 \u00b0C set). Watch the slope to see the limit coming.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"MDI\")\n |> filter(fn: (r) => r[\"class\"] == \"MDIMotorTemperature\")\n |> filter(fn: (r) => r[\"_field\"] == \"MotorTemperature\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "celsius", + "color": { + "mode": "palette-classic" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "#ff9830", + "value": 95 + }, + { + "color": "#ff1a1a", + "value": 100 + } + ] + }, + "mappings": [], + "decimals": 0, + "min": 0, + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 0, + "showPoints": "never", + "spanNulls": true, + "axisPlacement": "auto", + "axisLabel": "", + "thresholdsStyle": { + "mode": "dashed+area" + }, + "stacking": { + "mode": "none", + "group": "A" + }, + "lineInterpolation": "linear", + "gradientMode": "none", + "barAlignment": 0, + "scaleDistribution": { + "type": "linear" + }, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "calcs": [ + "lastNotNull" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 16, + "w": 16, + "h": 9 + }, + "id": 9 + }, + { + "type": "state-timeline", + "title": "MDI: RTD read", + "description": "RTDReadSuccess (worst in window). READ FAIL means the temperature is not valid.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"MDI\")\n |> filter(fn: (r) => r[\"class\"] == \"MDIMotorTemperature\")\n |> filter(fn: (r) => r[\"_field\"] == \"RTDReadSuccess\")\n |> aggregateWindow(every: v.windowPeriod, fn: min, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"min\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "color": { + "mode": "fixed", + "fixedColor": "#8e8e8e" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#8e8e8e", + "value": null + } + ] + }, + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "READ FAIL", + "color": "#ff1a1a", + "index": 0 + }, + "1": { + "text": "OK", + "color": "#8e8e8e", + "index": 1 + } + } + }, + { + "type": "range", + "options": { + "from": -1000000000000.0, + "to": 1000000000000.0, + "result": { + "text": "UNKNOWN", + "color": "#ff9830", + "index": 2 + } + } + } + ], + "custom": { + "fillOpacity": 80, + "lineWidth": 0, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "showValue": "auto", + "mergeValues": true, + "alignValue": "left", + "rowHeight": 0.85, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 16, + "y": 16, + "w": 8, + "h": 4 + }, + "id": 10 + }, + { + "type": "state-timeline", + "title": "MDI: RTD fault flags", + "description": "MAX31865 fault register: 0x80 RTD high (open), 0x40 RTD low (short), 0x04 over/undervoltage. Other values: several bits at once (UNKNOWN).", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"MDI\")\n |> filter(fn: (r) => r[\"class\"] == \"MDIMotorTemperature\")\n |> filter(fn: (r) => r[\"_field\"] == \"RTDFaultFlags\")\n |> aggregateWindow(every: v.windowPeriod, fn: max, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"max\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "color": { + "mode": "fixed", + "fixedColor": "#8e8e8e" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#8e8e8e", + "value": null + } + ] + }, + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "NONE", + "color": "#8e8e8e", + "index": 0 + }, + "4": { + "text": "OV/UV", + "color": "#ff1a1a", + "index": 1 + }, + "64": { + "text": "RTD LOW", + "color": "#ff1a1a", + "index": 2 + }, + "128": { + "text": "RTD HIGH", + "color": "#ff1a1a", + "index": 3 + } + } + }, + { + "type": "range", + "options": { + "from": -1000000000000.0, + "to": 1000000000000.0, + "result": { + "text": "UNKNOWN", + "color": "#ff9830", + "index": 4 + } + } + } + ], + "custom": { + "fillOpacity": 80, + "lineWidth": 0, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "showValue": "auto", + "mergeValues": true, + "alignValue": "left", + "rowHeight": 0.85, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 16, + "y": 20, + "w": 8, + "h": 5 + }, + "id": 11 + }, + { + "type": "row", + "title": "Raw / diagnostic (bench, bug reports)", + "collapsed": true, + "panels": [ + { + "type": "table", + "title": "MDI: All signals (latest raw values)", + "description": "Every signal this board sent in the time range, decoded with the DBC: latest value and when it arrived. Use for bench checks and when filing a bug.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"MDI\")\n |> last()\n |> toFloat()\n |> keep(columns: [\"_time\", \"class\", \"_field\", \"_value\"])\n |> group()\n |> sort(columns: [\"class\", \"_field\"])\n |> rename(columns: {class: \"Message\", _field: \"Signal\", _value: \"Latest value\", _time: \"Received\"})\n |> yield(name: \"raw\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "custom": { + "align": "auto", + "filterable": true + }, + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#8e8e8e", + "value": null + } + ] + }, + "mappings": [] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Received" + }, + "properties": [ + { + "id": "unit", + "value": "dateTimeAsLocal" + } + ] + } + ] + }, + "options": { + "showHeader": true, + "cellHeight": "sm", + "footer": { + "show": false + }, + "sortBy": [ + { + "displayName": "Message", + "desc": false + } + ] + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 26, + "w": 24, + "h": 14 + }, + "id": 13 + } + ], + "gridPos": { + "x": 0, + "y": 25, + "w": 24, + "h": 1 + }, + "id": 12 + } + ] +} diff --git a/dashboards/PROD/PROD_STR_Cascadia.json b/dashboards/PROD/PROD_STR_Cascadia.json new file mode 100644 index 00000000..d4ea86be --- /dev/null +++ b/dashboards/PROD/PROD_STR_Cascadia.json @@ -0,0 +1,809 @@ +{ + "title": "PROD: STR - Cascadia", + "uid": "cascadia-prod-str", + "tags": [ + "cascadia", + "prod" + ], + "description": "Steering wheel: link health, faults, button/switch states and cruise set speed.", + "editable": true, + "graphTooltip": 1, + "fiscalYearStartMonth": 0, + "liveNow": false, + "refresh": "1s", + "schemaVersion": 38, + "version": 1, + "style": "dark", + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "templating": { + "list": [] + }, + "annotations": { + "list": [] + }, + "links": [ + { + "title": "Cascadia boards", + "type": "dashboards", + "tags": [ + "cascadia", + "prod" + ], + "asDropdown": false, + "includeVars": false, + "keepTime": true, + "icon": "external link", + "targetBlank": false + } + ], + "panels": [ + { + "type": "stat", + "title": "STR: Fault banner", + "description": "Red: a fault bit is set now (names listed). Orange: something is wrong that is not a fault bit: STR link lost / rebooted. Blank when everything is normal.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "import \"array\"\nimport \"strings\"\nst = from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"STR\")\n |> filter(fn: (r) => r[\"class\"] == \"STRFlags\")\n |> filter(fn: (r) => r[\"_field\"] == \"STRMCUUnderVoltage\" or r[\"_field\"] == \"STRMCUOverTemp\")\n |> aggregateWindow(every: v.windowPeriod, fn: max, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> toInt()\n |> duplicate(column: \"_value\", as: \"now\")\n |> cumulativeSum(columns: [\"_value\"])\n |> map(fn: (r) => ({_time: r._time, _field: r._field, _value: if r.now > 0 then 2 else if r._value > 0 then 1 else 0}))\n |> last()\n |> group()\nact = st |> filter(fn: (r) => r._value == 2) |> findColumn(fn: (key) => true, column: \"_field\")\nwl = from(bucket: \"CAN_prod\")\n |> range(start: -1h)\n |> filter(fn: (r) => r[\"_measurement\"] == \"STR\" and r[\"class\"] == \"STRTimeSinceBootup\")\n |> last()\n |> map(fn: (r) => ({r with _value:\n if float(v: uint(v: now()) - uint(v: r._time)) / 1000000000.0 > 3.0 then \"STR link lost\"\n else if float(v: r._value) < 60.0 then \"STR rebooted \" + string(v: int(v: r._value)) + \" s ago\"\n else \"\"}))\n |> keep(columns: [\"_value\"])\nwarn = wl |> group() |> filter(fn: (r) => r._value != \"\") |> findColumn(fn: (key) => true, column: \"_value\")\nf = if length(arr: act) > 0 then \"ACTIVE FAULT: \" + strings.joinStr(arr: act, v: \", \") else \"\"\nw = if length(arr: warn) > 0 then \"WARNING: \" + strings.joinStr(arr: warn, v: \", \") else \"\"\nmsg = if f != \"\" and w != \"\" then f + \" | \" + w else f + w\narray.from(rows: [{_value: msg}]) |> yield(name: \"banner\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [ + { + "type": "regex", + "options": { + "pattern": "^ACTIVE FAULT.*", + "result": { + "color": "#ff1a1a", + "index": 0 + } + } + }, + { + "type": "regex", + "options": { + "pattern": "^WARNING.*", + "result": { + "color": "#ff9830", + "index": 1 + } + } + }, + { + "type": "special", + "options": { + "match": "empty", + "result": { + "text": " ", + "color": "transparent", + "index": 2 + } + } + } + ] + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/.*/", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "textMode": "value", + "text": { + "valueSize": 28 + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 0, + "w": 24, + "h": 3 + }, + "id": 1 + }, + { + "type": "row", + "title": "Health & faults", + "collapsed": false, + "panels": [], + "gridPos": { + "x": 0, + "y": 3, + "w": 24, + "h": 1 + }, + "id": 2 + }, + { + "type": "stat", + "title": "STR: Link (s since last frame)", + "description": "Time since the last STRTimeSinceBootup frame reached InfluxDB (looks back 1 h). Red after 3 s: the board, CAN or the radio link is down.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: -1h)\n |> filter(fn: (r) => r[\"_measurement\"] == \"STR\")\n |> filter(fn: (r) => r[\"class\"] == \"STRTimeSinceBootup\")\n |> last()\n |> map(fn: (r) => ({_time: r._time, _field: \"Last frame\", _value: float(v: uint(v: now()) - uint(v: r._time)) / 1000000000.0}))\n |> yield(name: \"age\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "s", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "#ff1a1a", + "value": 3.0 + } + ] + }, + "mappings": [], + "decimals": 1 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "textMode": "auto" + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 4, + "w": 6, + "h": 5 + }, + "id": 3 + }, + { + "type": "stat", + "title": "STR: Uptime (s)", + "description": "STRTimeSinceBootUp: seconds since boot. A drop to ~0 means the board reset.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"STR\")\n |> filter(fn: (r) => r[\"class\"] == \"STRTimeSinceBootup\")\n |> filter(fn: (r) => r[\"_field\"] == \"STRTimeSinceBootUp\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"last\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "s", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [], + "decimals": 0 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "textMode": "auto" + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 6, + "y": 4, + "w": 6, + "h": 5 + }, + "id": 4 + }, + { + "type": "stat", + "title": "STR: MCU Vdd (mV)", + "description": "STRMCUVoltage: MCU supply. Red below 2400 mV, where firmware sets the undervoltage flag. 0 means the ADC read failed.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"STR\")\n |> filter(fn: (r) => r[\"class\"] == \"STRMCUHealth\")\n |> filter(fn: (r) => r[\"_field\"] == \"STRMCUVoltage\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "suffix: mV", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "#ff1a1a", + "value": 0 + }, + { + "color": "transparent", + "value": 2400 + } + ] + }, + "mappings": [], + "decimals": 0 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "textMode": "auto" + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 12, + "y": 4, + "w": 6, + "h": 5 + }, + "id": 5 + }, + { + "type": "stat", + "title": "STR: MCU temperature (\u00b0C)", + "description": "STRMCUTemperature: MCU die temperature. Red at 105 \u00b0C, where firmware sets the overtemperature flag.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"STR\")\n |> filter(fn: (r) => r[\"class\"] == \"STRMCUHealth\")\n |> filter(fn: (r) => r[\"_field\"] == \"STRMCUTemperature\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "celsius", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "#ff1a1a", + "value": 105 + } + ] + }, + "mappings": [], + "decimals": 0 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "textMode": "auto" + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 18, + "y": 4, + "w": 6, + "h": 5 + }, + "id": 6 + }, + { + "type": "state-timeline", + "title": "STR: Faults", + "description": "STRFlags. Current STR firmware has no watchdog-reset flag (STRDiagnostics 0x582 is not sent). Per row: OK, FAULT (red) while set, CLEARED (yellow) once it has faulted in the time range shown. Zoom the time range past an old fault to clear the yellow.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"STR\")\n |> filter(fn: (r) => r[\"class\"] == \"STRFlags\")\n |> filter(fn: (r) => r[\"_field\"] == \"STRMCUUnderVoltage\" or r[\"_field\"] == \"STRMCUOverTemp\")\n |> aggregateWindow(every: v.windowPeriod, fn: max, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> toInt()\n |> duplicate(column: \"_value\", as: \"now\")\n |> cumulativeSum(columns: [\"_value\"])\n |> map(fn: (r) => ({_time: r._time, _field: r._field, _value: if r.now > 0 then 2 else if r._value > 0 then 1 else 0}))\n |> yield(name: \"fault_state\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "color": { + "mode": "fixed", + "fixedColor": "#8e8e8e" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#8e8e8e", + "value": null + } + ] + }, + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "OK", + "color": "#8e8e8e", + "index": 0 + }, + "1": { + "text": "CLEARED", + "color": "#f2cc0c", + "index": 1 + }, + "2": { + "text": "FAULT", + "color": "#ff1a1a", + "index": 2 + } + } + }, + { + "type": "range", + "options": { + "from": -1000000000000.0, + "to": 1000000000000.0, + "result": { + "text": "UNKNOWN", + "color": "#ff9830", + "index": 3 + } + } + } + ], + "custom": { + "fillOpacity": 80, + "lineWidth": 0, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "showValue": "auto", + "mergeValues": true, + "alignValue": "left", + "rowHeight": 0.85, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 9, + "w": 24, + "h": 4 + }, + "id": 7 + }, + { + "type": "row", + "title": "Driver controls", + "collapsed": false, + "panels": [], + "gridPos": { + "x": 0, + "y": 13, + "w": 24, + "h": 1 + }, + "id": 8 + }, + { + "type": "state-timeline", + "title": "STR: Buttons & switches", + "description": "STRDRDControl inputs (max per window so short presses still show).", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"STR\")\n |> filter(fn: (r) => r[\"class\"] == \"STRDRDControl\")\n |> filter(fn: (r) => r[\"_field\"] == \"STRRegenEnabled\" or r[\"_field\"] == \"STRCruiseEnabled\" or r[\"_field\"] == \"STRLeftTurnSignalEnabled\" or r[\"_field\"] == \"STRRightTurnSignalEnabled\" or r[\"_field\"] == \"STRHornEnabled\" or r[\"_field\"] == \"STRPushToTalkEnabled\" or r[\"_field\"] == \"STRLCDScreenNextPage\" or r[\"_field\"] == \"STRCruiseIncrement\" or r[\"_field\"] == \"STRCruiseDecrement\")\n |> aggregateWindow(every: v.windowPeriod, fn: max, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"max\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "color": { + "mode": "fixed", + "fixedColor": "#8e8e8e" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#8e8e8e", + "value": null + } + ] + }, + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "OFF", + "color": "#464c54", + "index": 0 + }, + "1": { + "text": "ON", + "color": "#5794f2", + "index": 1 + } + } + }, + { + "type": "range", + "options": { + "from": -1000000000000.0, + "to": 1000000000000.0, + "result": { + "text": "UNKNOWN", + "color": "#ff9830", + "index": 2 + } + } + } + ], + "custom": { + "fillOpacity": 80, + "lineWidth": 0, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "showValue": "auto", + "mergeValues": true, + "alignValue": "left", + "rowHeight": 0.85, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 14, + "w": 16, + "h": 10 + }, + "id": 9 + }, + { + "type": "timeseries", + "title": "STR: Cruise set speed (km/h)", + "description": "STRCruiseSetVelocity requested by the driver.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"STR\")\n |> filter(fn: (r) => r[\"class\"] == \"STRDRDControl\")\n |> filter(fn: (r) => r[\"_field\"] == \"STRCruiseSetVelocity\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"last\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "velocitykmh", + "color": { + "mode": "palette-classic" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [], + "decimals": 0, + "min": 0, + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 0, + "showPoints": "never", + "spanNulls": true, + "axisPlacement": "auto", + "axisLabel": "", + "thresholdsStyle": { + "mode": "off" + }, + "stacking": { + "mode": "none", + "group": "A" + }, + "lineInterpolation": "linear", + "gradientMode": "none", + "barAlignment": 0, + "scaleDistribution": { + "type": "linear" + }, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "calcs": [ + "lastNotNull" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 16, + "y": 14, + "w": 8, + "h": 10 + }, + "id": 10 + }, + { + "type": "row", + "title": "Raw / diagnostic (bench, bug reports)", + "collapsed": true, + "panels": [ + { + "type": "table", + "title": "STR: All signals (latest raw values)", + "description": "Every signal this board sent in the time range, decoded with the DBC: latest value and when it arrived. Use for bench checks and when filing a bug.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"STR\")\n |> last()\n |> toFloat()\n |> keep(columns: [\"_time\", \"class\", \"_field\", \"_value\"])\n |> group()\n |> sort(columns: [\"class\", \"_field\"])\n |> rename(columns: {class: \"Message\", _field: \"Signal\", _value: \"Latest value\", _time: \"Received\"})\n |> yield(name: \"raw\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "custom": { + "align": "auto", + "filterable": true + }, + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#8e8e8e", + "value": null + } + ] + }, + "mappings": [] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Received" + }, + "properties": [ + { + "id": "unit", + "value": "dateTimeAsLocal" + } + ] + } + ] + }, + "options": { + "showHeader": true, + "cellHeight": "sm", + "footer": { + "show": false + }, + "sortBy": [ + { + "displayName": "Message", + "desc": false + } + ] + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 25, + "w": 24, + "h": 14 + }, + "id": 12 + } + ], + "gridPos": { + "x": 0, + "y": 24, + "w": 24, + "h": 1 + }, + "id": 11 + } + ] +} diff --git a/dashboards/PROD/PROD_TEL_Cascadia.json b/dashboards/PROD/PROD_TEL_Cascadia.json new file mode 100644 index 00000000..e238d42d --- /dev/null +++ b/dashboards/PROD/PROD_TEL_Cascadia.json @@ -0,0 +1,2140 @@ +{ + "title": "PROD: TEL - Cascadia", + "uid": "cascadia-prod-tel", + "tags": [ + "cascadia", + "prod" + ], + "description": "Telemetry board: link health, GPS position/status/motion, IMU. Health and faults first, detail below.", + "editable": true, + "graphTooltip": 1, + "fiscalYearStartMonth": 0, + "liveNow": false, + "refresh": "1s", + "schemaVersion": 38, + "version": 1, + "style": "dark", + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "templating": { + "list": [] + }, + "annotations": { + "list": [] + }, + "links": [ + { + "title": "Cascadia boards", + "type": "dashboards", + "tags": [ + "cascadia", + "prod" + ], + "asDropdown": false, + "includeVars": false, + "keepTime": true, + "icon": "external link", + "targetBlank": false + } + ], + "panels": [ + { + "type": "stat", + "title": "TEL: Fault banner", + "description": "Red: a fault bit is set now (names listed). Orange: something is wrong that is not a fault bit: TEL link lost / rebooted; GPS satellites; GPS no 3D fix; CAN bus load. Blank when everything is normal.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "import \"array\"\nimport \"strings\"\nst = from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> filter(fn: (r) => r[\"class\"] == \"TELDiagnostics\")\n |> filter(fn: (r) => r[\"_field\"] == \"TELWatchdogReset\" or r[\"_field\"] == \"IMUReadFail\" or r[\"_field\"] == \"IMUWriteFail\" or r[\"_field\"] == \"GPSReadFail\" or r[\"_field\"] == \"GPSWriteFail\" or r[\"_field\"] == \"MCUOvertemp\" or r[\"_field\"] == \"MCUUndervoltage\")\n |> aggregateWindow(every: v.windowPeriod, fn: max, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> toInt()\n |> duplicate(column: \"_value\", as: \"now\")\n |> cumulativeSum(columns: [\"_value\"])\n |> map(fn: (r) => ({_time: r._time, _field: r._field, _value: if r.now > 0 then 2 else if r._value > 0 then 1 else 0}))\n |> last()\n |> group()\nact = st |> filter(fn: (r) => r._value == 2) |> findColumn(fn: (key) => true, column: \"_field\")\nwl = from(bucket: \"CAN_prod\")\n |> range(start: -1h)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\" and r[\"class\"] == \"TELTimeSinceBootup\")\n |> last()\n |> map(fn: (r) => ({r with _value:\n if float(v: uint(v: now()) - uint(v: r._time)) / 1000000000.0 > 3.0 then \"TEL link lost\"\n else if float(v: r._value) < 60.0 then \"TEL rebooted \" + string(v: int(v: r._value)) + \" s ago\"\n else \"\"}))\n |> keep(columns: [\"_value\"])\nw0 = from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\" and r[\"class\"] == \"TELGpsStatus\" and r[\"_field\"] == \"GpsSatsUsed\")\n |> last()\n |> map(fn: (r) => ({r with _value: if r._value < 4 then \"GPS satellites\" + \": \" + string(v: int(v: r._value)) + \"\" else \"\"}))\n |> keep(columns: [\"_value\"])\nw1 = from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\" and r[\"class\"] == \"TELGpsStatus\" and r[\"_field\"] == \"GpsFixType\")\n |> last()\n |> map(fn: (r) => ({r with _value: if r._value != 3 and r._value != 4 then \"GPS no 3D fix\" + \"\" else \"\"}))\n |> keep(columns: [\"_value\"])\nw2 = from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\" and r[\"class\"] == \"CANBusLoad\" and r[\"_field\"] == \"BusLoad\")\n |> last()\n |> map(fn: (r) => ({r with _value: if r._value >= 70 then \"CAN bus load\" + \": \" + string(v: int(v: r._value)) + \"%\" else \"\"}))\n |> keep(columns: [\"_value\"])\nwarn = union(tables: [wl, w0, w1, w2]) |> group() |> filter(fn: (r) => r._value != \"\") |> findColumn(fn: (key) => true, column: \"_value\")\nf = if length(arr: act) > 0 then \"ACTIVE FAULT: \" + strings.joinStr(arr: act, v: \", \") else \"\"\nw = if length(arr: warn) > 0 then \"WARNING: \" + strings.joinStr(arr: warn, v: \", \") else \"\"\nmsg = if f != \"\" and w != \"\" then f + \" | \" + w else f + w\narray.from(rows: [{_value: msg}]) |> yield(name: \"banner\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [ + { + "type": "regex", + "options": { + "pattern": "^ACTIVE FAULT.*", + "result": { + "color": "#ff1a1a", + "index": 0 + } + } + }, + { + "type": "regex", + "options": { + "pattern": "^WARNING.*", + "result": { + "color": "#ff9830", + "index": 1 + } + } + }, + { + "type": "special", + "options": { + "match": "empty", + "result": { + "text": " ", + "color": "transparent", + "index": 2 + } + } + } + ] + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/.*/", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "textMode": "value", + "text": { + "valueSize": 28 + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 0, + "w": 24, + "h": 3 + }, + "id": 1 + }, + { + "type": "row", + "title": "Health & faults", + "collapsed": false, + "panels": [], + "gridPos": { + "x": 0, + "y": 3, + "w": 24, + "h": 1 + }, + "id": 2 + }, + { + "type": "stat", + "title": "TEL: Link (s since last frame)", + "description": "Time since the last TELTimeSinceBootup frame reached InfluxDB (looks back 1 h). Red after 3 s: the board, CAN or the radio link is down.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: -1h)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> filter(fn: (r) => r[\"class\"] == \"TELTimeSinceBootup\")\n |> last()\n |> map(fn: (r) => ({_time: r._time, _field: \"Last frame\", _value: float(v: uint(v: now()) - uint(v: r._time)) / 1000000000.0}))\n |> yield(name: \"age\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "s", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "#ff1a1a", + "value": 3.0 + } + ] + }, + "mappings": [], + "decimals": 1 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "textMode": "auto" + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 4, + "w": 4, + "h": 5 + }, + "id": 3 + }, + { + "type": "stat", + "title": "TEL: Uptime (s)", + "description": "TELTimeSinceBootUp: seconds since boot. A drop to ~0 means the board reset.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> filter(fn: (r) => r[\"class\"] == \"TELTimeSinceBootup\")\n |> filter(fn: (r) => r[\"_field\"] == \"TELTimeSinceBootUp\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"last\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "s", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [], + "decimals": 0 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "textMode": "auto" + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 4, + "y": 4, + "w": 4, + "h": 5 + }, + "id": 4 + }, + { + "type": "stat", + "title": "TEL: MCU Vdd (mV)", + "description": "MCU_Voltage: MCU supply. Red below 2000 mV, where firmware sets the undervoltage flag. 0 means the ADC read failed.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> filter(fn: (r) => r[\"class\"] == \"TELMCUHealth\")\n |> filter(fn: (r) => r[\"_field\"] == \"MCU_Voltage\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "suffix: mV", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "#ff1a1a", + "value": 0 + }, + { + "color": "transparent", + "value": 2000 + } + ] + }, + "mappings": [], + "decimals": 0 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "textMode": "auto" + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 8, + "y": 4, + "w": 4, + "h": 5 + }, + "id": 5 + }, + { + "type": "stat", + "title": "TEL: MCU temperature (\u00b0C)", + "description": "MCU_Temperature: MCU die temperature. Red at 125 \u00b0C, where firmware sets the overtemperature flag.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> filter(fn: (r) => r[\"class\"] == \"TELMCUHealth\")\n |> filter(fn: (r) => r[\"_field\"] == \"MCU_Temperature\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "celsius", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "#ff1a1a", + "value": 125 + } + ] + }, + "mappings": [], + "decimals": 0 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "textMode": "auto" + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 12, + "y": 4, + "w": 4, + "h": 5 + }, + "id": 6 + }, + { + "type": "stat", + "title": "TEL: CAN bus load (%)", + "description": "BusLoad measured by TEL. Orange from 70 %, red from 90 % (frames start to be delayed).", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> filter(fn: (r) => r[\"class\"] == \"CANBusLoad\")\n |> filter(fn: (r) => r[\"_field\"] == \"BusLoad\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "percent", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "#ff9830", + "value": 70 + }, + { + "color": "#ff1a1a", + "value": 90 + } + ] + }, + "mappings": [], + "decimals": 0, + "min": 0, + "max": 100 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "textMode": "auto" + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 16, + "y": 4, + "w": 4, + "h": 5 + }, + "id": 7 + }, + { + "type": "stat", + "title": "TEL: GPS fix", + "description": "GpsFixType. Only 3D fix is normal; anything else is shown in orange with its name.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> filter(fn: (r) => r[\"class\"] == \"TELGpsStatus\")\n |> filter(fn: (r) => r[\"_field\"] == \"GpsFixType\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"last\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "NO FIX", + "color": "#ff9830", + "index": 0 + }, + "1": { + "text": "DEAD RECKONING", + "color": "#ff9830", + "index": 1 + }, + "2": { + "text": "2D FIX", + "color": "#ff9830", + "index": 2 + }, + "3": { + "text": "3D FIX", + "color": "transparent", + "index": 3 + }, + "4": { + "text": "GNSS + DR", + "color": "transparent", + "index": 4 + }, + "5": { + "text": "TIME ONLY", + "color": "#ff9830", + "index": 5 + } + } + }, + { + "type": "range", + "options": { + "from": -1000000000000.0, + "to": 1000000000000.0, + "result": { + "text": "UNKNOWN", + "color": "#ff9830", + "index": 6 + } + } + } + ] + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "textMode": "auto" + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 20, + "y": 4, + "w": 4, + "h": 5 + }, + "id": 8 + }, + { + "type": "state-timeline", + "title": "TEL: Faults", + "description": "TELDiagnostics bits (max per window, so a single faulted frame is never averaged away). Per row: OK, FAULT (red) while set, CLEARED (yellow) once it has faulted in the time range shown. Zoom the time range past an old fault to clear the yellow.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> filter(fn: (r) => r[\"class\"] == \"TELDiagnostics\")\n |> filter(fn: (r) => r[\"_field\"] == \"TELWatchdogReset\" or r[\"_field\"] == \"IMUReadFail\" or r[\"_field\"] == \"IMUWriteFail\" or r[\"_field\"] == \"GPSReadFail\" or r[\"_field\"] == \"GPSWriteFail\" or r[\"_field\"] == \"MCUOvertemp\" or r[\"_field\"] == \"MCUUndervoltage\")\n |> aggregateWindow(every: v.windowPeriod, fn: max, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> toInt()\n |> duplicate(column: \"_value\", as: \"now\")\n |> cumulativeSum(columns: [\"_value\"])\n |> map(fn: (r) => ({_time: r._time, _field: r._field, _value: if r.now > 0 then 2 else if r._value > 0 then 1 else 0}))\n |> yield(name: \"fault_state\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "color": { + "mode": "fixed", + "fixedColor": "#8e8e8e" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#8e8e8e", + "value": null + } + ] + }, + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "OK", + "color": "#8e8e8e", + "index": 0 + }, + "1": { + "text": "CLEARED", + "color": "#f2cc0c", + "index": 1 + }, + "2": { + "text": "FAULT", + "color": "#ff1a1a", + "index": 2 + } + } + }, + { + "type": "range", + "options": { + "from": -1000000000000.0, + "to": 1000000000000.0, + "result": { + "text": "UNKNOWN", + "color": "#ff9830", + "index": 3 + } + } + } + ], + "custom": { + "fillOpacity": 80, + "lineWidth": 0, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "showValue": "auto", + "mergeValues": true, + "alignValue": "left", + "rowHeight": 0.85, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 9, + "w": 24, + "h": 6 + }, + "id": 9 + }, + { + "type": "row", + "title": "GPS position & status", + "collapsed": false, + "panels": [], + "gridPos": { + "x": 0, + "y": 15, + "w": 24, + "h": 1 + }, + "id": 10 + }, + { + "type": "geomap", + "title": "TEL: GPS track", + "description": "GpsLatitude/GpsLongitude (deg). TEL only sends a position when the receiver has a fix.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> filter(fn: (r) => r[\"class\"] == \"TELGpsPosition\")\n |> filter(fn: (r) => r[\"_field\"] == \"GpsLatitude\" or r[\"_field\"] == \"GpsLongitude\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> pivot(rowKey: [\"_time\"], columnKey: [\"_field\"], valueColumn: \"_value\")\n |> keep(columns: [\"_time\", \"GpsLatitude\", \"GpsLongitude\"])\n |> yield(name: \"track\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "degree", + "color": { + "mode": "fixed", + "fixedColor": "#5794f2" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#5794f2", + "value": null + } + ] + }, + "mappings": [], + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "view": { + "id": "fit", + "lat": 0, + "lon": 0, + "zoom": 15, + "allLayers": true, + "lastOnly": false + }, + "controls": { + "showZoom": true, + "mouseWheelZoom": false, + "showAttribution": true + }, + "basemap": { + "type": "xyz", + "name": "OpenStreetMap", + "config": { + "url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png", + "attribution": "OpenStreetMap contributors" + } + }, + "layers": [ + { + "type": "route", + "name": "Track", + "tooltip": true, + "location": { + "mode": "coords", + "latitude": "GpsLatitude", + "longitude": "GpsLongitude" + }, + "config": { + "style": { + "color": { + "fixed": "#5794f2" + }, + "size": { + "fixed": 3 + }, + "opacity": 0.9 + } + } + }, + { + "type": "markers", + "name": "Now", + "tooltip": true, + "location": { + "mode": "coords", + "latitude": "GpsLatitude", + "longitude": "GpsLongitude" + }, + "filterData": { + "id": "byRefId", + "options": "A" + }, + "config": { + "showLegend": false, + "style": { + "color": { + "fixed": "#5794f2" + }, + "size": { + "fixed": 6 + }, + "symbol": { + "fixed": "img/icons/marker/circle.svg", + "mode": "fixed" + } + } + } + } + ], + "tooltip": { + "mode": "details" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 16, + "w": 12, + "h": 14 + }, + "id": 11 + }, + { + "type": "state-timeline", + "title": "TEL: GPS status", + "description": "Fix type and receiver flags from TELGpsStatus. Orange only when there is no usable fix.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> filter(fn: (r) => r[\"class\"] == \"TELGpsStatus\")\n |> filter(fn: (r) => r[\"_field\"] == \"GpsFixType\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"last\")", + "refId": "A" + }, + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> filter(fn: (r) => r[\"class\"] == \"TELGpsStatus\")\n |> filter(fn: (r) => r[\"_field\"] == \"GpsFixOk\")\n |> aggregateWindow(every: v.windowPeriod, fn: min, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"min\")", + "refId": "B" + }, + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> filter(fn: (r) => r[\"class\"] == \"TELGpsStatus\")\n |> filter(fn: (r) => r[\"_field\"] == \"GpsDiffCorrections\" or r[\"_field\"] == \"GpsUtcTimeValid\" or r[\"_field\"] == \"GpsUtcDateValid\" or r[\"_field\"] == \"GpsUtcTimeResolved\")\n |> aggregateWindow(every: v.windowPeriod, fn: min, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"min\")", + "refId": "C" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "color": { + "mode": "fixed", + "fixedColor": "#8e8e8e" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#8e8e8e", + "value": null + } + ] + }, + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "NO FIX", + "color": "#ff9830", + "index": 0 + }, + "1": { + "text": "DEAD RECKONING", + "color": "#ff9830", + "index": 1 + }, + "2": { + "text": "2D FIX", + "color": "#ff9830", + "index": 2 + }, + "3": { + "text": "3D FIX", + "color": "#8e8e8e", + "index": 3 + }, + "4": { + "text": "GNSS + DR", + "color": "#8e8e8e", + "index": 4 + }, + "5": { + "text": "TIME ONLY", + "color": "#ff9830", + "index": 5 + } + } + }, + { + "type": "range", + "options": { + "from": -1000000000000.0, + "to": 1000000000000.0, + "result": { + "text": "UNKNOWN", + "color": "#ff9830", + "index": 6 + } + } + } + ], + "custom": { + "fillOpacity": 80, + "lineWidth": 0, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "type": "value", + "options": { + "0": { + "text": "NO FIX", + "color": "#ff9830", + "index": 0 + }, + "1": { + "text": "FIX OK", + "color": "#8e8e8e", + "index": 1 + } + } + }, + { + "type": "range", + "options": { + "from": -1000000000000.0, + "to": 1000000000000.0, + "result": { + "text": "UNKNOWN", + "color": "#ff9830", + "index": 2 + } + } + } + ] + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "C" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "type": "value", + "options": { + "0": { + "text": "NO", + "color": "#464c54", + "index": 0 + }, + "1": { + "text": "YES", + "color": "#5794f2", + "index": 1 + } + } + }, + { + "type": "range", + "options": { + "from": -1000000000000.0, + "to": 1000000000000.0, + "result": { + "text": "UNKNOWN", + "color": "#ff9830", + "index": 2 + } + } + } + ] + } + ] + } + ] + }, + "options": { + "showValue": "auto", + "mergeValues": true, + "alignValue": "left", + "rowHeight": 0.85, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 12, + "y": 16, + "w": 12, + "h": 6 + }, + "id": 12 + }, + { + "type": "timeseries", + "title": "TEL: GPS satellites (count)", + "description": "Satellites used in the solution vs heard. Fewer than 4 used cannot give a 3D fix.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> filter(fn: (r) => r[\"class\"] == \"TELGpsStatus\")\n |> filter(fn: (r) => r[\"_field\"] == \"GpsSatsUsed\" or r[\"_field\"] == \"GpsSatsHeard\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"last\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "suffix: sats", + "color": { + "mode": "palette-classic" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#ff1a1a", + "value": null + }, + { + "color": "transparent", + "value": 4 + } + ] + }, + "mappings": [], + "min": 0, + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 0, + "showPoints": "never", + "spanNulls": true, + "axisPlacement": "auto", + "axisLabel": "", + "thresholdsStyle": { + "mode": "dashed+area" + }, + "stacking": { + "mode": "none", + "group": "A" + }, + "lineInterpolation": "linear", + "gradientMode": "none", + "barAlignment": 0, + "scaleDistribution": { + "type": "linear" + }, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "calcs": [ + "lastNotNull" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 30, + "w": 6, + "h": 8 + }, + "id": 13 + }, + { + "type": "timeseries", + "title": "TEL: GPS horizontal accuracy (m)", + "description": "Receiver's horizontal accuracy estimate. Orange above 5 m, red above 20 m.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> filter(fn: (r) => r[\"class\"] == \"TELGpsStatus\")\n |> filter(fn: (r) => r[\"_field\"] == \"GpsHorizAccuracy\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "lengthm", + "color": { + "mode": "palette-classic" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "#ff9830", + "value": 5 + }, + { + "color": "#ff1a1a", + "value": 20 + } + ] + }, + "mappings": [], + "decimals": 1, + "min": 0, + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 0, + "showPoints": "never", + "spanNulls": true, + "axisPlacement": "auto", + "axisLabel": "", + "thresholdsStyle": { + "mode": "dashed+area" + }, + "stacking": { + "mode": "none", + "group": "A" + }, + "lineInterpolation": "linear", + "gradientMode": "none", + "barAlignment": 0, + "scaleDistribution": { + "type": "linear" + }, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "calcs": [ + "lastNotNull" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 6, + "y": 30, + "w": 6, + "h": 8 + }, + "id": 14 + }, + { + "type": "row", + "title": "GPS motion", + "collapsed": false, + "panels": [], + "gridPos": { + "x": 0, + "y": 38, + "w": 24, + "h": 1 + }, + "id": 15 + }, + { + "type": "timeseries", + "title": "TEL: GPS ground speed (km/h)", + "description": "GpsGroundSpeed from the receiver (independent of the motor controller).", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> filter(fn: (r) => r[\"class\"] == \"TELGpsMotion\")\n |> filter(fn: (r) => r[\"_field\"] == \"GpsGroundSpeed\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "velocitykmh", + "color": { + "mode": "palette-classic" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [], + "decimals": 1, + "min": 0, + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 0, + "showPoints": "never", + "spanNulls": true, + "axisPlacement": "auto", + "axisLabel": "", + "thresholdsStyle": { + "mode": "off" + }, + "stacking": { + "mode": "none", + "group": "A" + }, + "lineInterpolation": "linear", + "gradientMode": "none", + "barAlignment": 0, + "scaleDistribution": { + "type": "linear" + }, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "calcs": [ + "lastNotNull" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 39, + "w": 8, + "h": 7 + }, + "id": 16 + }, + { + "type": "timeseries", + "title": "TEL: GPS heading (\u00b0)", + "description": "GpsHeading, 0 = north, clockwise.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> filter(fn: (r) => r[\"class\"] == \"TELGpsMotion\")\n |> filter(fn: (r) => r[\"_field\"] == \"GpsHeading\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"last\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "degree", + "color": { + "mode": "palette-classic" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [], + "decimals": 0, + "min": 0, + "max": 360, + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 0, + "showPoints": "never", + "spanNulls": true, + "axisPlacement": "auto", + "axisLabel": "", + "thresholdsStyle": { + "mode": "off" + }, + "stacking": { + "mode": "none", + "group": "A" + }, + "lineInterpolation": "linear", + "gradientMode": "none", + "barAlignment": 0, + "scaleDistribution": { + "type": "linear" + }, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "calcs": [ + "lastNotNull" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 8, + "y": 39, + "w": 8, + "h": 7 + }, + "id": 17 + }, + { + "type": "timeseries", + "title": "TEL: GPS altitude (m)", + "description": "GpsAltitude above mean sea level.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> filter(fn: (r) => r[\"class\"] == \"TELGpsMotion\")\n |> filter(fn: (r) => r[\"_field\"] == \"GpsAltitude\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "lengthm", + "color": { + "mode": "palette-classic" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [], + "decimals": 1, + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 0, + "showPoints": "never", + "spanNulls": true, + "axisPlacement": "auto", + "axisLabel": "", + "thresholdsStyle": { + "mode": "off" + }, + "stacking": { + "mode": "none", + "group": "A" + }, + "lineInterpolation": "linear", + "gradientMode": "none", + "barAlignment": 0, + "scaleDistribution": { + "type": "linear" + }, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "calcs": [ + "lastNotNull" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 16, + "y": 39, + "w": 8, + "h": 7 + }, + "id": 18 + }, + { + "type": "timeseries", + "title": "TEL: GPS PDOP", + "description": "Position dilution of precision (dimensionless). Under 2.5 is good; orange above 2.5, red above 6.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> filter(fn: (r) => r[\"class\"] == \"TELGpsStatus\")\n |> filter(fn: (r) => r[\"_field\"] == \"GpsPDOP\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "color": { + "mode": "palette-classic" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "#ff9830", + "value": 2.5 + }, + { + "color": "#ff1a1a", + "value": 6 + } + ] + }, + "mappings": [], + "decimals": 2, + "min": 0, + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 0, + "showPoints": "never", + "spanNulls": true, + "axisPlacement": "auto", + "axisLabel": "", + "thresholdsStyle": { + "mode": "dashed+area" + }, + "stacking": { + "mode": "none", + "group": "A" + }, + "lineInterpolation": "linear", + "gradientMode": "none", + "barAlignment": 0, + "scaleDistribution": { + "type": "linear" + }, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "calcs": [ + "lastNotNull" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 46, + "w": 8, + "h": 7 + }, + "id": 19 + }, + { + "type": "stat", + "title": "TEL: GPS UTC time", + "description": "UTC time from the receiver (h, min, s). Check it against GpsUtcTimeValid in GPS status before trusting it.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> filter(fn: (r) => r[\"class\"] == \"TELGpsUtcTime\")\n |> filter(fn: (r) => r[\"_field\"] == \"GpsHour\" or r[\"_field\"] == \"GpsMinute\" or r[\"_field\"] == \"GpsSecond\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"last\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [], + "decimals": 0 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "colorMode": "background_solid", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "textMode": "auto" + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 8, + "y": 46, + "w": 8, + "h": 7 + }, + "id": 20 + }, + { + "type": "timeseries", + "title": "TEL: GPS date", + "description": "UTC date from the receiver.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> filter(fn: (r) => r[\"class\"] == \"TELGpsUtcTime\")\n |> filter(fn: (r) => r[\"_field\"] == \"GpsYear\" or r[\"_field\"] == \"GpsMonth\" or r[\"_field\"] == \"GpsDay\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"last\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "color": { + "mode": "palette-classic" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [], + "decimals": 0, + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 0, + "showPoints": "never", + "spanNulls": true, + "axisPlacement": "auto", + "axisLabel": "", + "thresholdsStyle": { + "mode": "off" + }, + "stacking": { + "mode": "none", + "group": "A" + }, + "lineInterpolation": "linear", + "gradientMode": "none", + "barAlignment": 0, + "scaleDistribution": { + "type": "linear" + }, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "calcs": [ + "lastNotNull" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 16, + "y": 46, + "w": 8, + "h": 7 + }, + "id": 21 + }, + { + "type": "row", + "title": "IMU", + "collapsed": false, + "panels": [], + "gridPos": { + "x": 0, + "y": 53, + "w": 24, + "h": 1 + }, + "id": 22 + }, + { + "type": "timeseries", + "title": "TEL: IMU acceleration (m/s\u00b2)", + "description": "Linear acceleration, X/Y/Z.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> filter(fn: (r) => (r[\"class\"] == \"TELImuAG_X\" and r[\"_field\"] == \"ImuAGXAccel\") or (r[\"class\"] == \"TELImuAGY\" and r[\"_field\"] == \"ImuAGYAccel\") or (r[\"class\"] == \"TELImuAGZ\" and r[\"_field\"] == \"ImuAGZAccel\"))\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "accMS2", + "color": { + "mode": "palette-classic" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [], + "decimals": 2, + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 0, + "showPoints": "never", + "spanNulls": true, + "axisPlacement": "auto", + "axisLabel": "", + "thresholdsStyle": { + "mode": "off" + }, + "stacking": { + "mode": "none", + "group": "A" + }, + "lineInterpolation": "linear", + "gradientMode": "none", + "barAlignment": 0, + "scaleDistribution": { + "type": "linear" + }, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "calcs": [ + "lastNotNull" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 54, + "w": 8, + "h": 8 + }, + "id": 23 + }, + { + "type": "timeseries", + "title": "TEL: IMU angular rate (rad/s)", + "description": "Gyroscope, X/Y/Z.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> filter(fn: (r) => (r[\"class\"] == \"TELImuAG_X\" and r[\"_field\"] == \"ImuAGXGyro\") or (r[\"class\"] == \"TELImuAGY\" and r[\"_field\"] == \"ImuAGYGyro\") or (r[\"class\"] == \"TELImuAGZ\" and r[\"_field\"] == \"ImuAGZGyro\"))\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "rotrads", + "color": { + "mode": "palette-classic" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [], + "decimals": 3, + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 0, + "showPoints": "never", + "spanNulls": true, + "axisPlacement": "auto", + "axisLabel": "", + "thresholdsStyle": { + "mode": "off" + }, + "stacking": { + "mode": "none", + "group": "A" + }, + "lineInterpolation": "linear", + "gradientMode": "none", + "barAlignment": 0, + "scaleDistribution": { + "type": "linear" + }, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "calcs": [ + "lastNotNull" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 8, + "y": 54, + "w": 8, + "h": 8 + }, + "id": 24 + }, + { + "type": "timeseries", + "title": "TEL: IMU magnetic field (\u00b5T)", + "description": "Magnetometer, X/Y/Z.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> filter(fn: (r) => (r[\"class\"] == \"TELImuMX\" and r[\"_field\"] == \"ImuMXMag\") or (r[\"class\"] == \"TELImuMY\" and r[\"_field\"] == \"ImuMYMag\") or (r[\"class\"] == \"TELImuMZ\" and r[\"_field\"] == \"ImuMZMag\"))\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"mean\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "suffix: \u00b5T", + "color": { + "mode": "palette-classic" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + }, + "mappings": [], + "decimals": 1, + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 0, + "showPoints": "never", + "spanNulls": true, + "axisPlacement": "auto", + "axisLabel": "", + "thresholdsStyle": { + "mode": "off" + }, + "stacking": { + "mode": "none", + "group": "A" + }, + "lineInterpolation": "linear", + "gradientMode": "none", + "barAlignment": 0, + "scaleDistribution": { + "type": "linear" + }, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "calcs": [ + "lastNotNull" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 16, + "y": 54, + "w": 8, + "h": 8 + }, + "id": 25 + }, + { + "type": "row", + "title": "Raw / diagnostic (bench, bug reports)", + "collapsed": true, + "panels": [ + { + "type": "table", + "title": "TEL: All signals (latest raw values)", + "description": "Every signal this board sent in the time range, decoded with the DBC: latest value and when it arrived. Use for bench checks and when filing a bug.", + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "P951FEA4DE68E13C5" + }, + "query": "from(bucket: \"CAN_prod\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"TEL\")\n |> last()\n |> toFloat()\n |> keep(columns: [\"_time\", \"class\", \"_field\", \"_value\"])\n |> group()\n |> sort(columns: [\"class\", \"_field\"])\n |> rename(columns: {class: \"Message\", _field: \"Signal\", _value: \"Latest value\", _time: \"Received\"})\n |> yield(name: \"raw\")", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "none", + "custom": { + "align": "auto", + "filterable": true + }, + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#8e8e8e", + "value": null + } + ] + }, + "mappings": [] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Received" + }, + "properties": [ + { + "id": "unit", + "value": "dateTimeAsLocal" + } + ] + } + ] + }, + "options": { + "showHeader": true, + "cellHeight": "sm", + "footer": { + "show": false + }, + "sortBy": [ + { + "displayName": "Message", + "desc": false + } + ] + }, + "pluginVersion": "9.5.2", + "gridPos": { + "x": 0, + "y": 63, + "w": 24, + "h": 14 + }, + "id": 27 + } + ], + "gridPos": { + "x": 0, + "y": 62, + "w": 24, + "h": 1 + }, + "id": 26 + } + ] +} diff --git a/dbc/cascadia.dbc b/dbc/cascadia.dbc index 081cb7c1..2382568e 100644 --- a/dbc/cascadia.dbc +++ b/dbc/cascadia.dbc @@ -470,6 +470,10 @@ BO_ 1025 MotorCommand: 5 DRD SG_ EcoModeOn : 33|1@1+ (1,0) [0|1] "" MDI SG_ SpeedUnits : 34|1@1+ (1,0) [0|1] "" STR +BO_ 1026 DRDMCUHealth: 4 DRD + SG_ DRDMCUSupplyVoltage : 0|16@1+ (1,0) [0|4000] "mV" Vector__XXX + SG_ DRDMCUTemperature : 16|16@1- (1,0) [-40|125] "degC" Vector__XXX + BO_ 1027 DRDDiagnostics: 8 DRD SG_ RawADC1 : 0|16@1+ (1,0) [0|65535] "" Vector__XXX SG_ RawADC2 : 16|16@1+ (1,0) [0|65535] "" Vector__XXX @@ -479,6 +483,8 @@ BO_ 1027 DRDDiagnostics: 8 DRD SG_ ADCMismatch : 35|1@1+ (1,0) [0|1] "" Vector__XXX SG_ DRDWatchdogReset : 36|1@1+ (1,0) [0|1] "" Vector__XXX SG_ MotorCommFault : 37|1@1+ (1,0) [0|1] "" Vector__XXX + SG_ DRDMCUOverTemp : 38|1@1+ (1,0) [0|1] "" Vector__XXX + SG_ DRDMCUUnderVoltage : 39|1@1+ (1,0) [0|1] "" Vector__XXX SG_ DriveState : 40|8@1+ (1,0) [0|255] "" Vector__XXX SG_ SpeedTimeout : 48|1@1+ (1,0) [0|1] "" Vector__XXX SG_ DriveStateTimeout : 49|1@1+ (1,0) [0|1] "" Vector__XXX @@ -498,10 +504,13 @@ BO_ 1281 MDIDiagnostics: 1 MDI SG_ MDIWatchdogReset : 0|1@1+ (1,0) [0|1] "" Vector__XXX SG_ MDIVoltageOverThreshold : 1|1@1+ (1,0) [0|1] "" Vector__XXX SG_ MDIMotorOverTemp : 2|1@1+ (1,0) [0|1] "" Vector__XXX + SG_ MDIRTDFault : 3|1@1+ (1,0) [0|1] "" Vector__XXX + SG_ MDIRTDCommError : 4|1@1+ (1,0) [0|1] "" Vector__XXX -BO_ 1282 MDIMotorTemperature: 5 MDI +BO_ 1282 MDIMotorTemperature: 6 MDI SG_ RTDReadSuccess : 0|1@1+ (1,0) [0|1] "" Vector__XXX SG_ MotorTemperature : 8|32@1- (1,0) [-100|300] "degC" Vector__XXX + SG_ RTDFaultFlags : 40|8@1+ (1,0) [0|255] "" Vector__XXX BO_ 1408 STRDRDControl: 8 STR SG_ STRRegenEnabled : 0|1@1+ (1,0) [0|1] "" Vector__XXX @@ -521,6 +530,14 @@ BO_ 1409 STRTimeSinceBootup: 4 STR BO_ 1410 STRDiagnostics: 1 STR SG_ STRWatchdogReset : 0|1@1+ (1,0) [0|1] "" Vector__XXX +BO_ 1425 STRMCUHealth: 4 STR + SG_ STRMCUVoltage : 0|16@1+ (1,0) [0|65535] "mV" Vector__XXX + SG_ STRMCUTemperature : 16|16@1- (1,0) [-40|125] "degC" Vector__XXX + +BO_ 1426 STRFlags: 1 STR + SG_ STRMCUUnderVoltage : 0|1@1+ (1,0) [0|1] "" Vector__XXX + SG_ STRMCUOverTemp : 1|1@1+ (1,0) [0|1] "" Vector__XXX + BO_ 1696 MPPTInputMeasurementsA: 8 MPPT SG_ InputVoltageA : 0|32@1- (1,0) [-3.4e+38|3.4e+38] "V" Vector__XXX SG_ InputCurrentA : 32|32@1- (1,0) [-3.4e+38|3.4e+38] "A" Vector__XXX @@ -758,6 +775,8 @@ BO_ 1873 TELDiagnostics: 1 TEL SG_ IMUWriteFail : 2|1@1+ (1,0) [0|1] "" Vector__XXX SG_ GPSReadFail : 3|1@1+ (1,0) [0|1] "" Vector__XXX SG_ GPSWriteFail : 4|1@1+ (1,0) [0|1] "" Vector__XXX + SG_ MCUOvertemp : 5|1@1+ (1,0) [0|1] "" Vector__XXX + SG_ MCUUndervoltage : 6|1@1+ (1,0) [0|1] "" Vector__XXX BO_ 1874 TELImuAG_X: 8 TEL SG_ ImuAGXAccel : 0|32@1- (1,0) [-3.4e+38|3.4e+38] "m/s^2" Vector__XXX @@ -812,8 +831,9 @@ BO_ 1883 TELGpsUtcTime: 6 TEL BO_ 1891 CANBusLoad: 1 TEL SG_ BusLoad : 0|8@1+ (1,0) [0|100] "%" Vector__XXX -BO_ 1892 CPULoad: 4 TEL - SG_ CPULoad : 0|32@1- (1,0) [-3.4e+38|3.4e+38] "" Vector__XXX +BO_ 1892 TELMCUHealth: 4 TEL + SG_ MCU_Voltage : 0|16@1+ (1,0) [0|65535] "mV" Vector__XXX + SG_ MCU_Temperature : 16|16@1- (1,0) [-40|125] "degC" Vector__XXX BO_ 2290418213 Mitsuba_Frame0: 8 MC SG_ BatteryVoltage : 0|10@1+ (0.5,0) [0|511.5] "V" DRD @@ -931,6 +951,17 @@ CM_ BO_ 932 "Firmware branch ID"; CM_ BO_ 1025 "Motor command to MDI. Also sent immediately when the brake is pressed."; CM_ SG_ 1027 SOC "SOC_MAX is 1.04 so values up to 104 are possible"; CM_ SG_ 1281 MDIMotorOverTemp "Set at 100 degC, cleared at 95 degC"; +CM_ SG_ 1281 MDIVoltageOverThreshold "Not set by current MDI firmware (always 0)"; +CM_ SG_ 1281 MDIRTDFault "1 = RTD fault latched: +1 per faulted read, -1 per clean read, sets at 5, clears at 0"; +CM_ SG_ 1281 MDIRTDCommError "1 = SPI error, or MAX31865 missing/reset"; +CM_ SG_ 1282 RTDReadSuccess "1 = valid read, 0 = fault/HAL error"; +CM_ SG_ 1282 RTDFaultFlags "MAX31865 fault register: 0x80 = RTD high, 0x40 = RTD low, 0x04 = OV/UV, 0 = none"; +CM_ SG_ 1026 DRDMCUSupplyVoltage "0 = read failed"; +CM_ SG_ 1027 DRDMCUOverTemp "1 = fault: DRD MCU over temperature"; +CM_ SG_ 1027 DRDMCUUnderVoltage "1 = fault: DRD MCU under voltage"; +CM_ SG_ 1425 STRMCUVoltage "0 = read failed"; +CM_ SG_ 1426 STRMCUUnderVoltage "1 = fault: STR MCU Vdd below 2400 mV"; +CM_ SG_ 1426 STRMCUOverTemp "1 = fault: STR MCU above 105 degC"; CM_ BO_ 1408 "Steering inputs. Firmware sends 8 bytes; bytes 4-7 are 0."; CM_ BO_ 1410 "Not sent by current STR firmware (kept for existing dashboards)"; CM_ BO_ 1704 "Command to MPPT A"; @@ -949,7 +980,7 @@ CM_ BO_ 1872 "HVC uses this as the TEL heartbeat"; CM_ BO_ 1880 "Only sent with a valid fix"; CM_ BO_ 1881 "Only sent with a valid fix"; CM_ BO_ 1883 "Only sent with a valid UTC date and time"; -CM_ BO_ 1892 "Not sent by current TEL firmware"; +CM_ BO_ 1892 "TEL MCU health: Vdd and die temperature, 10 Hz (firmware tel/app/diagnostics.c)"; CM_ BO_ 2290418213 "Sent in response to MitsubaDataRequest"; CM_ SG_ 2290418213 MotorRotatingSpeed "rpm x 0.106688 (0.283 m wheel radius). Raw value is rpm."; CM_ SG_ 2290418213 PWMDuty "Only valid in PWM control mode"; @@ -1000,6 +1031,7 @@ BA_ "GenMsgCycleTime" BO_ 930 500; BA_ "GenMsgCycleTime" BO_ 931 1000; BA_ "GenMsgCycleTime" BO_ 932 1000; BA_ "GenMsgCycleTime" BO_ 1025 25; +BA_ "GenMsgCycleTime" BO_ 1026 1000; BA_ "GenMsgCycleTime" BO_ 1027 100; BA_ "GenMsgCycleTime" BO_ 1028 1000; BA_ "GenMsgCycleTime" BO_ 1280 1000; @@ -1007,6 +1039,8 @@ BA_ "GenMsgCycleTime" BO_ 1281 1000; BA_ "GenMsgCycleTime" BO_ 1282 1000; BA_ "GenMsgCycleTime" BO_ 1408 100; BA_ "GenMsgCycleTime" BO_ 1409 1000; +BA_ "GenMsgCycleTime" BO_ 1425 100; +BA_ "GenMsgCycleTime" BO_ 1426 100; BA_ "GenMsgCycleTime" BO_ 1872 1000; BA_ "GenMsgCycleTime" BO_ 1873 100; BA_ "GenMsgCycleTime" BO_ 1874 100; @@ -1020,6 +1054,7 @@ BA_ "GenMsgCycleTime" BO_ 1881 1000; BA_ "GenMsgCycleTime" BO_ 1882 1000; BA_ "GenMsgCycleTime" BO_ 1883 1000; BA_ "GenMsgCycleTime" BO_ 1891 100; +BA_ "GenMsgCycleTime" BO_ 1892 100; BA_ "VFrameFormat" BO_ 2290418213 1; BA_ "GenMsgCycleTime" BO_ 2290418213 100; BA_ "VFrameFormat" BO_ 2291466789 1; @@ -1034,6 +1069,7 @@ VAL_ 1025 Direction 1 "Forward" 0 "Reverse"; VAL_ 1025 EcoModeOn 1 "Eco" 0 "Power"; VAL_ 1025 SpeedUnits 1 "KPH" 0 "MPH"; VAL_ 1027 DriveState 4 "REVERSE" 3 "PARK" 2 "CRUISE" 1 "FORWARD" 0 "INVALID"; +VAL_ 1282 RTDFaultFlags 128 "RTD high" 64 "RTD low" 4 "OV/UV" 0 "None"; VAL_ 1882 GpsFixType 5 "TimeOnly" 4 "GnssDeadReckoning" 3 "Fix3D" 2 "Fix2D" 1 "DeadReckoning" 0 "NoFix"; VAL_ 2290418213 BatteryCurrentDirection 1 "Negative" 0 "Positive"; VAL_ 2291466789 PowerMode 1 "Power" 0 "Eco"; @@ -1122,4 +1158,3 @@ SIG_VALTYPE_ 1876 ImuAGZGyro : 1; SIG_VALTYPE_ 1877 ImuMXMag : 1; SIG_VALTYPE_ 1878 ImuMYMag : 1; SIG_VALTYPE_ 1879 ImuMZMag : 1; -SIG_VALTYPE_ 1892 CPULoad : 1; diff --git a/images/cascadia_dashboards/cascadia_sim_panel.png b/images/cascadia_dashboards/cascadia_sim_panel.png new file mode 100644 index 00000000..d88b7a37 Binary files /dev/null and b/images/cascadia_dashboards/cascadia_sim_panel.png differ diff --git a/images/cascadia_dashboards/drd_fault_and_warning.png b/images/cascadia_dashboards/drd_fault_and_warning.png new file mode 100644 index 00000000..a86ea376 Binary files /dev/null and b/images/cascadia_dashboards/drd_fault_and_warning.png differ diff --git a/images/cascadia_dashboards/drd_normal.png b/images/cascadia_dashboards/drd_normal.png new file mode 100644 index 00000000..f24bc354 Binary files /dev/null and b/images/cascadia_dashboards/drd_normal.png differ diff --git a/images/cascadia_dashboards/drd_raw_diagnostic.png b/images/cascadia_dashboards/drd_raw_diagnostic.png new file mode 100644 index 00000000..644bea45 Binary files /dev/null and b/images/cascadia_dashboards/drd_raw_diagnostic.png differ diff --git a/images/cascadia_dashboards/mdi_normal.png b/images/cascadia_dashboards/mdi_normal.png new file mode 100644 index 00000000..4d403d8f Binary files /dev/null and b/images/cascadia_dashboards/mdi_normal.png differ diff --git a/images/cascadia_dashboards/mdi_overtemp.png b/images/cascadia_dashboards/mdi_overtemp.png new file mode 100644 index 00000000..a49425cb Binary files /dev/null and b/images/cascadia_dashboards/mdi_overtemp.png differ diff --git a/images/cascadia_dashboards/str_normal.png b/images/cascadia_dashboards/str_normal.png new file mode 100644 index 00000000..07ed1f9a Binary files /dev/null and b/images/cascadia_dashboards/str_normal.png differ diff --git a/images/cascadia_dashboards/tel_gps_warning.png b/images/cascadia_dashboards/tel_gps_warning.png new file mode 100644 index 00000000..fbe6dc7a Binary files /dev/null and b/images/cascadia_dashboards/tel_gps_warning.png differ diff --git a/images/cascadia_dashboards/tel_normal.png b/images/cascadia_dashboards/tel_normal.png new file mode 100644 index 00000000..e3020830 Binary files /dev/null and b/images/cascadia_dashboards/tel_normal.png differ diff --git a/tools/cascadia_sim/README.md b/tools/cascadia_sim/README.md new file mode 100644 index 00000000..28ca92ef --- /dev/null +++ b/tools/cascadia_sim/README.md @@ -0,0 +1,118 @@ +# Cascadia data simulator + +Generates realistic Cascadia CAN data for the **TEL, MDI, DRD and STR** boards and writes it to your +local InfluxDB. Use it to look at the Cascadia Grafana dashboards without the car. It sends the same +frame IDs, rates and fault logic as `firmware_v4`. Each frame is encoded with `dbc/cascadia.dbc` and +decoded back, with enum signals stored as raw ints (the dashboards map them back to names). Points +are written with sunlink's layout: measurement = sender, field = signal, tags `car` and `class`. + +A control panel lets you change what's happening while it runs: drive state, throttle, GPS fix, +temperatures, supply voltages, faults and so on. Presets jump straight to common cases. + +## 1. Install + +You need the normal sunlink setup: Docker running InfluxDB and Grafana, the `environment` venv, and a +`.env` file in the sunlink root. If you haven't done that yet, follow the main [README](../../README.md) +(`MacOS_setup.sh` on Mac, `setup.sh` on Linux). + +Then, from the sunlink root: + +```bash +source environment/bin/activate +pip install -r requirements.txt # Mac: requirements_MacOS.txt +``` + +The simulator only uses `cantools`, `influxdb-client` and `python-dotenv` from that list. The Tk +window (`--tk`) also needs a Python with Tk support. Most installs have it. If yours doesn't, use the +browser panel. + +Check that: + +- the containers are up: `docker compose up -d`. InfluxDB should be on `localhost:8086` and Grafana + on `localhost:3000`. +- `.env` contains `INFLUX_TOKEN` and `INFLUX_ORG`. The setup script writes both. +- the `CAN_prod` bucket exists. The setup script creates it. + +The Cascadia dashboards (`dashboards/PROD/PROD_*_Cascadia.json`) are mounted into Grafana +automatically. If they don't appear, run `docker compose restart grafana`. + +## 2. Run + +Run every command from the sunlink root with the venv active. + +```bash +python tools/cascadia_sim/cascadia_sim.py +``` + +This starts writing data and opens the control panel at . The panel has +scenario buttons, toggles and sliders for every input, a live readout, and links to the four +dashboards. Stop it with Ctrl+C. + +Other ways to run it: + +```bash +python tools/cascadia_sim/cascadia_sim.py --tk # same controls in a Tk window +python tools/cascadia_sim/cascadia_sim.py --list-scenarios # show the presets +python tools/cascadia_sim/cascadia_sim.py --scenario gps_lost --headless --duration 120 # no UI, 2 minutes +``` + +Then open a dashboard. Set the time range to the last 5 minutes and refresh to 1 s: + +| Board | URL | +|---|---| +| TEL | | +| MDI | | +| DRD | | +| STR | | + +### Options + +| Flag | Default | What it does | +|---|---|---| +| `--scenario NAME` | `normal` | Preset to start in (see below) | +| `--headless` | off | No window or panel. Runs the scenario and prints how many frames were written | +| `--duration SECONDS` | forever | How long a headless run lasts | +| `--tk` | off | Tk window instead of the browser panel | +| `--port N` | `8090` | Port for the browser panel (bound to 127.0.0.1 only) | +| `--url URL` | `http://localhost:8086` | InfluxDB to write to | +| `--bucket NAME` | `CAN_prod` | Bucket to write to | +| `--dbc PATH` | `dbc/cascadia.dbc` | DBC used to encode and decode | +| `--allow-remote` | off | Allow writing to an InfluxDB that isn't on localhost | + +### Scenarios + +| Name | Situation | +|---|---| +| `normal` | Normal driving: 3D fix, everything in band | +| `parked` | Parked on the bench, no throttle | +| `cruise` | Cruise at 60 km/h | +| `gps_lost` | GPS lost (no fix, few satellites) | +| `motor_overheat` | Hard driving, motor temperature climbs past 100 °C | +| `rtd_failure` | Motor RTD open circuit | +| `drd_mcu_hot` | DRD MCU over temperature | +| `str_brownout` | STR supply sagging below 2400 mV | +| `tel_link_down` | TEL stops sending (radio/CAN down) | +| `drd_reset` | DRD watchdog reset | +| `pedal_mismatch` | Throttle sensors disagree | +| `timeouts` | DRD lost its cyclic inputs (BMS/MC silent) | +| `bus_overload` | CAN bus load 92 % | + +## Safety + +- **Local only by default.** The simulator refuses to write anywhere other than `localhost` unless you + pass `--allow-remote`. Don't point it at a shared or race InfluxDB. +- **Simulated points are tagged.** Every point it writes has the tag `source=cascadia_sim`. + **Reset everything to normal** in the panel restores normal inputs, clears latched faults, and + deletes only points with that tag. Real data imported into the same bucket is left alone. The + dashboards ignore the tag, so simulated and real data look the same on them. +- The `car` tag comes from `CAR_NAME` in `LINK_CONSTANTS.py`. + +## Troubleshooting + +| Problem | Fix | +|---|---| +| `ModuleNotFoundError: No module named 'cantools'` | The venv isn't active, or the requirements aren't installed (step 1) | +| `KeyError: 'INFLUX_TOKEN'` | `.env` is missing or incomplete. Re-run the setup script | +| `refusing to write simulated data to ...` | `--url` isn't localhost. Add `--allow-remote` only if you really mean it | +| Dashboards are empty | Check the time range is recent, the bucket is `CAN_prod`, and the panel's frame counter is going up | +| Port 8090 already in use | Pass `--port 8091` (or any free port) | diff --git a/tools/cascadia_sim/cascadia_sim.py b/tools/cascadia_sim/cascadia_sim.py new file mode 100644 index 00000000..1e9b4b6b --- /dev/null +++ b/tools/cascadia_sim/cascadia_sim.py @@ -0,0 +1,503 @@ +#!/usr/bin/env python3 +""" +cascadia_sim.py: adjustable, realistic Cascadia CAN data for TEL, MDI, DRD and STR. + +Builds the frames each board sends (same IDs, rates and fault logic as firmware_v4), +encodes them with the DBC, decodes them back (enums as raw ints), and writes them to +InfluxDB with sunlink's layout (measurement = sender, field = signal, tags car/class). +A small window lets you change the situation while it runs; presets jump to common cases. + + python tools/cascadia_sim/cascadia_sim.py # browser control panel (localhost:8090) + python tools/cascadia_sim/cascadia_sim.py --tk # same controls in a Tk window + python tools/cascadia_sim/cascadia_sim.py --scenario gps_lost --headless --duration 120 + python tools/cascadia_sim/cascadia_sim.py --list-scenarios + +Reads INFLUX_TOKEN / INFLUX_ORG from ./.env. Writes only to localhost unless --allow-remote. +""" +from __future__ import annotations + +import argparse +import math +import random +import sys +import threading +import time +from pathlib import Path +from urllib.parse import urlparse + +import cantools + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +try: + from LINK_CONSTANTS import CAR_NAME +except Exception: # noqa: BLE001 + CAR_NAME = "Brightside" + +# Firmware limits (firmware_v4 origin/main, 2026-10-31) +UV_MV = {"TEL": 2000, "DRD": 2000, "STR": 2400} +OT_C = {"TEL": 125, "DRD": 125, "STR": 105} +MOTOR_OT_SET, MOTOR_OT_CLEAR = 100, 95 +MOTOR_MAX_C = 140.0 # simulator ceiling for the motor model + +DRIVE_STATES = {"PARK": 3, "FORWARD": 1, "REVERSE": 4, "CRUISE": 2} +GPS_FIX = {"NoFix": 0, "DeadReckoning": 1, "Fix2D": 2, "Fix3D": 3, "GnssDeadReckoning": 4, "TimeOnly": 5} +RTD_FLAGS = {"none": 0, "RTD high (open)": 128, "RTD low (short)": 64, "OV/UV": 4} + +TRACK_CENTER = (49.2606, -123.2460) # generic oval near UBC; ~2.2 km lap +TRACK_A, TRACK_B = 450.0, 220.0 # semi-axes in metres + + +class State: + """Everything you can adjust. The GUI and the presets only touch this.""" + + def __init__(self): + self.link = {b: True for b in ("TEL", "MDI", "DRD", "STR")} + self.vdd = {"TEL": 3300.0, "DRD": 3300.0, "STR": 3300.0} + self.mcu_temp = {"TEL": 38.0, "DRD": 34.0, "STR": 31.0} + self.watchdog = {b: False for b in ("TEL", "MDI", "DRD", "STR")} + # driving + self.drive_state = "FORWARD" + self.throttle = 35.0 # % pedal + self.brake = False + self.regen_enabled = True + self.eco = False + self.kph = True + self.soc = 82.0 + self.cruise_set = 50 + # DRD diagnostics + self.adc_mismatch = False + self.adc_out_of_range = False + self.motor_comm_fault = False + self.timeouts = {k: False for k in ("Speed", "DriveState", "SOC", "Voltage", "Current", "Accel")} + # MDI + self.motor_heat = 1.0 # heating factor (1 = normal) + self.rtd_ok = True + self.rtd_flags = "none" + # TEL + self.gps_fix = "Fix3D" + self.sats = 14 + self.gps_read_fail = False + self.imu_read_fail = False + self.bus_load = 35.0 + # STR + self.left = self.right = self.horn = self.ptt = False + self.cruise_inc = self.cruise_dec = False + + +SCENARIOS = { + "normal": ("Normal driving: 3D fix, everything in band", {}), + "parked": ("Parked on the bench, no throttle", {"drive_state": "PARK", "throttle": 0.0}), + "cruise": ("Cruise at 60 km/h", {"drive_state": "CRUISE", "cruise_set": 60}), + "gps_lost": ("GPS lost (no fix, few satellites)", {"gps_fix": "NoFix", "sats": 2}), + "motor_overheat": ("Hard driving, motor temperature climbs past 100 °C", {"throttle": 90.0, "motor_heat": 6.0}), + "rtd_failure": ("Motor RTD open circuit", {"rtd_ok": False, "rtd_flags": "RTD high (open)"}), + "drd_mcu_hot": ("DRD MCU over temperature", {"mcu_temp.DRD": 128.0}), + "str_brownout": ("STR supply sagging below 2400 mV", {"vdd.STR": 2300.0}), + "tel_link_down": ("TEL stops sending (radio/CAN down)", {"link.TEL": False}), + "drd_reset": ("DRD watchdog reset", {"watchdog.DRD": True, "_reset": "DRD"}), + "pedal_mismatch": ("Throttle sensors disagree", {"adc_mismatch": True}), + "timeouts": ("DRD lost its cyclic inputs (BMS/MC silent)", + {"timeouts": {k: True for k in ("Speed", "DriveState", "SOC", "Voltage", "Current", "Accel")}}), + "bus_overload": ("CAN bus load 92 %", {"bus_load": 92.0}), +} + + +def apply_scenario(sim: "Sim", name: str): + sim.state = State() + sim.motor_temp = 45.0 + for key, val in SCENARIOS[name][1].items(): + if key == "_reset": + sim.reset_board(val) + elif "." in key: + attr, sub = key.split(".") + getattr(sim.state, attr)[sub] = val + else: + setattr(sim.state, key, val) + + +class Sim: + def __init__(self, dbc: str, writer, clear_history=None): + self.db = cantools.database.load_file(dbc) + self.write = writer + self.clear_history = clear_history + self.lock = threading.Lock() + self.state = State() + self.boot = {b: time.time() - random.uniform(300, 900) for b in ("TEL", "MDI", "DRD", "STR")} + self.speed = 0.0 # m/s + self.s = 0.0 # distance along track, m + self.motor_temp = 45.0 + self.motor_ot = False + self.rtd_bad = 0 + self.tick = 0 + self.frames = 0 + self.stop = threading.Event() + + def reset_board(self, b): + self.boot[b] = time.time() + + def reset_all(self): + """Back to normal and forget the past: normal inputs, latches cleared, and the simulated + TEL/MDI/DRD/STR history (points tagged source=cascadia_sim) deleted, so no old faults show as + CLEARED. Real data in the same bucket is left alone.""" + with self.lock: + apply_scenario(self, "normal") + self.scenario = "normal" + self.motor_ot = False + self.rtd_bad = 0 + if self.clear_history: + self.clear_history() + + # ------------------------------------------------------------------ physics + def step(self, dt): + st = self.state + target = 0.0 + if st.drive_state == "CRUISE": + target = st.cruise_set / 3.6 + elif st.drive_state in ("FORWARD", "REVERSE") and not st.brake: + target = (st.throttle / 100.0) * (80 if st.eco else 100) / 3.6 + tau = 1.5 if st.brake else 6.0 + accel = (target - self.speed) / tau + self.prev_speed = self.speed + self.speed = max(0.0, self.speed + accel * dt) + self.accel = accel + self.s += self.speed * dt + power = (st.throttle / 100.0) * (self.speed / 25.0) if st.drive_state != "PARK" else 0.0 + heat_in = 4.0 * st.motor_heat * power + self.motor_temp += (heat_in - 0.012 * (self.motor_temp - 25.0)) * dt + self.motor_temp = min(self.motor_temp, MOTOR_MAX_C) # keep runaway heating plausible + if not self.motor_ot and self.motor_temp >= MOTOR_OT_SET: + self.motor_ot = True + elif self.motor_ot and self.motor_temp <= MOTOR_OT_CLEAR: + self.motor_ot = False + st.soc = max(0.0, st.soc - power * 0.002 * dt) + st.cruise_set = max(0, min(150, st.cruise_set + (1 if st.cruise_inc else 0) - (1 if st.cruise_dec else 0))) + + def position(self): + # ellipse parametrised by arc length (approximate) + perim = math.pi * (3 * (TRACK_A + TRACK_B) - math.sqrt((3 * TRACK_A + TRACK_B) * (TRACK_A + 3 * TRACK_B))) + th = 2 * math.pi * (self.s % perim) / perim + x, y = TRACK_A * math.cos(th), TRACK_B * math.sin(th) + dx, dy = -TRACK_A * math.sin(th), TRACK_B * math.cos(th) + heading = (math.degrees(math.atan2(dx, dy)) + 360) % 360 # 0 = north, clockwise + lat = TRACK_CENTER[0] + y / 111_320 + lon = TRACK_CENTER[1] + x / (111_320 * math.cos(math.radians(TRACK_CENTER[0]))) + # curvature radius for lateral accel / yaw rate + r = ((dx * dx + dy * dy) ** 1.5) / abs(dx * (-TRACK_B * math.sin(th)) - dy * (-TRACK_A * math.cos(th))) + return lat, lon, heading, r + + # ------------------------------------------------------------------ frames + def frames_now(self): + st, t, now = self.state, self.tick, time.time() + n = lambda v, s: v + random.gauss(0, s) # noqa: E731 + out = [] + every_s = t % 10 == 0 # 1 Hz at 10 Hz tick + + def mcu(b): + v, c = n(st.vdd[b], 4), n(st.mcu_temp[b], 0.3) + return round(v), round(c), int(c > OT_C[b]), int(v < UV_MV[b]) + + # ---- TEL + if st.link["TEL"]: + v, c, ot, uv = mcu("TEL") + lat, lon, heading, r = self.position() + fix = GPS_FIX[st.gps_fix] + # a receiver can't hold a fix it doesn't have satellites for + if st.sats < 3 and fix in (2, 3, 4): + fix = 0 + elif st.sats < 4 and fix in (3, 4): + fix = 2 + out += [("TELDiagnostics", {"TELWatchdogReset": int(st.watchdog["TEL"]), "IMUReadFail": int(st.imu_read_fail), + "IMUWriteFail": 0, "GPSReadFail": int(st.gps_read_fail), "GPSWriteFail": 0, + "MCUOvertemp": ot, "MCUUndervoltage": uv}), + ("TELMCUHealth", {"MCU_Voltage": v, "MCU_Temperature": c}), + ("CANBusLoad", {"BusLoad": max(0, min(100, round(n(st.bus_load, 2))))})] + if not st.imu_read_fail: + lat_acc = self.speed ** 2 / r if r else 0.0 + yaw = self.speed / r if r else 0.0 + hd = math.radians(heading) + out += [("TELImuAG_X", {"ImuAGXAccel": n(self.accel, 0.08), "ImuAGXGyro": n(0, 0.01)}), + ("TELImuAGY", {"ImuAGYAccel": n(lat_acc, 0.08), "ImuAGYGyro": n(0, 0.01)}), + ("TELImuAGZ", {"ImuAGZAccel": n(9.81, 0.05), "ImuAGZGyro": n(yaw, 0.005)}), + ("TELImuMX", {"ImuMXMag": n(18 * math.cos(hd), 0.4)}), + ("TELImuMY", {"ImuMYMag": n(-18 * math.sin(hd), 0.4)}), + ("TELImuMZ", {"ImuMZMag": n(-49, 0.4)})] + if every_s: + out.append(("TELTimeSinceBootup", {"TELTimeSinceBootUp": int(now - self.boot["TEL"])})) + sats = st.sats + pdop = max(0.8, 12 / max(sats, 1)) + acc = 0.6 * pdop if fix in (3, 4) else 25.0 if fix == 2 else 0 + gm = time.gmtime(now) + out += [("TELGpsStatus", {"GpsFixType": fix, "GpsFixOk": int(fix in (2, 3, 4)), "GpsDiffCorrections": 0, + "GpsUtcDateValid": int(fix != 0), "GpsUtcTimeValid": int(fix != 0), + "GpsUtcTimeResolved": int(fix != 0), "GpsSatsUsed": sats if fix else 0, + "GpsSatsHeard": sats + 6, "GpsPDOP": round(pdop, 2) if fix else 99.99, + "GpsHorizAccuracy": round(acc, 2) if fix else 655.35}), + ("TELGpsUtcTime", {"GpsSecond": gm.tm_sec, "GpsMinute": gm.tm_min, "GpsHour": gm.tm_hour, + "GpsDay": gm.tm_mday, "GpsMonth": gm.tm_mon, "GpsYear": gm.tm_year})] + if fix in (2, 3, 4): # firmware only sends position/motion with a fix + out += [("TELGpsPosition", {"GpsLatitude": lat + random.gauss(0, 2e-6), "GpsLongitude": lon + random.gauss(0, 2e-6)}), + ("TELGpsMotion", {"GpsAltitude": n(82, 0.3), "GpsGroundSpeed": max(0, n(self.speed * 3.6, 0.2)), + "GpsHeading": round(heading, 2)})] + # ---- MDI (1 Hz superloop) + if st.link["MDI"] and every_s: + if st.rtd_ok: + self.rtd_bad = max(0, self.rtd_bad - 1) + else: + self.rtd_bad = min(5, self.rtd_bad + 1) + out += [("MDITimeSinceBootup", {"MDITimeSinceBootUp": int(now - self.boot["MDI"])}), + ("MDIDiagnostics", {"MDIWatchdogReset": int(st.watchdog["MDI"]), "MDIVoltageOverThreshold": 0, + "MDIMotorOverTemp": int(self.motor_ot), "MDIRTDFault": int(self.rtd_bad >= 5), + "MDIRTDCommError": 0}), + ("MDIMotorTemperature", {"RTDReadSuccess": int(st.rtd_ok), + "MotorTemperature": round(n(self.motor_temp, 0.2)) if st.rtd_ok else 0, + "RTDFaultFlags": 0 if st.rtd_ok else RTD_FLAGS[st.rtd_flags]})] + # ---- DRD + if st.link["DRD"]: + ds = DRIVE_STATES[st.drive_state] + moving = st.drive_state in ("FORWARD", "REVERSE", "CRUISE") + thr = 0 if (st.brake or not moving) else round(1023 * st.throttle / 100) + adc1 = round(600 + 2800 * st.throttle / 100 + random.gauss(0, 6)) + adc2 = adc1 + (900 if st.adc_mismatch else round(random.gauss(0, 6))) + if st.adc_out_of_range: + adc1 = 4095 + out += [("MotorCommand", {"ThrottleDAC": min(thr, 1023), "RegenDAC": 1023 if (st.brake and st.regen_enabled) else 0, + "Direction": 0 if st.drive_state == "REVERSE" else 1, "EcoModeOn": int(st.eco), + "SpeedUnits": int(st.kph)}), + ("DRDDiagnostics", {"RawADC1": max(0, min(4095, adc1)), "RawADC2": max(0, min(4095, adc2)), + "BrakePressed": int(st.brake), "RegenEnabled": int(st.regen_enabled), + "ADCOutOfRange": int(st.adc_out_of_range), "ADCMismatch": int(st.adc_mismatch), + "DRDWatchdogReset": int(st.watchdog["DRD"]), "MotorCommFault": int(st.motor_comm_fault), + "DRDMCUOverTemp": mcu("DRD")[2], "DRDMCUUnderVoltage": mcu("DRD")[3], "DriveState": ds, + **{f"{k}Timeout": int(v) for k, v in st.timeouts.items()}, + "SOC": round(st.soc)})] + if every_s: + v, c, _, _ = mcu("DRD") + out += [("DRDMCUHealth", {"DRDMCUSupplyVoltage": v, "DRDMCUTemperature": c}), + ("DRDTimeSinceBootup", {"DRDTimeSinceBootUp": int(now - self.boot["DRD"])})] + # ---- STR + if st.link["STR"]: + blink = int(now * 2) % 2 # turn signals blink at 1 Hz + v, c, ot, uv = mcu("STR") + out += [("STRDRDControl", {"STRRegenEnabled": int(st.regen_enabled), "STRCruiseEnabled": int(st.drive_state == "CRUISE"), + "STRLCDScreenNextPage": 0, "STRLeftTurnSignalEnabled": int(st.left and blink), + "STRRightTurnSignalEnabled": int(st.right and blink), "STRHornEnabled": int(st.horn), + "STRPushToTalkEnabled": int(st.ptt), "STRCruiseSetVelocity": st.cruise_set, + "STRCruiseIncrement": int(st.cruise_inc), "STRCruiseDecrement": int(st.cruise_dec)}), + ("STRMCUHealth", {"STRMCUVoltage": v, "STRMCUTemperature": c}), + ("STRFlags", {"STRMCUUnderVoltage": uv, "STRMCUOverTemp": ot})] + if every_s: + out.append(("STRTimeSinceBootup", {"STRTimeSinceBootUp": int(now - self.boot["STR"])})) + return out + + def roundtrip(self, name, signals): + """Encode with the DBC and decode back, enums as raw ints (what the dashboards expect).""" + msg = self.db.get_message_by_name(name) + data = msg.encode(signals, scaling=True, padding=True, strict=False) + return msg.senders[0], msg.decode(data, decode_choices=False) + + def run(self, hz=10.0, duration=None): + t0 = time.time() + while not self.stop.is_set() and (duration is None or time.time() - t0 < duration): + start = time.time() + self.step(1 / hz) + pts = [] + for name, sigs in self.frames_now(): + sender, vals = self.roundtrip(name, sigs) + pts.append((sender, name, vals)) + with self.lock: + self.write(pts) + self.frames += len(pts) + self.tick += 1 + time.sleep(max(0.0, 1 / hz - (time.time() - start))) + + +# ---------------------------------------------------------------------- InfluxDB +def influx_writer(url, token, org, bucket): + from influxdb_client import InfluxDBClient, Point + from influxdb_client.client.write_api import SYNCHRONOUS + w = InfluxDBClient(url=url, token=token, org=org).write_api(write_options=SYNCHRONOUS) + + def write(frames): + pts = [] + for sender, cls, vals in frames: + for k, v in vals.items(): + pts.append(Point(sender).tag("car", CAR_NAME).tag("class", cls).tag("source", SOURCE_TAG).field(k, v)) + if pts: + w.write(bucket=bucket, record=pts) + return write + + +# Every simulated point carries source=cascadia_sim, so "reset" deletes only simulated data and never +# touches real logs imported into the same bucket. Dashboards ignore the tag. +SOURCE_TAG = "cascadia_sim" +HISTORY_START = "1970-01-01T00:00:00Z" + + +def influx_clearer(url, token, org, bucket): + from datetime import datetime, timezone + from influxdb_client import InfluxDBClient + d = InfluxDBClient(url=url, token=token, org=org).delete_api() + + def clear(): + stop = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + for m in ("TEL", "MDI", "DRD", "STR"): + d.delete(HISTORY_START, stop, f'_measurement="{m}" AND source="{SOURCE_TAG}"', bucket=bucket, org=org) + return clear + + +# ---------------------------------------------------------------------- GUI +def gui(sim: Sim): + import tkinter as tk + from tkinter import ttk + + root = tk.Tk() + root.title("Cascadia CAN simulator: TEL / MDI / DRD / STR") + vars_ = [] # (tk var, getter-free setter) for refresh after presets + + def bind(var, setter, getter): + var.trace_add("write", lambda *_: setter(var.get())) + vars_.append((var, getter)) + return var + + def slider(parent, label, lo, hi, get, set_, res=1.0): + f = ttk.Frame(parent); f.pack(fill="x", padx=6, pady=1) + ttk.Label(f, text=label, width=22).pack(side="left") + v = bind(tk.DoubleVar(value=get()), set_, get) + tk.Scale(f, from_=lo, to=hi, resolution=res, orient="horizontal", variable=v, length=260).pack(side="left") + + def check(parent, label, get, set_): + v = bind(tk.BooleanVar(value=get()), set_, get) + ttk.Checkbutton(parent, text=label, variable=v).pack(anchor="w", padx=6) + + def choice(parent, label, options, get, set_): + f = ttk.Frame(parent); f.pack(fill="x", padx=6, pady=1) + ttk.Label(f, text=label, width=22).pack(side="left") + v = bind(tk.StringVar(value=get()), set_, get) + ttk.OptionMenu(f, v, get(), *options).pack(side="left") + + S = lambda: sim.state # noqa: E731 + + top = ttk.Frame(root); top.pack(fill="x", padx=6, pady=4) + ttk.Label(top, text="Scenario:").pack(side="left") + sc = tk.StringVar(value="normal") + desc = ttk.Label(root, text=SCENARIOS["normal"][0], foreground="#555") + + def load(*_): + apply_scenario(sim, sc.get()) + desc.config(text=SCENARIOS[sc.get()][0]) + for var, getter in vars_: + var.set(getter()) + ttk.OptionMenu(top, sc, "normal", *SCENARIOS, command=load).pack(side="left") + + def reset_all(): + sim.reset_all() + sc.set("normal") + desc.config(text="Reset: normal inputs, fault history cleared") + for var, getter in vars_: + var.set(getter()) + ttk.Button(top, text="Reset everything (clear fault history)", command=reset_all).pack(side="left", padx=8) + desc.pack(anchor="w", padx=8) + + nb = ttk.Notebook(root); nb.pack(fill="both", expand=True, padx=6, pady=6) + tabs = {b: ttk.Frame(nb) for b in ("DRD", "MDI", "TEL", "STR")} + for b, f in tabs.items(): + nb.add(f, text=b) + check(f, f"{b} link up (sending)", lambda b=b: S().link[b], lambda v, b=b: S().link.__setitem__(b, v)) + check(f, f"{b} watchdog reset flag", lambda b=b: S().watchdog[b], lambda v, b=b: S().watchdog.__setitem__(b, v)) + ttk.Button(f, text=f"Reboot {b} (uptime → 0)", command=lambda b=b: sim.reset_board(b)).pack(anchor="w", padx=6, pady=2) + if b in S().vdd: + slider(f, "MCU Vdd (mV)", 1800, 3600, lambda b=b: S().vdd[b], lambda v, b=b: S().vdd.__setitem__(b, v), 10) + slider(f, "MCU temperature (°C)", -20, 140, lambda b=b: S().mcu_temp[b], lambda v, b=b: S().mcu_temp.__setitem__(b, v)) + + d = tabs["DRD"] + choice(d, "Drive state", list(DRIVE_STATES), lambda: S().drive_state, lambda v: setattr(S(), "drive_state", v)) + slider(d, "Throttle pedal (%)", 0, 100, lambda: S().throttle, lambda v: setattr(S(), "throttle", v)) + for lbl, a in [("Brake pressed", "brake"), ("Regen enabled", "regen_enabled"), ("Eco mode", "eco"), ("Units KPH", "kph"), + ("Throttle sensors mismatch", "adc_mismatch"), ("Throttle ADC out of range", "adc_out_of_range"), + ("Motor comm fault", "motor_comm_fault")]: + check(d, lbl, lambda a=a: getattr(S(), a), lambda v, a=a: setattr(S(), a, v)) + slider(d, "SOC (%)", 0, 104, lambda: S().soc, lambda v: setattr(S(), "soc", v)) + for k in S().timeouts: + check(d, f"{k} timeout", lambda k=k: S().timeouts[k], lambda v, k=k: S().timeouts.__setitem__(k, v)) + + m = tabs["MDI"] + slider(m, "Motor heating factor", 0, 10, lambda: S().motor_heat, lambda v: setattr(S(), "motor_heat", v), 0.1) + ttk.Button(m, text="Set motor temp to 25 °C", command=lambda: setattr(sim, "motor_temp", 25.0)).pack(anchor="w", padx=6) + ttk.Button(m, text="Set motor temp to 98 °C", command=lambda: setattr(sim, "motor_temp", 98.0)).pack(anchor="w", padx=6) + check(m, "RTD reads OK", lambda: S().rtd_ok, lambda v: setattr(S(), "rtd_ok", v)) + choice(m, "RTD fault flags (when failing)", list(RTD_FLAGS), lambda: S().rtd_flags, lambda v: setattr(S(), "rtd_flags", v)) + + te = tabs["TEL"] + choice(te, "GPS fix", list(GPS_FIX), lambda: S().gps_fix, lambda v: setattr(S(), "gps_fix", v)) + slider(te, "Satellites used", 0, 30, lambda: S().sats, lambda v: setattr(S(), "sats", int(v))) + slider(te, "CAN bus load (%)", 0, 100, lambda: S().bus_load, lambda v: setattr(S(), "bus_load", v)) + check(te, "GPS read fail", lambda: S().gps_read_fail, lambda v: setattr(S(), "gps_read_fail", v)) + check(te, "IMU read fail (no IMU frames)", lambda: S().imu_read_fail, lambda v: setattr(S(), "imu_read_fail", v)) + + s = tabs["STR"] + for lbl, a in [("Left turn signal", "left"), ("Right turn signal", "right"), ("Horn", "horn"), ("Push to talk", "ptt"), + ("Cruise + (held)", "cruise_inc"), ("Cruise − (held)", "cruise_dec")]: + check(s, lbl, lambda a=a: getattr(S(), a), lambda v, a=a: setattr(S(), a, v)) + slider(s, "Cruise set speed (km/h)", 0, 150, lambda: S().cruise_set, lambda v: setattr(S(), "cruise_set", int(v))) + + status = ttk.Label(root, text="", font=("Menlo", 11)); status.pack(anchor="w", padx=8, pady=4) + + def refresh(): + status.config(text=f"speed {sim.speed * 3.6:5.1f} km/h motor {sim.motor_temp:5.1f} °C " + f"SOC {sim.state.soc:5.1f} % frames {sim.frames}") + root.after(500, refresh) + refresh() + root.protocol("WM_DELETE_WINDOW", lambda: (sim.stop.set(), root.destroy())) + root.mainloop() + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--dbc", default=str(ROOT / "dbc" / "cascadia.dbc")) + ap.add_argument("--url", default="http://localhost:8086") + ap.add_argument("--bucket", default="CAN_prod") + ap.add_argument("--scenario", default="normal", choices=list(SCENARIOS)) + ap.add_argument("--headless", action="store_true", help="no window; run the scenario") + ap.add_argument("--duration", type=float, help="seconds (headless)") + ap.add_argument("--allow-remote", action="store_true", help="allow writing to a non-localhost InfluxDB") + ap.add_argument("--list-scenarios", action="store_true") + ap.add_argument("--tk", action="store_true", help="use the Tk window instead of the browser control panel") + ap.add_argument("--port", type=int, default=8090, help="browser control panel port (localhost only)") + a = ap.parse_args() + if a.list_scenarios: + for k, (d, _) in SCENARIOS.items(): + print(f"{k:16s} {d}") + return + if urlparse(a.url).hostname not in ("localhost", "127.0.0.1", "::1") and not a.allow_remote: + sys.exit(f"refusing to write simulated data to {a.url}; pass --allow-remote if you really mean it") + from dotenv import dotenv_values + env = dotenv_values(ROOT / ".env") + sim = Sim(a.dbc, influx_writer(a.url, env["INFLUX_TOKEN"], env["INFLUX_ORG"], a.bucket), + influx_clearer(a.url, env["INFLUX_TOKEN"], env["INFLUX_ORG"], a.bucket)) + apply_scenario(sim, a.scenario) + sim.scenario = a.scenario + if a.headless: + print(f"scenario {a.scenario}: {SCENARIOS[a.scenario][0]} -> {a.url} {a.bucket}") + sim.run(duration=a.duration) + print("frames written:", sim.frames) + elif a.tk: + threading.Thread(target=sim.run, daemon=True).start() + gui(sim) + else: + import webbrowser + sys.path.insert(0, str(Path(__file__).resolve().parent)) + import web_ui + httpd = web_ui.serve(sim, sys.modules[__name__], a.port) + threading.Thread(target=sim.run, daemon=True).start() + url = f"http://localhost:{a.port}" + print(f"control panel: {url} (Ctrl+C to stop)") + webbrowser.open(url) + try: + httpd.serve_forever() + except KeyboardInterrupt: + sim.stop.set() + + +if __name__ == "__main__": + main() diff --git a/tools/cascadia_sim/web_ui.py b/tools/cascadia_sim/web_ui.py new file mode 100644 index 00000000..3f0e0284 --- /dev/null +++ b/tools/cascadia_sim/web_ui.py @@ -0,0 +1,247 @@ +""" +web_ui.py: browser control panel for cascadia_sim (stdlib only). + +Serves one page on http://localhost:8090 with scenario buttons, toggle buttons, sliders and +action buttons for every adjustable input, plus a live readout. Bound to 127.0.0.1 only. +""" +from __future__ import annotations + +import json +import threading +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +GRAFANA = "http://localhost:3000/d/cascadia-prod-{}?orgId=1&from=now-5m&to=now&refresh=1s" + + +def controls(mod): + """UI spec: (board, [controls]). Keys are State attributes, 'attr.sub' for dict entries.""" + boards = [] + for b in ("DRD", "MDI", "TEL", "STR"): + c = [{"type": "toggle", "key": f"link.{b}", "label": "Sending (link up)"}, + {"type": "toggle", "key": f"watchdog.{b}", "label": "Watchdog reset flag", "bad": True}, + {"type": "action", "name": f"reboot.{b}", "label": "Reboot (uptime → 0)"}] + if b in ("TEL", "DRD", "STR"): + c += [{"type": "slider", "key": f"vdd.{b}", "label": "MCU Vdd", "unit": "mV", "min": 1800, "max": 3600, + "step": 10, "limit": mod.UV_MV[b], "limit_dir": "below"}, + {"type": "slider", "key": f"mcu_temp.{b}", "label": "MCU temperature", "unit": "°C", "min": -20, + "max": 140, "step": 1, "limit": mod.OT_C[b], "limit_dir": "above"}] + boards.append([b, c]) + extra = { + "DRD": [{"type": "choice", "key": "drive_state", "label": "Drive state", "options": list(mod.DRIVE_STATES)}, + {"type": "slider", "key": "throttle", "label": "Throttle pedal", "unit": "%", "min": 0, "max": 100, "step": 1}, + {"type": "toggle", "key": "brake", "label": "Brake pressed"}, + {"type": "toggle", "key": "regen_enabled", "label": "Regen enabled"}, + {"type": "toggle", "key": "eco", "label": "Eco mode"}, + {"type": "toggle", "key": "kph", "label": "Units KPH"}, + {"type": "slider", "key": "soc", "label": "SOC", "unit": "%", "min": 0, "max": 104, "step": 1}, + {"type": "toggle", "key": "adc_mismatch", "label": "Throttle sensors mismatch", "bad": True}, + {"type": "toggle", "key": "adc_out_of_range", "label": "Throttle ADC out of range", "bad": True}, + {"type": "toggle", "key": "motor_comm_fault", "label": "Motor comm fault", "bad": True}] + + [{"type": "toggle", "key": f"timeouts.{k}", "label": f"{k} timeout", "bad": True} + for k in ("Speed", "DriveState", "SOC", "Voltage", "Current", "Accel")], + "MDI": [{"type": "slider", "key": "motor_heat", "label": "Motor heating factor", "unit": "×", "min": 0, "max": 10, "step": 0.1}, + {"type": "action", "name": "motor_temp.25", "label": "Motor temp → 25 °C"}, + {"type": "action", "name": "motor_temp.98", "label": "Motor temp → 98 °C"}, + {"type": "action", "name": "motor_temp.105", "label": "Motor temp → 105 °C"}, + {"type": "toggle", "key": "rtd_ok", "label": "RTD reads OK"}, + {"type": "choice", "key": "rtd_flags", "label": "RTD fault (when failing)", "options": list(mod.RTD_FLAGS)}], + "TEL": [{"type": "choice", "key": "gps_fix", "label": "GPS fix", "options": list(mod.GPS_FIX)}, + {"type": "slider", "key": "sats", "label": "Satellites used", "unit": "", "min": 0, "max": 30, "step": 1}, + {"type": "slider", "key": "bus_load", "label": "CAN bus load", "unit": "%", "min": 0, "max": 100, "step": 1}, + {"type": "toggle", "key": "gps_read_fail", "label": "GPS read fail", "bad": True}, + {"type": "toggle", "key": "imu_read_fail", "label": "IMU read fail (no IMU frames)", "bad": True}], + "STR": [{"type": "toggle", "key": "left", "label": "Left turn signal"}, + {"type": "toggle", "key": "right", "label": "Right turn signal"}, + {"type": "toggle", "key": "horn", "label": "Horn"}, + {"type": "toggle", "key": "ptt", "label": "Push to talk"}, + {"type": "toggle", "key": "cruise_inc", "label": "Cruise + (held)"}, + {"type": "toggle", "key": "cruise_dec", "label": "Cruise − (held)"}, + {"type": "slider", "key": "cruise_set", "label": "Cruise set speed", "unit": "km/h", "min": 0, "max": 150, "step": 1}], + } + for bc in boards: + bc[1] += extra[bc[0]] + return boards + + +def get_key(state, key): + if "." in key: + a, s = key.split(".", 1) + return getattr(state, a)[s] + return getattr(state, key) + + +def set_key(state, key, value): + cur = get_key(state, key) + if isinstance(cur, bool): + value = bool(value) + elif isinstance(cur, int): + value = int(float(value)) + elif isinstance(cur, float): + value = float(value) + if "." in key: + a, s = key.split(".", 1) + getattr(state, a)[s] = value + else: + setattr(state, key, value) + + +def snapshot(sim, spec): + vals = {c["key"]: get_key(sim.state, c["key"]) for _, cs in spec for c in cs if "key" in c} + return {"values": vals, "speed": round(sim.speed * 3.6, 1), "motor_temp": round(sim.motor_temp, 1), + "motor_ot": sim.motor_ot, "soc": round(sim.state.soc, 1), "frames": sim.frames, + "scenario": getattr(sim, "scenario", "normal")} + + +def serve(sim, mod, port=8090): + spec = controls(mod) + lock = threading.Lock() + + class H(BaseHTTPRequestHandler): + def log_message(self, *a): + pass + + def _send(self, code, body, ctype="application/json"): + data = body.encode() if isinstance(body, str) else body + self.send_response(code) + self.send_header("Content-Type", ctype) + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + def do_GET(self): + if self.path in ("/", "/index.html"): + return self._send(200, PAGE, "text/html; charset=utf-8") + if self.path == "/api/spec": + scen = [{"name": k, "desc": d} for k, (d, _) in mod.SCENARIOS.items()] + return self._send(200, json.dumps({"boards": spec, "scenarios": scen, "grafana": GRAFANA})) + if self.path == "/api/state": + with lock: + return self._send(200, json.dumps(snapshot(sim, spec))) + self._send(404, "{}") + + def do_POST(self): + n = int(self.headers.get("Content-Length", 0)) + body = json.loads(self.rfile.read(n) or b"{}") + with lock: + try: + if self.path == "/api/set": + set_key(sim.state, body["key"], body["value"]) + elif self.path == "/api/scenario": + mod.apply_scenario(sim, body["name"]) + sim.scenario = body["name"] + elif self.path == "/api/action": + what, arg = body["name"].split(".", 1) + if what == "reboot": + sim.reset_board(arg) + elif what == "motor_temp": + sim.motor_temp = float(arg) + elif what == "reset": + sim.reset_all() + else: + return self._send(404, "{}") + except (KeyError, ValueError, AttributeError) as e: + return self._send(400, json.dumps({"error": str(e)})) + return self._send(200, json.dumps(snapshot(sim, spec))) + + httpd = ThreadingHTTPServer(("127.0.0.1", port), H) + return httpd + + +PAGE = r""" + +Cascadia Sim Controls + +

Cascadia CAN simulator: what goes in

+
+
+
+
+ +"""