layout-data 0.2.0 → 0.3.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/src/Ltag.js ADDED
@@ -0,0 +1,87 @@
1
+ import { EventEmitter } from "node:events";
2
+
3
+ /** @import { Endian, Tag_Listener, Tag_Type } from "./typedefs.js" */
4
+ /** @import { Layout_Entry } from "./internal_types.js" */
5
+
6
+ /**
7
+ * The interface of one tag of an Ldata (doc/spec-data-api.md §3). It holds no
8
+ * value: reading `value` decodes it from the buffer of the Ldata, writing it
9
+ * is the same as `ldata[name] = value`, and `load` / `store` are `load_tag` /
10
+ * `store_tag` of the Ldata for the name of the tag. `change` events
11
+ * (old_value, new_value) are emitted by the Observer of the Ldata.
12
+ */
13
+ export class Ltag extends EventEmitter {
14
+ /** @type {import("./Ldata.js").Ldata} */
15
+ #ldata;
16
+ /** @type {Layout_Entry} */
17
+ #entry;
18
+
19
+ /**
20
+ * @param {import("./Ldata.js").Ldata} ldata - The Ldata the tag belongs to.
21
+ * @param {Layout_Entry} entry - The compiled entry of the tag.
22
+ */
23
+ constructor(ldata, entry) {
24
+ super();
25
+ this.#ldata = ldata;
26
+ this.#entry = entry;
27
+ this.setMaxListeners(20);
28
+ }
29
+
30
+ /**
31
+ * Subscribes a listener; `change` is typed (old_value, new_value), any
32
+ * listener for another event name (doc/spec-data-api.md §3).
33
+ *
34
+ * @template {string | symbol} E
35
+ * @param {E} event
36
+ * @param {Tag_Listener<E>} listener
37
+ * @returns {this}
38
+ */
39
+ on(event, listener) { return super.on(event, listener); }
40
+
41
+ /**
42
+ * Subscribes a listener for one emission, typed like `on`.
43
+ *
44
+ * @template {string | symbol} E
45
+ * @param {E} event
46
+ * @param {Tag_Listener<E>} listener
47
+ * @returns {this}
48
+ */
49
+ once(event, listener) { return super.once(event, listener); }
50
+
51
+ /**
52
+ * Removes a listener, typed like `on`.
53
+ *
54
+ * @template {string | symbol} E
55
+ * @param {E} event
56
+ * @param {Tag_Listener<E>} listener
57
+ * @returns {this}
58
+ */
59
+ off(event, listener) { return super.off(event, listener); }
60
+
61
+ /** @returns {string} */
62
+ get name() { return this.#entry.name; }
63
+ /** @returns {Tag_Type} Lower case type name. */
64
+ get type() { return this.#entry.type; }
65
+ /** @returns {number} Size in bytes; a Bool has the byte it lies in. */
66
+ get size() { return this.#entry.size; }
67
+ /** @returns {number | boolean} */
68
+ get value() { return this.#ldata[this.#entry.name]; }
69
+ /** @param {number | boolean} value */
70
+ set value(value) { this.#ldata[this.#entry.name] = value; }
71
+
72
+ /**
73
+ * Same as `ldata.load_tag(name, io_buffer, endian)` (doc/spec-data-api.md §2.9).
74
+ *
75
+ * @param {Uint8Array} io_buffer
76
+ * @param {Endian} endian
77
+ */
78
+ load(io_buffer, endian) { this.#ldata.load_tag(this.#entry.name, io_buffer, endian); }
79
+
80
+ /**
81
+ * Same as `ldata.store_tag(name, io_buffer, endian)` (doc/spec-data-api.md §2.9).
82
+ *
83
+ * @param {Uint8Array} io_buffer
84
+ * @param {Endian} endian
85
+ */
86
+ store(io_buffer, endian) { this.#ldata.store_tag(this.#entry.name, io_buffer, endian); }
87
+ }
package/src/codecs.js ADDED
@@ -0,0 +1,64 @@
1
+ // Codecs of the tag types on a big-endian buffer (doc/design.md §4).
2
+ // `byte` is the byte offset of the tag; `mask` is the bit mask of a Bool.
3
+ // Device byte order is handled at the IO boundary, not here.
4
+
5
+ /** The largest finite float32 value; a finite number above it is refused by a Real. */
6
+ export const FLOAT32_MAX = 3.4028234663852886e38;
7
+
8
+ const read_bool = (buffer, byte, mask) => (buffer[byte] & mask) !== 0;
9
+ const write_bool = (buffer, byte, mask, value) => {
10
+ buffer[byte] = value ? buffer[byte] | mask : buffer[byte] & ~mask;
11
+ };
12
+
13
+ /** A Bool takes any value, by truthiness. */
14
+ const check_bool = () => null;
15
+
16
+ /**
17
+ * The check of an integer type: a number that is an integer inside the range.
18
+ *
19
+ * @param {number} min
20
+ * @param {number} max
21
+ * @returns {(value: any) => string | null}
22
+ */
23
+ const check_integer = (min, max) => (value) => {
24
+ if (typeof value !== "number") return `a ${typeof value} is not a number`;
25
+ if (!Number.isInteger(value)) return `${value} is not an integer`;
26
+ if (value < min || value > max) return `${value} is outside ${min}..${max}`;
27
+ return null;
28
+ };
29
+
30
+ /** The check of a Real: any number, NaN and the infinities included, but no finite value beyond float32. */
31
+ const check_real = (value) => {
32
+ if (typeof value !== "number") return `a ${typeof value} is not a number`;
33
+ if (Number.isFinite(value) && Math.abs(value) > FLOAT32_MAX) return `${value} is beyond the float32 range`;
34
+ return null;
35
+ };
36
+
37
+ const check_byte = check_integer(0, 0xff);
38
+ const check_sint = check_integer(-0x80, 0x7f);
39
+ const check_int = check_integer(-0x8000, 0x7fff);
40
+ const check_uint = check_integer(0, 0xffff);
41
+ const check_dint = check_integer(-0x80000000, 0x7fffffff);
42
+ const check_udint = check_integer(0, 0xffffffff);
43
+
44
+ /**
45
+ * Codec of each tag type (lower case): its size in bytes, `check(value)`, which
46
+ * returns null for a value that can be written and the reason otherwise,
47
+ * `get(buffer, byte, mask)` and `set(buffer, byte, mask, value)`. `set` is
48
+ * called with a checked value only (doc/spec-data-api.md §2.3).
49
+ *
50
+ * @type {Record<string, import("./internal_types.js").Codec>}
51
+ */
52
+ export const CODECS = {
53
+ bool: { size: 1, check: check_bool, get: read_bool, set: write_bool },
54
+ byte: { size: 1, check: check_byte, get: (b, i) => b.readUInt8(i), set: (b, i, _m, v) => b.writeUInt8(v, i) },
55
+ usint: { size: 1, check: check_byte, get: (b, i) => b.readUInt8(i), set: (b, i, _m, v) => b.writeUInt8(v, i) },
56
+ sint: { size: 1, check: check_sint, get: (b, i) => b.readInt8(i), set: (b, i, _m, v) => b.writeInt8(v, i) },
57
+ int: { size: 2, check: check_int, get: (b, i) => b.readInt16BE(i), set: (b, i, _m, v) => b.writeInt16BE(v, i) },
58
+ uint: { size: 2, check: check_uint, get: (b, i) => b.readUInt16BE(i), set: (b, i, _m, v) => b.writeUInt16BE(v, i) },
59
+ word: { size: 2, check: check_uint, get: (b, i) => b.readUInt16BE(i), set: (b, i, _m, v) => b.writeUInt16BE(v, i) },
60
+ dint: { size: 4, check: check_dint, get: (b, i) => b.readInt32BE(i), set: (b, i, _m, v) => b.writeInt32BE(v, i) },
61
+ udint: { size: 4, check: check_udint, get: (b, i) => b.readUInt32BE(i), set: (b, i, _m, v) => b.writeUInt32BE(v, i) },
62
+ dword: { size: 4, check: check_udint, get: (b, i) => b.readUInt32BE(i), set: (b, i, _m, v) => b.writeUInt32BE(v, i) },
63
+ real: { size: 4, check: check_real, get: (b, i) => b.readFloatBE(i), set: (b, i, _m, v) => b.writeFloatBE(v, i) },
64
+ };
@@ -0,0 +1,23 @@
1
+ // Dispatch of events that does not stop at a listener that throws
2
+ // (doc/spec-data-api.md §4.4, doc/design.md §5.1). Internal.
3
+
4
+ /**
5
+ * Calls every listener of an event, in the order they subscribed, and pushes
6
+ * what a listener throws to `errors` instead of letting it stop the others.
7
+ * A `once` listener removes itself as usual.
8
+ *
9
+ * @param {import("node:events").EventEmitter | undefined} emitter - Nothing happens for undefined.
10
+ * @param {string} event
11
+ * @param {any[]} args
12
+ * @param {unknown[]} errors
13
+ */
14
+ export function emit_all(emitter, event, args, errors) {
15
+ if (!emitter) return;
16
+ for (const listener of emitter.rawListeners(event)) {
17
+ try {
18
+ listener.apply(emitter, args);
19
+ } catch (error) {
20
+ errors.push(error);
21
+ }
22
+ }
23
+ }
package/src/endian.js ADDED
@@ -0,0 +1,197 @@
1
+ // Conversion between the device byte order of an IO buffer and the big-endian
2
+ // buffer of an Ldata, per compiled layout entry (doc/spec-data-api.md §2.9,
3
+ // doc/design.md §6). Device byte order acts per 16-bit register and per 32-bit
4
+ // value; the IO buffer is indexed like the Ldata buffer. Only io_index_table is
5
+ // exported by the package: Ldata calls the other functions with its own buffer.
6
+ import { compiled_of } from "./registry.js";
7
+
8
+ /** @import { Endian, Layout } from "./typedefs.js" */
9
+ /** @import { Layout_Entry } from "./internal_types.js" */
10
+
11
+ // The device byte orders; there are no aliases.
12
+ const ENDIANS = ["BE", "LE", "BEBS", "LEBS"];
13
+
14
+ // Endians that swap the two bytes of a 16-bit register.
15
+ const BYTE_SWAP_ENDIANS = ["LE", "BEBS"];
16
+
17
+ /**
18
+ * Throws for a byte order that is not one of BE, LE, BEBS and LEBS, instead of
19
+ * treating it as BE and corrupting the data silently.
20
+ *
21
+ * @param {string} endian
22
+ * @throws {RangeError}
23
+ */
24
+ export function check_endian(endian) {
25
+ if (!ENDIANS.includes(endian)) {
26
+ throw new RangeError(`unknown byte order ${endian}: use BE, LE, BEBS or LEBS`);
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Converts the endianness of a 2-byte value between the specified endian format and Big Endian (BE).
32
+ *
33
+ * This function performs bidirectional conversion:
34
+ * - If the data is not in BE, it converts it to BE.
35
+ * - If the data is in BE, it converts it to the specified endian format.
36
+ *
37
+ * @param {Uint8Array} buffer_from - The source buffer containing the data to be converted.
38
+ * @param {Uint8Array} buffer_to - The destination buffer where the converted data will be written.
39
+ * @param {number} index - The starting index in the buffers for the 2-byte value.
40
+ * @param {Endian} endian - The endian format to convert to/from.
41
+ */
42
+ function convert_endian_2byte(buffer_from, buffer_to, index, endian) {
43
+ if (BYTE_SWAP_ENDIANS.includes(endian)) { // LE, BEBS: BA
44
+ buffer_to[index] = buffer_from[index + 1];
45
+ buffer_to[index + 1] = buffer_from[index];
46
+ } else { // BE, LEBS: AB
47
+ buffer_to[index] = buffer_from[index];
48
+ buffer_to[index + 1] = buffer_from[index + 1];
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Converts the endianness of a 4-byte value between the specified endian format and Big Endian (BE).
54
+ *
55
+ * This function performs bidirectional conversion:
56
+ * - If the data is not in BE, it converts it to BE.
57
+ * - If the data is in BE, it converts it to the specified endian format.
58
+ *
59
+ * The function handles different endian formats:
60
+ * - LE (Little Endian): DCBA <-> ABCD
61
+ * - BEBS (Big Endian Byte Swap): BADC <-> ABCD
62
+ * - LEBS (Little Endian Byte Swap): CDAB <-> ABCD
63
+ *
64
+ * @param {Uint8Array} buffer_from - The source buffer containing the data to be converted.
65
+ * @param {Uint8Array} buffer_to - The destination buffer where the converted data will be written.
66
+ * @param {number} index - The starting index in the buffers for the 4-byte value.
67
+ * @param {Endian} endian - The endian format to convert to/from.
68
+ */
69
+ function convert_endian_4bytes(buffer_from, buffer_to, index, endian) {
70
+ switch (endian) {
71
+ case "LE": { // DCBA <-> ABCD
72
+ buffer_to[index] = buffer_from[index + 3];
73
+ buffer_to[index + 1] = buffer_from[index + 2];
74
+ buffer_to[index + 2] = buffer_from[index + 1];
75
+ buffer_to[index + 3] = buffer_from[index];
76
+ break;
77
+ }
78
+ case "BEBS": { // BADC <-> ABCD
79
+ buffer_to[index] = buffer_from[index + 1];
80
+ buffer_to[index + 1] = buffer_from[index];
81
+ buffer_to[index + 2] = buffer_from[index + 3];
82
+ buffer_to[index + 3] = buffer_from[index + 2];
83
+ break;
84
+ }
85
+ case "LEBS": { // CDAB <-> ABCD
86
+ buffer_to[index] = buffer_from[index + 2];
87
+ buffer_to[index + 1] = buffer_from[index + 3];
88
+ buffer_to[index + 2] = buffer_from[index];
89
+ buffer_to[index + 3] = buffer_from[index + 1];
90
+ break;
91
+ }
92
+ default: { // BE
93
+ for (let k = 0; k < 4; k++) buffer_to[index + k] = buffer_from[index + k];
94
+ }
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Index in a device-ordered IO buffer of the byte of a single-byte entry
100
+ * (Bool, Byte, SInt, USInt): when the endian swaps the register bytes, the
101
+ * byte sits in the other half of its register.
102
+ *
103
+ * @param {number} byte - Byte offset of the entry.
104
+ * @param {Endian} endian - Device byte order.
105
+ * @returns {number}
106
+ */
107
+ export function io_index(byte, endian) {
108
+ return BYTE_SWAP_ENDIANS.includes(endian) ? byte ^ 1 : byte;
109
+ }
110
+
111
+ /**
112
+ * The index in a device-ordered IO buffer of every byte of a data block: the
113
+ * byte `i` of the block is the byte `table[i]` of the IO buffer. The table
114
+ * makes loading and storing a whole block a plain byte copy, and it agrees
115
+ * with `load_entry` / `store_entry` for every entry (a Bool or a single-byte
116
+ * entry maps as `io_index`, a multi-byte entry as its endian).
117
+ *
118
+ * @param {Layout} layout - The compiled layout of the data block.
119
+ * @param {Endian} endian - Device byte order: BE, LE, BEBS or LEBS.
120
+ * @returns {Int32Array}
121
+ * @throws {RangeError} For another byte order.
122
+ */
123
+ export function io_index_table(layout, endian) {
124
+ check_endian(endian);
125
+ const table = new Int32Array(layout.size);
126
+ for (let i = 0; i < table.length; i++) table[i] = io_index(i, endian);
127
+ for (const { byte, size, type } of compiled_of(layout).entries.values()) {
128
+ if (size !== 4 || type === "bool") continue;
129
+ for (let k = 0; k < 4; k++) {
130
+ switch (endian) {
131
+ case "LE": table[byte + k] = byte + 3 - k; break;
132
+ case "BEBS": table[byte + k] = byte + (k ^ 1); break;
133
+ case "LEBS": table[byte + k] = byte + ((k + 2) % 4); break;
134
+ default: table[byte + k] = byte + k;
135
+ }
136
+ }
137
+ }
138
+ return table;
139
+ }
140
+
141
+ /**
142
+ * Byte range [start, end) an entry occupies in a device-ordered IO buffer.
143
+ *
144
+ * @param {Layout_Entry} entry
145
+ * @param {Endian} endian - Device byte order.
146
+ * @returns {{start: number, end: number}}
147
+ */
148
+ export function io_range(entry, endian) {
149
+ const start = entry.size === 1 ? io_index(entry.byte, endian) : entry.byte;
150
+ return { start, end: start + entry.size };
151
+ }
152
+
153
+ /**
154
+ * Copies an entry from a device-ordered IO buffer into a big-endian buffer.
155
+ * A Bool only changes its own bit. No change is detected here, and the byte
156
+ * order and the range in the IO buffer are not checked (Ldata does).
157
+ *
158
+ * @param {Layout_Entry} entry
159
+ * @param {Uint8Array} io_buffer - The device-ordered source.
160
+ * @param {Uint8Array} buffer - The big-endian destination.
161
+ * @param {Endian} endian - Device byte order.
162
+ */
163
+ export function load_entry(entry, io_buffer, buffer, endian) {
164
+ const { byte, size, mask } = entry;
165
+ if (entry.type === "bool") {
166
+ const value = io_buffer[io_index(byte, endian)] & mask;
167
+ buffer[byte] = value ? buffer[byte] | mask : buffer[byte] & ~mask;
168
+ } else if (size === 1) {
169
+ buffer[byte] = io_buffer[io_index(byte, endian)];
170
+ } else if (size === 2) {
171
+ convert_endian_2byte(io_buffer, buffer, byte, endian);
172
+ } else {
173
+ convert_endian_4bytes(io_buffer, buffer, byte, endian);
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Copies an entry from a big-endian buffer into a device-ordered IO buffer.
179
+ *
180
+ * @param {Layout_Entry} entry
181
+ * @param {Uint8Array} buffer - The big-endian source.
182
+ * @param {Uint8Array} io_buffer - The device-ordered destination.
183
+ * @param {Endian} endian - Device byte order.
184
+ */
185
+ export function store_entry(entry, buffer, io_buffer, endian) {
186
+ const { byte, size, mask } = entry;
187
+ if (entry.type === "bool") {
188
+ const index = io_index(byte, endian);
189
+ io_buffer[index] = buffer[byte] & mask ? io_buffer[index] | mask : io_buffer[index] & ~mask;
190
+ } else if (size === 1) {
191
+ io_buffer[io_index(byte, endian)] = buffer[byte];
192
+ } else if (size === 2) {
193
+ convert_endian_2byte(buffer, io_buffer, byte, endian);
194
+ } else {
195
+ convert_endian_4bytes(buffer, io_buffer, byte, endian);
196
+ }
197
+ }
package/src/index.js ADDED
@@ -0,0 +1,8 @@
1
+ // Public interface of the package (doc/design.md §1).
2
+ export { Ldata } from "./Ldata.js";
3
+ export { Ltag } from "./Ltag.js";
4
+ export { register_layout } from "./layout.js";
5
+ export { get_layout } from "./registry.js";
6
+ export { io_index_table } from "./endian.js";
7
+ export { render_types } from "./types.js";
8
+ export * from "./typedefs.js";
@@ -0,0 +1,47 @@
1
+ // Type definitions of the internals. This file has no run-time content, and
2
+ // index.js does not export it: the compiled data of a layout is not part of
3
+ // the public interface (doc/spec-data-api.md §6.6).
4
+
5
+ /**
6
+ * Codec of one tag type on a big-endian buffer (doc/design.md §4).
7
+ *
8
+ * @typedef {object} Codec
9
+ * @property {number} size - Size in bytes.
10
+ * @property {(value: any) => string | null} check - The reason a value is refused, null if it can be written.
11
+ * @property {(buffer: Buffer, byte: number, mask: number) => number | boolean} get
12
+ * @property {(buffer: Buffer, byte: number, mask: number, value: number | boolean) => void} set
13
+ */
14
+
15
+ /**
16
+ * One tag of a compiled layout.
17
+ *
18
+ * @typedef {object} Layout_Entry
19
+ * @property {string} name
20
+ * @property {import("./typedefs.js").Tag_Type} type - Lower case type name.
21
+ * @property {number | boolean} init_value
22
+ * @property {number} byte - Byte offset.
23
+ * @property {number} index - Position among the entries of the layout.
24
+ * @property {number} size - Size in bytes.
25
+ * @property {number} mask - Bit mask of a Bool, 0xff otherwise.
26
+ * @property {Codec} codec
27
+ */
28
+
29
+ /**
30
+ * The expanded structure of a data block (the result of build_structure).
31
+ *
32
+ * @typedef {object} Data_Struct
33
+ * @property {string} name
34
+ * @property {number} length - Total length in bits.
35
+ * @property {import("./typedefs.js").Layout_Item[]} items
36
+ */
37
+
38
+ /**
39
+ * The compiled data of a layout, kept apart from the public Layout
40
+ * (doc/design.md §3).
41
+ *
42
+ * @typedef {object} Compiled_Layout
43
+ * @property {Map<string, Layout_Entry>} entries
44
+ * @property {Layout_Entry[][]} tags_at_byte - The entries covering each byte.
45
+ */
46
+
47
+ export {};