@springfield/ham-radio-utils 4.6.0 → 4.8.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.
- package/dist/memory/memory-map-channels.d.ts.map +1 -1
- package/dist/memory/memory-map-channels.js +186 -8
- package/dist/memory/memory-map-channels.js.map +1 -1
- package/dist/memory/memory-map-codec.d.ts +6 -1
- package/dist/memory/memory-map-codec.d.ts.map +1 -1
- package/dist/memory/memory-map-codec.js +73 -19
- package/dist/memory/memory-map-codec.js.map +1 -1
- package/dist/memory/memory-map-ui.d.ts.map +1 -1
- package/dist/memory/memory-map-ui.js +18 -9
- package/dist/memory/memory-map-ui.js.map +1 -1
- package/dist/schemas/radio-memory-map-schema.json +33 -3
- package/dist/schemas/radio-protocol-schema.json +15 -0
- package/package.json +2 -2
- package/src/memory/memory-map-channels.ts +230 -8
- package/src/memory/memory-map-codec.ts +85 -19
- package/src/memory/memory-map-ui.ts +20 -9
- package/src/schemas/radio-memory-map-schema.json +33 -3
- package/src/schemas/radio-protocol-schema.json +15 -0
|
@@ -41,6 +41,115 @@ function radioToneToMapTone(tone: RadioTone | undefined): RadioMemoryMapToneValu
|
|
|
41
41
|
return { mode: 'ctcss', value: Number(tone.tone) };
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function noneTone(): RadioTone {
|
|
45
|
+
return { tone: 0, type: RadioToneType.CTCSS };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isNoneTone(tone: RadioTone | undefined): boolean {
|
|
49
|
+
return !tone || !tone.tone;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function structHasField(struct: { fields: { id: string }[] } | undefined, fieldId: string): boolean {
|
|
53
|
+
return Boolean(struct?.fields.some((field) => field.id === fieldId));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function transmitFrequencyFromRecord(record: Record<string, RadioSettingValue>, bindings: NonNullable<RadioMemoryMap['channelBindings']>): number {
|
|
57
|
+
const receiveFrequency = Number(record[bindings.receiveFrequency] ?? 0);
|
|
58
|
+
const offsetOrTransmit = Number(record[bindings.transmitFrequency] ?? 0);
|
|
59
|
+
|
|
60
|
+
if (record.split === true) {
|
|
61
|
+
return offsetOrTransmit;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (record.duplex === '+') {
|
|
65
|
+
return receiveFrequency + offsetOrTransmit;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (record.duplex === '-') {
|
|
69
|
+
return receiveFrequency - offsetOrTransmit;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Kenwood stores simplex as duplex "" and offset 0. Empty duplex is not an absolute TX frequency.
|
|
73
|
+
if (record.duplex === '') {
|
|
74
|
+
return receiveFrequency;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (record.duplex === undefined) {
|
|
78
|
+
return bindings.transmitFrequency === bindings.receiveFrequency ? receiveFrequency : offsetOrTransmit;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return offsetOrTransmit;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function applyKenwoodToneMode(
|
|
85
|
+
record: Record<string, RadioSettingValue>,
|
|
86
|
+
bindings: NonNullable<RadioMemoryMap['channelBindings']>,
|
|
87
|
+
): { receiveTone: RadioTone; transmitTone: RadioTone } {
|
|
88
|
+
const hasToneModeBits = 'tone_mode' in record || 'ctcss_mode' in record || 'dtcs_mode' in record;
|
|
89
|
+
|
|
90
|
+
if (!hasToneModeBits) {
|
|
91
|
+
return {
|
|
92
|
+
receiveTone: toneToRadioTone(record[bindings.receiveTone]),
|
|
93
|
+
transmitTone: toneToRadioTone(record[bindings.transmitTone]),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (record.tone_mode === true) {
|
|
98
|
+
return { receiveTone: noneTone(), transmitTone: toneToRadioTone(record[bindings.transmitTone]) };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (record.ctcss_mode === true) {
|
|
102
|
+
const tone = toneToRadioTone(record[bindings.receiveTone]);
|
|
103
|
+
return { receiveTone: tone, transmitTone: tone };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (record.dtcs_mode === true) {
|
|
107
|
+
const tone = toneToRadioTone(record.dtcs_code);
|
|
108
|
+
return { receiveTone: tone, transmitTone: tone };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { receiveTone: noneTone(), transmitTone: noneTone() };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function kenwoodUsedFlag(receiveFrequency: number, transmitFrequency: number): number {
|
|
115
|
+
const frequency = transmitFrequency || receiveFrequency;
|
|
116
|
+
|
|
117
|
+
if (frequency < 150_000_000) {
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (frequency < 400_000_000) {
|
|
122
|
+
return 1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return 2;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** TM-D710 chmap band codes: 0=118 MHz, 5=144, 6=200, 7=300, 8=400, 9=800. */
|
|
129
|
+
function kenwoodChmapBand(receiveFrequency: number): number {
|
|
130
|
+
if (receiveFrequency < 136_000_000) {
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (receiveFrequency < 200_000_000) {
|
|
135
|
+
return 5;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (receiveFrequency < 300_000_000) {
|
|
139
|
+
return 6;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (receiveFrequency < 400_000_000) {
|
|
143
|
+
return 7;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (receiveFrequency < 800_000_000) {
|
|
147
|
+
return 8;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return 9;
|
|
151
|
+
}
|
|
152
|
+
|
|
44
153
|
function asRecord(value: RadioSettingValue | undefined): Record<string, RadioSettingValue> | undefined {
|
|
45
154
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
46
155
|
return undefined;
|
|
@@ -64,31 +173,41 @@ export function bindingsToChannels(
|
|
|
64
173
|
|
|
65
174
|
const records = settings[bindings.records];
|
|
66
175
|
const names = bindings.names ? settings[bindings.names] : undefined;
|
|
176
|
+
const extras = bindings.extras ? settings[bindings.extras] : undefined;
|
|
67
177
|
const recordItems = Array.isArray(records) ? records : [];
|
|
68
178
|
const nameItems = Array.isArray(names) ? names : [];
|
|
179
|
+
const extraItems = Array.isArray(extras) ? extras : [];
|
|
69
180
|
const nameField = bindings.nameField ?? 'name';
|
|
70
181
|
const boundFieldIds = new Set([
|
|
71
182
|
bindings.receiveFrequency,
|
|
72
183
|
bindings.transmitFrequency,
|
|
73
184
|
bindings.receiveTone,
|
|
74
185
|
bindings.transmitTone,
|
|
186
|
+
nameField,
|
|
75
187
|
]);
|
|
76
188
|
|
|
77
189
|
const channels: RadioProgrammedChannel[] = [];
|
|
78
190
|
|
|
79
191
|
for (let index = 0; index < recordItems.length; index += 1) {
|
|
80
192
|
const record = asRecord(recordItems[index]);
|
|
193
|
+
const extraRecord = asRecord(extraItems[index]);
|
|
194
|
+
|
|
195
|
+
if (bindings.extras && extraItems.length > 0 && extraRecord === undefined) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
81
198
|
|
|
82
199
|
if (!record) {
|
|
83
200
|
continue;
|
|
84
201
|
}
|
|
85
202
|
|
|
86
203
|
const nameRecord = asRecord(nameItems[index]);
|
|
87
|
-
const
|
|
88
|
-
const
|
|
204
|
+
const nameFromTable = typeof nameRecord?.[nameField] === 'string' ? nameRecord[nameField] : '';
|
|
205
|
+
const nameFromRecord = typeof record[nameField] === 'string' ? record[nameField] : '';
|
|
206
|
+
const name = nameFromTable || nameFromRecord;
|
|
89
207
|
|
|
90
208
|
const receiveFrequency = Number(record[bindings.receiveFrequency] ?? 0);
|
|
91
|
-
const transmitFrequency =
|
|
209
|
+
const transmitFrequency = transmitFrequencyFromRecord(record, bindings);
|
|
210
|
+
const tones = applyKenwoodToneMode(record, bindings);
|
|
92
211
|
|
|
93
212
|
const channelSettings: RadioSettings = {};
|
|
94
213
|
|
|
@@ -100,6 +219,20 @@ export function bindingsToChannels(
|
|
|
100
219
|
channelSettings[fieldId] = fieldValue;
|
|
101
220
|
}
|
|
102
221
|
|
|
222
|
+
if (extraRecord) {
|
|
223
|
+
for (const [fieldId, fieldValue] of Object.entries(extraRecord)) {
|
|
224
|
+
if (fieldId.startsWith('_') || fieldId === 'used') {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
channelSettings[fieldId] = fieldValue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (typeof extraRecord.lockout === 'boolean') {
|
|
232
|
+
channelSettings.skip = extraRecord.lockout ? 'S' : '';
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
103
236
|
// Map Chirp lowpower index onto legacy transmitPower watts for UV-5R UI compatibility.
|
|
104
237
|
if (typeof channelSettings.lowpower === 'number') {
|
|
105
238
|
channelSettings.transmitPower = channelSettings.lowpower === 0 ? 5 : 1;
|
|
@@ -113,12 +246,16 @@ export function bindingsToChannels(
|
|
|
113
246
|
channelSettings.skip = channelSettings.scan ? '' : 'S';
|
|
114
247
|
}
|
|
115
248
|
|
|
249
|
+
if (typeof channelSettings.skip === 'boolean') {
|
|
250
|
+
channelSettings.skip = channelSettings.skip ? 'S' : '';
|
|
251
|
+
}
|
|
252
|
+
|
|
116
253
|
const radioChannel: RadioChannel = {
|
|
117
254
|
name,
|
|
118
255
|
receiveFrequency: Frequency(receiveFrequency),
|
|
119
256
|
transmitFrequency: Frequency(transmitFrequency),
|
|
120
|
-
receiveTone:
|
|
121
|
-
transmitTone:
|
|
257
|
+
receiveTone: tones.receiveTone,
|
|
258
|
+
transmitTone: tones.transmitTone,
|
|
122
259
|
};
|
|
123
260
|
|
|
124
261
|
channels.push({
|
|
@@ -149,6 +286,10 @@ export function settingsWithoutChannels(memoryMap: RadioMemoryMap, settings: Rad
|
|
|
149
286
|
delete next[bindings.names];
|
|
150
287
|
}
|
|
151
288
|
|
|
289
|
+
if (bindings.extras) {
|
|
290
|
+
delete next[bindings.extras];
|
|
291
|
+
}
|
|
292
|
+
|
|
152
293
|
return next;
|
|
153
294
|
}
|
|
154
295
|
|
|
@@ -164,11 +305,13 @@ export function programToChannelSettings(memoryMap: RadioMemoryMap, program: Rad
|
|
|
164
305
|
|
|
165
306
|
const recordsStruct = memoryMap.structs.find((struct) => struct.id === bindings.records);
|
|
166
307
|
const namesStruct = bindings.names ? memoryMap.structs.find((struct) => struct.id === bindings.names) : undefined;
|
|
308
|
+
const extrasStruct = bindings.extras ? memoryMap.structs.find((struct) => struct.id === bindings.extras) : undefined;
|
|
167
309
|
const count = recordsStruct?.count ?? 0;
|
|
168
310
|
const nameField = bindings.nameField ?? 'name';
|
|
169
311
|
|
|
170
312
|
const records: RadioSettingValue[] = Array.from({ length: count }, () => null);
|
|
171
313
|
const names: RadioSettingValue[] = Array.from({ length: count }, () => null);
|
|
314
|
+
const extras: RadioSettingValue[] = Array.from({ length: count }, () => null);
|
|
172
315
|
|
|
173
316
|
for (const programmed of program.channels) {
|
|
174
317
|
const index = programmed.channelNumber;
|
|
@@ -183,13 +326,67 @@ export function programToChannelSettings(memoryMap: RadioMemoryMap, program: Rad
|
|
|
183
326
|
|
|
184
327
|
const channel = programmed.radioChannel;
|
|
185
328
|
const channelSettings = programmed.settings ?? {};
|
|
329
|
+
const receiveFrequency = Number(channel.receiveFrequency);
|
|
330
|
+
const transmitFrequency = Number(channel.transmitFrequency);
|
|
331
|
+
let offset = transmitFrequency;
|
|
332
|
+
let duplex: RadioSettingValue = '';
|
|
333
|
+
let split = false;
|
|
334
|
+
|
|
335
|
+
if (transmitFrequency === receiveFrequency) {
|
|
336
|
+
offset = 0;
|
|
337
|
+
duplex = '';
|
|
338
|
+
} else if (transmitFrequency > receiveFrequency) {
|
|
339
|
+
offset = transmitFrequency - receiveFrequency;
|
|
340
|
+
duplex = '+';
|
|
341
|
+
} else {
|
|
342
|
+
offset = receiveFrequency - transmitFrequency;
|
|
343
|
+
duplex = '-';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (channelSettings.split === true) {
|
|
347
|
+
split = true;
|
|
348
|
+
duplex = '';
|
|
349
|
+
offset = transmitFrequency;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const usesOffset =
|
|
353
|
+
'duplex' in channelSettings || extrasStruct || structHasField(recordsStruct, 'duplex');
|
|
354
|
+
const usesKenwoodToneBits =
|
|
355
|
+
'tone_mode' in channelSettings ||
|
|
356
|
+
'ctcss_mode' in channelSettings ||
|
|
357
|
+
extrasStruct ||
|
|
358
|
+
structHasField(recordsStruct, 'tone_mode') ||
|
|
359
|
+
structHasField(recordsStruct, 'ctcss_mode');
|
|
360
|
+
|
|
186
361
|
const record: Record<string, RadioSettingValue> = {
|
|
187
|
-
[bindings.receiveFrequency]:
|
|
188
|
-
[bindings.transmitFrequency]:
|
|
362
|
+
[bindings.receiveFrequency]: receiveFrequency,
|
|
363
|
+
[bindings.transmitFrequency]: usesOffset ? offset : transmitFrequency,
|
|
189
364
|
[bindings.receiveTone]: radioToneToMapTone(channel.receiveTone),
|
|
190
365
|
[bindings.transmitTone]: radioToneToMapTone(channel.transmitTone),
|
|
191
366
|
};
|
|
192
367
|
|
|
368
|
+
if (usesOffset) {
|
|
369
|
+
record.duplex = typeof channelSettings.duplex === 'string' ? channelSettings.duplex : duplex;
|
|
370
|
+
record.split = typeof channelSettings.split === 'boolean' ? channelSettings.split : split;
|
|
371
|
+
record.offset = offset;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (usesKenwoodToneBits) {
|
|
375
|
+
record.tone_mode = false;
|
|
376
|
+
record.ctcss_mode = false;
|
|
377
|
+
record.dtcs_mode = false;
|
|
378
|
+
record.cross_mode = false;
|
|
379
|
+
|
|
380
|
+
if (channel.transmitTone?.type === RadioToneType.DCS || channel.receiveTone?.type === RadioToneType.DCS) {
|
|
381
|
+
record.dtcs_mode = true;
|
|
382
|
+
record.dtcs_code = radioToneToMapTone(channel.transmitTone?.type === RadioToneType.DCS ? channel.transmitTone : channel.receiveTone);
|
|
383
|
+
} else if (!isNoneTone(channel.receiveTone) && !isNoneTone(channel.transmitTone)) {
|
|
384
|
+
record.ctcss_mode = true;
|
|
385
|
+
} else if (!isNoneTone(channel.transmitTone)) {
|
|
386
|
+
record.tone_mode = true;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
193
390
|
for (const [key, value] of Object.entries(channelSettings)) {
|
|
194
391
|
if (key === 'transmitPower') {
|
|
195
392
|
record.lowpower = value === 5 || value === 4 || value === 0 ? 0 : 1;
|
|
@@ -198,11 +395,13 @@ export function programToChannelSettings(memoryMap: RadioMemoryMap, program: Rad
|
|
|
198
395
|
|
|
199
396
|
if (key === 'mode') {
|
|
200
397
|
record.wide = value === 'FM';
|
|
398
|
+
record.mode = value;
|
|
201
399
|
continue;
|
|
202
400
|
}
|
|
203
401
|
|
|
204
402
|
if (key === 'skip') {
|
|
205
403
|
record.scan = value !== 'S';
|
|
404
|
+
record.skip = value === 'S';
|
|
206
405
|
continue;
|
|
207
406
|
}
|
|
208
407
|
|
|
@@ -222,7 +421,26 @@ export function programToChannelSettings(memoryMap: RadioMemoryMap, program: Rad
|
|
|
222
421
|
}
|
|
223
422
|
|
|
224
423
|
records[index] = record;
|
|
225
|
-
|
|
424
|
+
|
|
425
|
+
if (namesStruct) {
|
|
426
|
+
names[index] = { [nameField]: channel.name ?? '' };
|
|
427
|
+
} else {
|
|
428
|
+
record[nameField] = channel.name ?? '';
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (extrasStruct) {
|
|
432
|
+
const extraRecord: Record<string, RadioSettingValue> = {
|
|
433
|
+
used: kenwoodUsedFlag(receiveFrequency, transmitFrequency),
|
|
434
|
+
lockout: channelSettings.skip === 'S' || channelSettings.lockout === true,
|
|
435
|
+
group: typeof channelSettings.group === 'number' ? channelSettings.group : 0,
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
if (structHasField(extrasStruct, 'band')) {
|
|
439
|
+
extraRecord.band = typeof channelSettings.band === 'number' ? channelSettings.band : kenwoodChmapBand(receiveFrequency);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
extras[index] = extraRecord;
|
|
443
|
+
}
|
|
226
444
|
}
|
|
227
445
|
|
|
228
446
|
const bag: RadioSettings = { ...program.settings, [bindings.records]: records };
|
|
@@ -231,6 +449,10 @@ export function programToChannelSettings(memoryMap: RadioMemoryMap, program: Rad
|
|
|
231
449
|
bag[bindings.names] = names;
|
|
232
450
|
}
|
|
233
451
|
|
|
452
|
+
if (bindings.extras && extrasStruct) {
|
|
453
|
+
bag[bindings.extras] = extras;
|
|
454
|
+
}
|
|
455
|
+
|
|
234
456
|
return bag;
|
|
235
457
|
}
|
|
236
458
|
|
|
@@ -78,6 +78,52 @@ function writeByte(contents: Uint8Array, offset: number, value: number): void {
|
|
|
78
78
|
contents[offset] = value & 0xff;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
function decodeInteger(raw: number | number[]): number {
|
|
82
|
+
if (typeof raw === 'number') {
|
|
83
|
+
return raw;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let value = 0;
|
|
87
|
+
|
|
88
|
+
for (let index = 0; index < raw.length; index += 1) {
|
|
89
|
+
value += raw[index] * 256 ** index;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function encodeInteger(value: RadioSettingValue, byteLength: number): number[] {
|
|
96
|
+
let numeric = typeof value === 'number' ? value : 0;
|
|
97
|
+
const bytes: number[] = [];
|
|
98
|
+
|
|
99
|
+
for (let index = 0; index < byteLength; index += 1) {
|
|
100
|
+
bytes.push(numeric & 0xff);
|
|
101
|
+
numeric = Math.floor(numeric / 256);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return bytes;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Radio address of repeated-struct instance `index`, including Kenwood-style
|
|
109
|
+
* grouped stride (N records + pad bytes per clone block).
|
|
110
|
+
*/
|
|
111
|
+
export function structInstanceAddress(struct: RadioMemoryMapStruct, instanceIndex: number): number {
|
|
112
|
+
const seek = parseSeekAddress(struct.seek);
|
|
113
|
+
const stride = struct.stride ?? 0;
|
|
114
|
+
const groupSize = struct.groupSize;
|
|
115
|
+
|
|
116
|
+
if (!groupSize) {
|
|
117
|
+
return seek + instanceIndex * stride;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const groupPad = struct.groupPad ?? 0;
|
|
121
|
+
const groupIndex = Math.floor(instanceIndex / groupSize);
|
|
122
|
+
const indexInGroup = instanceIndex % groupSize;
|
|
123
|
+
|
|
124
|
+
return seek + groupIndex * (stride * groupSize + groupPad) + indexInGroup * stride;
|
|
125
|
+
}
|
|
126
|
+
|
|
81
127
|
function decodeRawValue(kind: RadioMemoryMapValueKind | undefined, raw: number | number[]): RadioSettingValue {
|
|
82
128
|
if (!kind) {
|
|
83
129
|
return typeof raw === 'number' ? raw : raw;
|
|
@@ -85,7 +131,7 @@ function decodeRawValue(kind: RadioMemoryMapValueKind | undefined, raw: number |
|
|
|
85
131
|
|
|
86
132
|
switch (kind.kind) {
|
|
87
133
|
case 'integer':
|
|
88
|
-
return
|
|
134
|
+
return decodeInteger(raw);
|
|
89
135
|
case 'boolean':
|
|
90
136
|
return (typeof raw === 'number' ? raw : raw[0]) !== 0;
|
|
91
137
|
case 'enum': {
|
|
@@ -183,6 +229,16 @@ function decodeRawValue(kind: RadioMemoryMapValueKind | undefined, raw: number |
|
|
|
183
229
|
const code = kind.values[index] ?? 0;
|
|
184
230
|
return { mode: 'dcs', code, polarity };
|
|
185
231
|
}
|
|
232
|
+
case 'ctcss-index': {
|
|
233
|
+
const index = decodeInteger(raw);
|
|
234
|
+
const tenths = kind.values[index] ?? kind.values[0] ?? 0;
|
|
235
|
+
return { mode: 'ctcss', value: tenths };
|
|
236
|
+
}
|
|
237
|
+
case 'dcs-index': {
|
|
238
|
+
const index = decodeInteger(raw);
|
|
239
|
+
const code = kind.values[index] ?? 0;
|
|
240
|
+
return { mode: 'dcs', code, polarity: 'N' };
|
|
241
|
+
}
|
|
186
242
|
default: {
|
|
187
243
|
const exhaustive: never = kind;
|
|
188
244
|
return exhaustive;
|
|
@@ -203,8 +259,7 @@ function encodeRawValue(kind: RadioMemoryMapValueKind | undefined, value: RadioS
|
|
|
203
259
|
|
|
204
260
|
switch (kind.kind) {
|
|
205
261
|
case 'integer': {
|
|
206
|
-
|
|
207
|
-
return bytes;
|
|
262
|
+
return encodeInteger(value, byteLength);
|
|
208
263
|
}
|
|
209
264
|
case 'boolean': {
|
|
210
265
|
bytes[0] = value ? 1 : 0;
|
|
@@ -217,9 +272,10 @@ function encodeRawValue(kind: RadioMemoryMapValueKind | undefined, value: RadioS
|
|
|
217
272
|
}
|
|
218
273
|
case 'ascii': {
|
|
219
274
|
const text = typeof value === 'string' ? value : '';
|
|
275
|
+
const pad = kind.pad ?? 0xff;
|
|
220
276
|
|
|
221
277
|
for (let index = 0; index < byteLength; index += 1) {
|
|
222
|
-
bytes[index] = index < text.length ? (text.codePointAt(index) ??
|
|
278
|
+
bytes[index] = index < text.length ? (text.codePointAt(index) ?? pad) : pad;
|
|
223
279
|
}
|
|
224
280
|
|
|
225
281
|
return bytes;
|
|
@@ -309,6 +365,18 @@ function encodeRawValue(kind: RadioMemoryMapValueKind | undefined, value: RadioS
|
|
|
309
365
|
bytes[1] = (rawValue >> 8) & 0xff;
|
|
310
366
|
return bytes;
|
|
311
367
|
}
|
|
368
|
+
case 'ctcss-index': {
|
|
369
|
+
const tone = value && typeof value === 'object' && !Array.isArray(value) ? (value as { mode?: string; value?: number }) : undefined;
|
|
370
|
+
const tenths = tone?.mode === 'ctcss' && typeof tone.value === 'number' ? tone.value : 0;
|
|
371
|
+
const index = kind.values.indexOf(tenths);
|
|
372
|
+
return encodeInteger(index >= 0 ? index : 0, byteLength);
|
|
373
|
+
}
|
|
374
|
+
case 'dcs-index': {
|
|
375
|
+
const tone = value && typeof value === 'object' && !Array.isArray(value) ? (value as { mode?: string; code?: number }) : undefined;
|
|
376
|
+
const code = tone?.mode === 'dcs' && typeof tone.code === 'number' ? tone.code : 0;
|
|
377
|
+
const index = kind.values.indexOf(code);
|
|
378
|
+
return encodeInteger(index >= 0 ? index : 0, byteLength);
|
|
379
|
+
}
|
|
312
380
|
default: {
|
|
313
381
|
const exhaustive: never = kind;
|
|
314
382
|
return exhaustive;
|
|
@@ -317,6 +385,10 @@ function encodeRawValue(kind: RadioMemoryMapValueKind | undefined, value: RadioS
|
|
|
317
385
|
}
|
|
318
386
|
|
|
319
387
|
function fieldByteLength(field: RadioMemoryMapField): number {
|
|
388
|
+
if (field.type === 'u32') {
|
|
389
|
+
return 4;
|
|
390
|
+
}
|
|
391
|
+
|
|
320
392
|
if (field.type === 'u16') {
|
|
321
393
|
return 2;
|
|
322
394
|
}
|
|
@@ -433,7 +505,7 @@ function decodeStruct(
|
|
|
433
505
|
memoryConfig: RadioMemoryConfig,
|
|
434
506
|
instanceIndex: number,
|
|
435
507
|
): Record<string, RadioSettingValue> {
|
|
436
|
-
const base =
|
|
508
|
+
const base = structInstanceAddress(struct, instanceIndex);
|
|
437
509
|
const result: Record<string, RadioSettingValue> = {};
|
|
438
510
|
let radioAddress = base;
|
|
439
511
|
let bitCursor: BitfieldCursor | undefined;
|
|
@@ -465,7 +537,7 @@ function decodeStruct(
|
|
|
465
537
|
bytes.push(readByte(contents, bufferOffsetFor(radioAddress + index, memoryConfig, contents)));
|
|
466
538
|
}
|
|
467
539
|
|
|
468
|
-
if (field.type === 'u16') {
|
|
540
|
+
if (field.type === 'u16' || field.type === 'u32') {
|
|
469
541
|
if (!field.reserved) {
|
|
470
542
|
result[field.id] = decodeRawValue(field.value ?? { kind: 'integer' }, bytes);
|
|
471
543
|
}
|
|
@@ -486,7 +558,7 @@ function encodeStruct(
|
|
|
486
558
|
memoryConfig: RadioMemoryConfig,
|
|
487
559
|
instanceIndex: number,
|
|
488
560
|
): void {
|
|
489
|
-
const base =
|
|
561
|
+
const base = structInstanceAddress(struct, instanceIndex);
|
|
490
562
|
let radioAddress = base;
|
|
491
563
|
let bitCursor: BitfieldCursor | undefined;
|
|
492
564
|
let bitStartAddress = base;
|
|
@@ -525,18 +597,12 @@ function encodeStruct(
|
|
|
525
597
|
|
|
526
598
|
const settingValue = values[field.id];
|
|
527
599
|
|
|
528
|
-
if (field.type === 'u16') {
|
|
529
|
-
const encoded = encodeRawValue(field.value ?? { kind: 'integer' }, settingValue ?? 0,
|
|
530
|
-
let value = 0;
|
|
600
|
+
if (field.type === 'u16' || field.type === 'u32') {
|
|
601
|
+
const encoded = encodeRawValue(field.value ?? { kind: 'integer' }, settingValue ?? 0, length);
|
|
531
602
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
} else {
|
|
535
|
-
value = (encoded[0] ?? 0) & 0xffff;
|
|
603
|
+
for (let index = 0; index < length; index += 1) {
|
|
604
|
+
writeByte(contents, bufferOffsetFor(radioAddress + index, memoryConfig, contents), encoded[index] ?? 0);
|
|
536
605
|
}
|
|
537
|
-
|
|
538
|
-
writeByte(contents, bufferOffsetFor(radioAddress, memoryConfig, contents), value & 0xff);
|
|
539
|
-
writeByte(contents, bufferOffsetFor(radioAddress + 1, memoryConfig, contents), (value >> 8) & 0xff);
|
|
540
606
|
} else {
|
|
541
607
|
const encoded = encodeRawValue(field.value, settingValue ?? (field.value?.kind === 'ascii' ? '' : 0), length);
|
|
542
608
|
|
|
@@ -563,7 +629,7 @@ function isStructInstanceEmpty(
|
|
|
563
629
|
return false;
|
|
564
630
|
}
|
|
565
631
|
|
|
566
|
-
const base =
|
|
632
|
+
const base = structInstanceAddress(struct, instanceIndex);
|
|
567
633
|
const firstByte = readByte(contents, bufferOffsetFor(base, memoryConfig, contents));
|
|
568
634
|
return firstByte === struct.emptyWhen.equals;
|
|
569
635
|
}
|
|
@@ -574,7 +640,7 @@ function clearStructInstance(
|
|
|
574
640
|
memoryConfig: RadioMemoryConfig,
|
|
575
641
|
instanceIndex: number,
|
|
576
642
|
): void {
|
|
577
|
-
const base =
|
|
643
|
+
const base = structInstanceAddress(struct, instanceIndex);
|
|
578
644
|
const length = struct.stride ?? 16;
|
|
579
645
|
|
|
580
646
|
for (let index = 0; index < length; index += 1) {
|
|
@@ -55,6 +55,10 @@ export function collectMemoryMapUiFields(memoryMap: RadioMemoryMap): RadioMemory
|
|
|
55
55
|
if (memoryMap.channelBindings.names) {
|
|
56
56
|
channelStructIds.add(memoryMap.channelBindings.names);
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
if (memoryMap.channelBindings.extras) {
|
|
60
|
+
channelStructIds.add(memoryMap.channelBindings.extras);
|
|
61
|
+
}
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
for (const struct of memoryMap.structs) {
|
|
@@ -105,6 +109,7 @@ export function collectChannelMemoryMapUiFields(memoryMap: RadioMemoryMap): Radi
|
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
const struct = memoryMap.structs.find((entry) => entry.id === bindings.records);
|
|
112
|
+
const extrasStruct = bindings.extras ? memoryMap.structs.find((entry) => entry.id === bindings.extras) : undefined;
|
|
108
113
|
|
|
109
114
|
if (!struct) {
|
|
110
115
|
return [];
|
|
@@ -113,18 +118,24 @@ export function collectChannelMemoryMapUiFields(memoryMap: RadioMemoryMap): Radi
|
|
|
113
118
|
const boundIds = channelBoundFieldIds(memoryMap);
|
|
114
119
|
const fields: RadioMemoryMapUiField[] = [];
|
|
115
120
|
|
|
116
|
-
for (const
|
|
117
|
-
if (
|
|
121
|
+
for (const source of [struct, extrasStruct]) {
|
|
122
|
+
if (!source) {
|
|
118
123
|
continue;
|
|
119
124
|
}
|
|
120
125
|
|
|
121
|
-
fields
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
for (const field of source.fields) {
|
|
127
|
+
if (field.reserved || !field.ui || boundIds.has(field.id)) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fields.push({
|
|
132
|
+
path: field.id,
|
|
133
|
+
structId: source.id,
|
|
134
|
+
fieldId: field.id,
|
|
135
|
+
ui: field.ui,
|
|
136
|
+
value: field.value,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
128
139
|
}
|
|
129
140
|
|
|
130
141
|
return fields;
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"receiveFrequency": { "type": "string", "minLength": 1 },
|
|
41
41
|
"transmitFrequency": { "type": "string", "minLength": 1 },
|
|
42
42
|
"receiveTone": { "type": "string", "minLength": 1 },
|
|
43
|
-
"transmitTone": { "type": "string", "minLength": 1 }
|
|
43
|
+
"transmitTone": { "type": "string", "minLength": 1 },
|
|
44
|
+
"extras": { "type": "string", "minLength": 1 }
|
|
44
45
|
},
|
|
45
46
|
"additionalProperties": false
|
|
46
47
|
},
|
|
@@ -105,7 +106,8 @@
|
|
|
105
106
|
"required": ["kind", "length"],
|
|
106
107
|
"properties": {
|
|
107
108
|
"kind": { "const": "ascii" },
|
|
108
|
-
"length": { "type": "integer", "minimum": 1 }
|
|
109
|
+
"length": { "type": "integer", "minimum": 1 },
|
|
110
|
+
"pad": { "type": "integer", "minimum": 0, "maximum": 255 }
|
|
109
111
|
},
|
|
110
112
|
"additionalProperties": false
|
|
111
113
|
},
|
|
@@ -162,6 +164,32 @@
|
|
|
162
164
|
"reverseOffset": { "type": "integer" }
|
|
163
165
|
},
|
|
164
166
|
"additionalProperties": false
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"type": "object",
|
|
170
|
+
"required": ["kind", "values"],
|
|
171
|
+
"properties": {
|
|
172
|
+
"kind": { "const": "ctcss-index" },
|
|
173
|
+
"values": {
|
|
174
|
+
"type": "array",
|
|
175
|
+
"items": { "type": "integer" },
|
|
176
|
+
"minItems": 1
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
"additionalProperties": false
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"type": "object",
|
|
183
|
+
"required": ["kind", "values"],
|
|
184
|
+
"properties": {
|
|
185
|
+
"kind": { "const": "dcs-index" },
|
|
186
|
+
"values": {
|
|
187
|
+
"type": "array",
|
|
188
|
+
"items": { "type": "integer" },
|
|
189
|
+
"minItems": 1
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
"additionalProperties": false
|
|
165
193
|
}
|
|
166
194
|
]
|
|
167
195
|
},
|
|
@@ -172,7 +200,7 @@
|
|
|
172
200
|
"id": { "type": "string", "minLength": 1 },
|
|
173
201
|
"type": {
|
|
174
202
|
"type": "string",
|
|
175
|
-
"enum": ["u8", "u16", "bits", "char"]
|
|
203
|
+
"enum": ["u8", "u16", "u32", "bits", "char"]
|
|
176
204
|
},
|
|
177
205
|
"width": { "type": "integer", "minimum": 1, "maximum": 8 },
|
|
178
206
|
"reserved": { "type": "boolean" },
|
|
@@ -194,6 +222,8 @@
|
|
|
194
222
|
},
|
|
195
223
|
"count": { "type": "integer", "minimum": 1 },
|
|
196
224
|
"stride": { "type": "integer", "minimum": 1 },
|
|
225
|
+
"groupSize": { "type": "integer", "minimum": 1 },
|
|
226
|
+
"groupPad": { "type": "integer", "minimum": 0 },
|
|
197
227
|
"emptyWhen": { "$ref": "#/definitions/emptyWhen" },
|
|
198
228
|
"clearEmpty": { "type": "boolean" }
|
|
199
229
|
},
|