From 116afcab98bf5fef4cd562a94faa9edf9446f23f Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Thu, 23 Jul 2026 21:32:14 +0200 Subject: [PATCH] feat(sensors): runtime seam for overlay-registered sensor controls registerSensorControls() lets a private build add SensorControlDef entries for sensors it ships outside the OSS tree (e.g. the DFRobot Gravity analog family) so they get the live slider panel; every SENSOR_CONTROLS lookup now goes through getSensorControl(id) which falls back to the registered map. Dead code in a pure OSS build, same contract as proBoardRegistry / registerComponentDoc. --- .../components/simulator/SensorControlPanel.tsx | 4 ++-- .../components/simulator/SimulatorCanvas.tsx | 8 ++++---- frontend/src/simulation/sensorControlConfig.ts | 17 +++++++++++++++++ frontend/src/store/useSimulatorStore.ts | 8 ++++---- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/simulator/SensorControlPanel.tsx b/frontend/src/components/simulator/SensorControlPanel.tsx index 7824545d..6a90be77 100644 --- a/frontend/src/components/simulator/SensorControlPanel.tsx +++ b/frontend/src/components/simulator/SensorControlPanel.tsx @@ -9,7 +9,7 @@ import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { - SENSOR_CONTROLS, + getSensorControl, type SensorControl, type SliderControl, } from '../../simulation/sensorControlConfig'; @@ -49,7 +49,7 @@ export const SensorControlPanel: React.FC = ({ onClose, }) => { const { t } = useTranslation(); - const def = SENSOR_CONTROLS[metadataId]; + const def = getSensorControl(metadataId); // Local slider/button state — hydrated from the registry's last-known // values for this componentId (so reopening a sensor or switching between diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index 768bc299..223cf696 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -11,7 +11,7 @@ import { ComponentPickerModal } from '../ComponentPickerModal'; import { ComponentPropertyDialog } from './ComponentPropertyDialog'; import { CustomChipDialog } from '../customChips/CustomChipDialog'; import { SensorControlPanel } from './SensorControlPanel'; -import { SENSOR_CONTROLS } from '../../simulation/sensorControlConfig'; +import { SENSOR_CONTROLS, getSensorControl } from '../../simulation/sensorControlConfig'; import { DynamicComponent, createComponentFromMetadata } from '../DynamicComponent'; import { InstrumentComponent } from '../components-instruments/InstrumentComponent'; import { ComponentRegistry } from '../../services/ComponentRegistry'; @@ -996,7 +996,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { const component = componentsRef.current.find((c) => c.id === touchId); if (component) { if (interactionRunningRef.current) { - if (SENSOR_CONTROLS[component.metadataId] !== undefined) { + if (getSensorControl(component.metadataId) !== undefined) { setSensorControlComponentId(touchId); setSensorControlMetadataId(component.metadataId); } @@ -1417,7 +1417,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { // panel). Mirrors the touch tap flow above. if (interactionRunning) { const component = components.find((c) => c.id === componentId); - const isSensor = !!component && SENSOR_CONTROLS[component.metadataId] !== undefined; + const isSensor = !!component && getSensorControl(component.metadataId) !== undefined; if (!isSensor) return; } @@ -1710,7 +1710,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { // handle its own clicks, so we suppress the property // dialog entirely. This is the path that also unblocks // wokwi-slide-switch toggling in the digital examples. - if (SENSOR_CONTROLS[component.metadataId] !== undefined) { + if (getSensorControl(component.metadataId) !== undefined) { setSensorControlComponentId(draggedComponentId); setSensorControlMetadataId(component.metadataId); } diff --git a/frontend/src/simulation/sensorControlConfig.ts b/frontend/src/simulation/sensorControlConfig.ts index 096d511d..227bd653 100644 --- a/frontend/src/simulation/sensorControlConfig.ts +++ b/frontend/src/simulation/sensorControlConfig.ts @@ -377,3 +377,20 @@ export const SENSOR_CONTROLS: Record = { defaultValues: { xAxis: 0, yAxis: 0 }, }, }; + +// ── Overlay seam ───────────────────────────────────────────────────────────── +// A private build (velxio.com) registers sensor-control definitions for the +// sensors it ships outside the OSS tree (e.g. the DFRobot Gravity analog +// family). Same contract as proBoardRegistry / registerComponentDoc: dead code +// in a pure OSS build. Read every SENSOR_CONTROLS lookup through +// getSensorControl() so overlay-registered sensors surface their slider panel. +const proSensorControls: Record = {}; + +export function registerSensorControls(defs: Record): void { + Object.assign(proSensorControls, defs); +} + +export function getSensorControl(id: string | null | undefined): SensorControlDef | undefined { + if (!id) return undefined; + return SENSOR_CONTROLS[id] ?? proSensorControls[id]; +} diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index d5ac1820..d39fdbe6 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -53,7 +53,7 @@ import { updateWires as icUpdateWires, setInterconnectRuntime, } from '../simulation/Interconnect'; -import { SENSOR_CONTROLS } from '../simulation/sensorControlConfig'; +import { SENSOR_CONTROLS, getSensorControl } from '../simulation/sensorControlConfig'; import { dispatchSensorUpdate } from '../simulation/SensorUpdateRegistry'; // ── Sensor pre-registration ────────────────────────────────────────────────── @@ -2150,18 +2150,18 @@ export const useSimulatorStore = create((set, get) => { // 2.5V) and refreshes the panel's cached value; bumping sensorResetNonce // remounts the open SensorControlPanel so its slider snaps back too. const sensorComps = get().components.filter( - (c) => c.metadataId && SENSOR_CONTROLS[c.metadataId], + (c) => c.metadataId && getSensorControl(c.metadataId), ); if (sensorComps.length > 0) { set((s) => ({ components: s.components.map((c) => { - const def = c.metadataId ? SENSOR_CONTROLS[c.metadataId] : undefined; + const def = getSensorControl(c.metadataId); return def ? { ...c, properties: { ...c.properties, ...def.defaultValues } } : c; }), sensorResetNonce: s.sensorResetNonce + 1, })); for (const c of sensorComps) { - dispatchSensorUpdate(c.id, SENSOR_CONTROLS[c.metadataId].defaultValues); + dispatchSensorUpdate(c.id, getSensorControl(c.metadataId)!.defaultValues); } } },