@springfield/ham-radio-utils 3.2.6 → 4.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "title": "Radio Protocol DSL Schema",
4
- "description": "Schema for defining radio communication protocols in a declarative JSON format",
4
+ "description": "Schema for defining radio communication protocols as send/expect exchanges",
5
5
  "type": "object",
6
6
  "required": [
7
7
  "id",
@@ -169,7 +169,7 @@
169
169
  },
170
170
  "endAddress": {
171
171
  "type": "integer",
172
- "description": "Ending address (inclusive) of the memory segment",
172
+ "description": "Inclusive ending address of the memory segment",
173
173
  "minimum": 0
174
174
  }
175
175
  }
@@ -194,358 +194,192 @@
194
194
  },
195
195
  "definitions": {
196
196
  "protocolStep": {
197
- "type": "object",
198
- "description": "A single protocol step",
197
+ "description": "A single protocol step: an exchange, a chunked read, or a chunked write",
199
198
  "oneOf": [
200
199
  {
201
- "type": "object",
202
- "required": [
203
- "sendReceive"
204
- ],
205
- "properties": {
206
- "sendReceive": {
207
- "$ref": "#/definitions/sendReceiveStep"
208
- }
209
- }
200
+ "$ref": "#/definitions/readStep"
210
201
  },
211
202
  {
212
- "type": "object",
213
- "required": [
214
- "send"
215
- ],
216
- "properties": {
217
- "send": {
218
- "$ref": "#/definitions/sendStep"
219
- }
220
- }
203
+ "$ref": "#/definitions/writeStep"
221
204
  },
222
205
  {
223
- "type": "object",
224
- "required": [
225
- "receive"
226
- ],
227
- "properties": {
228
- "receive": {
229
- "$ref": "#/definitions/receiveStep"
230
- }
231
- }
206
+ "$ref": "#/definitions/exchangeStep"
207
+ }
208
+ ]
209
+ },
210
+ "byteToken": {
211
+ "description": "A send/expect token: a byte, hex string (0x50), ASCII opcode (S), or placeholder ($address)",
212
+ "oneOf": [
213
+ {
214
+ "type": "integer",
215
+ "minimum": 0,
216
+ "maximum": 255
232
217
  },
233
218
  {
234
- "type": "object",
235
- "required": [
236
- "readSegment"
237
- ],
238
- "properties": {
239
- "readSegment": {
240
- "$ref": "#/definitions/readSegmentStep"
241
- }
242
- }
219
+ "type": "string",
220
+ "minLength": 1
221
+ }
222
+ ]
223
+ },
224
+ "sendData": {
225
+ "type": "array",
226
+ "description": "Bytes and placeholders to send",
227
+ "items": {
228
+ "$ref": "#/definitions/byteToken"
229
+ }
230
+ },
231
+ "expect": {
232
+ "description": "Expected serial reply: exact byte(s), opaque length, or a framed pattern",
233
+ "oneOf": [
234
+ {
235
+ "$ref": "#/definitions/byteToken"
243
236
  },
244
237
  {
245
- "type": "object",
246
- "required": [
247
- "writeSegment"
248
- ],
249
- "properties": {
250
- "writeSegment": {
251
- "$ref": "#/definitions/writeSegmentStep"
252
- }
253
- }
238
+ "type": "array",
239
+ "items": {
240
+ "$ref": "#/definitions/byteToken"
241
+ },
242
+ "minItems": 1
254
243
  },
255
244
  {
256
245
  "type": "object",
257
246
  "required": [
258
- "setVariable"
247
+ "bytes"
259
248
  ],
249
+ "additionalProperties": false,
260
250
  "properties": {
261
- "setVariable": {
262
- "$ref": "#/definitions/setVariableStep"
251
+ "bytes": {
252
+ "type": "integer",
253
+ "description": "Accept any content of this many bytes",
254
+ "minimum": 1
263
255
  }
264
256
  }
265
257
  }
266
258
  ]
267
259
  },
268
- "sendReceiveStep": {
260
+ "exchangeStep": {
269
261
  "type": "object",
270
- "description": "Send data and receive response",
271
- "required": [
272
- "send",
273
- "receive"
262
+ "description": "Send bytes and/or wait for a reply",
263
+ "anyOf": [
264
+ {
265
+ "required": [
266
+ "send"
267
+ ]
268
+ },
269
+ {
270
+ "required": [
271
+ "expect"
272
+ ]
273
+ }
274
274
  ],
275
+ "additionalProperties": false,
275
276
  "properties": {
277
+ "description": {
278
+ "type": "string",
279
+ "description": "Human-readable description of the step"
280
+ },
276
281
  "send": {
277
282
  "$ref": "#/definitions/sendData"
278
283
  },
279
- "receive": {
280
- "$ref": "#/definitions/receiveConfig"
284
+ "expect": {
285
+ "$ref": "#/definitions/expect"
281
286
  },
282
287
  "timeout": {
283
288
  "type": "integer",
284
289
  "description": "Timeout in milliseconds",
285
290
  "minimum": 1,
286
291
  "default": 5000
287
- },
288
- "description": {
289
- "type": "string",
290
- "description": "Human-readable description of the step"
291
292
  }
292
293
  }
293
294
  },
294
- "sendStep": {
295
+ "readStep": {
295
296
  "type": "object",
296
- "description": "Send data only",
297
+ "description": "Chunked memory read",
297
298
  "required": [
298
- "data"
299
+ "read"
299
300
  ],
301
+ "additionalProperties": false,
300
302
  "properties": {
301
- "data": {
302
- "$ref": "#/definitions/sendData"
303
- },
304
303
  "description": {
305
304
  "type": "string",
306
305
  "description": "Human-readable description of the step"
307
- }
308
- }
309
- },
310
- "receiveStep": {
311
- "type": "object",
312
- "description": "Receive data only",
313
- "required": [
314
- "type",
315
- "length"
316
- ],
317
- "properties": {
318
- "type": {
319
- "type": "string",
320
- "enum": [
321
- "exact",
322
- "variable",
323
- "pattern",
324
- "any"
325
- ],
326
- "description": "Type of receive operation"
327
- },
328
- "value": {
329
- "type": "integer",
330
- "description": "Expected value for exact match",
331
- "minimum": 0,
332
- "maximum": 255
333
- },
334
- "length": {
335
- "type": "integer",
336
- "description": "Expected length in bytes",
337
- "minimum": 1
338
306
  },
339
- "pattern": {
340
- "type": "array",
341
- "description": "Pattern for structured responses",
342
- "items": {
343
- "oneOf": [
344
- {
345
- "type": "string",
346
- "description": "Literal value (1 byte)"
347
- },
348
- {
349
- "type": "number",
350
- "description": "Literal value (1 byte)"
307
+ "read": {
308
+ "type": "object",
309
+ "required": [
310
+ "segments",
311
+ "send",
312
+ "expect"
313
+ ],
314
+ "additionalProperties": false,
315
+ "properties": {
316
+ "segments": {
317
+ "type": "array",
318
+ "description": "Memory segment names to read",
319
+ "items": {
320
+ "type": "string"
351
321
  },
352
- {
353
- "type": "object",
354
- "required": [
355
- "field",
356
- "size"
357
- ],
358
- "properties": {
359
- "field": {
360
- "type": "string",
361
- "description": "Field name for extracted data"
362
- },
363
- "size": {
364
- "type": "integer",
365
- "description": "Size in bytes (0 for variable length)",
366
- "minimum": 0
367
- }
368
- }
369
- }
370
- ]
371
- }
372
- },
373
- "description": {
374
- "type": "string",
375
- "description": "Human-readable description of the step"
376
- }
377
- }
378
- },
379
- "readSegmentStep": {
380
- "type": "object",
381
- "description": "Read memory segments",
382
- "required": [
383
- "segments",
384
- "startChunk",
385
- "endChunk"
386
- ],
387
- "properties": {
388
- "segments": {
389
- "type": "array",
390
- "description": "List of segment names to read",
391
- "items": {
392
- "type": "string"
322
+ "minItems": 1
323
+ },
324
+ "send": {
325
+ "$ref": "#/definitions/sendData"
326
+ },
327
+ "expect": {
328
+ "$ref": "#/definitions/expect"
329
+ },
330
+ "ack": {
331
+ "$ref": "#/definitions/exchangeStep",
332
+ "description": "Optional exchange after each chunk"
333
+ },
334
+ "timeout": {
335
+ "type": "integer",
336
+ "description": "Timeout in milliseconds for each chunk exchange",
337
+ "minimum": 1
338
+ }
393
339
  }
394
- },
395
- "startChunk": {
396
- "$ref": "#/definitions/sendReceiveStep",
397
- "description": "Protocol for starting a chunk read"
398
- },
399
- "endChunk": {
400
- "$ref": "#/definitions/sendReceiveStep",
401
- "description": "Protocol for ending a chunk read"
402
- },
403
- "description": {
404
- "type": "string",
405
- "description": "Human-readable description of the step"
406
340
  }
407
341
  }
408
342
  },
409
- "writeSegmentStep": {
343
+ "writeStep": {
410
344
  "type": "object",
411
- "description": "Write memory segments",
345
+ "description": "Chunked memory write",
412
346
  "required": [
413
- "segments",
414
- "send",
415
- "data",
416
- "receive"
347
+ "write"
417
348
  ],
349
+ "additionalProperties": false,
418
350
  "properties": {
419
- "segments": {
420
- "type": "array",
421
- "description": "List of segment names to write",
422
- "items": {
423
- "type": "string"
424
- }
425
- },
426
- "send": {
427
- "$ref": "#/definitions/sendData",
428
- "description": "Data to send for write command"
429
- },
430
- "data": {
431
- "type": "string",
432
- "description": "Expression for segment data to write"
433
- },
434
- "receive": {
435
- "$ref": "#/definitions/receiveConfig",
436
- "description": "Expected response for write command"
437
- },
438
351
  "description": {
439
352
  "type": "string",
440
353
  "description": "Human-readable description of the step"
441
- }
442
- }
443
- },
444
- "setVariableStep": {
445
- "type": "object",
446
- "description": "Set a variable value",
447
- "required": [
448
- "name",
449
- "value"
450
- ],
451
- "properties": {
452
- "name": {
453
- "type": "string",
454
- "description": "Variable name"
455
354
  },
456
- "value": {
457
- "oneOf": [
458
- {
459
- "type": "string",
460
- "description": "Expression or literal value"
461
- },
462
- {
463
- "type": "number",
464
- "description": "Numeric value"
465
- }
466
- ],
467
- "description": "Value to assign to the variable"
468
- }
469
- }
470
- },
471
- "sendData": {
472
- "type": "array",
473
- "description": "Array of data to send",
474
- "items": {
475
- "oneOf": [
476
- {
477
- "type": "string",
478
- "description": "String literal or expression"
479
- },
480
- {
481
- "type": "number",
482
- "description": "Numeric literal or expression",
483
- "minimum": 0,
484
- "maximum": 255
485
- }
486
- ]
487
- }
488
- },
489
- "receiveConfig": {
490
- "type": "object",
491
- "description": "Configuration for receiving data",
492
- "required": [
493
- "type"
494
- ],
495
- "properties": {
496
- "type": {
497
- "type": "string",
498
- "enum": [
499
- "exact",
500
- "variable",
501
- "pattern",
502
- "any"
355
+ "write": {
356
+ "type": "object",
357
+ "required": [
358
+ "segments",
359
+ "send",
360
+ "expect"
503
361
  ],
504
- "description": "Type of receive operation"
505
- },
506
- "value": {
507
- "type": "integer",
508
- "description": "Expected value for exact match",
509
- "minimum": 0,
510
- "maximum": 255
511
- },
512
- "length": {
513
- "type": "integer",
514
- "description": "Expected length in bytes",
515
- "minimum": 1
516
- },
517
- "pattern": {
518
- "type": "array",
519
- "description": "Pattern for structured responses",
520
- "items": {
521
- "oneOf": [
522
- {
523
- "type": "string",
524
- "description": "Literal value (1 byte)"
525
- },
526
- {
527
- "type": "number",
528
- "description": "Literal value (1 byte)"
362
+ "additionalProperties": false,
363
+ "properties": {
364
+ "segments": {
365
+ "type": "array",
366
+ "description": "Memory segment names to write",
367
+ "items": {
368
+ "type": "string"
529
369
  },
530
- {
531
- "type": "object",
532
- "required": [
533
- "field",
534
- "size"
535
- ],
536
- "properties": {
537
- "field": {
538
- "type": "string",
539
- "description": "Field name for extracted data"
540
- },
541
- "size": {
542
- "type": "integer",
543
- "description": "Size in bytes (0 for variable length)",
544
- "minimum": 0
545
- }
546
- }
547
- }
548
- ]
370
+ "minItems": 1
371
+ },
372
+ "send": {
373
+ "$ref": "#/definitions/sendData"
374
+ },
375
+ "expect": {
376
+ "$ref": "#/definitions/expect"
377
+ },
378
+ "timeout": {
379
+ "type": "integer",
380
+ "description": "Timeout in milliseconds for each chunk exchange",
381
+ "minimum": 1
382
+ }
549
383
  }
550
384
  }
551
385
  }
@@ -5,20 +5,6 @@ import type { ILogLayer } from 'loglayer';
5
5
  * This module provides a specialized logger for capturing command-level information
6
6
  * that can be easily parsed and displayed in a UI. It captures details about
7
7
  * commands, data sent, data expected, and data received in a structured format.
8
- *
9
- * Purpose:
10
- * - Captures command-level information for UI display
11
- * - Provides structured JSON logging for easy parsing
12
- * - Tracks command execution details including timing
13
- * - Supports protocol debugging in UI environments
14
- * - Maintains separation from debug logging
15
- *
16
- * Design Rationale:
17
- * - Single log entry per command provides clean UI display
18
- * - JSON structure enables easy parsing and filtering
19
- * - Command-level granularity is appropriate for UI debugging
20
- * - Structured data supports rich UI representations
21
- * - Separate from debug logging avoids UI noise
22
8
  */
23
9
  interface ProtocolContext {
24
10
  variables?: Map<string, unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"ui-logger.d.ts","sourceRoot":"","sources":["../../src/utils/ui-logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1C;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,UAAU,eAAe;IACvB,SAAS,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AA0BrD;;GAEG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,iBAAiB,CAA6B;IAEtD,YAAY,MAAM,EAAE,SAAS,EAE5B;IAGD,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,IAAI,CAmBjG;IAGD,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAwChI;IAGD,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAiC9I;IAED,OAAO,CAAC,cAAc;IAsBtB,OAAO,CAAC,kBAAkB;IAmB1B,OAAO,CAAC,eAAe;CAoBxB"}
1
+ {"version":3,"file":"ui-logger.d.ts","sourceRoot":"","sources":["../../src/utils/ui-logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1C;;;;;;GAMG;AAEH,UAAU,eAAe;IACvB,SAAS,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAKrD;;GAEG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,iBAAiB,CAA6B;IAEtD,YAAY,MAAM,EAAE,SAAS,EAE5B;IAGD,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,IAAI,CAmBjG;IAGD,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAoChI;IAGD,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CA+B9I;IAED,OAAO,CAAC,cAAc;IAatB,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,eAAe;CAgBxB"}
@@ -1,20 +1,4 @@
1
- /**
2
- * Type guards for protocol step properties
3
- */
4
1
  const hasDescription = (obj) => typeof obj === 'object' && obj !== null && 'description' in obj && typeof obj.description === 'string';
5
- const hasReceive = (obj) => typeof obj === 'object' && obj !== null && 'receive' in obj;
6
- const hasEndChunk = (obj) => typeof obj === 'object' &&
7
- obj !== null &&
8
- 'endChunk' in obj &&
9
- typeof obj.endChunk === 'object' &&
10
- obj.endChunk !== null &&
11
- 'receive' in obj.endChunk;
12
- const hasStartChunk = (obj) => typeof obj === 'object' &&
13
- obj !== null &&
14
- 'startChunk' in obj &&
15
- typeof obj.startChunk === 'object' &&
16
- obj.startChunk !== null &&
17
- 'receive' in obj.startChunk;
18
2
  /**
19
3
  * UI Logger for capturing command-level information for UI display
20
4
  */
@@ -51,13 +35,10 @@ export class UILogger {
51
35
  const duration = endTime - startTime;
52
36
  const commandType = this.getCommandType(step);
53
37
  const description = this.getStepDescription(step);
54
- // Extract sent and received data from context
55
38
  const dataSent = context.variables && context.variables.get('lastSentData');
56
39
  const dataReceived = context.variables && context.variables.get('lastReceivedData');
57
40
  const dataExpected = this.getExpectedData(step);
58
- // For readSegment, also extract chunk logs if present
59
- const dataChunks = commandType === 'readSegment' && context.variables && context.variables.get('lastReadSegmentChunks');
60
- // Convert dataSent and dataReceived to byte array format if they exist
41
+ const dataChunks = commandType === 'read' && context.variables && context.variables.get('lastReadSegmentChunks');
61
42
  // eslint-disable-next-line unicorn/prefer-spread
62
43
  const dataSentArray = dataSent && Array.from(dataSent);
63
44
  // eslint-disable-next-line unicorn/prefer-spread
@@ -88,10 +69,8 @@ export class UILogger {
88
69
  const duration = endTime - startTime;
89
70
  const commandType = this.getCommandType(step);
90
71
  const description = this.getStepDescription(step);
91
- // Extract sent data from context
92
72
  const dataSent = context.variables && context.variables.get('lastSentData');
93
73
  const dataExpected = this.getExpectedData(step);
94
- // Convert dataSent to byte array format if it exists
95
74
  // eslint-disable-next-line unicorn/prefer-spread
96
75
  const dataSentArray = dataSent && Array.from(dataSent);
97
76
  this.logger
@@ -112,61 +91,36 @@ export class UILogger {
112
91
  .info('Command failed');
113
92
  }
114
93
  getCommandType(step) {
115
- if ('sendReceive' in step) {
116
- return 'sendReceive';
117
- }
118
- if ('send' in step) {
119
- return 'send';
120
- }
121
- if ('receive' in step) {
122
- return 'receive';
123
- }
124
- if ('readSegment' in step) {
125
- return 'readSegment';
94
+ if ('read' in step) {
95
+ return 'read';
126
96
  }
127
- if ('writeSegment' in step) {
128
- return 'writeSegment';
97
+ if ('write' in step) {
98
+ return 'write';
129
99
  }
130
- if ('setVariable' in step) {
131
- return 'setVariable';
100
+ if ('send' in step || 'expect' in step) {
101
+ return 'exchange';
132
102
  }
133
103
  return 'unknown';
134
104
  }
135
105
  getStepDescription(step) {
136
- if ('sendReceive' in step && hasDescription(step.sendReceive)) {
137
- return step.sendReceive.description;
138
- }
139
- if ('send' in step && hasDescription(step.send)) {
140
- return step.send.description;
141
- }
142
- if ('receive' in step && hasDescription(step.receive)) {
143
- return step.receive.description;
144
- }
145
- if ('readSegment' in step && hasDescription(step.readSegment)) {
146
- return step.readSegment.description;
147
- }
148
- if ('writeSegment' in step && hasDescription(step.writeSegment)) {
149
- return step.writeSegment.description;
106
+ if (hasDescription(step)) {
107
+ return step.description;
150
108
  }
151
109
  return 'No description';
152
110
  }
153
111
  getExpectedData(step) {
154
- if ('sendReceive' in step && hasReceive(step.sendReceive)) {
155
- return step.sendReceive.receive;
112
+ if ('read' in step && typeof step.read === 'object' && step.read !== null && 'expect' in step.read) {
113
+ const read = step.read;
114
+ return {
115
+ expect: read.expect,
116
+ ack: read.ack?.expect,
117
+ };
156
118
  }
157
- if ('receive' in step) {
158
- return step.receive;
119
+ if ('write' in step && typeof step.write === 'object' && step.write !== null && 'expect' in step.write) {
120
+ return step.write.expect;
159
121
  }
160
- if ('readSegment' in step) {
161
- // For readSegment, return both start and end chunk receive patterns
162
- const { readSegment } = step;
163
- if (hasEndChunk(readSegment) && hasStartChunk(readSegment)) {
164
- return {
165
- endChunk: readSegment.endChunk.receive,
166
- startChunk: readSegment.startChunk.receive,
167
- type: 'readSegment',
168
- };
169
- }
122
+ if ('expect' in step) {
123
+ return step.expect;
170
124
  }
171
125
  return {};
172
126
  }