@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,280 @@
1
+ // ---------------------------------------------------------------------------
2
+ // @typecad/simulator — Serial port simulation
3
+ // ---------------------------------------------------------------------------
4
+
5
+ import { UARTStatus } from '../contracts.js';
6
+ import type {
7
+ ISerialPort,
8
+ UARTStatusInfo,
9
+ ErrorPolicy,
10
+ } from '../contracts.js';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // SimSerialPort
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /**
17
+ * Simulated serial port for testing UART communication.
18
+ *
19
+ * Tests can inject bytes into the RX buffer and inspect the TX buffer
20
+ * to verify that sketch code sends the expected data.
21
+ */
22
+ export class SimSerialPort implements ISerialPort {
23
+ readonly uartNumber: number;
24
+ baudRate: number = 0;
25
+ isEnabled: boolean = false;
26
+ errorPolicy: ErrorPolicy = 'callback';
27
+
28
+ private _rxBuffer: number[] = [];
29
+ private _txBuffer: number[] = [];
30
+ private _rxBufferSize: number;
31
+ private _txBufferSize: number;
32
+ private _overrunError: boolean = false;
33
+ private _parityError: boolean = false;
34
+ private _framingError: boolean = false;
35
+ private _breakDetected: boolean = false;
36
+
37
+ private _onReceiveCallback: ((bytesAvailable: number) => void) | null = null;
38
+ private _onTransmitCompleteCallback: (() => void) | null = null;
39
+ private _onErrorCallback: ((status: UARTStatus) => void) | null = null;
40
+
41
+ constructor(uartNumber: number = 0, rxBufferSize: number = 256, txBufferSize: number = 256) {
42
+ this.uartNumber = uartNumber;
43
+ this._rxBufferSize = rxBufferSize;
44
+ this._txBufferSize = txBufferSize;
45
+ }
46
+
47
+ // --- IUARTBus methods ---
48
+
49
+ begin(baud: number): void {
50
+ this.baudRate = baud;
51
+ this.isEnabled = true;
52
+ }
53
+
54
+ end(): void {
55
+ // In simulation, ending the port disables it without destroying buffers.
56
+ this.isEnabled = false;
57
+ }
58
+
59
+ read(): number {
60
+ if (this._rxBuffer.length === 0) return -1;
61
+ return this._rxBuffer.shift()!;
62
+ }
63
+
64
+ peek(): number {
65
+ if (this._rxBuffer.length === 0) return -1;
66
+ return this._rxBuffer[0];
67
+ }
68
+
69
+ readLine(): string {
70
+ const idx = this._rxBuffer.findIndex(b => b === 0x0A);
71
+ if (idx === -1) return '';
72
+ const lineBytes = this._rxBuffer.splice(0, idx + 1);
73
+ return new TextDecoder().decode(new Uint8Array(lineBytes)).trimEnd();
74
+ }
75
+
76
+ readBytes(count: number): Uint8Array {
77
+ if (this._rxBuffer.length < count) {
78
+ const available = this._rxBuffer.splice(0, this._rxBuffer.length);
79
+ return new Uint8Array(available);
80
+ }
81
+ const result = this._rxBuffer.splice(0, count);
82
+ return new Uint8Array(result);
83
+ }
84
+
85
+ readString(): string {
86
+ const result = new Uint8Array(this._rxBuffer);
87
+ this._rxBuffer = [];
88
+ return new TextDecoder().decode(result);
89
+ }
90
+
91
+ available(): number {
92
+ return this._rxBuffer.length;
93
+ }
94
+
95
+ write(data: number | Uint8Array | string): number {
96
+ const bytes = typeof data === 'number'
97
+ ? [data & 0xFF]
98
+ : typeof data === 'string'
99
+ ? this._encodeString(data)
100
+ : Array.from(data);
101
+ return this._pushTxBytes(bytes);
102
+ }
103
+
104
+ flush(): void {
105
+ // In simulation, data is "sent" immediately when written to TX buffer.
106
+ // Flush triggers the transmit complete callback.
107
+ if (this._onTransmitCompleteCallback) {
108
+ this._onTransmitCompleteCallback();
109
+ }
110
+ }
111
+
112
+ getStatus(): UARTStatusInfo {
113
+ return {
114
+ available: this._rxBuffer.length,
115
+ writeAvailable: this._txBufferSize - this._txBuffer.length,
116
+ overrunError: this._overrunError,
117
+ parityError: this._parityError,
118
+ framingError: this._framingError,
119
+ breakDetected: this._breakDetected,
120
+ };
121
+ }
122
+
123
+ clearErrors(): void {
124
+ this._overrunError = false;
125
+ this._parityError = false;
126
+ this._framingError = false;
127
+ this._breakDetected = false;
128
+ }
129
+
130
+ onReceive(callback: (bytesAvailable: number) => void): void {
131
+ this._onReceiveCallback = callback;
132
+ }
133
+
134
+ onTransmitComplete(callback: () => void): void {
135
+ this._onTransmitCompleteCallback = callback;
136
+ }
137
+
138
+ onError(callback: (status: UARTStatus) => void): void {
139
+ this._onErrorCallback = callback;
140
+ }
141
+
142
+ // --- ISerialPort methods ---
143
+
144
+ print(...args: unknown[]): void {
145
+ const text = args.map(a => String(a)).join('');
146
+ this.write(text);
147
+ }
148
+
149
+ println(...args: unknown[]): void {
150
+ const text = args.map(a => String(a)).join('');
151
+ this.write(text + '\r\n');
152
+ }
153
+
154
+ printf(format: string, ...args: unknown[]): void {
155
+ const text = this._formatString(format, args);
156
+ this.write(text);
157
+ }
158
+
159
+ isConnected(): boolean {
160
+ return this.isEnabled;
161
+ }
162
+
163
+ async waitForConnection(_timeout?: number): Promise<void> {
164
+ // In simulation, just resolve immediately
165
+ }
166
+
167
+ // --- Simulation helpers ---
168
+
169
+ /**
170
+ * Inject bytes into the RX buffer (simulating received data).
171
+ * Triggers onReceive callback if registered.
172
+ */
173
+ injectRx(data: Uint8Array | number[] | string): void {
174
+ const bytes = typeof data === 'string'
175
+ ? this._encodeString(data)
176
+ : Array.isArray(data)
177
+ ? data
178
+ : Array.from(data);
179
+
180
+ for (const b of bytes) {
181
+ if (this._rxBuffer.length >= this._rxBufferSize) {
182
+ this._overrunError = true;
183
+ break;
184
+ }
185
+ this._rxBuffer.push(b & 0xFF);
186
+ }
187
+
188
+ if (this._onReceiveCallback) {
189
+ this._onReceiveCallback(this._rxBuffer.length);
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Get all bytes written to the TX buffer since the last call.
195
+ * Clears the TX buffer.
196
+ */
197
+ flushTx(): number[] {
198
+ const result = this._txBuffer.slice();
199
+ this._txBuffer = [];
200
+ if (this._onTransmitCompleteCallback) {
201
+ this._onTransmitCompleteCallback();
202
+ }
203
+ return result;
204
+ }
205
+
206
+ /**
207
+ * Get the TX buffer contents without clearing.
208
+ */
209
+ peekTx(): readonly number[] {
210
+ return this._txBuffer;
211
+ }
212
+
213
+ /**
214
+ * Get the TX buffer contents as a string.
215
+ */
216
+ peekTxAsString(): string {
217
+ return new TextDecoder().decode(new Uint8Array(this._txBuffer));
218
+ }
219
+
220
+ /**
221
+ * Reset all buffers and state.
222
+ */
223
+ reset(): void {
224
+ this._rxBuffer = [];
225
+ this._txBuffer = [];
226
+ this._overrunError = false;
227
+ this._parityError = false;
228
+ this._framingError = false;
229
+ this._breakDetected = false;
230
+ this.isEnabled = false;
231
+ this.baudRate = 0;
232
+ }
233
+
234
+ // --- Private helpers ---
235
+
236
+ private _pushTxBytes(bytes: number[]): number {
237
+ let written = 0;
238
+ for (const b of bytes) {
239
+ if (this._txBuffer.length >= this._txBufferSize) break;
240
+ this._txBuffer.push(b & 0xFF);
241
+ written++;
242
+ }
243
+ return written;
244
+ }
245
+
246
+ private _encodeString(text: string): number[] {
247
+ return Array.from(new TextEncoder().encode(text));
248
+ }
249
+
250
+ private _formatString(fmt: string, args: unknown[]): string {
251
+ let result = '';
252
+ let argIdx = 0;
253
+ for (let i = 0; i < fmt.length; i++) {
254
+ if (fmt[i] === '{' && fmt[i + 1] === '}') {
255
+ result += String(args[argIdx++] ?? '');
256
+ i++;
257
+ } else if (fmt[i] === '%' && i + 1 < fmt.length) {
258
+ const spec = fmt[i + 1];
259
+ if (spec === 'd' || spec === 'i') {
260
+ result += String(Number(args[argIdx++]) || 0);
261
+ i++;
262
+ } else if (spec === 's') {
263
+ result += String(args[argIdx++] ?? '');
264
+ i++;
265
+ } else if (spec === 'f') {
266
+ result += String(Number(args[argIdx++]) || 0.0);
267
+ i++;
268
+ } else if (spec === '%') {
269
+ result += '%';
270
+ i++;
271
+ } else {
272
+ result += fmt[i];
273
+ }
274
+ } else {
275
+ result += fmt[i];
276
+ }
277
+ }
278
+ return result;
279
+ }
280
+ }
@@ -0,0 +1,269 @@
1
+ // ---------------------------------------------------------------------------
2
+ // @typecad/simulator — SPI bus simulation
3
+ // ---------------------------------------------------------------------------
4
+
5
+ import { SPIStatus } from '../contracts.js';
6
+ import type {
7
+ ISPIBus,
8
+ ISPIDevice,
9
+ BasePin,
10
+ ErrorPolicy,
11
+ } from '../contracts.js';
12
+ import type { SPIMode, SPIBitOrder, SPISettings } from '@typecad/hal';
13
+ import type { ISimSPIDevice } from '../types.js';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // SimSPIBus
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /**
20
+ * Simulated SPI bus for testing SPI communication.
21
+ *
22
+ * Tests register mock devices via `attachDevice()`. When sketch code performs
23
+ * transfers, the corresponding mock device handles the operation.
24
+ */
25
+ export class SimSPIBus implements ISPIBus {
26
+ isEnabled: boolean = false;
27
+ errorPolicy: ErrorPolicy = 'callback';
28
+
29
+ private _frequency: number = 4000000;
30
+ private _mode: SPIMode = 0;
31
+ private _bitOrder: SPIBitOrder = 'msb';
32
+ private _devices: Map<string, ISimSPIDevice> = new Map();
33
+ private _inTransaction: boolean = false;
34
+
35
+ /** Track all operations for test assertions. */
36
+ private _log: SPIOperationLog[] = [];
37
+
38
+ // --- ISPIBus methods ---
39
+
40
+ begin(): void {
41
+ this.isEnabled = true;
42
+ }
43
+
44
+ end(): void {
45
+ // In simulation, ending the bus disables it without releasing mock devices.
46
+ this.isEnabled = false;
47
+ }
48
+
49
+ setMode(mode: SPIMode): void {
50
+ this._mode = mode;
51
+ }
52
+
53
+ setBitOrder(order: SPIBitOrder): void {
54
+ this._bitOrder = order;
55
+ }
56
+
57
+ setFrequency(hz: number): void {
58
+ this._frequency = hz;
59
+ }
60
+
61
+ beginTransaction(_settings: SPISettings): void {
62
+ this._inTransaction = true;
63
+ }
64
+
65
+ endTransaction(): void {
66
+ this._inTransaction = false;
67
+ }
68
+
69
+ transfer(data: number | Uint8Array): Uint8Array {
70
+ const mosiData = typeof data === 'number' ? [data] : Array.from(data);
71
+ const timestamp = Date.now();
72
+
73
+ // Bus-level transfer (no CS pin) — log but no device interaction
74
+ this._log.push({ operation: 'transfer', data: mosiData, timestamp });
75
+ return new Uint8Array(mosiData.length);
76
+ }
77
+
78
+ write(data: number | Uint8Array): void {
79
+ const mosiData = typeof data === 'number' ? [data] : Array.from(data);
80
+ const timestamp = Date.now();
81
+ this._log.push({ operation: 'write', data: mosiData, timestamp });
82
+ }
83
+
84
+ write16(value: number): void {
85
+ const msb = (value >> 8) & 0xFF;
86
+ const lsb = value & 0xFF;
87
+ this.write(new Uint8Array([msb, lsb]));
88
+ }
89
+
90
+ device(chipSelect: BasePin): ISPIDevice {
91
+ return new SimSPIDevice(this, chipSelect);
92
+ }
93
+
94
+ // --- Simulation helpers ---
95
+
96
+ /**
97
+ * Attach a mock SPI device for the given chip-select pin.
98
+ */
99
+ attachDevice(csPin: BasePin, device: ISimSPIDevice): void {
100
+ this._devices.set(String(csPin.number), device);
101
+ }
102
+
103
+ /**
104
+ * Remove a mock device.
105
+ */
106
+ detachDevice(csPin: BasePin): void {
107
+ this._devices.delete(String(csPin.number));
108
+ }
109
+
110
+ /**
111
+ * Get the operation log for test assertions.
112
+ */
113
+ getLog(): readonly SPIOperationLog[] {
114
+ return this._log;
115
+ }
116
+
117
+ /**
118
+ * Clear the operation log.
119
+ */
120
+ clearLog(): void {
121
+ this._log = [];
122
+ }
123
+
124
+ onError(_handler: (status: SPIStatus, operation: 'transfer' | 'read' | 'write') => void): void {
125
+ // No-op in simulator
126
+ }
127
+
128
+ /**
129
+ * Reset all state.
130
+ */
131
+ reset(): void {
132
+ this._devices.clear();
133
+ this._log = [];
134
+ this.isEnabled = false;
135
+ this._inTransaction = false;
136
+ this._frequency = 4000000;
137
+ this._mode = 0;
138
+ this._bitOrder = 'msb';
139
+ }
140
+
141
+ // --- Internal ---
142
+
143
+ /** @internal */
144
+ _getDevice(csKey: string): ISimSPIDevice | undefined {
145
+ return this._devices.get(csKey);
146
+ }
147
+
148
+ /** @internal */
149
+ _logOperation(op: SPIOperationLog): void {
150
+ this._log.push(op);
151
+ }
152
+ }
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // SimSPIDevice — implements ISPIDevice
156
+ // ---------------------------------------------------------------------------
157
+
158
+ class SimSPIDevice implements ISPIDevice {
159
+ readonly bus: ISPIBus;
160
+ readonly chipSelect: BasePin;
161
+ private readonly _simBus: SimSPIBus;
162
+
163
+ constructor(bus: SimSPIBus, chipSelect: BasePin) {
164
+ this.bus = bus;
165
+ this._simBus = bus;
166
+ this.chipSelect = chipSelect;
167
+ }
168
+
169
+ transfer(data: number | Uint8Array): Uint8Array {
170
+ const csKey = String(this.chipSelect.number);
171
+ const mosiData = typeof data === 'number' ? [data] : Array.from(data);
172
+ const timestamp = Date.now();
173
+ const device = this._simBus._getDevice(csKey);
174
+
175
+ if (!device) {
176
+ this._simBus._logOperation({ operation: 'transfer', data: mosiData, timestamp });
177
+ return new Uint8Array(0);
178
+ }
179
+
180
+ const misoData = device.transfer(mosiData);
181
+ this._simBus._logOperation({ operation: 'transfer', data: mosiData, response: misoData, timestamp });
182
+ return new Uint8Array(misoData);
183
+ }
184
+
185
+ write(data: number | Uint8Array): void {
186
+ const csKey = String(this.chipSelect.number);
187
+ const dataArray = typeof data === 'number' ? [data] : Array.from(data);
188
+ const timestamp = Date.now();
189
+ const device = this._simBus._getDevice(csKey);
190
+
191
+ if (!device) {
192
+ this._simBus._logOperation({ operation: 'write', data: dataArray, timestamp });
193
+ return;
194
+ }
195
+
196
+ if (device.write) {
197
+ // Write without register — use register 0 as placeholder
198
+ device.write(0, dataArray);
199
+ } else {
200
+ device.transfer(dataArray);
201
+ }
202
+ this._simBus._logOperation({ operation: 'write', data: dataArray, timestamp });
203
+ }
204
+
205
+ read(count: number): Uint8Array {
206
+ const csKey = String(this.chipSelect.number);
207
+ const timestamp = Date.now();
208
+ const device = this._simBus._getDevice(csKey);
209
+
210
+ if (!device) {
211
+ this._simBus._logOperation({ operation: 'read', count, timestamp });
212
+ return new Uint8Array(0);
213
+ }
214
+
215
+ // Send dummy bytes to read
216
+ const misoData = device.transfer(new Array(count).fill(0));
217
+ this._simBus._logOperation({ operation: 'read', count, data: misoData, timestamp });
218
+ return new Uint8Array(misoData);
219
+ }
220
+
221
+ writeRegister(register: number, data: number | Uint8Array): void {
222
+ const csKey = String(this.chipSelect.number);
223
+ const dataArray = typeof data === 'number' ? [data] : Array.from(data);
224
+ const timestamp = Date.now();
225
+ const device = this._simBus._getDevice(csKey);
226
+
227
+ if (!device) {
228
+ this._simBus._logOperation({ operation: 'write', register, data: dataArray, timestamp });
229
+ return;
230
+ }
231
+
232
+ if (device.write) {
233
+ device.write(register, dataArray);
234
+ } else {
235
+ device.transfer([register, ...dataArray]);
236
+ }
237
+ this._simBus._logOperation({ operation: 'write', register, data: dataArray, timestamp });
238
+ }
239
+
240
+ readRegister(register: number, count: number): Uint8Array {
241
+ const csKey = String(this.chipSelect.number);
242
+ const timestamp = Date.now();
243
+ const device = this._simBus._getDevice(csKey);
244
+
245
+ if (!device) {
246
+ this._simBus._logOperation({ operation: 'read', register, count, timestamp });
247
+ return new Uint8Array(0);
248
+ }
249
+
250
+ const data = device.readRegister
251
+ ? device.readRegister(register, count)
252
+ : device.transfer([register, ...new Array(count).fill(0)]).slice(1);
253
+ this._simBus._logOperation({ operation: 'read', register, count, data, timestamp });
254
+ return new Uint8Array(data);
255
+ }
256
+ }
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // Operation log
260
+ // ---------------------------------------------------------------------------
261
+
262
+ export interface SPIOperationLog {
263
+ operation: 'read' | 'write' | 'transfer';
264
+ register?: number;
265
+ count?: number;
266
+ data?: number[];
267
+ response?: number[];
268
+ timestamp: number;
269
+ }