velxio/frontend/src/simulation/I2CBusManager.ts

216 lines
6.3 KiB
TypeScript

/**
* I2C Bus Manager — virtual I2C devices that attach to avr8js AVRTWI
*
* Each device registers at a 7-bit I2C address. When the Arduino sketch
* does Wire.beginTransmission(addr) / Wire.requestFrom(addr, ...), the
* TWI event handler routes events to the matching virtual device.
*/
import type { AVRTWI, TWIEventHandler } from 'avr8js';
// ── Virtual I2C device interface ────────────────────────────────────────────
export interface I2CDevice {
/** 7-bit I2C address (e.g. 0x27 for PCF8574 LCD backpack, 0x3C for SSD1306) */
address: number;
/** Called when master sends a byte after addressing this device for write */
writeByte(value: number): boolean; // return true for ACK
/** Called when master requests a byte from this device (read mode) */
readByte(): number;
/** Optional: called on STOP condition */
stop?(): void;
}
// ── I2C Bus Manager (TWIEventHandler for avr8js) ───────────────────────────
export class I2CBusManager implements TWIEventHandler {
private devices: Map<number, I2CDevice> = new Map();
private activeDevice: I2CDevice | null = null;
private writeMode = true;
constructor(private twi: AVRTWI) {
twi.eventHandler = this;
}
/** Register a virtual I2C device on the bus */
addDevice(device: I2CDevice): void {
this.devices.set(device.address, device);
}
/** Remove a device by address */
removeDevice(address: number): void {
this.devices.delete(address);
}
// ── TWIEventHandler implementation ──────────────────────────────────────
start(_repeated: boolean): void {
this.twi.completeStart();
}
stop(): void {
if (this.activeDevice?.stop) this.activeDevice.stop();
this.activeDevice = null;
this.twi.completeStop();
}
connectToSlave(addr: number, write: boolean): void {
const device = this.devices.get(addr);
if (device) {
this.activeDevice = device;
this.writeMode = write;
this.twi.completeConnect(true); // ACK
} else {
this.activeDevice = null;
this.twi.completeConnect(false); // NACK — no such address
}
}
writeByte(value: number): void {
if (this.activeDevice) {
const ack = this.activeDevice.writeByte(value);
this.twi.completeWrite(ack);
} else {
this.twi.completeWrite(false);
}
}
readByte(_ack: boolean): void {
if (this.activeDevice) {
const value = this.activeDevice.readByte();
this.twi.completeRead(value);
} else {
this.twi.completeRead(0xff);
}
}
}
// ── Built-in virtual I2C devices ───────────────────────────────────────────
/**
* Generic I2C memory / register device.
* Emulates a device with 256 byte registers.
* First write byte = register address, subsequent bytes = data.
* Reads return register contents sequentially.
*
* Used to test I2C communication without a specific device implementation.
*/
export class I2CMemoryDevice implements I2CDevice {
public registers = new Uint8Array(256);
private regPointer = 0;
private firstByte = true;
/** Callback fired whenever a register is written */
public onRegisterWrite: ((reg: number, value: number) => void) | null = null;
constructor(public address: number) {}
writeByte(value: number): boolean {
if (this.firstByte) {
this.regPointer = value;
this.firstByte = false;
} else {
this.registers[this.regPointer] = value;
if (this.onRegisterWrite) {
this.onRegisterWrite(this.regPointer, value);
}
this.regPointer = (this.regPointer + 1) & 0xFF;
}
return true; // ACK
}
readByte(): number {
const value = this.registers[this.regPointer];
this.regPointer = (this.regPointer + 1) & 0xFF;
return value;
}
stop(): void {
this.firstByte = true;
}
}
/**
* Virtual DS1307 RTC — returns system time via I2C (address 0x68).
* Supports Wire.requestFrom(0x68, 7) to read seconds..year in BCD.
*/
export class VirtualDS1307 implements I2CDevice {
public address = 0x68;
private regPointer = 0;
private firstByte = true;
private toBCD(n: number): number {
return ((Math.floor(n / 10) & 0xF) << 4) | (n % 10 & 0xF);
}
writeByte(value: number): boolean {
if (this.firstByte) {
this.regPointer = value;
this.firstByte = false;
}
return true;
}
readByte(): number {
const now = new Date();
let val = 0;
switch (this.regPointer) {
case 0: val = this.toBCD(now.getSeconds()); break; // seconds
case 1: val = this.toBCD(now.getMinutes()); break; // minutes
case 2: val = this.toBCD(now.getHours()); break; // hours (24h)
case 3: val = this.toBCD(now.getDay() + 1); break; // day of week (1=Sun)
case 4: val = this.toBCD(now.getDate()); break; // date
case 5: val = this.toBCD(now.getMonth() + 1); break; // month
case 6: val = this.toBCD(now.getFullYear() % 100); break; // year
default: val = 0;
}
this.regPointer = (this.regPointer + 1) & 0x3F;
return val;
}
stop(): void {
this.firstByte = true;
}
}
/**
* Virtual temperature / humidity sensor (address 0x48).
* Returns fixed temperature (configurable) and humidity.
*/
export class VirtualTempSensor implements I2CDevice {
public address = 0x48;
private regPointer = 0;
private firstByte = true;
/** Temperature in degrees C * 100 (e.g. 2350 = 23.50 C) */
public temperature = 2350;
/** Humidity in % * 100 */
public humidity = 5500;
writeByte(value: number): boolean {
if (this.firstByte) {
this.regPointer = value;
this.firstByte = false;
}
return true;
}
readByte(): number {
let val = 0;
// Register 0: temp high byte, 1: temp low byte, 2: humidity high, 3: humidity low
switch (this.regPointer) {
case 0: val = (this.temperature >> 8) & 0xFF; break;
case 1: val = this.temperature & 0xFF; break;
case 2: val = (this.humidity >> 8) & 0xFF; break;
case 3: val = this.humidity & 0xFF; break;
default: val = 0xFF;
}
this.regPointer = (this.regPointer + 1) & 0xFF;
return val;
}
stop(): void {
this.firstByte = true;
}
}