@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
package/src/errors.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
// Copyright (C) 2026 comm14cux-ts contributors
|
|
3
|
+
|
|
4
|
+
/** Base class for every error thrown by this library. */
|
|
5
|
+
export class Comm14cuxError extends Error {
|
|
6
|
+
/** The name of this error class. */
|
|
7
|
+
override name = 'Comm14cuxError';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** The ECU (or serial port) did not deliver the expected bytes in time. */
|
|
11
|
+
export class TimeoutError extends Comm14cuxError {
|
|
12
|
+
/** The name of this error class. */
|
|
13
|
+
override name = 'TimeoutError';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The ECU's reply did not match the protocol (for example a wrong echo). */
|
|
17
|
+
export class ProtocolError extends Comm14cuxError {
|
|
18
|
+
/** The name of this error class. */
|
|
19
|
+
override name = 'ProtocolError';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** An operation was attempted while the transport was not open. */
|
|
23
|
+
export class NotConnectedError extends Comm14cuxError {
|
|
24
|
+
/** The name of this error class. */
|
|
25
|
+
override name = 'NotConnectedError';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A multi-chunk read was stopped by `cancelRead()`. */
|
|
29
|
+
export class ReadCancelledError extends Comm14cuxError {
|
|
30
|
+
/** The name of this error class. */
|
|
31
|
+
override name = 'ReadCancelledError';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The ECU returned data that is outside the range the firmware can produce. */
|
|
35
|
+
export class InvalidReadingError extends Comm14cuxError {
|
|
36
|
+
/** The name of this error class. */
|
|
37
|
+
override name = 'InvalidReadingError';
|
|
38
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
// Copyright (C) 2026 comm14cux-ts contributors
|
|
3
|
+
|
|
4
|
+
export { Ecu, type EcuOptions, type FuelMap } from './ecu.js';
|
|
5
|
+
export * from './constants.js';
|
|
6
|
+
export * from './errors.js';
|
|
7
|
+
export type { Transport } from './transport/types.js';
|
|
8
|
+
export { SimulatedTransport } from './transport/simulated.js';
|
|
9
|
+
export {
|
|
10
|
+
WebSerialTransport,
|
|
11
|
+
type WebSerialTransportOptions,
|
|
12
|
+
} from './transport/webSerial.js';
|
|
13
|
+
export type { FaultCodes, FaultCodeName } from './decoders/faultCodes.js';
|
|
14
|
+
export type { FuelMapIndex } from './decoders/fuelMap.js';
|
|
15
|
+
export type { TuneRevision } from './decoders/tuneRevision.js';
|
|
16
|
+
export type { Version } from './version.js';
|
|
@@ -0,0 +1,167 @@
|
|
|
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 { DEFAULT_READ_TIMEOUT_MS } from '../constants.js';
|
|
6
|
+
import { ProtocolError, ReadCancelledError } from '../errors.js';
|
|
7
|
+
import type { Transport } from '../transport/types.js';
|
|
8
|
+
import { nextRead } from './readCount.js';
|
|
9
|
+
|
|
10
|
+
const ADDRESS_SPACE = 0x10000;
|
|
11
|
+
const COARSE_WINDOW = 64;
|
|
12
|
+
// Writes use the coarse-address command with a length code of 0.
|
|
13
|
+
const WRITE_LENGTH_CODE = 0;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Memory read and write commands over a `Transport`
|
|
17
|
+
* (`protocol.c`: `c14cux_readMem`, `c14cux_writeMem`).
|
|
18
|
+
*
|
|
19
|
+
* This class is not re-entrant: callers must serialise operations (the
|
|
20
|
+
* `Ecu` class does so with a `CommandQueue`). Cancellation is decided by the
|
|
21
|
+
* caller, through the `isCancelled` callback given to `readMem`.
|
|
22
|
+
*/
|
|
23
|
+
export class Protocol {
|
|
24
|
+
readonly #transport: Transport;
|
|
25
|
+
readonly #timeoutMs: number;
|
|
26
|
+
#lastReadCoarseAddress = 0;
|
|
27
|
+
#lastReadQuantity = 0;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Creates a protocol handler.
|
|
31
|
+
*
|
|
32
|
+
* @param transport - The link to the ECU.
|
|
33
|
+
* @param timeoutMs - Silence timeout for each read, in milliseconds.
|
|
34
|
+
*/
|
|
35
|
+
constructor(transport: Transport, timeoutMs = DEFAULT_READ_TIMEOUT_MS) {
|
|
36
|
+
this.#transport = transport;
|
|
37
|
+
this.#timeoutMs = timeoutMs;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Forgets the last coarse address, forcing the next read to set it. */
|
|
41
|
+
resetCache(): void {
|
|
42
|
+
this.#lastReadCoarseAddress = 0;
|
|
43
|
+
this.#lastReadQuantity = 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Reads `length` bytes starting at `addr`, splitting long reads into chunks
|
|
48
|
+
* (`c14cux_readMem`).
|
|
49
|
+
*
|
|
50
|
+
* @param addr - First address to read, 0 to 0xFFFF.
|
|
51
|
+
* @param length - Number of bytes to read. `addr + length` must not exceed 0x10000.
|
|
52
|
+
* @param isCancelled - Checked before each chunk of a multi-chunk read; a
|
|
53
|
+
* read that fits in one chunk is never cancelled.
|
|
54
|
+
* @returns The bytes read.
|
|
55
|
+
* @throws {@link RangeError} if the address or length is out of range.
|
|
56
|
+
* @throws {@link ReadCancelledError} if `isCancelled` returns `true`.
|
|
57
|
+
* @throws {@link ProtocolError} if an echo is wrong.
|
|
58
|
+
* @throws {@link TimeoutError} if the ECU stops responding.
|
|
59
|
+
*/
|
|
60
|
+
async readMem(
|
|
61
|
+
addr: number,
|
|
62
|
+
length: number,
|
|
63
|
+
isCancelled: () => boolean,
|
|
64
|
+
): Promise<Uint8Array> {
|
|
65
|
+
assertUint16(addr, 'address');
|
|
66
|
+
assertUint16(length, 'length');
|
|
67
|
+
|
|
68
|
+
if (addr + length > ADDRESS_SPACE) {
|
|
69
|
+
throw new RangeError('Read extends past the end of the address space');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const result = new Uint8Array(length);
|
|
73
|
+
const multiChunk = nextRead(length, 0).count < length;
|
|
74
|
+
let totalRead = 0;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
while (totalRead < length) {
|
|
78
|
+
if (multiChunk && isCancelled()) {
|
|
79
|
+
throw new ReadCancelledError('Read cancelled');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const { count: quantity, code } = nextRead(length, totalRead);
|
|
83
|
+
const chunkAddr = addr + totalRead;
|
|
84
|
+
|
|
85
|
+
// The ECU latches only the 64-byte-aligned block (addr >> 6), so the
|
|
86
|
+
// final command byte alone is enough only within that same block.
|
|
87
|
+
const lastByteOnly =
|
|
88
|
+
quantity === this.#lastReadQuantity &&
|
|
89
|
+
Math.trunc(chunkAddr / COARSE_WINDOW) ===
|
|
90
|
+
Math.trunc(this.#lastReadCoarseAddress / COARSE_WINDOW);
|
|
91
|
+
|
|
92
|
+
if (!lastByteOnly) {
|
|
93
|
+
await this.setCoarseAddr(chunkAddr, code);
|
|
94
|
+
this.#lastReadCoarseAddress = chunkAddr;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The ECU does not echo the read command; it starts sending data.
|
|
98
|
+
await this.#transport.write(Uint8Array.of(0xc0 | (chunkAddr & 0x3f)));
|
|
99
|
+
result.set(
|
|
100
|
+
await this.#transport.read(quantity, this.#timeoutMs),
|
|
101
|
+
totalRead,
|
|
102
|
+
);
|
|
103
|
+
totalRead += quantity;
|
|
104
|
+
this.#lastReadQuantity = quantity;
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
this.resetCache();
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Writes one byte to ECU memory (`c14cux_writeMem`).
|
|
116
|
+
*
|
|
117
|
+
* @param addr - Address to write, 0 to 0xFFFF.
|
|
118
|
+
* @param value - Byte to write, 0 to 255.
|
|
119
|
+
* @throws {@link RangeError} if the address or value is out of range.
|
|
120
|
+
* @throws {@link ProtocolError} if an echo is wrong.
|
|
121
|
+
* @throws {@link TimeoutError} if the ECU stops responding.
|
|
122
|
+
*/
|
|
123
|
+
async writeMem(addr: number, value: number): Promise<void> {
|
|
124
|
+
assertUint16(addr, 'address');
|
|
125
|
+
|
|
126
|
+
if (!Number.isInteger(value) || value < 0 || value > 0xff) {
|
|
127
|
+
throw new RangeError(`Invalid value: ${value}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.resetCache();
|
|
131
|
+
await this.setCoarseAddr(addr, WRITE_LENGTH_CODE);
|
|
132
|
+
await this.#sendEchoed(0x80 | (addr & 0x3f));
|
|
133
|
+
await this.#sendEchoed(value);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Sets the coarse address and length code for the next command
|
|
138
|
+
* (`c14cux_setCoarseAddr`).
|
|
139
|
+
*
|
|
140
|
+
* @param addr - Address of the read or write, 0 to 0xFFFF.
|
|
141
|
+
* @param code - Length code of the following read, or 0 for a write.
|
|
142
|
+
* @throws {@link ProtocolError} if an echo is wrong.
|
|
143
|
+
* @throws {@link TimeoutError} if the ECU stops responding.
|
|
144
|
+
*/
|
|
145
|
+
async setCoarseAddr(addr: number, code: number): Promise<void> {
|
|
146
|
+
await this.#sendEchoed(((code << 2) | (addr >> 14)) & 0xff);
|
|
147
|
+
await this.#sendEchoed((addr >> 6) & 0xff);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async #sendEchoed(byte: number): Promise<void> {
|
|
151
|
+
await this.#transport.write(Uint8Array.of(byte));
|
|
152
|
+
|
|
153
|
+
const [echo] = await this.#transport.read(1, this.#timeoutMs);
|
|
154
|
+
|
|
155
|
+
if (echo !== byte) {
|
|
156
|
+
throw new ProtocolError(
|
|
157
|
+
`Echo mismatch: sent 0x${byte.toString(16)}, received 0x${(echo ?? 0).toString(16)}`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function assertUint16(value: number, name: string): void {
|
|
164
|
+
if (!Number.isInteger(value) || value < 0 || value >= ADDRESS_SPACE) {
|
|
165
|
+
throw new RangeError(`Invalid ${name}: ${value}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
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 { ReadCount, ReadCountValue } from '../constants.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A single read request: how many bytes, and the length code that asks the
|
|
9
|
+
* ECU for them.
|
|
10
|
+
*/
|
|
11
|
+
export interface ReadChunk {
|
|
12
|
+
/** Number of bytes the ECU will return. */
|
|
13
|
+
count: number;
|
|
14
|
+
/** 5-bit length code for the coarse-address command. */
|
|
15
|
+
code: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const PRESETS: readonly ReadChunk[] = [
|
|
19
|
+
{ count: ReadCount.Count4, code: ReadCountValue.Count4 },
|
|
20
|
+
{ count: ReadCount.Count3, code: ReadCountValue.Count3 },
|
|
21
|
+
{ count: ReadCount.Count2, code: ReadCountValue.Count2 },
|
|
22
|
+
{ count: ReadCount.Count1, code: ReadCountValue.Count1 },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Chooses the next single read (`c14cux_getByteCountForNextRead`). The ECU
|
|
27
|
+
* can return 1 to 16, 80, 100, 400 or 512 bytes; 1 to 16 bytes use the code
|
|
28
|
+
* `count - 1`.
|
|
29
|
+
*
|
|
30
|
+
* @param total - Total bytes being read over all requests.
|
|
31
|
+
* @param alreadyRead - Bytes read so far; less than `total`.
|
|
32
|
+
* @returns 512, 400, 100 or 80 bytes if that many remain, otherwise up to 16.
|
|
33
|
+
*/
|
|
34
|
+
export function nextRead(total: number, alreadyRead: number): ReadChunk {
|
|
35
|
+
const bytesLeft = total - alreadyRead;
|
|
36
|
+
const preset = PRESETS.find(({ count }) => bytesLeft >= count);
|
|
37
|
+
|
|
38
|
+
if (preset) {
|
|
39
|
+
return preset;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const count = Math.min(bytesLeft, ReadCount.Count0);
|
|
43
|
+
|
|
44
|
+
return { count, code: count - 1 };
|
|
45
|
+
}
|
package/src/queue.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
// Copyright (C) 2026 comm14cux-ts contributors
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Runs asynchronous tasks one at a time, in submission order. Replaces the
|
|
6
|
+
* per-connection mutex that libcomm14cux holds around each operation.
|
|
7
|
+
*/
|
|
8
|
+
export class CommandQueue {
|
|
9
|
+
#tail: Promise<unknown> = Promise.resolve();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Queues a task and runs it once all earlier tasks have settled.
|
|
13
|
+
*
|
|
14
|
+
* @param task - Function that performs the work.
|
|
15
|
+
* @returns The task's result. If the task rejects, only this call rejects.
|
|
16
|
+
*/
|
|
17
|
+
run<T>(task: () => Promise<T>): Promise<T> {
|
|
18
|
+
const result = this.#tail.then(task);
|
|
19
|
+
|
|
20
|
+
this.#tail = result.catch(() => undefined);
|
|
21
|
+
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
|
|
6
|
+
// Helpers shared by the test files. Everything here goes through the
|
|
7
|
+
// package entry point, as a user of the library would.
|
|
8
|
+
import { DataOffsetRev, Ecu, SimulatedTransport } from '../index.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates an `Ecu` over a fresh `SimulatedTransport` and connects it.
|
|
12
|
+
*
|
|
13
|
+
* @returns The connected `Ecu` and the simulated ECU behind it.
|
|
14
|
+
*/
|
|
15
|
+
export async function connected(): Promise<{
|
|
16
|
+
transport: SimulatedTransport;
|
|
17
|
+
ecu: Ecu;
|
|
18
|
+
}> {
|
|
19
|
+
const transport = new SimulatedTransport();
|
|
20
|
+
const ecu = new Ecu(transport, { readTimeoutMs: 5 });
|
|
21
|
+
|
|
22
|
+
await ecu.connect();
|
|
23
|
+
|
|
24
|
+
return { transport, ecu };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Fills the simulated memory with a non-repeating pattern, so a read from
|
|
29
|
+
* the wrong address cannot pass by accident.
|
|
30
|
+
*
|
|
31
|
+
* @param transport - The simulated ECU.
|
|
32
|
+
*/
|
|
33
|
+
export function fillPattern(transport: SimulatedTransport): void {
|
|
34
|
+
transport.memory.forEach((_, i) => {
|
|
35
|
+
transport.memory[i] = (i * 7 + (i >> 8)) & 0xff;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Sets memory so that the ROM looks like the given data layout.
|
|
41
|
+
*
|
|
42
|
+
* @param transport - The simulated ECU.
|
|
43
|
+
* @param rev - The layout to imitate.
|
|
44
|
+
*/
|
|
45
|
+
export function setRevision(
|
|
46
|
+
transport: SimulatedTransport,
|
|
47
|
+
rev: DataOffsetRev,
|
|
48
|
+
): void {
|
|
49
|
+
if (rev === DataOffsetRev.RevC) {
|
|
50
|
+
transport.memory[0xc23f] = 0x40;
|
|
51
|
+
} else {
|
|
52
|
+
transport.memory.fill(0x10, 0xc23f, 0xc23f + 16);
|
|
53
|
+
transport.memory[0xc79b] = rev === DataOffsetRev.RevA ? 0xff : 0x64;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** One command sent to the ECU, decoded from the transport's write log. */
|
|
58
|
+
export type WireCommand =
|
|
59
|
+
| { kind: 'coarse'; lengthCode: number; address: number }
|
|
60
|
+
| { kind: 'read'; low: number }
|
|
61
|
+
| { kind: 'write'; low: number; value: number };
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Splits the bytes written to the ECU back into commands.
|
|
65
|
+
*
|
|
66
|
+
* @param written - `SimulatedTransport.written`.
|
|
67
|
+
* @returns The commands, in order.
|
|
68
|
+
*/
|
|
69
|
+
export function parseWire(written: readonly number[]): WireCommand[] {
|
|
70
|
+
const commands: WireCommand[] = [];
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < written.length; i++) {
|
|
73
|
+
const byte = written[i] ?? 0;
|
|
74
|
+
|
|
75
|
+
if (byte < 0x80) {
|
|
76
|
+
const second = written[++i] ?? 0;
|
|
77
|
+
|
|
78
|
+
commands.push({
|
|
79
|
+
kind: 'coarse',
|
|
80
|
+
lengthCode: byte >> 2,
|
|
81
|
+
address: ((byte & 0x03) << 14) | (second << 6),
|
|
82
|
+
});
|
|
83
|
+
} else if (byte >= 0xc0) {
|
|
84
|
+
commands.push({ kind: 'read', low: byte & 0x3f });
|
|
85
|
+
} else {
|
|
86
|
+
commands.push({
|
|
87
|
+
kind: 'write',
|
|
88
|
+
low: byte & 0x3f,
|
|
89
|
+
value: written[++i] ?? 0,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return commands;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const PRESET_LENGTHS: Record<number, number> = {
|
|
98
|
+
0x10: 80,
|
|
99
|
+
0x11: 100,
|
|
100
|
+
0x12: 400,
|
|
101
|
+
0x13: 512,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Gives the size of each read chunk the ECU was asked for.
|
|
106
|
+
*
|
|
107
|
+
* @param written - `SimulatedTransport.written`.
|
|
108
|
+
* @returns One length per read command, in order.
|
|
109
|
+
*/
|
|
110
|
+
export function readChunkLengths(written: readonly number[]): number[] {
|
|
111
|
+
const lengths: number[] = [];
|
|
112
|
+
let current = 0;
|
|
113
|
+
|
|
114
|
+
for (const command of parseWire(written)) {
|
|
115
|
+
if (command.kind === 'coarse') {
|
|
116
|
+
current =
|
|
117
|
+
command.lengthCode <= 0x0f
|
|
118
|
+
? command.lengthCode + 1
|
|
119
|
+
: (PRESET_LENGTHS[command.lengthCode] ?? -1);
|
|
120
|
+
} else if (command.kind === 'read') {
|
|
121
|
+
lengths.push(current);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return lengths;
|
|
126
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
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
|
+
import type { Transport } from './types.js';
|
|
8
|
+
|
|
9
|
+
const MEMORY_SIZE = 0x10000;
|
|
10
|
+
|
|
11
|
+
type State = 'idle' | 'coarse2' | 'writeValue';
|
|
12
|
+
|
|
13
|
+
function lengthFromCode(code: number): number | undefined {
|
|
14
|
+
if (code <= 0x0f) {
|
|
15
|
+
return code + 1;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
switch (code) {
|
|
19
|
+
case ReadCountValue.Count1:
|
|
20
|
+
return 0x50;
|
|
21
|
+
case ReadCountValue.Count2:
|
|
22
|
+
return 0x64;
|
|
23
|
+
case ReadCountValue.Count3:
|
|
24
|
+
return 0x190;
|
|
25
|
+
case ReadCountValue.Count4:
|
|
26
|
+
return 0x200;
|
|
27
|
+
default:
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Emulates the ECU's side of the serial protocol over an in-memory 64 KiB
|
|
34
|
+
* address space. Used for tests and for developing a UI without a car.
|
|
35
|
+
*/
|
|
36
|
+
export class SimulatedTransport implements Transport {
|
|
37
|
+
/** The ECU's address space. Tests may read and modify it directly. */
|
|
38
|
+
readonly memory = new Uint8Array(MEMORY_SIZE);
|
|
39
|
+
/** Every byte written to the ECU, in order. */
|
|
40
|
+
readonly written: number[] = [];
|
|
41
|
+
|
|
42
|
+
/** Fault injection: invert the next echoed byte. */
|
|
43
|
+
corruptNextEcho = false;
|
|
44
|
+
/** Fault injection: never reply. */
|
|
45
|
+
silent = false;
|
|
46
|
+
/** Fault injection: reject every write. */
|
|
47
|
+
failWrites = false;
|
|
48
|
+
/** Fault injection: stream at most this many bytes per read command. */
|
|
49
|
+
streamLimit: number | undefined = undefined;
|
|
50
|
+
|
|
51
|
+
#open = false;
|
|
52
|
+
#state: State = 'idle';
|
|
53
|
+
#output: number[] = [];
|
|
54
|
+
#firstCoarse = 0;
|
|
55
|
+
#coarse = 0;
|
|
56
|
+
#latched = false;
|
|
57
|
+
#lengthCode = 0;
|
|
58
|
+
#writeAddr = 0;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Whether the transport is open.
|
|
62
|
+
*
|
|
63
|
+
* @returns `true` between {@link SimulatedTransport.open} and {@link SimulatedTransport.close}.
|
|
64
|
+
*/
|
|
65
|
+
get isOpen(): boolean {
|
|
66
|
+
return this.#open;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Places a 16 KiB firmware image at 0xC000 to 0xFFFF.
|
|
71
|
+
*
|
|
72
|
+
* @param image - The ROM image, exactly 0x4000 bytes.
|
|
73
|
+
* @throws {@link RangeError} if the image is the wrong size.
|
|
74
|
+
*/
|
|
75
|
+
loadRom(image: Uint8Array): void {
|
|
76
|
+
if (image.length !== DataSize.ROM) {
|
|
77
|
+
throw new RangeError(`ROM image must be ${DataSize.ROM} bytes`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.memory.set(image, MemoryOffset.ROMAddress);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Opens the simulated link.
|
|
85
|
+
*
|
|
86
|
+
* @returns A promise that resolves immediately.
|
|
87
|
+
*/
|
|
88
|
+
open(): Promise<void> {
|
|
89
|
+
this.#open = true;
|
|
90
|
+
|
|
91
|
+
return Promise.resolve();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Closes the simulated link and discards any pending ECU output.
|
|
96
|
+
*
|
|
97
|
+
* @returns A promise that resolves immediately.
|
|
98
|
+
*/
|
|
99
|
+
close(): Promise<void> {
|
|
100
|
+
this.#open = false;
|
|
101
|
+
this.#state = 'idle';
|
|
102
|
+
this.#latched = false;
|
|
103
|
+
this.#output = [];
|
|
104
|
+
|
|
105
|
+
return Promise.resolve();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Delivers bytes to the simulated ECU, which may queue a reply.
|
|
110
|
+
*
|
|
111
|
+
* @param data - Bytes to send.
|
|
112
|
+
* @returns A promise that resolves once the bytes are delivered.
|
|
113
|
+
* @throws {@link NotConnectedError} if the transport is closed.
|
|
114
|
+
* @throws Error if {@link SimulatedTransport.failWrites} is set.
|
|
115
|
+
*/
|
|
116
|
+
write(data: Uint8Array): Promise<void> {
|
|
117
|
+
if (!this.#open) {
|
|
118
|
+
return Promise.reject(new NotConnectedError('Transport is not open'));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (this.failWrites) {
|
|
122
|
+
return Promise.reject(new Error('Simulated write failure'));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const byte of data) {
|
|
126
|
+
this.written.push(byte);
|
|
127
|
+
this.#receive(byte);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return Promise.resolve();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Takes bytes from the simulated ECU's reply.
|
|
135
|
+
*
|
|
136
|
+
* @param length - Number of bytes wanted.
|
|
137
|
+
* @param _timeoutMs - Ignored; the simulation answers immediately.
|
|
138
|
+
* @returns Exactly `length` bytes.
|
|
139
|
+
* @throws {@link TimeoutError} if the ECU has not produced that many bytes.
|
|
140
|
+
* @throws {@link NotConnectedError} if the transport is closed.
|
|
141
|
+
*/
|
|
142
|
+
read(length: number, _timeoutMs: number): Promise<Uint8Array> {
|
|
143
|
+
if (!this.#open) {
|
|
144
|
+
return Promise.reject(new NotConnectedError('Transport is not open'));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (this.#output.length < length) {
|
|
148
|
+
this.#output = [];
|
|
149
|
+
|
|
150
|
+
return Promise.reject(new TimeoutError('Simulated ECU is silent'));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return Promise.resolve(Uint8Array.from(this.#output.splice(0, length)));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#echo(byte: number): void {
|
|
157
|
+
if (this.silent) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (this.corruptNextEcho) {
|
|
162
|
+
this.corruptNextEcho = false;
|
|
163
|
+
this.#output.push(byte ^ 0xff);
|
|
164
|
+
} else {
|
|
165
|
+
this.#output.push(byte);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#receive(byte: number): void {
|
|
170
|
+
if (this.#state === 'writeValue') {
|
|
171
|
+
this.memory[this.#writeAddr] = byte;
|
|
172
|
+
this.#echo(byte);
|
|
173
|
+
this.#state = 'idle';
|
|
174
|
+
this.#latched = false;
|
|
175
|
+
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (this.#state === 'coarse2') {
|
|
180
|
+
this.#coarse = ((this.#firstCoarse & 0x03) << 14) | (byte << 6);
|
|
181
|
+
this.#lengthCode = (this.#firstCoarse >> 2) & 0x1f;
|
|
182
|
+
this.#echo(byte);
|
|
183
|
+
this.#state = 'idle';
|
|
184
|
+
this.#latched = true;
|
|
185
|
+
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (byte < 0x80) {
|
|
190
|
+
this.#firstCoarse = byte;
|
|
191
|
+
this.#echo(byte);
|
|
192
|
+
this.#state = 'coarse2';
|
|
193
|
+
this.#latched = false;
|
|
194
|
+
} else if (!this.#latched) {
|
|
195
|
+
// A command byte without a preceding coarse address is ignored.
|
|
196
|
+
} else if ((byte & 0xc0) === 0xc0) {
|
|
197
|
+
this.#streamRead(this.#coarse | (byte & 0x3f));
|
|
198
|
+
} else {
|
|
199
|
+
this.#writeAddr = this.#coarse | (byte & 0x3f);
|
|
200
|
+
this.#echo(byte);
|
|
201
|
+
this.#state = 'writeValue';
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
#streamRead(address: number): void {
|
|
206
|
+
if (this.silent) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let length = lengthFromCode(this.#lengthCode) ?? 0;
|
|
211
|
+
|
|
212
|
+
if (this.streamLimit !== undefined) {
|
|
213
|
+
length = Math.min(length, this.streamLimit);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (let i = 0; i < length; i++) {
|
|
217
|
+
this.#output.push(this.memory[(address + i) % MEMORY_SIZE] ?? 0);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
// Copyright (C) 2026 comm14cux-ts contributors
|
|
3
|
+
|
|
4
|
+
/** Byte-level link to the ECU. The protocol layer uses nothing else. */
|
|
5
|
+
export interface Transport {
|
|
6
|
+
open(): Promise<void>;
|
|
7
|
+
close(): Promise<void>;
|
|
8
|
+
write(data: Uint8Array): Promise<void>;
|
|
9
|
+
/** Resolves with exactly `length` bytes, or rejects with a TimeoutError. */
|
|
10
|
+
read(length: number, timeoutMs: number): Promise<Uint8Array>;
|
|
11
|
+
}
|