@springfield/ham-radio-utils 4.8.0 → 4.10.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.
@@ -78,10 +78,21 @@
78
78
  "properties": {
79
79
  "baudRate": {
80
80
  "type": "integer",
81
- "description": "Serial baud rate",
81
+ "description": "Default serial baud rate used to open the programming port. Must be included in baudRates when that list is present.",
82
82
  "minimum": 1200,
83
83
  "maximum": 115200
84
84
  },
85
+ "baudRates": {
86
+ "type": "array",
87
+ "description": "Baud rates the radio programming port accepts. Omit when only baudRate is valid. The HamBench import and write dialogs show a selector when more than one value is listed.",
88
+ "minItems": 1,
89
+ "uniqueItems": true,
90
+ "items": {
91
+ "type": "integer",
92
+ "minimum": 1200,
93
+ "maximum": 115200
94
+ }
95
+ },
85
96
  "dataBits": {
86
97
  "type": "integer",
87
98
  "description": "Number of data bits",
@@ -124,6 +135,14 @@
124
135
  "rtscts": {
125
136
  "type": "boolean",
126
137
  "description": "Hardware RTS/CTS flow control (Kenwood TH-D74 clone on macOS)"
138
+ },
139
+ "rts": {
140
+ "type": "boolean",
141
+ "description": "RTS line after open. TH-F6 live CAT needs false"
142
+ },
143
+ "dtr": {
144
+ "type": "boolean",
145
+ "description": "DTR line after open"
127
146
  }
128
147
  }
129
148
  },
@@ -198,7 +217,7 @@
198
217
  },
199
218
  "definitions": {
200
219
  "protocolStep": {
201
- "description": "A single protocol step: an exchange, a chunked read, or a chunked write",
220
+ "description": "A single protocol step: an exchange, a chunked read, a chunked write, or a live CAT memory loop",
202
221
  "oneOf": [
203
222
  {
204
223
  "$ref": "#/definitions/readStep"
@@ -206,6 +225,12 @@
206
225
  {
207
226
  "$ref": "#/definitions/writeStep"
208
227
  },
228
+ {
229
+ "$ref": "#/definitions/catReadStep"
230
+ },
231
+ {
232
+ "$ref": "#/definitions/catWriteStep"
233
+ },
209
234
  {
210
235
  "$ref": "#/definitions/exchangeStep"
211
236
  }
@@ -258,9 +283,112 @@
258
283
  "minimum": 1
259
284
  }
260
285
  }
286
+ },
287
+ {
288
+ "type": "object",
289
+ "required": [
290
+ "until"
291
+ ],
292
+ "additionalProperties": false,
293
+ "properties": {
294
+ "until": {
295
+ "$ref": "#/definitions/byteToken",
296
+ "description": "Read until this delimiter byte; the delimiter is not included (Kenwood CAT CR)"
297
+ }
298
+ }
261
299
  }
262
300
  ]
263
301
  },
