feat: add VirtualBMP280, VirtualDS3231, VirtualPCF8574 I2C devices with 60 tests

Co-authored-by: davidmonterocrespo24 <47928504+davidmonterocrespo24@users.noreply.github.com>
pull/16/head
copilot-swe-agent[bot] 2026-03-11 15:14:18 +00:00
parent bea986764b
commit 4de45ecb0a
3 changed files with 1073 additions and 1 deletions

View File

@ -0,0 +1,639 @@
/**
* virtual-i2c-devices.test.ts
*
* Unit tests for the virtual I2C sensor library:
* VirtualBMP280 barometric pressure / temperature sensor (0x76 / 0x77)
* VirtualDS3231 real-time clock with on-chip temperature sensor (0x68)
* VirtualPCF8574 8-bit I/O expander (0x200x27 / 0x380x3F)
*
* Also covers the I2CBusManager routing (connectToSlave / writeByte / readByte)
* and the pre-existing VirtualDS1307, VirtualTempSensor, I2CMemoryDevice helpers.
*
* NOTE: these tests import directly from I2CBusManager.ts which only uses
* `import type` from avr8js so they run in the plain Node / Vitest environment
* without needing the wokwi-libs to be built.
*/
import { describe, it, expect } from 'vitest';
import {
I2CBusManager,
I2CMemoryDevice,
VirtualDS1307,
VirtualTempSensor,
VirtualBMP280,
VirtualDS3231,
VirtualPCF8574,
} from '../simulation/I2CBusManager';
// ─── Helpers ──────────────────────────────────────────────────────────────────
/** Minimal mock of AVRTWI used by I2CBusManager */
function makeTWI() {
const calls: string[] = [];
let readResult = 0xFF;
let writeAck = true;
let connectAck = true;
return {
calls,
_setReadResult : (v: number) => { readResult = v; },
_setWriteAck : (v: boolean) => { writeAck = v; },
_setConnectAck : (v: boolean) => { connectAck = v; },
// --- AVRTWI API ---
set eventHandler(_: any) { /* set by I2CBusManager constructor */ },
completeStart () { calls.push('start'); },
completeStop () { calls.push('stop'); },
completeConnect (ack: boolean) { calls.push(`connect:${ack}`); },
completeWrite (ack: boolean) { calls.push(`write:${ack}`); },
completeRead (value: number) { calls.push(`read:${value}`); },
};
}
// ─── I2CBusManager routing ────────────────────────────────────────────────────
describe('I2CBusManager — routing', () => {
it('completes start unconditionally', () => {
const twi = makeTWI();
const bus = new I2CBusManager(twi as any);
bus.start(false);
expect(twi.calls).toContain('start');
});
it('NACKs an address with no registered device', () => {
const twi = makeTWI();
const bus = new I2CBusManager(twi as any);
bus.connectToSlave(0x42, true);
expect(twi.calls).toContain('connect:false');
});
it('ACKs when a device is registered at the address', () => {
const twi = makeTWI();
const bus = new I2CBusManager(twi as any);
bus.addDevice(new I2CMemoryDevice(0x42));
bus.connectToSlave(0x42, true);
expect(twi.calls).toContain('connect:true');
});
it('routes writeByte to active device and returns ACK', () => {
const twi = makeTWI();
const bus = new I2CBusManager(twi as any);
const device = new I2CMemoryDevice(0x50);
bus.addDevice(device);
bus.connectToSlave(0x50, true);
bus.writeByte(0x10); // set register pointer
expect(twi.calls).toContain('write:true');
});
it('routes readByte to active device', () => {
const twi = makeTWI();
const bus = new I2CBusManager(twi as any);
const device = new I2CMemoryDevice(0x50);
device.registers[0x00] = 0xAB;
bus.addDevice(device);
bus.connectToSlave(0x50, true);
bus.writeByte(0x00); // set register pointer to 0
bus.connectToSlave(0x50, false); // repeated start, read mode
bus.readByte(true);
const readCall = twi.calls.find(c => c.startsWith('read:'));
expect(readCall).toBe('read:171'); // 0xAB = 171
});
it('returns 0xFF on read when no device at address', () => {
const twi = makeTWI();
const bus = new I2CBusManager(twi as any);
bus.readByte(true);
expect(twi.calls).toContain('read:255');
});
it('NACKs write when no active device', () => {
const twi = makeTWI();
const bus = new I2CBusManager(twi as any);
bus.writeByte(0x00);
expect(twi.calls).toContain('write:false');
});
it('removeDevice stops routing to that address', () => {
const twi = makeTWI();
const bus = new I2CBusManager(twi as any);
bus.addDevice(new I2CMemoryDevice(0x42));
bus.removeDevice(0x42);
bus.connectToSlave(0x42, true);
expect(twi.calls).toContain('connect:false');
});
it('calls device.stop() on stop condition', () => {
const twi = makeTWI();
const bus = new I2CBusManager(twi as any);
let stopped = false;
const device: any = {
address : 0x42,
writeByte : () => true,
readByte : () => 0,
stop : () => { stopped = true; },
};
bus.addDevice(device);
bus.connectToSlave(0x42, true);
bus.stop();
expect(stopped).toBe(true);
expect(twi.calls).toContain('stop');
});
});
// ─── I2CMemoryDevice ─────────────────────────────────────────────────────────
describe('I2CMemoryDevice', () => {
it('first byte sets register pointer, subsequent bytes write data', () => {
const dev = new I2CMemoryDevice(0x50);
dev.writeByte(0x05); // register pointer
dev.writeByte(0xAB); // write data to register 5
expect(dev.registers[0x05]).toBe(0xAB);
});
it('reads back written data starting at register pointer', () => {
const dev = new I2CMemoryDevice(0x50);
dev.registers[0x02] = 0xCC;
dev.writeByte(0x02); // set pointer
expect(dev.readByte()).toBe(0xCC);
});
it('auto-increments register pointer on read', () => {
const dev = new I2CMemoryDevice(0x50);
dev.registers[0x00] = 0x11;
dev.registers[0x01] = 0x22;
dev.writeByte(0x00);
expect(dev.readByte()).toBe(0x11);
expect(dev.readByte()).toBe(0x22);
});
it('fires onRegisterWrite callback', () => {
const dev = new I2CMemoryDevice(0x50);
const log: [number, number][] = [];
dev.onRegisterWrite = (r, v) => log.push([r, v]);
dev.writeByte(0x0A); // pointer
dev.writeByte(0xFF); // data → register 0x0A
expect(log).toEqual([[0x0A, 0xFF]]);
});
it('resets firstByte on stop()', () => {
const dev = new I2CMemoryDevice(0x50);
dev.writeByte(0x03); // pointer set, firstByte = false
dev.stop(); // reset
dev.writeByte(0x07); // new pointer
dev.writeByte(0x55); // write to register 7
expect(dev.registers[0x07]).toBe(0x55);
});
});
// ─── VirtualDS1307 ────────────────────────────────────────────────────────────
describe('VirtualDS1307', () => {
it('address is 0x68', () => {
expect(new VirtualDS1307().address).toBe(0x68);
});
it('readByte for reg 0 returns valid BCD seconds (059)', () => {
const dev = new VirtualDS1307();
dev.writeByte(0x00); // set pointer to seconds
const raw = dev.readByte();
const tens = (raw >> 4) & 0xF;
const units = raw & 0xF;
expect(tens).toBeLessThanOrEqual(5);
expect(units).toBeLessThanOrEqual(9);
});
it('reads 7 consecutive BCD time registers', () => {
const dev = new VirtualDS1307();
dev.writeByte(0x00); // start at seconds
for (let i = 0; i < 7; i++) {
const byte = dev.readByte();
expect(byte).toBeGreaterThanOrEqual(0x00);
expect(byte).toBeLessThanOrEqual(0x99);
}
});
it('stop() resets firstByte so next write is a new pointer', () => {
const dev = new VirtualDS1307();
dev.writeByte(0x02); // set pointer to hours
dev.stop();
dev.writeByte(0x00); // new pointer (seconds)
const raw = dev.readByte();
// raw should be seconds BCD (tens ≤ 5)
expect((raw >> 4) & 0xF).toBeLessThanOrEqual(5);
});
});
// ─── VirtualTempSensor ────────────────────────────────────────────────────────
describe('VirtualTempSensor', () => {
it('address is 0x48', () => {
expect(new VirtualTempSensor().address).toBe(0x48);
});
it('reads temperature high byte (register 0) correctly', () => {
const dev = new VirtualTempSensor();
dev.temperature = 2350; // 23.50°C
dev.writeByte(0x00);
expect(dev.readByte()).toBe((2350 >> 8) & 0xFF);
});
it('reads humidity bytes from registers 23', () => {
const dev = new VirtualTempSensor();
dev.humidity = 5500; // 55.00%
dev.writeByte(0x02);
expect(dev.readByte()).toBe((5500 >> 8) & 0xFF);
expect(dev.readByte()).toBe(5500 & 0xFF);
});
});
// ─── VirtualBMP280 ────────────────────────────────────────────────────────────
describe('VirtualBMP280 — construction & addresses', () => {
it('default address is 0x76', () => {
expect(new VirtualBMP280().address).toBe(0x76);
});
it('accepts 0x77 as alternate address', () => {
expect(new VirtualBMP280(0x77).address).toBe(0x77);
});
it('chip_id register (0xD0) reads 0x60', () => {
const dev = new VirtualBMP280();
dev.writeByte(0xD0);
expect(dev.readByte()).toBe(0x60);
});
it('stop() resets firstByte so register pointer can be re-set', () => {
const dev = new VirtualBMP280();
dev.writeByte(0xD0);
dev.stop();
dev.writeByte(0xF3); // new pointer → status register
expect(dev.readByte()).toBe(0x00); // status = 0 (ready)
});
});
describe('VirtualBMP280 — calibration registers', () => {
/**
* Reads an unsigned 16-bit little-endian value from two consecutive register
* bytes starting at `regAddr`.
*/
function readU16LE(dev: VirtualBMP280, regAddr: number): number {
dev.writeByte(regAddr);
const lo = dev.readByte();
const hi = dev.readByte();
dev.stop();
return lo | (hi << 8);
}
function readS16LE(dev: VirtualBMP280, regAddr: number): number {
const u = readU16LE(dev, regAddr);
return u > 0x7FFF ? u - 0x10000 : u;
}
it('dig_T1 (0x88) equals 27504', () => {
const dev = new VirtualBMP280();
expect(readU16LE(dev, 0x88)).toBe(27504);
});
it('dig_T2 (0x8A) equals 26435', () => {
expect(readS16LE(new VirtualBMP280(), 0x8A)).toBe(26435);
});
it('dig_T3 (0x8C) equals -1000', () => {
expect(readS16LE(new VirtualBMP280(), 0x8C)).toBe(-1000);
});
it('dig_P1 (0x8E) equals 36477', () => {
expect(readU16LE(new VirtualBMP280(), 0x8E)).toBe(36477);
});
it('dig_P2 (0x90) equals -10685', () => {
expect(readS16LE(new VirtualBMP280(), 0x90)).toBe(-10685);
});
});
describe('VirtualBMP280 — temperature compensation', () => {
/**
* Reads the 6-byte pressure+temperature burst (0xF70xFC) and reconstructs
* the two 20-bit raw ADC values. Returns { adcP, adcT }.
*/
function readRawAdc(dev: VirtualBMP280): { adcP: number; adcT: number } {
dev.writeByte(0xF7);
const pMsb = dev.readByte();
const pLsb = dev.readByte();
const pXlsb = dev.readByte();
const tMsb = dev.readByte();
const tLsb = dev.readByte();
const tXlsb = dev.readByte();
dev.stop();
const adcP = (pMsb << 12) | (pLsb << 4) | (pXlsb >> 4);
const adcT = (tMsb << 12) | (tLsb << 4) | (tXlsb >> 4);
return { adcP, adcT };
}
/**
* BMP280 Bosch 32-bit integer temperature compensation formula.
* Returns temperature in 0.01°C.
*/
function compensateT(adcT: number, digT1 = 27504, digT2 = 26435, digT3 = -1000): number {
const var1 = (((adcT >> 3) - (digT1 << 1)) * digT2) >> 11;
const sub = (adcT >> 4) - digT1;
const var2 = ((sub * sub >> 12) * digT3) >> 14;
const tFine = var1 + var2;
return (tFine * 5 + 128) >> 8;
}
/**
* BMP280 floating-point pressure compensation formula.
* Returns pressure in Pa.
*/
function compensateP(
adcP: number, adcT: number,
digT1 = 27504, digT2 = 26435, digT3 = -1000,
digP1 = 36477, digP2 = -10685, digP3 = 3024,
digP4 = 2855, digP5 = 140, digP6 = -7,
digP7 = 15500, digP8 = -14600, digP9 = 6000,
): number {
const var1 = (((adcT >> 3) - (digT1 << 1)) * digT2) >> 11;
const sub = (adcT >> 4) - digT1;
const var2 = ((sub * sub >> 12) * digT3) >> 14;
const tf = var1 + var2;
let v1 = tf / 2.0 - 64000.0;
let v2 = v1 * v1 * digP6 / 32768.0;
v2 = v2 + v1 * digP5 * 2.0;
v2 = v2 / 4.0 + digP4 * 65536.0;
v1 = (digP3 * v1 * v1 / 524288.0 + digP2 * v1) / 524288.0;
v1 = (1.0 + v1 / 32768.0) * digP1;
if (v1 === 0) return 0;
let p = 1048576.0 - adcP;
p = (p - v2 / 4096.0) * 6250.0 / v1;
return p + (digP9 * p * p / 2147483648.0 + p * digP8 / 32768.0 + digP7) / 16.0;
}
it('default 25°C produces compensated temperature within ±0.5°C', () => {
const dev = new VirtualBMP280();
const { adcT } = readRawAdc(dev);
const centideg = compensateT(adcT);
expect(centideg / 100).toBeCloseTo(25, 0);
});
it('setting temperatureC = 20 produces ~20°C compensated output', () => {
const dev = new VirtualBMP280();
dev.temperatureC = 20;
const { adcT } = readRawAdc(dev);
expect(compensateT(adcT) / 100).toBeCloseTo(20, 0);
});
it('setting temperatureC = 0 produces ~0°C compensated output', () => {
const dev = new VirtualBMP280();
dev.temperatureC = 0;
const { adcT } = readRawAdc(dev);
expect(compensateT(adcT) / 100).toBeCloseTo(0, 0);
});
it('setting temperatureC = 85 produces ~85°C compensated output', () => {
const dev = new VirtualBMP280();
dev.temperatureC = 85;
const { adcT } = readRawAdc(dev);
expect(compensateT(adcT) / 100).toBeCloseTo(85, 0);
});
});
describe('VirtualBMP280 — pressure compensation', () => {
function readRawAdc(dev: VirtualBMP280): { adcP: number; adcT: number } {
dev.writeByte(0xF7);
const pMsb = dev.readByte(), pLsb = dev.readByte(), pXlsb = dev.readByte();
const tMsb = dev.readByte(), tLsb = dev.readByte(), tXlsb = dev.readByte();
dev.stop();
return {
adcP: (pMsb << 12) | (pLsb << 4) | (pXlsb >> 4),
adcT: (tMsb << 12) | (tLsb << 4) | (tXlsb >> 4),
};
}
function compensateP(adcP: number, adcT: number): number {
const digT1 = 27504, digT2 = 26435, digT3 = -1000;
const digP1 = 36477, digP2 = -10685, digP3 = 3024;
const digP4 = 2855, digP5 = 140, digP6 = -7;
const digP7 = 15500, digP8 = -14600, digP9 = 6000;
const var1 = (((adcT >> 3) - (digT1 << 1)) * digT2) >> 11;
const sub = (adcT >> 4) - digT1;
const var2 = ((sub * sub >> 12) * digT3) >> 14;
const tf = var1 + var2;
let v1 = tf / 2.0 - 64000.0;
let v2 = v1 * v1 * digP6 / 32768.0;
v2 = v2 + v1 * digP5 * 2.0;
v2 = v2 / 4.0 + digP4 * 65536.0;
v1 = (digP3 * v1 * v1 / 524288.0 + digP2 * v1) / 524288.0;
v1 = (1.0 + v1 / 32768.0) * digP1;
if (v1 === 0) return 0;
let p = 1048576.0 - adcP;
p = (p - v2 / 4096.0) * 6250.0 / v1;
return p + (digP9 * p * p / 2147483648.0 + p * digP8 / 32768.0 + digP7) / 16.0;
}
it('default 1013.25 hPa produces compensated pressure within ±5 hPa', () => {
const dev = new VirtualBMP280();
const { adcP, adcT } = readRawAdc(dev);
const pHPa = compensateP(adcP, adcT) / 100;
expect(Math.abs(pHPa - 1013.25)).toBeLessThan(5);
});
it('setting pressureHPa = 900 produces ~900 hPa compensated output (±5)', () => {
const dev = new VirtualBMP280();
dev.pressureHPa = 900;
const { adcP, adcT } = readRawAdc(dev);
expect(Math.abs(compensateP(adcP, adcT) / 100 - 900)).toBeLessThan(5);
});
it('setting pressureHPa = 1100 produces ~1100 hPa compensated output (±5)', () => {
const dev = new VirtualBMP280();
dev.pressureHPa = 1100;
const { adcP, adcT } = readRawAdc(dev);
expect(Math.abs(compensateP(adcP, adcT) / 100 - 1100)).toBeLessThan(5);
});
});
describe('VirtualBMP280 — ctrl_meas register is writable', () => {
it('can write and read back ctrl_meas (0xF4)', () => {
const dev = new VirtualBMP280();
// Set reg pointer via normal write
dev.writeByte(0xF4); // pointer → 0xF4
dev.writeByte(0x57); // write 0x57 (forced mode + oversampling)
dev.stop();
dev.writeByte(0xF4); // read back
expect(dev.readByte()).toBe(0x57);
});
});
// ─── VirtualDS3231 ────────────────────────────────────────────────────────────
describe('VirtualDS3231 — time registers', () => {
it('address is 0x68', () => {
expect(new VirtualDS3231().address).toBe(0x68);
});
it('register 0x00 returns valid BCD seconds (059)', () => {
const dev = new VirtualDS3231();
dev.writeByte(0x00);
const raw = dev.readByte();
const tens = (raw >> 4) & 0xF;
const units = raw & 0xF;
expect(tens).toBeLessThanOrEqual(5);
expect(units).toBeLessThanOrEqual(9);
});
it('register 0x02 (hours) returns valid BCD 023', () => {
const dev = new VirtualDS3231();
dev.writeByte(0x02);
const raw = dev.readByte();
const tens = (raw >> 4) & 0xF;
const units = raw & 0xF;
const hours = tens * 10 + units;
expect(hours).toBeGreaterThanOrEqual(0);
expect(hours).toBeLessThanOrEqual(23);
});
it('reads 7 consecutive BCD time registers without error', () => {
const dev = new VirtualDS3231();
dev.writeByte(0x00);
for (let i = 0; i < 7; i++) {
expect(() => dev.readByte()).not.toThrow();
}
});
it('register 0x0E (control) reads 0x00', () => {
const dev = new VirtualDS3231();
dev.writeByte(0x0E);
expect(dev.readByte()).toBe(0x00);
});
it('register 0x0F (status) reads 0x00 (OSF cleared)', () => {
const dev = new VirtualDS3231();
dev.writeByte(0x0F);
expect(dev.readByte()).toBe(0x00);
});
});
describe('VirtualDS3231 — temperature registers', () => {
it('register 0x11 returns integer part of temperature (25°C = 0x19)', () => {
const dev = new VirtualDS3231();
dev.temperatureC = 25.0;
dev.writeByte(0x11);
expect(dev.readByte()).toBe(25); // 25 = 0x19
});
it('register 0x12 returns 0x00 for integer temperature (no fractional)', () => {
const dev = new VirtualDS3231();
dev.temperatureC = 25.0;
dev.writeByte(0x12);
expect(dev.readByte()).toBe(0x00);
});
it('register 0x12 returns 0x80 for 0.5°C fractional (bits 7:6 = 0b10)', () => {
const dev = new VirtualDS3231();
dev.temperatureC = 25.5;
dev.writeByte(0x12);
// 0.5°C / 0.25°C = 2 = 0b10 → stored in bits 7:6 = 0x80
expect(dev.readByte()).toBe(0x80);
});
it('handles negative temperature: -5°C MSB = 0xFB (251 as unsigned)', () => {
const dev = new VirtualDS3231();
dev.temperatureC = -5.0;
dev.writeByte(0x11);
// trunc(-5) & 0xFF = 251
expect(dev.readByte()).toBe((-5) & 0xFF);
});
});
describe('VirtualDS3231 — stop/firstByte reset', () => {
it('stop() resets pointer acquisition', () => {
const dev = new VirtualDS3231();
dev.writeByte(0x11); // set pointer
dev.stop();
dev.writeByte(0x00); // new pointer → seconds
const raw = dev.readByte();
expect((raw >> 4) & 0xF).toBeLessThanOrEqual(5); // valid BCD tens digit
});
});
// ─── VirtualPCF8574 ────────────────────────────────────────────────────────────
describe('VirtualPCF8574 — construction', () => {
it('default address is 0x27', () => {
expect(new VirtualPCF8574().address).toBe(0x27);
});
it('accepts custom address', () => {
expect(new VirtualPCF8574(0x20).address).toBe(0x20);
expect(new VirtualPCF8574(0x3F).address).toBe(0x3F);
});
it('default portState and outputLatch are both 0xFF', () => {
const dev = new VirtualPCF8574();
expect(dev.portState).toBe(0xFF);
expect(dev.outputLatch).toBe(0xFF);
});
});
describe('VirtualPCF8574 — write', () => {
it('writeByte updates outputLatch', () => {
const dev = new VirtualPCF8574();
dev.writeByte(0b10101010);
expect(dev.outputLatch).toBe(0b10101010);
});
it('writeByte fires onWrite callback with the written value', () => {
const dev = new VirtualPCF8574();
const log: number[] = [];
dev.onWrite = v => log.push(v);
dev.writeByte(0x42);
expect(log).toEqual([0x42]);
});
it('multiple writes update outputLatch each time', () => {
const dev = new VirtualPCF8574();
dev.writeByte(0xAA);
dev.writeByte(0x55);
expect(dev.outputLatch).toBe(0x55);
});
});
describe('VirtualPCF8574 — read (open-drain model)', () => {
it('readByte returns portState & outputLatch (both 0xFF → 0xFF)', () => {
const dev = new VirtualPCF8574();
expect(dev.readByte()).toBe(0xFF);
});
it('readByte: pin driven LOW by Arduino (outputLatch=0) → reads 0 regardless of portState', () => {
const dev = new VirtualPCF8574();
dev.portState = 0xFF;
dev.writeByte(0x00); // drive all LOW
expect(dev.readByte()).toBe(0x00);
});
it('readByte: external input LOW overrides Hi-Z output (open-drain)', () => {
const dev = new VirtualPCF8574();
dev.portState = 0b00001111; // lower 4 pins pulled low by external device
dev.outputLatch = 0xFF; // Arduino released all pins
expect(dev.readByte()).toBe(0b00001111);
});
it('readByte reflects mix of output-low and external-low', () => {
const dev = new VirtualPCF8574();
dev.outputLatch = 0b11110000; // Arduino drives lower 4 LOW, upper 4 Hi-Z
dev.portState = 0b10101010; // external: alternate HIGH/LOW
// result: upper 4 from portState masked, lower 4 forced LOW by outputLatch
expect(dev.readByte()).toBe(0b10100000);
});
});
describe('VirtualPCF8574 — writeByte returns ACK', () => {
it('always returns true (ACK)', () => {
const dev = new VirtualPCF8574();
expect(dev.writeByte(0x00)).toBe(true);
expect(dev.writeByte(0xFF)).toBe(true);
});
});

