diff --git a/dashboard/src/components/widgets/p987/live/EcuDebugWidget.tsx b/dashboard/src/components/widgets/p987/live/EcuDebugWidget.tsx new file mode 100644 index 00000000..789d8e2b --- /dev/null +++ b/dashboard/src/components/widgets/p987/live/EcuDebugWidget.tsx @@ -0,0 +1,49 @@ +import LiveWidget from "@/components/widgets/LiveWidget"; + +interface EcuDebugWidgetProps { + vehicle_id: string; + showDeltaBanner?: boolean; + title: string; + signals: string[]; +} + +// Shared by every stock-CAN ECU on the 987. gr26 has one debug widget per +// node written out longhand; the 987 has seven ECUs decoded from the DBC, +// so they share this and pass their own signal list. +export default function EcuDebugWidget({ + vehicle_id, + showDeltaBanner = false, + title, + signals, +}: EcuDebugWidgetProps) { + const sorted = [...signals].sort(); + + return ( + + {(_, currentSignals) => ( +
+

{title}

+
+ {sorted.map((signal) => ( +
+ + {signal.replace(/^pcan_/, "")} + + + {currentSignals.get(signal)?.value ?? 0} + +
+ ))} +
+
+ )} +
+ ); +} diff --git a/dashboard/src/components/widgets/p987/live/TcmResourceWidget.tsx b/dashboard/src/components/widgets/p987/live/TcmResourceWidget.tsx new file mode 100644 index 00000000..a16fcbff --- /dev/null +++ b/dashboard/src/components/widgets/p987/live/TcmResourceWidget.tsx @@ -0,0 +1,203 @@ +import LiveWidget from "@/components/widgets/LiveWidget"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { Cpu, HardDrive, MemoryStick, Thermometer } from "lucide-react"; + +interface TcmResourceWidgetProps { + vehicle_id: string; + showDeltaBanner?: boolean; +} + +// The 987's TCM is a Pi Zero 2 W, so this is deliberately not the gr25 +// layout: four cores instead of six, and no GPU or power-rail readings +// because the board has no sensors for them. The throttle flags take their +// place — under-voltage is the failure mode this hardware actually has, +// and it shows up in none of the other metrics. +export default function TcmResourceWidget({ + vehicle_id, + showDeltaBanner = false, +}: TcmResourceWidgetProps) { + const cores = [0, 1, 2, 3]; + const signals = [ + "tcm_cpu_total_util", + ...cores.map((n) => `tcm_cpu_${n}_util`), + ...cores.map((n) => `tcm_cpu_${n}_freq`), + "tcm_cpu_temp", + "tcm_ram_total", + "tcm_ram_used", + "tcm_ram_util", + "tcm_disk_total", + "tcm_disk_used", + "tcm_disk_util", + "tcm_undervoltage", + "tcm_undervoltage_since_boot", + "tcm_thermal_throttled", + "tcm_thermal_throttled_since_boot", + ]; + + const utilizationColor = (value: number) => { + if (value < 50) return "bg-green-500"; + if (value < 80) return "bg-yellow-500"; + return "bg-red-500"; + }; + + const tempColor = (value: number) => { + // The Pi soft-throttles at 80C and hard-throttles at 85C. + if (value < 60) return "text-green-500"; + if (value < 80) return "text-yellow-500"; + return "text-red-500"; + }; + + return ( + + {(_, currentSignals) => { + const value = (name: string) => currentSignals.get(name)?.value ?? 0; + const flag = (name: string) => value(name) === 1; + + return ( +
+

TCM Resources

+
+ + + + CPU Utilization + + + + +
+ {value("tcm_cpu_total_util").toFixed(0)}% +
+ +
+ {cores.map((n) => ( +
+ core {n} + + {value(`tcm_cpu_${n}_util`).toFixed(0)}% ·{" "} + {value(`tcm_cpu_${n}_freq`).toFixed(0)} MHz + +
+ ))} +
+
+
+ + + + Memory + + + +
+ {value("tcm_ram_util").toFixed(0)}% +
+ +
+ {value("tcm_ram_used").toFixed(0)} /{" "} + {value("tcm_ram_total").toFixed(0)} MB +
+
+
+ + + + Disk + + + +
+ {value("tcm_disk_util").toFixed(0)}% +
+ +
+ {value("tcm_disk_used").toFixed(0)} /{" "} + {value("tcm_disk_total").toFixed(0)} MB +
+
+
+ + + + + Temperature & Throttling + + + + +
+ {value("tcm_cpu_temp").toFixed(0)}°C +
+
+ + +
+
+
+
+
+ ); + }} +
+ ); +} + +// A since-boot flag that is set while the live flag is clear means the +// event happened earlier in this power cycle — worth surfacing separately +// rather than collapsing both into one indicator. +function ThrottleRow({ + label, + now, + sinceBoot, +}: { + label: string; + now: boolean; + sinceBoot: boolean; +}) { + const color = now + ? "text-red-500" + : sinceBoot + ? "text-yellow-500" + : "text-muted-foreground"; + const state = now ? "active" : sinceBoot ? "seen since boot" : "clear"; + return ( +
+ {label} + {state} +
+ ); +} diff --git a/dashboard/src/components/widgets/p987/live/TcmStatusWidget.tsx b/dashboard/src/components/widgets/p987/live/TcmStatusWidget.tsx new file mode 100644 index 00000000..599d84fa --- /dev/null +++ b/dashboard/src/components/widgets/p987/live/TcmStatusWidget.tsx @@ -0,0 +1,65 @@ +import LiveWidget from "@/components/widgets/LiveWidget"; + +interface TcmStatusWidgetProps { + vehicle_id: string; + showDeltaBanner?: boolean; +} + +// 0x200, published every 5s. Each connectivity bit is its own signal, so +// this is a straight read rather than any bit-twiddling. +export default function TcmStatusWidget({ + vehicle_id, + showDeltaBanner = false, +}: TcmStatusWidgetProps) { + const flags = [ + { signal: "tcm_connection_ok", label: "Internet" }, + { signal: "tcm_mqtt_ok", label: "Cloud broker" }, + { signal: "tcm_mapache_ok", label: "Mapache" }, + { signal: "tcm_clock_ok", label: "Clock synced" }, + ]; + const signals = [...flags.map((f) => f.signal), "tcm_mapache_ping"]; + + return ( + + {(_, currentSignals) => ( +
+