302
+ "catMemory": {
303
+ "type": "object",
304
+ "description": "Live CAT memory loop over logical channel records",
305
+ "required": [
306
+ "segment",
307
+ "count",
308
+ "recordSize",
309
+ "pack"
310
+ ],
311
+ "additionalProperties": false,
312
+ "properties": {
313
+ "segment": {
314
+ "type": "string",
315
+ "description": "Memory segment that receives packed records"
316
+ },
317
+ "count": {
318
+ "type": "integer",
319
+ "description": "Number of channel indexes to scan",
320
+ "minimum": 1
321
+ },
322
+ "recordSize": {
323
+ "type": "integer",
324
+ "description": "Packed logical record size in bytes",
325
+ "minimum": 1
326
+ },
327
+ "pack": {
328
+ "type": "string",
329
+ "description": "CAT reply packer",
330
+ "enum": [
331
+ "kenwood-th-f6"
332
+ ]
333
+ },
334
+ "indexWidth": {
335
+ "type": "integer",
336
+ "description": "Zero-padded decimal width of the memory index (TH-F6 uses 3)",
337
+ "minimum": 1,
338
+ "maximum": 6,
339
+ "default": 3
340
+ },
341
+ "emptyByte": {
342
+ "type": "integer",
343
+ "description": "Fill byte for empty memories",
344
+ "minimum": 0,
345
+ "maximum": 255,
346
+ "default": 255
347
+ },
348
+ "timeout": {
349
+ "type": "integer",
350
+ "description": "Timeout in milliseconds for each CAT line",
351
+ "minimum": 1
352
+ },
353
+ "interCommandDelayMs": {
354
+ "type": "integer",
355
+ "description": "Milliseconds to wait between CAT commands",
356
+ "minimum": 0
357
+ }
358
+ }
359
+ },
360
+ "catReadStep": {
361
+ "type": "object",
362
+ "description": "Read memories with live CAT commands",
363
+ "required": [
364
+ "catRead"
365
+ ],
366
+ "additionalProperties": false,
367
+ "properties": {
368
+ "description": {
369
+ "type": "string"
370
+ },
371
+ "catRead": {
372
+ "$ref": "#/definitions/catMemory"
373
+ }
374
+ }
375
+ },
376
+ "catWriteStep": {
377
+ "type": "object",
378
+ "description": "Write memories with live CAT commands",
379
+ "required": [
380
+ "catWrite"
381
+ ],
382
+ "additionalProperties": false,
383
+ "properties": {
384
+ "description": {
385
+ "type": "string"
386
+ },
387
+ "catWrite": {
388
+ "$ref": "#/definitions/catMemory"
389
+ }
390
+ }
391
+ },
264
392
  "exchangeStep": {
265
393
  "type": "object",
266
394
  "description": "Send bytes and/or wait for a reply",
@@ -299,6 +427,11 @@
299
427
  "minimum": 1,
300
428
  "default": 5000
301
429
  },
