feat: Add HC-SR04 sensor support and enhance DHT22 response timing in simulation

pull/47/head
David Montero Crespo 2026-03-22 18:39:09 -03:00
parent 6a55f58e46
commit 083d8d69a8
6 changed files with 230 additions and 22 deletions

View File

@ -287,6 +287,29 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
if sensor:
sensor['responding'] = False
def _hcsr04_respond(trig_pin: int, echo_pin: int, distance_cm: float) -> None:
"""Thread function: inject the HC-SR04 echo pulse via qemu_picsimlab_set_pin."""
echo_slot = echo_pin + 1 # identity pinmap: slot = gpio + 1
# Echo pulse width = distance_cm * 58 µs (speed of sound round trip)
echo_us = max(100, int(distance_cm * 58))
try:
# Wait for TRIG pulse to finish + propagation delay (~600 µs)
_busy_wait_us(600)
# Drive ECHO HIGH
lib.qemu_picsimlab_set_pin(echo_slot, 1)
# Hold ECHO HIGH for distance-proportional duration
_busy_wait_us(echo_us)
# Drive ECHO LOW
lib.qemu_picsimlab_set_pin(echo_slot, 0)
except Exception as exc:
_log(f'HC-SR04 respond error on TRIG {trig_pin} ECHO {echo_pin}: {exc}')
finally:
with _sensors_lock:
sensor = _sensors.get(trig_pin)
if sensor:
sensor['responding'] = False
# ── 5. ctypes callbacks (called from QEMU thread) ─────────────────────────
def _on_pin_change(slot: int, value: int) -> None:
@ -298,7 +321,12 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
# Sensor protocol dispatch by type
with _sensors_lock:
sensor = _sensors.get(gpio)
if sensor is not None and sensor.get('type') == 'dht22':
if sensor is None:
return
stype = sensor.get('type', '')
if stype == 'dht22':
if value == 0 and not sensor.get('responding', False):
sensor['saw_low'] = True
elif value == 1 and sensor.get('saw_low', False):
@ -312,6 +340,19 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
name=f'dht22-gpio{gpio}',
).start()
elif stype == 'hc-sr04':
# HC-SR04: detect TRIG going HIGH (firmware sends 10µs pulse)
if value == 1 and not sensor.get('responding', False):
sensor['responding'] = True
echo_pin = int(sensor.get('echo_pin', gpio + 1))
distance = float(sensor.get('distance', 40.0))
threading.Thread(
target=_hcsr04_respond,
args=(gpio, echo_pin, distance),
daemon=True,
name=f'hcsr04-gpio{gpio}',
).start()
def _on_dir_change(slot: int, direction: int) -> None:
if _stopped.is_set():
return

View File

@ -71,7 +71,10 @@ function makeSimulator(adc?: ReturnType<typeof makeADC> | null) {
pinManager,
getADC: vi.fn().mockReturnValue(adc ?? null),
setPinState: vi.fn(),
cpu: { data: new Uint8Array(512).fill(0) },
isRunning: vi.fn().mockReturnValue(true),
getCurrentCycles: vi.fn().mockReturnValue(1000),
getClockHz: vi.fn().mockReturnValue(16_000_000),
cpu: { data: new Uint8Array(512).fill(0), cycles: 1000 },
};
}

View File

