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:
parent
9761aad0be
commit
689f8e71db
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 (0–48) 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 () => {};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) ─
|
||||
|
|
|
|||
Loading…
Reference in New Issue