@typecad/simulator 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -0
  3. package/dist/board/board-sim.d.ts +52 -0
  4. package/dist/board/board-sim.js +188 -0
  5. package/dist/board/from-definition.d.ts +43 -0
  6. package/dist/board/from-definition.js +82 -0
  7. package/dist/bus/i2c-sim.d.ts +61 -0
  8. package/dist/bus/i2c-sim.js +155 -0
  9. package/dist/bus/serial-sim.d.ts +71 -0
  10. package/dist/bus/serial-sim.js +241 -0
  11. package/dist/bus/spi-sim.d.ts +65 -0
  12. package/dist/bus/spi-sim.js +202 -0
  13. package/dist/contracts.d.ts +223 -0
  14. package/dist/contracts.js +87 -0
  15. package/dist/gpio/analog-pin-sim.d.ts +40 -0
  16. package/dist/gpio/analog-pin-sim.js +75 -0
  17. package/dist/gpio/digital-pin-sim.d.ts +68 -0
  18. package/dist/gpio/digital-pin-sim.js +172 -0
  19. package/dist/gpio/interrupt-sim.d.ts +69 -0
  20. package/dist/gpio/interrupt-sim.js +118 -0
  21. package/dist/gpio/pwm-pin-sim.d.ts +41 -0
  22. package/dist/gpio/pwm-pin-sim.js +85 -0
  23. package/dist/helpers.d.ts +40 -0
  24. package/dist/helpers.js +57 -0
  25. package/dist/index.d.ts +18 -0
  26. package/dist/index.js +21 -0
  27. package/dist/types.d.ts +92 -0
  28. package/dist/types.js +4 -0
  29. package/package.json +62 -0
  30. package/src/board/board-sim.ts +208 -0
  31. package/src/board/from-definition.ts +91 -0
  32. package/src/bus/i2c-sim.ts +212 -0
  33. package/src/bus/serial-sim.ts +280 -0
  34. package/src/bus/spi-sim.ts +269 -0
  35. package/src/contracts.ts +330 -0
  36. package/src/gpio/analog-pin-sim.ts +91 -0
  37. package/src/gpio/digital-pin-sim.ts +234 -0
  38. package/src/gpio/interrupt-sim.ts +151 -0
  39. package/src/gpio/pwm-pin-sim.ts +103 -0
  40. package/src/helpers.ts +100 -0
  41. package/src/index.ts +59 -0
  42. package/src/types.ts +104 -0
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Callback signature for pin state change events.
3
+ * @param pinNumber - The pin number that changed
4
+ * @param newValue - The new digital value (HIGH=1 or LOW=0)
5
+ */
6
+ export type PinChangeCallback = (pinNumber: number, newValue: number) => void;
7
+ /**
8
+ * Callback signature for interrupt events.
9
+ * @param pinNumber - The interrupt pin number
10
+ * @param mode - The trigger mode ('rising', 'falling', or 'change')
11
+ */
12
+ export type InterruptCallback = (pinNumber: number, mode: 'rising' | 'falling' | 'change') => void;
13
+ /**
14
+ * A simulated I2C device that responds to register reads and writes.
15
+ * Tests register mock devices to simulate sensor behavior.
16
+ */
17
+ export interface ISimI2CDevice {
18
+ /**
19
+ * Handle a register read from the bus master.
20
+ * @param register - The register address being read
21
+ * @param count - Number of bytes requested
22
+ * @returns Array of byte values to return
23
+ */
24
+ read(register: number, count: number): number[];
25
+ /**
26
+ * Handle a register write from the bus master.
27
+ * @param register - The register address being written
28
+ * @param data - The data bytes being written
29
+ */
30
+ write(register: number, data: number[]): void;
31
+ }
32
+ /**
33
+ * A simulated SPI device that responds to transfers.
34
+ */
35
+ export interface ISimSPIDevice {
36
+ /**
37
+ * Handle a full-duplex SPI transfer.
38
+ * @param mosiData - Data sent from master (MOSI)
39
+ * @returns Data received from device (MISO)
40
+ */
41
+ transfer(mosiData: number[]): number[];
42
+ /**
43
+ * Handle a register write (optional).
44
+ * If not provided, falls back to transfer().
45
+ * @param register - The register address
46
+ * @param data - The data bytes being written
47
+ */
48
+ write?(register: number, data: number[]): void;
49
+ /**
50
+ * Handle a register read (optional).
51
+ * If not provided, falls back to transfer() with dummy bytes.
52
+ * @param register - The register address
53
+ * @param count - Number of bytes to read
54
+ * @returns Data bytes read from the register
55
+ */
56
+ readRegister?(register: number, count: number): number[];
57
+ }
58
+ /**
59
+ * Board type identifiers for the simulator factory.
60
+ */
61
+ export type SimBoardType = 'arduino-uno' | 'arduino-nano' | string;
62
+ /**
63
+ * Configuration for creating a simulated board.
64
+ */
65
+ export interface SimBoardConfig {
66
+ /** Board type identifier. Determines pin count and peripheral availability. */
67
+ boardType: SimBoardType;
68
+ /** Number of digital pins (default: 14 for Uno) */
69
+ digitalPinCount?: number;
70
+ /** Number of analog input pins (default: 6 for Uno) */
71
+ analogPinCount?: number;
72
+ /** Number of I2C buses (default: 1) */
73
+ i2cBusCount?: number;
74
+ /** Number of SPI buses (default: 1) */
75
+ spiBusCount?: number;
76
+ /** Number of UART ports (default: 1) */
77
+ uartCount?: number;
78
+ /**
79
+ * PWM-capable pin numbers. Overrides the board type's default PWM pin map.
80
+ * For unknown/custom board types the default is empty (no PWM pins).
81
+ */
82
+ pwmPins?: number[];
83
+ /**
84
+ * Interrupt-capable pin numbers. Overrides the board type's default interrupt
85
+ * pin map. For unknown/custom board types the default is empty.
86
+ */
87
+ interruptPins?: number[];
88
+ /** RX buffer size for UART simulation (default: 256) */
89
+ uartRxBufferSize?: number;
90
+ /** TX buffer size for UART simulation (default: 256) */
91
+ uartTxBufferSize?: number;
92
+ }
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ // ---------------------------------------------------------------------------
2
+ // @typecad/simulator — Simulator-specific types
3
+ // ---------------------------------------------------------------------------
4
+ export {};
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@typecad/simulator",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "Hardware simulation runtime for TypeCAD — test HAL code in Node.js without hardware",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "src"
11
+ ],
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "dependencies": {
20
+ "@typecad/hal": "0.1.0-alpha.1"
21
+ },
22
+ "devDependencies": {
23
+ "typescript": "^5.7.3",
24
+ "@types/node": "^22.10.7",
25
+ "@typecad/cuttlefish": "0.1.0-alpha.1"
26
+ },
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/justind000/typecode.git",
31
+ "directory": "packages/simulator"
32
+ },
33
+ "homepage": "https://github.com/justind000/typecode/tree/main/packages/simulator",
34
+ "bugs": {
35
+ "url": "https://github.com/justind000/typecode/issues"
36
+ },
37
+ "keywords": [
38
+ "arduino",
39
+ "cpp",
40
+ "cuttlefish",
41
+ "desktop",
42
+ "embedded",
43
+ "emulator",
44
+ "firmware",
45
+ "microcontroller",
46
+ "simulator",
47
+ "typecad",
48
+ "typescript"
49
+ ],
50
+ "engines": {
51
+ "node": ">=18"
52
+ },
53
+ "author": "typecad0",
54
+ "sideEffects": false,
55
+ "exports": {
56
+ ".": {
57
+ "types": "./dist/index.d.ts",
58
+ "default": "./dist/index.js"
59
+ },
60
+ "./package.json": "./package.json"
61
+ }
62
+ }
@@ -0,0 +1,208 @@
1
+ // ---------------------------------------------------------------------------
2
+ // @typecad/simulator — Simulated board factory
3
+ // ---------------------------------------------------------------------------
4
+
5
+ import type { SimBoardConfig } from '../types.js';
6
+ import { SimDigitalPin } from '../gpio/digital-pin-sim.js';
7
+ import { SimAnalogPin } from '../gpio/analog-pin-sim.js';
8
+ import { SimPWMPin } from '../gpio/pwm-pin-sim.js';
9
+ import { SimInterruptPin } from '../gpio/interrupt-sim.js';
10
+ import { SimSerialPort } from '../bus/serial-sim.js';
11
+ import { SimI2CBus } from '../bus/i2c-sim.js';
12
+ import { SimSPIBus } from '../bus/spi-sim.js';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // SimBoard
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /**
19
+ * A fully simulated board with GPIO pins and bus peripherals.
20
+ *
21
+ * Tests create a `SimBoard` via `createSimBoard()`, then access pins
22
+ * and buses to inject data and verify behavior.
23
+ */
24
+ export class SimBoard {
25
+ readonly digitalPins: ReadonlyMap<number, SimDigitalPin>;
26
+ readonly analogPins: ReadonlyMap<number, SimAnalogPin>;
27
+ readonly pwmPins: ReadonlyMap<number, SimPWMPin>;
28
+ readonly interruptPins: ReadonlyMap<number, SimInterruptPin>;
29
+ readonly serialPorts: ReadonlyMap<number, SimSerialPort>;
30
+ readonly i2cBuses: ReadonlyMap<number, SimI2CBus>;
31
+ readonly spiBuses: ReadonlyMap<number, SimSPIBus>;
32
+
33
+ constructor(config: SimBoardConfig) {
34
+ const digitalCount = config.digitalPinCount ?? 14;
35
+ const analogCount = config.analogPinCount ?? 6;
36
+ const uartCount = config.uartCount ?? 1;
37
+ const i2cCount = config.i2cBusCount ?? 1;
38
+ const spiCount = config.spiBusCount ?? 1;
39
+
40
+ // Digital pins
41
+ const digitalMap = new Map<number, SimDigitalPin>();
42
+ for (let i = 0; i < digitalCount; i++) {
43
+ digitalMap.set(i, new SimDigitalPin(i));
44
+ }
45
+ this.digitalPins = digitalMap;
46
+
47
+ // Analog pins (A0, A1, ...)
48
+ const analogMap = new Map<number, SimAnalogPin>();
49
+ for (let i = 0; i < analogCount; i++) {
50
+ analogMap.set(i, new SimAnalogPin(i));
51
+ }
52
+ this.analogPins = analogMap;
53
+
54
+ // PWM pins (e.g. 3, 5, 6, 9, 10, 11 on Uno). Custom/unknown board types
55
+ // default to no PWM pins unless explicitly configured via `pwmPins`.
56
+ const pwmMap = new Map<number, SimPWMPin>();
57
+ const pwmPinNumbers = config.pwmPins ?? getPwmPinsForBoard(config.boardType);
58
+ for (const pinNum of pwmPinNumbers) {
59
+ if (pinNum < digitalCount) {
60
+ pwmMap.set(pinNum, new SimPWMPin(pinNum));
61
+ }
62
+ }
63
+ this.pwmPins = pwmMap;
64
+
65
+ // Interrupt pins (e.g. 2, 3 on Uno). Custom/unknown board types default
66
+ // to no interrupt pins unless explicitly configured via `interruptPins`.
67
+ const interruptMap = new Map<number, SimInterruptPin>();
68
+ const intPinNumbers = config.interruptPins ?? getInterruptPinsForBoard(config.boardType);
69
+ for (const pinNum of intPinNumbers) {
70
+ if (pinNum < digitalCount) {
71
+ interruptMap.set(pinNum, new SimInterruptPin(pinNum));
72
+ }
73
+ }
74
+ this.interruptPins = interruptMap;
75
+
76
+ // Serial ports
77
+ const serialMap = new Map<number, SimSerialPort>();
78
+ for (let i = 0; i < uartCount; i++) {
79
+ serialMap.set(i, new SimSerialPort(i, config.uartRxBufferSize, config.uartTxBufferSize));
80
+ }
81
+ this.serialPorts = serialMap;
82
+
83
+ // I2C buses
84
+ const i2cMap = new Map<number, SimI2CBus>();
85
+ for (let i = 0; i < i2cCount; i++) {
86
+ i2cMap.set(i, new SimI2CBus(i));
87
+ }
88
+ this.i2cBuses = i2cMap;
89
+
90
+ // SPI buses
91
+ const spiMap = new Map<number, SimSPIBus>();
92
+ for (let i = 0; i < spiCount; i++) {
93
+ spiMap.set(i, new SimSPIBus());
94
+ }
95
+ this.spiBuses = spiMap;
96
+ }
97
+
98
+ // --- Convenience accessors ---
99
+
100
+ /** Get digital pin by number. Throws if out of range. */
101
+ digital(pin: number): SimDigitalPin {
102
+ const p = this.digitalPins.get(pin);
103
+ if (!p) throw new Error(`Digital pin ${pin} not available on this board`);
104
+ return p;
105
+ }
106
+
107
+ /** Get analog pin by number (A0=0, A1=1, ...). Throws if out of range. */
108
+ analog(pin: number): SimAnalogPin {
109
+ const p = this.analogPins.get(pin);
110
+ if (!p) throw new Error(`Analog pin A${pin} not available on this board`);
111
+ return p;
112
+ }
113
+
114
+ /** Get PWM pin by number. Throws if not a PWM pin. */
115
+ pwm(pin: number): SimPWMPin {
116
+ const p = this.pwmPins.get(pin);
117
+ if (!p) throw new Error(`Pin ${pin} is not a PWM pin on this board`);
118
+ return p;
119
+ }
120
+
121
+ /** Get interrupt pin by number. Throws if not an interrupt pin. */
122
+ interrupt(pin: number): SimInterruptPin {
123
+ const p = this.interruptPins.get(pin);
124
+ if (!p) throw new Error(`Pin ${pin} is not an interrupt pin on this board`);
125
+ return p;
126
+ }
127
+
128
+ /** Get serial port by number (0 = UART0). */
129
+ serial(port: number = 0): SimSerialPort {
130
+ const s = this.serialPorts.get(port);
131
+ if (!s) throw new Error(`Serial port UART${port} not available on this board`);
132
+ return s;
133
+ }
134
+
135
+ /** Get I2C bus by number (0 = I2C0). */
136
+ i2c(bus: number = 0): SimI2CBus {
137
+ const b = this.i2cBuses.get(bus);
138
+ if (!b) throw new Error(`I2C bus ${bus} not available on this board`);
139
+ return b;
140
+ }
141
+
142
+ /** Get SPI bus by number (0 = SPI0). */
143
+ spi(bus: number = 0): SimSPIBus {
144
+ const b = this.spiBuses.get(bus);
145
+ if (!b) throw new Error(`SPI bus ${bus} not available on this board`);
146
+ return b;
147
+ }
148
+
149
+ // --- Reset ---
150
+
151
+ /** Reset all peripherals to initial state. */
152
+ reset(): void {
153
+ for (const pin of this.digitalPins.values()) pin.reset();
154
+ for (const pin of this.analogPins.values()) pin.reset();
155
+ for (const pin of this.pwmPins.values()) pin.reset();
156
+ for (const pin of this.interruptPins.values()) pin.reset();
157
+ for (const port of this.serialPorts.values()) port.reset();
158
+ for (const bus of this.i2cBuses.values()) bus.reset();
159
+ for (const bus of this.spiBuses.values()) bus.reset();
160
+ }
161
+ }
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Factory
165
+ // ---------------------------------------------------------------------------
166
+
167
+ /**
168
+ * Create a simulated board with the given configuration.
169
+ *
170
+ * @example
171
+ * ```ts
172
+ * const board = createSimBoard({ boardType: 'arduino-uno' });
173
+ * board.digital(13).output();
174
+ * board.digital(13).high();
175
+ * expect(board.digital(13).getBitValue()).toBe(1);
176
+ * ```
177
+ */
178
+ export function createSimBoard(config: SimBoardConfig): SimBoard {
179
+ return new SimBoard(config);
180
+ }
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Board-specific pin maps
184
+ //
185
+ // Only known board types carry a default capability pin map. Custom and
186
+ // unrecognized board types return an empty set — use `pwmPins` /
187
+ // `interruptPins` on SimBoardConfig to declare capability pins for them.
188
+ // ---------------------------------------------------------------------------
189
+
190
+ function getPwmPinsForBoard(boardType: string): number[] {
191
+ switch (boardType) {
192
+ case 'arduino-uno':
193
+ case 'arduino-nano':
194
+ return [3, 5, 6, 9, 10, 11];
195
+ default:
196
+ return [];
197
+ }
198
+ }
199
+
200
+ function getInterruptPinsForBoard(boardType: string): number[] {
201
+ switch (boardType) {
202
+ case 'arduino-uno':
203
+ case 'arduino-nano':
204
+ return [2, 3];
205
+ default:
206
+ return [];
207
+ }
208
+ }
@@ -0,0 +1,91 @@
1
+ // ---------------------------------------------------------------------------
2
+ // @typecad/simulator — Derive a SimBoard from a BoardDefinition
3
+ // ---------------------------------------------------------------------------
4
+
5
+ import type { BoardDefinition } from '@typecad/cuttlefish/api/schema';
6
+ import { createSimBoard } from './board-sim.js';
7
+ import type { SimBoard } from './board-sim.js';
8
+
9
+ /**
10
+ * Build a simulated board from a board package's `BoardDefinition`.
11
+ *
12
+ * Every `@typecad/board-*` package exports a `BoardDefinition` whose `pins.all`
13
+ * entries carry real framework pin numbers and per-pin capability flags, and
14
+ * whose `peripherals` describe the ADC resolution/reference and bus counts.
15
+ * This helper reads that authoritative data and produces a `SimBoard` whose
16
+ * pin layout, PWM/interrupt pins, analog ADC config, and bus counts match the
17
+ * real board — without hardcoding them.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { ArduinoUno } from '@typecad/board-arduino-uno';
22
+ * import { createBoardFromDefinition } from '@typecad/simulator';
23
+ *
24
+ * const board = createBoardFromDefinition(ArduinoUno);
25
+ * board.digital(13).asOutput().high(); // LED on PB5
26
+ * board.analog(0).injectVoltage(2.5); // A0, 10-bit / 5V reference
27
+ * ```
28
+ *
29
+ * The board package must be built (its `dist/`) so the `BoardDefinition` is
30
+ * importable at runtime.
31
+ *
32
+ * **Derived from the `BoardDefinition`:**
33
+ *
34
+ * | SimBoard field | BoardDefinition source |
35
+ * |----------------|------------------------|
36
+ * | `digitalPinCount` | `pins.digital.length` |
37
+ * | `analogPinCount` | `pins.analog.length` |
38
+ * | `pwmPins` | `pins.all` filtered by `capabilities.pwm` |
39
+ * | `interruptPins` | `pins.all` filtered by `capabilities.interrupt` (excluding `unsafe`) |
40
+ * | `i2cBusCount` | `peripherals.i2c.length` |
41
+ * | `spiBusCount` | `peripherals.spi.length` |
42
+ * | `uartCount` | `peripherals.uart.length` |
43
+ * | ADC resolution | `peripherals.adc[0].resolution` |
44
+ * | ADC reference | `peripherals.adc[0].referenceVoltage` |
45
+ *
46
+ * @param def - The board package's `BoardDefinition` (e.g. `ArduinoUno`).
47
+ * @returns A `SimBoard` configured to match the board.
48
+ */
49
+ export function createBoardFromDefinition(def: BoardDefinition): SimBoard {
50
+ const all = def.pins.all;
51
+
52
+ const pwmPins = all
53
+ .filter(p => p.capabilities?.pwm)
54
+ .map(p => p.number);
55
+
56
+ // Interrupt-capable pins, excluding any marked unsafe (e.g. boot-strap pins).
57
+ // The `interrupt` capability flag is set per-pin in the board definition's
58
+ // `pins.all`; for ATmega328P only PD2 (INT0) and PD3 (INT1) carry it, so
59
+ // this yields [2, 3] on Uno. Override with `createSimBoard({ interruptPins })`
60
+ // if your board's flagging differs from the interrupts you want to simulate.
61
+ const interruptPins = all
62
+ .filter(p => p.capabilities?.interrupt && !p.unsafe)
63
+ .map(p => p.number);
64
+
65
+ const board = createSimBoard({
66
+ boardType: def.id,
67
+ digitalPinCount: def.pins.digital.length,
68
+ analogPinCount: def.pins.analog.length,
69
+ i2cBusCount: def.peripherals.i2c?.length ?? 1,
70
+ spiBusCount: def.peripherals.spi?.length ?? 1,
71
+ uartCount: def.peripherals.uart?.length ?? 1,
72
+ pwmPins,
73
+ interruptPins,
74
+ });
75
+
76
+ // Apply the board's ADC resolution and reference voltage to every analog
77
+ // pin so readVoltage() produces board-accurate values out of the box.
78
+ const adc = def.peripherals.adc?.[0];
79
+ if (adc) {
80
+ for (const pin of board.analogPins.values()) {
81
+ if (typeof adc.resolution === 'number') {
82
+ pin.setResolution(adc.resolution);
83
+ }
84
+ if (typeof adc.referenceVoltage === 'number') {
85
+ pin.setAnalogReference(adc.referenceVoltage);
86
+ }
87
+ }
88
+ }
89
+
90
+ return board;
91
+ }
@@ -0,0 +1,212 @@
1
+ // ---------------------------------------------------------------------------
2
+ // @typecad/simulator — I2C bus simulation
3
+ // ---------------------------------------------------------------------------
4
+
5
+ import { I2CStatus } from '../contracts.js';
6
+ import type {
7
+ II2CBus,
8
+ II2CDeviceAccessor,
9
+ ErrorPolicy,
10
+ } from '../contracts.js';
11
+ import type { I2CAddress } from '@typecad/hal';
12
+ import type { ISimI2CDevice } from '../types.js';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // SimI2CBus
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /**
19
+ * Simulated I2C bus for testing I2C communication.
20
+ *
21
+ * Tests register mock devices via `attachDevice()`. When sketch code reads
22
+ * or writes to an address, the corresponding mock device handles the operation.
23
+ */
24
+ export class SimI2CBus implements II2CBus {
25
+ readonly busNumber: number;
26
+ isEnabled: boolean = false;
27
+ errorPolicy: ErrorPolicy = 'callback';
28
+
29
+ private _devices: Map<I2CAddress, ISimI2CDevice> = new Map();
30
+ private _speed: number = 100000;
31
+ private _slaveAddress: I2CAddress | null = null;
32
+ private _errorHandler: ((status: I2CStatus, address: I2CAddress, operation: 'read' | 'write') => void) | null = null;
33
+
34
+ /** Track all operations for test assertions. */
35
+ private _log: I2COperationLog[] = [];
36
+
37
+ constructor(busNumber: number = 0) {
38
+ this.busNumber = busNumber;
39
+ }
40
+
41
+ // --- II2CBus methods ---
42
+
43
+ begin(address?: I2CAddress): void {
44
+ if (address !== undefined) {
45
+ this._slaveAddress = address;
46
+ }
47
+ this.isEnabled = true;
48
+ }
49
+
50
+ end(): void {
51
+ // In simulation, ending the bus disables it without releasing mock devices.
52
+ this.isEnabled = false;
53
+ }
54
+
55
+ setClock(hz: number): void {
56
+ this._speed = hz;
57
+ }
58
+
59
+ device(address: I2CAddress): II2CDeviceAccessor {
60
+ return createDeviceAccessor(this, address);
61
+ }
62
+
63
+ onError(handler: (status: I2CStatus, address: I2CAddress, operation: 'read' | 'write') => void): void {
64
+ this._errorHandler = handler;
65
+ }
66
+
67
+ recover(): boolean {
68
+ return true;
69
+ }
70
+
71
+ // --- Simulation helpers ---
72
+
73
+ /**
74
+ * Attach a mock I2C device at the given address.
75
+ */
76
+ attachDevice(address: I2CAddress, device: ISimI2CDevice): void {
77
+ this._devices.set(address, device);
78
+ }
79
+
80
+ /**
81
+ * Remove a mock device.
82
+ */
83
+ detachDevice(address: I2CAddress): void {
84
+ this._devices.delete(address);
85
+ }
86
+
87
+ /**
88
+ * Get the operation log for test assertions.
89
+ */
90
+ getLog(): readonly I2COperationLog[] {
91
+ return this._log;
92
+ }
93
+
94
+ /**
95
+ * Clear the operation log.
96
+ */
97
+ clearLog(): void {
98
+ this._log = [];
99
+ }
100
+
101
+ /**
102
+ * Reset all state.
103
+ */
104
+ reset(): void {
105
+ this._devices.clear();
106
+ this._log = [];
107
+ this._errorHandler = null;
108
+ this.isEnabled = false;
109
+ this._speed = 100000;
110
+ this._slaveAddress = null;
111
+ }
112
+
113
+ // --- Internal ---
114
+
115
+ /** @internal */
116
+ _getDevice(address: I2CAddress): ISimI2CDevice | undefined {
117
+ return this._devices.get(address);
118
+ }
119
+
120
+ /** @internal */
121
+ _logOperation(op: I2COperationLog): void {
122
+ this._log.push(op);
123
+ }
124
+
125
+ /** @internal */
126
+ _fireError(status: I2CStatus, address: I2CAddress, operation: 'read' | 'write'): void {
127
+ if (this._errorHandler) {
128
+ this._errorHandler(status, address, operation);
129
+ }
130
+ }
131
+ }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Operation log
135
+ // ---------------------------------------------------------------------------
136
+
137
+ export interface I2COperationLog {
138
+ operation: 'read' | 'write';
139
+ address: I2CAddress;
140
+ register: number;
141
+ data?: number[];
142
+ timestamp: number;
143
+ }
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // Device accessor factory
147
+ // ---------------------------------------------------------------------------
148
+
149
+ function createDeviceAccessor(bus: SimI2CBus, address: I2CAddress): II2CDeviceAccessor {
150
+ return {
151
+ address,
152
+
153
+ readByte(register: number): number {
154
+ const device = bus._getDevice(address);
155
+ const timestamp = Date.now();
156
+
157
+ if (!device) {
158
+ bus._logOperation({ operation: 'read', address, register, timestamp });
159
+ bus._fireError(I2CStatus.NACK_ON_ADDRESS, address, 'read');
160
+ return 0;
161
+ }
162
+
163
+ const data = device.read(register, 1);
164
+ bus._logOperation({ operation: 'read', address, register, data, timestamp });
165
+ return data[0] ?? 0;
166
+ },
167
+
168
+ readBytes(register: number, count: number): Uint8Array {
169
+ const device = bus._getDevice(address);
170
+ const timestamp = Date.now();
171
+
172
+ if (!device) {
173
+ bus._logOperation({ operation: 'read', address, register, timestamp });
174
+ bus._fireError(I2CStatus.NACK_ON_ADDRESS, address, 'read');
175
+ return new Uint8Array(0);
176
+ }
177
+
178
+ const data = device.read(register, count);
179
+ bus._logOperation({ operation: 'read', address, register, data, timestamp });
180
+ return new Uint8Array(data);
181
+ },
182
+
183
+ writeByte(register: number, value: number): void {
184
+ const device = bus._getDevice(address);
185
+ const timestamp = Date.now();
186
+
187
+ if (!device) {
188
+ bus._logOperation({ operation: 'write', address, register, data: [value], timestamp });
189
+ bus._fireError(I2CStatus.NACK_ON_ADDRESS, address, 'write');
190
+ return;
191
+ }
192
+
193
+ device.write(register, [value]);
194
+ bus._logOperation({ operation: 'write', address, register, data: [value], timestamp });
195
+ },
196
+
197
+ writeBytes(register: number, data: Uint8Array | number[]): void {
198
+ const device = bus._getDevice(address);
199
+ const dataArray = Array.isArray(data) ? data : Array.from(data);
200
+ const timestamp = Date.now();
201
+
202
+ if (!device) {
203
+ bus._logOperation({ operation: 'write', address, register, data: dataArray, timestamp });
204
+ bus._fireError(I2CStatus.NACK_ON_ADDRESS, address, 'write');
205
+ return;
206
+ }
207
+
208
+ device.write(register, dataArray);
209
+ bus._logOperation({ operation: 'write', address, register, data: dataArray, timestamp });
210
+ },
211
+ };
212
+ }