@springfield/ham-radio-utils 4.1.2 → 4.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.
@@ -0,0 +1,84 @@
1
+ import type { RadioMemoryMap, RadioMemoryMapFieldUi, RadioMemoryMapValueKind } from '@springfield/ham-radio-api';
2
+
3
+ /**
4
+ * Flattened field descriptor for schema-driven settings UI.
5
+ */
6
+ export interface RadioMemoryMapUiField {
7
+ /** Dot path into decoded settings, e.g. settings.squelch or pttid.0.code */
8
+ path: string;
9
+ structId: string;
10
+ fieldId: string;
11
+ arrayIndex?: number;
12
+ ui: RadioMemoryMapFieldUi;
13
+ value?: RadioMemoryMapValueKind;
14
+ }
15
+
16
+ /**
17
+ * Collect writable UI fields from a memory map, in declaration order.
18
+ */
19
+ export function collectMemoryMapUiFields(memoryMap: RadioMemoryMap): RadioMemoryMapUiField[] {
20
+ const fields: RadioMemoryMapUiField[] = [];
21
+ const channelStructIds = new Set<string>();
22
+
23
+ if (memoryMap.channelBindings) {
24
+ channelStructIds.add(memoryMap.channelBindings.records);
25
+
26
+ if (memoryMap.channelBindings.names) {
27
+ channelStructIds.add(memoryMap.channelBindings.names);
28
+ }
29
+ }
30
+
31
+ for (const struct of memoryMap.structs) {
32
+ if (channelStructIds.has(struct.id)) {
33
+ continue;
34
+ }
35
+
36
+ const count = struct.count ?? 1;
37
+
38
+ for (let index = 0; index < count; index += 1) {
39
+ for (const field of struct.fields) {
40
+ if (field.reserved || !field.ui) {
41
+ continue;
42
+ }
43
+
44
+ const path =
45
+ count > 1 ? `${struct.id}.${index}.${field.id}` : `${struct.id}.${field.id}`;
46
+
47
+ const label =
48
+ count > 1 && field.ui.label.indexOf('%') === -1
49
+ ? `${field.ui.label} ${index + 1}`
50
+ : field.ui.label;
51
+
52
+ fields.push({
53
+ path,
54
+ structId: struct.id,
55
+ fieldId: field.id,
56
+ arrayIndex: count > 1 ? index : undefined,
57
+ ui: { ...field.ui, label },
58
+ value: field.value,
59
+ });
60
+ }
61
+ }
62
+ }
63
+
64
+ return fields;
65
+ }
66
+
67
+ /**
68
+ * Group UI fields by their ui.group key, preserving first-seen group order.
69
+ */
70
+ export function groupMemoryMapUiFields(fields: RadioMemoryMapUiField[]): Map<string, RadioMemoryMapUiField[]> {
71
+ const groups = new Map<string, RadioMemoryMapUiField[]>();
72
+
73
+ for (const field of fields) {
74
+ const existing = groups.get(field.ui.group);
75
+
76
+ if (existing) {
77
+ existing.push(field);
78
+ } else {
79
+ groups.set(field.ui.group, [field]);
80
+ }
81
+ }
82
+
83
+ return groups;
84
+ }
@@ -0,0 +1,203 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Radio Memory-Map DSL Schema",
4
+ "description": "Schema for defining radio EEPROM settings layout as JSON (Chirp MEM_FORMAT as data)",
5
+ "type": "object",
6
+ "required": ["structs"],
7
+ "properties": {
8
+ "version": {
9
+ "type": "string",
10
+ "description": "Optional map version"
11
+ },
12
+ "description": {
13
+ "type": "string"
14
+ },
15
+ "structs": {
16
+ "type": "array",
17
+ "minItems": 1,
18
+ "items": {
19
+ "$ref": "#/definitions/struct"
20
+ }
21
+ },
22
+ "channelBindings": {
23
+ "$ref": "#/definitions/channelBindings"
24
+ }
25
+ },
26
+ "definitions": {
27
+ "seek": {
28
+ "oneOf": [
29
+ { "type": "integer", "minimum": 0 },
30
+ { "type": "string", "pattern": "^(0[xX][0-9a-fA-F]+|[0-9]+)$" }
31
+ ]
32
+ },
33
+ "channelBindings": {
34
+ "type": "object",
35
+ "required": ["records", "receiveFrequency", "transmitFrequency", "receiveTone", "transmitTone"],
36
+ "properties": {
37
+ "records": { "type": "string", "minLength": 1 },
38
+ "names": { "type": "string", "minLength": 1 },
39
+ "nameField": { "type": "string", "minLength": 1 },
40
+ "receiveFrequency": { "type": "string", "minLength": 1 },
41
+ "transmitFrequency": { "type": "string", "minLength": 1 },
42
+ "receiveTone": { "type": "string", "minLength": 1 },
43
+ "transmitTone": { "type": "string", "minLength": 1 }
44
+ },
45
+ "additionalProperties": false
46
+ },
47
+ "emptyWhen": {
48
+ "type": "object",
49
+ "required": ["equals"],
50
+ "properties": {
51
+ "equals": { "type": "integer", "minimum": 0, "maximum": 255 }
52
+ },
53
+ "additionalProperties": false
54
+ },
55
+ "ui": {
56
+ "type": "object",
57
+ "required": ["group", "label", "widget"],
58
+ "properties": {
59
+ "group": { "type": "string" },
60
+ "label": { "type": "string" },
61
+ "widget": {
62
+ "type": "string",
63
+ "enum": ["integer", "select", "switch", "text", "number"]
64
+ },
65
+ "description": { "type": "string" },
66
+ "writable": { "type": "boolean" }
67
+ },
68
+ "additionalProperties": false
69
+ },
70
+ "value": {
71
+ "oneOf": [
72
+ {
73
+ "type": "object",
74
+ "required": ["kind"],
75
+ "properties": {
76
+ "kind": { "const": "integer" },
77
+ "min": { "type": "number" },
78
+ "max": { "type": "number" }
79
+ },
80
+ "additionalProperties": false
81
+ },
82
+ {
83
+ "type": "object",
84
+ "required": ["kind"],
85
+ "properties": {
86
+ "kind": { "const": "boolean" }
87
+ },
88
+ "additionalProperties": false
89
+ },
90
+ {
91
+ "type": "object",
92
+ "required": ["kind", "values"],
93
+ "properties": {
94
+ "kind": { "const": "enum" },
95
+ "values": {
96
+ "type": "array",
97
+ "items": { "type": "string" },
98
+ "minItems": 1
99
+ }
100
+ },
101
+ "additionalProperties": false
102
+ },
103
+ {
104
+ "type": "object",
105
+ "required": ["kind", "length"],
106
+ "properties": {
107
+ "kind": { "const": "ascii" },
108
+ "length": { "type": "integer", "minimum": 1 }
109
+ },
110
+ "additionalProperties": false
111
+ },
112
+ {
113
+ "type": "object",
114
+ "required": ["kind", "length"],
115
+ "properties": {
116
+ "kind": { "const": "digits" },
117
+ "length": { "type": "integer", "minimum": 1 },
118
+ "scale": { "type": "number" }
119
+ },
120
+ "additionalProperties": false
121
+ },
122
+ {
123
+ "type": "object",
124
+ "required": ["kind", "length"],
125
+ "properties": {
126
+ "kind": { "const": "dtmf" },
127
+ "length": { "type": "integer", "minimum": 1 },
128
+ "charset": { "type": "string" }
129
+ },
130
+ "additionalProperties": false
131
+ },
132
+ {
133
+ "type": "object",
134
+ "required": ["kind", "length"],
135
+ "properties": {
136
+ "kind": { "const": "bbcd" },
137
+ "length": { "type": "integer", "minimum": 1 }
138
+ },
139
+ "additionalProperties": false
140
+ },
141
+ {
142
+ "type": "object",
143
+ "required": ["kind", "length"],
144
+ "properties": {
145
+ "kind": { "const": "lbcd" },
146
+ "length": { "type": "integer", "minimum": 1 },
147
+ "scale": { "type": "number" }
148
+ },
149
+ "additionalProperties": false
150
+ },
151
+ {
152
+ "type": "object",
153
+ "required": ["kind", "values"],
154
+ "properties": {
155
+ "kind": { "const": "tone" },
156
+ "values": {
157
+ "type": "array",
158
+ "items": { "type": "integer" },
159
+ "minItems": 1
160
+ },
161
+ "ctcssMin": { "type": "integer" },
162
+ "reverseOffset": { "type": "integer" }
163
+ },
164
+ "additionalProperties": false
165
+ }
166
+ ]
167
+ },
168
+ "field": {
169
+ "type": "object",
170
+ "required": ["id", "type"],
171
+ "properties": {
172
+ "id": { "type": "string", "minLength": 1 },
173
+ "type": {
174
+ "type": "string",
175
+ "enum": ["u8", "u16", "bits", "char"]
176
+ },
177
+ "width": { "type": "integer", "minimum": 1, "maximum": 8 },
178
+ "reserved": { "type": "boolean" },
179
+ "value": { "$ref": "#/definitions/value" },
180
+ "ui": { "$ref": "#/definitions/ui" }
181
+ },
182
+ "additionalProperties": false
183
+ },
184
+ "struct": {
185
+ "type": "object",
186
+ "required": ["id", "seek", "fields"],
187
+ "properties": {
188
+ "id": { "type": "string", "minLength": 1 },
189
+ "seek": { "$ref": "#/definitions/seek" },
190
+ "fields": {
191
+ "type": "array",
192
+ "minItems": 1,
193
+ "items": { "$ref": "#/definitions/field" }
194
+ },
195
+ "count": { "type": "integer", "minimum": 1 },
196
+ "stride": { "type": "integer", "minimum": 1 },
197
+ "emptyWhen": { "$ref": "#/definitions/emptyWhen" },
198
+ "clearEmpty": { "type": "boolean" }
199
+ },
200
+ "additionalProperties": false
201
+ }
202
+ }
203
+ }
@@ -1,6 +1,7 @@
1
1
  import { Ajv } from 'ajv';
2
2
  import addFormats from 'ajv-formats';
3
3
  import radioProtocolSchema from '../schemas/radio-protocol-schema.json' with { type: 'json' };
4
+ import radioMemoryMapSchema from '../schemas/radio-memory-map-schema.json' with { type: 'json' };
4
5
 
5
6
  export interface ValidationResult {
6
7
  valid: boolean;
@@ -24,8 +25,16 @@ export class SchemaValidator {
24
25
  }
25
26
 
26
27
  validateRadioProtocol(config: unknown): ValidationResult {
27
- const validate = this.ajv.compile(radioProtocolSchema);
28
- const valid = validate(config);
28
+ return this.validateAgainst(radioProtocolSchema, config);
29
+ }
30
+
31
+ validateMemoryMap(memoryMap: unknown): ValidationResult {
32
+ return this.validateAgainst(radioMemoryMapSchema, memoryMap);
33
+ }
34
+
35
+ private validateAgainst(schema: object, data: unknown): ValidationResult {
36
+ const validate = this.ajv.compile(schema);
37
+ const valid = validate(data);
29
38
 
30
39
  if (valid) {
31
40
  return { valid: true };