@springfield/ham-radio-utils 4.9.0 → 4.10.1
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/dist/memory/memory-map-radio-codec.d.ts +4 -0
- package/dist/memory/memory-map-radio-codec.d.ts.map +1 -1
- package/dist/memory/memory-map-radio-codec.js +5 -14
- package/dist/memory/memory-map-radio-codec.js.map +1 -1
- package/dist/schemas/radio-protocol-schema.json +12 -1
- package/dist/utils/schema-validator.d.ts.map +1 -1
- package/dist/utils/schema-validator.js +34 -1
- package/dist/utils/schema-validator.js.map +1 -1
- package/package.json +1 -1
- package/src/memory/memory-map-radio-codec.ts +5 -15
- package/src/schemas/radio-protocol-schema.json +12 -1
- package/src/utils/schema-validator.ts +45 -1
|
@@ -25,6 +25,10 @@ export declare class MemoryMapRadioCodec implements RadioCodec {
|
|
|
25
25
|
private readonly logger;
|
|
26
26
|
constructor(options: MemoryMapRadioCodecOptions);
|
|
27
27
|
decode(memory: RadioMemory): RadioProgram;
|
|
28
|
+
/**
|
|
29
|
+
* Patch `program` into a copy of `memory`. Existing image length is preserved
|
|
30
|
+
* (packed driver reads stay packed; sparse images stay sparse).
|
|
31
|
+
*/
|
|
28
32
|
encode(program: RadioProgram, memory: RadioMemory): RadioMemory;
|
|
29
33
|
}
|
|
30
34
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-map-radio-codec.d.ts","sourceRoot":"","sources":["../../src/memory/memory-map-radio-codec.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,WAAW,EACX,iBAAiB,EACjB,cAAc,EACd,YAAY,EACZ,YAAY,EACb,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAG1C;;GAEG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,iBAAiB,GAAG,MAAM,CAGvE;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,CAIpE;AAED,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,YAAY,CAAC;IACzB,SAAS,EAAE,cAAc,CAAC;IAC1B,YAAY,EAAE,iBAAiB,CAAC;IAChC,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB;AAED;;;GAGG;AACH,qBAAa,mBAAoB,YAAW,UAAU;IACpD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAe;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiB;IAC3C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAoB;IACjD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwB;IAE/C,YAAY,OAAO,EAAE,0BAA0B,EAK9C;IAED,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,YAAY,CAOxC;IAED,MAAM,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,GAAG,WAAW,
|
|
1
|
+
{"version":3,"file":"memory-map-radio-codec.d.ts","sourceRoot":"","sources":["../../src/memory/memory-map-radio-codec.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,WAAW,EACX,iBAAiB,EACjB,cAAc,EACd,YAAY,EACZ,YAAY,EACb,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAG1C;;GAEG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,iBAAiB,GAAG,MAAM,CAGvE;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,CAIpE;AAED,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,YAAY,CAAC;IACzB,SAAS,EAAE,cAAc,CAAC;IAC1B,YAAY,EAAE,iBAAiB,CAAC;IAChC,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB;AAED;;;GAGG;AACH,qBAAa,mBAAoB,YAAW,UAAU;IACpD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAe;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiB;IAC3C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAoB;IACjD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwB;IAE/C,YAAY,OAAO,EAAE,0BAA0B,EAK9C;IAED,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,YAAY,CAOxC;IAED;;;OAGG;IACH,MAAM,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,GAAG,WAAW,CAY9D;CACF;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,0BAA0B,GAAG,UAAU,CAEpF"}
|
|
@@ -38,21 +38,12 @@ export class MemoryMapRadioCodec {
|
|
|
38
38
|
return { channels: [], settings: {} };
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Patch `program` into a copy of `memory`. Existing image length is preserved
|
|
43
|
+
* (packed driver reads stay packed; sparse images stay sparse).
|
|
44
|
+
*/
|
|
41
45
|
encode(program, memory) {
|
|
42
|
-
const
|
|
43
|
-
const contents = memory.contents.length > 0 ? new Uint8Array(memory.contents) : createEmptyMemoryImage(totalSize);
|
|
44
|
-
if (contents.length < totalSize) {
|
|
45
|
-
const expanded = createEmptyMemoryImage(totalSize);
|
|
46
|
-
expanded.set(contents);
|
|
47
|
-
try {
|
|
48
|
-
encodeRadioProgram(this.memoryMap, program, expanded, this.memoryConfig);
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
this.logger?.withError(error).warn('Failed to encode radio program into memory map');
|
|
52
|
-
}
|
|
53
|
-
this.logger?.debug(`Memory size: ${expanded.length} bytes`);
|
|
54
|
-
return { contents: expanded, radioModel: this.radioModel };
|
|
55
|
-
}
|
|
46
|
+
const contents = memory.contents.length > 0 ? new Uint8Array(memory.contents) : createEmptyMemoryImage(memoryImageSize(this.memoryConfig));
|
|
56
47
|
try {
|
|
57
48
|
encodeRadioProgram(this.memoryMap, program, contents, this.memoryConfig);
|
|
58
49
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-map-radio-codec.js","sourceRoot":"","sources":["../../src/memory/memory-map-radio-codec.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAElF;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,YAA+B;IAC7D,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IACvF,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;AAC/B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,sBAAsB,CAAC,SAAiB;IACtD,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,SAAS,CAAC,CAAC;IACzC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClB,OAAO,MAAM,CAAC;AAChB,CAAC;AASD;;;GAGG;AACH,MAAM,OAAO,mBAAmB;IACb,UAAU,CAAe;IACzB,SAAS,CAAiB;IAC1B,YAAY,CAAoB;IAChC,MAAM,CAAwB;IAE/C,YAAY,OAAmC;QAC7C,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;QACrC,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;QACnC,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;QACzC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAC/B,CAAC;IAED,MAAM,CAAC,MAAmB;QACxB,IAAI,CAAC;YACH,OAAO,kBAAkB,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QAChF,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,gDAAgD,CAAC,CAAC;YACrF,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;QACxC,CAAC;IACH,CAAC;IAED,MAAM,CAAC,OAAqB,EAAE,MAAmB;QAC/C,MAAM,
|
|
1
|
+
{"version":3,"file":"memory-map-radio-codec.js","sourceRoot":"","sources":["../../src/memory/memory-map-radio-codec.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAElF;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,YAA+B;IAC7D,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IACvF,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;AAC/B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,sBAAsB,CAAC,SAAiB;IACtD,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,SAAS,CAAC,CAAC;IACzC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClB,OAAO,MAAM,CAAC;AAChB,CAAC;AASD;;;GAGG;AACH,MAAM,OAAO,mBAAmB;IACb,UAAU,CAAe;IACzB,SAAS,CAAiB;IAC1B,YAAY,CAAoB;IAChC,MAAM,CAAwB;IAE/C,YAAY,OAAmC;QAC7C,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;QACrC,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;QACnC,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;QACzC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAC/B,CAAC;IAED,MAAM,CAAC,MAAmB;QACxB,IAAI,CAAC;YACH,OAAO,kBAAkB,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QAChF,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,gDAAgD,CAAC,CAAC;YACrF,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;QACxC,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,OAAqB,EAAE,MAAmB;QAC/C,MAAM,QAAQ,GACZ,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,sBAAsB,CAAC,eAAe,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;QAE5H,IAAI,CAAC;YACH,kBAAkB,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QAC3E,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,gDAAgD,CAAC,CAAC;QACvF,CAAC;QAED,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,gBAAgB,QAAQ,CAAC,MAAM,QAAQ,CAAC,CAAC;QAC5D,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC;IACnD,CAAC;CACF;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAmC;IACtE,OAAO,IAAI,mBAAmB,CAAC,OAAO,CAAC,CAAC;AAC1C,CAAC","sourcesContent":["import type {\n RadioCodec,\n RadioMemory,\n RadioMemoryConfig,\n RadioMemoryMap,\n RadioModelId,\n RadioProgram,\n} from '@springfield/ham-radio-api';\nimport type { ILogLayer } from 'loglayer';\nimport { decodeRadioProgram, encodeRadioProgram } from './memory-map-channels.js';\n\n/**\n * Compute the sparse image size that covers every configured memory segment.\n */\nexport function memoryImageSize(memoryConfig: RadioMemoryConfig): number {\n const ends = Object.values(memoryConfig.segments).map((segment) => segment.endAddress);\n return Math.max(...ends) + 1;\n}\n\n/**\n * Create a Chirp-like empty EEPROM image filled with 0xFF.\n */\nexport function createEmptyMemoryImage(totalSize: number): Uint8Array {\n const memory = new Uint8Array(totalSize);\n memory.fill(0xff);\n return memory;\n}\n\nexport interface MemoryMapRadioCodecOptions {\n radioModel: RadioModelId;\n memoryMap: RadioMemoryMap;\n memoryConfig: RadioMemoryConfig;\n logger?: ILogLayer;\n}\n\n/**\n * Generic RadioCodec driven entirely by a JSON memory map + memoryConfig.\n * Radio modules that ship DSL JSON do not need TypeScript codecs.\n */\nexport class MemoryMapRadioCodec implements RadioCodec {\n private readonly radioModel: RadioModelId;\n private readonly memoryMap: RadioMemoryMap;\n private readonly memoryConfig: RadioMemoryConfig;\n private readonly logger: ILogLayer | undefined;\n\n constructor(options: MemoryMapRadioCodecOptions) {\n this.radioModel = options.radioModel;\n this.memoryMap = options.memoryMap;\n this.memoryConfig = options.memoryConfig;\n this.logger = options.logger;\n }\n\n decode(memory: RadioMemory): RadioProgram {\n try {\n return decodeRadioProgram(this.memoryMap, memory.contents, this.memoryConfig);\n } catch (error) {\n this.logger?.withError(error).warn('Failed to decode radio program from memory map');\n return { channels: [], settings: {} };\n }\n }\n\n /**\n * Patch `program` into a copy of `memory`. Existing image length is preserved\n * (packed driver reads stay packed; sparse images stay sparse).\n */\n encode(program: RadioProgram, memory: RadioMemory): RadioMemory {\n const contents =\n memory.contents.length > 0 ? new Uint8Array(memory.contents) : createEmptyMemoryImage(memoryImageSize(this.memoryConfig));\n\n try {\n encodeRadioProgram(this.memoryMap, program, contents, this.memoryConfig);\n } catch (error) {\n this.logger?.withError(error).warn('Failed to encode radio program into memory map');\n }\n\n this.logger?.debug(`Memory size: ${contents.length} bytes`);\n return { contents, radioModel: this.radioModel };\n }\n}\n\n/**\n * Create a memory-map codec from hydrated radio config fields.\n */\nexport function createMemoryMapCodec(options: MemoryMapRadioCodecOptions): RadioCodec {\n return new MemoryMapRadioCodec(options);\n}\n"]}
|
|
@@ -78,10 +78,21 @@
|
|
|
78
78
|
"properties": {
|
|
79
79
|
"baudRate": {
|
|
80
80
|
"type": "integer",
|
|
81
|
-
"description": "
|
|
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",
|
|
@@ -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,
|
|
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
|
-
|
|
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,
|
|
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
|
@@ -59,23 +59,13 @@ export class MemoryMapRadioCodec implements RadioCodec {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Patch `program` into a copy of `memory`. Existing image length is preserved
|
|
64
|
+
* (packed driver reads stay packed; sparse images stay sparse).
|
|
65
|
+
*/
|
|
62
66
|
encode(program: RadioProgram, memory: RadioMemory): RadioMemory {
|
|
63
|
-
const totalSize = memoryImageSize(this.memoryConfig);
|
|
64
67
|
const contents =
|
|
65
|
-
memory.contents.length > 0 ? new Uint8Array(memory.contents) : createEmptyMemoryImage(
|
|
66
|
-
|
|
67
|
-
if (contents.length < totalSize) {
|
|
68
|
-
const expanded = createEmptyMemoryImage(totalSize);
|
|
69
|
-
expanded.set(contents);
|
|
70
|
-
try {
|
|
71
|
-
encodeRadioProgram(this.memoryMap, program, expanded, this.memoryConfig);
|
|
72
|
-
} catch (error) {
|
|
73
|
-
this.logger?.withError(error).warn('Failed to encode radio program into memory map');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
this.logger?.debug(`Memory size: ${expanded.length} bytes`);
|
|
77
|
-
return { contents: expanded, radioModel: this.radioModel };
|
|
78
|
-
}
|
|
68
|
+
memory.contents.length > 0 ? new Uint8Array(memory.contents) : createEmptyMemoryImage(memoryImageSize(this.memoryConfig));
|
|
79
69
|
|
|
80
70
|
try {
|
|
81
71
|
encodeRadioProgram(this.memoryMap, program, contents, this.memoryConfig);
|
|
@@ -78,10 +78,21 @@
|
|
|
78
78
|
"properties": {
|
|
79
79
|
"baudRate": {
|
|
80
80
|
"type": "integer",
|
|
81
|
-
"description": "
|
|
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",
|
|
@@ -25,7 +25,22 @@ export class SchemaValidator {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
validateRadioProtocol(config: unknown): ValidationResult {
|
|
28
|
-
|
|
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
|
+
}
|