2026-04-08 10:02:43 +07:00
|
|
|
|
"""
|
|
|
|
|
|
esp32_i2c_slaves.py — Standalone I2C slave state machines for ESP32 QEMU simulation.
|
|
|
|
|
|
|
|
|
|
|
|
Each class emulates the I2C register map of a real sensor, handling the picsimlab
|
2026-04-10 01:06:39 +07:00
|
|
|
|
I2C event protocol as defined in hw/i2c/picsimlab_i2c.c:
|
|
|
|
|
|
|
|
|
|
|
|
picsimlab_i2c_ev(event) → passes raw QEMU i2c_event enum value:
|
|
|
|
|
|
0x00 = I2C_START_RECV — firmware doing requestFrom (read direction START)
|
|
|
|
|
|
0x01 = I2C_START_SEND — firmware doing beginTransmission (write direction START)
|
|
|
|
|
|
0x02 = I2C_START_SEND_ASYNC (rarely used)
|
|
|
|
|
|
0x03 = I2C_FINISH — end of transaction (STOP or RSTART between write+read)
|
|
|
|
|
|
0x04 = I2C_NACK
|
|
|
|
|
|
|
|
|
|
|
|
picsimlab_i2c_tx(data) → event = (data << 8) | (I2C_NACK+1) = (data<<8)|0x05
|
|
|
|
|
|
picsimlab_i2c_rx() → event = I2C_NACK+2 = 0x06 (return data byte to firmware)
|
|
|
|
|
|
|
|
|
|
|
|
ACK convention (matches QEMU i2c core):
|
|
|
|
|
|
return 0 → ACK (success, device present / byte accepted)
|
|
|
|
|
|
return ≠0 → NACK (error)
|
|
|
|
|
|
For READ events: return value is the data byte delivered to the firmware.
|
2026-04-08 10:02:43 +07:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import datetime as _datetime
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-10 01:06:39 +07:00
|
|
|
|
# ── Protocol constants ────────────────────────────────────────────────────────
|
2026-04-08 10:02:54 +07:00
|
|
|
|
|
2026-04-10 01:06:39 +07:00
|
|
|
|
I2C_START_RECV = 0x00 # firmware called requestFrom (read direction START)
|
|
|
|
|
|
I2C_START_SEND = 0x01 # firmware called beginTransmission (write direction START)
|
|
|
|
|
|
I2C_FINISH = 0x03 # end of transaction (STOP or repeated-START between phases)
|
|
|
|
|
|
I2C_WRITE = 0x05 # firmware sent a byte; data = (event >> 8) & 0xFF
|
|
|
|
|
|
I2C_READ = 0x06 # firmware requesting a byte; return the data byte
|
2026-04-08 10:02:54 +07:00
|
|
|
|
|
|
|
|
|
|
|
2026-04-08 10:02:43 +07:00
|
|
|
|
# ── MPU-6050 IMU ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
class MPU6050Slave:
|
|
|
|
|
|
"""Full MPU-6050 register-map I2C slave emulation (address 0x68 or 0x69)."""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, addr: int = 0x68):
|
2026-04-10 01:06:39 +07:00
|
|
|
|
self.addr = addr
|
|
|
|
|
|
self.regs = bytearray(256)
|
|
|
|
|
|
self.reg_ptr = 0
|
2026-04-08 10:02:43 +07:00
|
|
|
|
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:
|
2026-04-08 10:02:54 +07:00
|
|
|
|
op = event & 0xFF # low byte = operation type
|
|
|
|
|
|
data = (event >> 8) & 0xFF # high byte = data byte (for WRITE)
|
|
|
|
|
|
|
2026-04-10 01:06:39 +07:00
|
|
|
|
if op in (I2C_START_RECV, I2C_START_SEND):
|
|
|
|
|
|
# New transaction beginning. Reset first_byte flag.
|
|
|
|
|
|
# reg_ptr is NOT reset here — a write-then-read (repeated START)
|
|
|
|
|
|
# relies on reg_ptr having been set by the preceding WRITE phase.
|
2026-04-08 10:02:43 +07:00
|
|
|
|
self.first_byte = True
|
2026-04-10 01:06:39 +07:00
|
|
|
|
return 0 # ACK (0 = success in QEMU convention)
|
|
|
|
|
|
|
|
|
|
|
|
elif op == I2C_WRITE:
|
2026-04-08 10:02:43 +07:00
|
|
|
|
if self.first_byte:
|
2026-04-10 01:06:39 +07:00
|
|
|
|
# First byte after START is the register address pointer
|
2026-04-08 10:02:43 +07:00
|
|
|
|
self.reg_ptr = data
|
|
|
|
|
|
self.first_byte = False
|
|
|
|
|
|
else:
|
2026-04-10 01:06:39 +07:00
|
|
|
|
# Subsequent bytes are data written into the register map
|
2026-04-08 10:02:43 +07:00
|
|
|
|
self.regs[self.reg_ptr] = data
|
2026-04-10 01:06:39 +07:00
|
|
|
|
# Auto-clear DEVICE_RESET bit (bit 7 of PWR_MGMT_1 = 0x6B)
|
|
|
|
|
|
# so the Adafruit begin() reset-wait loop exits immediately.
|
2026-04-08 10:02:54 +07:00
|
|
|
|
if self.reg_ptr == 0x6B:
|
|
|
|
|
|
self.regs[0x6B] &= 0x7F
|
2026-04-08 10:02:43 +07:00
|
|
|
|
self.reg_ptr = (self.reg_ptr + 1) & 0xFF
|
2026-04-10 01:06:39 +07:00
|
|
|
|
return 0 # ACK
|
|
|
|
|
|
|
2026-04-08 10:02:54 +07:00
|
|
|
|
elif op == I2C_READ:
|
2026-04-10 01:06:39 +07:00
|
|
|
|
# Return the byte at the current register pointer, then advance it.
|
2026-04-08 10:02:43 +07:00
|
|
|
|
val = self.regs[self.reg_ptr]
|
|
|
|
|
|
self.reg_ptr = (self.reg_ptr + 1) & 0xFF
|
|
|
|
|
|
return val
|
2026-04-10 01:06:39 +07:00
|
|
|
|
|
|
|
|
|
|
else: # I2C_FINISH, I2C_NACK, unknown
|
2026-04-08 10:02:43 +07:00
|
|
|
|
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:
|
2026-04-15 03:27:30 +07:00
|
|
|
|
self.regs[0xD0] = 0x58 # chip_id BMP280 (production silicon; BME280 uses 0x60)
|
2026-04-08 10:02:43 +07:00
|
|
|
|
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:
|
2026-04-08 10:02:54 +07:00
|
|
|
|
op = event & 0xFF
|
|
|
|
|
|
data = (event >> 8) & 0xFF
|
|
|
|
|
|
|
2026-04-10 01:06:39 +07:00
|
|
|
|
if op in (I2C_START_RECV, I2C_START_SEND):
|
|
|
|
|
|
self.first_byte = True; return 0
|
|
|
|
|
|
elif op == I2C_WRITE:
|
2026-04-08 10:02:43 +07:00
|
|
|
|
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
|
2026-04-10 01:06:39 +07:00
|
|
|
|
return 0
|
2026-04-08 10:02:54 +07:00
|
|
|
|
elif op == I2C_READ:
|
2026-04-08 10:02:43 +07:00
|
|
|
|
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 ──────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
2026-04-08 10:02:54 +07:00
|
|
|
|
op = event & 0xFF
|
|
|
|
|
|
data = (event >> 8) & 0xFF
|
|
|
|
|
|
|
2026-04-10 01:06:39 +07:00
|
|
|
|
if op in (I2C_START_RECV, I2C_START_SEND):
|
|
|
|
|
|
self.first_byte = True; return 0
|
|
|
|
|
|
elif op == I2C_WRITE:
|
2026-04-08 10:02:43 +07:00
|
|
|
|
if self.first_byte:
|
|
|
|
|
|
self.reg_ptr = data; self.first_byte = False
|
2026-04-10 01:06:39 +07:00
|
|
|
|
return 0
|
2026-04-08 10:02:54 +07:00
|
|
|
|
elif op == I2C_READ:
|
2026-04-08 10:02:43 +07:00
|
|
|
|
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:
|
2026-04-10 01:06:39 +07:00
|
|
|
|
"""ACKs all I2C writes, emits complete transaction to frontend on FINISH."""
|
2026-04-08 10:02:43 +07:00
|
|
|
|
|
|
|
|
|
|
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:
|
2026-04-08 10:02:54 +07:00
|
|
|
|
op = event & 0xFF
|
|
|
|
|
|
data = (event >> 8) & 0xFF
|
|
|
|
|
|
|
2026-04-10 01:06:39 +07:00
|
|
|
|
if op in (I2C_START_RECV, I2C_START_SEND):
|
|
|
|
|
|
self._buf = []; return 0
|
|
|
|
|
|
elif op == I2C_WRITE:
|
|
|
|
|
|
self._buf.append(data); return 0
|
|
|
|
|
|
elif op == I2C_READ:
|
|
|
|
|
|
return 0xFF # write-only device
|
|
|
|
|
|
else: # I2C_FINISH — emit accumulated transaction
|
2026-04-08 10:02:43 +07:00
|
|
|
|
if self._buf:
|
|
|
|
|
|
self._emit({'type': 'i2c_transaction',
|
|
|
|
|
|
'addr': self.addr, 'data': list(self._buf)})
|
|
|
|
|
|
self._buf = []
|
|
|
|
|
|
return 0
|
2026-05-13 02:55:15 +07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Generic Proxy Slave (used for cross-board I2C bridging) ──────────────────
|
|
|
|
|
|
|
|
|
|
|
|
class ProxySlave:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Generic 256-register I2C slave whose contents are pushed by the frontend.
|
|
|
|
|
|
|
|
|
|
|
|
Used by Interconnect when a non-ESP32 board (Uno, Pico, etc.) has an
|
|
|
|
|
|
I2C device wired to an ESP32 board through a cross-board bridge. The
|
|
|
|
|
|
real device emulation lives on the frontend (an `I2CDevice` instance);
|
|
|
|
|
|
we mirror its register state into this proxy so the ESP32 firmware's
|
|
|
|
|
|
Wire master reads succeed synchronously inside QEMU.
|
|
|
|
|
|
|
feat(i2c): cross-board bridging across all velxio boards (AVR/RP2040/ESP32 xtensa+riscv)
Closes the remaining gaps in cross-board I2C so any topology of
supported boards (Uno↔ESP32, two ESP32s, Uno↔Uno↔Uno, ESP32-C3
connected to anything, etc.) works end-to-end with all I2C
components including write-only sinks (SSD1306, PCF8574, LCD-I2C).
Implementation (6 phases):
1. **BFS routing in I2CBusManager**: connectToSlave + handleExternalConnect
walk the bridge graph with a visited Set so multi-hop chains
(A↔B↔C with the device on C) resolve transparently. A new
forwarder-device shim is installed at intermediate hops so the
existing handleExternalWrite/Read/Stop machinery routes
through without per-method visited tracking.
2. **Per-peer proxy ownership in Esp32BridgeShim**: replaces the
global _proxiedAddrs Set with _proxiedByPeer Map so concurrent
bridges to the same ESP32 (e.g. wired to both Uno and Pico)
don't wipe each other's proxies on teardown. Interconnect's
per-wire teardown calls clearProxiesForPeer(peerBus) instead of
clearAllProxies.
3. **BFS-aware proxy sync**: syncProxyFromPeer now walks the peer
bus + its transitive bridges, so an ESP32 sees devices on
boards two or more hops away. _peerDeviceLookup keeps a flat
addr → device map for write-forwarding and resync.
4. **Periodic resync (250 ms)**: Esp32BridgeShim runs a setInterval
while any proxy is live, re-dumping each device with
dumpRegisters() and pushing updateProxyI2c only when an XOR-
stride hash changes. This keeps RTC time advancing visible to
ESP32 firmware without flooding the WS pipe with static
calibration dumps. Hash is primed during initial sync so the
first tick doesn't push a redundant identical buffer.
5. **Write-forwarding ProxySlave → peer**: backend ProxySlave
buffers write bytes during the transaction and emits a
`proxy_i2c_complete` event on STOP / repeated-START. Frontend
Esp32Bridge dispatches the event to a new onProxyI2cComplete
callback; the shim replays the byte sequence on the actual
peer I2CDevice via writeByte() + stop(). Makes ESP32 firmware
writes to peer SSD1306 actually repaint the OLED, peer PCF8574
latch updates, peer I2CMemoryDevice register mutations propagate.
6. **ESP32-C3 routed as bridge**: Interconnect.isBrowserSim no
longer claims c3/xiao-c3/c3-supermini — they were already
going through Esp32Bridge per the store's ESP32_RISCV_KINDS
routing, but Interconnect was treating them as browser sims
which broke proxy install. isEsp32Bridge now correctly
includes c3 family + ESP32-S3 + Arduino Nano ESP32.
Defensive: addBoard now disposes any existing shim's proxies
before overwriting simulatorMap entry so test reruns don't leak
timers.
Tests:
- 4 BFS multi-hop tests (i2c-multi-board-slave-gap.test.ts)
- 11 cross-board scenarios + per-peer + write-forward + resync
(i2c-esp32-multiboard-bridge.test.ts)
- 1 real-firmware E2E for write-forward via QEMU (compile +
load + observe proxy_i2c_complete arriving with the byte)
- New sketch fixture: esp32_i2c_write_to_peer.ino
Result: 90 test files / 1295 tests pass / 0 fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 03:51:39 +07:00
|
|
|
|
Writes from the ESP32 firmware are buffered locally AND emitted to the
|
|
|
|
|
|
frontend on STOP as a `proxy_i2c_complete` event. The frontend's
|
|
|
|
|
|
`Esp32BridgeShim` then replays the bytes on the actual peer device so
|
|
|
|
|
|
its state stays in sync (e.g. PCF8574 outputLatch updates, SSD1306
|
|
|
|
|
|
GDDRAM mutates, I2CMemoryDevice registers change).
|
|
|
|
|
|
|
|
|
|
|
|
Reads always answer from the local cached snapshot — there is no
|
|
|
|
|
|
deadlock-safe way to round-trip the frontend on a per-byte basis
|
|
|
|
|
|
inside the QEMU synchronous callback. The frontend's periodic
|
|
|
|
|
|
resync keeps the snapshot fresh for time-based devices.
|
2026-05-13 02:55:15 +07:00
|
|
|
|
"""
|
|
|
|
|
|
|
feat(i2c): cross-board bridging across all velxio boards (AVR/RP2040/ESP32 xtensa+riscv)
Closes the remaining gaps in cross-board I2C so any topology of
supported boards (Uno↔ESP32, two ESP32s, Uno↔Uno↔Uno, ESP32-C3
connected to anything, etc.) works end-to-end with all I2C
components including write-only sinks (SSD1306, PCF8574, LCD-I2C).
Implementation (6 phases):
1. **BFS routing in I2CBusManager**: connectToSlave + handleExternalConnect
walk the bridge graph with a visited Set so multi-hop chains
(A↔B↔C with the device on C) resolve transparently. A new
forwarder-device shim is installed at intermediate hops so the
existing handleExternalWrite/Read/Stop machinery routes
through without per-method visited tracking.
2. **Per-peer proxy ownership in Esp32BridgeShim**: replaces the
global _proxiedAddrs Set with _proxiedByPeer Map so concurrent
bridges to the same ESP32 (e.g. wired to both Uno and Pico)
don't wipe each other's proxies on teardown. Interconnect's
per-wire teardown calls clearProxiesForPeer(peerBus) instead of
clearAllProxies.
3. **BFS-aware proxy sync**: syncProxyFromPeer now walks the peer
bus + its transitive bridges, so an ESP32 sees devices on
boards two or more hops away. _peerDeviceLookup keeps a flat
addr → device map for write-forwarding and resync.
4. **Periodic resync (250 ms)**: Esp32BridgeShim runs a setInterval
while any proxy is live, re-dumping each device with
dumpRegisters() and pushing updateProxyI2c only when an XOR-
stride hash changes. This keeps RTC time advancing visible to
ESP32 firmware without flooding the WS pipe with static
calibration dumps. Hash is primed during initial sync so the
first tick doesn't push a redundant identical buffer.
5. **Write-forwarding ProxySlave → peer**: backend ProxySlave
buffers write bytes during the transaction and emits a
`proxy_i2c_complete` event on STOP / repeated-START. Frontend
Esp32Bridge dispatches the event to a new onProxyI2cComplete
callback; the shim replays the byte sequence on the actual
peer I2CDevice via writeByte() + stop(). Makes ESP32 firmware
writes to peer SSD1306 actually repaint the OLED, peer PCF8574
latch updates, peer I2CMemoryDevice register mutations propagate.
6. **ESP32-C3 routed as bridge**: Interconnect.isBrowserSim no
longer claims c3/xiao-c3/c3-supermini — they were already
going through Esp32Bridge per the store's ESP32_RISCV_KINDS
routing, but Interconnect was treating them as browser sims
which broke proxy install. isEsp32Bridge now correctly
includes c3 family + ESP32-S3 + Arduino Nano ESP32.
Defensive: addBoard now disposes any existing shim's proxies
before overwriting simulatorMap entry so test reruns don't leak
timers.
Tests:
- 4 BFS multi-hop tests (i2c-multi-board-slave-gap.test.ts)
- 11 cross-board scenarios + per-peer + write-forward + resync
(i2c-esp32-multiboard-bridge.test.ts)
- 1 real-firmware E2E for write-forward via QEMU (compile +
load + observe proxy_i2c_complete arriving with the byte)
- New sketch fixture: esp32_i2c_write_to_peer.ino
Result: 90 test files / 1295 tests pass / 0 fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 03:51:39 +07:00
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
addr: int,
|
|
|
|
|
|
regs: bytes | bytearray | None = None,
|
|
|
|
|
|
emit_fn=None,
|
|
|
|
|
|
) -> None:
|
2026-05-13 02:55:15 +07:00
|
|
|
|
self.addr = addr
|
|
|
|
|
|
self.regs = bytearray(regs) if regs else bytearray(256)
|
|
|
|
|
|
if len(self.regs) < 256:
|
|
|
|
|
|
self.regs.extend(bytes(256 - len(self.regs)))
|
|
|
|
|
|
self.reg_ptr = 0
|
|
|
|
|
|
self.first_byte = True
|
feat(i2c): cross-board bridging across all velxio boards (AVR/RP2040/ESP32 xtensa+riscv)
Closes the remaining gaps in cross-board I2C so any topology of
supported boards (Uno↔ESP32, two ESP32s, Uno↔Uno↔Uno, ESP32-C3
connected to anything, etc.) works end-to-end with all I2C
components including write-only sinks (SSD1306, PCF8574, LCD-I2C).
Implementation (6 phases):
1. **BFS routing in I2CBusManager**: connectToSlave + handleExternalConnect
walk the bridge graph with a visited Set so multi-hop chains
(A↔B↔C with the device on C) resolve transparently. A new
forwarder-device shim is installed at intermediate hops so the
existing handleExternalWrite/Read/Stop machinery routes
through without per-method visited tracking.
2. **Per-peer proxy ownership in Esp32BridgeShim**: replaces the
global _proxiedAddrs Set with _proxiedByPeer Map so concurrent
bridges to the same ESP32 (e.g. wired to both Uno and Pico)
don't wipe each other's proxies on teardown. Interconnect's
per-wire teardown calls clearProxiesForPeer(peerBus) instead of
clearAllProxies.
3. **BFS-aware proxy sync**: syncProxyFromPeer now walks the peer
bus + its transitive bridges, so an ESP32 sees devices on
boards two or more hops away. _peerDeviceLookup keeps a flat
addr → device map for write-forwarding and resync.
4. **Periodic resync (250 ms)**: Esp32BridgeShim runs a setInterval
while any proxy is live, re-dumping each device with
dumpRegisters() and pushing updateProxyI2c only when an XOR-
stride hash changes. This keeps RTC time advancing visible to
ESP32 firmware without flooding the WS pipe with static
calibration dumps. Hash is primed during initial sync so the
first tick doesn't push a redundant identical buffer.
5. **Write-forwarding ProxySlave → peer**: backend ProxySlave
buffers write bytes during the transaction and emits a
`proxy_i2c_complete` event on STOP / repeated-START. Frontend
Esp32Bridge dispatches the event to a new onProxyI2cComplete
callback; the shim replays the byte sequence on the actual
peer I2CDevice via writeByte() + stop(). Makes ESP32 firmware
writes to peer SSD1306 actually repaint the OLED, peer PCF8574
latch updates, peer I2CMemoryDevice register mutations propagate.
6. **ESP32-C3 routed as bridge**: Interconnect.isBrowserSim no
longer claims c3/xiao-c3/c3-supermini — they were already
going through Esp32Bridge per the store's ESP32_RISCV_KINDS
routing, but Interconnect was treating them as browser sims
which broke proxy install. isEsp32Bridge now correctly
includes c3 family + ESP32-S3 + Arduino Nano ESP32.
Defensive: addBoard now disposes any existing shim's proxies
before overwriting simulatorMap entry so test reruns don't leak
timers.
Tests:
- 4 BFS multi-hop tests (i2c-multi-board-slave-gap.test.ts)
- 11 cross-board scenarios + per-peer + write-forward + resync
(i2c-esp32-multiboard-bridge.test.ts)
- 1 real-firmware E2E for write-forward via QEMU (compile +
load + observe proxy_i2c_complete arriving with the byte)
- New sketch fixture: esp32_i2c_write_to_peer.ino
Result: 90 test files / 1295 tests pass / 0 fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 03:51:39 +07:00
|
|
|
|
# Buffer of bytes the master wrote during the current
|
|
|
|
|
|
# transaction. Flushed as `proxy_i2c_complete` on STOP /
|
|
|
|
|
|
# repeated-START so the frontend can replay them on the actual
|
|
|
|
|
|
# peer device. The first byte is preserved as `data[0]` because
|
|
|
|
|
|
# most peer `I2CDevice` implementations treat their first
|
|
|
|
|
|
# writeByte() call as the pointer/control byte.
|
|
|
|
|
|
self._emit = emit_fn
|
|
|
|
|
|
self._write_buf: list[int] = []
|
|
|
|
|
|
self._wrote = False
|
2026-05-13 02:55:15 +07:00
|
|
|
|
|
|
|
|
|
|
def update_registers(self, regs: bytes | bytearray) -> None:
|
|
|
|
|
|
"""Replace the register dump. Pads or truncates to 256 bytes."""
|
|
|
|
|
|
new = bytearray(regs)
|
|
|
|
|
|
if len(new) < 256:
|
|
|
|
|
|
new.extend(bytes(256 - len(new)))
|
|
|
|
|
|
elif len(new) > 256:
|
|
|
|
|
|
new = new[:256]
|
|
|
|
|
|
self.regs = new
|
|
|
|
|
|
|
feat(i2c): cross-board bridging across all velxio boards (AVR/RP2040/ESP32 xtensa+riscv)
Closes the remaining gaps in cross-board I2C so any topology of
supported boards (Uno↔ESP32, two ESP32s, Uno↔Uno↔Uno, ESP32-C3
connected to anything, etc.) works end-to-end with all I2C
components including write-only sinks (SSD1306, PCF8574, LCD-I2C).
Implementation (6 phases):
1. **BFS routing in I2CBusManager**: connectToSlave + handleExternalConnect
walk the bridge graph with a visited Set so multi-hop chains
(A↔B↔C with the device on C) resolve transparently. A new
forwarder-device shim is installed at intermediate hops so the
existing handleExternalWrite/Read/Stop machinery routes
through without per-method visited tracking.
2. **Per-peer proxy ownership in Esp32BridgeShim**: replaces the
global _proxiedAddrs Set with _proxiedByPeer Map so concurrent
bridges to the same ESP32 (e.g. wired to both Uno and Pico)
don't wipe each other's proxies on teardown. Interconnect's
per-wire teardown calls clearProxiesForPeer(peerBus) instead of
clearAllProxies.
3. **BFS-aware proxy sync**: syncProxyFromPeer now walks the peer
bus + its transitive bridges, so an ESP32 sees devices on
boards two or more hops away. _peerDeviceLookup keeps a flat
addr → device map for write-forwarding and resync.
4. **Periodic resync (250 ms)**: Esp32BridgeShim runs a setInterval
while any proxy is live, re-dumping each device with
dumpRegisters() and pushing updateProxyI2c only when an XOR-
stride hash changes. This keeps RTC time advancing visible to
ESP32 firmware without flooding the WS pipe with static
calibration dumps. Hash is primed during initial sync so the
first tick doesn't push a redundant identical buffer.
5. **Write-forwarding ProxySlave → peer**: backend ProxySlave
buffers write bytes during the transaction and emits a
`proxy_i2c_complete` event on STOP / repeated-START. Frontend
Esp32Bridge dispatches the event to a new onProxyI2cComplete
callback; the shim replays the byte sequence on the actual
peer I2CDevice via writeByte() + stop(). Makes ESP32 firmware
writes to peer SSD1306 actually repaint the OLED, peer PCF8574
latch updates, peer I2CMemoryDevice register mutations propagate.
6. **ESP32-C3 routed as bridge**: Interconnect.isBrowserSim no
longer claims c3/xiao-c3/c3-supermini — they were already
going through Esp32Bridge per the store's ESP32_RISCV_KINDS
routing, but Interconnect was treating them as browser sims
which broke proxy install. isEsp32Bridge now correctly
includes c3 family + ESP32-S3 + Arduino Nano ESP32.
Defensive: addBoard now disposes any existing shim's proxies
before overwriting simulatorMap entry so test reruns don't leak
timers.
Tests:
- 4 BFS multi-hop tests (i2c-multi-board-slave-gap.test.ts)
- 11 cross-board scenarios + per-peer + write-forward + resync
(i2c-esp32-multiboard-bridge.test.ts)
- 1 real-firmware E2E for write-forward via QEMU (compile +
load + observe proxy_i2c_complete arriving with the byte)
- New sketch fixture: esp32_i2c_write_to_peer.ino
Result: 90 test files / 1295 tests pass / 0 fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 03:51:39 +07:00
|
|
|
|
def _flush_write_transaction(self) -> None:
|
|
|
|
|
|
"""Forward the accumulated write bytes back to the frontend."""
|
|
|
|
|
|
if self._wrote and self._emit is not None and self._write_buf:
|
|
|
|
|
|
try:
|
|
|
|
|
|
self._emit({
|
|
|
|
|
|
'type': 'proxy_i2c_complete',
|
|
|
|
|
|
'addr': self.addr,
|
|
|
|
|
|
'data': list(self._write_buf),
|
|
|
|
|
|
})
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
self._write_buf = []
|
|
|
|
|
|
self._wrote = False
|
|
|
|
|
|
|
2026-05-13 02:55:15 +07:00
|
|
|
|
def handle_event(self, event: int) -> int:
|
|
|
|
|
|
op = event & 0xFF
|
|
|
|
|
|
data = (event >> 8) & 0xFF
|
|
|
|
|
|
|
|
|
|
|
|
if op in (I2C_START_RECV, I2C_START_SEND):
|
feat(i2c): cross-board bridging across all velxio boards (AVR/RP2040/ESP32 xtensa+riscv)
Closes the remaining gaps in cross-board I2C so any topology of
supported boards (Uno↔ESP32, two ESP32s, Uno↔Uno↔Uno, ESP32-C3
connected to anything, etc.) works end-to-end with all I2C
components including write-only sinks (SSD1306, PCF8574, LCD-I2C).
Implementation (6 phases):
1. **BFS routing in I2CBusManager**: connectToSlave + handleExternalConnect
walk the bridge graph with a visited Set so multi-hop chains
(A↔B↔C with the device on C) resolve transparently. A new
forwarder-device shim is installed at intermediate hops so the
existing handleExternalWrite/Read/Stop machinery routes
through without per-method visited tracking.
2. **Per-peer proxy ownership in Esp32BridgeShim**: replaces the
global _proxiedAddrs Set with _proxiedByPeer Map so concurrent
bridges to the same ESP32 (e.g. wired to both Uno and Pico)
don't wipe each other's proxies on teardown. Interconnect's
per-wire teardown calls clearProxiesForPeer(peerBus) instead of
clearAllProxies.
3. **BFS-aware proxy sync**: syncProxyFromPeer now walks the peer
bus + its transitive bridges, so an ESP32 sees devices on
boards two or more hops away. _peerDeviceLookup keeps a flat
addr → device map for write-forwarding and resync.
4. **Periodic resync (250 ms)**: Esp32BridgeShim runs a setInterval
while any proxy is live, re-dumping each device with
dumpRegisters() and pushing updateProxyI2c only when an XOR-
stride hash changes. This keeps RTC time advancing visible to
ESP32 firmware without flooding the WS pipe with static
calibration dumps. Hash is primed during initial sync so the
first tick doesn't push a redundant identical buffer.
5. **Write-forwarding ProxySlave → peer**: backend ProxySlave
buffers write bytes during the transaction and emits a
`proxy_i2c_complete` event on STOP / repeated-START. Frontend
Esp32Bridge dispatches the event to a new onProxyI2cComplete
callback; the shim replays the byte sequence on the actual
peer I2CDevice via writeByte() + stop(). Makes ESP32 firmware
writes to peer SSD1306 actually repaint the OLED, peer PCF8574
latch updates, peer I2CMemoryDevice register mutations propagate.
6. **ESP32-C3 routed as bridge**: Interconnect.isBrowserSim no
longer claims c3/xiao-c3/c3-supermini — they were already
going through Esp32Bridge per the store's ESP32_RISCV_KINDS
routing, but Interconnect was treating them as browser sims
which broke proxy install. isEsp32Bridge now correctly
includes c3 family + ESP32-S3 + Arduino Nano ESP32.
Defensive: addBoard now disposes any existing shim's proxies
before overwriting simulatorMap entry so test reruns don't leak
timers.
Tests:
- 4 BFS multi-hop tests (i2c-multi-board-slave-gap.test.ts)
- 11 cross-board scenarios + per-peer + write-forward + resync
(i2c-esp32-multiboard-bridge.test.ts)
- 1 real-firmware E2E for write-forward via QEMU (compile +
load + observe proxy_i2c_complete arriving with the byte)
- New sketch fixture: esp32_i2c_write_to_peer.ino
Result: 90 test files / 1295 tests pass / 0 fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 03:51:39 +07:00
|
|
|
|
# Repeated START or fresh transaction. If we accumulated a
|
|
|
|
|
|
# write phase before this (write-then-read pattern), flush
|
|
|
|
|
|
# it now so the peer device sees the pointer-byte plus any
|
|
|
|
|
|
# data bytes BEFORE the read phase starts on the frontend
|
|
|
|
|
|
# mirror.
|
|
|
|
|
|
self._flush_write_transaction()
|
2026-05-13 02:55:15 +07:00
|
|
|
|
self.first_byte = True
|
|
|
|
|
|
return 0
|
|
|
|
|
|
elif op == I2C_WRITE:
|
feat(i2c): cross-board bridging across all velxio boards (AVR/RP2040/ESP32 xtensa+riscv)
Closes the remaining gaps in cross-board I2C so any topology of
supported boards (Uno↔ESP32, two ESP32s, Uno↔Uno↔Uno, ESP32-C3
connected to anything, etc.) works end-to-end with all I2C
components including write-only sinks (SSD1306, PCF8574, LCD-I2C).
Implementation (6 phases):
1. **BFS routing in I2CBusManager**: connectToSlave + handleExternalConnect
walk the bridge graph with a visited Set so multi-hop chains
(A↔B↔C with the device on C) resolve transparently. A new
forwarder-device shim is installed at intermediate hops so the
existing handleExternalWrite/Read/Stop machinery routes
through without per-method visited tracking.
2. **Per-peer proxy ownership in Esp32BridgeShim**: replaces the
global _proxiedAddrs Set with _proxiedByPeer Map so concurrent
bridges to the same ESP32 (e.g. wired to both Uno and Pico)
don't wipe each other's proxies on teardown. Interconnect's
per-wire teardown calls clearProxiesForPeer(peerBus) instead of
clearAllProxies.
3. **BFS-aware proxy sync**: syncProxyFromPeer now walks the peer
bus + its transitive bridges, so an ESP32 sees devices on
boards two or more hops away. _peerDeviceLookup keeps a flat
addr → device map for write-forwarding and resync.
4. **Periodic resync (250 ms)**: Esp32BridgeShim runs a setInterval
while any proxy is live, re-dumping each device with
dumpRegisters() and pushing updateProxyI2c only when an XOR-
stride hash changes. This keeps RTC time advancing visible to
ESP32 firmware without flooding the WS pipe with static
calibration dumps. Hash is primed during initial sync so the
first tick doesn't push a redundant identical buffer.
5. **Write-forwarding ProxySlave → peer**: backend ProxySlave
buffers write bytes during the transaction and emits a
`proxy_i2c_complete` event on STOP / repeated-START. Frontend
Esp32Bridge dispatches the event to a new onProxyI2cComplete
callback; the shim replays the byte sequence on the actual
peer I2CDevice via writeByte() + stop(). Makes ESP32 firmware
writes to peer SSD1306 actually repaint the OLED, peer PCF8574
latch updates, peer I2CMemoryDevice register mutations propagate.
6. **ESP32-C3 routed as bridge**: Interconnect.isBrowserSim no
longer claims c3/xiao-c3/c3-supermini — they were already
going through Esp32Bridge per the store's ESP32_RISCV_KINDS
routing, but Interconnect was treating them as browser sims
which broke proxy install. isEsp32Bridge now correctly
includes c3 family + ESP32-S3 + Arduino Nano ESP32.
Defensive: addBoard now disposes any existing shim's proxies
before overwriting simulatorMap entry so test reruns don't leak
timers.
Tests:
- 4 BFS multi-hop tests (i2c-multi-board-slave-gap.test.ts)
- 11 cross-board scenarios + per-peer + write-forward + resync
(i2c-esp32-multiboard-bridge.test.ts)
- 1 real-firmware E2E for write-forward via QEMU (compile +
load + observe proxy_i2c_complete arriving with the byte)
- New sketch fixture: esp32_i2c_write_to_peer.ino
Result: 90 test files / 1295 tests pass / 0 fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 03:51:39 +07:00
|
|
|
|
# Capture every byte the master sends for the write-forward
|
|
|
|
|
|
# path. Includes the pointer-byte so the peer device's
|
|
|
|
|
|
# writeByte() runs through its full state machine.
|
|
|
|
|
|
self._write_buf.append(data)
|
|
|
|
|
|
self._wrote = True
|
2026-05-13 02:55:15 +07:00
|
|
|
|
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 0
|
|
|
|
|
|
elif op == I2C_READ:
|
|
|
|
|
|
val = self.regs[self.reg_ptr]
|
|
|
|
|
|
self.reg_ptr = (self.reg_ptr + 1) & 0xFF
|
|
|
|
|
|
return val
|
feat(i2c): cross-board bridging across all velxio boards (AVR/RP2040/ESP32 xtensa+riscv)
Closes the remaining gaps in cross-board I2C so any topology of
supported boards (Uno↔ESP32, two ESP32s, Uno↔Uno↔Uno, ESP32-C3
connected to anything, etc.) works end-to-end with all I2C
components including write-only sinks (SSD1306, PCF8574, LCD-I2C).
Implementation (6 phases):
1. **BFS routing in I2CBusManager**: connectToSlave + handleExternalConnect
walk the bridge graph with a visited Set so multi-hop chains
(A↔B↔C with the device on C) resolve transparently. A new
forwarder-device shim is installed at intermediate hops so the
existing handleExternalWrite/Read/Stop machinery routes
through without per-method visited tracking.
2. **Per-peer proxy ownership in Esp32BridgeShim**: replaces the
global _proxiedAddrs Set with _proxiedByPeer Map so concurrent
bridges to the same ESP32 (e.g. wired to both Uno and Pico)
don't wipe each other's proxies on teardown. Interconnect's
per-wire teardown calls clearProxiesForPeer(peerBus) instead of
clearAllProxies.
3. **BFS-aware proxy sync**: syncProxyFromPeer now walks the peer
bus + its transitive bridges, so an ESP32 sees devices on
boards two or more hops away. _peerDeviceLookup keeps a flat
addr → device map for write-forwarding and resync.
4. **Periodic resync (250 ms)**: Esp32BridgeShim runs a setInterval
while any proxy is live, re-dumping each device with
dumpRegisters() and pushing updateProxyI2c only when an XOR-
stride hash changes. This keeps RTC time advancing visible to
ESP32 firmware without flooding the WS pipe with static
calibration dumps. Hash is primed during initial sync so the
first tick doesn't push a redundant identical buffer.
5. **Write-forwarding ProxySlave → peer**: backend ProxySlave
buffers write bytes during the transaction and emits a
`proxy_i2c_complete` event on STOP / repeated-START. Frontend
Esp32Bridge dispatches the event to a new onProxyI2cComplete
callback; the shim replays the byte sequence on the actual
peer I2CDevice via writeByte() + stop(). Makes ESP32 firmware
writes to peer SSD1306 actually repaint the OLED, peer PCF8574
latch updates, peer I2CMemoryDevice register mutations propagate.
6. **ESP32-C3 routed as bridge**: Interconnect.isBrowserSim no
longer claims c3/xiao-c3/c3-supermini — they were already
going through Esp32Bridge per the store's ESP32_RISCV_KINDS
routing, but Interconnect was treating them as browser sims
which broke proxy install. isEsp32Bridge now correctly
includes c3 family + ESP32-S3 + Arduino Nano ESP32.
Defensive: addBoard now disposes any existing shim's proxies
before overwriting simulatorMap entry so test reruns don't leak
timers.
Tests:
- 4 BFS multi-hop tests (i2c-multi-board-slave-gap.test.ts)
- 11 cross-board scenarios + per-peer + write-forward + resync
(i2c-esp32-multiboard-bridge.test.ts)
- 1 real-firmware E2E for write-forward via QEMU (compile +
load + observe proxy_i2c_complete arriving with the byte)
- New sketch fixture: esp32_i2c_write_to_peer.ino
Result: 90 test files / 1295 tests pass / 0 fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 03:51:39 +07:00
|
|
|
|
else: # I2C_FINISH / NACK / unknown
|
|
|
|
|
|
self._flush_write_transaction()
|
2026-05-13 02:55:15 +07:00
|
|
|
|
self.first_byte = True
|
|
|
|
|
|
return 0
|