@kb1rma/libcomm14cux-ts 0.2.0 → 0.2.1-beta.9
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.
- package/README.md +2 -0
- package/dist/constants.d.ts +6 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +6 -0
- package/dist/constants.js.map +1 -1
- package/dist/ecu.d.ts +9 -0
- package/dist/ecu.d.ts.map +1 -1
- package/dist/ecu.js +2 -2
- package/dist/ecu.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/protocol/protocol.d.ts +3 -1
- package/dist/protocol/protocol.d.ts.map +1 -1
- package/dist/protocol/protocol.js +50 -6
- package/dist/protocol/protocol.js.map +1 -1
- package/dist/transport/simulated.d.ts +42 -2
- package/dist/transport/simulated.d.ts.map +1 -1
- package/dist/transport/simulated.js +73 -6
- package/dist/transport/simulated.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +7 -0
- package/src/ecu.ts +11 -0
- package/src/index.ts +1 -0
- package/src/protocol/protocol.ts +62 -6
- package/src/transport/simulated.ts +108 -6
- package/src/version.ts +1 -1
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import type { Transport } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Where a memory write injected by {@link SimulatedTransport.failMemoryWritesAfter}
|
|
4
|
+
* goes wrong:
|
|
5
|
+
*
|
|
6
|
+
* - `'commandLost'`: the write command never reaches the ECU. No echo, and
|
|
7
|
+
* nothing is stored.
|
|
8
|
+
* - `'commandEchoCorrupted'`: the ECU takes the write command, but its echo
|
|
9
|
+
* is garbled on the way back. The ECU then waits for the value byte.
|
|
10
|
+
* - `'valueEchoLost'`: the ECU stores the value, but its echo is lost.
|
|
11
|
+
* - `'valueEchoCorrupted'`: the ECU stores the value, but its echo is garbled
|
|
12
|
+
* on the way back.
|
|
13
|
+
*/
|
|
14
|
+
export type MemoryWriteFault = 'commandLost' | 'commandEchoCorrupted' | 'valueEchoLost' | 'valueEchoCorrupted';
|
|
2
15
|
/**
|
|
3
16
|
* Emulates the ECU's side of the serial protocol over an in-memory 64 KiB
|
|
4
17
|
* address space. Used for tests and for developing a UI without a car.
|
|
@@ -15,8 +28,34 @@ export declare class SimulatedTransport implements Transport {
|
|
|
15
28
|
silent: boolean;
|
|
16
29
|
/** Fault injection: reject every write. */
|
|
17
30
|
failWrites: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Fault injection: where a write failed by
|
|
33
|
+
* {@link SimulatedTransport.failMemoryWritesAfter} goes wrong. Defaults to
|
|
34
|
+
* `'commandLost'`.
|
|
35
|
+
*/
|
|
36
|
+
memoryWriteFault: MemoryWriteFault;
|
|
37
|
+
/**
|
|
38
|
+
* How long the ECU waits, with nothing received, before it drops a
|
|
39
|
+
* half-received command, in milliseconds. A read that times out counts as
|
|
40
|
+
* that much silence.
|
|
41
|
+
*/
|
|
42
|
+
commandTimeoutMs: number;
|
|
18
43
|
/** Fault injection: stream at most this many bytes per read command. */
|
|
19
44
|
streamLimit: number | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Fault injection: let this many memory writes succeed, then fail every
|
|
47
|
+
* later one, while reads keep working. `undefined` (the default) never
|
|
48
|
+
* fails; `0` fails the first. Setting it starts the count again.
|
|
49
|
+
*
|
|
50
|
+
* @returns The number of memory writes allowed to succeed.
|
|
51
|
+
*/
|
|
52
|
+
get failMemoryWritesAfter(): number | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Sets the memory-write fault limit and starts the count again.
|
|
55
|
+
*
|
|
56
|
+
* @param count - Memory writes to allow before failing, or `undefined` for none.
|
|
57
|
+
*/
|
|
58
|
+
set failMemoryWritesAfter(count: number | undefined);
|
|
20
59
|
/**
|
|
21
60
|
* Whether the transport is open.
|
|
22
61
|
*
|
|
@@ -55,11 +94,12 @@ export declare class SimulatedTransport implements Transport {
|
|
|
55
94
|
* Takes bytes from the simulated ECU's reply.
|
|
56
95
|
*
|
|
57
96
|
* @param length - Number of bytes wanted.
|
|
58
|
-
* @param
|
|
97
|
+
* @param timeoutMs - The simulation answers at once, but a read that times
|
|
98
|
+
* out counts as this much silence on the line.
|
|
59
99
|
* @returns Exactly `length` bytes.
|
|
60
100
|
* @throws {@link TimeoutError} if the ECU has not produced that many bytes.
|
|
61
101
|
* @throws {@link NotConnectedError} if the transport is closed.
|
|
62
102
|
*/
|
|
63
|
-
read(length: number,
|
|
103
|
+
read(length: number, timeoutMs: number): Promise<Uint8Array>;
|
|
64
104
|
}
|
|
65
105
|
//# sourceMappingURL=simulated.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"simulated.d.ts","sourceRoot":"","sources":["../../src/transport/simulated.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"simulated.d.ts","sourceRoot":"","sources":["../../src/transport/simulated.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAM5C;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,gBAAgB,GACxB,aAAa,GACb,sBAAsB,GACtB,eAAe,GACf,oBAAoB,CAAC;AA4BzB;;;GAGG;AACH,qBAAa,kBAAmB,YAAW,SAAS;;IAClD,sEAAsE;IACtE,QAAQ,CAAC,MAAM,0BAA+B;IAC9C,+CAA+C;IAC/C,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAM;IAEhC,oDAAoD;IACpD,eAAe,UAAS;IACxB,oCAAoC;IACpC,MAAM,UAAS;IACf,2CAA2C;IAC3C,UAAU,UAAS;IACnB;;;;OAIG;IACH,gBAAgB,EAAE,gBAAgB,CAAiB;IACnD;;;;OAIG;IACH,gBAAgB,SAAgC;IAChD,wEAAwE;IACxE,WAAW,EAAE,MAAM,GAAG,SAAS,CAAa;IAgB5C;;;;;;OAMG;IACH,IAAI,qBAAqB,IAAI,MAAM,GAAG,SAAS,CAE9C;IAED;;;;OAIG;IACH,IAAI,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAGlD;IAED;;;;OAIG;IACH,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAQhC;;;;OAIG;IACH,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAMrB;;;;OAIG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAStB;;;;;;;OAOG;IACH,KAAK,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBtC;;;;;;;;;OASG;IACH,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;CAoH7D"}
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
import { NotConnectedError, TimeoutError } from '../errors.js';
|
|
6
6
|
import { DataSize, MemoryOffset, ReadCountValue } from '../constants.js';
|
|
7
7
|
const MEMORY_SIZE = 0x10000;
|
|
8
|
+
/**
|
|
9
|
+
* How long the simulated ECU waits, with nothing received, before it drops a
|
|
10
|
+
* half-received command. The real firmware does this after 256 passes of its
|
|
11
|
+
* main loop; the time that takes is not documented and varies with load.
|
|
12
|
+
*/
|
|
13
|
+
const SIMULATED_COMMAND_TIMEOUT_MS = 250;
|
|
8
14
|
function lengthFromCode(code) {
|
|
9
15
|
if (code <= 0x0f) {
|
|
10
16
|
return code + 1;
|
|
@@ -37,6 +43,18 @@ export class SimulatedTransport {
|
|
|
37
43
|
silent = false;
|
|
38
44
|
/** Fault injection: reject every write. */
|
|
39
45
|
failWrites = false;
|
|
46
|
+
/**
|
|
47
|
+
* Fault injection: where a write failed by
|
|
48
|
+
* {@link SimulatedTransport.failMemoryWritesAfter} goes wrong. Defaults to
|
|
49
|
+
* `'commandLost'`.
|
|
50
|
+
*/
|
|
51
|
+
memoryWriteFault = 'commandLost';
|
|
52
|
+
/**
|
|
53
|
+
* How long the ECU waits, with nothing received, before it drops a
|
|
54
|
+
* half-received command, in milliseconds. A read that times out counts as
|
|
55
|
+
* that much silence.
|
|
56
|
+
*/
|
|
57
|
+
commandTimeoutMs = SIMULATED_COMMAND_TIMEOUT_MS;
|
|
40
58
|
/** Fault injection: stream at most this many bytes per read command. */
|
|
41
59
|
streamLimit = undefined;
|
|
42
60
|
#open = false;
|
|
@@ -47,6 +65,30 @@ export class SimulatedTransport {
|
|
|
47
65
|
#latched = false;
|
|
48
66
|
#lengthCode = 0;
|
|
49
67
|
#writeAddr = 0;
|
|
68
|
+
#failMemoryWritesAfter = undefined;
|
|
69
|
+
#memoryWritesDone = 0;
|
|
70
|
+
#failingWrite = false;
|
|
71
|
+
#lastByteAt = 0;
|
|
72
|
+
#quietMs = 0;
|
|
73
|
+
/**
|
|
74
|
+
* Fault injection: let this many memory writes succeed, then fail every
|
|
75
|
+
* later one, while reads keep working. `undefined` (the default) never
|
|
76
|
+
* fails; `0` fails the first. Setting it starts the count again.
|
|
77
|
+
*
|
|
78
|
+
* @returns The number of memory writes allowed to succeed.
|
|
79
|
+
*/
|
|
80
|
+
get failMemoryWritesAfter() {
|
|
81
|
+
return this.#failMemoryWritesAfter;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Sets the memory-write fault limit and starts the count again.
|
|
85
|
+
*
|
|
86
|
+
* @param count - Memory writes to allow before failing, or `undefined` for none.
|
|
87
|
+
*/
|
|
88
|
+
set failMemoryWritesAfter(count) {
|
|
89
|
+
this.#failMemoryWritesAfter = count;
|
|
90
|
+
this.#memoryWritesDone = 0;
|
|
91
|
+
}
|
|
50
92
|
/**
|
|
51
93
|
* Whether the transport is open.
|
|
52
94
|
*
|
|
@@ -113,22 +155,24 @@ export class SimulatedTransport {
|
|
|
113
155
|
* Takes bytes from the simulated ECU's reply.
|
|
114
156
|
*
|
|
115
157
|
* @param length - Number of bytes wanted.
|
|
116
|
-
* @param
|
|
158
|
+
* @param timeoutMs - The simulation answers at once, but a read that times
|
|
159
|
+
* out counts as this much silence on the line.
|
|
117
160
|
* @returns Exactly `length` bytes.
|
|
118
161
|
* @throws {@link TimeoutError} if the ECU has not produced that many bytes.
|
|
119
162
|
* @throws {@link NotConnectedError} if the transport is closed.
|
|
120
163
|
*/
|
|
121
|
-
read(length,
|
|
164
|
+
read(length, timeoutMs) {
|
|
122
165
|
if (!this.#open) {
|
|
123
166
|
return Promise.reject(new NotConnectedError('Transport is not open'));
|
|
124
167
|
}
|
|
125
168
|
if (this.#output.length < length) {
|
|
126
169
|
this.#output = [];
|
|
170
|
+
this.#quietMs += timeoutMs;
|
|
127
171
|
return Promise.reject(new TimeoutError('Simulated ECU is silent'));
|
|
128
172
|
}
|
|
129
173
|
return Promise.resolve(Uint8Array.from(this.#output.splice(0, length)));
|
|
130
174
|
}
|
|
131
|
-
#echo(byte) {
|
|
175
|
+
#echo(byte, garbled = false) {
|
|
132
176
|
if (this.silent) {
|
|
133
177
|
return;
|
|
134
178
|
}
|
|
@@ -137,13 +181,28 @@ export class SimulatedTransport {
|
|
|
137
181
|
this.#output.push(byte ^ 0xff);
|
|
138
182
|
}
|
|
139
183
|
else {
|
|
140
|
-
this.#output.push(byte);
|
|
184
|
+
this.#output.push(garbled ? byte ^ 0xff : byte);
|
|
141
185
|
}
|
|
142
186
|
}
|
|
143
187
|
#receive(byte) {
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
// Like the firmware, drop a half-received command after a long enough
|
|
190
|
+
// silence. The address already latched is kept.
|
|
191
|
+
if (this.#state !== 'idle' &&
|
|
192
|
+
now - this.#lastByteAt + this.#quietMs >= this.commandTimeoutMs) {
|
|
193
|
+
this.#state = 'idle';
|
|
194
|
+
}
|
|
195
|
+
this.#lastByteAt = now;
|
|
196
|
+
this.#quietMs = 0;
|
|
144
197
|
if (this.#state === 'writeValue') {
|
|
145
198
|
this.memory[this.#writeAddr] = byte;
|
|
146
|
-
this.#
|
|
199
|
+
if (!this.#failingWrite) {
|
|
200
|
+
this.#memoryWritesDone += 1;
|
|
201
|
+
this.#echo(byte);
|
|
202
|
+
}
|
|
203
|
+
else if (this.memoryWriteFault !== 'valueEchoLost') {
|
|
204
|
+
this.#echo(byte, this.memoryWriteFault === 'valueEchoCorrupted');
|
|
205
|
+
}
|
|
147
206
|
this.#state = 'idle';
|
|
148
207
|
this.#latched = false;
|
|
149
208
|
return;
|
|
@@ -169,11 +228,19 @@ export class SimulatedTransport {
|
|
|
169
228
|
this.#streamRead(this.#coarse | (byte & 0x3f));
|
|
170
229
|
}
|
|
171
230
|
else {
|
|
231
|
+
this.#failingWrite = this.#memoryWriteFails();
|
|
232
|
+
if (this.#failingWrite && this.memoryWriteFault === 'commandLost') {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
172
235
|
this.#writeAddr = this.#coarse | (byte & 0x3f);
|
|
173
|
-
this.#echo(byte);
|
|
236
|
+
this.#echo(byte, this.#failingWrite && this.memoryWriteFault === 'commandEchoCorrupted');
|
|
174
237
|
this.#state = 'writeValue';
|
|
175
238
|
}
|
|
176
239
|
}
|
|
240
|
+
#memoryWriteFails() {
|
|
241
|
+
return (this.#failMemoryWritesAfter !== undefined &&
|
|
242
|
+
this.#memoryWritesDone >= this.#failMemoryWritesAfter);
|
|
243
|
+
}
|
|
177
244
|
#streamRead(address) {
|
|
178
245
|
if (this.silent) {
|
|
179
246
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"simulated.js","sourceRoot":"","sources":["../../src/transport/simulated.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: GPL-3.0-only\n// Derived from libcomm14cux (https://github.com/colinbourassa/libcomm14cux)\n// Copyright (C) Colin Bourassa. Licensed under the GNU GPL v3.\n// Ported to TypeScript and modified for comm14cux-ts, 2026.\nimport { NotConnectedError, TimeoutError } from '../errors.js';\nimport { DataSize, MemoryOffset, ReadCountValue } from '../constants.js';\nimport type { Transport } from './types.js';\n\nconst MEMORY_SIZE = 0x10000;\n\ntype State = 'idle' | 'coarse2' | 'writeValue';\n\nfunction lengthFromCode(code: number): number | undefined {\n if (code <= 0x0f) {\n return code + 1;\n }\n\n switch (code) {\n case ReadCountValue.Count1:\n return 0x50;\n case ReadCountValue.Count2:\n return 0x64;\n case ReadCountValue.Count3:\n return 0x190;\n case ReadCountValue.Count4:\n return 0x200;\n default:\n return undefined;\n }\n}\n\n/**\n * Emulates the ECU's side of the serial protocol over an in-memory 64 KiB\n * address space. Used for tests and for developing a UI without a car.\n */\nexport class SimulatedTransport implements Transport {\n /** The ECU's address space. Tests may read and modify it directly. */\n readonly memory = new Uint8Array(MEMORY_SIZE);\n /** Every byte written to the ECU, in order. */\n readonly written: number[] = [];\n\n /** Fault injection: invert the next echoed byte. */\n corruptNextEcho = false;\n /** Fault injection: never reply. */\n silent = false;\n /** Fault injection: reject every write. */\n failWrites = false;\n /** Fault injection: stream at most this many bytes per read command. */\n streamLimit: number | undefined = undefined;\n\n #open = false;\n #state: State = 'idle';\n #output: number[] = [];\n #firstCoarse = 0;\n #coarse = 0;\n #latched = false;\n #lengthCode = 0;\n #writeAddr = 0;\n\n /**\n * Whether the transport is open.\n *\n * @returns `true` between {@link SimulatedTransport.open} and {@link SimulatedTransport.close}.\n */\n get isOpen(): boolean {\n return this.#open;\n }\n\n /**\n * Places a 16 KiB firmware image at 0xC000 to 0xFFFF.\n *\n * @param image - The ROM image, exactly 0x4000 bytes.\n * @throws {@link RangeError} if the image is the wrong size.\n */\n loadRom(image: Uint8Array): void {\n if (image.length !== DataSize.ROM) {\n throw new RangeError(`ROM image must be ${DataSize.ROM} bytes`);\n }\n\n this.memory.set(image, MemoryOffset.ROMAddress);\n }\n\n /**\n * Opens the simulated link.\n *\n * @returns A promise that resolves immediately.\n */\n open(): Promise<void> {\n this.#open = true;\n\n return Promise.resolve();\n }\n\n /**\n * Closes the simulated link and discards any pending ECU output.\n *\n * @returns A promise that resolves immediately.\n */\n close(): Promise<void> {\n this.#open = false;\n this.#state = 'idle';\n this.#latched = false;\n this.#output = [];\n\n return Promise.resolve();\n }\n\n /**\n * Delivers bytes to the simulated ECU, which may queue a reply.\n *\n * @param data - Bytes to send.\n * @returns A promise that resolves once the bytes are delivered.\n * @throws {@link NotConnectedError} if the transport is closed.\n * @throws Error if {@link SimulatedTransport.failWrites} is set.\n */\n write(data: Uint8Array): Promise<void> {\n if (!this.#open) {\n return Promise.reject(new NotConnectedError('Transport is not open'));\n }\n\n if (this.failWrites) {\n return Promise.reject(new Error('Simulated write failure'));\n }\n\n for (const byte of data) {\n this.written.push(byte);\n this.#receive(byte);\n }\n\n return Promise.resolve();\n }\n\n /**\n * Takes bytes from the simulated ECU's reply.\n *\n * @param length - Number of bytes wanted.\n * @param _timeoutMs - Ignored; the simulation answers immediately.\n * @returns Exactly `length` bytes.\n * @throws {@link TimeoutError} if the ECU has not produced that many bytes.\n * @throws {@link NotConnectedError} if the transport is closed.\n */\n read(length: number, _timeoutMs: number): Promise<Uint8Array> {\n if (!this.#open) {\n return Promise.reject(new NotConnectedError('Transport is not open'));\n }\n\n if (this.#output.length < length) {\n this.#output = [];\n\n return Promise.reject(new TimeoutError('Simulated ECU is silent'));\n }\n\n return Promise.resolve(Uint8Array.from(this.#output.splice(0, length)));\n }\n\n #echo(byte: number): void {\n if (this.silent) {\n return;\n }\n\n if (this.corruptNextEcho) {\n this.corruptNextEcho = false;\n this.#output.push(byte ^ 0xff);\n } else {\n this.#output.push(byte);\n }\n }\n\n #receive(byte: number): void {\n if (this.#state === 'writeValue') {\n this.memory[this.#writeAddr] = byte;\n this.#echo(byte);\n this.#state = 'idle';\n this.#latched = false;\n\n return;\n }\n\n if (this.#state === 'coarse2') {\n this.#coarse = ((this.#firstCoarse & 0x03) << 14) | (byte << 6);\n this.#lengthCode = (this.#firstCoarse >> 2) & 0x1f;\n this.#echo(byte);\n this.#state = 'idle';\n this.#latched = true;\n\n return;\n }\n\n if (byte < 0x80) {\n this.#firstCoarse = byte;\n this.#echo(byte);\n this.#state = 'coarse2';\n this.#latched = false;\n } else if (!this.#latched) {\n // A command byte without a preceding coarse address is ignored.\n } else if ((byte & 0xc0) === 0xc0) {\n this.#streamRead(this.#coarse | (byte & 0x3f));\n } else {\n this.#writeAddr = this.#coarse | (byte & 0x3f);\n this.#echo(byte);\n this.#state = 'writeValue';\n }\n }\n\n #streamRead(address: number): void {\n if (this.silent) {\n return;\n }\n\n let length = lengthFromCode(this.#lengthCode) ?? 0;\n\n if (this.streamLimit !== undefined) {\n length = Math.min(length, this.streamLimit);\n }\n\n for (let i = 0; i < length; i++) {\n this.#output.push(this.memory[(address + i) % MEMORY_SIZE] ?? 0);\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"simulated.js","sourceRoot":"","sources":["../../src/transport/simulated.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: GPL-3.0-only\n// Derived from libcomm14cux (https://github.com/colinbourassa/libcomm14cux)\n// Copyright (C) Colin Bourassa. Licensed under the GNU GPL v3.\n// Ported to TypeScript and modified for comm14cux-ts, 2026.\nimport { NotConnectedError, TimeoutError } from '../errors.js';\nimport { DataSize, MemoryOffset, ReadCountValue } from '../constants.js';\nimport type { Transport } from './types.js';\n\nconst MEMORY_SIZE = 0x10000;\n\ntype State = 'idle' | 'coarse2' | 'writeValue';\n\n/**\n * Where a memory write injected by {@link SimulatedTransport.failMemoryWritesAfter}\n * goes wrong:\n *\n * - `'commandLost'`: the write command never reaches the ECU. No echo, and\n * nothing is stored.\n * - `'commandEchoCorrupted'`: the ECU takes the write command, but its echo\n * is garbled on the way back. The ECU then waits for the value byte.\n * - `'valueEchoLost'`: the ECU stores the value, but its echo is lost.\n * - `'valueEchoCorrupted'`: the ECU stores the value, but its echo is garbled\n * on the way back.\n */\nexport type MemoryWriteFault =\n | 'commandLost'\n | 'commandEchoCorrupted'\n | 'valueEchoLost'\n | 'valueEchoCorrupted';\n\n/**\n * How long the simulated ECU waits, with nothing received, before it drops a\n * half-received command. The real firmware does this after 256 passes of its\n * main loop; the time that takes is not documented and varies with load.\n */\nconst SIMULATED_COMMAND_TIMEOUT_MS = 250;\n\nfunction lengthFromCode(code: number): number | undefined {\n if (code <= 0x0f) {\n return code + 1;\n }\n\n switch (code) {\n case ReadCountValue.Count1:\n return 0x50;\n case ReadCountValue.Count2:\n return 0x64;\n case ReadCountValue.Count3:\n return 0x190;\n case ReadCountValue.Count4:\n return 0x200;\n default:\n return undefined;\n }\n}\n\n/**\n * Emulates the ECU's side of the serial protocol over an in-memory 64 KiB\n * address space. Used for tests and for developing a UI without a car.\n */\nexport class SimulatedTransport implements Transport {\n /** The ECU's address space. Tests may read and modify it directly. */\n readonly memory = new Uint8Array(MEMORY_SIZE);\n /** Every byte written to the ECU, in order. */\n readonly written: number[] = [];\n\n /** Fault injection: invert the next echoed byte. */\n corruptNextEcho = false;\n /** Fault injection: never reply. */\n silent = false;\n /** Fault injection: reject every write. */\n failWrites = false;\n /**\n * Fault injection: where a write failed by\n * {@link SimulatedTransport.failMemoryWritesAfter} goes wrong. Defaults to\n * `'commandLost'`.\n */\n memoryWriteFault: MemoryWriteFault = 'commandLost';\n /**\n * How long the ECU waits, with nothing received, before it drops a\n * half-received command, in milliseconds. A read that times out counts as\n * that much silence.\n */\n commandTimeoutMs = SIMULATED_COMMAND_TIMEOUT_MS;\n /** Fault injection: stream at most this many bytes per read command. */\n streamLimit: number | undefined = undefined;\n\n #open = false;\n #state: State = 'idle';\n #output: number[] = [];\n #firstCoarse = 0;\n #coarse = 0;\n #latched = false;\n #lengthCode = 0;\n #writeAddr = 0;\n #failMemoryWritesAfter: number | undefined = undefined;\n #memoryWritesDone = 0;\n #failingWrite = false;\n #lastByteAt = 0;\n #quietMs = 0;\n\n /**\n * Fault injection: let this many memory writes succeed, then fail every\n * later one, while reads keep working. `undefined` (the default) never\n * fails; `0` fails the first. Setting it starts the count again.\n *\n * @returns The number of memory writes allowed to succeed.\n */\n get failMemoryWritesAfter(): number | undefined {\n return this.#failMemoryWritesAfter;\n }\n\n /**\n * Sets the memory-write fault limit and starts the count again.\n *\n * @param count - Memory writes to allow before failing, or `undefined` for none.\n */\n set failMemoryWritesAfter(count: number | undefined) {\n this.#failMemoryWritesAfter = count;\n this.#memoryWritesDone = 0;\n }\n\n /**\n * Whether the transport is open.\n *\n * @returns `true` between {@link SimulatedTransport.open} and {@link SimulatedTransport.close}.\n */\n get isOpen(): boolean {\n return this.#open;\n }\n\n /**\n * Places a 16 KiB firmware image at 0xC000 to 0xFFFF.\n *\n * @param image - The ROM image, exactly 0x4000 bytes.\n * @throws {@link RangeError} if the image is the wrong size.\n */\n loadRom(image: Uint8Array): void {\n if (image.length !== DataSize.ROM) {\n throw new RangeError(`ROM image must be ${DataSize.ROM} bytes`);\n }\n\n this.memory.set(image, MemoryOffset.ROMAddress);\n }\n\n /**\n * Opens the simulated link.\n *\n * @returns A promise that resolves immediately.\n */\n open(): Promise<void> {\n this.#open = true;\n\n return Promise.resolve();\n }\n\n /**\n * Closes the simulated link and discards any pending ECU output.\n *\n * @returns A promise that resolves immediately.\n */\n close(): Promise<void> {\n this.#open = false;\n this.#state = 'idle';\n this.#latched = false;\n this.#output = [];\n\n return Promise.resolve();\n }\n\n /**\n * Delivers bytes to the simulated ECU, which may queue a reply.\n *\n * @param data - Bytes to send.\n * @returns A promise that resolves once the bytes are delivered.\n * @throws {@link NotConnectedError} if the transport is closed.\n * @throws Error if {@link SimulatedTransport.failWrites} is set.\n */\n write(data: Uint8Array): Promise<void> {\n if (!this.#open) {\n return Promise.reject(new NotConnectedError('Transport is not open'));\n }\n\n if (this.failWrites) {\n return Promise.reject(new Error('Simulated write failure'));\n }\n\n for (const byte of data) {\n this.written.push(byte);\n this.#receive(byte);\n }\n\n return Promise.resolve();\n }\n\n /**\n * Takes bytes from the simulated ECU's reply.\n *\n * @param length - Number of bytes wanted.\n * @param timeoutMs - The simulation answers at once, but a read that times\n * out counts as this much silence on the line.\n * @returns Exactly `length` bytes.\n * @throws {@link TimeoutError} if the ECU has not produced that many bytes.\n * @throws {@link NotConnectedError} if the transport is closed.\n */\n read(length: number, timeoutMs: number): Promise<Uint8Array> {\n if (!this.#open) {\n return Promise.reject(new NotConnectedError('Transport is not open'));\n }\n\n if (this.#output.length < length) {\n this.#output = [];\n this.#quietMs += timeoutMs;\n\n return Promise.reject(new TimeoutError('Simulated ECU is silent'));\n }\n\n return Promise.resolve(Uint8Array.from(this.#output.splice(0, length)));\n }\n\n #echo(byte: number, garbled = false): void {\n if (this.silent) {\n return;\n }\n\n if (this.corruptNextEcho) {\n this.corruptNextEcho = false;\n this.#output.push(byte ^ 0xff);\n } else {\n this.#output.push(garbled ? byte ^ 0xff : byte);\n }\n }\n\n #receive(byte: number): void {\n const now = Date.now();\n\n // Like the firmware, drop a half-received command after a long enough\n // silence. The address already latched is kept.\n if (\n this.#state !== 'idle' &&\n now - this.#lastByteAt + this.#quietMs >= this.commandTimeoutMs\n ) {\n this.#state = 'idle';\n }\n\n this.#lastByteAt = now;\n this.#quietMs = 0;\n\n if (this.#state === 'writeValue') {\n this.memory[this.#writeAddr] = byte;\n\n if (!this.#failingWrite) {\n this.#memoryWritesDone += 1;\n this.#echo(byte);\n } else if (this.memoryWriteFault !== 'valueEchoLost') {\n this.#echo(byte, this.memoryWriteFault === 'valueEchoCorrupted');\n }\n\n this.#state = 'idle';\n this.#latched = false;\n\n return;\n }\n\n if (this.#state === 'coarse2') {\n this.#coarse = ((this.#firstCoarse & 0x03) << 14) | (byte << 6);\n this.#lengthCode = (this.#firstCoarse >> 2) & 0x1f;\n this.#echo(byte);\n this.#state = 'idle';\n this.#latched = true;\n\n return;\n }\n\n if (byte < 0x80) {\n this.#firstCoarse = byte;\n this.#echo(byte);\n this.#state = 'coarse2';\n this.#latched = false;\n } else if (!this.#latched) {\n // A command byte without a preceding coarse address is ignored.\n } else if ((byte & 0xc0) === 0xc0) {\n this.#streamRead(this.#coarse | (byte & 0x3f));\n } else {\n this.#failingWrite = this.#memoryWriteFails();\n\n if (this.#failingWrite && this.memoryWriteFault === 'commandLost') {\n return;\n }\n\n this.#writeAddr = this.#coarse | (byte & 0x3f);\n this.#echo(\n byte,\n this.#failingWrite && this.memoryWriteFault === 'commandEchoCorrupted',\n );\n this.#state = 'writeValue';\n }\n }\n\n #memoryWriteFails(): boolean {\n return (\n this.#failMemoryWritesAfter !== undefined &&\n this.#memoryWritesDone >= this.#failMemoryWritesAfter\n );\n }\n\n #streamRead(address: number): void {\n if (this.silent) {\n return;\n }\n\n let length = lengthFromCode(this.#lengthCode) ?? 0;\n\n if (this.streamLimit !== undefined) {\n length = Math.min(length, this.streamLimit);\n }\n\n for (let i = 0; i < length; i++) {\n this.#output.push(this.memory[(address + i) % MEMORY_SIZE] ?? 0);\n }\n }\n}\n"]}
|
package/dist/version.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
2
|
// Copyright (C) 2026 comm14cux-ts contributors
|
|
3
3
|
/** Updated by release-please; must match `version` in package.json (enforced by a test). */
|
|
4
|
-
const PACKAGE_VERSION = '0.2.
|
|
4
|
+
const PACKAGE_VERSION = '0.2.1'; // x-release-please-version
|
|
5
5
|
const [major = 0, minor = 0, patch = 0] = PACKAGE_VERSION.split('.').map(Number);
|
|
6
6
|
export /**
|
|
7
7
|
*
|
package/dist/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,+CAA+C;AAkB/C,4FAA4F;AAC5F,MAAM,eAAe,GAAG,OAAO,CAAC,CAAC,2BAA2B;AAE5D,MAAM,CAAC,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,GACrC,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AAEzC,MAAM,CAAC;;GAEJ,CACH,MAAM,eAAe,GAAY,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC","sourcesContent":["// SPDX-License-Identifier: GPL-3.0-only\n// Copyright (C) 2026 comm14cux-ts contributors\n\n/** Major/minor/patch version of this library (`c14cux_version`). */\nexport interface Version {\n /**\n * Major version number.\n */\n major: number;\n /**\n * Minor version number.\n */\n minor: number;\n /**\n * Patch version number.\n */\n patch: number;\n}\n\n/** Updated by release-please; must match `version` in package.json (enforced by a test). */\nconst PACKAGE_VERSION = '0.2.
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,+CAA+C;AAkB/C,4FAA4F;AAC5F,MAAM,eAAe,GAAG,OAAO,CAAC,CAAC,2BAA2B;AAE5D,MAAM,CAAC,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,GACrC,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AAEzC,MAAM,CAAC;;GAEJ,CACH,MAAM,eAAe,GAAY,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC","sourcesContent":["// SPDX-License-Identifier: GPL-3.0-only\n// Copyright (C) 2026 comm14cux-ts contributors\n\n/** Major/minor/patch version of this library (`c14cux_version`). */\nexport interface Version {\n /**\n * Major version number.\n */\n major: number;\n /**\n * Minor version number.\n */\n minor: number;\n /**\n * Patch version number.\n */\n patch: number;\n}\n\n/** Updated by release-please; must match `version` in package.json (enforced by a test). */\nconst PACKAGE_VERSION = '0.2.1'; // x-release-please-version\n\nconst [major = 0, minor = 0, patch = 0] =\n PACKAGE_VERSION.split('.').map(Number);\n\nexport /**\n *\n */\nconst LIBRARY_VERSION: Version = { major, minor, patch };\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kb1rma/libcomm14cux-ts",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1-beta.9",
|
|
4
4
|
"description": "TypeScript library for communicating with the Lucas 14CUX engine ECU over a serial port using the Web Serial API",
|
|
5
5
|
"license": "GPL-3.0-only",
|
|
6
6
|
"repository": {
|
package/src/constants.ts
CHANGED
|
@@ -173,3 +173,10 @@ export type PurgeValveState =
|
|
|
173
173
|
|
|
174
174
|
/** Default per-read silence timeout, matching libcomm14cux (100 ms). */
|
|
175
175
|
export const DEFAULT_READ_TIMEOUT_MS = 100;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Default quiet time after a failed command (500 ms). The ECU drops a
|
|
179
|
+
* half-received command after 256 passes of its main loop with nothing
|
|
180
|
+
* received; this is a conservative estimate of how long that takes.
|
|
181
|
+
*/
|
|
182
|
+
export const DEFAULT_COMMAND_RESET_MS = 500;
|
package/src/ecu.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Bank,
|
|
10
10
|
DataOffsetRev,
|
|
11
11
|
DataSize,
|
|
12
|
+
DEFAULT_COMMAND_RESET_MS,
|
|
12
13
|
DEFAULT_READ_TIMEOUT_MS,
|
|
13
14
|
FUEL_MAP_COLUMNS,
|
|
14
15
|
MemoryOffset,
|
|
@@ -76,6 +77,15 @@ export interface EcuOptions {
|
|
|
76
77
|
* libcomm14cux.
|
|
77
78
|
*/
|
|
78
79
|
readTimeoutMs?: number;
|
|
80
|
+
/**
|
|
81
|
+
* After a command fails part-way, the ECU may still be waiting for the rest
|
|
82
|
+
* of it, and would take the next byte sent as that rest (for a write, as the
|
|
83
|
+
* value to store). So before the next command, the `Ecu` waits until the
|
|
84
|
+
* line has been quiet this long, in milliseconds, discarding anything that
|
|
85
|
+
* arrives late. It should exceed the time the ECU takes to drop a
|
|
86
|
+
* half-received command. Defaults to 500.
|
|
87
|
+
*/
|
|
88
|
+
commandResetMs?: number;
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
/**
|
|
@@ -147,6 +157,7 @@ export class Ecu {
|
|
|
147
157
|
this.#protocol = new Protocol(
|
|
148
158
|
transport,
|
|
149
159
|
options.readTimeoutMs ?? DEFAULT_READ_TIMEOUT_MS,
|
|
160
|
+
options.commandResetMs ?? DEFAULT_COMMAND_RESET_MS,
|
|
150
161
|
);
|
|
151
162
|
}
|
|
152
163
|
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from './constants.js';
|
|
|
6
6
|
export * from './errors.js';
|
|
7
7
|
export type { Transport } from './transport/types.js';
|
|
8
8
|
export { SimulatedTransport } from './transport/simulated.js';
|
|
9
|
+
export type { MemoryWriteFault } from './transport/simulated.js';
|
|
9
10
|
export {
|
|
10
11
|
WebSerialTransport,
|
|
11
12
|
type WebSerialTransportOptions,
|
package/src/protocol/protocol.ts
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
// Derived from libcomm14cux (https://github.com/colinbourassa/libcomm14cux)
|
|
3
3
|
// Copyright (C) Colin Bourassa. Licensed under the GNU GPL v3.
|
|
4
4
|
// Ported to TypeScript and modified for comm14cux-ts, 2026.
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_COMMAND_RESET_MS,
|
|
7
|
+
DEFAULT_READ_TIMEOUT_MS,
|
|
8
|
+
} from '../constants.js';
|
|
9
|
+
import { ProtocolError, ReadCancelledError, TimeoutError } from '../errors.js';
|
|
7
10
|
import type { Transport } from '../transport/types.js';
|
|
8
11
|
import { nextRead } from './readCount.js';
|
|
9
12
|
|
|
@@ -11,6 +14,9 @@ const ADDRESS_SPACE = 0x10000;
|
|
|
11
14
|
const COARSE_WINDOW = 64;
|
|
12
15
|
// Writes use the coarse-address command with a length code of 0.
|
|
13
16
|
const WRITE_LENGTH_CODE = 0;
|
|
17
|
+
// The longest the ECU can keep talking after a failed command: a whole
|
|
18
|
+
// 512-byte read reply, plus a few echoes.
|
|
19
|
+
const MAX_LATE_BYTES = 0x200 + 4;
|
|
14
20
|
|
|
15
21
|
/**
|
|
16
22
|
* Memory read and write commands over a `Transport`
|
|
@@ -23,6 +29,8 @@ const WRITE_LENGTH_CODE = 0;
|
|
|
23
29
|
export class Protocol {
|
|
24
30
|
readonly #transport: Transport;
|
|
25
31
|
readonly #timeoutMs: number;
|
|
32
|
+
readonly #commandResetMs: number;
|
|
33
|
+
#resyncNeeded = false;
|
|
26
34
|
#lastReadCoarseAddress = 0;
|
|
27
35
|
#lastReadQuantity = 0;
|
|
28
36
|
|
|
@@ -31,10 +39,17 @@ export class Protocol {
|
|
|
31
39
|
*
|
|
32
40
|
* @param transport - The link to the ECU.
|
|
33
41
|
* @param timeoutMs - Silence timeout for each read, in milliseconds.
|
|
42
|
+
* @param commandResetMs - Quiet time to wait for after a failed command,
|
|
43
|
+
* in milliseconds.
|
|
34
44
|
*/
|
|
35
|
-
constructor(
|
|
45
|
+
constructor(
|
|
46
|
+
transport: Transport,
|
|
47
|
+
timeoutMs = DEFAULT_READ_TIMEOUT_MS,
|
|
48
|
+
commandResetMs = DEFAULT_COMMAND_RESET_MS,
|
|
49
|
+
) {
|
|
36
50
|
this.#transport = transport;
|
|
37
51
|
this.#timeoutMs = timeoutMs;
|
|
52
|
+
this.#commandResetMs = commandResetMs;
|
|
38
53
|
}
|
|
39
54
|
|
|
40
55
|
/** Forgets the last coarse address, forcing the next read to set it. */
|
|
@@ -74,6 +89,8 @@ export class Protocol {
|
|
|
74
89
|
let totalRead = 0;
|
|
75
90
|
|
|
76
91
|
try {
|
|
92
|
+
await this.#resync();
|
|
93
|
+
|
|
77
94
|
while (totalRead < length) {
|
|
78
95
|
if (multiChunk && isCancelled()) {
|
|
79
96
|
throw new ReadCancelledError('Read cancelled');
|
|
@@ -105,6 +122,8 @@ export class Protocol {
|
|
|
105
122
|
}
|
|
106
123
|
} catch (error) {
|
|
107
124
|
this.resetCache();
|
|
125
|
+
// Cancellation happens between chunks, when the ECU is idle.
|
|
126
|
+
this.#resyncNeeded ||= !(error instanceof ReadCancelledError);
|
|
108
127
|
throw error;
|
|
109
128
|
}
|
|
110
129
|
|
|
@@ -128,9 +147,16 @@ export class Protocol {
|
|
|
128
147
|
}
|
|
129
148
|
|
|
130
149
|
this.resetCache();
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
await this.#resync();
|
|
153
|
+
await this.setCoarseAddr(addr, WRITE_LENGTH_CODE);
|
|
154
|
+
await this.#sendEchoed(0x80 | (addr & 0x3f));
|
|
155
|
+
await this.#sendEchoed(value);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
this.#resyncNeeded = true;
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
134
160
|
}
|
|
135
161
|
|
|
136
162
|
/**
|
|
@@ -147,6 +173,36 @@ export class Protocol {
|
|
|
147
173
|
await this.#sendEchoed((addr >> 6) & 0xff);
|
|
148
174
|
}
|
|
149
175
|
|
|
176
|
+
/**
|
|
177
|
+
* After a failed command the ECU may still be waiting for the rest of it,
|
|
178
|
+
* and would take the next byte sent as that rest: for a write, as the value
|
|
179
|
+
* to store. Waits until the line has been quiet long enough for the ECU to
|
|
180
|
+
* drop the half-received command, discarding any late bytes.
|
|
181
|
+
*
|
|
182
|
+
* @throws {@link ProtocolError} if the ECU never goes quiet.
|
|
183
|
+
*/
|
|
184
|
+
async #resync(): Promise<void> {
|
|
185
|
+
if (!this.#resyncNeeded) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (let i = 0; i <= MAX_LATE_BYTES; i++) {
|
|
190
|
+
try {
|
|
191
|
+
await this.#transport.read(1, this.#commandResetMs);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (!(error instanceof TimeoutError)) {
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this.#resyncNeeded = false;
|
|
198
|
+
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
throw new ProtocolError('ECU did not go quiet after a failed command');
|
|
204
|
+
}
|
|
205
|
+
|
|
150
206
|
async #sendEchoed(byte: number): Promise<void> {
|
|
151
207
|
await this.#transport.write(Uint8Array.of(byte));
|
|
152
208
|
|
|
@@ -10,6 +10,31 @@ const MEMORY_SIZE = 0x10000;
|
|
|
10
10
|
|
|
11
11
|
type State = 'idle' | 'coarse2' | 'writeValue';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Where a memory write injected by {@link SimulatedTransport.failMemoryWritesAfter}
|
|
15
|
+
* goes wrong:
|
|
16
|
+
*
|
|
17
|
+
* - `'commandLost'`: the write command never reaches the ECU. No echo, and
|
|
18
|
+
* nothing is stored.
|
|
19
|
+
* - `'commandEchoCorrupted'`: the ECU takes the write command, but its echo
|
|
20
|
+
* is garbled on the way back. The ECU then waits for the value byte.
|
|
21
|
+
* - `'valueEchoLost'`: the ECU stores the value, but its echo is lost.
|
|
22
|
+
* - `'valueEchoCorrupted'`: the ECU stores the value, but its echo is garbled
|
|
23
|
+
* on the way back.
|
|
24
|
+
*/
|
|
25
|
+
export type MemoryWriteFault =
|
|
26
|
+
| 'commandLost'
|
|
27
|
+
| 'commandEchoCorrupted'
|
|
28
|
+
| 'valueEchoLost'
|
|
29
|
+
| 'valueEchoCorrupted';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* How long the simulated ECU waits, with nothing received, before it drops a
|
|
33
|
+
* half-received command. The real firmware does this after 256 passes of its
|
|
34
|
+
* main loop; the time that takes is not documented and varies with load.
|
|
35
|
+
*/
|
|
36
|
+
const SIMULATED_COMMAND_TIMEOUT_MS = 250;
|
|
37
|
+
|
|
13
38
|
function lengthFromCode(code: number): number | undefined {
|
|
14
39
|
if (code <= 0x0f) {
|
|
15
40
|
return code + 1;
|
|
@@ -45,6 +70,18 @@ export class SimulatedTransport implements Transport {
|
|
|
45
70
|
silent = false;
|
|
46
71
|
/** Fault injection: reject every write. */
|
|
47
72
|
failWrites = false;
|
|
73
|
+
/**
|
|
74
|
+
* Fault injection: where a write failed by
|
|
75
|
+
* {@link SimulatedTransport.failMemoryWritesAfter} goes wrong. Defaults to
|
|
76
|
+
* `'commandLost'`.
|
|
77
|
+
*/
|
|
78
|
+
memoryWriteFault: MemoryWriteFault = 'commandLost';
|
|
79
|
+
/**
|
|
80
|
+
* How long the ECU waits, with nothing received, before it drops a
|
|
81
|
+
* half-received command, in milliseconds. A read that times out counts as
|
|
82
|
+
* that much silence.
|
|
83
|
+
*/
|
|
84
|
+
commandTimeoutMs = SIMULATED_COMMAND_TIMEOUT_MS;
|
|
48
85
|
/** Fault injection: stream at most this many bytes per read command. */
|
|
49
86
|
streamLimit: number | undefined = undefined;
|
|
50
87
|
|
|
@@ -56,6 +93,32 @@ export class SimulatedTransport implements Transport {
|
|
|
56
93
|
#latched = false;
|
|
57
94
|
#lengthCode = 0;
|
|
58
95
|
#writeAddr = 0;
|
|
96
|
+
#failMemoryWritesAfter: number | undefined = undefined;
|
|
97
|
+
#memoryWritesDone = 0;
|
|
98
|
+
#failingWrite = false;
|
|
99
|
+
#lastByteAt = 0;
|
|
100
|
+
#quietMs = 0;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Fault injection: let this many memory writes succeed, then fail every
|
|
104
|
+
* later one, while reads keep working. `undefined` (the default) never
|
|
105
|
+
* fails; `0` fails the first. Setting it starts the count again.
|
|
106
|
+
*
|
|
107
|
+
* @returns The number of memory writes allowed to succeed.
|
|
108
|
+
*/
|
|
109
|
+
get failMemoryWritesAfter(): number | undefined {
|
|
110
|
+
return this.#failMemoryWritesAfter;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Sets the memory-write fault limit and starts the count again.
|
|
115
|
+
*
|
|
116
|
+
* @param count - Memory writes to allow before failing, or `undefined` for none.
|
|
117
|
+
*/
|
|
118
|
+
set failMemoryWritesAfter(count: number | undefined) {
|
|
119
|
+
this.#failMemoryWritesAfter = count;
|
|
120
|
+
this.#memoryWritesDone = 0;
|
|
121
|
+
}
|
|
59
122
|
|
|
60
123
|
/**
|
|
61
124
|
* Whether the transport is open.
|
|
@@ -134,18 +197,20 @@ export class SimulatedTransport implements Transport {
|
|
|
134
197
|
* Takes bytes from the simulated ECU's reply.
|
|
135
198
|
*
|
|
136
199
|
* @param length - Number of bytes wanted.
|
|
137
|
-
* @param
|
|
200
|
+
* @param timeoutMs - The simulation answers at once, but a read that times
|
|
201
|
+
* out counts as this much silence on the line.
|
|
138
202
|
* @returns Exactly `length` bytes.
|
|
139
203
|
* @throws {@link TimeoutError} if the ECU has not produced that many bytes.
|
|
140
204
|
* @throws {@link NotConnectedError} if the transport is closed.
|
|
141
205
|
*/
|
|
142
|
-
read(length: number,
|
|
206
|
+
read(length: number, timeoutMs: number): Promise<Uint8Array> {
|
|
143
207
|
if (!this.#open) {
|
|
144
208
|
return Promise.reject(new NotConnectedError('Transport is not open'));
|
|
145
209
|
}
|
|
146
210
|
|
|
147
211
|
if (this.#output.length < length) {
|
|
148
212
|
this.#output = [];
|
|
213
|
+
this.#quietMs += timeoutMs;
|
|
149
214
|
|
|
150
215
|
return Promise.reject(new TimeoutError('Simulated ECU is silent'));
|
|
151
216
|
}
|
|
@@ -153,7 +218,7 @@ export class SimulatedTransport implements Transport {
|
|
|
153
218
|
return Promise.resolve(Uint8Array.from(this.#output.splice(0, length)));
|
|
154
219
|
}
|
|
155
220
|
|
|
156
|
-
#echo(byte: number): void {
|
|
221
|
+
#echo(byte: number, garbled = false): void {
|
|
157
222
|
if (this.silent) {
|
|
158
223
|
return;
|
|
159
224
|
}
|
|
@@ -162,14 +227,35 @@ export class SimulatedTransport implements Transport {
|
|
|
162
227
|
this.corruptNextEcho = false;
|
|
163
228
|
this.#output.push(byte ^ 0xff);
|
|
164
229
|
} else {
|
|
165
|
-
this.#output.push(byte);
|
|
230
|
+
this.#output.push(garbled ? byte ^ 0xff : byte);
|
|
166
231
|
}
|
|
167
232
|
}
|
|
168
233
|
|
|
169
234
|
#receive(byte: number): void {
|
|
235
|
+
const now = Date.now();
|
|
236
|
+
|
|
237
|
+
// Like the firmware, drop a half-received command after a long enough
|
|
238
|
+
// silence. The address already latched is kept.
|
|
239
|
+
if (
|
|
240
|
+
this.#state !== 'idle' &&
|
|
241
|
+
now - this.#lastByteAt + this.#quietMs >= this.commandTimeoutMs
|
|
242
|
+
) {
|
|
243
|
+
this.#state = 'idle';
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
this.#lastByteAt = now;
|
|
247
|
+
this.#quietMs = 0;
|
|
248
|
+
|
|
170
249
|
if (this.#state === 'writeValue') {
|
|
171
250
|
this.memory[this.#writeAddr] = byte;
|
|
172
|
-
|
|
251
|
+
|
|
252
|
+
if (!this.#failingWrite) {
|
|
253
|
+
this.#memoryWritesDone += 1;
|
|
254
|
+
this.#echo(byte);
|
|
255
|
+
} else if (this.memoryWriteFault !== 'valueEchoLost') {
|
|
256
|
+
this.#echo(byte, this.memoryWriteFault === 'valueEchoCorrupted');
|
|
257
|
+
}
|
|
258
|
+
|
|
173
259
|
this.#state = 'idle';
|
|
174
260
|
this.#latched = false;
|
|
175
261
|
|
|
@@ -196,12 +282,28 @@ export class SimulatedTransport implements Transport {
|
|
|
196
282
|
} else if ((byte & 0xc0) === 0xc0) {
|
|
197
283
|
this.#streamRead(this.#coarse | (byte & 0x3f));
|
|
198
284
|
} else {
|
|
285
|
+
this.#failingWrite = this.#memoryWriteFails();
|
|
286
|
+
|
|
287
|
+
if (this.#failingWrite && this.memoryWriteFault === 'commandLost') {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
199
291
|
this.#writeAddr = this.#coarse | (byte & 0x3f);
|
|
200
|
-
this.#echo(
|
|
292
|
+
this.#echo(
|
|
293
|
+
byte,
|
|
294
|
+
this.#failingWrite && this.memoryWriteFault === 'commandEchoCorrupted',
|
|
295
|
+
);
|
|
201
296
|
this.#state = 'writeValue';
|
|
202
297
|
}
|
|
203
298
|
}
|
|
204
299
|
|
|
300
|
+
#memoryWriteFails(): boolean {
|
|
301
|
+
return (
|
|
302
|
+
this.#failMemoryWritesAfter !== undefined &&
|
|
303
|
+
this.#memoryWritesDone >= this.#failMemoryWritesAfter
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
205
307
|
#streamRead(address: number): void {
|
|
206
308
|
if (this.silent) {
|
|
207
309
|
return;
|