2026-03-05 16:56:14 +07:00
|
|
|
|
/**
|
2026-05-13 00:26:33 +07:00
|
|
|
|
* I2C Bus Manager — virtual I2C bus shared between an MCU peripheral
|
|
|
|
|
|
* (avr8js AVRTWI or rp2040js RPI2C) and a set of JavaScript virtual
|
|
|
|
|
|
* devices, with optional cross-board bridging for multi-board sims.
|
2026-03-05 16:56:14 +07:00
|
|
|
|
*
|
|
|
|
|
|
* Each device registers at a 7-bit I2C address. When the Arduino sketch
|
|
|
|
|
|
* does Wire.beginTransmission(addr) / Wire.requestFrom(addr, ...), the
|
2026-05-13 00:26:33 +07:00
|
|
|
|
* MCU peripheral's event handler routes events to the matching virtual
|
|
|
|
|
|
* device on the LOCAL bus, OR — if a bridge to another board's bus is
|
|
|
|
|
|
* installed and the address is registered THERE — to the remote device.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Bridges are installed by Interconnect when both SDA and SCL of two
|
|
|
|
|
|
* boards are wired together. This lets two physical-style boards
|
|
|
|
|
|
* exchange I2C transactions without requiring slave-mode emulation
|
|
|
|
|
|
* inside avr8js / rp2040js (neither library supports it natively).
|
2026-03-05 16:56:14 +07:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
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 */
|
2026-04-22 02:45:45 +07:00
|
|
|
|
writeByte(value: number): boolean; // return true for ACK
|
2026-03-05 16:56:14 +07:00
|
|
|
|
/** Called when master requests a byte from this device (read mode) */
|
|
|
|
|
|
readByte(): number;
|
|
|
|
|
|
/** Optional: called on STOP condition */
|
|
|
|
|
|
stop?(): void;
|
2026-05-13 02:55:15 +07:00
|
|
|
|
/**
|
|
|
|
|
|
* Optional snapshot of the device's 256-byte register state. Used by
|
|
|
|
|
|
* the cross-board I2C proxy path (Interconnect → Esp32Bridge) to mirror
|
|
|
|
|
|
* the device into a backend `ProxySlave` so ESP32 firmware running in
|
|
|
|
|
|
* QEMU can read it synchronously. Devices that don't have a register
|
|
|
|
|
|
* map (write-only sinks, time-based responders) can omit this.
|
|
|
|
|
|
*/
|
|
|
|
|
|
dumpRegisters?(): Uint8Array;
|
2026-03-05 16:56:14 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 00:26:33 +07:00
|
|
|
|
/**
|
|
|
|
|
|
* Minimal contract the I2C bus needs from the MCU peripheral that is
|
|
|
|
|
|
* driving it as master. Both avr8js AVRTWI and rp2040js RPI2C
|
|
|
|
|
|
* implement this shape verbatim.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface I2CMaster {
|
|
|
|
|
|
completeStart(): void;
|
|
|
|
|
|
completeStop(): void;
|
|
|
|
|
|
completeConnect(ack: boolean): void;
|
|
|
|
|
|
completeWrite(ack: boolean): void;
|
|
|
|
|
|
completeRead(value: number): void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── I2C Bus Manager (implements TWIEventHandler for avr8js) ────────────────
|
2026-03-05 16:56:14 +07:00
|
|
|
|
|
|
|
|
|
|
export class I2CBusManager implements TWIEventHandler {
|
|
|
|
|
|
private devices: Map<number, I2CDevice> = new Map();
|
|
|
|
|
|
private activeDevice: I2CDevice | null = null;
|
|
|
|
|
|
private writeMode = true;
|
|
|
|
|
|
|
2026-05-13 00:26:33 +07:00
|
|
|
|
/** Peer buses that this bus can forward transactions to when the requested address is not local. */
|
|
|
|
|
|
private bridges: I2CBusManager[] = [];
|
|
|
|
|
|
/** When the master-side transaction was routed to a peer, this holds it. */
|
|
|
|
|
|
private activeExternal: I2CBusManager | null = null;
|
|
|
|
|
|
/** When this bus is acting as the target of an external peer's master, this holds the addressed device. */
|
|
|
|
|
|
private externalActiveDevice: I2CDevice | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Construct a bus bound to an `I2CMaster`. For backward
|
|
|
|
|
|
* compatibility, if the master has a settable `eventHandler`
|
|
|
|
|
|
* property (the AVRTWI shape), it is wired to `this` automatically
|
|
|
|
|
|
* so existing AVRSimulator code continues to work unchanged. For
|
|
|
|
|
|
* peripherals with per-callback wiring (RPI2C), the caller is
|
|
|
|
|
|
* responsible for routing each master event into the bus's methods.
|
|
|
|
|
|
*/
|
|
|
|
|
|
constructor(private master: I2CMaster) {
|
|
|
|
|
|
this.bindEventHandler(master);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bindEventHandler(master: I2CMaster): void {
|
|
|
|
|
|
if (
|
|
|
|
|
|
master !== null &&
|
|
|
|
|
|
typeof master === 'object' &&
|
|
|
|
|
|
'eventHandler' in (master as object)
|
|
|
|
|
|
) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
(master as { eventHandler: TWIEventHandler }).eventHandler = this;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
/* setter rejected — caller will wire events manually */
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Swap the master peripheral this bus drives. Used when the
|
|
|
|
|
|
* I2CBusManager is constructed early (so cross-board bridges and
|
|
|
|
|
|
* device registration can happen before firmware loads) and the
|
|
|
|
|
|
* real MCU peripheral becomes available later (e.g. after loadHex).
|
|
|
|
|
|
* Local devices and bridges are preserved.
|
|
|
|
|
|
*/
|
|
|
|
|
|
attachMaster(master: I2CMaster): void {
|
|
|
|
|
|
this.master = master;
|
|
|
|
|
|
this.bindEventHandler(master);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Backward-compat accessor for the underlying AVRTWI, when constructed from one. */
|
|
|
|
|
|
get twi(): AVRTWI {
|
|
|
|
|
|
return this.master as AVRTWI;
|
2026-03-05 16:56:14 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 02:55:15 +07:00
|
|
|
|
/**
|
|
|
|
|
|
* Snapshot of currently-registered local devices. Used by Interconnect
|
|
|
|
|
|
* to enumerate which addresses to mirror as proxies on a bridged ESP32.
|
|
|
|
|
|
*/
|
|
|
|
|
|
listDevices(): I2CDevice[] {
|
|
|
|
|
|
return Array.from(this.devices.values());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 00:26:33 +07:00
|
|
|
|
// ── Cross-board bridging ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Install a peer bus that this bus will forward unresolved master
|
|
|
|
|
|
* transactions to. The pair is one-directional — to make traffic
|
|
|
|
|
|
* flow in both directions, call attachBridge symmetrically on both
|
|
|
|
|
|
* buses. Idempotent.
|
|
|
|
|
|
*/
|
|
|
|
|
|
attachBridge(peer: I2CBusManager): void {
|
|
|
|
|
|
if (peer === this) return;
|
|
|
|
|
|
if (!this.bridges.includes(peer)) this.bridges.push(peer);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Detach a previously-installed peer bus. */
|
|
|
|
|
|
detachBridge(peer: I2CBusManager): void {
|
|
|
|
|
|
this.bridges = this.bridges.filter((b) => b !== peer);
|
|
|
|
|
|
if (this.activeExternal === peer) this.activeExternal = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Whether this bus is currently acting as a slave to an external
|
|
|
|
|
|
* master. Exposed for diagnostics + tests.
|
|
|
|
|
|
*/
|
|
|
|
|
|
isHandlingExternal(): boolean {
|
|
|
|
|
|
return this.externalActiveDevice !== null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── TWIEventHandler implementation (master-side events from the local MCU) ──
|
2026-03-05 16:56:14 +07:00
|
|
|
|
|
|
|
|
|
|
start(_repeated: boolean): void {
|
2026-05-13 00:26:33 +07:00
|
|
|
|
this.master.completeStart();
|
2026-03-05 16:56:14 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stop(): void {
|
2026-05-13 00:26:33 +07:00
|
|
|
|
if (this.activeExternal) {
|
|
|
|
|
|
this.activeExternal.handleExternalStop();
|
|
|
|
|
|
this.activeExternal = null;
|
|
|
|
|
|
} else if (this.activeDevice?.stop) {
|
|
|
|
|
|
this.activeDevice.stop();
|
|
|
|
|
|
}
|
2026-03-05 16:56:14 +07:00
|
|
|
|
this.activeDevice = null;
|
2026-05-13 00:26:33 +07:00
|
|
|
|
this.master.completeStop();
|
2026-03-05 16:56:14 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
connectToSlave(addr: number, write: boolean): void {
|
2026-05-13 00:26:33 +07:00
|
|
|
|
// 1. Local devices win — fastest path and what single-board sketches expect.
|
|
|
|
|
|
const local = this.devices.get(addr);
|
|
|
|
|
|
if (local) {
|
|
|
|
|
|
this.activeDevice = local;
|
|
|
|
|
|
this.activeExternal = null;
|
2026-03-05 16:56:14 +07:00
|
|
|
|
this.writeMode = write;
|
2026-05-13 00:26:33 +07:00
|
|
|
|
this.master.completeConnect(true);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 2. Try each bridged peer in registration order.
|
|
|
|
|
|
for (const bridge of this.bridges) {
|
|
|
|
|
|
if (bridge.handleExternalConnect(addr, write)) {
|
|
|
|
|
|
this.activeExternal = bridge;
|
|
|
|
|
|
this.activeDevice = null;
|
|
|
|
|
|
this.writeMode = write;
|
|
|
|
|
|
this.master.completeConnect(true);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-03-05 16:56:14 +07:00
|
|
|
|
}
|
2026-05-13 00:26:33 +07:00
|
|
|
|
// 3. NACK — no device anywhere knows this address.
|
|
|
|
|
|
this.activeDevice = null;
|
|
|
|
|
|
this.activeExternal = null;
|
|
|
|
|
|
this.master.completeConnect(false);
|
2026-03-05 16:56:14 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
writeByte(value: number): void {
|
|
|
|
|
|
if (this.activeDevice) {
|
2026-05-13 00:26:33 +07:00
|
|
|
|
this.master.completeWrite(this.activeDevice.writeByte(value));
|
|
|
|
|
|
} else if (this.activeExternal) {
|
|
|
|
|
|
this.master.completeWrite(this.activeExternal.handleExternalWrite(value));
|
2026-03-05 16:56:14 +07:00
|
|
|
|
} else {
|
2026-05-13 00:26:33 +07:00
|
|
|
|
this.master.completeWrite(false);
|
2026-03-05 16:56:14 +07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
readByte(_ack: boolean): void {
|
|
|
|
|
|
if (this.activeDevice) {
|
2026-05-13 00:26:33 +07:00
|
|
|
|
this.master.completeRead(this.activeDevice.readByte());
|
|
|
|
|
|
} else if (this.activeExternal) {
|
|
|
|
|
|
this.master.completeRead(this.activeExternal.handleExternalRead());
|
2026-03-05 16:56:14 +07:00
|
|
|
|
} else {
|
2026-05-13 00:26:33 +07:00
|
|
|
|
this.master.completeRead(0xff);
|
2026-03-05 16:56:14 +07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-13 00:26:33 +07:00
|
|
|
|
|
|
|
|
|
|
// ── External-master inbound handlers (called by a bridged peer bus) ────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Attempt to address `addr` on this bus's local devices on behalf of
|
|
|
|
|
|
* an external master. Returns true when this bus has a device that
|
|
|
|
|
|
* acknowledged the addressing phase, false otherwise (NACK).
|
|
|
|
|
|
*/
|
|
|
|
|
|
handleExternalConnect(addr: number, _write: boolean): boolean {
|
|
|
|
|
|
const dev = this.devices.get(addr);
|
|
|
|
|
|
if (!dev) return false;
|
|
|
|
|
|
this.externalActiveDevice = dev;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** External master is sending a byte to the previously-addressed device. */
|
|
|
|
|
|
handleExternalWrite(value: number): boolean {
|
|
|
|
|
|
return this.externalActiveDevice?.writeByte(value) ?? false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** External master is requesting the next byte from the previously-addressed device. */
|
|
|
|
|
|
handleExternalRead(): number {
|
|
|
|
|
|
return this.externalActiveDevice?.readByte() ?? 0xff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** External master issued STOP — release the active device and call its lifecycle hook. */
|
|
|
|
|
|
handleExternalStop(): void {
|
|
|
|
|
|
this.externalActiveDevice?.stop?.();
|
|
|
|
|
|
this.externalActiveDevice = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* A no-op `I2CMaster` used as a placeholder before the real MCU
|
|
|
|
|
|
* peripheral has been constructed. Lets `I2CBusManager` be created
|
|
|
|
|
|
* up-front so cross-board bridges and device registrations can land
|
|
|
|
|
|
* before firmware loads, then swapped to the real peripheral via
|
|
|
|
|
|
* `attachMaster()`.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function nullI2CMaster(): I2CMaster {
|
|
|
|
|
|
return {
|
|
|
|
|
|
completeStart() {},
|
|
|
|
|
|
completeStop() {},
|
|
|
|
|
|
completeConnect(_ack: boolean) {},
|
|
|
|
|
|
completeWrite(_ack: boolean) {},
|
|
|
|
|
|
completeRead(_value: number) {},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Wire a non-AVR I2C master (e.g. rp2040js RPI2C) into an
|
|
|
|
|
|
* `I2CBusManager`. Returns the bus, with the master peripheral's
|
|
|
|
|
|
* `onStart` / `onConnect` / `onWriteByte` / `onReadByte` / `onStop`
|
|
|
|
|
|
* callbacks routed to `bus.start` etc. Matches the per-callback
|
|
|
|
|
|
* pattern RPI2C uses (it does not have a single `eventHandler`).
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function wireRpI2cToBus(
|
|
|
|
|
|
master: I2CMaster & {
|
|
|
|
|
|
onStart?: () => void;
|
|
|
|
|
|
onConnect?: (address: number, mode?: number) => void;
|
|
|
|
|
|
onWriteByte?: (value: number) => void;
|
|
|
|
|
|
onReadByte?: (ack?: boolean) => void;
|
|
|
|
|
|
onStop?: () => void;
|
|
|
|
|
|
},
|
|
|
|
|
|
bus: I2CBusManager,
|
|
|
|
|
|
): void {
|
|
|
|
|
|
master.onStart = () => bus.start(false);
|
|
|
|
|
|
master.onConnect = (addr: number, mode?: number) =>
|
|
|
|
|
|
bus.connectToSlave(addr, mode === undefined ? true : mode === 0);
|
|
|
|
|
|
master.onWriteByte = (v: number) => bus.writeByte(v);
|
|
|
|
|
|
master.onReadByte = (ack?: boolean) => bus.readByte(ack ?? true);
|
|
|
|
|
|
master.onStop = () => bus.stop();
|
2026-03-05 16:56:14 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 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);
|
|
|
|
|
|
}
|
2026-04-22 02:45:45 +07:00
|
|
|
|
this.regPointer = (this.regPointer + 1) & 0xff;
|
2026-03-05 16:56:14 +07:00
|
|
|
|
}
|
|
|
|
|
|
return true; // ACK
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
readByte(): number {
|
|
|
|
|
|
const value = this.registers[this.regPointer];
|
2026-04-22 02:45:45 +07:00
|
|
|
|
this.regPointer = (this.regPointer + 1) & 0xff;
|
2026-03-05 16:56:14 +07:00
|
|
|
|
return value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stop(): void {
|
|
|
|
|
|
this.firstByte = true;
|
|
|
|
|
|
}
|
2026-05-13 02:55:15 +07:00
|
|
|
|
|
|
|
|
|
|
/** Return the full 256-byte register snapshot for cross-board proxying. */
|
|
|
|
|
|
dumpRegisters(): Uint8Array {
|
|
|
|
|
|
return new Uint8Array(this.registers);
|
|
|
|
|
|
}
|
2026-03-05 16:56:14 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 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 {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
return ((Math.floor(n / 10) & 0xf) << 4) | ((n % 10) & 0xf);
|
2026-03-05 16:56:14 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 02:55:15 +07:00
|
|
|
|
/** Snapshot of the 7-byte time + 1-byte control register set. */
|
|
|
|
|
|
dumpRegisters(): Uint8Array {
|
|
|
|
|
|
const buf = new Uint8Array(256);
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
buf[0] = this.toBCD(now.getSeconds());
|
|
|
|
|
|
buf[1] = this.toBCD(now.getMinutes());
|
|
|
|
|
|
buf[2] = this.toBCD(now.getHours());
|
|
|
|
|
|
buf[3] = this.toBCD(now.getDay() + 1);
|
|
|
|
|
|
buf[4] = this.toBCD(now.getDate());
|
|
|
|
|
|
buf[5] = this.toBCD(now.getMonth() + 1);
|
|
|
|
|
|
buf[6] = this.toBCD(now.getFullYear() % 100);
|
|
|
|
|
|
return buf;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 16:56:14 +07:00
|
|
|
|
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) {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
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;
|
2026-03-05 16:56:14 +07:00
|
|
|
|
}
|
2026-04-22 02:45:45 +07:00
|
|
|
|
this.regPointer = (this.regPointer + 1) & 0x3f;
|
2026-03-05 16:56:14 +07:00
|
|
|
|
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) {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
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;
|
2026-03-05 16:56:14 +07:00
|
|
|
|
}
|
2026-04-22 02:45:45 +07:00
|
|
|
|
this.regPointer = (this.regPointer + 1) & 0xff;
|
2026-03-05 16:56:14 +07:00
|
|
|
|
return val;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stop(): void {
|
|
|
|
|
|
this.firstByte = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-11 22:14:18 +07:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Virtual BMP280 barometric pressure / temperature sensor.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Supports I2C addresses 0x76 (SDO=0) or 0x77 (SDO=1).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Register map (subset):
|
|
|
|
|
|
* 0x88–0x9F Calibration data (trimming parameters)
|
2026-04-15 03:27:30 +07:00
|
|
|
|
* 0xD0 chip_id = 0x58 (BMP280 production; BME280 = 0x60)
|
2026-03-11 22:14:18 +07:00
|
|
|
|
* 0xF3 status = 0x00 (measurement complete, no NVM copy)
|
|
|
|
|
|
* 0xF4 ctrl_meas (mode, osrs_t, osrs_p) — writable
|
|
|
|
|
|
* 0xF5 config — writable
|
|
|
|
|
|
* 0xF7–0xF9 press_msb / press_lsb / press_xlsb (20-bit ADC)
|
|
|
|
|
|
* 0xFA–0xFC temp_msb / temp_lsb / temp_xlsb (20-bit ADC)
|
|
|
|
|
|
*
|
|
|
|
|
|
* The calibration parameters are the BMP280 datasheet example values (Section 8.2).
|
|
|
|
|
|
* They produce T ≈ 25°C, P ≈ 1006 hPa from the corresponding raw ADC values.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Setting `temperature` (°C) and `pressure` (hPa) properties recomputes raw ADC
|
|
|
|
|
|
* registers using a binary search over the Bosch compensation formulas so that
|
|
|
|
|
|
* Arduino sketches using the Adafruit_BMP280 / Bosch driver get realistic values.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export class VirtualBMP280 implements I2CDevice {
|
|
|
|
|
|
public address: number;
|
|
|
|
|
|
|
|
|
|
|
|
private readonly registers = new Uint8Array(256);
|
2026-04-22 02:45:45 +07:00
|
|
|
|
private regPtr = 0;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
private firstByte = true;
|
|
|
|
|
|
|
|
|
|
|
|
// ── BMP280 datasheet Section 8.2 example calibration ───────────────────
|
2026-04-22 02:45:45 +07:00
|
|
|
|
private readonly DIG_T1 = 27504;
|
|
|
|
|
|
private readonly DIG_T2 = 26435;
|
|
|
|
|
|
private readonly DIG_T3 = -1000;
|
|
|
|
|
|
private readonly DIG_P1 = 36477;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
private readonly DIG_P2 = -10685;
|
2026-04-22 02:45:45 +07:00
|
|
|
|
private readonly DIG_P3 = 3024;
|
|
|
|
|
|
private readonly DIG_P4 = 2855;
|
|
|
|
|
|
private readonly DIG_P5 = 140;
|
|
|
|
|
|
private readonly DIG_P6 = -7;
|
|
|
|
|
|
private readonly DIG_P7 = 15500;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
private readonly DIG_P8 = -14600;
|
2026-04-22 02:45:45 +07:00
|
|
|
|
private readonly DIG_P9 = 6000;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
private _temperatureC = 25.0;
|
|
|
|
|
|
private _pressureHPa = 1013.25;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
|
|
|
|
|
|
constructor(address = 0x76) {
|
|
|
|
|
|
this.address = address;
|
|
|
|
|
|
this.initCalibration();
|
|
|
|
|
|
this.updateMeasurements();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Public configurable properties ──────────────────────────────────────
|
|
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
get temperatureC(): number {
|
|
|
|
|
|
return this._temperatureC;
|
|
|
|
|
|
}
|
|
|
|
|
|
set temperatureC(v: number) {
|
|
|
|
|
|
this._temperatureC = v;
|
|
|
|
|
|
this.updateMeasurements();
|
|
|
|
|
|
}
|
2026-03-11 22:14:18 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
get pressureHPa(): number {
|
|
|
|
|
|
return this._pressureHPa;
|
|
|
|
|
|
}
|
|
|
|
|
|
set pressureHPa(v: number) {
|
|
|
|
|
|
this._pressureHPa = v;
|
|
|
|
|
|
this.updateMeasurements();
|
|
|
|
|
|
}
|
2026-03-11 22:14:18 +07:00
|
|
|
|
|
|
|
|
|
|
// ── I2CDevice interface ─────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
writeByte(value: number): boolean {
|
|
|
|
|
|
if (this.firstByte) {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
this.regPtr = value;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
this.firstByte = false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Writable registers (ctrl_meas, config) — store them
|
|
|
|
|
|
this.registers[this.regPtr] = value;
|
2026-04-22 02:45:45 +07:00
|
|
|
|
this.regPtr = (this.regPtr + 1) & 0xff;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
readByte(): number {
|
|
|
|
|
|
const val = this.registers[this.regPtr];
|
2026-04-22 02:45:45 +07:00
|
|
|
|
this.regPtr = (this.regPtr + 1) & 0xff;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
return val;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stop(): void {
|
|
|
|
|
|
this.firstByte = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 02:55:15 +07:00
|
|
|
|
/** Snapshot the full 256-byte register file (calibration + ADC results). */
|
|
|
|
|
|
dumpRegisters(): Uint8Array {
|
|
|
|
|
|
return new Uint8Array(this.registers);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 22:14:18 +07:00
|
|
|
|
// ── Compensation formulas (Bosch 32-bit integer + double precision) ────
|
|
|
|
|
|
|
|
|
|
|
|
/** Compute t_fine from a 20-bit raw temperature ADC value. */
|
|
|
|
|
|
private tFine(adcT: number): number {
|
|
|
|
|
|
const var1 = (((adcT >> 3) - (this.DIG_T1 << 1)) * this.DIG_T2) >> 11;
|
2026-04-22 02:45:45 +07:00
|
|
|
|
const sub = (adcT >> 4) - this.DIG_T1;
|
|
|
|
|
|
const var2 = (((sub * sub) >> 12) * this.DIG_T3) >> 14;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
return var1 + var2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Compute temperature in 0.01 °C from a 20-bit raw ADC value. */
|
|
|
|
|
|
private compensateT(adcT: number): number {
|
|
|
|
|
|
return (this.tFine(adcT) * 5 + 128) >> 8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Compute pressure in Pa (double precision) from raw ADC values.
|
|
|
|
|
|
* Uses the Bosch floating-point compensation formula.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private compensateP(adcP: number, adcT: number): number {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
const tf = this.tFine(adcT);
|
|
|
|
|
|
let var1 = tf / 2.0 - 64000.0;
|
|
|
|
|
|
let var2 = (var1 * var1 * this.DIG_P6) / 32768.0;
|
|
|
|
|
|
var2 = var2 + var1 * this.DIG_P5 * 2.0;
|
|
|
|
|
|
var2 = var2 / 4.0 + this.DIG_P4 * 65536.0;
|
|
|
|
|
|
var1 = ((this.DIG_P3 * var1 * var1) / 524288.0 + this.DIG_P2 * var1) / 524288.0;
|
|
|
|
|
|
var1 = (1.0 + var1 / 32768.0) * this.DIG_P1;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
if (var1 === 0) return 0;
|
2026-04-22 02:45:45 +07:00
|
|
|
|
let p = 1048576.0 - adcP;
|
|
|
|
|
|
p = ((p - var2 / 4096.0) * 6250.0) / var1;
|
|
|
|
|
|
const v1b = (this.DIG_P9 * p * p) / 2147483648.0;
|
|
|
|
|
|
const v2b = (p * this.DIG_P8) / 32768.0;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
return p + (v1b + v2b + this.DIG_P7) / 16.0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Binary-search for the 20-bit raw ADC value that produces the target
|
|
|
|
|
|
* temperature (in 0.01 °C units after integer compensation).
|
|
|
|
|
|
*/
|
|
|
|
|
|
private findAdcT(targetCentidegrees: number): number {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
let lo = 0,
|
|
|
|
|
|
hi = (1 << 20) - 1;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
while (lo < hi) {
|
|
|
|
|
|
const mid = (lo + hi) >> 1;
|
|
|
|
|
|
if (this.compensateT(mid) < targetCentidegrees) lo = mid + 1;
|
|
|
|
|
|
else hi = mid;
|
|
|
|
|
|
}
|
|
|
|
|
|
return lo;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Binary-search for the 20-bit raw ADC value that produces the target
|
|
|
|
|
|
* pressure (in Pa). Pressure is monotonically decreasing in adcP.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private findAdcP(targetPa: number, adcT: number): number {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
let lo = 0,
|
|
|
|
|
|
hi = (1 << 20) - 1;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
while (lo < hi) {
|
|
|
|
|
|
const mid = (lo + hi) >> 1;
|
|
|
|
|
|
if (this.compensateP(mid, adcT) > targetPa) lo = mid + 1;
|
|
|
|
|
|
else hi = mid;
|
|
|
|
|
|
}
|
|
|
|
|
|
return lo;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Encode a 20-bit ADC value into three register bytes (msb, lsb, xlsb). */
|
|
|
|
|
|
private static encodeAdc20(val: number): [number, number, number] {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
return [(val >> 12) & 0xff, (val >> 4) & 0xff, (val & 0xf) << 4];
|
2026-03-11 22:14:18 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Register initialisation ─────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
private initCalibration(): void {
|
|
|
|
|
|
const r = this.registers;
|
2026-04-22 02:45:45 +07:00
|
|
|
|
const wu16 = (a: number, v: number) => {
|
|
|
|
|
|
r[a] = v & 0xff;
|
|
|
|
|
|
r[a + 1] = (v >> 8) & 0xff;
|
|
|
|
|
|
};
|
|
|
|
|
|
const ws16 = (a: number, v: number) => wu16(a, v & 0xffff);
|
2026-03-11 22:14:18 +07:00
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
r[0xd0] = 0x58; // chip_id BMP280 (production silicon; BME280 uses 0x60)
|
|
|
|
|
|
r[0xf3] = 0x00; // status (measurement done)
|
|
|
|
|
|
r[0xf4] = 0x00; // ctrl_meas default
|
|
|
|
|
|
r[0xf5] = 0x00; // config default
|
2026-03-11 22:14:18 +07:00
|
|
|
|
|
|
|
|
|
|
wu16(0x88, this.DIG_T1);
|
2026-04-22 02:45:45 +07:00
|
|
|
|
ws16(0x8a, this.DIG_T2);
|
|
|
|
|
|
ws16(0x8c, this.DIG_T3);
|
|
|
|
|
|
wu16(0x8e, this.DIG_P1);
|
2026-03-11 22:14:18 +07:00
|
|
|
|
ws16(0x90, this.DIG_P2);
|
|
|
|
|
|
ws16(0x92, this.DIG_P3);
|
|
|
|
|
|
ws16(0x94, this.DIG_P4);
|
|
|
|
|
|
ws16(0x96, this.DIG_P5);
|
|
|
|
|
|
ws16(0x98, this.DIG_P6);
|
2026-04-22 02:45:45 +07:00
|
|
|
|
ws16(0x9a, this.DIG_P7);
|
|
|
|
|
|
ws16(0x9c, this.DIG_P8);
|
|
|
|
|
|
ws16(0x9e, this.DIG_P9);
|
2026-03-11 22:14:18 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Recompute raw ADC registers from current temperature / pressure. */
|
|
|
|
|
|
private updateMeasurements(): void {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
const targetT = Math.round(this._temperatureC * 100);
|
|
|
|
|
|
const targetP = this._pressureHPa * 100; // hPa → Pa
|
2026-03-11 22:14:18 +07:00
|
|
|
|
|
|
|
|
|
|
const adcT = this.findAdcT(targetT);
|
|
|
|
|
|
const adcP = this.findAdcP(targetP, adcT);
|
|
|
|
|
|
|
|
|
|
|
|
const [pMsb, pLsb, pXlsb] = VirtualBMP280.encodeAdc20(adcP);
|
|
|
|
|
|
const [tMsb, tLsb, tXlsb] = VirtualBMP280.encodeAdc20(adcT);
|
|
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
this.registers[0xf7] = pMsb;
|
|
|
|
|
|
this.registers[0xf8] = pLsb;
|
|
|
|
|
|
this.registers[0xf9] = pXlsb;
|
|
|
|
|
|
this.registers[0xfa] = tMsb;
|
|
|
|
|
|
this.registers[0xfb] = tLsb;
|
|
|
|
|
|
this.registers[0xfc] = tXlsb;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Virtual DS3231 real-time clock with on-chip temperature sensor.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Address: 0x68 (fixed — same package as DS1307, one or the other per bus).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Register map (subset):
|
|
|
|
|
|
* 0x00 Seconds (BCD, 0–59)
|
|
|
|
|
|
* 0x01 Minutes (BCD, 0–59)
|
|
|
|
|
|
* 0x02 Hours (BCD, 0–23, 24-hour mode)
|
|
|
|
|
|
* 0x03 Day (BCD, 1–7, 1=Sunday)
|
|
|
|
|
|
* 0x04 Date (BCD, 1–31)
|
|
|
|
|
|
* 0x05 Month (BCD, 1–12)
|
|
|
|
|
|
* 0x06 Year (BCD, 0–99)
|
|
|
|
|
|
* 0x0E Control (writable)
|
|
|
|
|
|
* 0x0F Status = 0x00 (OSF cleared, no alarms)
|
|
|
|
|
|
* 0x11 Temp MSB = integer degrees C (signed)
|
|
|
|
|
|
* 0x12 Temp LSB = fractional in bits 7:6 (0.25°C steps)
|
|
|
|
|
|
*
|
|
|
|
|
|
* Time is taken from the host browser clock.
|
|
|
|
|
|
* Temperature defaults to 25°C and is configurable via `temperatureC`.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export class VirtualDS3231 implements I2CDevice {
|
|
|
|
|
|
public readonly address = 0x68;
|
|
|
|
|
|
|
|
|
|
|
|
public temperatureC = 25.0;
|
|
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
|
private regPtr = 0;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
private firstByte = true;
|
|
|
|
|
|
|
|
|
|
|
|
private toBCD(n: number): number {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
return ((Math.floor(n / 10) & 0xf) << 4) | ((n % 10) & 0xf);
|
2026-03-11 22:14:18 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private readRegister(reg: number): number {
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
switch (reg) {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
case 0x00:
|
|
|
|
|
|
return this.toBCD(now.getSeconds());
|
|
|
|
|
|
case 0x01:
|
|
|
|
|
|
return this.toBCD(now.getMinutes());
|
|
|
|
|
|
case 0x02:
|
|
|
|
|
|
return this.toBCD(now.getHours());
|
|
|
|
|
|
case 0x03:
|
|
|
|
|
|
return this.toBCD(now.getDay() + 1); // 1=Sunday
|
|
|
|
|
|
case 0x04:
|
|
|
|
|
|
return this.toBCD(now.getDate());
|
|
|
|
|
|
case 0x05:
|
|
|
|
|
|
return this.toBCD(now.getMonth() + 1);
|
|
|
|
|
|
case 0x06:
|
|
|
|
|
|
return this.toBCD(now.getFullYear() % 100);
|
|
|
|
|
|
case 0x0e:
|
|
|
|
|
|
return 0x00; // Control: oscillator enabled, no alarm outputs
|
|
|
|
|
|
case 0x0f:
|
|
|
|
|
|
return 0x00; // Status: OSF=0 (no oscillator stop), alarms cleared
|
2026-03-11 22:14:18 +07:00
|
|
|
|
case 0x11: {
|
|
|
|
|
|
// Temperature MSB: signed integer degrees C
|
|
|
|
|
|
const intTemp = Math.trunc(this.temperatureC);
|
2026-04-22 02:45:45 +07:00
|
|
|
|
return intTemp & 0xff;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
}
|
|
|
|
|
|
case 0x12: {
|
|
|
|
|
|
// Temperature LSB: fractional in bits 7:6, 0.25°C resolution
|
|
|
|
|
|
const frac = this.temperatureC - Math.trunc(this.temperatureC);
|
2026-04-22 02:45:45 +07:00
|
|
|
|
const q = Math.round(frac / 0.25) & 0x03;
|
|
|
|
|
|
return (q << 6) & 0xff;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
}
|
2026-04-22 02:45:45 +07:00
|
|
|
|
default:
|
|
|
|
|
|
return 0x00;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
writeByte(value: number): boolean {
|
|
|
|
|
|
if (this.firstByte) {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
this.regPtr = value;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
this.firstByte = false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Accept writes to control registers (0x0E, 0x0F, alarm registers, etc.)
|
|
|
|
|
|
// We simply ignore the written value since this is a read-only time source.
|
2026-04-22 02:45:45 +07:00
|
|
|
|
this.regPtr = (this.regPtr + 1) & 0x1f;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
readByte(): number {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
const val = this.readRegister(this.regPtr);
|
|
|
|
|
|
this.regPtr = (this.regPtr + 1) & 0x1f;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
return val;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stop(): void {
|
|
|
|
|
|
this.firstByte = true;
|
|
|
|
|
|
}
|
2026-05-13 02:55:15 +07:00
|
|
|
|
|
|
|
|
|
|
/** Snapshot the current register state (time + temperature). */
|
|
|
|
|
|
dumpRegisters(): Uint8Array {
|
|
|
|
|
|
const buf = new Uint8Array(256);
|
|
|
|
|
|
for (let r = 0; r < 0x20; r++) buf[r] = this.readRegister(r);
|
|
|
|
|
|
return buf;
|
|
|
|
|
|
}
|
2026-03-11 22:14:18 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Virtual PCF8574 8-bit I/O expander.
|
|
|
|
|
|
*
|
|
|
|
|
|
* The PCF8574 exposes a single 8-bit quasi-bidirectional I/O port over I2C.
|
|
|
|
|
|
* - Writing one byte sets the output latch (pins driven LOW for 0, HIGH/HiZ for 1).
|
|
|
|
|
|
* - Reading one byte returns the current pin state (output latch AND-ed with external input).
|
|
|
|
|
|
*
|
|
|
|
|
|
* This is the most common I2C interface for 4-bit LCD backpacks.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Configurable address: 0x20–0x27 (PCF8574) or 0x38–0x3F (PCF8574A).
|
|
|
|
|
|
* Default: 0x27 (all address pins HIGH, typical for LCD backpacks).
|
|
|
|
|
|
*
|
|
|
|
|
|
* `portState` holds the current 8-bit port value read back by the Arduino.
|
|
|
|
|
|
* Sketch writes update `outputLatch`; reads return `portState & outputLatch` (open-drain).
|
|
|
|
|
|
*/
|
|
|
|
|
|
export class VirtualPCF8574 implements I2CDevice {
|
|
|
|
|
|
public address: number;
|
|
|
|
|
|
|
|
|
|
|
|
/** Current state of the 8 I/O pins as seen from the outside (external input). */
|
2026-04-22 02:45:45 +07:00
|
|
|
|
public portState = 0xff;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
|
|
|
|
|
|
/** Output latch: bits the Arduino last wrote. 1 = released (input/Hi-Z), 0 = driven LOW. */
|
2026-04-22 02:45:45 +07:00
|
|
|
|
public outputLatch = 0xff;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
|
|
|
|
|
|
/** Optional callback when the Arduino writes to the port (e.g. to update an LCD visual). */
|
|
|
|
|
|
public onWrite: ((value: number) => void) | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
constructor(address = 0x27) {
|
|
|
|
|
|
this.address = address;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
writeByte(value: number): boolean {
|
|
|
|
|
|
this.outputLatch = value;
|
|
|
|
|
|
if (this.onWrite) this.onWrite(value);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
readByte(): number {
|
|
|
|
|
|
// Open-drain: pin reads HIGH only when both outputLatch and portState are HIGH
|
2026-04-22 02:45:45 +07:00
|
|
|
|
return this.portState & this.outputLatch & 0xff;
|
2026-03-11 22:14:18 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stop(): void {
|
|
|
|
|
|
// PCF8574 is stateless (no register pointer) — explicit no-op for interface clarity
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|