@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,151 @@
1
+ // ---------------------------------------------------------------------------
2
+ // @typecad/simulator — Interrupt simulation
3
+ // ---------------------------------------------------------------------------
4
+
5
+ import type { InterruptPin, InterruptOptions } from '../contracts.js';
6
+ import type { InterruptHandler } from '@typecad/hal';
7
+ import { SimDigitalPin } from './digital-pin-sim.js';
8
+
9
+ type InterruptMode = 'rising' | 'falling' | 'change';
10
+
11
+ /**
12
+ * Tracks interrupt firings for test assertions.
13
+ */
14
+ export interface InterruptEvent {
15
+ /** Timestamp (ms since simulation start) */
16
+ timestamp: number;
17
+ /** The trigger mode */
18
+ mode: InterruptMode;
19
+ /** The pin value at time of interrupt */
20
+ pinValue: number;
21
+ }
22
+
23
+ /**
24
+ * Simulated interrupt controller for a single pin.
25
+ *
26
+ * Extends SimDigitalPin with interrupt capabilities.
27
+ * Tests can fire interrupts manually via `fireInterrupt()` and verify
28
+ * that handlers were called with the correct arguments.
29
+ */
30
+ export class SimInterruptPin extends SimDigitalPin implements InterruptPin {
31
+ private _handlers: Map<InterruptMode, InterruptHandler> = new Map();
32
+ private _debounceMs: number = 0;
33
+ private _events: InterruptEvent[] = [];
34
+
35
+ constructor(pinNum: number) {
36
+ super(pinNum, { interrupt: true });
37
+ }
38
+
39
+ // --- InterruptPin capability methods ---
40
+
41
+ onRising(handler: InterruptHandler, options?: InterruptOptions): void {
42
+ this._handlers.set('rising', handler);
43
+ if (options?.debounce) this._debounceMs = options.debounce;
44
+ }
45
+
46
+ onFalling(handler: InterruptHandler, options?: InterruptOptions): void {
47
+ this._handlers.set('falling', handler);
48
+ if (options?.debounce) this._debounceMs = options.debounce;
49
+ }
50
+
51
+ onChange(handler: InterruptHandler, options?: InterruptOptions): void {
52
+ this._handlers.set('change', handler);
53
+ if (options?.debounce) this._debounceMs = options.debounce;
54
+ }
55
+
56
+ offInterrupts(): void {
57
+ this._handlers.clear();
58
+ }
59
+
60
+ /** Remove the rising-edge handler. */
61
+ offRising(): void {
62
+ this._handlers.delete('rising');
63
+ }
64
+
65
+ /** Remove the falling-edge handler. */
66
+ offFalling(): void {
67
+ this._handlers.delete('falling');
68
+ }
69
+
70
+ /** Remove all interrupt handlers. */
71
+ offAll(): void {
72
+ this._handlers.clear();
73
+ }
74
+
75
+ /** Check if any interrupt handlers are registered. */
76
+ hasInterrupt(): boolean {
77
+ return this._handlers.size > 0;
78
+ }
79
+
80
+ // --- Simulation helpers ---
81
+
82
+ /**
83
+ * Fire an interrupt manually from test code.
84
+ * @param mode - The interrupt mode to fire
85
+ * @param pinValue - The current pin value (for event logging)
86
+ */
87
+ fireInterrupt(mode: InterruptMode, pinValue: number = 0): void {
88
+ const handler = this._handlers.get(mode);
89
+ if (handler) {
90
+ handler();
91
+ }
92
+
93
+ // Also fire 'change' handler for any edge
94
+ if (mode !== 'change' && this._handlers.has('change')) {
95
+ this._handlers.get('change')!();
96
+ }
97
+
98
+ this._events.push({
99
+ timestamp: Date.now() - this._startTime,
100
+ mode,
101
+ pinValue,
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Simulate a pin value transition, automatically firing the appropriate interrupts.
107
+ * @param from - Previous pin value (0 or 1)
108
+ * @param to - New pin value (0 or 1)
109
+ */
110
+ simulateTransition(from: number, to: number): void {
111
+ if (from === to) return;
112
+
113
+ if (from === 0 && to === 1) {
114
+ this.fireInterrupt('rising', to);
115
+ } else if (from === 1 && to === 0) {
116
+ this.fireInterrupt('falling', to);
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Get all recorded interrupt events.
122
+ */
123
+ getEvents(): readonly InterruptEvent[] {
124
+ return this._events;
125
+ }
126
+
127
+ /**
128
+ * Clear interrupt event history.
129
+ */
130
+ clearEvents(): void {
131
+ this._events = [];
132
+ }
133
+
134
+ /**
135
+ * Get the configured debounce time.
136
+ */
137
+ getDebounceMs(): number {
138
+ return this._debounceMs;
139
+ }
140
+
141
+ /**
142
+ * Reset to initial state.
143
+ */
144
+ override reset(): void {
145
+ super.reset();
146
+ this._handlers.clear();
147
+ this._debounceMs = 0;
148
+ this._events = [];
149
+ this._startTime = Date.now();
150
+ }
151
+ }
@@ -0,0 +1,103 @@
1
+ // ---------------------------------------------------------------------------
2
+ // @typecad/simulator — Simulated PWM pin
3
+ // ---------------------------------------------------------------------------
4
+
5
+ import type { PWMPin } from '../contracts.js';
6
+ import type { AnalogValue, DigitalValue } from '@typecad/hal';
7
+ import { SimDigitalPin } from './digital-pin-sim.js';
8
+
9
+ /**
10
+ * Simulated PWM pin. Extends digital pin with PWM duty cycle control.
11
+ */
12
+ export class SimPWMPin extends SimDigitalPin implements PWMPin {
13
+ private _pwmPercent: number = 0;
14
+ private _pwmFrequency: number = 490; // Default Arduino PWM frequency
15
+ private _pwmResolution: number = 8; // Default 8-bit
16
+ private _attached: boolean = false;
17
+
18
+ constructor(pinNum: number) {
19
+ super(pinNum);
20
+ }
21
+
22
+ // --- PWMPin ---
23
+
24
+ write(value: DigitalValue | AnalogValue): void {
25
+ if (typeof value === 'number' && value > 1) {
26
+ // Analog write — treat as PWM duty cycle value
27
+ this.pwm((value / ((1 << this._pwmResolution) - 1)) * 100);
28
+ } else {
29
+ super.write(value as DigitalValue);
30
+ }
31
+ }
32
+
33
+ pwm(percent?: number): void {
34
+ // Calling pwm() (no args) activates PWM mode, pwm(x) sets duty cycle
35
+ // 0% duty cycle means PWM is inactive
36
+ this._attached = percent === undefined || percent > 0;
37
+ if (percent !== undefined) {
38
+ this._pwmPercent = Math.max(0, Math.min(100, percent));
39
+ }
40
+ }
41
+
42
+ setFrequency(hz: number): void {
43
+ this._pwmFrequency = hz;
44
+ }
45
+
46
+ /** Current PWM frequency in Hz (contract method name). */
47
+ getPwmFrequency(): number {
48
+ return this._pwmFrequency;
49
+ }
50
+
51
+ /** Current PWM resolution in bits (contract method name). */
52
+ getPwmResolution(): number {
53
+ return this._pwmResolution;
54
+ }
55
+
56
+ /** @deprecated Use getPwmFrequency() to match the HAL contract. */
57
+ getFrequency(): number {
58
+ return this.getPwmFrequency();
59
+ }
60
+
61
+ /** @deprecated Use getPwmResolution() to match the HAL contract. */
62
+ getResolution(): number {
63
+ return this.getPwmResolution();
64
+ }
65
+
66
+ // --- Simulation helpers ---
67
+
68
+ /**
69
+ * Get the current PWM duty cycle as a percentage (0-100).
70
+ */
71
+ getPwmPercent(): number {
72
+ return this._pwmPercent;
73
+ }
74
+
75
+ /**
76
+ * Get the current PWM duty cycle as a raw value (0 to 2^resolution - 1).
77
+ */
78
+ getPwmValue(): number {
79
+ return Math.round((this._pwmPercent / 100) * ((1 << this._pwmResolution) - 1));
80
+ }
81
+
82
+ /**
83
+ * Check if PWM is currently active.
84
+ */
85
+ isPwmActive(): boolean {
86
+ return this._attached;
87
+ }
88
+
89
+ /**
90
+ * Set the PWM resolution in bits.
91
+ */
92
+ setResolution(bits: number): void {
93
+ this._pwmResolution = bits;
94
+ }
95
+
96
+ override reset(): void {
97
+ super.reset();
98
+ this._pwmPercent = 0;
99
+ this._pwmFrequency = 490;
100
+ this._pwmResolution = 8;
101
+ this._attached = false;
102
+ }
103
+ }
package/src/helpers.ts ADDED
@@ -0,0 +1,100 @@
1
+ // ---------------------------------------------------------------------------
2
+ // @typecad/simulator — Result factory helpers
3
+ // ---------------------------------------------------------------------------
4
+
5
+ /**
6
+ * Result of a byte read operation (used internally by bus simulators).
7
+ */
8
+ export interface IByteReadResult {
9
+ ok: boolean;
10
+ status: number;
11
+ bytes: Uint8Array;
12
+ bytesRead: number;
13
+ timedOut: boolean;
14
+ asUint8(): number;
15
+ asInt8(): number;
16
+ asUint16(endian: 'be' | 'le'): number;
17
+ asInt16(endian: 'be' | 'le'): number;
18
+ asUint32(endian: 'be' | 'le'): number;
19
+ asInt32(endian: 'be' | 'le'): number;
20
+ asString(): string;
21
+ asStringTrim(): string;
22
+ asInt(): number;
23
+ asFloat(): number;
24
+ unwrap(): Uint8Array;
25
+ unwrapOr(defaultValue: Uint8Array): Uint8Array;
26
+ }
27
+
28
+ /**
29
+ * Result of a write operation (used internally by bus simulators).
30
+ */
31
+ export interface IWriteResult {
32
+ ok: boolean;
33
+ status: number;
34
+ bytesWritten: number;
35
+ unwrap(): number;
36
+ unwrapOr(defaultValue: number): number;
37
+ }
38
+
39
+ /**
40
+ * Create an IByteReadResult with the given bytes.
41
+ */
42
+ export function createByteReadResult(
43
+ bytes: Uint8Array,
44
+ ok: boolean = true,
45
+ status: number = 0,
46
+ timedOut: boolean = false,
47
+ ): IByteReadResult {
48
+ return {
49
+ ok,
50
+ status,
51
+ bytes,
52
+ bytesRead: bytes.length,
53
+ timedOut,
54
+ asUint8(): number { return bytes[0] ?? 0; },
55
+ asInt8(): number { const v = bytes[0] ?? 0; return v > 127 ? v - 256 : v; },
56
+ asUint16(endian: 'be' | 'le'): number {
57
+ if (bytes.length < 2) return 0;
58
+ return endian === 'be'
59
+ ? ((bytes[0]! << 8) | bytes[1]!) >>> 0
60
+ : ((bytes[1]! << 8) | bytes[0]!) >>> 0;
61
+ },
62
+ asInt16(endian: 'be' | 'le'): number {
63
+ const v = this.asUint16(endian);
64
+ return v > 32767 ? v - 65536 : v;
65
+ },
66
+ asUint32(endian: 'be' | 'le'): number {
67
+ if (bytes.length < 4) return 0;
68
+ return endian === 'be'
69
+ ? ((bytes[0]! << 24) | (bytes[1]! << 16) | (bytes[2]! << 8) | bytes[3]!) >>> 0
70
+ : ((bytes[3]! << 24) | (bytes[2]! << 16) | (bytes[1]! << 8) | bytes[0]!) >>> 0;
71
+ },
72
+ asInt32(endian: 'be' | 'le'): number {
73
+ const v = this.asUint32(endian);
74
+ return v > 2147483647 ? v - 4294967296 : v;
75
+ },
76
+ asString(): string { return new TextDecoder().decode(bytes); },
77
+ asStringTrim(): string { return this.asString().trim(); },
78
+ asInt(): number { return parseInt(this.asString(), 10) || 0; },
79
+ asFloat(): number { return parseFloat(this.asString()) || 0; },
80
+ unwrap(): Uint8Array { return bytes; },
81
+ unwrapOr(defaultValue: Uint8Array): Uint8Array { return ok ? bytes : defaultValue; },
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Create an IWriteResult with the given bytesWritten count.
87
+ */
88
+ export function createWriteResult(
89
+ bytesWritten: number,
90
+ ok: boolean = true,
91
+ status: number = 0,
92
+ ): IWriteResult {
93
+ return {
94
+ ok,
95
+ status,
96
+ bytesWritten,
97
+ unwrap(): number { return bytesWritten; },
98
+ unwrapOr(defaultValue: number): number { return ok ? bytesWritten : defaultValue; },
99
+ };
100
+ }
package/src/index.ts ADDED
@@ -0,0 +1,59 @@
1
+ // ---------------------------------------------------------------------------
2
+ // @typecad/simulator — Barrel export
3
+ // ---------------------------------------------------------------------------
4
+
5
+ // --- GPIO simulation ---
6
+ export { SimDigitalPin } from './gpio/digital-pin-sim.js';
7
+ export { SimAnalogPin } from './gpio/analog-pin-sim.js';
8
+ export { SimPWMPin } from './gpio/pwm-pin-sim.js';
9
+ export { SimInterruptPin } from './gpio/interrupt-sim.js';
10
+ export type { InterruptEvent } from './gpio/interrupt-sim.js';
11
+
12
+ // --- Bus simulation ---
13
+ export { SimSerialPort } from './bus/serial-sim.js';
14
+ export { SimI2CBus } from './bus/i2c-sim.js';
15
+ export type { I2COperationLog } from './bus/i2c-sim.js';
16
+ export { SimSPIBus } from './bus/spi-sim.js';
17
+ export type { SPIOperationLog } from './bus/spi-sim.js';
18
+
19
+ // --- Board factory ---
20
+ export { SimBoard, createSimBoard } from './board/board-sim.js';
21
+ export { createBoardFromDefinition } from './board/from-definition.js';
22
+
23
+ // --- Runtime contract interfaces (relocated from @typecad/hal) ---
24
+ // Pin/bus contracts implemented by the Sim* classes above.
25
+ export type {
26
+ BasePin,
27
+ PWMPin,
28
+ AnalogPin,
29
+ InterruptPin,
30
+ IOutputModePin,
31
+ IInputModePin,
32
+ II2CBus,
33
+ II2CDeviceAccessor,
34
+ ISPIBus,
35
+ ISPIDevice,
36
+ IUARTBus,
37
+ ISerialPort,
38
+ PinCapabilityFlags,
39
+ IToneAttachment,
40
+ UARTStatusInfo,
41
+ InterruptOptions,
42
+ ErrorPolicy,
43
+ } from './contracts.js';
44
+ export { I2CStatus, SPIStatus, UARTStatus } from './contracts.js';
45
+ export { hasPWM, hasAnalogInput, hasInterrupt, assertPWM, assertAnalog, assertInterrupt } from './contracts.js';
46
+
47
+ // --- Types ---
48
+ export { PinMode } from '@typecad/hal';
49
+ export type {
50
+ PinChangeCallback,
51
+ InterruptCallback,
52
+ ISimI2CDevice,
53
+ ISimSPIDevice,
54
+ SimBoardType,
55
+ SimBoardConfig,
56
+ } from './types.js';
57
+
58
+ // --- Helpers ---
59
+ export { createByteReadResult, createWriteResult } from './helpers.js';
package/src/types.ts ADDED
@@ -0,0 +1,104 @@
1
+ // ---------------------------------------------------------------------------
2
+ // @typecad/simulator — Simulator-specific types
3
+ // ---------------------------------------------------------------------------
4
+
5
+ /**
6
+ * Callback signature for pin state change events.
7
+ * @param pinNumber - The pin number that changed
8
+ * @param newValue - The new digital value (HIGH=1 or LOW=0)
9
+ */
10
+ export type PinChangeCallback = (pinNumber: number, newValue: number) => void;
11
+
12
+ /**
13
+ * Callback signature for interrupt events.
14
+ * @param pinNumber - The interrupt pin number
15
+ * @param mode - The trigger mode ('rising', 'falling', or 'change')
16
+ */
17
+ export type InterruptCallback = (pinNumber: number, mode: 'rising' | 'falling' | 'change') => void;
18
+
19
+ /**
20
+ * A simulated I2C device that responds to register reads and writes.
21
+ * Tests register mock devices to simulate sensor behavior.
22
+ */
23
+ export interface ISimI2CDevice {
24
+ /**
25
+ * Handle a register read from the bus master.
26
+ * @param register - The register address being read
27
+ * @param count - Number of bytes requested
28
+ * @returns Array of byte values to return
29
+ */
30
+ read(register: number, count: number): number[];
31
+
32
+ /**
33
+ * Handle a register write from the bus master.
34
+ * @param register - The register address being written
35
+ * @param data - The data bytes being written
36
+ */
37
+ write(register: number, data: number[]): void;
38
+ }
39
+
40
+ /**
41
+ * A simulated SPI device that responds to transfers.
42
+ */
43
+ export interface ISimSPIDevice {
44
+ /**
45
+ * Handle a full-duplex SPI transfer.
46
+ * @param mosiData - Data sent from master (MOSI)
47
+ * @returns Data received from device (MISO)
48
+ */
49
+ transfer(mosiData: number[]): number[];
50
+
51
+ /**
52
+ * Handle a register write (optional).
53
+ * If not provided, falls back to transfer().
54
+ * @param register - The register address
55
+ * @param data - The data bytes being written
56
+ */
57
+ write?(register: number, data: number[]): void;
58
+
59
+ /**
60
+ * Handle a register read (optional).
61
+ * If not provided, falls back to transfer() with dummy bytes.
62
+ * @param register - The register address
63
+ * @param count - Number of bytes to read
64
+ * @returns Data bytes read from the register
65
+ */
66
+ readRegister?(register: number, count: number): number[];
67
+ }
68
+
69
+ /**
70
+ * Board type identifiers for the simulator factory.
71
+ */
72
+ export type SimBoardType = 'arduino-uno' | 'arduino-nano' | string;
73
+
74
+ /**
75
+ * Configuration for creating a simulated board.
76
+ */
77
+ export interface SimBoardConfig {
78
+ /** Board type identifier. Determines pin count and peripheral availability. */
79
+ boardType: SimBoardType;
80
+ /** Number of digital pins (default: 14 for Uno) */
81
+ digitalPinCount?: number;
82
+ /** Number of analog input pins (default: 6 for Uno) */
83
+ analogPinCount?: number;
84
+ /** Number of I2C buses (default: 1) */
85
+ i2cBusCount?: number;
86
+ /** Number of SPI buses (default: 1) */
87
+ spiBusCount?: number;
88
+ /** Number of UART ports (default: 1) */
89
+ uartCount?: number;
90
+ /**
91
+ * PWM-capable pin numbers. Overrides the board type's default PWM pin map.
92
+ * For unknown/custom board types the default is empty (no PWM pins).
93
+ */
94
+ pwmPins?: number[];
95
+ /**
96
+ * Interrupt-capable pin numbers. Overrides the board type's default interrupt
97
+ * pin map. For unknown/custom board types the default is empty.
98
+ */
99
+ interruptPins?: number[];
100
+ /** RX buffer size for UART simulation (default: 256) */
101
+ uartRxBufferSize?: number;
102
+ /** TX buffer size for UART simulation (default: 256) */
103
+ uartTxBufferSize?: number;
104
+ }