@kb1rma/libcomm14cux-ts 0.0.0
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/LICENSE +674 -0
- package/README.md +154 -0
- package/dist/bytes.d.ts +9 -0
- package/dist/bytes.d.ts.map +1 -0
- package/dist/bytes.js +13 -0
- package/dist/bytes.js.map +1 -0
- package/dist/constants.d.ts +174 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +122 -0
- package/dist/constants.js.map +1 -0
- package/dist/decoders/airflow.d.ts +19 -0
- package/dist/decoders/airflow.d.ts.map +1 -0
- package/dist/decoders/airflow.js +36 -0
- package/dist/decoders/airflow.js.map +1 -0
- package/dist/decoders/engineSpeed.d.ts +20 -0
- package/dist/decoders/engineSpeed.d.ts.map +1 -0
- package/dist/decoders/engineSpeed.js +33 -0
- package/dist/decoders/engineSpeed.js.map +1 -0
- package/dist/decoders/faultCodes.d.ts +19 -0
- package/dist/decoders/faultCodes.d.ts.map +1 -0
- package/dist/decoders/faultCodes.js +48 -0
- package/dist/decoders/faultCodes.js.map +1 -0
- package/dist/decoders/flags.d.ts +45 -0
- package/dist/decoders/flags.d.ts.map +1 -0
- package/dist/decoders/flags.js +54 -0
- package/dist/decoders/flags.js.map +1 -0
- package/dist/decoders/fuelMap.d.ts +89 -0
- package/dist/decoders/fuelMap.d.ts.map +1 -0
- package/dist/decoders/fuelMap.js +109 -0
- package/dist/decoders/fuelMap.js.map +1 -0
- package/dist/decoders/gear.d.ts +10 -0
- package/dist/decoders/gear.d.ts.map +1 -0
- package/dist/decoders/gear.js +23 -0
- package/dist/decoders/gear.js.map +1 -0
- package/dist/decoders/idleBypass.d.ts +8 -0
- package/dist/decoders/idleBypass.d.ts.map +1 -0
- package/dist/decoders/idleBypass.js +15 -0
- package/dist/decoders/idleBypass.js.map +1 -0
- package/dist/decoders/lambdaTrim.d.ts +15 -0
- package/dist/decoders/lambdaTrim.d.ts.map +1 -0
- package/dist/decoders/lambdaTrim.js +23 -0
- package/dist/decoders/lambdaTrim.js.map +1 -0
- package/dist/decoders/mainVoltage.d.ts +14 -0
- package/dist/decoders/mainVoltage.d.ts.map +1 -0
- package/dist/decoders/mainVoltage.js +26 -0
- package/dist/decoders/mainVoltage.js.map +1 -0
- package/dist/decoders/roadSpeed.d.ts +8 -0
- package/dist/decoders/roadSpeed.d.ts.map +1 -0
- package/dist/decoders/roadSpeed.js +14 -0
- package/dist/decoders/roadSpeed.js.map +1 -0
- package/dist/decoders/romRevision.d.ts +18 -0
- package/dist/decoders/romRevision.d.ts.map +1 -0
- package/dist/decoders/romRevision.js +26 -0
- package/dist/decoders/romRevision.js.map +1 -0
- package/dist/decoders/temperature.d.ts +12 -0
- package/dist/decoders/temperature.d.ts.map +1 -0
- package/dist/decoders/temperature.js +39 -0
- package/dist/decoders/temperature.js.map +1 -0
- package/dist/decoders/throttle.d.ts +18 -0
- package/dist/decoders/throttle.d.ts.map +1 -0
- package/dist/decoders/throttle.js +33 -0
- package/dist/decoders/throttle.js.map +1 -0
- package/dist/decoders/tuneRevision.d.ts +25 -0
- package/dist/decoders/tuneRevision.d.ts.map +1 -0
- package/dist/decoders/tuneRevision.js +22 -0
- package/dist/decoders/tuneRevision.js.map +1 -0
- package/dist/ecu.d.ts +473 -0
- package/dist/ecu.d.ts.map +1 -0
- package/dist/ecu.js +700 -0
- package/dist/ecu.js.map +1 -0
- package/dist/errors.d.ts +31 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +33 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol/protocol.d.ts +57 -0
- package/dist/protocol/protocol.d.ts.map +1 -0
- package/dist/protocol/protocol.js +137 -0
- package/dist/protocol/protocol.js.map +1 -0
- package/dist/protocol/readCount.d.ts +21 -0
- package/dist/protocol/readCount.d.ts.map +1 -0
- package/dist/protocol/readCount.js +30 -0
- package/dist/protocol/readCount.js.map +1 -0
- package/dist/queue.d.ts +15 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +21 -0
- package/dist/queue.js.map +1 -0
- package/dist/transport/simulated.d.ts +65 -0
- package/dist/transport/simulated.d.ts.map +1 -0
- package/dist/transport/simulated.js +190 -0
- package/dist/transport/simulated.js.map +1 -0
- package/dist/transport/types.d.ts +9 -0
- package/dist/transport/types.d.ts.map +1 -0
- package/dist/transport/types.js +4 -0
- package/dist/transport/types.js.map +1 -0
- package/dist/transport/webSerial.d.ts +49 -0
- package/dist/transport/webSerial.d.ts.map +1 -0
- package/dist/transport/webSerial.js +129 -0
- package/dist/transport/webSerial.js.map +1 -0
- package/dist/version.d.ts +18 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +58 -0
- package/src/bytes.ts +13 -0
- package/src/constants.ts +175 -0
- package/src/decoders/airflow.ts +41 -0
- package/src/decoders/engineSpeed.ts +36 -0
- package/src/decoders/faultCodes.ts +61 -0
- package/src/decoders/flags.ts +64 -0
- package/src/decoders/fuelMap.ts +167 -0
- package/src/decoders/gear.ts +26 -0
- package/src/decoders/idleBypass.ts +16 -0
- package/src/decoders/lambdaTrim.ts +24 -0
- package/src/decoders/mainVoltage.ts +37 -0
- package/src/decoders/roadSpeed.ts +14 -0
- package/src/decoders/romRevision.ts +30 -0
- package/src/decoders/temperature.ts +42 -0
- package/src/decoders/throttle.ts +37 -0
- package/src/decoders/tuneRevision.ts +42 -0
- package/src/ecu.ts +948 -0
- package/src/errors.ts +38 -0
- package/src/index.ts +16 -0
- package/src/protocol/protocol.ts +167 -0
- package/src/protocol/readCount.ts +45 -0
- package/src/queue.ts +24 -0
- package/src/test-support/ecu.ts +126 -0
- package/src/transport/simulated.ts +220 -0
- package/src/transport/types.ts +11 -0
- package/src/transport/webSerial.ts +163 -0
- package/src/version.ts +21 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
// Derived from libcomm14cux (https://github.com/colinbourassa/libcomm14cux)
|
|
3
|
+
// Copyright (C) Colin Bourassa. Licensed under the GNU GPL v3.
|
|
4
|
+
// Ported to TypeScript and modified for comm14cux-ts, 2026.
|
|
5
|
+
import { NotConnectedError, TimeoutError } from '../errors.js';
|
|
6
|
+
import { DataSize, MemoryOffset, ReadCountValue } from '../constants.js';
|
|
7
|
+
const MEMORY_SIZE = 0x10000;
|
|
8
|
+
function lengthFromCode(code) {
|
|
9
|
+
if (code <= 0x0f) {
|
|
10
|
+
return code + 1;
|
|
11
|
+
}
|
|
12
|
+
switch (code) {
|
|
13
|
+
case ReadCountValue.Count1:
|
|
14
|
+
return 0x50;
|
|
15
|
+
case ReadCountValue.Count2:
|
|
16
|
+
return 0x64;
|
|
17
|
+
case ReadCountValue.Count3:
|
|
18
|
+
return 0x190;
|
|
19
|
+
case ReadCountValue.Count4:
|
|
20
|
+
return 0x200;
|
|
21
|
+
default:
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Emulates the ECU's side of the serial protocol over an in-memory 64 KiB
|
|
27
|
+
* address space. Used for tests and for developing a UI without a car.
|
|
28
|
+
*/
|
|
29
|
+
export class SimulatedTransport {
|
|
30
|
+
/** The ECU's address space. Tests may read and modify it directly. */
|
|
31
|
+
memory = new Uint8Array(MEMORY_SIZE);
|
|
32
|
+
/** Every byte written to the ECU, in order. */
|
|
33
|
+
written = [];
|
|
34
|
+
/** Fault injection: invert the next echoed byte. */
|
|
35
|
+
corruptNextEcho = false;
|
|
36
|
+
/** Fault injection: never reply. */
|
|
37
|
+
silent = false;
|
|
38
|
+
/** Fault injection: reject every write. */
|
|
39
|
+
failWrites = false;
|
|
40
|
+
/** Fault injection: stream at most this many bytes per read command. */
|
|
41
|
+
streamLimit = undefined;
|
|
42
|
+
#open = false;
|
|
43
|
+
#state = 'idle';
|
|
44
|
+
#output = [];
|
|
45
|
+
#firstCoarse = 0;
|
|
46
|
+
#coarse = 0;
|
|
47
|
+
#latched = false;
|
|
48
|
+
#lengthCode = 0;
|
|
49
|
+
#writeAddr = 0;
|
|
50
|
+
/**
|
|
51
|
+
* Whether the transport is open.
|
|
52
|
+
*
|
|
53
|
+
* @returns `true` between {@link SimulatedTransport.open} and {@link SimulatedTransport.close}.
|
|
54
|
+
*/
|
|
55
|
+
get isOpen() {
|
|
56
|
+
return this.#open;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Places a 16 KiB firmware image at 0xC000 to 0xFFFF.
|
|
60
|
+
*
|
|
61
|
+
* @param image - The ROM image, exactly 0x4000 bytes.
|
|
62
|
+
* @throws {@link RangeError} if the image is the wrong size.
|
|
63
|
+
*/
|
|
64
|
+
loadRom(image) {
|
|
65
|
+
if (image.length !== DataSize.ROM) {
|
|
66
|
+
throw new RangeError(`ROM image must be ${DataSize.ROM} bytes`);
|
|
67
|
+
}
|
|
68
|
+
this.memory.set(image, MemoryOffset.ROMAddress);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Opens the simulated link.
|
|
72
|
+
*
|
|
73
|
+
* @returns A promise that resolves immediately.
|
|
74
|
+
*/
|
|
75
|
+
open() {
|
|
76
|
+
this.#open = true;
|
|
77
|
+
return Promise.resolve();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Closes the simulated link and discards any pending ECU output.
|
|
81
|
+
*
|
|
82
|
+
* @returns A promise that resolves immediately.
|
|
83
|
+
*/
|
|
84
|
+
close() {
|
|
85
|
+
this.#open = false;
|
|
86
|
+
this.#state = 'idle';
|
|
87
|
+
this.#latched = false;
|
|
88
|
+
this.#output = [];
|
|
89
|
+
return Promise.resolve();
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Delivers bytes to the simulated ECU, which may queue a reply.
|
|
93
|
+
*
|
|
94
|
+
* @param data - Bytes to send.
|
|
95
|
+
* @returns A promise that resolves once the bytes are delivered.
|
|
96
|
+
* @throws {@link NotConnectedError} if the transport is closed.
|
|
97
|
+
* @throws Error if {@link SimulatedTransport.failWrites} is set.
|
|
98
|
+
*/
|
|
99
|
+
write(data) {
|
|
100
|
+
if (!this.#open) {
|
|
101
|
+
return Promise.reject(new NotConnectedError('Transport is not open'));
|
|
102
|
+
}
|
|
103
|
+
if (this.failWrites) {
|
|
104
|
+
return Promise.reject(new Error('Simulated write failure'));
|
|
105
|
+
}
|
|
106
|
+
for (const byte of data) {
|
|
107
|
+
this.written.push(byte);
|
|
108
|
+
this.#receive(byte);
|
|
109
|
+
}
|
|
110
|
+
return Promise.resolve();
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Takes bytes from the simulated ECU's reply.
|
|
114
|
+
*
|
|
115
|
+
* @param length - Number of bytes wanted.
|
|
116
|
+
* @param _timeoutMs - Ignored; the simulation answers immediately.
|
|
117
|
+
* @returns Exactly `length` bytes.
|
|
118
|
+
* @throws {@link TimeoutError} if the ECU has not produced that many bytes.
|
|
119
|
+
* @throws {@link NotConnectedError} if the transport is closed.
|
|
120
|
+
*/
|
|
121
|
+
read(length, _timeoutMs) {
|
|
122
|
+
if (!this.#open) {
|
|
123
|
+
return Promise.reject(new NotConnectedError('Transport is not open'));
|
|
124
|
+
}
|
|
125
|
+
if (this.#output.length < length) {
|
|
126
|
+
this.#output = [];
|
|
127
|
+
return Promise.reject(new TimeoutError('Simulated ECU is silent'));
|
|
128
|
+
}
|
|
129
|
+
return Promise.resolve(Uint8Array.from(this.#output.splice(0, length)));
|
|
130
|
+
}
|
|
131
|
+
#echo(byte) {
|
|
132
|
+
if (this.silent) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (this.corruptNextEcho) {
|
|
136
|
+
this.corruptNextEcho = false;
|
|
137
|
+
this.#output.push(byte ^ 0xff);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
this.#output.push(byte);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
#receive(byte) {
|
|
144
|
+
if (this.#state === 'writeValue') {
|
|
145
|
+
this.memory[this.#writeAddr] = byte;
|
|
146
|
+
this.#echo(byte);
|
|
147
|
+
this.#state = 'idle';
|
|
148
|
+
this.#latched = false;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (this.#state === 'coarse2') {
|
|
152
|
+
this.#coarse = ((this.#firstCoarse & 0x03) << 14) | (byte << 6);
|
|
153
|
+
this.#lengthCode = (this.#firstCoarse >> 2) & 0x1f;
|
|
154
|
+
this.#echo(byte);
|
|
155
|
+
this.#state = 'idle';
|
|
156
|
+
this.#latched = true;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (byte < 0x80) {
|
|
160
|
+
this.#firstCoarse = byte;
|
|
161
|
+
this.#echo(byte);
|
|
162
|
+
this.#state = 'coarse2';
|
|
163
|
+
this.#latched = false;
|
|
164
|
+
}
|
|
165
|
+
else if (!this.#latched) {
|
|
166
|
+
// A command byte without a preceding coarse address is ignored.
|
|
167
|
+
}
|
|
168
|
+
else if ((byte & 0xc0) === 0xc0) {
|
|
169
|
+
this.#streamRead(this.#coarse | (byte & 0x3f));
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
this.#writeAddr = this.#coarse | (byte & 0x3f);
|
|
173
|
+
this.#echo(byte);
|
|
174
|
+
this.#state = 'writeValue';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
#streamRead(address) {
|
|
178
|
+
if (this.silent) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
let length = lengthFromCode(this.#lengthCode) ?? 0;
|
|
182
|
+
if (this.streamLimit !== undefined) {
|
|
183
|
+
length = Math.min(length, this.streamLimit);
|
|
184
|
+
}
|
|
185
|
+
for (let i = 0; i < length; i++) {
|
|
186
|
+
this.#output.push(this.memory[(address + i) % MEMORY_SIZE] ?? 0);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=simulated.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Byte-level link to the ECU. The protocol layer uses nothing else. */
|
|
2
|
+
export interface Transport {
|
|
3
|
+
open(): Promise<void>;
|
|
4
|
+
close(): Promise<void>;
|
|
5
|
+
write(data: Uint8Array): Promise<void>;
|
|
6
|
+
/** Resolves with exactly `length` bytes, or rejects with a TimeoutError. */
|
|
7
|
+
read(length: number, timeoutMs: number): Promise<Uint8Array>;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/transport/types.ts"],"names":[],"mappings":"AAGA,wEAAwE;AACxE,MAAM,WAAW,SAAS;IACxB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,KAAK,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,4EAA4E;IAC5E,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CAC9D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/transport/types.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,+CAA+C","sourcesContent":["// SPDX-License-Identifier: GPL-3.0-only\n// Copyright (C) 2026 comm14cux-ts contributors\n\n/** Byte-level link to the ECU. The protocol layer uses nothing else. */\nexport interface Transport {\n open(): Promise<void>;\n close(): Promise<void>;\n write(data: Uint8Array): Promise<void>;\n /** Resolves with exactly `length` bytes, or rejects with a TimeoutError. */\n read(length: number, timeoutMs: number): Promise<Uint8Array>;\n}\n"]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Transport } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Options for {@link WebSerialTransport}.
|
|
4
|
+
*/
|
|
5
|
+
export interface WebSerialTransportOptions {
|
|
6
|
+
/** 7812 for standard ECUs; 15625 for modified double-speed firmware. */
|
|
7
|
+
baudRate?: number;
|
|
8
|
+
}
|
|
9
|
+
/** `Transport` over a Web Serial `SerialPort` (8N1, no flow control). */
|
|
10
|
+
export declare class WebSerialTransport implements Transport {
|
|
11
|
+
#private;
|
|
12
|
+
/**
|
|
13
|
+
* Wraps a serial port. The port is not opened until {@link WebSerialTransport.open}.
|
|
14
|
+
*
|
|
15
|
+
* @param port - A port the user has granted access to, from
|
|
16
|
+
* `navigator.serial.requestPort()`.
|
|
17
|
+
* @param options - Optional settings.
|
|
18
|
+
*/
|
|
19
|
+
constructor(port: SerialPort, options?: WebSerialTransportOptions);
|
|
20
|
+
/**
|
|
21
|
+
* Opens the port at the configured baud rate, 8N1 with no flow control.
|
|
22
|
+
* Does nothing if already open.
|
|
23
|
+
*/
|
|
24
|
+
open(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Cancels any pending read, releases the stream locks and closes the port.
|
|
27
|
+
* Does nothing if not open.
|
|
28
|
+
*/
|
|
29
|
+
close(): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Writes bytes to the port.
|
|
32
|
+
*
|
|
33
|
+
* @param data - Bytes to send.
|
|
34
|
+
* @throws {@link NotConnectedError} if the port is not open.
|
|
35
|
+
*/
|
|
36
|
+
write(data: Uint8Array): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Reads exactly `length` bytes, assembling them from as many chunks as needed.
|
|
39
|
+
* Surplus bytes are kept for the next call.
|
|
40
|
+
*
|
|
41
|
+
* @param length - Number of bytes wanted.
|
|
42
|
+
* @param timeoutMs - Longest silence to wait for more data, in milliseconds.
|
|
43
|
+
* @returns Exactly `length` bytes.
|
|
44
|
+
* @throws {@link TimeoutError} if no data arrives within `timeoutMs`.
|
|
45
|
+
* @throws {@link NotConnectedError} if the port is not open or the stream ends.
|
|
46
|
+
*/
|
|
47
|
+
read(length: number, timeoutMs: number): Promise<Uint8Array>;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=webSerial.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webSerial.d.ts","sourceRoot":"","sources":["../../src/transport/webSerial.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,yEAAyE;AACzE,qBAAa,kBAAmB,YAAW,SAAS;;IAQlD;;;;;;OAMG;gBACS,IAAI,EAAE,UAAU,EAAE,OAAO,GAAE,yBAA8B;IAKrE;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAgB3B;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB5B;;;;;OAKG;IACG,KAAK,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ5C;;;;;;;;;OASG;IACG,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;CA2DnE"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
// Copyright (C) 2026 comm14cux-ts contributors
|
|
3
|
+
import { BAUD } from '../constants.js';
|
|
4
|
+
import { NotConnectedError, TimeoutError } from '../errors.js';
|
|
5
|
+
/** `Transport` over a Web Serial `SerialPort` (8N1, no flow control). */
|
|
6
|
+
export class WebSerialTransport {
|
|
7
|
+
#port;
|
|
8
|
+
#baudRate;
|
|
9
|
+
#reader;
|
|
10
|
+
#writer;
|
|
11
|
+
#pending;
|
|
12
|
+
#leftover = new Uint8Array(0);
|
|
13
|
+
/**
|
|
14
|
+
* Wraps a serial port. The port is not opened until {@link WebSerialTransport.open}.
|
|
15
|
+
*
|
|
16
|
+
* @param port - A port the user has granted access to, from
|
|
17
|
+
* `navigator.serial.requestPort()`.
|
|
18
|
+
* @param options - Optional settings.
|
|
19
|
+
*/
|
|
20
|
+
constructor(port, options = {}) {
|
|
21
|
+
this.#port = port;
|
|
22
|
+
this.#baudRate = options.baudRate ?? BAUD;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Opens the port at the configured baud rate, 8N1 with no flow control.
|
|
26
|
+
* Does nothing if already open.
|
|
27
|
+
*/
|
|
28
|
+
async open() {
|
|
29
|
+
if (this.#reader) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
await this.#port.open({
|
|
33
|
+
baudRate: this.#baudRate,
|
|
34
|
+
dataBits: 8,
|
|
35
|
+
stopBits: 1,
|
|
36
|
+
parity: 'none',
|
|
37
|
+
flowControl: 'none',
|
|
38
|
+
});
|
|
39
|
+
this.#reader = this.#port.readable?.getReader();
|
|
40
|
+
this.#writer = this.#port.writable?.getWriter();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Cancels any pending read, releases the stream locks and closes the port.
|
|
44
|
+
* Does nothing if not open.
|
|
45
|
+
*/
|
|
46
|
+
async close() {
|
|
47
|
+
const reader = this.#reader;
|
|
48
|
+
const writer = this.#writer;
|
|
49
|
+
this.#reader = undefined;
|
|
50
|
+
this.#writer = undefined;
|
|
51
|
+
this.#pending = undefined;
|
|
52
|
+
this.#leftover = new Uint8Array(0);
|
|
53
|
+
if (!reader) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
await reader.cancel().catch(() => undefined);
|
|
57
|
+
reader.releaseLock();
|
|
58
|
+
writer?.releaseLock();
|
|
59
|
+
await this.#port.close();
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Writes bytes to the port.
|
|
63
|
+
*
|
|
64
|
+
* @param data - Bytes to send.
|
|
65
|
+
* @throws {@link NotConnectedError} if the port is not open.
|
|
66
|
+
*/
|
|
67
|
+
async write(data) {
|
|
68
|
+
if (!this.#writer) {
|
|
69
|
+
throw new NotConnectedError('Serial port is not open');
|
|
70
|
+
}
|
|
71
|
+
await this.#writer.write(data);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Reads exactly `length` bytes, assembling them from as many chunks as needed.
|
|
75
|
+
* Surplus bytes are kept for the next call.
|
|
76
|
+
*
|
|
77
|
+
* @param length - Number of bytes wanted.
|
|
78
|
+
* @param timeoutMs - Longest silence to wait for more data, in milliseconds.
|
|
79
|
+
* @returns Exactly `length` bytes.
|
|
80
|
+
* @throws {@link TimeoutError} if no data arrives within `timeoutMs`.
|
|
81
|
+
* @throws {@link NotConnectedError} if the port is not open or the stream ends.
|
|
82
|
+
*/
|
|
83
|
+
async read(length, timeoutMs) {
|
|
84
|
+
const reader = this.#reader;
|
|
85
|
+
if (!reader) {
|
|
86
|
+
throw new NotConnectedError('Serial port is not open');
|
|
87
|
+
}
|
|
88
|
+
const result = new Uint8Array(length);
|
|
89
|
+
let filled = 0;
|
|
90
|
+
while (filled < length) {
|
|
91
|
+
if (this.#leftover.length === 0) {
|
|
92
|
+
this.#leftover = await this.#nextChunk(reader, timeoutMs);
|
|
93
|
+
}
|
|
94
|
+
const take = Math.min(length - filled, this.#leftover.length);
|
|
95
|
+
result.set(this.#leftover.subarray(0, take), filled);
|
|
96
|
+
this.#leftover = this.#leftover.subarray(take);
|
|
97
|
+
filled += take;
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
async #nextChunk(reader, timeoutMs) {
|
|
102
|
+
// A read that timed out is still pending; reuse it so no data is lost.
|
|
103
|
+
const pending = (this.#pending ??= reader.read());
|
|
104
|
+
let timer;
|
|
105
|
+
const timeout = new Promise((_, reject) => {
|
|
106
|
+
timer = setTimeout(() => {
|
|
107
|
+
reject(new TimeoutError(`No data from ECU for ${timeoutMs} ms`));
|
|
108
|
+
}, timeoutMs);
|
|
109
|
+
});
|
|
110
|
+
try {
|
|
111
|
+
const chunk = await Promise.race([pending, timeout]);
|
|
112
|
+
this.#pending = undefined;
|
|
113
|
+
if (chunk.done) {
|
|
114
|
+
throw new NotConnectedError('Serial stream ended');
|
|
115
|
+
}
|
|
116
|
+
return chunk.value;
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (!(error instanceof TimeoutError)) {
|
|
120
|
+
this.#pending = undefined;
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=webSerial.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webSerial.js","sourceRoot":"","sources":["../../src/transport/webSerial.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: GPL-3.0-only\n// Copyright (C) 2026 comm14cux-ts contributors\n\nimport { BAUD } from '../constants.js';\nimport { NotConnectedError, TimeoutError } from '../errors.js';\nimport type { Transport } from './types.js';\n\n/**\n * Options for {@link WebSerialTransport}.\n */\nexport interface WebSerialTransportOptions {\n /** 7812 for standard ECUs; 15625 for modified double-speed firmware. */\n baudRate?: number;\n}\n\n/** `Transport` over a Web Serial `SerialPort` (8N1, no flow control). */\nexport class WebSerialTransport implements Transport {\n readonly #port: SerialPort;\n readonly #baudRate: number;\n #reader: ReadableStreamDefaultReader<Uint8Array> | undefined;\n #writer: WritableStreamDefaultWriter<Uint8Array> | undefined;\n #pending: Promise<ReadableStreamReadResult<Uint8Array>> | undefined;\n #leftover: Uint8Array = new Uint8Array(0);\n\n /**\n * Wraps a serial port. The port is not opened until {@link WebSerialTransport.open}.\n *\n * @param port - A port the user has granted access to, from\n * `navigator.serial.requestPort()`.\n * @param options - Optional settings.\n */\n constructor(port: SerialPort, options: WebSerialTransportOptions = {}) {\n this.#port = port;\n this.#baudRate = options.baudRate ?? BAUD;\n }\n\n /**\n * Opens the port at the configured baud rate, 8N1 with no flow control.\n * Does nothing if already open.\n */\n async open(): Promise<void> {\n if (this.#reader) {\n return;\n }\n\n await this.#port.open({\n baudRate: this.#baudRate,\n dataBits: 8,\n stopBits: 1,\n parity: 'none',\n flowControl: 'none',\n });\n this.#reader = this.#port.readable?.getReader();\n this.#writer = this.#port.writable?.getWriter();\n }\n\n /**\n * Cancels any pending read, releases the stream locks and closes the port.\n * Does nothing if not open.\n */\n async close(): Promise<void> {\n const reader = this.#reader;\n const writer = this.#writer;\n\n this.#reader = undefined;\n this.#writer = undefined;\n this.#pending = undefined;\n this.#leftover = new Uint8Array(0);\n\n if (!reader) {\n return;\n }\n\n await reader.cancel().catch(() => undefined);\n reader.releaseLock();\n writer?.releaseLock();\n await this.#port.close();\n }\n\n /**\n * Writes bytes to the port.\n *\n * @param data - Bytes to send.\n * @throws {@link NotConnectedError} if the port is not open.\n */\n async write(data: Uint8Array): Promise<void> {\n if (!this.#writer) {\n throw new NotConnectedError('Serial port is not open');\n }\n\n await this.#writer.write(data);\n }\n\n /**\n * Reads exactly `length` bytes, assembling them from as many chunks as needed.\n * Surplus bytes are kept for the next call.\n *\n * @param length - Number of bytes wanted.\n * @param timeoutMs - Longest silence to wait for more data, in milliseconds.\n * @returns Exactly `length` bytes.\n * @throws {@link TimeoutError} if no data arrives within `timeoutMs`.\n * @throws {@link NotConnectedError} if the port is not open or the stream ends.\n */\n async read(length: number, timeoutMs: number): Promise<Uint8Array> {\n const reader = this.#reader;\n\n if (!reader) {\n throw new NotConnectedError('Serial port is not open');\n }\n\n const result = new Uint8Array(length);\n let filled = 0;\n\n while (filled < length) {\n if (this.#leftover.length === 0) {\n this.#leftover = await this.#nextChunk(reader, timeoutMs);\n }\n\n const take = Math.min(length - filled, this.#leftover.length);\n\n result.set(this.#leftover.subarray(0, take), filled);\n this.#leftover = this.#leftover.subarray(take);\n filled += take;\n }\n\n return result;\n }\n\n async #nextChunk(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n timeoutMs: number,\n ): Promise<Uint8Array> {\n // A read that timed out is still pending; reuse it so no data is lost.\n const pending = (this.#pending ??= reader.read());\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n reject(new TimeoutError(`No data from ECU for ${timeoutMs} ms`));\n }, timeoutMs);\n });\n\n try {\n const chunk = await Promise.race([pending, timeout]);\n\n this.#pending = undefined;\n\n if (chunk.done) {\n throw new NotConnectedError('Serial stream ended');\n }\n\n return chunk.value;\n } catch (error) {\n if (!(error instanceof TimeoutError)) {\n this.#pending = undefined;\n }\n\n throw error;\n } finally {\n clearTimeout(timer);\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Major/minor/patch version of this library (`c14cux_version`). */
|
|
2
|
+
export interface Version {
|
|
3
|
+
/**
|
|
4
|
+
* Major version number.
|
|
5
|
+
*/
|
|
6
|
+
major: number;
|
|
7
|
+
/**
|
|
8
|
+
* Minor version number.
|
|
9
|
+
*/
|
|
10
|
+
minor: number;
|
|
11
|
+
/**
|
|
12
|
+
* Patch version number.
|
|
13
|
+
*/
|
|
14
|
+
patch: number;
|
|
15
|
+
}
|
|
16
|
+
/** Must match `version` in package.json (enforced by a test). */
|
|
17
|
+
export declare const LIBRARY_VERSION: Version;
|
|
18
|
+
//# sourceMappingURL=version.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAGA,oEAAoE;AACpE,MAAM,WAAW,OAAO;IACtB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;CACf;AAED,iEAAiE;AACjE,eAAO,MAAM,eAAe,EAAE,OAA0C,CAAC"}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,+CAA+C;AAkB/C,iEAAiE;AACjE,MAAM,CAAC,MAAM,eAAe,GAAY,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,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/** Must match `version` in package.json (enforced by a test). */\nexport const LIBRARY_VERSION: Version = { major: 0, minor: 0, patch: 0 };\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kb1rma/libcomm14cux-ts",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "TypeScript library for communicating with the Lucas 14CUX engine ECU over a serial port using the Web Serial API",
|
|
5
|
+
"license": "GPL-3.0-only",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/KB1RMA/comm14cux-ts.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"packageManager": "npm@11.8.0",
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=24"
|
|
14
|
+
},
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"src",
|
|
26
|
+
"!src/**/*.test.ts",
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc -p tsconfig.build.json",
|
|
32
|
+
"lint": "eslint .",
|
|
33
|
+
"lint:fix": "eslint . --fix",
|
|
34
|
+
"type:check": "tsc --noEmit",
|
|
35
|
+
"test": "vitest",
|
|
36
|
+
"test:ui": "vitest --ui",
|
|
37
|
+
"test:coverage": "vitest --coverage",
|
|
38
|
+
"test:acceptance": "vitest run --config vitest.acceptance.config.ts",
|
|
39
|
+
"test:acceptance:dist": "npm run build && tsc -p tsconfig.acceptance.dist.json && ACCEPTANCE_TARGET=dist vitest run --config vitest.acceptance.config.ts"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@eslint/js": "^10.0.0",
|
|
43
|
+
"@types/node": "^25.9.1",
|
|
44
|
+
"@types/w3c-web-serial": "^1.0.8",
|
|
45
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
46
|
+
"@vitest/eslint-plugin": "^1.6.27",
|
|
47
|
+
"@vitest/ui": "^4.0.18",
|
|
48
|
+
"eslint": "^10.8.1",
|
|
49
|
+
"eslint-config-prettier": "^10.1.8",
|
|
50
|
+
"eslint-plugin-jsdoc": "^65.1.0",
|
|
51
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
52
|
+
"eslint-plugin-tsdoc": "^0.5.4",
|
|
53
|
+
"prettier": "^3.9.6",
|
|
54
|
+
"typescript": "^5.9.3",
|
|
55
|
+
"typescript-eslint": "^8.67.0",
|
|
56
|
+
"vitest": "^4.0.18"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/bytes.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
// Copyright (C) 2026 comm14cux-ts contributors
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Reads an unsigned big-endian 16-bit value, the ECU's byte order.
|
|
6
|
+
*
|
|
7
|
+
* @param bytes - Buffer to read from.
|
|
8
|
+
* @param offset - Index of the high byte. Defaults to 0.
|
|
9
|
+
* @returns The value, 0 to 65535. A missing byte is treated as 0.
|
|
10
|
+
*/
|
|
11
|
+
export function be16(bytes: Uint8Array, offset = 0): number {
|
|
12
|
+
return ((bytes[offset] ?? 0) << 8) | (bytes[offset + 1] ?? 0);
|
|
13
|
+
}
|