@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,330 @@
1
+ // ---------------------------------------------------------------------------
2
+ // @typecad/simulator — Runtime contract interfaces
3
+ //
4
+ // These types define the runtime contracts that @typecad/simulator's simulated
5
+ // peripheral classes implement (SimDigitalPin, SimI2CBus, SimSPIBus,
6
+ // SimSerialPort, ...). They were relocated here from @typecad/hal so that HAL
7
+ // contains only its transpiler-shim surface, and the simulator owns the
8
+ // hierarchy that matches its job (real behavioral objects, not IR emitters).
9
+ //
10
+ // Primitive aliases that the contracts still reference (DigitalValue,
11
+ // AnalogValue, InterruptHandler) and the protocol-shape types (I2CAddress,
12
+ // SPIMode, SPIBitOrder, SPISettings) remain in @typecad/hal and are imported
13
+ // below. The dependency direction is simulator → hal (already established).
14
+ // ---------------------------------------------------------------------------
15
+
16
+ import type {
17
+ DigitalValue,
18
+ AnalogValue,
19
+ InterruptHandler,
20
+ I2CAddress,
21
+ } from '@typecad/hal';
22
+ import type {
23
+ SPIMode,
24
+ SPIBitOrder,
25
+ SPISettings,
26
+ } from '@typecad/hal';
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Pin capability flags
30
+ // ---------------------------------------------------------------------------
31
+
32
+ export interface PinCapabilityFlags {
33
+ digitalInput: boolean;
34
+ digitalOutput: boolean;
35
+ analogInput: boolean;
36
+ /** DAC output */
37
+ analogOutput: boolean;
38
+ pwm: boolean;
39
+ interrupt: boolean;
40
+ pullUp: boolean;
41
+ pullDown: boolean;
42
+ touch: boolean;
43
+ openDrain: boolean;
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Tone attachment (returned by tone() for chaining .for() duration)
48
+ // ---------------------------------------------------------------------------
49
+
50
+ export interface IToneAttachment {
51
+ for(duration: number): void;
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Interrupt handler types
56
+ // ---------------------------------------------------------------------------
57
+
58
+ export interface InterruptOptions {
59
+ debounce?: number;
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Pin interfaces (used by the simulator package)
64
+ // ---------------------------------------------------------------------------
65
+
66
+ export interface BasePin {
67
+ readonly number: number;
68
+ readonly gpio: number;
69
+ readonly capabilities: PinCapabilityFlags;
70
+ read(): DigitalValue;
71
+ isHigh(): boolean;
72
+ isLow(): boolean;
73
+ write(value: DigitalValue): void;
74
+ high(): void;
75
+ low(): void;
76
+ toggle(): void;
77
+ pulse(duration: number): void;
78
+ tone(frequency: number): IToneAttachment;
79
+ noTone(): void;
80
+ inputPullUp(): void;
81
+ inputPullDown?(): void;
82
+ outputOpenDrain(initial?: DigitalValue): void;
83
+ asOutput(initial?: DigitalValue): IOutputModePin;
84
+ asInput(): IInputModePin;
85
+ asInputPullUp(): IInputModePin;
86
+ pwm?(percent: number): void;
87
+ getPwmFrequency?(): number;
88
+ getPwmResolution?(): number;
89
+ readAnalog?(): AnalogValue;
90
+ readVoltage?(): number;
91
+ setAnalogReference?(voltage: number): void;
92
+ getAnalogResolution?(): number;
93
+ onRising?(handler: InterruptHandler, options?: InterruptOptions): void;
94
+ onFalling?(handler: InterruptHandler, options?: InterruptOptions): void;
95
+ onChange?(handler: InterruptHandler, options?: InterruptOptions): void;
96
+ offInterrupts?(): void;
97
+ waitForRising(timeout?: number): Promise<void>;
98
+ waitForFalling(timeout?: number): Promise<void>;
99
+ }
100
+
101
+ export interface IOutputModePin {
102
+ readonly number: number;
103
+ readonly gpio: number;
104
+ readonly capabilities: PinCapabilityFlags;
105
+ write(value: DigitalValue): void;
106
+ high(): void;
107
+ low(): void;
108
+ toggle(): void;
109
+ pulse(duration: number): void;
110
+ tone(frequency: number): IToneAttachment;
111
+ noTone(): void;
112
+ pwm?(percent: number): void;
113
+ getPwmFrequency?(): number;
114
+ getPwmResolution?(): number;
115
+ asOutput(initial?: DigitalValue): IOutputModePin;
116
+ asInput(): IInputModePin;
117
+ asInputPullUp(): IInputModePin;
118
+ inputPullUp(): void;
119
+ inputPullDown?(): void;
120
+ outputOpenDrain(initial?: DigitalValue): void;
121
+ }
122
+
123
+ export interface IInputModePin {
124
+ readonly number: number;
125
+ readonly gpio: number;
126
+ readonly capabilities: PinCapabilityFlags;
127
+ read(): DigitalValue;
128
+ isHigh(): boolean;
129
+ isLow(): boolean;
130
+ readAnalog?(): AnalogValue;
131
+ readVoltage?(): number;
132
+ setAnalogReference?(voltage: number): void;
133
+ getAnalogResolution?(): number;
134
+ onRising?(handler: InterruptHandler, options?: InterruptOptions): void;
135
+ onFalling?(handler: InterruptHandler, options?: InterruptOptions): void;
136
+ onChange?(handler: InterruptHandler, options?: InterruptOptions): void;
137
+ offInterrupts?(): void;
138
+ waitForRising(timeout?: number): Promise<void>;
139
+ waitForFalling(timeout?: number): Promise<void>;
140
+ asOutput(initial?: DigitalValue): IOutputModePin;
141
+ asInput(): IInputModePin;
142
+ asInputPullUp(): IInputModePin;
143
+ inputPullUp(): void;
144
+ inputPullDown?(): void;
145
+ outputOpenDrain(initial?: DigitalValue): void;
146
+ }
147
+
148
+ /** Pin with PWM output capability. */
149
+ export type PWMPin = BasePin & { pwm: NonNullable<BasePin['pwm']> };
150
+
151
+ /** Pin with analog input capability. */
152
+ export type AnalogPin = BasePin & { readAnalog: NonNullable<BasePin['readAnalog']> };
153
+
154
+ /** Pin with interrupt capability. */
155
+ export type InterruptPin = BasePin & { onRising: NonNullable<BasePin['onRising']> };
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Capability type guards and assertions
159
+ // ---------------------------------------------------------------------------
160
+
161
+ function isBasePin(value: unknown): value is BasePin {
162
+ return (
163
+ typeof value === 'object' &&
164
+ value !== null &&
165
+ 'number' in value &&
166
+ 'gpio' in value
167
+ );
168
+ }
169
+
170
+ export function hasPWM(pin: unknown): pin is PWMPin {
171
+ return isBasePin(pin) && 'pwm' in pin && typeof pin.pwm === 'function';
172
+ }
173
+
174
+ export function hasAnalogInput(pin: unknown): pin is AnalogPin {
175
+ return isBasePin(pin) && 'readAnalog' in pin && typeof pin.readAnalog === 'function';
176
+ }
177
+
178
+ export function hasInterrupt(pin: unknown): pin is InterruptPin {
179
+ return isBasePin(pin) && 'onRising' in pin && typeof pin.onRising === 'function';
180
+ }
181
+
182
+ export function assertPWM(pin: BasePin, message?: string): asserts pin is PWMPin {
183
+ if (!hasPWM(pin)) {
184
+ throw new Error(message ?? 'Pin does not support PWM');
185
+ }
186
+ }
187
+
188
+ export function assertAnalog(pin: BasePin, message?: string): asserts pin is AnalogPin {
189
+ if (!hasAnalogInput(pin)) {
190
+ throw new Error(message ?? 'Pin does not support analog input');
191
+ }
192
+ }
193
+
194
+ export function assertInterrupt(pin: BasePin, message?: string): asserts pin is InterruptPin {
195
+ if (!hasInterrupt(pin)) {
196
+ throw new Error(message ?? 'Pin does not support interrupts');
197
+ }
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Error policy
202
+ // ---------------------------------------------------------------------------
203
+
204
+ export type ErrorPolicy = 'throw' | 'callback' | 'silent';
205
+
206
+ // ---------------------------------------------------------------------------
207
+ // I2C types
208
+ // ---------------------------------------------------------------------------
209
+
210
+ export enum I2CStatus {
211
+ SUCCESS = 0,
212
+ DATA_TOO_LONG = 1,
213
+ NACK_ON_ADDRESS = 2,
214
+ NACK_ON_DATA = 3,
215
+ OTHER_ERROR = 4,
216
+ PARTIAL_READ = 5,
217
+ }
218
+
219
+ export interface II2CDeviceAccessor {
220
+ readonly address: I2CAddress;
221
+ readByte(register: number): number;
222
+ readBytes(register: number, count: number): Uint8Array;
223
+ writeByte(register: number, value: number): void;
224
+ writeBytes(register: number, data: Uint8Array | number[]): void;
225
+ }
226
+
227
+ export interface II2CBus {
228
+ readonly busNumber: number;
229
+ readonly isEnabled: boolean;
230
+ begin(): void;
231
+ begin(address: I2CAddress): void;
232
+ end(): void;
233
+ setClock(hz: number): void;
234
+ device(address: I2CAddress): II2CDeviceAccessor;
235
+ onError(handler: (status: I2CStatus, address: I2CAddress, operation: 'read' | 'write') => void): void;
236
+ errorPolicy: ErrorPolicy;
237
+ recover(): boolean;
238
+ }
239
+
240
+ // ---------------------------------------------------------------------------
241
+ // SPI types
242
+ // ---------------------------------------------------------------------------
243
+
244
+ export enum SPIStatus {
245
+ SUCCESS = 0,
246
+ NOT_INITIALIZED = 1,
247
+ TRANSFER_FAILED = 2,
248
+ INVALID_CONFIG = 3,
249
+ TIMEOUT = 4,
250
+ DEVICE_ERROR = 5,
251
+ }
252
+
253
+ export interface ISPIDevice {
254
+ readonly chipSelect: BasePin;
255
+ transfer(data: number | Uint8Array): Uint8Array;
256
+ write(data: number | Uint8Array): void;
257
+ read(count: number): Uint8Array;
258
+ writeRegister(register: number, data: number | Uint8Array): void;
259
+ readRegister(register: number, count: number): Uint8Array;
260
+ }
261
+
262
+ export interface ISPIBus {
263
+ readonly isEnabled: boolean;
264
+ begin(): void;
265
+ end(): void;
266
+ setMode(mode: SPIMode): void;
267
+ setBitOrder(order: SPIBitOrder): void;
268
+ setFrequency(hz: number): void;
269
+ beginTransaction(settings: SPISettings): void;
270
+ endTransaction(): void;
271
+ device(chipSelect: BasePin): ISPIDevice;
272
+ onError(handler: (status: SPIStatus, operation: 'transfer' | 'read' | 'write') => void): void;
273
+ errorPolicy: ErrorPolicy;
274
+ }
275
+
276
+ // ---------------------------------------------------------------------------
277
+ // UART types
278
+ // ---------------------------------------------------------------------------
279
+
280
+ export enum UARTStatus {
281
+ SUCCESS = 0,
282
+ NOT_INITIALIZED = 1,
283
+ TIMEOUT = 2,
284
+ BUFFER_OVERFLOW = 3,
285
+ OVERRUN_ERROR = 4,
286
+ PARITY_ERROR = 5,
287
+ FRAMING_ERROR = 6,
288
+ BREAK_DETECTED = 7,
289
+ WRITE_FAILED = 8,
290
+ READ_FAILED = 9,
291
+ }
292
+
293
+ export interface UARTStatusInfo {
294
+ available: number;
295
+ writeAvailable: number;
296
+ overrunError: boolean;
297
+ parityError: boolean;
298
+ framingError: boolean;
299
+ breakDetected: boolean;
300
+ }
301
+
302
+ export interface IUARTBus {
303
+ readonly uartNumber: number;
304
+ readonly baudRate: number;
305
+ readonly isEnabled: boolean;
306
+ begin(baud: number): void;
307
+ end(): void;
308
+ read(): number;
309
+ peek(): number;
310
+ readLine(): string;
311
+ readBytes(count: number): Uint8Array;
312
+ readString(): string;
313
+ available(): number;
314
+ write(data: number | Uint8Array | string): number;
315
+ flush(): void;
316
+ getStatus(): UARTStatusInfo;
317
+ clearErrors(): void;
318
+ onReceive(callback: (bytesAvailable: number) => void): void;
319
+ onTransmitComplete(callback: () => void): void;
320
+ onError(callback: (status: UARTStatus) => void): void;
321
+ errorPolicy: ErrorPolicy;
322
+ }
323
+
324
+ export interface ISerialPort extends IUARTBus {
325
+ print(...args: unknown[]): void;
326
+ println(...args: unknown[]): void;
327
+ printf(format: string, ...args: unknown[]): void;
328
+ isConnected(): boolean;
329
+ waitForConnection(timeout?: number): Promise<void>;
330
+ }
@@ -0,0 +1,91 @@
1
+ // ---------------------------------------------------------------------------
2
+ // @typecad/simulator — Simulated analog input pin
3
+ // ---------------------------------------------------------------------------
4
+
5
+ import type { AnalogPin } from '../contracts.js';
6
+ import type { AnalogValue } from '@typecad/hal';
7
+ import { SimDigitalPin } from './digital-pin-sim.js';
8
+
9
+ /**
10
+ * Simulated analog input pin.
11
+ *
12
+ * Extends SimDigitalPin with analog read capabilities.
13
+ * Tests can inject values via `injectValue()` and verify readings.
14
+ *
15
+ * - `read()` returns a digital boolean (HIGH if above half-scale).
16
+ * - `readAnalog()` returns the raw ADC value.
17
+ */
18
+ export class SimAnalogPin extends SimDigitalPin implements AnalogPin {
19
+ private _analogValue: AnalogValue = 0;
20
+ private _voltage: number = 0;
21
+ private _reference: number = 5.0; // Default 5V reference
22
+ private _resolution: number = 10; // Default 10-bit (0-1023)
23
+
24
+ constructor(pinNum: number) {
25
+ super(pinNum, { analogInput: true });
26
+ }
27
+
28
+ // --- AnalogPin capability methods ---
29
+
30
+ /** Convenience: set pin to analog input mode. */
31
+ analog(): void {
32
+ this.asInput();
33
+ }
34
+
35
+ readAnalog(): AnalogValue {
36
+ return this._analogValue;
37
+ }
38
+
39
+ readVoltage(): number {
40
+ return this._voltage;
41
+ }
42
+
43
+ setAnalogReference(voltage: number): void {
44
+ this._reference = voltage;
45
+ }
46
+
47
+ getAnalogResolution(): number {
48
+ return this._resolution;
49
+ }
50
+
51
+ // --- Simulation helpers ---
52
+
53
+ /**
54
+ * Inject an analog value (e.g., sensor reading).
55
+ * Also updates the digital read state based on threshold.
56
+ * @param value - Raw ADC value (0 to 2^resolution - 1)
57
+ */
58
+ override injectValue(value: AnalogValue): void {
59
+ this._analogValue = value;
60
+ // Calculate voltage from ADC value
61
+ const maxAdc = (1 << this._resolution) - 1;
62
+ this._voltage = (value / maxAdc) * this._reference;
63
+ // Update digital state: HIGH if above half-scale
64
+ super.injectValue(value > (maxAdc / 2) ? 1 : 0);
65
+ }
66
+
67
+ /**
68
+ * Inject a voltage directly. Converts to ADC value based on resolution.
69
+ */
70
+ injectVoltage(voltage: number): void {
71
+ this._voltage = voltage;
72
+ const maxAdc = (1 << this._resolution) - 1;
73
+ this._analogValue = Math.round((voltage / this._reference) * maxAdc);
74
+ super.injectValue(this._analogValue > (maxAdc / 2) ? 1 : 0);
75
+ }
76
+
77
+ /**
78
+ * Set the ADC resolution in bits.
79
+ */
80
+ setResolution(bits: number): void {
81
+ this._resolution = bits;
82
+ }
83
+
84
+ override reset(): void {
85
+ super.reset();
86
+ this._analogValue = 0;
87
+ this._voltage = 0;
88
+ this._reference = 5.0;
89
+ this._resolution = 10;
90
+ }
91
+ }
@@ -0,0 +1,234 @@
1
+ // ---------------------------------------------------------------------------
2
+ // @typecad/simulator — Simulated digital pin
3
+ // ---------------------------------------------------------------------------
4
+
5
+ import type {
6
+ BasePin,
7
+ IToneAttachment,
8
+ PinCapabilityFlags,
9
+ } from '../contracts.js';
10
+ import { PinMode } from '@typecad/hal';
11
+ import type { DigitalValue } from '@typecad/hal';
12
+
13
+ /**
14
+ * Tracks the history of pin state changes for test assertions.
15
+ */
16
+ export interface PinStateChange {
17
+ /** Timestamp (ms since simulation start) */
18
+ timestamp: number;
19
+ /** Previous value (0 or 1) */
20
+ from: number;
21
+ /** New value (0 or 1) */
22
+ to: number;
23
+ }
24
+
25
+ /**
26
+ * Convert a DigitalValue to a plain 0/1 number.
27
+ */
28
+ function toBitValue(value: DigitalValue): number {
29
+ return value ? 1 : 0;
30
+ }
31
+
32
+ /**
33
+ * Simulated digital pin that tracks state transitions.
34
+ */
35
+ /** Default capability flags for a digital pin. */
36
+ const DEFAULT_DIGITAL_CAPS: PinCapabilityFlags = {
37
+ digitalInput: true,
38
+ digitalOutput: true,
39
+ analogInput: false,
40
+ analogOutput: false,
41
+ pwm: false,
42
+ interrupt: false,
43
+ pullUp: true,
44
+ pullDown: false,
45
+ touch: false,
46
+ openDrain: false,
47
+ };
48
+
49
+ export class SimDigitalPin implements BasePin {
50
+ readonly number: number;
51
+ readonly gpio: number;
52
+ readonly capabilities: PinCapabilityFlags;
53
+
54
+ private _value: number = 0;
55
+ private _pinMode: PinMode = PinMode.OUTPUT;
56
+ private _history: PinStateChange[] = [];
57
+ protected _startTime: number = Date.now();
58
+
59
+ constructor(pinNum: number, caps?: Partial<PinCapabilityFlags>) {
60
+ this.number = pinNum;
61
+ this.gpio = pinNum;
62
+ this.capabilities = { ...DEFAULT_DIGITAL_CAPS, ...caps };
63
+ }
64
+
65
+ // --- BasePin ---
66
+
67
+ getMode(): PinMode {
68
+ return this._pinMode;
69
+ }
70
+
71
+ setMode(mode: PinMode): void {
72
+ this._pinMode = mode;
73
+ }
74
+
75
+ has(capability: keyof PinCapabilityFlags): boolean {
76
+ return this.capabilities[capability];
77
+ }
78
+
79
+ // --- Pin configuration ---
80
+
81
+ inputPullUp(): void {
82
+ this._pinMode = PinMode.INPUT_PULLUP;
83
+ this._setBitValue(1);
84
+ }
85
+
86
+ inputPullDown(): void {
87
+ this._pinMode = PinMode.INPUT_PULLDOWN;
88
+ this._setBitValue(0);
89
+ }
90
+
91
+ outputOpenDrain(initial?: DigitalValue): void {
92
+ this._pinMode = PinMode.OUTPUT_OPEN_DRAIN;
93
+ if (initial !== undefined) {
94
+ this._setBitValue(toBitValue(initial));
95
+ }
96
+ }
97
+
98
+ // --- Fluent mode conversion ---
99
+
100
+ asOutput(initial?: DigitalValue): any {
101
+ this._pinMode = PinMode.OUTPUT;
102
+ if (initial !== undefined) {
103
+ this._setBitValue(toBitValue(initial));
104
+ }
105
+ return this;
106
+ }
107
+
108
+ asInput(): any {
109
+ this._pinMode = PinMode.INPUT;
110
+ return this;
111
+ }
112
+
113
+ asInputPullUp(): any {
114
+ this.inputPullUp();
115
+ return this;
116
+ }
117
+
118
+ // --- IDigitalInput ---
119
+
120
+ read(): DigitalValue {
121
+ return this._value === 1;
122
+ }
123
+
124
+ isHigh(): boolean {
125
+ return this._value === 1;
126
+ }
127
+
128
+ isLow(): boolean {
129
+ return this._value === 0;
130
+ }
131
+
132
+ async waitForRising(_timeout?: number): Promise<void> {
133
+ // In simulation, this resolves immediately.
134
+ // Tests should use injectValue() + explicit assertions instead.
135
+ }
136
+
137
+ async waitForFalling(_timeout?: number): Promise<void> {
138
+ // In simulation, this resolves immediately.
139
+ }
140
+
141
+ // --- IDigitalOutput ---
142
+
143
+ write(value: DigitalValue): void {
144
+ this._setBitValue(toBitValue(value));
145
+ }
146
+
147
+ high(): void {
148
+ this._setBitValue(1);
149
+ }
150
+
151
+ low(): void {
152
+ this._setBitValue(0);
153
+ }
154
+
155
+ toggle(): void {
156
+ this._setBitValue(this._value === 1 ? 0 : 1);
157
+ }
158
+
159
+ pulse(_duration: number): void {
160
+ // In simulation the pulse duration is not awaited, so a pulse produces no
161
+ // observable transition — leave both pin state and history unchanged.
162
+ }
163
+
164
+ tone(_frequency: number): IToneAttachment {
165
+ return {
166
+ for(_duration: number): void {
167
+ // No-op in simulation
168
+ },
169
+ };
170
+ }
171
+
172
+ noTone(): void {
173
+ // No-op in simulation
174
+ }
175
+
176
+ stop(): void {
177
+ this.noTone();
178
+ }
179
+
180
+ // --- Simulation helpers ---
181
+
182
+ /**
183
+ * Inject a value into this pin (simulates external signal).
184
+ * Use this in tests to simulate button presses, sensor triggers, etc.
185
+ */
186
+ injectValue(value: number): void {
187
+ this._setBitValue(value & 1);
188
+ }
189
+
190
+ /**
191
+ * Get the history of all state changes on this pin.
192
+ */
193
+ getHistory(): readonly PinStateChange[] {
194
+ return this._history;
195
+ }
196
+
197
+ /**
198
+ * Clear the state change history.
199
+ */
200
+ clearHistory(): void {
201
+ this._history = [];
202
+ }
203
+
204
+ /**
205
+ * Get the current value as a plain number (0 or 1).
206
+ */
207
+ getBitValue(): number {
208
+ return this._value;
209
+ }
210
+
211
+ /**
212
+ * Reset the pin to its initial state.
213
+ */
214
+ reset(): void {
215
+ this._value = 0;
216
+ this._pinMode = PinMode.OUTPUT;
217
+ this._history = [];
218
+ this._startTime = Date.now();
219
+ }
220
+
221
+ // --- Internal ---
222
+
223
+ private _setBitValue(newValue: number): void {
224
+ const oldValue = this._value;
225
+ this._value = newValue & 1;
226
+ if (oldValue !== this._value) {
227
+ this._history.push({
228
+ timestamp: Date.now() - this._startTime,
229
+ from: oldValue,
230
+ to: this._value,
231
+ });
232
+ }
233
+ }
234
+ }