2026-03-08 12:20:21 +07:00
|
|
|
|
/**
|
|
|
|
|
|
* SensorParts.ts — Simulation logic for sensors, stepper motor, and NeoPixel devices.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Implements:
|
|
|
|
|
|
* - tilt-switch
|
|
|
|
|
|
* - ntc-temperature-sensor
|
2026-04-24 21:42:36 +07:00
|
|
|
|
* - photodiode
|
2026-03-08 12:20:21 +07:00
|
|
|
|
* - gas-sensor (MQ-series)
|
|
|
|
|
|
* - flame-sensor
|
|
|
|
|
|
* - heart-beat-sensor
|
|
|
|
|
|
* - big-sound-sensor
|
|
|
|
|
|
* - small-sound-sensor
|
|
|
|
|
|
* - stepper-motor (NEMA full-step decode)
|
|
|
|
|
|
* - led-ring (WS2812B NeoPixel ring)
|
|
|
|
|
|
* - neopixel-matrix (WS2812B NeoPixel matrix)
|
2026-03-19 11:52:46 +07:00
|
|
|
|
* - pir-motion-sensor
|
|
|
|
|
|
* - hc-sr04
|
2026-03-08 12:20:21 +07:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { PartSimulationRegistry } from './PartSimulationRegistry';
|
fix(ntc): the temperature sensor read wrong on 3.3V boards, and three examples decoded backwards
Two independent defects, both visible as "the NTC reports the wrong
temperature" (issue #233).
1. The part built its divider against a hard-coded 5 V rail, while the SPICE
path used the board's real rail. They only agreed on AVR. On a 3.3 V board
the injected node sat above the ADC's full scale: the slider at 25 C read
~1.4 C, and every slider value below ~11 C saturated to the same number, so
-40, 0 and 10 C were indistinguishable. The rail now comes from the board
(analogRailVolts), matching setAdcVoltage's own AVR/RP2040/ESP32 split.
2. The divider convention was corrected in 608c2538 (VCC -> R_pull -> OUT ->
R_NTC -> GND) but data/examples.ts was not updated with it, so three of the
four bundled NTC sketches still decoded the OTHER topology. Their readings
came out mirrored about 25 C: the uno-ntc example printed -3.42 C with the
slider at 60 C - the exact symptom dd22bcfe had recorded as the bug. Fixed
in uno-ntc, pico-ntc and attiny85-ntc-sensor; examples-circuits.ts was
already right.
The sibling analog sensors (gas, flame, heart-beat, sound) map their sliders
0..1023 onto the same hard-coded 5 V and will saturate the same way on 3.3 V
boards. Left alone here: that is not what #233 reports, and each one needs its
examples re-checked before changing.
2026-08-03 12:33:20 +07:00
|
|
|
|
import { setAdcVoltage, emitPropertyChange, analogRailVolts } from './partUtils';
|
2026-03-19 11:52:46 +07:00
|
|
|
|
import { registerSensorUpdate, unregisterSensorUpdate } from '../SensorUpdateRegistry';
|
2026-03-08 12:20:21 +07:00
|
|
|
|
|
|
|
|
|
|
// ─── Tilt Switch ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Tilt switch — click the element to toggle between tilted (OUT HIGH) and
|
2026-03-19 11:52:46 +07:00
|
|
|
|
* upright (OUT LOW). Also controllable via SensorControlPanel "Toggle tilt" button.
|
2026-03-08 12:20:21 +07:00
|
|
|
|
*/
|
|
|
|
|
|
PartSimulationRegistry.register('tilt-switch', {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
attachEvents: (element, simulator, getArduinoPinHelper, componentId) => {
|
|
|
|
|
|
const pin = getArduinoPinHelper('OUT');
|
|
|
|
|
|
if (pin === null) return () => {};
|
2026-03-08 12:20:21 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
let tilted = false;
|
2026-03-08 12:20:21 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
const triggerToggle = () => {
|
|
|
|
|
|
tilted = !tilted;
|
|
|
|
|
|
simulator.setPinState(pin, tilted);
|
|
|
|
|
|
console.log(`[TiltSwitch] pin ${pin} → ${tilted ? 'HIGH (tilted)' : 'LOW (upright)'}`);
|
|
|
|
|
|
};
|
2026-03-08 12:20:21 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
// Start LOW (upright)
|
|
|
|
|
|
simulator.setPinState(pin, false);
|
|
|
|
|
|
element.addEventListener('click', triggerToggle);
|
2026-03-19 11:52:46 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
// SensorControlPanel callback
|
|
|
|
|
|
registerSensorUpdate(componentId, (values) => {
|
|
|
|
|
|
if (values.toggle === true) triggerToggle();
|
|
|
|
|
|
});
|
2026-03-19 11:52:46 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
return () => {
|
|
|
|
|
|
element.removeEventListener('click', triggerToggle);
|
|
|
|
|
|
unregisterSensorUpdate(componentId);
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
2026-03-08 12:20:21 +07:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── NTC Temperature Sensor ──────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-19 11:52:46 +07:00
|
|
|
|
* NTC thermistor sensor — injects analog voltage representing temperature.
|
|
|
|
|
|
* Default 25°C → 2.5V. SensorControlPanel slider adjusts temperature.
|
|
|
|
|
|
*
|
2026-06-16 04:21:36 +07:00
|
|
|
|
* The injected OUT voltage uses the SAME β-model voltage divider the circuit
|
|
|
|
|
|
* (and the example sketch) assume: a 10k pull-up from VCC to OUT and the NTC
|
fix(ntc): the temperature sensor read wrong on 3.3V boards, and three examples decoded backwards
Two independent defects, both visible as "the NTC reports the wrong
temperature" (issue #233).
1. The part built its divider against a hard-coded 5 V rail, while the SPICE
path used the board's real rail. They only agreed on AVR. On a 3.3 V board
the injected node sat above the ADC's full scale: the slider at 25 C read
~1.4 C, and every slider value below ~11 C saturated to the same number, so
-40, 0 and 10 C were indistinguishable. The rail now comes from the board
(analogRailVolts), matching setAdcVoltage's own AVR/RP2040/ESP32 split.
2. The divider convention was corrected in 608c2538 (VCC -> R_pull -> OUT ->
R_NTC -> GND) but data/examples.ts was not updated with it, so three of the
four bundled NTC sketches still decoded the OTHER topology. Their readings
came out mirrored about 25 C: the uno-ntc example printed -3.42 C with the
slider at 60 C - the exact symptom dd22bcfe had recorded as the bug. Fixed
in uno-ntc, pico-ntc and attiny85-ntc-sensor; examples-circuits.ts was
already right.
The sibling analog sensors (gas, flame, heart-beat, sound) map their sliders
0..1023 onto the same hard-coded 5 V and will saturate the same way on 3.3 V
boards. Left alone here: that is not what #233 reports, and each one needs its
examples re-checked before changing.
2026-08-03 12:33:20 +07:00
|
|
|
|
* from OUT to GND, so V_OUT = VCC · R_ntc / (R_ntc + R_pull) with
|
2026-06-16 04:21:36 +07:00
|
|
|
|
* R_ntc(T) = R0 · exp(β · (1/T − 1/T0)). This makes the injected ADC voltage
|
|
|
|
|
|
* decode straight back to the slider value (set 50°C → read 50°C). When the
|
|
|
|
|
|
* electrical (SPICE) engine is active it drives A0 from the ngspice solve
|
|
|
|
|
|
* instead, using the matching topology in componentToSpice.ts — both agree.
|
fix(ntc): the temperature sensor read wrong on 3.3V boards, and three examples decoded backwards
Two independent defects, both visible as "the NTC reports the wrong
temperature" (issue #233).
1. The part built its divider against a hard-coded 5 V rail, while the SPICE
path used the board's real rail. They only agreed on AVR. On a 3.3 V board
the injected node sat above the ADC's full scale: the slider at 25 C read
~1.4 C, and every slider value below ~11 C saturated to the same number, so
-40, 0 and 10 C were indistinguishable. The rail now comes from the board
(analogRailVolts), matching setAdcVoltage's own AVR/RP2040/ESP32 split.
2. The divider convention was corrected in 608c2538 (VCC -> R_pull -> OUT ->
R_NTC -> GND) but data/examples.ts was not updated with it, so three of the
four bundled NTC sketches still decoded the OTHER topology. Their readings
came out mirrored about 25 C: the uno-ntc example printed -3.42 C with the
slider at 60 C - the exact symptom dd22bcfe had recorded as the bug. Fixed
in uno-ntc, pico-ntc and attiny85-ntc-sensor; examples-circuits.ts was
already right.
The sibling analog sensors (gas, flame, heart-beat, sound) map their sliders
0..1023 onto the same hard-coded 5 V and will saturate the same way on 3.3 V
boards. Left alone here: that is not what #233 reports, and each one needs its
examples re-checked before changing.
2026-08-03 12:33:20 +07:00
|
|
|
|
*
|
|
|
|
|
|
* VCC is the rail of the board the sensor is wired to, not a constant 5. The
|
|
|
|
|
|
* divider used to assume 5 V everywhere, which on a 3.3 V board put the node
|
|
|
|
|
|
* above the ADC's full scale: the slider read 1.4°C where it said 25, and
|
|
|
|
|
|
* everything below ~11°C pinned at the same saturated value (issue #233). The
|
|
|
|
|
|
* SPICE path already used the board's rail, so the two only agreed on AVR.
|
2026-03-08 12:20:21 +07:00
|
|
|
|
*/
|
|
|
|
|
|
PartSimulationRegistry.register('ntc-temperature-sensor', {
|
2026-06-16 04:21:36 +07:00
|
|
|
|
attachEvents: (_element, simulator, getArduinoPinHelper, componentId) => {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
const pin = getArduinoPinHelper('OUT');
|
|
|
|
|
|
|
2026-06-16 04:21:36 +07:00
|
|
|
|
const NTC_R0 = 10_000;
|
|
|
|
|
|
const NTC_BETA = 3950;
|
|
|
|
|
|
const R_PULL = 10_000;
|
fix(ntc): the temperature sensor read wrong on 3.3V boards, and three examples decoded backwards
Two independent defects, both visible as "the NTC reports the wrong
temperature" (issue #233).
1. The part built its divider against a hard-coded 5 V rail, while the SPICE
path used the board's real rail. They only agreed on AVR. On a 3.3 V board
the injected node sat above the ADC's full scale: the slider at 25 C read
~1.4 C, and every slider value below ~11 C saturated to the same number, so
-40, 0 and 10 C were indistinguishable. The rail now comes from the board
(analogRailVolts), matching setAdcVoltage's own AVR/RP2040/ESP32 split.
2. The divider convention was corrected in 608c2538 (VCC -> R_pull -> OUT ->
R_NTC -> GND) but data/examples.ts was not updated with it, so three of the
four bundled NTC sketches still decoded the OTHER topology. Their readings
came out mirrored about 25 C: the uno-ntc example printed -3.42 C with the
slider at 60 C - the exact symptom dd22bcfe had recorded as the bug. Fixed
in uno-ntc, pico-ntc and attiny85-ntc-sensor; examples-circuits.ts was
already right.
The sibling analog sensors (gas, flame, heart-beat, sound) map their sliders
0..1023 onto the same hard-coded 5 V and will saturate the same way on 3.3 V
boards. Left alone here: that is not what #233 reports, and each one needs its
examples re-checked before changing.
2026-08-03 12:33:20 +07:00
|
|
|
|
const vcc = analogRailVolts(simulator);
|
2026-06-16 04:21:36 +07:00
|
|
|
|
const tempToVolts = (temp: number) => {
|
|
|
|
|
|
const rNtc = NTC_R0 * Math.exp(NTC_BETA * (1 / (temp + 273.15) - 1 / 298.15));
|
fix(ntc): the temperature sensor read wrong on 3.3V boards, and three examples decoded backwards
Two independent defects, both visible as "the NTC reports the wrong
temperature" (issue #233).
1. The part built its divider against a hard-coded 5 V rail, while the SPICE
path used the board's real rail. They only agreed on AVR. On a 3.3 V board
the injected node sat above the ADC's full scale: the slider at 25 C read
~1.4 C, and every slider value below ~11 C saturated to the same number, so
-40, 0 and 10 C were indistinguishable. The rail now comes from the board
(analogRailVolts), matching setAdcVoltage's own AVR/RP2040/ESP32 split.
2. The divider convention was corrected in 608c2538 (VCC -> R_pull -> OUT ->
R_NTC -> GND) but data/examples.ts was not updated with it, so three of the
four bundled NTC sketches still decoded the OTHER topology. Their readings
came out mirrored about 25 C: the uno-ntc example printed -3.42 C with the
slider at 60 C - the exact symptom dd22bcfe had recorded as the bug. Fixed
in uno-ntc, pico-ntc and attiny85-ntc-sensor; examples-circuits.ts was
already right.
The sibling analog sensors (gas, flame, heart-beat, sound) map their sliders
0..1023 onto the same hard-coded 5 V and will saturate the same way on 3.3 V
boards. Left alone here: that is not what #233 reports, and each one needs its
examples re-checked before changing.
2026-08-03 12:33:20 +07:00
|
|
|
|
return Math.max(0, Math.min(vcc, vcc * (rNtc / (rNtc + R_PULL))));
|
2026-06-16 04:21:36 +07:00
|
|
|
|
};
|
2026-04-22 02:45:45 +07:00
|
|
|
|
|
|
|
|
|
|
// Room temperature default
|
|
|
|
|
|
if (pin !== null) setAdcVoltage(simulator, pin, tempToVolts(25));
|
|
|
|
|
|
|
|
|
|
|
|
registerSensorUpdate(componentId, (values) => {
|
|
|
|
|
|
if ('temperature' in values) {
|
|
|
|
|
|
if (pin !== null) {
|
|
|
|
|
|
setAdcVoltage(simulator, pin, tempToVolts(values.temperature as number));
|
|
|
|
|
|
}
|
|
|
|
|
|
// Mirror to store — the SPICE ntc-temperature-sensor handler
|
|
|
|
|
|
// reads comp.properties.temperature when computing R_ntc.
|
|
|
|
|
|
emitPropertyChange(componentId, 'temperature', values.temperature);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-03-19 11:52:46 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
return () => {
|
|
|
|
|
|
unregisterSensorUpdate(componentId);
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
2026-03-08 12:20:21 +07:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-24 21:42:36 +07:00
|
|
|
|
// ─── Photodiode ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Photodiode — 2-terminal passive, reverse-biased light sensor. The SPICE
|
|
|
|
|
|
* emitter (componentToSpice.ts) reads `properties.lux` and drives a current
|
|
|
|
|
|
* source (100 nA/lux). This handler wires the SensorControlPanel slider so
|
|
|
|
|
|
* moving it updates the store → netlist rebuild → re-solve.
|
|
|
|
|
|
*/
|
|
|
|
|
|
PartSimulationRegistry.register('photodiode', {
|
|
|
|
|
|
attachEvents: (_element, _simulator, _getArduinoPinHelper, componentId) => {
|
|
|
|
|
|
registerSensorUpdate(componentId, (values) => {
|
|
|
|
|
|
if ('lux' in values) {
|
|
|
|
|
|
emitPropertyChange(componentId, 'lux', values.lux);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
unregisterSensorUpdate(componentId);
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-08 12:20:21 +07:00
|
|
|
|
// ─── Gas Sensor (MQ-series) ──────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-19 11:52:46 +07:00
|
|
|
|
* Gas sensor — injects analog voltage on AOUT.
|
|
|
|
|
|
* Default 1.5V (clean air / low gas). SensorControlPanel slider adjusts level (0–1023).
|
|
|
|
|
|
* Higher value → higher voltage (more gas detected).
|
2026-03-08 12:20:21 +07:00
|
|
|
|
*/
|
|
|
|
|
|
PartSimulationRegistry.register('gas-sensor', {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
attachEvents: (element, simulator, getArduinoPinHelper, componentId) => {
|
|
|
|
|
|
const pinAOUT = getArduinoPinHelper('AOUT');
|
|
|
|
|
|
const pinDOUT = getArduinoPinHelper('DOUT');
|
|
|
|
|
|
const pinManager = (simulator as any).pinManager;
|
|
|
|
|
|
|
|
|
|
|
|
const el = element as any;
|
|
|
|
|
|
el.ledPower = true;
|
|
|
|
|
|
|
|
|
|
|
|
const unsubscribers: (() => void)[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
// Inject baseline analog voltage (1.5V ≈ clean air / low gas)
|
|
|
|
|
|
if (pinAOUT !== null) {
|
|
|
|
|
|
setAdcVoltage(simulator, pinAOUT, 1.5);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// DOUT from Arduino → threshold LED indicator
|
|
|
|
|
|
if (pinDOUT !== null && pinManager) {
|
|
|
|
|
|
unsubscribers.push(
|
|
|
|
|
|
pinManager.onPinChange(pinDOUT, (_: number, state: boolean) => {
|
|
|
|
|
|
el.ledD0 = state;
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Allow element to update analog value if it fires input events
|
|
|
|
|
|
const onInput = () => {
|
|
|
|
|
|
const val = (el as any).value;
|
|
|
|
|
|
if (val !== undefined && pinAOUT !== null) {
|
|
|
|
|
|
setAdcVoltage(simulator, pinAOUT, (val / 1023.0) * 5.0);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
element.addEventListener('input', onInput);
|
|
|
|
|
|
unsubscribers.push(() => element.removeEventListener('input', onInput));
|
|
|
|
|
|
|
|
|
|
|
|
registerSensorUpdate(componentId, (values) => {
|
|
|
|
|
|
if ('gasLevel' in values && pinAOUT !== null) {
|
|
|
|
|
|
setAdcVoltage(simulator, pinAOUT, ((values.gasLevel as number) / 1023) * 5.0);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-03-19 11:52:46 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
return () => {
|
|
|
|
|
|
unsubscribers.forEach((u) => u());
|
|
|
|
|
|
unregisterSensorUpdate(componentId);
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
2026-03-08 12:20:21 +07:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Flame Sensor ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-19 11:52:46 +07:00
|
|
|
|
* Flame sensor — injects analog voltage on AOUT.
|
|
|
|
|
|
* Default 4.5V (no flame). SensorControlPanel slider: 0 = no flame (high V),
|
|
|
|
|
|
* 1023 = intense flame (low V).
|
2026-03-08 12:20:21 +07:00
|
|
|
|
*/
|
|
|
|
|
|
PartSimulationRegistry.register('flame-sensor', {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
attachEvents: (element, simulator, getArduinoPinHelper, componentId) => {
|
|
|
|
|
|
const pinAOUT = getArduinoPinHelper('AOUT');
|
|
|
|
|
|
const pinDOUT = getArduinoPinHelper('DOUT');
|
|
|
|
|
|
const pinManager = (simulator as any).pinManager;
|
|
|
|
|
|
|
|
|
|
|
|
const el = element as any;
|
|
|
|
|
|
el.ledPower = true;
|
|
|
|
|
|
|
|
|
|
|
|
const unsubscribers: (() => void)[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (pinAOUT !== null) {
|
|
|
|
|
|
setAdcVoltage(simulator, pinAOUT, 4.5); // no flame = high voltage
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (pinDOUT !== null && pinManager) {
|
|
|
|
|
|
unsubscribers.push(
|
|
|
|
|
|
pinManager.onPinChange(pinDOUT, (_: number, state: boolean) => {
|
|
|
|
|
|
el.ledSignal = state;
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const onInput = () => {
|
|
|
|
|
|
const val = (el as any).value;
|
|
|
|
|
|
if (val !== undefined && pinAOUT !== null) {
|
|
|
|
|
|
setAdcVoltage(simulator, pinAOUT, (val / 1023.0) * 5.0);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
element.addEventListener('input', onInput);
|
|
|
|
|
|
unsubscribers.push(() => element.removeEventListener('input', onInput));
|
|
|
|
|
|
|
|
|
|
|
|
registerSensorUpdate(componentId, (values) => {
|
|
|
|
|
|
if ('intensity' in values && pinAOUT !== null) {
|
|
|
|
|
|
// 0 = no flame → high voltage (4.5V); 1023 = flame → low voltage (0.2V)
|
|
|
|
|
|
const volts = 5.0 - ((values.intensity as number) / 1023) * 5.0;
|
|
|
|
|
|
setAdcVoltage(simulator, pinAOUT, volts);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-03-19 11:52:46 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
return () => {
|
|
|
|
|
|
unsubscribers.forEach((u) => u());
|
|
|
|
|
|
unregisterSensorUpdate(componentId);
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
2026-03-08 12:20:21 +07:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Heart Beat Sensor ───────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Heart beat sensor — simulates a 60 BPM signal on OUT pin.
|
|
|
|
|
|
* Every 1000ms: briefly pulls OUT HIGH for 100ms, then LOW again.
|
|
|
|
|
|
*/
|
|
|
|
|
|
PartSimulationRegistry.register('heart-beat-sensor', {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
attachEvents: (element, simulator, getArduinoPinHelper) => {
|
|
|
|
|
|
const pin = getArduinoPinHelper('OUT');
|
|
|
|
|
|
if (pin === null) return () => {};
|
2026-03-08 12:20:21 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
simulator.setPinState(pin, false);
|
2026-03-08 12:20:21 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
const intervalId = setInterval(() => {
|
|
|
|
|
|
simulator.setPinState(pin, true); // pulse HIGH
|
|
|
|
|
|
setTimeout(() => simulator.setPinState(pin, false), 100);
|
|
|
|
|
|
}, 1000);
|
2026-03-08 12:20:21 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
return () => clearInterval(intervalId);
|
|
|
|
|
|
},
|
2026-03-08 12:20:21 +07:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Big Sound Sensor ────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-19 11:52:46 +07:00
|
|
|
|
* Big sound sensor (FC-04) — injects mid-range analog on AOUT.
|
|
|
|
|
|
* SensorControlPanel slider adjusts sound level (0–1023).
|
2026-03-08 12:20:21 +07:00
|
|
|
|
*/
|
|
|
|
|
|
PartSimulationRegistry.register('big-sound-sensor', {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
attachEvents: (element, simulator, getArduinoPinHelper, componentId) => {
|
|
|
|
|
|
const pinAOUT = getArduinoPinHelper('AOUT');
|
|
|
|
|
|
const pinDOUT = getArduinoPinHelper('DOUT');
|
|
|
|
|
|
const pinManager = (simulator as any).pinManager;
|
|
|
|
|
|
|
|
|
|
|
|
const el = element as any;
|
|
|
|
|
|
el.led2 = true; // Power LED
|
|
|
|
|
|
|
|
|
|
|
|
const unsubscribers: (() => void)[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (pinAOUT !== null) {
|
|
|
|
|
|
setAdcVoltage(simulator, pinAOUT, 2.5);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (pinDOUT !== null && pinManager) {
|
|
|
|
|
|
unsubscribers.push(
|
|
|
|
|
|
pinManager.onPinChange(pinDOUT, (_: number, state: boolean) => {
|
|
|
|
|
|
el.led1 = state;
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const onInput = () => {
|
|
|
|
|
|
const val = (el as any).value;
|
|
|
|
|
|
if (val !== undefined && pinAOUT !== null) {
|
|
|
|
|
|
setAdcVoltage(simulator, pinAOUT, (val / 1023.0) * 5.0);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
element.addEventListener('input', onInput);
|
|
|
|
|
|
unsubscribers.push(() => element.removeEventListener('input', onInput));
|
|
|
|
|
|
|
|
|
|
|
|
registerSensorUpdate(componentId, (values) => {
|
|
|
|
|
|
if ('soundLevel' in values && pinAOUT !== null) {
|
|
|
|
|
|
setAdcVoltage(simulator, pinAOUT, ((values.soundLevel as number) / 1023) * 5.0);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-03-19 11:52:46 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
return () => {
|
|
|
|
|
|
unsubscribers.forEach((u) => u());
|
|
|
|
|
|
unregisterSensorUpdate(componentId);
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
2026-03-08 12:20:21 +07:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Small Sound Sensor ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-19 11:52:46 +07:00
|
|
|
|
* Small sound sensor (KY-038) — injects mid-range analog on AOUT.
|
|
|
|
|
|
* SensorControlPanel slider adjusts sound level (0–1023).
|
2026-03-08 12:20:21 +07:00
|
|
|
|
*/
|
|
|
|
|
|
PartSimulationRegistry.register('small-sound-sensor', {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
attachEvents: (element, simulator, getArduinoPinHelper, componentId) => {
|
|
|
|
|
|
const pinAOUT = getArduinoPinHelper('AOUT');
|
|
|
|
|
|
const pinDOUT = getArduinoPinHelper('DOUT');
|
|
|
|
|
|
const pinManager = (simulator as any).pinManager;
|
|
|
|
|
|
|
|
|
|
|
|
const el = element as any;
|
|
|
|
|
|
el.ledPower = true;
|
|
|
|
|
|
|
|
|
|
|
|
const unsubscribers: (() => void)[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (pinAOUT !== null) {
|
|
|
|
|
|
setAdcVoltage(simulator, pinAOUT, 2.5);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (pinDOUT !== null && pinManager) {
|
|
|
|
|
|
unsubscribers.push(
|
|
|
|
|
|
pinManager.onPinChange(pinDOUT, (_: number, state: boolean) => {
|
|
|
|
|
|
el.ledSignal = state;
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const onInput = () => {
|
|
|
|
|
|
const val = (el as any).value;
|
|
|
|
|
|
if (val !== undefined && pinAOUT !== null) {
|
|
|
|
|
|
setAdcVoltage(simulator, pinAOUT, (val / 1023.0) * 5.0);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
element.addEventListener('input', onInput);
|
|
|
|
|
|
unsubscribers.push(() => element.removeEventListener('input', onInput));
|
|
|
|
|
|
|
|
|
|
|
|
registerSensorUpdate(componentId, (values) => {
|
|
|
|
|
|
if ('soundLevel' in values && pinAOUT !== null) {
|
|
|
|
|
|
setAdcVoltage(simulator, pinAOUT, ((values.soundLevel as number) / 1023) * 5.0);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-03-19 11:52:46 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
return () => {
|
|
|
|
|
|
unsubscribers.forEach((u) => u());
|
|
|
|
|
|
unregisterSensorUpdate(componentId);
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
2026-03-08 12:20:21 +07:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Stepper Motor (NEMA full-step decode) ───────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Stepper motor — monitors the 4 coil pins (A-, A+, B+, B-).
|
|
|
|
|
|
* Uses a full-step lookup table to detect direction of rotation and
|
|
|
|
|
|
* accumulates the shaft angle (1.8° per step = 200 steps per revolution).
|
|
|
|
|
|
*/
|
|
|
|
|
|
PartSimulationRegistry.register('stepper-motor', {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
attachEvents: (element, simulator, getArduinoPinHelper) => {
|
|
|
|
|
|
const pinManager = (simulator as any).pinManager;
|
|
|
|
|
|
if (!pinManager) return () => {};
|
2026-03-08 12:20:21 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
const el = element as any;
|
|
|
|
|
|
const STEP_ANGLE = 1.8; // degrees per step
|
|
|
|
|
|
|
|
|
|
|
|
const pinAMinus = getArduinoPinHelper('A-');
|
|
|
|
|
|
const pinAPlus = getArduinoPinHelper('A+');
|
|
|
|
|
|
const pinBPlus = getArduinoPinHelper('B+');
|
|
|
|
|
|
const pinBMinus = getArduinoPinHelper('B-');
|
|
|
|
|
|
|
|
|
|
|
|
const coils = { aMinus: false, aPlus: false, bPlus: false, bMinus: false };
|
|
|
|
|
|
let cumAngle = el.angle ?? 0;
|
2026-06-01 02:28:29 +07:00
|
|
|
|
// Track the magnetic-field electrical angle instead of matching a fixed
|
|
|
|
|
|
// coil table. The rotor follows the net field vector of the two coils, so
|
|
|
|
|
|
// this works for ANY drive mode the firmware (or a driver) produces:
|
|
|
|
|
|
// wave-drive (one coil), full-step two-phase (two coils), or half-step.
|
|
|
|
|
|
let prevField = Number.NaN; // previous field angle in radians, NaN = unset
|
2026-04-22 02:45:45 +07:00
|
|
|
|
|
|
|
|
|
|
function onCoilChange() {
|
2026-06-01 02:28:29 +07:00
|
|
|
|
// Coil currents: +1 / 0 / -1 from the H-bridge terminal pair.
|
|
|
|
|
|
const a = (coils.aPlus ? 1 : 0) - (coils.aMinus ? 1 : 0);
|
|
|
|
|
|
const b = (coils.bPlus ? 1 : 0) - (coils.bMinus ? 1 : 0);
|
|
|
|
|
|
if (a === 0 && b === 0) return; // no field → rotor holds position
|
|
|
|
|
|
|
|
|
|
|
|
const field = Math.atan2(b, a); // electrical angle of the field vector
|
|
|
|
|
|
if (Number.isNaN(prevField)) {
|
|
|
|
|
|
prevField = field;
|
2026-04-22 02:45:45 +07:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-06-01 02:28:29 +07:00
|
|
|
|
// Shortest signed rotation between the two field angles.
|
|
|
|
|
|
let delta = field - prevField;
|
|
|
|
|
|
while (delta > Math.PI) delta -= 2 * Math.PI;
|
|
|
|
|
|
while (delta < -Math.PI) delta += 2 * Math.PI;
|
|
|
|
|
|
prevField = field;
|
|
|
|
|
|
|
|
|
|
|
|
// One quarter electrical turn (90°, π/2 rad) = one full mechanical step.
|
|
|
|
|
|
cumAngle += (delta / (Math.PI / 2)) * STEP_ANGLE;
|
2026-04-22 02:45:45 +07:00
|
|
|
|
el.angle = ((cumAngle % 360) + 360) % 360;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const unsubscribers: (() => void)[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (pinAMinus !== null) {
|
|
|
|
|
|
unsubscribers.push(
|
|
|
|
|
|
pinManager.onPinChange(pinAMinus, (_: number, s: boolean) => {
|
|
|
|
|
|
coils.aMinus = s;
|
|
|
|
|
|
onCoilChange();
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (pinAPlus !== null) {
|
|
|
|
|
|
unsubscribers.push(
|
|
|
|
|
|
pinManager.onPinChange(pinAPlus, (_: number, s: boolean) => {
|
|
|
|
|
|
coils.aPlus = s;
|
|
|
|
|
|
onCoilChange();
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (pinBPlus !== null) {
|
|
|
|
|
|
unsubscribers.push(
|
|
|
|
|
|
pinManager.onPinChange(pinBPlus, (_: number, s: boolean) => {
|
|
|
|
|
|
coils.bPlus = s;
|
|
|
|
|
|
onCoilChange();
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (pinBMinus !== null) {
|
|
|
|
|
|
unsubscribers.push(
|
|
|
|
|
|
pinManager.onPinChange(pinBMinus, (_: number, s: boolean) => {
|
|
|
|
|
|
coils.bMinus = s;
|
|
|
|
|
|
onCoilChange();
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return () => unsubscribers.forEach((u) => u());
|
|
|
|
|
|
},
|
2026-03-08 12:20:21 +07:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── WS2812B NeoPixel decode helper ──────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Decode WS2812B bit-stream from DIN pin changes for NeoPixel devices.
|
|
|
|
|
|
*/
|
feat(sim): seated sockets are electrical, and capability is probed
Three generic seams, none naming any board:
The pin tracer gains a socket hop: a component that declares boardSocket
with a board actually seated on it resolves a pad to the SAME-NAMED pin
of that board — seating IS the connection, like a XIAO pushed into a
shield header or a HAT on the 40-pin. Until now a seated shield's
buttons and LEDs were dead unless the user drew wires the real stack
does not have. Wires keep working exactly as before for every other
host (Uno, Mega, Pi, STM32, ...).
setMicrophoneSource now RETURNS whether a bridge actually took the
source, so a part can state that this host has no I2S instead of
pretending to stream into a void — capability reported, never invented.
createNeopixelDecoder is exported: boards are not the only things
carrying an addressable LED, and a part with one on board needs the
same decode rather than a drifting copy.
2026-08-01 03:32:47 +07:00
|
|
|
|
/**
|
|
|
|
|
|
* WS2812/NeoPixel bit-bang decoder, shared. Exported because a board is not
|
|
|
|
|
|
* the only thing that carries an addressable LED: any part with one on board
|
|
|
|
|
|
* (the reSpeaker Lite's RGB, driven by whatever host is wired to it) needs the
|
|
|
|
|
|
* same decode, and re-implementing it per part is how two of them drift.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function createNeopixelDecoder(
|
2026-04-22 02:45:45 +07:00
|
|
|
|
simulator: any,
|
|
|
|
|
|
pinDIN: number,
|
|
|
|
|
|
onPixel: (index: number, r: number, g: number, b: number) => void,
|
2026-03-08 12:20:21 +07:00
|
|
|
|
): () => void {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
const pinManager = simulator.pinManager;
|
|
|
|
|
|
if (!pinManager) return () => {};
|
|
|
|
|
|
|
|
|
|
|
|
const RESET_CYCLES = 800;
|
|
|
|
|
|
const BIT1_THRESHOLD = 8;
|
|
|
|
|
|
|
|
|
|
|
|
let lastRisingCycle = 0;
|
|
|
|
|
|
let lastFallingCycle = 0;
|
|
|
|
|
|
let lastHigh = false;
|
|
|
|
|
|
|
|
|
|
|
|
let bitBuf = 0;
|
|
|
|
|
|
let bitsCollected = 0;
|
|
|
|
|
|
let byteBuf: number[] = [];
|
|
|
|
|
|
let pixelIndex = 0;
|
|
|
|
|
|
|
|
|
|
|
|
const unsub = pinManager.onPinChange(pinDIN, (_: number, high: boolean) => {
|
|
|
|
|
|
const cpu = simulator.cpu ?? (simulator as any).cpu;
|
|
|
|
|
|
const now: number = cpu?.cycles ?? 0;
|
|
|
|
|
|
|
|
|
|
|
|
if (high) {
|
|
|
|
|
|
const lowDur = now - lastFallingCycle;
|
|
|
|
|
|
if (lowDur > RESET_CYCLES) {
|
|
|
|
|
|
pixelIndex = 0;
|
|
|
|
|
|
byteBuf = [];
|
|
|
|
|
|
bitBuf = 0;
|
|
|
|
|
|
bitsCollected = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
lastRisingCycle = now;
|
|
|
|
|
|
lastHigh = true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (lastHigh) {
|
|
|
|
|
|
const highDur = now - lastRisingCycle;
|
|
|
|
|
|
const bit = highDur > BIT1_THRESHOLD ? 1 : 0;
|
|
|
|
|
|
|
|
|
|
|
|
bitBuf = (bitBuf << 1) | bit;
|
|
|
|
|
|
bitsCollected++;
|
|
|
|
|
|
|
|
|
|
|
|
if (bitsCollected === 8) {
|
|
|
|
|
|
byteBuf.push(bitBuf & 0xff);
|
|
|
|
|
|
bitBuf = 0;
|
|
|
|
|
|
bitsCollected = 0;
|
|
|
|
|
|
|
|
|
|
|
|
if (byteBuf.length === 3) {
|
|
|
|
|
|
const g = byteBuf[0];
|
|
|
|
|
|
const r = byteBuf[1];
|
|
|
|
|
|
const b = byteBuf[2];
|
|
|
|
|
|
onPixel(pixelIndex++, r, g, b);
|
|
|
|
|
|
byteBuf = [];
|
|
|
|
|
|
}
|
2026-03-08 12:20:21 +07:00
|
|
|
|
}
|
2026-04-22 02:45:45 +07:00
|
|
|
|
}
|
|
|
|
|
|
lastFallingCycle = now;
|
|
|
|
|
|
lastHigh = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-03-08 12:20:21 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
return unsub;
|
2026-03-08 12:20:21 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─── LED Ring (WS2812B NeoPixel ring) ────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
PartSimulationRegistry.register('led-ring', {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
attachEvents: (element, simulator, getArduinoPinHelper) => {
|
|
|
|
|
|
const pinDIN = getArduinoPinHelper('DIN');
|
|
|
|
|
|
if (pinDIN === null) return () => {};
|
|
|
|
|
|
|
|
|
|
|
|
const el = element as any;
|
|
|
|
|
|
|
|
|
|
|
|
const unsub = createNeopixelDecoder(simulator as any, pinDIN, (index, r, g, b) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
el.setPixel(index, { r, g, b });
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
// setPixel not yet available (element not upgraded) — ignore
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-03-08 12:20:21 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
return unsub;
|
|
|
|
|
|
},
|
2026-03-08 12:20:21 +07:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── NeoPixel Matrix (WS2812B matrix grid) ────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
PartSimulationRegistry.register('neopixel-matrix', {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
attachEvents: (element, simulator, getArduinoPinHelper) => {
|
|
|
|
|
|
const pinDIN = getArduinoPinHelper('DIN');
|
|
|
|
|
|
if (pinDIN === null) return () => {};
|
|
|
|
|
|
|
|
|
|
|
|
const el = element as any;
|
|
|
|
|
|
|
|
|
|
|
|
const unsub = createNeopixelDecoder(simulator as any, pinDIN, (index, r, g, b) => {
|
|
|
|
|
|
const cols: number = el.cols ?? 8;
|
|
|
|
|
|
const row = Math.floor(index / cols);
|
|
|
|
|
|
const col = index % cols;
|
|
|
|
|
|
try {
|
|
|
|
|
|
el.setPixel(row, col, { r, g, b });
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-03-08 12:20:21 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
return unsub;
|
|
|
|
|
|
},
|
2026-03-08 12:20:21 +07:00
|
|
|
|
});
|
feat: add new components and simulations for logic gates, protocols, and sensors
- Added LogicGateParts.ts for simulating various logic gates (AND, NAND, OR, NOR, XOR, NOT).
- Introduced ProtocolParts.ts for simulating I2C and SPI components including SSD1306 OLED, DS1307 RTC, MPU6050 IMU, DHT22 sensor, HX711 load cell, IR receiver, IR remote, and MicroSD card.
- Implemented BasicParts.ts with a membrane keypad and rotary dialer simulations.
- Enhanced SensorParts.ts with a single NeoPixel and PIR motion sensor.
- Updated index.ts to include new parts for logic gates and protocols.
- Modified vite-env.d.ts to declare new custom elements for the added components.
2026-03-09 02:14:03 +07:00
|
|
|
|
|
|
|
|
|
|
// ─── Single NeoPixel (WS2812B) ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-19 11:52:46 +07:00
|
|
|
|
* Single addressable RGB LED — decodes the WS2812B data stream on DIN.
|
feat: add new components and simulations for logic gates, protocols, and sensors
- Added LogicGateParts.ts for simulating various logic gates (AND, NAND, OR, NOR, XOR, NOT).
- Introduced ProtocolParts.ts for simulating I2C and SPI components including SSD1306 OLED, DS1307 RTC, MPU6050 IMU, DHT22 sensor, HX711 load cell, IR receiver, IR remote, and MicroSD card.
- Implemented BasicParts.ts with a membrane keypad and rotary dialer simulations.
- Enhanced SensorParts.ts with a single NeoPixel and PIR motion sensor.
- Updated index.ts to include new parts for logic gates and protocols.
- Modified vite-env.d.ts to declare new custom elements for the added components.
2026-03-09 02:14:03 +07:00
|
|
|
|
*/
|
|
|
|
|
|
PartSimulationRegistry.register('neopixel', {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
attachEvents: (element, simulator, getArduinoPinHelper) => {
|
|
|
|
|
|
const pinDIN = getArduinoPinHelper('DIN');
|
|
|
|
|
|
if (pinDIN === null) return () => {};
|
|
|
|
|
|
|
|
|
|
|
|
const el = element as any;
|
feat: add new components and simulations for logic gates, protocols, and sensors
- Added LogicGateParts.ts for simulating various logic gates (AND, NAND, OR, NOR, XOR, NOT).
- Introduced ProtocolParts.ts for simulating I2C and SPI components including SSD1306 OLED, DS1307 RTC, MPU6050 IMU, DHT22 sensor, HX711 load cell, IR receiver, IR remote, and MicroSD card.
- Implemented BasicParts.ts with a membrane keypad and rotary dialer simulations.
- Enhanced SensorParts.ts with a single NeoPixel and PIR motion sensor.
- Updated index.ts to include new parts for logic gates and protocols.
- Modified vite-env.d.ts to declare new custom elements for the added components.
2026-03-09 02:14:03 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
const unsub = createNeopixelDecoder(simulator as any, pinDIN, (_index, r, g, b) => {
|
|
|
|
|
|
el.r = r / 255;
|
|
|
|
|
|
el.g = g / 255;
|
|
|
|
|
|
el.b = b / 255;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return unsub;
|
|
|
|
|
|
},
|
feat: add new components and simulations for logic gates, protocols, and sensors
- Added LogicGateParts.ts for simulating various logic gates (AND, NAND, OR, NOR, XOR, NOT).
- Introduced ProtocolParts.ts for simulating I2C and SPI components including SSD1306 OLED, DS1307 RTC, MPU6050 IMU, DHT22 sensor, HX711 load cell, IR receiver, IR remote, and MicroSD card.
- Implemented BasicParts.ts with a membrane keypad and rotary dialer simulations.
- Enhanced SensorParts.ts with a single NeoPixel and PIR motion sensor.
- Updated index.ts to include new parts for logic gates and protocols.
- Modified vite-env.d.ts to declare new custom elements for the added components.
2026-03-09 02:14:03 +07:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── PIR Motion Sensor ───────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-19 11:52:46 +07:00
|
|
|
|
* PIR motion sensor — click the element OR press "Simulate motion" in the
|
|
|
|
|
|
* SensorControlPanel to trigger a 3-second HIGH pulse on OUT.
|
feat: add new components and simulations for logic gates, protocols, and sensors
- Added LogicGateParts.ts for simulating various logic gates (AND, NAND, OR, NOR, XOR, NOT).
- Introduced ProtocolParts.ts for simulating I2C and SPI components including SSD1306 OLED, DS1307 RTC, MPU6050 IMU, DHT22 sensor, HX711 load cell, IR receiver, IR remote, and MicroSD card.
- Implemented BasicParts.ts with a membrane keypad and rotary dialer simulations.
- Enhanced SensorParts.ts with a single NeoPixel and PIR motion sensor.
- Updated index.ts to include new parts for logic gates and protocols.
- Modified vite-env.d.ts to declare new custom elements for the added components.
2026-03-09 02:14:03 +07:00
|
|
|
|
*/
|
|
|
|
|
|
PartSimulationRegistry.register('pir-motion-sensor', {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
attachEvents: (element, simulator, getArduinoPinHelper, componentId) => {
|
|
|
|
|
|
const pin = getArduinoPinHelper('OUT');
|
|
|
|
|
|
if (pin === null) return () => {};
|
feat: add new components and simulations for logic gates, protocols, and sensors
- Added LogicGateParts.ts for simulating various logic gates (AND, NAND, OR, NOR, XOR, NOT).
- Introduced ProtocolParts.ts for simulating I2C and SPI components including SSD1306 OLED, DS1307 RTC, MPU6050 IMU, DHT22 sensor, HX711 load cell, IR receiver, IR remote, and MicroSD card.
- Implemented BasicParts.ts with a membrane keypad and rotary dialer simulations.
- Enhanced SensorParts.ts with a single NeoPixel and PIR motion sensor.
- Updated index.ts to include new parts for logic gates and protocols.
- Modified vite-env.d.ts to declare new custom elements for the added components.
2026-03-09 02:14:03 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
simulator.setPinState(pin, false); // idle LOW
|
feat: add new components and simulations for logic gates, protocols, and sensors
- Added LogicGateParts.ts for simulating various logic gates (AND, NAND, OR, NOR, XOR, NOT).
- Introduced ProtocolParts.ts for simulating I2C and SPI components including SSD1306 OLED, DS1307 RTC, MPU6050 IMU, DHT22 sensor, HX711 load cell, IR receiver, IR remote, and MicroSD card.
- Implemented BasicParts.ts with a membrane keypad and rotary dialer simulations.
- Enhanced SensorParts.ts with a single NeoPixel and PIR motion sensor.
- Updated index.ts to include new parts for logic gates and protocols.
- Modified vite-env.d.ts to declare new custom elements for the added components.
2026-03-09 02:14:03 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
feat: add new components and simulations for logic gates, protocols, and sensors
- Added LogicGateParts.ts for simulating various logic gates (AND, NAND, OR, NOR, XOR, NOT).
- Introduced ProtocolParts.ts for simulating I2C and SPI components including SSD1306 OLED, DS1307 RTC, MPU6050 IMU, DHT22 sensor, HX711 load cell, IR receiver, IR remote, and MicroSD card.
- Implemented BasicParts.ts with a membrane keypad and rotary dialer simulations.
- Enhanced SensorParts.ts with a single NeoPixel and PIR motion sensor.
- Updated index.ts to include new parts for logic gates and protocols.
- Modified vite-env.d.ts to declare new custom elements for the added components.
2026-03-09 02:14:03 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
const triggerMotion = () => {
|
|
|
|
|
|
if (timer !== null) clearTimeout(timer);
|
|
|
|
|
|
simulator.setPinState(pin, true);
|
|
|
|
|
|
console.log('[PIR] Motion detected → OUT HIGH');
|
|
|
|
|
|
timer = setTimeout(() => {
|
|
|
|
|
|
simulator.setPinState(pin, false);
|
|
|
|
|
|
timer = null;
|
|
|
|
|
|
console.log('[PIR] Motion ended → OUT LOW');
|
|
|
|
|
|
}, 3000);
|
|
|
|
|
|
};
|
feat: add new components and simulations for logic gates, protocols, and sensors
- Added LogicGateParts.ts for simulating various logic gates (AND, NAND, OR, NOR, XOR, NOT).
- Introduced ProtocolParts.ts for simulating I2C and SPI components including SSD1306 OLED, DS1307 RTC, MPU6050 IMU, DHT22 sensor, HX711 load cell, IR receiver, IR remote, and MicroSD card.
- Implemented BasicParts.ts with a membrane keypad and rotary dialer simulations.
- Enhanced SensorParts.ts with a single NeoPixel and PIR motion sensor.
- Updated index.ts to include new parts for logic gates and protocols.
- Modified vite-env.d.ts to declare new custom elements for the added components.
2026-03-09 02:14:03 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
element.addEventListener('click', triggerMotion);
|
2026-03-19 11:52:46 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
registerSensorUpdate(componentId, (values) => {
|
|
|
|
|
|
if (values.trigger === true) triggerMotion();
|
|
|
|
|
|
});
|
2026-03-19 11:52:46 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
return () => {
|
|
|
|
|
|
element.removeEventListener('click', triggerMotion);
|
|
|
|
|
|
if (timer !== null) clearTimeout(timer);
|
|
|
|
|
|
unregisterSensorUpdate(componentId);
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
feat: add new components and simulations for logic gates, protocols, and sensors
- Added LogicGateParts.ts for simulating various logic gates (AND, NAND, OR, NOR, XOR, NOT).
- Introduced ProtocolParts.ts for simulating I2C and SPI components including SSD1306 OLED, DS1307 RTC, MPU6050 IMU, DHT22 sensor, HX711 load cell, IR receiver, IR remote, and MicroSD card.
- Implemented BasicParts.ts with a membrane keypad and rotary dialer simulations.
- Enhanced SensorParts.ts with a single NeoPixel and PIR motion sensor.
- Updated index.ts to include new parts for logic gates and protocols.
- Modified vite-env.d.ts to declare new custom elements for the added components.
2026-03-09 02:14:03 +07:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── KS2E-M-DC5 Relay ────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Dual-coil relay — listens for COIL1/COIL2 pin state changes.
|
|
|
|
|
|
*/
|
|
|
|
|
|
PartSimulationRegistry.register('ks2e-m-dc5', {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
onPinStateChange: (pinName, state, _element) => {
|
|
|
|
|
|
if (pinName === 'COIL1' || pinName === 'COIL2') {
|
|
|
|
|
|
console.log(`[Relay KS2E] ${pinName} → ${state ? 'ACTIVATED' : 'RELEASED'}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
feat: add new components and simulations for logic gates, protocols, and sensors
- Added LogicGateParts.ts for simulating various logic gates (AND, NAND, OR, NOR, XOR, NOT).
- Introduced ProtocolParts.ts for simulating I2C and SPI components including SSD1306 OLED, DS1307 RTC, MPU6050 IMU, DHT22 sensor, HX711 load cell, IR receiver, IR remote, and MicroSD card.
- Implemented BasicParts.ts with a membrane keypad and rotary dialer simulations.
- Enhanced SensorParts.ts with a single NeoPixel and PIR motion sensor.
- Updated index.ts to include new parts for logic gates and protocols.
- Modified vite-env.d.ts to declare new custom elements for the added components.
2026-03-09 02:14:03 +07:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── HC-SR04 Ultrasonic Distance Sensor ──────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Ultrasonic sensor — monitors the TRIG pin.
|
2026-03-19 11:52:46 +07:00
|
|
|
|
* When TRIG goes HIGH, responds with an ECHO HIGH pulse whose duration
|
|
|
|
|
|
* encodes the configured distance (default 10 cm).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Echo timing: echoMs = distanceCm / 17.15
|
|
|
|
|
|
* (speed of sound ~343 m/s; round-trip halves: 17150 cm/s)
|
feat: add new components and simulations for logic gates, protocols, and sensors
- Added LogicGateParts.ts for simulating various logic gates (AND, NAND, OR, NOR, XOR, NOT).
- Introduced ProtocolParts.ts for simulating I2C and SPI components including SSD1306 OLED, DS1307 RTC, MPU6050 IMU, DHT22 sensor, HX711 load cell, IR receiver, IR remote, and MicroSD card.
- Implemented BasicParts.ts with a membrane keypad and rotary dialer simulations.
- Enhanced SensorParts.ts with a single NeoPixel and PIR motion sensor.
- Updated index.ts to include new parts for logic gates and protocols.
- Modified vite-env.d.ts to declare new custom elements for the added components.
2026-03-09 02:14:03 +07:00
|
|
|
|
*/
|
|
|
|
|
|
PartSimulationRegistry.register('hc-sr04', {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
attachEvents: (element, simulator, getArduinoPinHelper, componentId) => {
|
|
|
|
|
|
const trigPin = getArduinoPinHelper('TRIG');
|
|
|
|
|
|
const echoPin = getArduinoPinHelper('ECHO');
|
|
|
|
|
|
if (trigPin === null || echoPin === null) return () => {};
|
|
|
|
|
|
|
|
|
|
|
|
const el = element as any;
|
|
|
|
|
|
let distanceCm = parseFloat(el.distance) || 10; // default distance in cm
|
|
|
|
|
|
|
|
|
|
|
|
// ── ESP32 path: delegate protocol to backend QEMU worker ──
|
2026-04-22 03:14:27 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
const handledNatively =
|
|
|
|
|
|
typeof (simulator as any).registerSensor === 'function' &&
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
(simulator as any).registerSensor('hc-sr04', trigPin, {
|
|
|
|
|
|
distance: distanceCm,
|
|
|
|
|
|
echo_pin: echoPin,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (handledNatively) {
|
|
|
|
|
|
registerSensorUpdate(componentId, (values) => {
|
|
|
|
|
|
if ('distance' in values) {
|
|
|
|
|
|
distanceCm = Math.max(2, Math.min(400, values.distance as number));
|
2026-03-23 04:39:09 +07:00
|
|
|
|
}
|
2026-04-22 02:45:45 +07:00
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
(simulator as any).updateSensor(trigPin, {
|
|
|
|
|
|
distance: distanceCm,
|
|
|
|
|
|
echo_pin: echoPin,
|
feat: add new components and simulations for logic gates, protocols, and sensors
- Added LogicGateParts.ts for simulating various logic gates (AND, NAND, OR, NOR, XOR, NOT).
- Introduced ProtocolParts.ts for simulating I2C and SPI components including SSD1306 OLED, DS1307 RTC, MPU6050 IMU, DHT22 sensor, HX711 load cell, IR receiver, IR remote, and MicroSD card.
- Implemented BasicParts.ts with a membrane keypad and rotary dialer simulations.
- Enhanced SensorParts.ts with a single NeoPixel and PIR motion sensor.
- Updated index.ts to include new parts for logic gates and protocols.
- Modified vite-env.d.ts to declare new custom elements for the added components.
2026-03-09 02:14:03 +07:00
|
|
|
|
});
|
2026-04-22 02:45:45 +07:00
|
|
|
|
});
|
feat: add new components and simulations for logic gates, protocols, and sensors
- Added LogicGateParts.ts for simulating various logic gates (AND, NAND, OR, NOR, XOR, NOT).
- Introduced ProtocolParts.ts for simulating I2C and SPI components including SSD1306 OLED, DS1307 RTC, MPU6050 IMU, DHT22 sensor, HX711 load cell, IR receiver, IR remote, and MicroSD card.
- Implemented BasicParts.ts with a membrane keypad and rotary dialer simulations.
- Enhanced SensorParts.ts with a single NeoPixel and PIR motion sensor.
- Updated index.ts to include new parts for logic gates and protocols.
- Modified vite-env.d.ts to declare new custom elements for the added components.
2026-03-09 02:14:03 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
return () => {
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
(simulator as any).unregisterSensor(trigPin);
|
|
|
|
|
|
unregisterSensorUpdate(componentId);
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── AVR / RP2040 path: local pin scheduling ──
|
|
|
|
|
|
simulator.setPinState(echoPin, false); // ECHO LOW initially
|
|
|
|
|
|
|
|
|
|
|
|
const cleanup = simulator.pinManager.onPinChange(trigPin, (_: number, state: boolean) => {
|
|
|
|
|
|
if (!state) return; // only react on TRIG HIGH
|
|
|
|
|
|
if (typeof simulator.schedulePinChange === 'function') {
|
|
|
|
|
|
const clockHz: number =
|
|
|
|
|
|
typeof (simulator as any).getClockHz === 'function'
|
|
|
|
|
|
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
(simulator as any).getClockHz()
|
|
|
|
|
|
: 16_000_000;
|
|
|
|
|
|
const now = simulator.getCurrentCycles() as number;
|
|
|
|
|
|
const processingCycles = Math.round(600e-6 * clockHz); // 600 µs sensor overhead
|
|
|
|
|
|
const echoCycles = Math.round((distanceCm / 17150) * clockHz);
|
|
|
|
|
|
simulator.schedulePinChange(echoPin, true, now + processingCycles);
|
|
|
|
|
|
simulator.schedulePinChange(echoPin, false, now + processingCycles + echoCycles);
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
`[HC-SR04] Scheduled ECHO (${distanceCm} cm, echo=${(echoCycles / (clockHz / 1e6)).toFixed(1)} µs)`,
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Fallback: best-effort async (works with delay()-based sketches, not pulseIn)
|
|
|
|
|
|
const echoMs = Math.max(1, distanceCm / 17.15);
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
simulator.setPinState(echoPin, true);
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
simulator.setPinState(echoPin, false);
|
|
|
|
|
|
}, echoMs);
|
|
|
|
|
|
}, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
registerSensorUpdate(componentId, (values) => {
|
|
|
|
|
|
if ('distance' in values) {
|
|
|
|
|
|
distanceCm = Math.max(2, Math.min(400, values.distance as number));
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-03-19 11:52:46 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
return () => {
|
|
|
|
|
|
cleanup();
|
|
|
|
|
|
unregisterSensorUpdate(componentId);
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
feat: add new components and simulations for logic gates, protocols, and sensors
- Added LogicGateParts.ts for simulating various logic gates (AND, NAND, OR, NOR, XOR, NOT).
- Introduced ProtocolParts.ts for simulating I2C and SPI components including SSD1306 OLED, DS1307 RTC, MPU6050 IMU, DHT22 sensor, HX711 load cell, IR receiver, IR remote, and MicroSD card.
- Implemented BasicParts.ts with a membrane keypad and rotary dialer simulations.
- Enhanced SensorParts.ts with a single NeoPixel and PIR motion sensor.
- Updated index.ts to include new parts for logic gates and protocols.
- Modified vite-env.d.ts to declare new custom elements for the added components.
2026-03-09 02:14:03 +07:00
|
|
|
|
});
|