feat: Add I2C slave emulation for MPU-6050 and BMP280 sensors

- Implemented _MPU6050Slave and _BMP280Slave classes for I2C communication.
- Enhanced main function to register these sensors and handle I2C events.
- Updated sensor management to support MPU-6050, BMP280, DS1307, DS3231, SSD1306, and PCF8574.
- Added frontend examples for BMP280 weather station and SSD1306 OLED display.
- Modified Esp32Bridge to handle new I2C transaction events.
- Updated ProtocolParts to support ESP32 path for I2C devices.
- Enhanced useSimulatorStore to manage I2C transaction listeners.
This commit is contained in:
David Montero Crespo 2026-04-07 15:43:09 -03:00
parent 9761aad0be
commit 689f8e71db
5 changed files with 638 additions and 73 deletions

View File

@ -238,11 +238,285 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
argc = len(args_list)
argv = (ctypes.c_char_p * argc)(*args_list)
# ── I2C slave state machine (MPU-6050 and future register-map devices) ──────
class _MPU6050Slave:
"""Full MPU-6050 register-map I2C slave emulation.
I2C event constants (picsimlab convention):
0x0100 = START + address phase return 1 (ACK = device present)
0x0200 = WRITE byte (byte in bits 7:0) return 1 (ACK)
0x0300 = READ request return register byte at current pointer
0x0000 = STOP
"""
I2C_START = 0x0100
I2C_WRITE = 0x0200
I2C_READ = 0x0300
def __init__(self, addr: int = 0x68):
self.addr = addr
self.regs = bytearray(256)
self.reg_ptr = 0
self.first_byte = True
# WHO_AM_I
self.regs[0x75] = 0x68
# PWR_MGMT_1 — awake (0 = no sleep)
self.regs[0x6B] = 0x00
# ACCEL_CONFIG / GYRO_CONFIG (default ±2g / ±250°/s)
self.regs[0x1C] = 0x00
self.regs[0x1B] = 0x00
# ACCEL_Z = +1g = 16384 (0x4000) at ±2g full-scale
self.regs[0x3B] = 0x00; self.regs[0x3C] = 0x00 # X
self.regs[0x3D] = 0x00; self.regs[0x3E] = 0x00 # Y
self.regs[0x3F] = 0x40; self.regs[0x40] = 0x00 # Z = +1g
# TEMP: T(°C) = raw/340 + 36.53 → raw = (25 - 36.53) × 340 ≈ -3920 = 0xF190
temp_raw = round((25.0 - 36.53) * 340) & 0xFFFF
self.regs[0x41] = (temp_raw >> 8) & 0xFF
self.regs[0x42] = temp_raw & 0xFF
# GYRO all zero (stationary)
def handle_event(self, event: int) -> int:
phase = event & 0xFF00
if phase == self.I2C_START:
self.first_byte = True
return 1 # ACK — device present
elif phase == self.I2C_WRITE:
data = event & 0xFF
if self.first_byte:
self.reg_ptr = data
self.first_byte = False
else:
self.regs[self.reg_ptr] = data
self.reg_ptr = (self.reg_ptr + 1) & 0xFF
return 1 # ACK
elif phase == self.I2C_READ:
val = self.regs[self.reg_ptr]
self.reg_ptr = (self.reg_ptr + 1) & 0xFF
return val
else: # STOP / unknown
self.first_byte = True
return 0
def _write_i16(self, reg_h: int, raw_float: float) -> None:
raw = max(-32768, min(32767, round(raw_float))) & 0xFFFF
self.regs[reg_h] = (raw >> 8) & 0xFF
self.regs[reg_h + 1] = raw & 0xFF
def update(self, accel_x: float = 0, accel_y: float = 0, accel_z: float = 1,
gyro_x: float = 0, gyro_y: float = 0, gyro_z: float = 0,
temp: float = 25.0) -> None:
self._write_i16(0x3B, accel_x * 16384)
self._write_i16(0x3D, accel_y * 16384)
self._write_i16(0x3F, accel_z * 16384)
self._write_i16(0x43, gyro_x * 131)
self._write_i16(0x45, gyro_y * 131)
self._write_i16(0x47, gyro_z * 131)
self._write_i16(0x41, (temp - 36.53) * 340)
# ── BMP280 Barometric Pressure + Temperature Sensor ──────────────────────────
class _BMP280Slave:
"""Full BMP280 register-map I2C slave (address 0x76 or 0x77).
Uses BMP280 datasheet Section 8.2 example calibration constants.
Implements Bosch compensation formulas with binary-search inversion
to find raw ADC values from the desired temperature / pressure.
"""
# Section 8.2 calibration constants
DIG_T1 = 27504; DIG_T2 = 26435; DIG_T3 = -1000
DIG_P1 = 36477; DIG_P2 = -10685; DIG_P3 = 3024
DIG_P4 = 2855; DIG_P5 = 140; DIG_P6 = -7
DIG_P7 = 15500; DIG_P8 = -14600; DIG_P9 = 6000
def __init__(self, addr: int = 0x76):
self.addr = addr
self.regs = bytearray(256)
self.reg_ptr = 0
self.first_byte = True
self._temp_c = 25.0
self._press_hpa = 1013.25
self._init_calibration()
self._update_measurements()
# ── calibration register layout ──────────────────────────────────────
def _wu16(self, a: int, v: int) -> None:
self.regs[a] = v & 0xFF; self.regs[a + 1] = (v >> 8) & 0xFF
def _ws16(self, a: int, v: int) -> None:
self._wu16(a, v & 0xFFFF)
def _init_calibration(self) -> None:
self.regs[0xD0] = 0x60 # chip_id BMP280
self.regs[0xF3] = 0x00 # status (done)
self._wu16(0x88, self.DIG_T1); self._ws16(0x8A, self.DIG_T2); self._ws16(0x8C, self.DIG_T3)
self._wu16(0x8E, self.DIG_P1); self._ws16(0x90, self.DIG_P2); self._ws16(0x92, self.DIG_P3)
self._ws16(0x94, self.DIG_P4); self._ws16(0x96, self.DIG_P5); self._ws16(0x98, self.DIG_P6)
self._ws16(0x9A, self.DIG_P7); self._ws16(0x9C, self.DIG_P8); self._ws16(0x9E, self.DIG_P9)
# ── Bosch compensation formulas ───────────────────────────────────────
def _t_fine(self, adc_t: int) -> int:
v1 = (((adc_t >> 3) - (self.DIG_T1 << 1)) * self.DIG_T2) >> 11
s = (adc_t >> 4) - self.DIG_T1
v2 = ((s * s >> 12) * self.DIG_T3) >> 14
return v1 + v2
def _compensate_t(self, adc_t: int) -> int:
return (self._t_fine(adc_t) * 5 + 128) >> 8
def _compensate_p(self, adc_p: int, adc_t: int) -> float:
tf = self._t_fine(adc_t)
v1 = tf / 2.0 - 64000.0
v2 = v1 * v1 * self.DIG_P6 / 32768.0
v2 = v2 + v1 * self.DIG_P5 * 2.0
v2 = v2 / 4.0 + self.DIG_P4 * 65536.0
v1 = (self.DIG_P3 * v1 * v1 / 524288.0 + self.DIG_P2 * v1) / 524288.0
v1 = (1.0 + v1 / 32768.0) * self.DIG_P1
if v1 == 0:
return 0.0
p = 1048576.0 - adc_p
p = (p - v2 / 4096.0) * 6250.0 / v1
p = p + (self.DIG_P9 * p * p / 2147483648.0 + p * self.DIG_P8 / 32768.0 + self.DIG_P7) / 16.0
return p
def _find_adc_t(self, target_centideg: int) -> int:
lo, hi = 0, (1 << 20) - 1
while lo < hi:
mid = (lo + hi) >> 1
if self._compensate_t(mid) < target_centideg:
lo = mid + 1
else:
hi = mid
return lo
def _find_adc_p(self, target_pa: float, adc_t: int) -> int:
lo, hi = 0, (1 << 20) - 1
while lo < hi:
mid = (lo + hi) >> 1
if self._compensate_p(mid, adc_t) > target_pa:
lo = mid + 1
else:
hi = mid
return lo
def _encode20(self, v: int) -> tuple:
return (v >> 12) & 0xFF, (v >> 4) & 0xFF, (v & 0xF) << 4
def _update_measurements(self) -> None:
adc_t = self._find_adc_t(round(self._temp_c * 100))
adc_p = self._find_adc_p(self._press_hpa * 100.0, adc_t)
pm, pl, px = self._encode20(adc_p)
tm, tl, tx = self._encode20(adc_t)
self.regs[0xF7] = pm; self.regs[0xF8] = pl; self.regs[0xF9] = px
self.regs[0xFA] = tm; self.regs[0xFB] = tl; self.regs[0xFC] = tx
def update(self, temperature_c: float, pressure_hpa: float) -> None:
self._temp_c = temperature_c
self._press_hpa = pressure_hpa
self._update_measurements()
def handle_event(self, event: int) -> int:
phase = event & 0xFF00
if phase == 0x0100:
self.first_byte = True; return 1
elif phase == 0x0200:
data = event & 0xFF
if self.first_byte:
self.reg_ptr = data; self.first_byte = False
else:
self.regs[self.reg_ptr] = data
self.reg_ptr = (self.reg_ptr + 1) & 0xFF
return 1
elif phase == 0x0300:
val = self.regs[self.reg_ptr]
self.reg_ptr = (self.reg_ptr + 1) & 0xFF
return val
else:
self.first_byte = True; return 0
# ── DS1307 / DS3231 Real-Time Clock ───────────────────────────────────────
import datetime as _datetime
class _DS1307Slave:
"""DS1307 I2C RTC — returns current system time in BCD (address 0x68)."""
def __init__(self) -> None:
self.reg_ptr = 0
self.first_byte = True
@staticmethod
def _bcd(n: int) -> int:
return ((n // 10) << 4) | (n % 10)
def _read_reg(self, reg: int) -> int:
now = _datetime.datetime.now()
if reg == 0x00: return self._bcd(now.second)
elif reg == 0x01: return self._bcd(now.minute)
elif reg == 0x02: return self._bcd(now.hour)
elif reg == 0x03: return self._bcd(now.weekday() + 1) # Mon=1..Sun=7
elif reg == 0x04: return self._bcd(now.day)
elif reg == 0x05: return self._bcd(now.month)
elif reg == 0x06: return self._bcd(now.year % 100)
return 0x00
def handle_event(self, event: int) -> int:
phase = event & 0xFF00
if phase == 0x0100:
self.first_byte = True; return 1
elif phase == 0x0200:
data = event & 0xFF
if self.first_byte:
self.reg_ptr = data; self.first_byte = False
return 1
elif phase == 0x0300:
val = self._read_reg(self.reg_ptr)
self.reg_ptr = (self.reg_ptr + 1) & 0x3F
return val
else:
self.first_byte = True; return 0
class _DS3231Slave(_DS1307Slave):
"""DS3231 I2C RTC with on-chip temperature (address 0x68)."""
def __init__(self) -> None:
super().__init__()
self.temperatureC = 25.0
def _read_reg(self, reg: int) -> int:
if reg == 0x0E: return 0x00 # Control
if reg == 0x0F: return 0x00 # Status (OSF cleared)
if reg == 0x11: # Temp MSB (signed integer °C)
return int(self.temperatureC) & 0xFF
if reg == 0x12: # Temp LSB (fractional bits 7:6)
frac = abs(self.temperatureC) - int(abs(self.temperatureC))
return (round(frac / 0.25) & 0x03) << 6
return super()._read_reg(reg)
# ── I2C Write Sink (relay for write-only devices: SSD1306, PCF8574) ──────
class _I2CWriteSink:
"""ACKs all I2C writes, emits complete transaction to frontend on STOP."""
def __init__(self, addr: int, emit_fn) -> None:
self.addr = addr
self._emit = emit_fn
self._buf: list[int] = []
def handle_event(self, event: int) -> int:
phase = event & 0xFF00
if phase == 0x0100: # START — reset buffer
self._buf = []; return 1
elif phase == 0x0200: # WRITE — accumulate byte
self._buf.append(event & 0xFF); return 1
elif phase == 0x0300: # READ — write-only device
return 0xFF
else: # STOP — emit transaction
if self._buf:
self._emit({'type': 'i2c_transaction',
'addr': self.addr, 'data': list(self._buf)})
self._buf = []
return 0
# ── 4. Shared mutable state ───────────────────────────────────────────────
_stopped = threading.Event() # set on "stop" command
_init_done = threading.Event() # set when qemu_init() returns
_sensors_ready = threading.Event() # set after pre-registering initial sensors
_i2c_responses: dict[int, int] = {} # 7-bit addr → response byte
_i2c_responses: dict[int, int] = {} # 7-bit addr → response byte (simple)
_i2c_slaves: dict = {} # 7-bit addr → I2C slave/sink instance
_spi_response = [0xFF] # MISO byte for SPI transfers
_rmt_decoders: dict[int, _RmtDecoder] = {}
_uart0_buf = bytearray() # accumulate UART0 for crash detection
@ -560,6 +834,10 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
def _on_i2c_event(bus_id: int, addr: int, event: int) -> int:
"""Synchronous — must return immediately; called from QEMU thread."""
# Register-map slaves (MPU-6050, etc.) take priority over static responses
slave = _i2c_slaves.get(addr)
if slave is not None:
return slave.handle_event(event)
resp = _i2c_responses.get(addr, 0)
if not _stopped.is_set():
_emit({'type': 'i2c_event', 'bus': bus_id, 'addr': addr,
@ -725,13 +1003,39 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
gpio = int(cmd['pin'])
sensor_type = cmd.get('sensor_type', '')
with _sensors_lock:
_sensors[gpio] = {
sensor_data: dict = {
'type': sensor_type,
**{k: v for k, v in cmd.items()
if k not in ('cmd', 'pin', 'sensor_type')},
'saw_low': False,
'responding': False,
}
if sensor_type == 'mpu6050':
i2c_addr = int(cmd.get('addr', 0x68))
slave = _MPU6050Slave(i2c_addr)
_i2c_slaves[i2c_addr] = slave
sensor_data['i2c_addr'] = i2c_addr
sensor_data['slave'] = slave
elif sensor_type == 'bmp280':
i2c_addr = int(cmd.get('addr', 0x76))
slave = _BMP280Slave(i2c_addr)
_i2c_slaves[i2c_addr] = slave
sensor_data['i2c_addr'] = i2c_addr
sensor_data['slave'] = slave
elif sensor_type in ('ds1307', 'ds3231'):
i2c_addr = 0x68
slave = _DS3231Slave() if sensor_type == 'ds3231' else _DS1307Slave()
_i2c_slaves[i2c_addr] = slave
sensor_data['i2c_addr'] = i2c_addr
sensor_data['slave'] = slave
elif sensor_type in ('ssd1306', 'pcf8574'):
default_addr = 0x3C if sensor_type == 'ssd1306' else 0x27
i2c_addr = int(cmd.get('addr', default_addr))
sink = _I2CWriteSink(i2c_addr, _emit)
_i2c_slaves[i2c_addr] = sink
sensor_data['i2c_addr'] = i2c_addr
sensor_data['slave'] = sink
_sensors[gpio] = sensor_data
_log(f'Sensor {sensor_type} attached on GPIO {gpio}')
elif c == 'sensor_update':
@ -742,11 +1046,32 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
for k, v in cmd.items():
if k not in ('cmd', 'pin'):
sensor[k] = v
stype = sensor.get('type')
slave = sensor.get('slave')
if stype == 'mpu6050' and slave is not None:
slave.update(
accel_x=float(sensor.get('accelX', 0)),
accel_y=float(sensor.get('accelY', 0)),
accel_z=float(sensor.get('accelZ', 1)),
gyro_x =float(sensor.get('gyroX', 0)),
gyro_y =float(sensor.get('gyroY', 0)),
gyro_z =float(sensor.get('gyroZ', 0)),
temp =float(sensor.get('temp', 25.0)),
)
elif stype == 'bmp280' and slave is not None:
slave.update(
temperature_c =float(sensor.get('temperature', 25.0)),
pressure_hpa =float(sensor.get('pressure', 1013.25)),
)
elif stype == 'ds3231' and slave is not None:
slave.temperatureC = float(sensor.get('temperature', 25.0))
elif c == 'sensor_detach':
gpio = int(cmd['pin'])
with _sensors_lock:
_sensors.pop(gpio, None)
sensor = _sensors.pop(gpio, None)
if sensor and 'i2c_addr' in sensor:
_i2c_slaves.pop(sensor['i2c_addr'], None)
_log(f'Sensor detached from GPIO {gpio}')
elif c == 'stop':

View File

@ -4675,6 +4675,128 @@ void loop() {
components: [],
wires: [],
},
// ── ESP32 BMP280 Weather Station ─────────────────────────────────────────────
{
id: 'esp32-bmp280',
title: 'ESP32: BMP280 Weather Station',
description: 'Read temperature and pressure from a BMP280 barometric sensor over I2C (SDA=D21, SCL=D22).',
libraries: ['Adafruit BMP280 Library', 'Adafruit Unified Sensor'],
category: 'sensors',
difficulty: 'intermediate',
boardType: 'esp32',
boardFilter: 'esp32',
code: `// ESP32 — BMP280 Barometric Pressure & Temperature (I2C)
// Requires: Adafruit BMP280 Library, Adafruit Unified Sensor
// Wiring: SDA → D21 | SCL → D22 | VCC → 3V3 | GND → GND
#include <Wire.h>
#include <Adafruit_BMP280.h>
Adafruit_BMP280 bmp;
void setup() {
Serial.begin(115200);
Wire.begin(21, 22);
if (!bmp.begin(0x76)) {
Serial.println("BMP280 not found! Check wiring.");
while (true) delay(10);
}
bmp.setSampling(Adafruit_BMP280::MODE_NORMAL,
Adafruit_BMP280::SAMPLING_X2,
Adafruit_BMP280::SAMPLING_X16,
Adafruit_BMP280::FILTER_X16,
Adafruit_BMP280::STANDBY_MS_500);
Serial.println("BMP280 ready!");
}
void loop() {
float tempC = bmp.readTemperature();
float pressure = bmp.readPressure() / 100.0F; // hPa
float altitude = bmp.readAltitude(1013.25); // m
Serial.printf("Temp: %.2f C Pressure: %.2f hPa Altitude: %.1f m\\n",
tempC, pressure, altitude);
delay(2000);
}`,
components: [
{ type: 'wokwi-bmp280', id: 'e32-bmp1', x: 420, y: 150, properties: { temperature: '25', pressure: '1013.25' } },
],
wires: [
{ id: 'e32b-vcc', start: { componentId: 'esp32', pinName: '3V3' }, end: { componentId: 'e32-bmp1', pinName: 'VCC' }, color: '#ff4444' },
{ id: 'e32b-gnd', start: { componentId: 'esp32', pinName: 'GND' }, end: { componentId: 'e32-bmp1', pinName: 'GND' }, color: '#000000' },
{ id: 'e32b-sda', start: { componentId: 'esp32', pinName: '21' }, end: { componentId: 'e32-bmp1', pinName: 'SDA' }, color: '#22aaff' },
{ id: 'e32b-scl', start: { componentId: 'esp32', pinName: '22' }, end: { componentId: 'e32-bmp1', pinName: 'SCL' }, color: '#ff8800' },
],
},
// ── ESP32 SSD1306 OLED Display ────────────────────────────────────────────────
{
id: 'esp32-oled',
title: 'ESP32: SSD1306 OLED Display',
description: 'Display text and graphics on a 128×64 SSD1306 OLED over I2C (SDA=D21, SCL=D22).',
libraries: ['Adafruit SSD1306', 'Adafruit GFX Library'],
category: 'displays',
difficulty: 'intermediate',
boardType: 'esp32',
boardFilter: 'esp32',
code: `// ESP32 — SSD1306 OLED Display (I2C 128×64)
// Requires: Adafruit SSD1306, Adafruit GFX Library
// Wiring: SDA → D21 | SCL → D22 | VCC → 3V3 | GND → GND
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
int counter = 0;
void setup() {
Serial.begin(115200);
Wire.begin(21, 22);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("SSD1306 not found!");
while (true) delay(10);
}
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("Hello");
display.println("Velxio!");
display.display();
Serial.println("OLED ready!");
}
void loop() {
counter++;
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("Hello");
display.println("Velxio!");
display.setTextSize(1);
display.setCursor(0, 48);
display.printf("Count: %d", counter);
display.display();
Serial.printf("Frame: %d\\n", counter);
delay(1000);
}`,
components: [
{ type: 'wokwi-ssd1306', id: 'e32-oled1', x: 420, y: 130, properties: {} },
],
wires: [
{ id: 'e32o-vcc', start: { componentId: 'esp32', pinName: '3V3' }, end: { componentId: 'e32-oled1', pinName: 'VCC' }, color: '#ff4444' },
{ id: 'e32o-gnd', start: { componentId: 'esp32', pinName: 'GND' }, end: { componentId: 'e32-oled1', pinName: 'GND' }, color: '#000000' },
{ id: 'e32o-sda', start: { componentId: 'esp32', pinName: '21' }, end: { componentId: 'e32-oled1', pinName: 'SDA' }, color: '#22aaff' },
{ id: 'e32o-scl', start: { componentId: 'esp32', pinName: '22' }, end: { componentId: 'e32-oled1', pinName: 'SCL' }, color: '#ff8800' },
],
},
];
// Get examples by category

View File

@ -24,8 +24,9 @@
* { type: 'gpio_dir', data: { pin: number, dir: 0 | 1 } }
* { type: 'ledc_update', data: { channel: number, duty: number, duty_pct: number } }
* { type: 'ws2812_update', data: { channel: number, pixels: [number, number, number][] } }
* { type: 'i2c_event', data: { addr: number, data: number } }
* { type: 'spi_event', data: { data: number } }
* { type: 'i2c_event', data: { addr: number, data: number } }
* { type: 'i2c_transaction', data: { addr: number, data: number[] } }
* { type: 'spi_event', data: { data: number } }
* { type: 'system', data: { event: string, ... } }
* { type: 'error', data: { message: string } }
*/
@ -76,8 +77,9 @@ export class Esp32Bridge {
onPinDir: ((gpioPin: number, dir: 0 | 1) => void) | null = null;
onLedcUpdate: ((update: LedcUpdate) => void) | null = null;
onWs2812Update: ((channel: number, pixels: Ws2812Pixel[]) => void) | null = null;
onI2cEvent: ((addr: number, data: number) => void) | null = null;
onSpiEvent: ((data: number) => void) | null = null;
onI2cEvent: ((addr: number, data: number) => void) | null = null;
onI2cTransaction: ((addr: number, data: number[]) => void) | null = null;
onSpiEvent: ((data: number) => void) | null = null;
onConnected: (() => void) | null = null;
onDisconnected: (() => void) | null = null;
onError: ((msg: string) => void) | null = null;
@ -179,6 +181,12 @@ export class Esp32Bridge {
this.onI2cEvent?.(addr, data);
break;
}
case 'i2c_transaction': {
const addr = msg.data.addr as number;
const data = msg.data.data as number[];
this.onI2cTransaction?.(addr, data);
break;
}
case 'spi_event': {
const data = msg.data.data as number;
this.onSpiEvent?.(data);

View File

@ -293,10 +293,30 @@ PartSimulationRegistry.register('ssd1306', {
// I2C mode (default)
const sim = simulator as any;
if (typeof sim.addI2CDevice !== 'function') return () => {};
const device = new VirtualSSD1306(0x3C, element);
sim.addI2CDevice(device);
return () => removeI2CDevice(sim, device.address);
const i2cAddr = 0x3C;
if (typeof sim.addI2CDevice === 'function') {
// ── AVR / RP2040 path ──────────────────────────────────────────────────
const device = new VirtualSSD1306(i2cAddr, element);
sim.addI2CDevice(device);
return () => removeI2CDevice(sim, device.address);
} else if (typeof sim.registerSensor === 'function') {
// ── ESP32 path: relay I2C writes via backend ───────────────────────────
const virtualPin = 200 + i2cAddr;
const device = new VirtualSSD1306(i2cAddr, element);
sim.registerSensor('ssd1306', virtualPin, { addr: i2cAddr });
sim.addI2CTransactionListener(i2cAddr, (data: number[]) => {
data.forEach((b: number) => device.writeByte(b));
device.stop();
});
return () => {
sim.unregisterSensor(virtualPin);
sim.removeI2CTransactionListener(i2cAddr);
};
}
return () => {};
},
});
@ -309,10 +329,21 @@ PartSimulationRegistry.register('ssd1306', {
PartSimulationRegistry.register('ds1307', {
attachEvents: (_element, simulator, _getPin) => {
const sim = simulator as any;
if (typeof sim.addI2CDevice !== 'function') return () => {};
const rtc = new VirtualDS1307();
sim.addI2CDevice(rtc);
return () => removeI2CDevice(sim, rtc.address);
if (typeof sim.addI2CDevice === 'function') {
// ── AVR / RP2040 path ──────────────────────────────────────────────────
const rtc = new VirtualDS1307();
sim.addI2CDevice(rtc);
return () => removeI2CDevice(sim, rtc.address);
} else if (typeof sim.registerSensor === 'function') {
// ── ESP32 path: delegate to backend QEMU RTC slave ────────────────────
const virtualPin = 200 + 0x68;
sim.registerSensor('ds1307', virtualPin, { addr: 0x68 });
return () => sim.unregisterSensor(virtualPin);
}
return () => {};
},
});
@ -386,35 +417,55 @@ class VirtualMPU6050 implements I2CDevice {
PartSimulationRegistry.register('mpu6050', {
attachEvents: (element, simulator, _getPin, componentId) => {
const sim = simulator as any;
if (typeof sim.addI2CDevice !== 'function') return () => {};
const el = element as any;
// Respect AD0 pin on element: `el.ad0 = true` → address 0x69
const sim = simulator as any;
const el = element as any;
// Respect AD0 pin: `el.ad0 = true` → address 0x69, else 0x68
const addr = (el.ad0 === true || el.ad0 === 'true') ? 0x69 : 0x68;
const device = new VirtualMPU6050(addr);
sim.addI2CDevice(device);
// Helper: write a signed 16-bit value to two consecutive registers (H, L)
const writeI16 = (regH: number, raw: number) => {
const v = Math.max(-32768, Math.min(32767, Math.round(raw))) & 0xFFFF;
device.registers[regH] = (v >> 8) & 0xFF;
device.registers[regH + 1] = v & 0xFF;
};
if (typeof sim.addI2CDevice === 'function') {
// ── AVR / RP2040 path: virtual I2C device in JavaScript ──────────────
const device = new VirtualMPU6050(addr);
sim.addI2CDevice(device);
registerSensorUpdate(componentId, (values) => {
if ('accelX' in values) writeI16(0x3B, (values.accelX as number) * 16384);
if ('accelY' in values) writeI16(0x3D, (values.accelY as number) * 16384);
if ('accelZ' in values) writeI16(0x3F, (values.accelZ as number) * 16384);
if ('gyroX' in values) writeI16(0x43, (values.gyroX as number) * 131);
if ('gyroY' in values) writeI16(0x45, (values.gyroY as number) * 131);
if ('gyroZ' in values) writeI16(0x47, (values.gyroZ as number) * 131);
if ('temp' in values) writeI16(0x41, ((values.temp as number) - 36.53) * 340);
});
const writeI16 = (regH: number, raw: number) => {
const v = Math.max(-32768, Math.min(32767, Math.round(raw))) & 0xFFFF;
device.registers[regH] = (v >> 8) & 0xFF;
device.registers[regH + 1] = v & 0xFF;
};
return () => {
removeI2CDevice(sim, device.address);
unregisterSensorUpdate(componentId);
};
registerSensorUpdate(componentId, (values) => {
if ('accelX' in values) writeI16(0x3B, (values.accelX as number) * 16384);
if ('accelY' in values) writeI16(0x3D, (values.accelY as number) * 16384);
if ('accelZ' in values) writeI16(0x3F, (values.accelZ as number) * 16384);
if ('gyroX' in values) writeI16(0x43, (values.gyroX as number) * 131);
if ('gyroY' in values) writeI16(0x45, (values.gyroY as number) * 131);
if ('gyroZ' in values) writeI16(0x47, (values.gyroZ as number) * 131);
if ('temp' in values) writeI16(0x41, ((values.temp as number) - 36.53) * 340);
});
return () => {
removeI2CDevice(sim, device.address);
unregisterSensorUpdate(componentId);
};
} else if (typeof sim.registerSensor === 'function') {
// ── ESP32 path: delegate to backend QEMU I2C slave state machine ─────
// Use (200 + addr) as a virtual pin for I2C sensors — above valid GPIO
// range (048) so it won't collide with real GPIO sensors.
const virtualPin = 200 + addr;
sim.registerSensor('mpu6050', virtualPin, { addr });
registerSensorUpdate(componentId, (values) => {
sim.updateSensor(virtualPin, values);
});
return () => {
sim.unregisterSensor(virtualPin);
unregisterSensorUpdate(componentId);
};
}
return () => {};
},
});
@ -972,28 +1023,47 @@ PartSimulationRegistry.register('microsd-card', {
*/
PartSimulationRegistry.register('bmp280', {
attachEvents: (element, simulator, _getPin, componentId) => {
const sim = simulator as any;
if (typeof sim.addI2CDevice !== 'function') return () => {};
const sim = simulator as any;
const el = element as any;
const addr = (el.address === '0x77' || el.address === 0x77) ? 0x77 : 0x76;
const dev = new VirtualBMP280(addr);
if (el.temperature !== undefined) dev.temperatureC = parseFloat(el.temperature);
if (el.pressure !== undefined) dev.pressureHPa = parseFloat(el.pressure);
if (typeof sim.addI2CDevice === 'function') {
// ── AVR / RP2040 path ──────────────────────────────────────────────────
const dev = new VirtualBMP280(addr);
sim.addI2CDevice(dev);
if (el.temperature !== undefined) dev.temperatureC = parseFloat(el.temperature);
if (el.pressure !== undefined) dev.pressureHPa = parseFloat(el.pressure);
// SensorControlPanel: update temperature / pressure in real-time
registerSensorUpdate(componentId, (values) => {
if ('temperature' in values) dev.temperatureC = values.temperature as number;
if ('pressure' in values) dev.pressureHPa = values.pressure as number;
});
sim.addI2CDevice(dev);
return () => {
removeI2CDevice(sim, dev.address);
unregisterSensorUpdate(componentId);
};
registerSensorUpdate(componentId, (values) => {
if ('temperature' in values) dev.temperatureC = values.temperature as number;
if ('pressure' in values) dev.pressureHPa = values.pressure as number;
});
return () => {
removeI2CDevice(sim, dev.address);
unregisterSensorUpdate(componentId);
};
} else if (typeof sim.registerSensor === 'function') {
// ── ESP32 path: delegate to backend QEMU BMP280 slave ─────────────────
const virtualPin = 200 + addr;
const initTemp = el.temperature !== undefined ? parseFloat(el.temperature) : 25.0;
const initPressure = el.pressure !== undefined ? parseFloat(el.pressure) : 1013.25;
sim.registerSensor('bmp280', virtualPin, { addr, temperature: initTemp, pressure: initPressure });
registerSensorUpdate(componentId, (values) => {
sim.updateSensor(virtualPin, values);
});
return () => {
sim.unregisterSensor(virtualPin);
unregisterSensorUpdate(componentId);
};
}
return () => {};
},
});
@ -1014,14 +1084,24 @@ PartSimulationRegistry.register('bmp280', {
PartSimulationRegistry.register('ds3231', {
attachEvents: (element, simulator, _getPin) => {
const sim = simulator as any;
if (typeof sim.addI2CDevice !== 'function') return () => {};
const el = element as any;
const dev = new VirtualDS3231();
if (el.temperature !== undefined) dev.temperatureC = parseFloat(el.temperature);
sim.addI2CDevice(dev);
return () => removeI2CDevice(sim, dev.address);
if (typeof sim.addI2CDevice === 'function') {
// ── AVR / RP2040 path ──────────────────────────────────────────────────
const dev = new VirtualDS3231();
if (el.temperature !== undefined) dev.temperatureC = parseFloat(el.temperature);
sim.addI2CDevice(dev);
return () => removeI2CDevice(sim, dev.address);
} else if (typeof sim.registerSensor === 'function') {
// ── ESP32 path: delegate to backend QEMU DS3231 slave ─────────────────
const virtualPin = 200 + 0x68;
const initTemp = el.temperature !== undefined ? parseFloat(el.temperature) : 25.0;
sim.registerSensor('ds3231', virtualPin, { addr: 0x68, temperature: initTemp });
return () => sim.unregisterSensor(virtualPin);
}
return () => {};
},
});
@ -1042,9 +1122,7 @@ PartSimulationRegistry.register('ds3231', {
PartSimulationRegistry.register('pcf8574', {
attachEvents: (element, simulator, _getPin) => {
const sim = simulator as any;
if (typeof sim.addI2CDevice !== 'function') return () => {};
const el = element as any;
const el = element as any;
// Parse address from element property (accepts '0x27', '39', or numeric)
let addr = 0x27;
@ -1056,15 +1134,30 @@ PartSimulationRegistry.register('pcf8574', {
if (!isNaN(parsed)) addr = parsed;
}
const dev = new VirtualPCF8574(addr);
if (typeof sim.addI2CDevice === 'function') {
// ── AVR / RP2040 path ──────────────────────────────────────────────────
const dev = new VirtualPCF8574(addr);
if (el.portState !== undefined) dev.portState = Number(el.portState) & 0xFF;
dev.onWrite = (value: number) => { el.value = value; };
sim.addI2CDevice(dev);
return () => removeI2CDevice(sim, dev.address);
// Seed port state from element if present
if (el.portState !== undefined) dev.portState = Number(el.portState) & 0xFF;
} else if (typeof sim.registerSensor === 'function') {
// ── ESP32 path: relay I2C writes via backend ───────────────────────────
const virtualPin = 200 + addr;
const dev = new VirtualPCF8574(addr);
if (el.portState !== undefined) dev.portState = Number(el.portState) & 0xFF;
dev.onWrite = (value: number) => { el.value = value; };
sim.registerSensor('pcf8574', virtualPin, { addr });
sim.addI2CTransactionListener(addr, (data: number[]) => {
if (data.length > 0) dev.writeByte(data[0]);
});
return () => {
sim.unregisterSensor(virtualPin);
sim.removeI2CTransactionListener(addr);
};
}
// Feed writes back to the element so visual components can re-render
dev.onWrite = (value: number) => { el.value = value; };
sim.addI2CDevice(dev);
return () => removeI2CDevice(sim, dev.address);
return () => {};
},
});

View File

@ -110,6 +110,23 @@ class Esp32BridgeShim {
unregisterSensor(pin: number): void {
this.bridge.sendSensorDetach(pin);
}
// ── I2C write-only device relay (SSD1306, PCF8574) ───────────────────────
private _i2cTransactionListeners = new Map<number, (data: number[]) => void>();
addI2CTransactionListener(addr: number, fn: (data: number[]) => void): void {
this._i2cTransactionListeners.set(addr, fn);
this.bridge.onI2cTransaction = (a: number, data: number[]) => {
this._i2cTransactionListeners.get(a)?.(data);
};
}
removeI2CTransactionListener(addr: number): void {
this._i2cTransactionListeners.delete(addr);
if (this._i2cTransactionListeners.size === 0) {
this.bridge.onI2cTransaction = null;
}
}
}
// ── Shared LEDC update handler (used by addBoard, setBoardType, initSimulator) ─