velxio/backend/app/services/esp32_i2c_slaves.py

325 lines
13 KiB
Python
Raw Normal View History

"""
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
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.
"""
import datetime as _datetime
# ── Protocol constants ────────────────────────────────────────────────────────
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
# ── MPU-6050 IMU ──────────────────────────────────────────────────────────────
class MPU6050Slave:
"""Full MPU-6050 register-map I2C slave emulation (address 0x68 or 0x69)."""
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:
op = event & 0xFF # low byte = operation type
data = (event >> 8) & 0xFF # high byte = data byte (for WRITE)
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.
self.first_byte = True
return 0 # ACK (0 = success in QEMU convention)
elif op == I2C_WRITE:
if self.first_byte:
# First byte after START is the register address pointer
self.reg_ptr = data
self.first_byte = False
else:
# Subsequent bytes are data written into the register map
self.regs[self.reg_ptr] = data
# Auto-clear DEVICE_RESET bit (bit 7 of PWR_MGMT_1 = 0x6B)
# so the Adafruit begin() reset-wait loop exits immediately.
if self.reg_ptr == 0x6B:
self.regs[0x6B] &= 0x7F
self.reg_ptr = (self.reg_ptr + 1) & 0xFF
return 0 # ACK
elif op == I2C_READ:
# Return the byte at the current register pointer, then advance it.
val = self.regs[self.reg_ptr]
self.reg_ptr = (self.reg_ptr + 1) & 0xFF
return val
else: # I2C_FINISH, I2C_NACK, 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] = 0x58 # chip_id BMP280 (production silicon; BME280 uses 0x60)
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:
op = event & 0xFF
data = (event >> 8) & 0xFF
if op in (I2C_START_RECV, I2C_START_SEND):
self.first_byte = True; return 0
elif op == I2C_WRITE:
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
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:
op = event & 0xFF
data = (event >> 8) & 0xFF
if op in (I2C_START_RECV, I2C_START_SEND):
self.first_byte = True; return 0
elif op == I2C_WRITE:
if self.first_byte:
self.reg_ptr = data; self.first_byte = False
return 0
elif op == I2C_READ:
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 FINISH."""
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:
op = event & 0xFF
data = (event >> 8) & 0xFF
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
if self._buf:
self._emit({'type': 'i2c_transaction',
'addr': self.addr, 'data': list(self._buf)})
self._buf = []
return 0