@ -2034,10 +2034,25 @@ void loop() {
// Blinks the built-in LED (GPIO2) and an external LED (GPIO4)
// Requires arduino-esp32 2.0.17 (IDF 4.4.x) — see docs/ESP32_EMULATION.md
#include <esp_task_wdt.h>
#include <soc/timer_group_struct.h>
#include <soc/timer_group_reg.h>
#define LED_BUILTIN_PIN 2 // Built-in blue LED on ESP32 DevKit
#define LED_EXT_PIN 4 // External red LED
void disableAllWDT() {
esp_task_wdt_deinit();
TIMERG0.wdt_wprotect = TIMG_WDT_WKEY_VALUE;
TIMERG0.wdt_config0.en = 0;
TIMERG0.wdt_wprotect = 0;
TIMERG1.wdt_wprotect = TIMG_WDT_WKEY_VALUE;
TIMERG1.wdt_config0.en = 0;
TIMERG1.wdt_wprotect = 0;
}
void setup() {
disableAllWDT();
Serial.begin(115200);
pinMode(LED_BUILTIN_PIN, OUTPUT);
pinMode(LED_EXT_PIN, OUTPUT);
@ -2045,6 +2060,7 @@ void setup() {
}
void loop() {
disableAllWDT();
digitalWrite(LED_BUILTIN_PIN, HIGH);
digitalWrite(LED_EXT_PIN, HIGH);
Serial.println("LED ON");
@ -2076,7 +2092,22 @@ void loop() {
// Echoes anything received on Serial (UART0) back to the sender.
// Open the Serial Monitor, type something, and see it echoed back.
#include <esp_task_wdt.h>
#include <soc/timer_group_struct.h>
#include <soc/timer_group_reg.h>
void disableAllWDT() {
esp_task_wdt_deinit();
TIMERG0.wdt_wprotect = TIMG_WDT_WKEY_VALUE;
TIMERG0.wdt_config0.en = 0;
TIMERG0.wdt_wprotect = 0;
TIMERG1.wdt_wprotect = TIMG_WDT_WKEY_VALUE;
TIMERG1.wdt_config0.en = 0;
TIMERG1.wdt_wprotect = 0;
}
void setup() {
disableAllWDT();
Serial.begin(115200);
delay(500);
Serial.println("ESP32 Serial Echo ready!");
@ -2084,6 +2115,7 @@ void setup() {
}
void loop() {
disableAllWDT();
if (Serial.available()) {
String input = Serial.readStringUntil('\\n');
input.trim();
@ -2905,6 +2937,10 @@ void loop() {
code: `// ESP32 — 7-Segment Display Counter 0-9
// Segments: a=12, b=13, c=14, d=25, e=26, f=27, g=32
#include <esp_task_wdt.h>
#include <soc/timer_group_struct.h>
#include <soc/timer_group_reg.h>
const int SEG[7] = {12, 13, 14, 25, 26, 27, 32};
const bool DIGITS[10][7] = {
@ -2920,18 +2956,30 @@ const bool DIGITS[10][7] = {
{1,1,1,1,0,1,1}, // 9
};
void disableAllWDT() {
esp_task_wdt_deinit();
TIMERG0.wdt_wprotect = TIMG_WDT_WKEY_VALUE;
TIMERG0.wdt_config0.en = 0;
TIMERG0.wdt_wprotect = 0;
TIMERG1.wdt_wprotect = TIMG_WDT_WKEY_VALUE;
TIMERG1.wdt_config0.en = 0;
TIMERG1.wdt_wprotect = 0;
}
void showDigit(int d) {
for (int i = 0; i < 7; i++)
digitalWrite(SEG[i], DIGITS[d][i] ? HIGH : LOW);
}
void setup() {
disableAllWDT();
for (int i = 0; i < 7; i++) pinMode(SEG[i], OUTPUT);
Serial.begin(115200);
Serial.println("ESP32 7-Segment Counter");
}
void loop() {
disableAllWDT();
for (int d = 0; d <= 9; d++) {
showDigit(d);
Serial.print("Digit: "); Serial.println(d);
@ -3770,10 +3818,25 @@ void loop() {
code: `// ESP32 — HC-SR04 Ultrasonic Distance Sensor
// Wiring: TRIG → D18 | ECHO → D19 | VCC → 3V3 | GND → GND
#include <esp_task_wdt.h>
#include <soc/timer_group_struct.h>
#include <soc/timer_group_reg.h>
#define TRIG_PIN 18
#define ECHO_PIN 19
void disableAllWDT() {
esp_task_wdt_deinit();
TIMERG0.wdt_wprotect = TIMG_WDT_WKEY_VALUE;
TIMERG0.wdt_config0.en = 0;
TIMERG0.wdt_wprotect = 0;
TIMERG1.wdt_wprotect = TIMG_WDT_WKEY_VALUE;
TIMERG1.wdt_config0.en = 0;
TIMERG1.wdt_wprotect = 0;
}
void setup() {
disableAllWDT();
Serial.begin(115200);
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
@ -3791,6 +3854,7 @@ long measureCm() {
}
void loop() {
disableAllWDT();
long cm = measureCm();
if (cm < 0) Serial.println("Out of range");
else Serial.printf("Distance: %ld cm\\n", cm);
@ -3818,13 +3882,27 @@ void loop() {
// Requires: Adafruit MPU6050, Adafruit Unified Sensor libraries
// Wiring: SDA → D21 | SCL → D22 | VCC → 3V3 | GND → GND
#include <esp_task_wdt.h>
#include <soc/timer_group_struct.h>
#include <soc/timer_group_reg.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Wire.h>
Adafruit_MPU6050 mpu;
void disableAllWDT() {
esp_task_wdt_deinit();
TIMERG0.wdt_wprotect = TIMG_WDT_WKEY_VALUE;
TIMERG0.wdt_config0.en = 0;
TIMERG0.wdt_wprotect = 0;
TIMERG1.wdt_wprotect = TIMG_WDT_WKEY_VALUE;
TIMERG1.wdt_config0.en = 0;
TIMERG1.wdt_wprotect = 0;
}
void setup() {
disableAllWDT();
Serial.begin(115200);
Wire.begin(21, 22); // SDA=21, SCL=22
if (!mpu.begin()) {
@ -3838,6 +3916,7 @@ void setup() {
}
void loop() {
disableAllWDT();
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
@ -3869,13 +3948,28 @@ void loop() {
code: `// ESP32 — PIR Motion Sensor
// Wiring: OUT → D5 | VCC → 3V3 | GND → GND
#include <esp_task_wdt.h>
#include <soc/timer_group_struct.h>
#include <soc/timer_group_reg.h>
#define PIR_PIN 5
#define LED_PIN 2 // built-in blue LED on ESP32 DevKit
bool prevMotion = false;
unsigned long detections = 0;
void disableAllWDT() {
esp_task_wdt_deinit();
TIMERG0.wdt_wprotect = TIMG_WDT_WKEY_VALUE;
TIMERG0.wdt_config0.en = 0;
TIMERG0.wdt_wprotect = 0;
TIMERG1.wdt_wprotect = TIMG_WDT_WKEY_VALUE;
TIMERG1.wdt_config0.en = 0;
TIMERG1.wdt_wprotect = 0;
}
void setup() {
disableAllWDT();
Serial.begin(115200);
pinMode(PIR_PIN, INPUT);
pinMode(LED_PIN, OUTPUT);
@ -3885,6 +3979,7 @@ void setup() {
}
void loop() {
disableAllWDT();
bool motion = (digitalRead(PIR_PIN) == HIGH);
if (motion && !prevMotion) {
detections++;
@ -3980,17 +4075,33 @@ void loop() {
// Wiring: HORZ → D35 | VERT → D34 | SEL → D15
// VCC → 3V3 | GND → GND
#include <esp_task_wdt.h>
#include <soc/timer_group_struct.h>
#include <soc/timer_group_reg.h>
#define JOY_HORZ 35 // input-only ADC pin
#define JOY_VERT 34 // input-only ADC pin
#define JOY_BTN 15 // GPIO with pull-up
void disableAllWDT() {
esp_task_wdt_deinit();
TIMERG0.wdt_wprotect = TIMG_WDT_WKEY_VALUE;
TIMERG0.wdt_config0.en = 0;
TIMERG0.wdt_wprotect = 0;
TIMERG1.wdt_wprotect = TIMG_WDT_WKEY_VALUE;
TIMERG1.wdt_config0.en = 0;
TIMERG1.wdt_wprotect = 0;
}
void setup() {
disableAllWDT();
Serial.begin(115200);
pinMode(JOY_BTN, INPUT_PULLUP);
Serial.println("ESP32 Joystick ready");
}
void loop() {
disableAllWDT();
int x = analogRead(JOY_HORZ); // 04095
int y = analogRead(JOY_VERT); // 04095
bool btn = (digitalRead(JOY_BTN) == LOW);

View File

@ -414,17 +414,18 @@ function scheduleDHT22Response(simulator: any, pin: number, element: HTMLElement
const payload = buildDHT22Payload(element);
const now = simulator.getCurrentCycles() as number;
// Timing constants at 16 MHz (cycles per µs = 16)
// DHT22 starts pulling the line LOW ~20 µs after MCU releases it HIGH.
// The Adafruit DHT library v1.4.7 calls expectPulse(LOW) at ~55 µs (pullTime default),
// so the preamble LOW must already be active by then. Starting at 20 µs (320 cycles)
// guarantees the pin IS LOW when the library checks.
const RESPONSE_START = 320; // 20 µs — DHT22 response start
const LOW80 = 1280; // 80 µs LOW preamble
const HIGH80 = 1280; // 80 µs HIGH preamble
const LOW50 = 800; // 50 µs LOW marker before each bit
const HIGH0 = 416; // 26 µs HIGH → bit '0'
const HIGH1 = 1120; // 70 µs HIGH → bit '1'
// Scale timing by CPU clock — AVR runs at 16 MHz, RP2040 at 125 MHz.
const clockHz: number = typeof simulator.getClockHz === 'function'
? simulator.getClockHz()
: 16_000_000;
const us = (microseconds: number) => Math.round(microseconds * clockHz / 1_000_000);
const RESPONSE_START = us(20); // DHT22 response start (~20 µs after MCU releases)
const LOW80 = us(80); // 80 µs LOW preamble
const HIGH80 = us(80); // 80 µs HIGH preamble
const LOW50 = us(50); // 50 µs LOW marker before each bit
const HIGH0 = us(26); // 26 µs HIGH → bit '0'
const HIGH1 = us(70); // 70 µs HIGH → bit '1'
let t = now + RESPONSE_START;
@ -487,9 +488,13 @@ PartSimulationRegistry.register('dht22', {
// Prevent DHT22's own scheduled pin changes from re-triggering the response.
// After the MCU releases DATA HIGH and we begin responding, we ignore all
// pin-change callbacks until the full waveform has been emitted.
// DHT22 response is ~5 ms; at 16 MHz that is ~80 000 cycles. We gate for
// 200 000 cycles (~12.5 ms) to give plenty of headroom.
const RESPONSE_GATE_CYCLES = 200_000;
// DHT22 response is ~5 ms; gate for ~12.5 ms scaled to the CPU clock.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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 RESPONSE_GATE_CYCLES = Math.round(12_500 * clockHz / 1_000_000);
let responseEndCycle = 0;
let responseEndTimeMs = 0; // time-based fallback for ESP32 (no cycle counter)

View File

@ -631,18 +631,46 @@ PartSimulationRegistry.register('hc-sr04', {
const echoPin = getArduinoPinHelper('ECHO');
if (trigPin === null || echoPin === null) return () => {};
simulator.setPinState(echoPin, false); // ECHO LOW initially
const el = element as any;
let distanceCm = parseFloat(el.distance) || 10; // default distance in cm
let distanceCm = 10; // default distance in cm
// ── ESP32 path: delegate protocol to backend QEMU worker ──
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(simulator as any).updateSensor(trigPin, {
distance: distanceCm,
echo_pin: echoPin,
});
});
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
// HC-SR04 timing (at 16 MHz):
// - Sensor processing delay after TRIG: ~600 µs = 9600 cycles
// - Echo duration = distanceCm / 17150 s × 16 000 000 cycles/s
// (17150 cm/s = speed of sound, one-way = round-trip/2)
if (typeof simulator.schedulePinChange === 'function') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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;

View File

@ -24,8 +24,10 @@ const SENSOR_COMPONENT_MAP: Record<string, {
sensorType: string;
dataPinName: string;
propertyKeys: string[];
extraPins?: Record<string, string>; // extra pin mappings: prop name → component pin name
}> = {
'dht22': { sensorType: 'dht22', dataPinName: 'SDA', propertyKeys: ['temperature', 'humidity'] },
'hc-sr04': { sensorType: 'hc-sr04', dataPinName: 'TRIG', propertyKeys: ['distance'], extraPins: { echo_pin: 'ECHO' } },
};
// ── Legacy type aliases (keep external consumers working) ──────────────────
@ -590,6 +592,24 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
const val = comp.properties[key];
if (val !== undefined) props[key] = typeof val === 'string' ? parseFloat(val) : val;
}
// Resolve extra pins (e.g. echo_pin for HC-SR04) from wires
if (sensorDef.extraPins) {
for (const [propName, compPinName] of Object.entries(sensorDef.extraPins)) {
for (const ew of wires) {
const epComp = (ew.start.componentId === comp.id && ew.start.pinName === compPinName)
? ew.start : (ew.end.componentId === comp.id && ew.end.pinName === compPinName)
? ew.end : null;
if (!epComp) continue;
const epBoard = epComp === ew.start ? ew.end : ew.start;
if (!isBoardComponent(epBoard.componentId)) continue;
const extraGpio = boardPinToNumber(board.boardKind, epBoard.pinName);
if (extraGpio !== null && extraGpio >= 0) {
props[propName] = extraGpio;
}
break;
}
}
}
sensors.push(props);
break; // only one data pin per sensor
}