430
+ "delay": {
431
+ "type": "integer",
432
+ "description": "Milliseconds to wait after send",
433
+ "minimum": 0
434
+ },
302
435
  "setBaudRate": {
303
436
  "type": "integer",
304
437
  "description": "Switch serial baud rate before send/expect (TH-D74 clone transfers at 57600)",
@@ -1 +1 @@
1
- {"version":3,"file":"schema-validator.d.ts","sourceRoot":"","sources":["../../src/utils/schema-validator.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAOD,qBAAa,eAAe;IAC1B,OAAO,CAAC,GAAG,CAAM;IAEjB,cAMC;IAED,qBAAqB,CAAC,MAAM,EAAE,OAAO,GAAG,gBAAgB,CAEvD;IAED,iBAAiB,CAAC,SAAS,EAAE,OAAO,GAAG,gBAAgB,CAEtD;IAED,OAAO,CAAC,eAAe;CAqBxB"}
1
+ {"version":3,"file":"schema-validator.d.ts","sourceRoot":"","sources":["../../src/utils/schema-validator.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAOD,qBAAa,eAAe;IAC1B,OAAO,CAAC,GAAG,CAAM;IAEjB,cAMC;IAED,qBAAqB,CAAC,MAAM,EAAE,OAAO,GAAG,gBAAgB,CAiBvD;IAED,iBAAiB,CAAC,SAAS,EAAE,OAAO,GAAG,gBAAgB,CAEtD;IAED,OAAO,CAAC,eAAe;CAsBxB"}
@@ -12,7 +12,18 @@ export class SchemaValidator {
12
12
  addFormats(this.ajv);
13
13
  }
14
14
  validateRadioProtocol(config) {
15
- return this.validateAgainst(radioProtocolSchema, config);
15
+ const schemaResult = this.validateAgainst(radioProtocolSchema, config);
16
+ if (!schemaResult.valid) {
17
+ return schemaResult;
18
+ }
19
+ const baudRateErrors = baudRateMembershipErrors(config);
20
+ if (baudRateErrors.length === 0) {
21
+ return schemaResult;
22
+ }
23
+ return {
24
+ valid: false,
25
+ errors: baudRateErrors,
26
+ };
16
27
  }
17
28
  validateMemoryMap(memoryMap) {
18
29
  return this.validateAgainst(radioMemoryMapSchema, memoryMap);
@@ -36,4 +47,26 @@ export class SchemaValidator {
36
47
  };
37
48
  }
38
49
  }
50
+ function isRecord(value) {
51
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
52
+ }
53
+ /**
54
+ * `baudRate` is the default open speed. When a radio lists `baudRates`, that
55
+ * default must be one of the accepted values so the UI can preselect it.
56
+ */
57
+ function baudRateMembershipErrors(config) {
58
+ if (!isRecord(config) || !isRecord(config.serialConfig)) {
59
+ return [];
60
+ }
61
+ const serialConfig = config.serialConfig;
62
+ const baudRate = serialConfig.baudRate;
63
+ const baudRates = serialConfig.baudRates;
64
+ if (!Array.isArray(baudRates) || typeof baudRate !== 'number') {
65
+ return [];
66
+ }
67
+ if (baudRates.includes(baudRate)) {
68
+ return [];
69
+ }
70
+ return ['/serialConfig/baudRate: must be included in baudRates'];
71
+ }
39
72
  //# sourceMappingURL=schema-validator.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema-validator.js","sourceRoot":"","sources":["../../src/utils/schema-validator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAC1B,OAAO,UAAU,MAAM,aAAa,CAAC;AACrC,OAAO,mBAAmB,MAAM,uCAAuC,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC;AAC9F,OAAO,oBAAoB,MAAM,yCAAyC,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC;AAYjG,MAAM,OAAO,eAAe;IAClB,GAAG,CAAM;IAEjB;QACE,IAAI,CAAC,GAAG,GAAG,IAAI,GAAG,CAAC;YACjB,SAAS,EAAE,IAAI;YACf,OAAO,EAAE,IAAI;SACd,CAAC,CAAC;QACF,UAA2C,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACzD,CAAC;IAED,qBAAqB,CAAC,MAAe;QACnC,OAAO,IAAI,CAAC,eAAe,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC;IAC3D,CAAC;IAED,iBAAiB,CAAC,SAAkB;QAClC,OAAO,IAAI,CAAC,eAAe,CAAC,oBAAoB,EAAE,SAAS,CAAC,CAAC;IAC/D,CAAC;IAEO,eAAe,CAAC,MAAc,EAAE,IAAa;QACnD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAC1C,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QAE7B,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QACzB,CAAC;QAED,IAAI,MAAM,GAAa,EAAE,CAAC;QAC1B,IAAI,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAClD,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAe,EAAE,EAAE;gBAC/C,MAAM,IAAI,GAAG,KAAK,CAAC,YAAY,IAAI,MAAM,CAAC;gBAC1C,OAAO,GAAG,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;YACrC,CAAC,CAAC,CAAC;QACL,CAAC;QAED,OAAO;YACL,MAAM;YACN,KAAK,EAAE,KAAK;SACb,CAAC;IACJ,CAAC;CACF","sourcesContent":["import { Ajv } from 'ajv';\nimport addFormats from 'ajv-formats';\nimport radioProtocolSchema from '../schemas/radio-protocol-schema.json' with { type: 'json' };\nimport radioMemoryMapSchema from '../schemas/radio-memory-map-schema.json' with { type: 'json' };\n\nexport interface ValidationResult {\n valid: boolean;\n errors?: string[];\n}\n\ninterface AjvError {\n instancePath?: string;\n message?: string;\n}\n\nexport class SchemaValidator {\n private ajv: Ajv;\n\n constructor() {\n this.ajv = new Ajv({\n allErrors: true,\n verbose: true,\n });\n (addFormats as unknown as (ajv: Ajv) => Ajv)(this.ajv);\n }\n\n validateRadioProtocol(config: unknown): ValidationResult {\n return this.validateAgainst(radioProtocolSchema, config);\n }\n\n validateMemoryMap(memoryMap: unknown): ValidationResult {\n return this.validateAgainst(radioMemoryMapSchema, memoryMap);\n }\n\n private validateAgainst(schema: object, data: unknown): ValidationResult {\n const validate = this.ajv.compile(schema);\n const valid = validate(data);\n\n if (valid) {\n return { valid: true };\n }\n\n let errors: string[] = [];\n if (validate.errors && validate.errors.length > 0) {\n errors = validate.errors.map((error: AjvError) => {\n const path = error.instancePath || 'root';\n return `${path}: ${error.message}`;\n });\n }\n\n return {\n errors,\n valid: false,\n };\n }\n}\n"]}
1
+ {"version":3,"file":"schema-validator.js","sourceRoot":"","sources":["../../src/utils/schema-validator.ts"],"names":[],"mappings":"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","sourcesContent":["import { Ajv } from 'ajv';\nimport addFormats from 'ajv-formats';\nimport radioProtocolSchema from '../schemas/radio-protocol-schema.json' with { type: 'json' };\nimport radioMemoryMapSchema from '../schemas/radio-memory-map-schema.json' with { type: 'json' };\n\nexport interface ValidationResult {\n valid: boolean;\n errors?: string[];\n}\n\ninterface AjvError {\n instancePath?: string;\n message?: string;\n}\n\nexport class SchemaValidator {\n private ajv: Ajv;\n\n constructor() {\n this.ajv = new Ajv({\n allErrors: true,\n verbose: true,\n });\n (addFormats as unknown as (ajv: Ajv) => Ajv)(this.ajv);\n }\n\n validateRadioProtocol(config: unknown): ValidationResult {\n const schemaResult = this.validateAgainst(radioProtocolSchema, config);\n\n if (!schemaResult.valid) {\n return schemaResult;\n }\n\n const baudRateErrors = baudRateMembershipErrors(config);\n\n if (baudRateErrors.length === 0) {\n return schemaResult;\n }\n\n return {\n valid: false,\n errors: baudRateErrors,\n };\n }\n\n validateMemoryMap(memoryMap: unknown): ValidationResult {\n return this.validateAgainst(radioMemoryMapSchema, memoryMap);\n }\n\n private validateAgainst(schema: object, data: unknown): ValidationResult {\n const validate = this.ajv.compile(schema);\n const valid = validate(data);\n\n if (valid) {\n return { valid: true };\n }\n\n let errors: string[] = [];\n\n if (validate.errors && validate.errors.length > 0) {\n errors = validate.errors.map((error: AjvError) => {\n const path = error.instancePath || 'root';\n return `${path}: ${error.message}`;\n });\n }\n\n return {\n errors,\n valid: false,\n };\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\n/**\n * `baudRate` is the default open speed. When a radio lists `baudRates`, that\n * default must be one of the accepted values so the UI can preselect it.\n */\nfunction baudRateMembershipErrors(config: unknown): string[] {\n if (!isRecord(config) || !isRecord(config.serialConfig)) {\n return [];\n }\n\n const serialConfig = config.serialConfig;\n const baudRate = serialConfig.baudRate;\n const baudRates = serialConfig.baudRates;\n\n if (!Array.isArray(baudRates) || typeof baudRate !== 'number') {\n return [];\n }\n\n if (baudRates.includes(baudRate)) {\n return [];\n }\n\n return ['/serialConfig/baudRate: must be included in baudRates'];\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springfield/ham-radio-utils",
3
- "version": "4.8.0",
3
+ "version": "4.10.0",
4
4
  "author": "Bryan Hunt",
5
5
  "license": "MIT",
6
6
  "readme": "README.md",
@@ -78,10 +78,21 @@
78
78
  "properties": {
79
79
  "baudRate": {
80
80
  "type": "integer",
81
- "description": "Serial baud rate",
81
+ "description": "Default serial baud rate used to open the programming port. Must be included in baudRates when that list is present.",
82
82
  "minimum": 1200,
83
83
  "maximum": 115200
84
84
  },
85
+ "baudRates": {
86
+ "type": "array",
87
+ "description": "Baud rates the radio programming port accepts. Omit when only baudRate is valid. The HamBench import and write dialogs show a selector when more than one value is listed.",
88
+ "minItems": 1,
89
+ "uniqueItems": true,
90
+ "items": {
91
+ "type": "integer",
92
+ "minimum": 1200,
93
+ "maximum": 115200
94
+ }
95
+ },
85
96
  "dataBits": {
86
97
  "type": "integer",
87
98
  "description": "Number of data bits",
@@ -124,6 +135,14 @@
124
135
  "rtscts": {
125
136
  "type": "boolean",
126
137
  "description": "Hardware RTS/CTS flow control (Kenwood TH-D74 clone on macOS)"
138
+ },
139
+ "rts": {
140
+ "type": "boolean",
141
+ "description": "RTS line after open. TH-F6 live CAT needs false"
142
+ },
143
+ "dtr": {
144
+ "type": "boolean",
145
+ "description": "DTR line after open"
127
146
  }
128
147
  }
129
148
  },
@@ -198,7 +217,7 @@
198
217
  },
199
218
  "definitions": {
200
219
  "protocolStep": {
201
- "description": "A single protocol step: an exchange, a chunked read, or a chunked write",
220
+ "description": "A single protocol step: an exchange, a chunked read, a chunked write, or a live CAT memory loop",
202
221
  "oneOf": [
203
222
  {
204
223
  "$ref": "#/definitions/readStep"
@@ -206,6 +225,12 @@
206
225
  {
207
226
  "$ref": "#/definitions/writeStep"
208
227
  },
228
+ {
229
+ "$ref": "#/definitions/catReadStep"
230
+ },
231
+ {
232
+ "$ref": "#/definitions/catWriteStep"
233
+ },
209
234
  {
210
235
  "$ref": "#/definitions/exchangeStep"
211
236
  }
@@ -258,9 +283,112 @@
258
283
  "minimum": 1
259
284
  }
260
285
  }
286
+ },
287
+ {
288
+ "type": "object",
289
+ "required": [
290
+ "until"
291
+ ],
292
+ "additionalProperties": false,
293
+ "properties": {
294
+ "until": {
295
+ "$ref": "#/definitions/byteToken",
296
+ "description": "Read until this delimiter byte; the delimiter is not included (Kenwood CAT CR)"
297
+ }
298
+ }
261
299
  }
262
300
  ]
263
301
  },
302
+ "catMemory": {
303
+ "type": "object",
304
+ "description": "Live CAT memory loop over logical channel records",
305
+ "required": [
306
+ "segment",
307
+ "count",
308
+ "recordSize",
309
+ "pack"
310
+ ],
311
+ "additionalProperties": false,
312
+ "properties": {
313
+ "segment": {
314
+ "type": "string",
315
+ "description": "Memory segment that receives packed records"
316
+ },
317
+ "count": {
318
+ "type": "integer",
319
+ "description": "Number of channel indexes to scan",
320
+ "minimum": 1
321
+ },
322
+ "recordSize": {
323
+ "type": "integer",
324
+ "description": "Packed logical record size in bytes",
325
+ "minimum": 1
326
+ },
327
+ "pack": {
328
+ "type": "string",
329
+ "description": "CAT reply packer",
330
+ "enum": [
331
+ "kenwood-th-f6"
332
+ ]
333
+ },
334
+ "indexWidth": {
335
+ "type": "integer",
336
+ "description": "Zero-padded decimal width of the memory index (TH-F6 uses 3)",
337
+ "minimum": 1,
338
+ "maximum": 6,
339
+ "default": 3
340
+ },
341
+ "emptyByte": {
342
+ "type": "integer",
343
+ "description": "Fill byte for empty memories",
344
+ "minimum": 0,
345
+ "maximum": 255,
346
+ "default": 255
347
+ },
348
+ "timeout": {
349
+ "type": "integer",
350
+ "description": "Timeout in milliseconds for each CAT line",
351
+ "minimum": 1
352
+ },
353
+ "interCommandDelayMs": {
354
+ "type": "integer",
355
+ "description": "Milliseconds to wait between CAT commands",
356
+ "minimum": 0
357
+ }
358
+ }
359
+ },
360
+ "catReadStep": {
361
+ "type": "object",
362
+ "description": "Read memories with live CAT commands",
363
+ "required": [
364
+ "catRead"
365
+ ],
366
+ "additionalProperties": false,
367
+ "properties": {
368
+ "description": {
369
+ "type": "string"
370
+ },
371
+ "catRead": {
372
+ "$ref": "#/definitions/catMemory"
373
+ }
374
+ }
375
+ },
376
+ "catWriteStep": {
377
+ "type": "object",
378
+ "description": "Write memories with live CAT commands",
379
+ "required": [
380
+ "catWrite"
381
+ ],
382
+ "additionalProperties": false,
383
+ "properties": {
384
+ "description": {
385
+ "type": "string"
386
+ },
387
+ "catWrite": {
388
+ "$ref": "#/definitions/catMemory"
389
+ }
390
+ }
391
+ },
264
392
  "exchangeStep": {
265
393
  "type": "object",
266
394
  "description": "Send bytes and/or wait for a reply",
@@ -299,6 +427,11 @@
299
427
  "minimum": 1,
300
428
  "default": 5000
301
429
  },
430
+ "delay": {
431
+ "type": "integer",
432
+ "description": "Milliseconds to wait after send",
433
+ "minimum": 0
434
+ },
302
435
  "setBaudRate": {
303
436
  "type": "integer",
304
437
  "description": "Switch serial baud rate before send/expect (TH-D74 clone transfers at 57600)",
@@ -25,7 +25,22 @@ export class SchemaValidator {
25
25
  }
26
26
 
27
27
  validateRadioProtocol(config: unknown): ValidationResult {
28
- return this.validateAgainst(radioProtocolSchema, config);
28
+ const schemaResult = this.validateAgainst(radioProtocolSchema, config);
29
+
30
+ if (!schemaResult.valid) {
31
+ return schemaResult;
32
+ }
33
+
34
+ const baudRateErrors = baudRateMembershipErrors(config);
35
+
36
+ if (baudRateErrors.length === 0) {
37
+ return schemaResult;
38
+ }
39
+
40
+ return {
41
+ valid: false,
42
+ errors: baudRateErrors,
43
+ };
29
44
  }
30
45
 
31
46
  validateMemoryMap(memoryMap: unknown): ValidationResult {
@@ -41,6 +56,7 @@ export class SchemaValidator {
41
56
  }
42
57
 
43
58
  let errors: string[] = [];
59
+
44
60
  if (validate.errors && validate.errors.length > 0) {
45
61
  errors = validate.errors.map((error: AjvError) => {
46
62
  const path = error.instancePath || 'root';
@@ -54,3 +70,31 @@ export class SchemaValidator {
54
70
  };
55
71
  }
56
72
  }
73
+
74
+ function isRecord(value: unknown): value is Record<string, unknown> {
75
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
76
+ }
77
+
78
+ /**
79
+ * `baudRate` is the default open speed. When a radio lists `baudRates`, that
80
+ * default must be one of the accepted values so the UI can preselect it.
81
+ */
82
+ function baudRateMembershipErrors(config: unknown): string[] {
83
+ if (!isRecord(config) || !isRecord(config.serialConfig)) {
84
+ return [];
85
+ }
86
+
87
+ const serialConfig = config.serialConfig;
88
+ const baudRate = serialConfig.baudRate;
89
+ const baudRates = serialConfig.baudRates;
90
+
91
+ if (!Array.isArray(baudRates) || typeof baudRate !== 'number') {
92
+ return [];
93
+ }
94
+
95
+ if (baudRates.includes(baudRate)) {
96
+ return [];
97
+ }
98
+
99
+ return ['/serialConfig/baudRate: must be included in baudRates'];
100
+ }