@robdobsn/raftjs 1.11.5 → 1.11.6

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.
@@ -37,7 +37,7 @@ export default class AttributeHandler {
37
37
  const msgDataStartIdx = msgBufIdx;
38
38
 
39
39
  // New attribute values (in order as they appear in the attributes JSON)
40
- let newAttrValues: number[][] = [];
40
+ let newAttrValues: (number | string)[][] = [];
41
41
  if ("c" in pollRespMetadata) {
42
42
 
43
43
  // Extract attribute values using custom handler
@@ -201,7 +201,7 @@ export default class AttributeHandler {
201
201
  }
202
202
  }
203
203
 
204
- private processMsgAttribute(attrDef: DeviceTypeAttribute, msgBuffer: Uint8Array, msgBufIdx: number, msgDataStartIdx: number): { values: number[], newMsgBufIdx: number} {
204
+ private processMsgAttribute(attrDef: DeviceTypeAttribute, msgBuffer: Uint8Array, msgBufIdx: number, msgDataStartIdx: number): { values: (number | string)[], newMsgBufIdx: number} {
205
205
 
206
206
  // Current field message string index
207
207
  let curFieldBufIdx = msgBufIdx;
@@ -253,11 +253,14 @@ export default class AttributeHandler {
253
253
 
254
254
  // Extract the value using python-struct
255
255
  const unpackValues = structUnpack(maskOnSignedValue ? attrTypesOnly.toUpperCase() : attrTypesOnly, attrBuf);
256
- let attrValues = unpackValues as number[];
256
+ let attrValues = unpackValues as (number | string)[];
257
257
 
258
258
  // Get number of bytes consumed
259
259
  const numBytesConsumed = structSizeOf(attrTypesOnly);
260
260
 
261
+ // Check if any values are strings (from 's' format) — skip numeric transforms for those
262
+ const hasStringValues = attrValues.some(v => typeof v === 'string');
263
+
261
264
  // // Check if sign extendable mask specified on signed value
262
265
  // if (mmSpecifiedOnSignedValue) {
263
266
  // const signBitMask = 1 << (signExtendableMaskSignPos - 1);
@@ -270,57 +273,57 @@ export default class AttributeHandler {
270
273
  // }
271
274
 
272
275
  // Check for XOR mask
273
- if ("x" in attrDef) {
276
+ if (!hasStringValues && "x" in attrDef) {
274
277
  const mask = typeof attrDef.x === "string" ? parseInt(attrDef.x, 16) : attrDef.x as number;
275
- attrValues = attrValues.map((value) => (value >>> 0) ^ mask);
278
+ attrValues = attrValues.map((value) => ((value as number) >>> 0) ^ mask);
276
279
  }
277
280
 
278
281
  // Check for AND mask
279
- if ("m" in attrDef) {
282
+ if (!hasStringValues && "m" in attrDef) {
280
283
  const mask = typeof attrDef.m === "string" ? parseInt(attrDef.m, 16) : attrDef.m as number;
281
- attrValues = attrValues.map((value) => (maskOnSignedValue ? this.signExtend(value, mask) : (value >>> 0) & mask));
284
+ attrValues = attrValues.map((value) => (maskOnSignedValue ? this.signExtend(value as number, mask) : ((value as number) >>> 0) & mask));
282
285
  }
283
286
 
284
287
  // Check for a sign-bit
285
- if ("sb" in attrDef) {
288
+ if (!hasStringValues && "sb" in attrDef) {
286
289
  const signBitPos = attrDef.sb as number;
287
290
  const signBitMask = 1 << signBitPos;
288
291
  if ("ss" in attrDef) {
289
292
  const signBitSubtract = attrDef.ss as number;
290
- attrValues = attrValues.map((value) => (value & signBitMask) ? signBitSubtract - value : value);
293
+ attrValues = attrValues.map((value) => ((value as number) & signBitMask) ? signBitSubtract - (value as number) : value);
291
294
  } else {
292
- attrValues = attrValues.map((value) => (value & signBitMask) ? value - (signBitMask << 1) : value);
295
+ attrValues = attrValues.map((value) => ((value as number) & signBitMask) ? (value as number) - (signBitMask << 1) : value);
293
296
  }
294
297
  }
295
298
 
296
299
  // Check for bit shift required
297
- if ("s" in attrDef && attrDef.s) {
300
+ if (!hasStringValues && "s" in attrDef && attrDef.s) {
298
301
  const bitshift = attrDef.s as number;
299
302
  if (bitshift > 0) {
300
- attrValues = attrValues.map((value) => (value >>> 0) >>> bitshift);
303
+ attrValues = attrValues.map((value) => ((value as number) >>> 0) >>> bitshift);
301
304
  } else if (bitshift < 0) {
302
- attrValues = attrValues.map((value) => (value >>> 0) << -bitshift);
305
+ attrValues = attrValues.map((value) => ((value as number) >>> 0) << -bitshift);
303
306
  }
304
307
  }
305
308
 
306
309
  // Check for divisor
307
- if ("d" in attrDef && attrDef.d) {
310
+ if (!hasStringValues && "d" in attrDef && attrDef.d) {
308
311
  const divisor = attrDef.d as number;
309
- attrValues = attrValues.map((value) => (value) / divisor);
312
+ attrValues = attrValues.map((value) => (value as number) / divisor);
310
313
  }
311
314
 
312
315
  // Check for value to add
313
- if ("a" in attrDef && attrDef.a !== undefined) {
316
+ if (!hasStringValues && "a" in attrDef && attrDef.a !== undefined) {
314
317
  const addValue = attrDef.a as number;
315
- attrValues = attrValues.map((value) => (value) + addValue);
318
+ attrValues = attrValues.map((value) => (value as number) + addValue);
316
319
  }
317
320
 
318
321
  // Apply lookup table if defined
319
- if ("lut" in attrDef && attrDef.lut !== undefined) {
322
+ if (!hasStringValues && "lut" in attrDef && attrDef.lut !== undefined) {
320
323
  attrValues = attrValues.map((value): number => {
321
324
  // Skip NaN values
322
- if (isNaN(value)) {
323
- return value;
325
+ if (isNaN(value as number)) {
326
+ return value as number;
324
327
  }
325
328
 
326
329
  // Search through the lookup table rows for a match
@@ -334,7 +337,7 @@ export default class AttributeHandler {
334
337
  }
335
338
 
336
339
  // Parse the range string
337
- if (this.isValueInRangeString(value, row.r)) {
340
+ if (this.isValueInRangeString(value as number, row.r)) {
338
341
  return row.v;
339
342
  }
340
343
  }
@@ -345,7 +348,7 @@ export default class AttributeHandler {
345
348
  }
346
349
 
347
350
  // Otherwise keep the original value
348
- return value;
351
+ return value as number;
349
352
  });
350
353
  }
351
354
 
@@ -23,7 +23,7 @@ export interface DeviceDecodedData {
23
23
  deviceAddress: string;
24
24
  deviceType: string;
25
25
  attrGroupName?: string;
26
- attrValues: Record<string, number[]>;
26
+ attrValues: Record<string, (number | string)[]>;
27
27
  timestampsUs: number[];
28
28
  markers?: Record<string, unknown>;
29
29
  fromOfflineBuffer?: boolean;
@@ -956,7 +956,7 @@ export class DeviceManager implements RaftDeviceMgrIF{
956
956
  return;
957
957
  }
958
958
 
959
- const attrValues: Record<string, number[]> = {};
959
+ const attrValues: Record<string, (number | string)[]> = {};
960
960
  let hasValues = false;
961
961
 
962
962
  pollRespMetadata.a.forEach((attr) => {
@@ -14,10 +14,14 @@ export function deviceAttrGetLatestFormatted(attrState: DeviceAttributeState): s
14
14
  if (attrState.values.length === 0) {
15
15
  return 'N/A';
16
16
  }
17
+ const value = attrState.values[attrState.values.length - 1];
18
+ // String values are returned directly
19
+ if (typeof value === 'string') {
20
+ return value;
21
+ }
17
22
  if (attrState.format.length === 0) {
18
- return attrState.values[attrState.values.length - 1].toString();
23
+ return value.toString();
19
24
  }
20
- const value = attrState.values[attrState.values.length - 1];
21
25
  let format = attrState.format;
22
26
  if (format.startsWith("%")) {
23
27
  format = format.slice(1);
@@ -52,7 +56,7 @@ export interface DeviceAttributeState {
52
56
  newAttribute: boolean;
53
57
  newData: boolean;
54
58
  numNewValues: number;
55
- values: number[];
59
+ values: (number | string)[];
56
60
  units: string;
57
61
  range: number[];
58
62
  format: string;
@@ -0,0 +1,229 @@
1
+ import { structUnpack, structPack, structSizeOf } from "./RaftStruct";
2
+
3
+ // Helper to create Uint8Array from byte values
4
+ function bytes(...vals: number[]): Uint8Array {
5
+ return new Uint8Array(vals);
6
+ }
7
+
8
+ // ===== Existing functionality (regression tests) =====
9
+
10
+ describe("structUnpack", () => {
11
+ test("single B", () => {
12
+ expect(structUnpack("B", bytes(0xff))).toEqual([255]);
13
+ });
14
+
15
+ test("single b (signed)", () => {
16
+ expect(structUnpack("b", bytes(0xff))).toEqual([-1]);
17
+ });
18
+
19
+ test("<H little-endian uint16", () => {
20
+ expect(structUnpack("<H", bytes(0x01, 0x02))).toEqual([0x0201]);
21
+ });
22
+
23
+ test(">H big-endian uint16", () => {
24
+ expect(structUnpack(">H", bytes(0x01, 0x02))).toEqual([0x0102]);
25
+ });
26
+
27
+ test("BBBB four bytes", () => {
28
+ expect(structUnpack("BBBB", bytes(1, 2, 3, 4))).toEqual([1, 2, 3, 4]);
29
+ });
30
+
31
+ test("<I little-endian uint32", () => {
32
+ const buf = bytes(0x78, 0x56, 0x34, 0x12);
33
+ expect(structUnpack("<I", buf)).toEqual([0x12345678]);
34
+ });
35
+
36
+ test(">I big-endian uint32", () => {
37
+ const buf = bytes(0x12, 0x34, 0x56, 0x78);
38
+ expect(structUnpack(">I", buf)).toEqual([0x12345678]);
39
+ });
40
+
41
+ test("[N] bracket repeat", () => {
42
+ expect(structUnpack("B[3]", bytes(10, 20, 30))).toEqual([10, 20, 30]);
43
+ });
44
+
45
+ test("x padding skips byte", () => {
46
+ expect(structUnpack("xB", bytes(0xff, 0x42))).toEqual([0x42]);
47
+ });
48
+
49
+ test("<f float32", () => {
50
+ const buf = new Uint8Array(4);
51
+ new DataView(buf.buffer).setFloat32(0, 3.14, true);
52
+ const result = structUnpack("<f", buf);
53
+ expect(result[0]).toBeCloseTo(3.14, 2);
54
+ });
55
+ });
56
+
57
+ // ===== Prefix digit syntax =====
58
+
59
+ describe("prefix digit syntax", () => {
60
+ test("3B equivalent to BBB", () => {
61
+ expect(structUnpack("3B", bytes(1, 2, 3))).toEqual([1, 2, 3]);
62
+ });
63
+
64
+ test("2H two uint16s", () => {
65
+ expect(structUnpack(">2H", bytes(0, 1, 0, 2))).toEqual([1, 2]);
66
+ });
67
+
68
+ test("prefix and bracket multiply", () => {
69
+ // 2B[3] means repeat=6
70
+ expect(structUnpack("2B[3]", bytes(1, 2, 3, 4, 5, 6))).toEqual([1, 2, 3, 4, 5, 6]);
71
+ });
72
+
73
+ test("prefix digits in structSizeOf", () => {
74
+ expect(structSizeOf("3B")).toBe(3);
75
+ expect(structSizeOf(">2H")).toBe(4);
76
+ expect(structSizeOf("2I")).toBe(8);
77
+ });
78
+
79
+ test("prefix digits in structPack", () => {
80
+ const packed = structPack("3B", [10, 20, 30]);
81
+ expect(Array.from(packed)).toEqual([10, 20, 30]);
82
+ });
83
+
84
+ test("multi-digit prefix count", () => {
85
+ expect(structSizeOf("16s")).toBe(16);
86
+ expect(structSizeOf("10B")).toBe(10);
87
+ });
88
+ });
89
+
90
+ // ===== s format code =====
91
+
92
+ describe("s format (byte strings)", () => {
93
+ test("unpack 5s reads a string", () => {
94
+ const buf = bytes(0x48, 0x65, 0x6c, 0x6c, 0x6f); // "Hello"
95
+ expect(structUnpack("5s", buf)).toEqual(["Hello"]);
96
+ });
97
+
98
+ test("unpack s with trailing nulls trims them", () => {
99
+ const buf = bytes(0x48, 0x69, 0x00, 0x00, 0x00); // "Hi\0\0\0"
100
+ expect(structUnpack("5s", buf)).toEqual(["Hi"]);
101
+ });
102
+
103
+ test("unpack s produces one value regardless of count", () => {
104
+ const buf = bytes(0x41, 0x42, 0x43); // "ABC"
105
+ const result = structUnpack("3s", buf);
106
+ expect(result).toHaveLength(1);
107
+ expect(result[0]).toBe("ABC");
108
+ });
109
+
110
+ test("s combined with other types", () => {
111
+ // B then 3s then B
112
+ const buf = bytes(0xff, 0x41, 0x42, 0x43, 0x01);
113
+ const result = structUnpack("B3sB", buf);
114
+ expect(result).toEqual([255, "ABC", 1]);
115
+ });
116
+
117
+ test("structSizeOf with s", () => {
118
+ expect(structSizeOf("5s")).toBe(5);
119
+ expect(structSizeOf("B5sH")).toBe(8); // 1 + 5 + 2
120
+ expect(structSizeOf(">16s")).toBe(16);
121
+ });
122
+
123
+ test("structPack with string value", () => {
124
+ const packed = structPack("5s", ["Hello"]);
125
+ expect(Array.from(packed)).toEqual([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
126
+ });
127
+
128
+ test("structPack s zero-pads short strings", () => {
129
+ const packed = structPack("5s", ["Hi"]);
130
+ expect(Array.from(packed)).toEqual([0x48, 0x69, 0x00, 0x00, 0x00]);
131
+ });
132
+
133
+ test("structPack s with Uint8Array value", () => {
134
+ const packed = structPack("3s", [bytes(0x01, 0x02, 0x03)]);
135
+ expect(Array.from(packed)).toEqual([1, 2, 3]);
136
+ });
137
+
138
+ test("structPack s combined with numeric types", () => {
139
+ const packed = structPack("B3sB", [0xff, "ABC", 0x01]);
140
+ expect(Array.from(packed)).toEqual([0xff, 0x41, 0x42, 0x43, 0x01]);
141
+ });
142
+
143
+ test("round-trip pack/unpack s", () => {
144
+ const original = "Test";
145
+ const packed = structPack("8s", [original]);
146
+ const [unpacked] = structUnpack("8s", packed);
147
+ expect(unpacked).toBe(original);
148
+ });
149
+ });
150
+
151
+ // ===== q/Q 64-bit integers =====
152
+
153
+ describe("q/Q 64-bit integers", () => {
154
+ test("unpack >q signed 64-bit", () => {
155
+ // 0x0000000000000001 = 1
156
+ const buf = bytes(0, 0, 0, 0, 0, 0, 0, 1);
157
+ expect(structUnpack(">q", buf)).toEqual([1]);
158
+ });
159
+
160
+ test("unpack >Q unsigned 64-bit", () => {
161
+ const buf = bytes(0, 0, 0, 0, 0, 0, 0, 42);
162
+ expect(structUnpack(">Q", buf)).toEqual([42]);
163
+ });
164
+
165
+ test("unpack <q little-endian", () => {
166
+ const buf = new Uint8Array(8);
167
+ new DataView(buf.buffer).setBigInt64(0, BigInt(-1000), true);
168
+ expect(structUnpack("<q", buf)).toEqual([-1000]);
169
+ });
170
+
171
+ test("unpack >q negative", () => {
172
+ const buf = new Uint8Array(8);
173
+ new DataView(buf.buffer).setBigInt64(0, BigInt(-1), false);
174
+ expect(structUnpack(">q", buf)).toEqual([-1]);
175
+ });
176
+
177
+ test("structSizeOf q and Q", () => {
178
+ expect(structSizeOf("q")).toBe(8);
179
+ expect(structSizeOf("Q")).toBe(8);
180
+ expect(structSizeOf(">qQ")).toBe(16);
181
+ });
182
+
183
+ test("structPack >q", () => {
184
+ const packed = structPack(">q", [1]);
185
+ expect(Array.from(packed)).toEqual([0, 0, 0, 0, 0, 0, 0, 1]);
186
+ });
187
+
188
+ test("structPack <Q", () => {
189
+ const packed = structPack("<Q", [256]);
190
+ const view = new DataView(packed.buffer);
191
+ expect(Number(view.getBigUint64(0, true))).toBe(256);
192
+ });
193
+
194
+ test("round-trip q", () => {
195
+ const packed = structPack(">q", [-123456789]);
196
+ const [unpacked] = structUnpack(">q", packed);
197
+ expect(unpacked).toBe(-123456789);
198
+ });
199
+
200
+ test("round-trip Q", () => {
201
+ const packed = structPack(">Q", [123456789]);
202
+ const [unpacked] = structUnpack(">Q", packed);
203
+ expect(unpacked).toBe(123456789);
204
+ });
205
+ });
206
+
207
+ // ===== Error handling =====
208
+
209
+ describe("error handling", () => {
210
+ test("unknown format code throws", () => {
211
+ expect(() => structUnpack("Z", bytes(0))).toThrow("Unknown format character");
212
+ });
213
+
214
+ test("missing ] throws", () => {
215
+ expect(() => structUnpack("B[3", bytes(1, 2, 3))).toThrow("missing closing ]");
216
+ });
217
+
218
+ test("digit without format code throws", () => {
219
+ expect(() => structUnpack("3", bytes(1, 2, 3))).toThrow("Expected format code after prefix count");
220
+ });
221
+
222
+ test("structPack insufficient values throws", () => {
223
+ expect(() => structPack("HH", [1])).toThrow("Insufficient values");
224
+ });
225
+
226
+ test("structPack s with number throws", () => {
227
+ expect(() => structPack("3s", [42])).toThrow("Expected string or Uint8Array");
228
+ });
229
+ });
package/src/RaftStruct.ts CHANGED
@@ -33,22 +33,40 @@ function parseFormatInstructions(format: string): FormatInstruction[] {
33
33
  continue;
34
34
  }
35
35
 
36
+ // Check for prefix digit count (e.g. 3H, 16s)
37
+ let repeat = 1;
38
+ if (/\d/.test(char)) {
39
+ let numStr = char;
40
+ idx++;
41
+ while (idx < format.length && /\d/.test(format[idx])) {
42
+ numStr += format[idx];
43
+ idx++;
44
+ }
45
+ repeat = parseInt(numStr, 10);
46
+ if (!Number.isFinite(repeat) || repeat <= 0) {
47
+ throw new Error(`Invalid prefix count "${numStr}" in format string "${format}"`);
48
+ }
49
+ if (idx >= format.length || /[\s<>\d]/.test(format[idx])) {
50
+ throw new Error(`Expected format code after prefix count "${numStr}" in format string "${format}"`);
51
+ }
52
+ }
53
+
36
54
  // Attribute code
37
- const code = char;
38
- idx++;
55
+ const code = /\d/.test(char) ? format[idx] : char;
56
+ if (/\d/.test(char)) idx++; else idx++;
39
57
 
40
- // Check for repeat count using [N] syntax
41
- let repeat = 1;
58
+ // Check for [N] suffix count (e.g. B[3]) — multiplied with any prefix count
42
59
  if (idx < format.length && format[idx] === "[") {
43
60
  const endIdx = format.indexOf("]", idx + 1);
44
61
  if (endIdx === -1) {
45
62
  throw new Error(`Invalid format string: missing closing ] in "${format}"`);
46
63
  }
47
64
  const repeatStr = format.slice(idx + 1, endIdx);
48
- repeat = parseInt(repeatStr, 10);
49
- if (!Number.isFinite(repeat) || repeat <= 0) {
65
+ const bracketRepeat = parseInt(repeatStr, 10);
66
+ if (!Number.isFinite(bracketRepeat) || bracketRepeat <= 0) {
50
67
  throw new Error(`Invalid repeat count "${repeatStr}" in format string "${format}"`);
51
68
  }
69
+ repeat *= bracketRepeat;
52
70
  idx = endIdx + 1;
53
71
  }
54
72
 
@@ -58,9 +76,9 @@ function parseFormatInstructions(format: string): FormatInstruction[] {
58
76
  return instructions;
59
77
  }
60
78
 
61
- export function structUnpack(format: string, data: Uint8Array): number[] {
79
+ export function structUnpack(format: string, data: Uint8Array): (number | string)[] {
62
80
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
63
- const results: number[] = [];
81
+ const results: (number | string)[] = [];
64
82
  let offset = 0;
65
83
  let littleEndian = false;
66
84
 
@@ -107,14 +125,14 @@ export function structUnpack(format: string, data: Uint8Array): number[] {
107
125
  results.push(view.getUint32(offset, littleEndian));
108
126
  offset += 4;
109
127
  break;
110
- // case "q": // Signed 64-bit integer
111
- // results.push(Number(view.getBigInt64(offset, littleEndian)));
112
- // offset += 8;
113
- // break;
114
- // case "Q": // Unsigned 64-bit integer
115
- // results.push(Number(view.getBigUint64(offset, littleEndian)));
116
- // offset += 8;
117
- // break;
128
+ case "q": // Signed 64-bit integer
129
+ results.push(Number(view.getBigInt64(offset, littleEndian)));
130
+ offset += 8;
131
+ break;
132
+ case "Q": // Unsigned 64-bit integer
133
+ results.push(Number(view.getBigUint64(offset, littleEndian)));
134
+ offset += 8;
135
+ break;
118
136
  case "f": // 32-bit float
119
137
  results.push(view.getFloat32(offset, littleEndian));
120
138
  offset += 4;
@@ -123,9 +141,20 @@ export function structUnpack(format: string, data: Uint8Array): number[] {
123
141
  results.push(view.getFloat64(offset, littleEndian));
124
142
  offset += 8;
125
143
  break;
144
+ case "s": { // Byte string (repeat = byte length, produces one string value)
145
+ const bytes = data.slice(offset, offset + repeat);
146
+ // Trim trailing null bytes for C-string compatibility
147
+ let end = bytes.length;
148
+ while (end > 0 && bytes[end - 1] === 0) end--;
149
+ results.push(new TextDecoder().decode(bytes.subarray(0, end)));
150
+ offset += repeat;
151
+ break;
152
+ }
126
153
  default:
127
154
  throw new Error(`Unknown format character: ${code}`);
128
155
  }
156
+ // For 's', the repeat is consumed as byte-length in one go
157
+ if (code === "s") break;
129
158
  }
130
159
  }
131
160
 
@@ -162,16 +191,19 @@ export function structSizeOf(format: string): number {
162
191
  case "L": // Unsigned 32-bit integer
163
192
  unitSize = 4;
164
193
  break;
165
- // case "q": // Signed 64-bit integer
166
- // case "Q": // Unsigned 64-bit integer
167
- // unitSize = 8;
168
- // break;
194
+ case "q": // Signed 64-bit integer
195
+ case "Q": // Unsigned 64-bit integer
196
+ unitSize = 8;
197
+ break;
169
198
  case "f": // 32-bit float
170
199
  unitSize = 4;
171
200
  break;
172
201
  case "d": // 64-bit float
173
202
  unitSize = 8;
174
203
  break;
204
+ case "s": // Byte string (repeat = byte length)
205
+ size += repeat;
206
+ continue;
175
207
  default:
176
208
  throw new Error(`Unknown format character: ${code}`);
177
209
  }
@@ -181,7 +213,7 @@ export function structSizeOf(format: string): number {
181
213
  return size;
182
214
  }
183
215
 
184
- export function structPack(format: string, values: number[]): Uint8Array {
216
+ export function structPack(format: string, values: (number | string | Uint8Array)[]): Uint8Array {
185
217
  const size = structSizeOf(format);
186
218
  const buffer = new ArrayBuffer(size);
187
219
  const view = new DataView(buffer);
@@ -209,7 +241,7 @@ export function structPack(format: string, values: number[]): Uint8Array {
209
241
  if (valueIdx >= values.length) {
210
242
  throw new Error("Insufficient values provided for structPack");
211
243
  }
212
- view.setInt8(offset, values[valueIdx++]);
244
+ view.setInt8(offset, values[valueIdx++] as number);
213
245
  offset += 1;
214
246
  break;
215
247
  case "B": // Unsigned 8-bit integer
@@ -217,21 +249,21 @@ export function structPack(format: string, values: number[]): Uint8Array {
217
249
  if (valueIdx >= values.length) {
218
250
  throw new Error("Insufficient values provided for structPack");
219
251
  }
220
- view.setUint8(offset, values[valueIdx++]);
252
+ view.setUint8(offset, values[valueIdx++] as number);
221
253
  offset += 1;
222
254
  break;
223
255
  case "h": // Signed 16-bit integer
224
256
  if (valueIdx >= values.length) {
225
257
  throw new Error("Insufficient values provided for structPack");
226
258
  }
227
- view.setInt16(offset, values[valueIdx++], littleEndian);
259
+ view.setInt16(offset, values[valueIdx++] as number, littleEndian);
228
260
  offset += 2;
229
261
  break;
230
262
  case "H": // Unsigned 16-bit integer
231
263
  if (valueIdx >= values.length) {
232
264
  throw new Error("Insufficient values provided for structPack");
233
265
  }
234
- view.setUint16(offset, values[valueIdx++], littleEndian);
266
+ view.setUint16(offset, values[valueIdx++] as number, littleEndian);
235
267
  offset += 2;
236
268
  break;
237
269
  case "i": // Signed 32-bit integer
@@ -239,7 +271,7 @@ export function structPack(format: string, values: number[]): Uint8Array {
239
271
  if (valueIdx >= values.length) {
240
272
  throw new Error("Insufficient values provided for structPack");
241
273
  }
242
- view.setInt32(offset, values[valueIdx++], littleEndian);
274
+ view.setInt32(offset, values[valueIdx++] as number, littleEndian);
243
275
  offset += 4;
244
276
  break;
245
277
  case "I": // Unsigned 32-bit integer
@@ -247,34 +279,66 @@ export function structPack(format: string, values: number[]): Uint8Array {
247
279
  if (valueIdx >= values.length) {
248
280
  throw new Error("Insufficient values provided for structPack");
249
281
  }
250
- view.setUint32(offset, values[valueIdx++], littleEndian);
282
+ view.setUint32(offset, values[valueIdx++] as number, littleEndian);
251
283
  offset += 4;
252
284
  break;
253
- // case "q": // Signed 64-bit integer
254
- // view.setBigInt64(offset, BigInt(values[valueIdx++]), littleEndian);
255
- // offset += 8;
256
- // break;
257
- // case "Q": // Unsigned 64-bit integer
258
- // view.setBigUint64(offset, BigInt(values[valueIdx++]), littleEndian);
259
- // offset += 8;
260
- // break;
285
+ case "q": // Signed 64-bit integer
286
+ if (valueIdx >= values.length) {
287
+ throw new Error("Insufficient values provided for structPack");
288
+ }
289
+ view.setBigInt64(offset, BigInt(values[valueIdx++] as number), littleEndian);
290
+ offset += 8;
291
+ break;
292
+ case "Q": // Unsigned 64-bit integer
293
+ if (valueIdx >= values.length) {
294
+ throw new Error("Insufficient values provided for structPack");
295
+ }
296
+ view.setBigUint64(offset, BigInt(values[valueIdx++] as number), littleEndian);
297
+ offset += 8;
298
+ break;
261
299
  case "f": // 32-bit float
262
300
  if (valueIdx >= values.length) {
263
301
  throw new Error("Insufficient values provided for structPack");
264
302
  }
265
- view.setFloat32(offset, values[valueIdx++], littleEndian);
303
+ view.setFloat32(offset, values[valueIdx++] as number, littleEndian);
266
304
  offset += 4;
267
305
  break;
268
306
  case "d": // 64-bit float
269
307
  if (valueIdx >= values.length) {
270
308
  throw new Error("Insufficient values provided for structPack");
271
309
  }
272
- view.setFloat64(offset, values[valueIdx++], littleEndian);
310
+ view.setFloat64(offset, values[valueIdx++] as number, littleEndian);
273
311
  offset += 8;
274
312
  break;
313
+ case "s": { // Byte string (repeat = byte length, consumes one value)
314
+ if (valueIdx >= values.length) {
315
+ throw new Error("Insufficient values provided for structPack");
316
+ }
317
+ const val = values[valueIdx++];
318
+ let bytes: Uint8Array;
319
+ if (val instanceof Uint8Array) {
320
+ bytes = val;
321
+ } else if (typeof val === "string") {
322
+ bytes = new TextEncoder().encode(val);
323
+ } else {
324
+ throw new Error(`Expected string or Uint8Array for 's' format, got number`);
325
+ }
326
+ // Copy bytes, zero-pad if shorter than repeat
327
+ const copyLen = Math.min(bytes.length, repeat);
328
+ for (let j = 0; j < copyLen; j++) {
329
+ view.setUint8(offset + j, bytes[j]);
330
+ }
331
+ for (let j = copyLen; j < repeat; j++) {
332
+ view.setUint8(offset + j, 0);
333
+ }
334
+ offset += repeat;
335
+ break;
336
+ }
275
337
  default:
276
338
  throw new Error(`Unknown format character: ${code}`);
277
339
  }
340
+ // For 's', the repeat is consumed as byte-length in one go
341
+ if (code === "s") break;
278
342
  }
279
343
  }
280
344