View File

@ -213,3 +213,330 @@ export class VirtualTempSensor implements I2CDevice {
this.firstByte = true;
}
}
/**
* Virtual BMP280 barometric pressure / temperature sensor.
*
* Supports I2C addresses 0x76 (SDO=0) or 0x77 (SDO=1).
*
* Register map (subset):
* 0x880x9F Calibration data (trimming parameters)
* 0xD0 chip_id = 0x60
* 0xF3 status = 0x00 (measurement complete, no NVM copy)
* 0xF4 ctrl_meas (mode, osrs_t, osrs_p) writable
* 0xF5 config writable
* 0xF70xF9 press_msb / press_lsb / press_xlsb (20-bit ADC)
* 0xFA0xFC 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);
private regPtr = 0;
private firstByte = true;
// ── BMP280 datasheet Section 8.2 example calibration ───────────────────
private readonly DIG_T1 = 27504;
private readonly DIG_T2 = 26435;
private readonly DIG_T3 = -1000;
private readonly DIG_P1 = 36477;
private readonly DIG_P2 = -10685;
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;
private readonly DIG_P8 = -14600;
private readonly DIG_P9 = 6000;
private _temperatureC = 25.0;
private _pressureHPa = 1013.25;
constructor(address = 0x76) {
this.address = address;
this.initCalibration();
this.updateMeasurements();
}
// ── Public configurable properties ──────────────────────────────────────
get temperatureC(): number { return this._temperatureC; }
set temperatureC(v: number) { this._temperatureC = v; this.updateMeasurements(); }
get pressureHPa(): number { return this._pressureHPa; }
set pressureHPa(v: number) { this._pressureHPa = v; this.updateMeasurements(); }
// ── I2CDevice interface ─────────────────────────────────────────────────
writeByte(value: number): boolean {
if (this.firstByte) {
this.regPtr = value;
this.firstByte = false;
} else {
// Writable registers (ctrl_meas, config) — store them
this.registers[this.regPtr] = value;
this.regPtr = (this.regPtr + 1) & 0xFF;
}
return true;
}
readByte(): number {
const val = this.registers[this.regPtr];
this.regPtr = (this.regPtr + 1) & 0xFF;
return val;
}
stop(): void {
this.firstByte = true;
}
// ── 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;
const sub = (adcT >> 4) - this.DIG_T1;
const var2 = ((sub * sub >> 12) * this.DIG_T3) >> 14;
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 {
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;
if (var1 === 0) return 0;
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;
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 {
let lo = 0, hi = (1 << 20) - 1;
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 {
let lo = 0, hi = (1 << 20) - 1;
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] {
return [(val >> 12) & 0xFF, (val >> 4) & 0xFF, (val & 0xF) << 4];
}
// ── Register initialisation ─────────────────────────────────────────────
private initCalibration(): void {
const r = this.registers;
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);
r[0xD0] = 0x60; // chip_id (BMP280)
r[0xF3] = 0x00; // status (measurement done)
r[0xF4] = 0x00; // ctrl_meas default
r[0xF5] = 0x00; // config default
wu16(0x88, this.DIG_T1);
ws16(0x8A, this.DIG_T2);
ws16(0x8C, this.DIG_T3);
wu16(0x8E, this.DIG_P1);
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);
ws16(0x9A, this.DIG_P7);
ws16(0x9C, this.DIG_P8);
ws16(0x9E, this.DIG_P9);
}
/** Recompute raw ADC registers from current temperature / pressure. */
private updateMeasurements(): void {
const targetT = Math.round(this._temperatureC * 100);
const targetP = this._pressureHPa * 100; // hPa → Pa
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);
this.registers[0xF7] = pMsb;
this.registers[0xF8] = pLsb;
this.registers[0xF9] = pXlsb;
this.registers[0xFA] = tMsb;
this.registers[0xFB] = tLsb;
this.registers[0xFC] = tXlsb;
}
}
/**
* 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, 059)
* 0x01 Minutes (BCD, 059)
* 0x02 Hours (BCD, 023, 24-hour mode)
* 0x03 Day (BCD, 17, 1=Sunday)
* 0x04 Date (BCD, 131)
* 0x05 Month (BCD, 112)
* 0x06 Year (BCD, 099)
* 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;
private regPtr = 0;
private firstByte = true;
private toBCD(n: number): number {
return ((Math.floor(n / 10) & 0xF) << 4) | (n % 10 & 0xF);
}
private readRegister(reg: number): number {
const now = new Date();
switch (reg) {
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
case 0x11: {
// Temperature MSB: signed integer degrees C
const intTemp = Math.trunc(this.temperatureC);
return intTemp & 0xFF;
}
case 0x12: {
// Temperature LSB: fractional in bits 7:6, 0.25°C resolution
const frac = this.temperatureC - Math.trunc(this.temperatureC);
const q = Math.round(frac / 0.25) & 0x03;
return (q << 6) & 0xFF;
}
default: return 0x00;
}
}
writeByte(value: number): boolean {
if (this.firstByte) {
this.regPtr = value;
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.
this.regPtr = (this.regPtr + 1) & 0x1F;
}
return true;
}
readByte(): number {
const val = this.readRegister(this.regPtr);
this.regPtr = (this.regPtr + 1) & 0x1F;
return val;
}
stop(): void {
this.firstByte = true;
}
}
/**
* 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: 0x200x27 (PCF8574) or 0x380x3F (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). */
public portState = 0xFF;
/** Output latch: bits the Arduino last wrote. 1 = released (input/Hi-Z), 0 = driven LOW. */
public outputLatch = 0xFF;
/** 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
return (this.portState & this.outputLatch) & 0xFF;
}
stop(): void {
// PCF8574 is stateless (no register pointer) — explicit no-op for interface clarity
}
}

View File

@ -19,7 +19,7 @@
*/
import { PartSimulationRegistry } from './PartSimulationRegistry';
import { VirtualDS1307 } from '../I2CBusManager';
import { VirtualDS1307, VirtualBMP280, VirtualDS3231, VirtualPCF8574 } from '../I2CBusManager';
import type { I2CDevice } from '../I2CBusManager';
// ─── Helpers ─────────────────────────────────────────────────────────────────
@ -749,3 +749,109 @@ PartSimulationRegistry.register('microsd-card', {
};
},
});
// ─── BMP280 Barometric Pressure / Temperature Sensor ─────────────────────────
/**
* BMP280 I2C barometric pressure + temperature sensor.
*
* Addresses:
* 0x76 (SDO pin pulled LOW, default)
* 0x77 (SDO pin pulled HIGH set element.address = '0x77')
*
* The element may expose `temperature` (°C) and `pressure` (hPa) properties
* that are read on attach and forwarded to the virtual device.
*
* The virtual device uses the BMP280 datasheet calibration example to compute
* raw ADC values for any desired temperature/pressure combination, so Arduino
* sketches using Adafruit_BMP280 or Bosch's reference driver receive correct
* compensated readings.
*/
PartSimulationRegistry.register('bmp280', {
attachEvents: (element, simulator, _getPin) => {
const sim = simulator as any;
if (typeof sim.addI2CDevice !== 'function') return () => {};
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);
sim.addI2CDevice(dev);
return () => removeI2CDevice(sim, dev.address);
},
});
// ─── DS3231 Real-Time Clock ───────────────────────────────────────────────────
/**
* DS3231 I2C RTC with on-chip temperature sensor (address 0x68).
*
* Returns the browser's current system time as BCD in registers 0x000x06,
* identical to DS1307 for the time registers. Additionally exposes:
* 0x0E Control register
* 0x0F Status register (OSF cleared)
* 0x11 Temperature MSB (integer °C, signed)
* 0x12 Temperature LSB (fractional, 0.25°C per bit in bits 7:6)
*
* Ambient temperature defaults to 25°C; override via `element.temperature`.
*/
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);
},
});
// ─── PCF8574 I/O Expander ────────────────────────────────────────────────────
/**
* PCF8574 I2C 8-bit quasi-bidirectional I/O expander.
*
* Default address: 0x27 (all three address pins HIGH typical LCD backpack).
* Override with `element.i2cAddress` (e.g. '0x20', '0x3F').
*
* `element.portState` (0255) sets the external input state visible to the
* Arduino on a read. Defaults to 0xFF (all pins pulled high / floating input).
*
* Writes from the Arduino update `dev.outputLatch` and fire `dev.onWrite`
* which sets `element.value` so wokwi-LCD-I2C or similar elements can render.
*/
PartSimulationRegistry.register('pcf8574', {
attachEvents: (element, simulator, _getPin) => {
const sim = simulator as any;
if (typeof sim.addI2CDevice !== 'function') return () => {};
const el = element as any;
// Parse address from element property (accepts '0x27', '39', or numeric)
let addr = 0x27;
if (el.i2cAddress !== undefined) {
const raw = String(el.i2cAddress).trim();
const parsed = raw.startsWith('0x') || raw.startsWith('0X')
? parseInt(raw, 16)
: parseInt(raw, 10);
if (!isNaN(parsed)) addr = parsed;
}
const dev = new VirtualPCF8574(addr);
// Seed port state from element if present
if (el.portState !== undefined) dev.portState = Number(el.portState) & 0xFF;
// 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);
},
});