2026-03-19 11:52:46 +07:00
|
|
|
/**
|
|
|
|
|
* sensorControlConfig.ts — defines the interactive controls shown in the
|
|
|
|
|
* SensorControlPanel for each sensor component type.
|
|
|
|
|
*
|
|
|
|
|
* Used by:
|
|
|
|
|
* - SensorControlPanel.tsx (renders the controls)
|
|
|
|
|
* - SimulatorCanvas.tsx (decides whether to show the panel on click)
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
export interface SliderControl {
|
|
|
|
|
type: 'slider';
|
|
|
|
|
key: string;
|
|
|
|
|
label: string;
|
|
|
|
|
min: number;
|
|
|
|
|
max: number;
|
|
|
|
|
step: number;
|
|
|
|
|
unit: string;
|
|
|
|
|
defaultValue: number;
|
|
|
|
|
/** Optional custom formatter — e.g. to show "24.0°C" instead of "24" */
|
|
|
|
|
formatValue?: (v: number) => string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ButtonControl {
|
|
|
|
|
type: 'button';
|
|
|
|
|
key: string;
|
|
|
|
|
label: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type SensorControl = SliderControl | ButtonControl;
|
|
|
|
|
|
|
|
|
|
export interface SensorControlDef {
|
|
|
|
|
title: string;
|
|
|
|
|
controls: SensorControl[];
|
|
|
|
|
defaultValues: Record<string, number | boolean>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
const oneDecimal = (v: number) => v.toFixed(1);
|
|
|
|
|
const twoDecimal = (v: number) => v.toFixed(2);
|
|
|
|
|
|
|
|
|
|
// ─── Sensor Control Definitions ──────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export const SENSOR_CONTROLS: Record<string, SensorControlDef> = {
|
|
|
|
|
// ── MPU-6050 6-axis IMU ────────────────────────────────────────────────────
|
|
|
|
|
mpu6050: {
|
|
|
|
|
title: 'MPU6050 Accelerometer + Gyroscope',
|
|
|
|
|
controls: [
|
|
|
|
|
// Acceleration
|
2026-04-22 02:45:45 +07:00
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'accelX',
|
|
|
|
|
label: 'X',
|
|
|
|
|
min: -2,
|
|
|
|
|
max: 2,
|
|
|
|
|
step: 0.01,
|
|
|
|
|
unit: 'g',
|
|
|
|
|
defaultValue: 0,
|
|
|
|
|
formatValue: oneDecimal,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'accelY',
|
|
|
|
|
label: 'Y',
|
|
|
|
|
min: -2,
|
|
|
|
|
max: 2,
|
|
|
|
|
step: 0.01,
|
|
|
|
|
unit: 'g',
|
|
|
|
|
defaultValue: 0,
|
|
|
|
|
formatValue: oneDecimal,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'accelZ',
|
|
|
|
|
label: 'Z',
|
|
|
|
|
min: -2,
|
|
|
|
|
max: 2,
|
|
|
|
|
step: 0.01,
|
|
|
|
|
unit: 'g',
|
|
|
|
|
defaultValue: 1,
|
|
|
|
|
formatValue: oneDecimal,
|
|
|
|
|
},
|
2026-03-19 11:52:46 +07:00
|
|
|
// Rotation (gyro)
|
2026-04-22 02:45:45 +07:00
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'gyroX',
|
|
|
|
|
label: 'X',
|
|
|
|
|
min: -250,
|
|
|
|
|
max: 250,
|
|
|
|
|
step: 1,
|
|
|
|
|
unit: '°/sec',
|
|
|
|
|
defaultValue: 0,
|
|
|
|
|
formatValue: oneDecimal,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'gyroY',
|
|
|
|
|
label: 'Y',
|
|
|
|
|
min: -250,
|
|
|
|
|
max: 250,
|
|
|
|
|
step: 1,
|
|
|
|
|
unit: '°/sec',
|
|
|
|
|
defaultValue: 0,
|
|
|
|
|
formatValue: oneDecimal,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'gyroZ',
|
|
|
|
|
label: 'Z',
|
|
|
|
|
min: -250,
|
|
|
|
|
max: 250,
|
|
|
|
|
step: 1,
|
|
|
|
|
unit: '°/sec',
|
|
|
|
|
defaultValue: 0,
|
|
|
|
|
formatValue: oneDecimal,
|
|
|
|
|
},
|
2026-03-19 11:52:46 +07:00
|
|
|
// Temperature
|
2026-04-22 02:45:45 +07:00
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'temp',
|
|
|
|
|
label: 'Temperature',
|
|
|
|
|
min: -40,
|
|
|
|
|
max: 85,
|
|
|
|
|
step: 1,
|
|
|
|
|
unit: '°C',
|
|
|
|
|
defaultValue: 24,
|
|
|
|
|
formatValue: oneDecimal,
|
|
|
|
|
},
|
2026-03-19 11:52:46 +07:00
|
|
|
],
|
|
|
|
|
defaultValues: { accelX: 0, accelY: 0, accelZ: 1, gyroX: 0, gyroY: 0, gyroZ: 0, temp: 24 },
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// ── DHT22 Temperature / Humidity ──────────────────────────────────────────
|
|
|
|
|
dht22: {
|
|
|
|
|
title: 'DHT22 Temperature & Humidity',
|
|
|
|
|
controls: [
|
2026-04-22 02:45:45 +07:00
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'temperature',
|
|
|
|
|
label: 'Temperature',
|
|
|
|
|
min: -40,
|
|
|
|
|
max: 80,
|
|
|
|
|
step: 0.5,
|
|
|
|
|
unit: '°C',
|
|
|
|
|
defaultValue: 25,
|
|
|
|
|
formatValue: oneDecimal,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'humidity',
|
|
|
|
|
label: 'Humidity',
|
|
|
|
|
min: 0,
|
|
|
|
|
max: 100,
|
|
|
|
|
step: 0.5,
|
|
|
|
|
unit: '%',
|
|
|
|
|
defaultValue: 50,
|
|
|
|
|
formatValue: oneDecimal,
|
|
|
|
|
},
|
2026-03-19 11:52:46 +07:00
|
|
|
],
|
|
|
|
|
defaultValues: { temperature: 25, humidity: 50 },
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// ── BMP280 Barometric Pressure + Temperature ───────────────────────────────
|
|
|
|
|
bmp280: {
|
|
|
|
|
title: 'BMP280 Barometric Pressure Sensor',
|
|
|
|
|
controls: [
|
2026-04-22 02:45:45 +07:00
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'temperature',
|
|
|
|
|
label: 'Temperature',
|
|
|
|
|
min: -40,
|
|
|
|
|
max: 85,
|
|
|
|
|
step: 1,
|
|
|
|
|
unit: '°C',
|
|
|
|
|
defaultValue: 24,
|
|
|
|
|
formatValue: oneDecimal,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'pressure',
|
|
|
|
|
label: 'Pressure',
|
|
|
|
|
min: 300,
|
|
|
|
|
max: 1100,
|
|
|
|
|
step: 0.25,
|
|
|
|
|
unit: 'hPa',
|
|
|
|
|
defaultValue: 1013.25,
|
|
|
|
|
formatValue: twoDecimal,
|
|
|
|
|
},
|
2026-03-19 11:52:46 +07:00
|
|
|
],
|
|
|
|
|
defaultValues: { temperature: 24, pressure: 1013.25 },
|
|
|
|
|
},
|
|
|
|
|
|
feat(components): DS3231 RTC module + GPS NEO-6M NMEA talker
DS3231 (I2C 0x68):
- New velxio-ds3231 Web Component (4-pin module: GND/VCC/SDA/SCL) with
pinInfo per CLAUDE.md 6a; registered in elements-register + main.tsx.
- Catalog entry in scripts/component-overrides.json (_customComponents)
mirrored into components-metadata.json exactly as the generator emits it
(verified by cloning wokwi-elements and running generate:metadata: zero drift).
- The existing VirtualDS3231 device sim (DS1307-compatible time regs
0x00-0x06 + control 0x0E / status 0x0F / temperature 0x11-0x12 at 0.25 C
steps) is now user-reachable from the picker; the AVR/RP2040 attach path
gains live temperature updates via the SensorControlPanel (new ds3231
slider in sensorControlConfig).
GPS NEO-6M (UART talker, 9600 baud):
- New velxio-gps-neo6m Web Component (VCC/RX/TX/GND, pulsing PPS LED).
- New part sim GpsParts.ts: emits a valid GPGGA+GPRMC cycle per second
(correct XOR checksums, ddmm.mmmmm coords, UTC clock advancing 1 s/cycle)
with lat/lng/altitude/speed configurable via property dialog and live
via a new SensorControlPanel entry. Defaults: Madrid 40.4168 N 3.7038 W.
- Injection routes, chosen by classifying the wired board pin:
1) hardware UART RX -> new uniform sim.feedUart(uart, data) seam
(AVR USART0, RP2040 uart0/1 via rp2040js feedByte, ESP32 uart0/2 and
STM32 USART1/2 via their bridge shims); byte feed is paced in 8-byte
wall-clock chunks so 32-byte RX FIFOs never overflow;
2) any other digital pin on a cycle-accurate sim -> real 8N1 bit-banged
waveform via schedulePinChange, so SoftwareSerial decodes it;
3) feedUart rejection (e.g. Mega USART1-3, not modelled by avr8js)
degrades permanently to the bit-banged path.
- Interconnect already probed sim.feedUart; implementing it also upgrades
cross-board UART byte shortcuts for RP2040 uart1.
Tests: new gps-neo6m.test.ts (16 tests: NMEA builders, byte injection on
Uno RX0 / ESP32 RX2, Mega RX1 fallback, waveform decode back to $GPGGA,
live position updates, PPS pulse) + 3 new ds3231 AVR-path tests in
protocol-parts.test.ts. Full frontend suite: 2294 passed, 2 pre-existing
Galaksija Z80 timeouts that pass in isolation. npx tsc --noEmit clean.
2026-08-01 01:38:06 +07:00
|
|
|
// ── DS3231 RTC (on-chip temperature sensor) ────────────────────────────────
|
|
|
|
|
ds3231: {
|
|
|
|
|
title: 'DS3231 RTC Temperature',
|
|
|
|
|
controls: [
|
|
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'temperature',
|
|
|
|
|
label: 'Temperature',
|
|
|
|
|
min: -40,
|
|
|
|
|
max: 85,
|
|
|
|
|
step: 0.25,
|
|
|
|
|
unit: '°C',
|
|
|
|
|
defaultValue: 25,
|
|
|
|
|
formatValue: twoDecimal,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
defaultValues: { temperature: 25 },
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// ── GPS NEO-6M (position fed into the NMEA stream) ─────────────────────────
|
|
|
|
|
'gps-neo6m': {
|
|
|
|
|
title: 'GPS NEO-6M Position',
|
|
|
|
|
controls: [
|
|
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'lat',
|
|
|
|
|
label: 'Latitude',
|
|
|
|
|
min: -90,
|
|
|
|
|
max: 90,
|
|
|
|
|
step: 0.0001,
|
|
|
|
|
unit: '°',
|
|
|
|
|
defaultValue: 40.4168,
|
|
|
|
|
formatValue: (v: number) => v.toFixed(4),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'lng',
|
|
|
|
|
label: 'Longitude',
|
|
|
|
|
min: -180,
|
|
|
|
|
max: 180,
|
|
|
|
|
step: 0.0001,
|
|
|
|
|
unit: '°',
|
|
|
|
|
defaultValue: -3.7038,
|
|
|
|
|
formatValue: (v: number) => v.toFixed(4),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'altitude',
|
|
|
|
|
label: 'Altitude',
|
|
|
|
|
min: -100,
|
|
|
|
|
max: 9000,
|
|
|
|
|
step: 1,
|
|
|
|
|
unit: 'm',
|
|
|
|
|
defaultValue: 667,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'speed',
|
|
|
|
|
label: 'Speed',
|
|
|
|
|
min: 0,
|
|
|
|
|
max: 200,
|
|
|
|
|
step: 0.5,
|
|
|
|
|
unit: 'kn',
|
|
|
|
|
defaultValue: 0,
|
|
|
|
|
formatValue: oneDecimal,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
defaultValues: { lat: 40.4168, lng: -3.7038, altitude: 667, speed: 0 },
|
|
|
|
|
},
|
|
|
|
|
|
2026-03-19 11:52:46 +07:00
|
|
|
// ── HC-SR04 Ultrasonic Distance ───────────────────────────────────────────
|
|
|
|
|
'hc-sr04': {
|
|
|
|
|
title: 'Ultrasonic Distance Sensor',
|
|
|
|
|
controls: [
|
2026-04-22 02:45:45 +07:00
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'distance',
|
|
|
|
|
label: 'Distance',
|
|
|
|
|
min: 2,
|
|
|
|
|
max: 400,
|
|
|
|
|
step: 1,
|
|
|
|
|
unit: 'cm',
|
|
|
|
|
defaultValue: 10,
|
|
|
|
|
},
|
2026-03-19 11:52:46 +07:00
|
|
|
],
|
|
|
|
|
defaultValues: { distance: 10 },
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// ── Photoresistor (LDR) ───────────────────────────────────────────────────
|
|
|
|
|
'photoresistor-sensor': {
|
|
|
|
|
title: 'Photoresistor (LDR)',
|
|
|
|
|
controls: [
|
2026-04-22 02:45:45 +07:00
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'lux',
|
|
|
|
|
label: 'Illumination',
|
2026-04-24 21:42:36 +07:00
|
|
|
min: 0,
|
|
|
|
|
max: 1000,
|
|
|
|
|
step: 1,
|
|
|
|
|
unit: 'lux',
|
|
|
|
|
defaultValue: 500,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
defaultValues: { lux: 500 },
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// ── Photodiode ────────────────────────────────────────────────────────────
|
|
|
|
|
photodiode: {
|
|
|
|
|
title: 'Photodiode',
|
|
|
|
|
controls: [
|
|
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'lux',
|
|
|
|
|
label: 'Illumination',
|
2026-04-22 02:45:45 +07:00
|
|
|
min: 0,
|
|
|
|
|
max: 1000,
|
|
|
|
|
step: 1,
|
|
|
|
|
unit: 'lux',
|
|
|
|
|
defaultValue: 500,
|
|
|
|
|
},
|
2026-03-19 11:52:46 +07:00
|
|
|
],
|
|
|
|
|
defaultValues: { lux: 500 },
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// ── PIR Motion Sensor ─────────────────────────────────────────────────────
|
|
|
|
|
'pir-motion-sensor': {
|
|
|
|
|
title: 'PIR Motion Sensor',
|
2026-04-22 02:45:45 +07:00
|
|
|
controls: [{ type: 'button', key: 'trigger', label: 'Simulate motion' }],
|
2026-03-19 11:52:46 +07:00
|
|
|
defaultValues: {},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// ── NTC Temperature Sensor ────────────────────────────────────────────────
|
|
|
|
|
'ntc-temperature-sensor': {
|
|
|
|
|
title: 'NTC Temperature Sensor',
|
|
|
|
|
controls: [
|
2026-04-22 02:45:45 +07:00
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'temperature',
|
|
|
|
|
label: 'Temperature',
|
|
|
|
|
min: -40,
|
|
|
|
|
max: 125,
|
|
|
|
|
step: 1,
|
|
|
|
|
unit: '°C',
|
|
|
|
|
defaultValue: 25,
|
|
|
|
|
formatValue: oneDecimal,
|
|
|
|
|
},
|
2026-03-19 11:52:46 +07:00
|
|
|
],
|
|
|
|
|
defaultValues: { temperature: 25 },
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// ── Gas Sensor (MQ-series) ────────────────────────────────────────────────
|
|
|
|
|
'gas-sensor': {
|
|
|
|
|
title: 'Gas Sensor (MQ-series)',
|
|
|
|
|
controls: [
|
2026-04-22 02:45:45 +07:00
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'gasLevel',
|
|
|
|
|
label: 'Gas Level',
|
|
|
|
|
min: 0,
|
|
|
|
|
max: 1023,
|
|
|
|
|
step: 1,
|
|
|
|
|
unit: '',
|
|
|
|
|
defaultValue: 100,
|
|
|
|
|
},
|
2026-03-19 11:52:46 +07:00
|
|
|
],
|
|
|
|
|
defaultValues: { gasLevel: 100 },
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// ── Flame Sensor ──────────────────────────────────────────────────────────
|
|
|
|
|
'flame-sensor': {
|
|
|
|
|
title: 'Flame Sensor',
|
|
|
|
|
controls: [
|
2026-04-22 02:45:45 +07:00
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'intensity',
|
|
|
|
|
label: 'Flame Intensity',
|
|
|
|
|
min: 0,
|
|
|
|
|
max: 1023,
|
|
|
|
|
step: 1,
|
|
|
|
|
unit: '',
|
|
|
|
|
defaultValue: 0,
|
|
|
|
|
},
|
2026-03-19 11:52:46 +07:00
|
|
|
],
|
|
|
|
|
defaultValues: { intensity: 0 },
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// ── Big Sound Sensor (FC-04) ──────────────────────────────────────────────
|
|
|
|
|
'big-sound-sensor': {
|
|
|
|
|
title: 'Sound Sensor',
|
|
|
|
|
controls: [
|
2026-04-22 02:45:45 +07:00
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'soundLevel',
|
|
|
|
|
label: 'Sound Level',
|
|
|
|
|
min: 0,
|
|
|
|
|
max: 1023,
|
|
|
|
|
step: 1,
|
|
|
|
|
unit: '',
|
|
|
|
|
defaultValue: 512,
|
|
|
|
|
},
|
2026-03-19 11:52:46 +07:00
|
|
|
],
|
|
|
|
|
defaultValues: { soundLevel: 512 },
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// ── Small Sound Sensor (KY-038) ───────────────────────────────────────────
|
|
|
|
|
'small-sound-sensor': {
|
|
|
|
|
title: 'Sound Sensor (KY-038)',
|
|
|
|
|
controls: [
|
2026-04-22 02:45:45 +07:00
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'soundLevel',
|
|
|
|
|
label: 'Sound Level',
|
|
|
|
|
min: 0,
|
|
|
|
|
max: 1023,
|
|
|
|
|
step: 1,
|
|
|
|
|
unit: '',
|
|
|
|
|
defaultValue: 512,
|
|
|
|
|
},
|
2026-03-19 11:52:46 +07:00
|
|
|
],
|
|
|
|
|
defaultValues: { soundLevel: 512 },
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// ── Tilt Switch ───────────────────────────────────────────────────────────
|
|
|
|
|
'tilt-switch': {
|
|
|
|
|
title: 'Tilt Switch',
|
2026-04-22 02:45:45 +07:00
|
|
|
controls: [{ type: 'button', key: 'toggle', label: 'Toggle tilt' }],
|
2026-03-19 11:52:46 +07:00
|
|
|
defaultValues: {},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// ── Analog Joystick ───────────────────────────────────────────────────────
|
|
|
|
|
'analog-joystick': {
|
|
|
|
|
title: 'Analog Joystick',
|
|
|
|
|
controls: [
|
2026-04-22 02:45:45 +07:00
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'xAxis',
|
|
|
|
|
label: 'X Axis',
|
|
|
|
|
min: -512,
|
|
|
|
|
max: 512,
|
|
|
|
|
step: 1,
|
|
|
|
|
unit: '',
|
|
|
|
|
defaultValue: 0,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'slider',
|
|
|
|
|
key: 'yAxis',
|
|
|
|
|
label: 'Y Axis',
|
|
|
|
|
min: -512,
|
|
|
|
|
max: 512,
|
|
|
|
|
step: 1,
|
|
|
|
|
unit: '',
|
|
|
|
|
defaultValue: 0,
|
|
|
|
|
},
|
2026-03-19 11:52:46 +07:00
|
|
|
],
|
|
|
|
|
defaultValues: { xAxis: 0, yAxis: 0 },
|
|
|
|
|
},
|
|
|
|
|
};
|
2026-07-24 02:32:14 +07:00
|
|
|
|
|
|
|
|
// ── 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<string, SensorControlDef> = {};
|
|
|
|
|
|
|
|
|
|
export function registerSensorControls(defs: Record<string, SensorControlDef>): void {
|
|
|
|
|
Object.assign(proSensorControls, defs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getSensorControl(id: string | null | undefined): SensorControlDef | undefined {
|
|
|
|
|
if (!id) return undefined;
|
|
|
|
|
return SENSOR_CONTROLS[id] ?? proSensorControls[id];
|
|
|
|
|
}
|