TCM Status

+
+ {flags.map((f) => { + const ok = (currentSignals.get(f.signal)?.value ?? 0) === 1; + return ( +
+ {f.label} + + {ok ? "up" : "down"} + +
+ ); + })} +
+ Round trip + + {(currentSignals.get("tcm_mapache_ping")?.value ?? 0).toFixed( + 0, + )}{" "} + ms + +
+
+
+ )} +
+ ); +} diff --git a/dashboard/src/components/widgets/p987/live/signals.ts b/dashboard/src/components/widgets/p987/live/signals.ts new file mode 100644 index 00000000..9f7afab2 --- /dev/null +++ b/dashboard/src/components/widgets/p987/live/signals.ts @@ -0,0 +1,101 @@ +// Signal names the p987 ingest publishes, grouped by the ECU that sends +// them. Names are `_`: the bus segment comes from the +// relay's CAN_INTERFACES (`can0:pcan`), so these assume the powertrain bus +// is labelled `pcan`. Relabel the interface and these need to follow. +const bus = "pcan"; +const on = (...names: string[]) => names.map((n) => `${bus}_${n}`); + +// DME — engine management. The full DBC set is ~100 signals across eight +// frames; this is the subset that describes what the engine is doing +// rather than counters, checksums and software revisions. +export const dmeSignals = on( + "DME_RPM", + "DME_EngineTorque", + "DME_DriverTrq", + "DME_EngineRunning", + "DME_AccelPedalAngle", + "DME_APP", + "DME_CoolantTemp", + "DME_OilTemp", + "DME_OilPressure", + "DME_EngineCompTemp", + "DME_BoostPressure", + "DME_AmbientPressure", + "DME_Lambda_Value", + "DME_Lambda_Status", + "DME_EngagedGear", + "DME_IdleSpeedTarget", + "DME_FuelConsumption1", + "DME_FuelConsumption2", + "DME_Odometer", + "DME_Interventions", + "DME_TorqueLoss", + "DME_ReducedPower", + "DME_Overboost", + "DME_CEL_Steady", + "DME_CEL_Flashing", + "DME_FuelReserve", + "DME_OilPressureAlert", + "DME_OilTempSensFault", + "DME_ChargingAlert", + "DME_EngCompFanAlert", + "DME_RadFanSpeedReq", + "Outside_Temp", + "Sport_Mode", +); + +// PSM — stability control, and the only source of wheel speeds. +export const psmSignals = on( + "PSM_WheelSpeedFL", + "PSM_WheelSpeedFR", + "PSM_WheelSpeedRL", + "PSM_WheelSpeedRR", + "Vref", + "PSM_BrakePressure", + "PSM_FootBrake", + "PSM_HandBrake", + "Yaw_Rate", + "Yaw_Rate_Sign", + "PSM_LateralAccel", + "Longitudinal_Accel", + "ABS_Status", + "ABS_Error", + "ESP_Control", + "ESP_Intervention", + "ESP_Error", + "ESP_Diag_Mode", + "PSM_Disabled", + "ASR_Requirement", + "ASR_Switching", + "MSR_Requirement", + "EBV_Error", + "Brake_Intervention", + "Brake_Fluid_Switch", + "Engagement_Torque", +); + +// SCCM — steering column. Angle and rate are magnitude-only, each with a +// separate sign bit, so both are shown rather than combined here. +export const sccmSignals = on( + "SCCM_SteeringAngle", + "SCCM_SteeringAngleSign", + "SCCM_SteeringAngleRate", + "SCCM_SteeringAngleRateSign", + "SCCM_CruiseAvailable", + "SCCM_CruiseEnable", + "SCCM_CruiseUp", + "SCCM_CruiseDown", + "SCCM_CruiseTowards", + "SCCM_CruiseAway", +); + +// PDK — transmission. Absent on manual cars, in which case these stay at +// their zero defaults. +export const pdkSignals = on( + "PDK_SelectedGear", + "PDK_ClutchStatus", + "PDK_OilTemp", + "PDK_ShiftFork1", + "PDK_ShiftFork2", + "PDK_ErrorFlags", +); diff --git a/dashboard/src/components/widgets/registry.tsx b/dashboard/src/components/widgets/registry.tsx index d722611c..4da4aa1b 100644 --- a/dashboard/src/components/widgets/registry.tsx +++ b/dashboard/src/components/widgets/registry.tsx @@ -42,6 +42,15 @@ import TcmCpuWidget from "@/components/widgets/gr25/live/TcmCpuWidget"; import TcmCpuGraphWidget from "@/components/widgets/gr25/live/TcmCpuGraphWidget"; import Gr25EcuDebugWidget from "@/components/widgets/gr25/live/EcuDebugWidget"; import Gr25InverterDebugWidget from "@/components/widgets/gr25/live/InverterDebugWidget"; +import P987TcmResourceWidget from "@/components/widgets/p987/live/TcmResourceWidget"; +import P987TcmStatusWidget from "@/components/widgets/p987/live/TcmStatusWidget"; +import P987EcuDebugWidget from "@/components/widgets/p987/live/EcuDebugWidget"; +import { + dmeSignals, + psmSignals, + sccmSignals, + pdkSignals, +} from "@/components/widgets/p987/live/signals"; export interface WidgetEntry { id: string; @@ -63,9 +72,112 @@ export const getWidgetRegistry = (vehicle_type: string) => { if (vehicle_type === "gr26") { return gr26_registry; } + if (vehicle_type === "p987") { + return p987_registry; + } return {}; }; +// The 987 is a stock Porsche, so its groups are the car's own ECUs rather +// than GR-designed nodes. Every debug widget shares one component and +// passes its own signal list — see p987/live/signals.ts. +export const p987_registry = { + TCM: [ + { + id: "tcm-resources", + name: "TCM Resources", + description: + "Pi Zero 2 W CPU, memory, disk and temperature, plus under-voltage and thermal throttle flags.", + component: P987TcmResourceWidget, + icon: Activity, + span: 6, + preview: "/widgets/p987/tcm-resources.png", + }, + { + id: "tcm-status", + name: "TCM Status", + description: + "Connectivity bits from 0x200 — internet, cloud broker, Mapache reachability, clock sync — and the round-trip time.", + component: P987TcmStatusWidget, + icon: Cpu, + span: 3, + preview: "/widgets/p987/tcm-status.png", + }, + ], + DME: [ + { + id: "dme-debug", + name: "Engine (DME)", + description: + "Engine speed, torque, pedal, coolant and oil, boost, lambda and fault flags.", + component: (props: any) => ( + + ), + icon: Gauge, + span: 12, + preview: "/widgets/p987/dme-debug.png", + }, + ], + PSM: [ + { + id: "psm-debug", + name: "Stability (PSM)", + description: + "Wheel speeds, brake pressure, yaw rate, accelerations and ABS/ESP state.", + component: (props: any) => ( + + ), + icon: Activity, + span: 12, + preview: "/widgets/p987/psm-debug.png", + }, + ], + SCCM: [ + { + id: "sccm-debug", + name: "Steering (SCCM)", + description: + "Steering angle and rate with their sign bits, plus cruise control stalk state.", + component: (props: any) => ( + + ), + icon: Crosshair, + span: 12, + preview: "/widgets/p987/sccm-debug.png", + }, + ], + PDK: [ + { + id: "pdk-debug", + name: "Transmission (PDK)", + description: + "Selected gear, clutch status, oil temperature and shift forks. Stays at zero on a manual car.", + component: (props: any) => ( + + ), + icon: Bug, + span: 12, + preview: "/widgets/p987/pdk-debug.png", + }, + ], +}; + export const gr25_registry = { TCM: [ { diff --git a/vehicle/model/vehicle_type.go b/vehicle/model/vehicle_type.go index addbd6e8..4fc80548 100644 --- a/vehicle/model/vehicle_type.go +++ b/vehicle/model/vehicle_type.go @@ -11,11 +11,15 @@ const ( GR24 VehicleType = "gr24" GR25 VehicleType = "gr25" GR26 VehicleType = "gr26" + // P987 is the Porsche 987 Cayman, not an FSAE car. It runs the same + // TCM/relay stack but its CAN is stock Porsche, decoded by the p987 + // service rather than gr26. + P987 VehicleType = "p987" ) // VehicleTypes is the canonical ordered list (newest first), surfaced via the // API so the frontend's create-vehicle and flag dialogs don't hardcode it. -var VehicleTypes = []VehicleType{GR26, GR25, GR24} +var VehicleTypes = []VehicleType{P987, GR26, GR25, GR24} // VehicleTypeInfo is the API shape for a selectable type. type VehicleTypeInfo struct {