Peripheral Mocking

Create stand-ins for the real chips and sensors your firmware talks to, so you can test how your code reads and writes them — without any physical parts. These stand-ins (called mock devices) let you decide exactly what each chip “replies,” so you can rehearse every situation your firmware might meet, on your computer.

Note

Quick glossary for this page: I2C and SPI are two common buses chips use to talk to each other. MOSI/MISO are the SPI data lines (Master-Out/Slave-In and Master-In/Slave-Out). MSB means “most significant byte” — the leading byte of a multi-byte value. A NACK is a “not acknowledged” reply (the chip saying it didn’t accept the message). A JEDEC ID is a chip-identification response some memory chips return when asked.


I2C Mock Devices

ISimI2CDevice Interface

Implement this interface to mock an I2C peripheral:

import type { ISimI2CDevice } from '@typecad/simulator';

const mockBME280: ISimI2CDevice = {
  read(register: number, count: number): number[] {
    // Return fake sensor data based on register
    if (register === 0xFA) return [0x7E, 0x02, 0x40]; // Temperature MSB
    if (register === 0xFD) return [0x6B, 0x00, 0x20]; // Humidity MSB
    if (register === 0xD0) return [0x60];              // Chip ID
    return new Array(count).fill(0);
  },

  write(register: number, data: number[]): void {
    // Handle configuration writes
    if (register === 0xF4) {
      console.log(`Ctrl meas: mode=${data[0]}`);
    }
  },
};
MethodParametersReturnsDescription
readregister: number, count: numbernumber[]Handle a register read, return byte array of length count
writeregister: number, data: number[]voidHandle a register write

Attaching to a Simulated Bus

import { createSimBoard } from '@typecad/simulator';

const board = createSimBoard({ boardType: 'arduino-uno', i2cBusCount: 1 });
const i2c = board.i2c(0);

// Attach mock BME280 at address 0x76
i2c.attachDevice(0x76, mockBME280);

// Detach when done
i2c.detachDevice(0x76);

Using the Bus with a Mock

Firmware code that uses I2C device accessors will automatically hit the mock:

i2c.begin();
i2c.setClock(100000);
const device = i2c.device(0x76);
const chipId = device.readByte(0xD0);  // → 0x60 (from mock)
device.writeByte(0xF4, 0x27);          // → logged by mock
const tempData = device.readBytes(0xFA, 3);  // → [0x7E, 0x02, 0x40]

Operation Logging

Every I2C read and write is logged for test assertions:

const log = i2c.getLog();
for (const entry of log) {
  console.log(`${entry.operation} @ 0x${entry.address.toString(16)} reg=0x${entry.register.toString(16)}`);
  if (entry.data) console.log(`  data: ${entry.data}`);
}

// Clear log between test cases
i2c.clearLog();

I2COperationLog fields: | Field | Type | Description | |-------|------|-------------| | operation | 'read' \| 'write' | Operation type | | address | number | I2C device address | | register | number | Register address | | data | number[] | Data bytes (writes) | | timestamp | number | Monotonic timestamp |

Error Handling

If firmware accesses an address with no mock device, the bus fires an error:

i2c.onError((status, address, operation) => {
  console.log(`I2C error: ${status} on ${address} during ${operation}`);
});

const unknown = i2c.device(0x42);
unknown.readByte(0x00);  // Fires NACK error — no device at 0x42

SPI Mock Devices

ISimSPIDevice Interface

Implement this interface to mock an SPI peripheral:

import type { ISimSPIDevice } from '@typecad/simulator';

const mockFlash: ISimSPIDevice = {
  // Required: handle full-duplex transfers
  transfer(mosiData: number[]): number[] {
    if (mosiData[0] === 0x9F) {
      return [0xEF, 0x40, 0x18, 0x00]; // JEDEC ID response
    }
    if (mosiData[0] === 0x03) {
      // Read command: return fake data
      return [0x00, ...new Array(mosiData.length - 1).fill(0x42)];
    }
    return new Array(mosiData.length).fill(0xFF);
  },

  // Optional: handle register writes
  write(register: number, data: number[]): void {
    console.log(`SPI write reg 0x${register.toString(16)}: [${data}]`);
  },

  // Optional: handle register reads
  readRegister(register: number, count: number): number[] {
    return new Array(count).fill(0x42);
  },
};
MethodRequiredDescription
transfer(mosiData)YesFull-duplex: receive MOSI, return MISO
write(register, data)NoFalls back to transfer() if not provided
readRegister(register, count)NoFalls back to transfer() with dummy bytes

Attaching to a Simulated SPI Bus

SPI devices are keyed by chip-select pin. Use a simulated digital pin as the CS so the mock device keys cleanly against its pin number:

import { createSimBoard } from '@typecad/simulator';

const board = createSimBoard({ boardType: 'arduino-uno', spiBusCount: 1 });
const spi = board.spi(0);

// Chip-select on pin 10
const cs = board.digital(10).asOutput();

// Attach mock flash memory on CS pin 10
spi.attachDevice(cs, mockFlash);

// Get a device accessor scoped to the CS pin
const flash = spi.device(cs);
const jedecId = flash.transfer([0x9F, 0x00, 0x00, 0x00]);
// → [0xEF, 0x40, 0x18, 0x00]

SPI Device Accessor Methods

MethodDescription
transfer(data)Full-duplex transfer through mock device
write(data)Write-only; uses mock’s write() or falls back to transfer()
read(count)Read N bytes by sending dummy zeros
writeRegister(reg, data)Write to a register address
readRegister(reg, count)Read from a register address

Operation Logging

const log = spi.getLog();
for (const entry of log) {
  console.log(`${entry.operation} reg=${entry.register ?? '-'} data=${entry.data}`);
}

spi.clearLog();

SPIOperationLog fields: | Field | Type | Description | |-------|------|-------------| | operation | 'read' \| 'write' \| 'transfer' | Operation type | | register | number | Register address (if applicable) | | count | number | Byte count requested | | data | number[] | MOSI data sent | | response | number[] | MISO data returned | | timestamp | number | Monotonic timestamp |


Testing Patterns

Verify Initialization Sequence

let initialized = false;
const mockSensor: ISimI2CDevice = {
  read() { return []; },
  write(reg, data) {
    // BME280 reset command
    if (reg === 0xE0 && data[0] === 0xB6) initialized = true;
  },
};

// After running firmware init code
describe('Sensor init')
  .it('sends reset command')
    .expect(initialized ? 1 : 0).toBeTruthy();
done();

Verify Register Read Sequence

const mockSensor: ISimI2CDevice = {
  read(reg) {
    if (reg === 0xD0) return [0x60];  // Chip ID
    return [0];
  },
  write() {},
};

// Run firmware that reads chip ID
// ...

const log = i2c.getLog();
describe('Chip ID read')
  .it('reads register 0xD0')
    .expect(log.length).toBeGreaterThanOrEqual(1)
  .it('reads correct chip ID')
    .expect(log[0].register).toBe(0xD0);
done();

Simulate Sensor Fault

const faultySensor: ISimI2CDevice = {
  read() { return []; },  // Empty read = bus error / no device
  write() {},
};

Verify SPI JEDEC ID

describe('Flash JEDEC ID')
  .it('reads correct manufacturer')
    .expect(jedecId[0]).toBe(0xEF)
  .it('reads correct memory type')
    .expect(jedecId[1]).toBe(0x40);
done();