@springfield/ham-radio-utils 4.3.0 → 4.5.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-ui.d.ts +11 -1
- package/dist/memory/memory-map-ui.d.ts.map +1 -1
- package/dist/memory/memory-map-ui.js +79 -0
- package/dist/memory/memory-map-ui.js.map +1 -1
- package/dist/schemas/radio-protocol-schema.json +34 -0
- package/package.json +1 -1
- package/src/memory/memory-map-ui.ts +109 -1
- package/src/schemas/radio-protocol-schema.json +34 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RadioMemoryMap, RadioMemoryMapFieldUi, RadioMemoryMapValueKind } from '@springfield/ham-radio-api';
|
|
1
|
+
import type { RadioMemoryMap, RadioMemoryMapFieldUi, RadioMemoryMapValueKind, RadioSettingValue } from '@springfield/ham-radio-api';
|
|
2
2
|
/**
|
|
3
3
|
* Flattened field descriptor for schema-driven settings UI.
|
|
4
4
|
*/
|
|
@@ -13,8 +13,18 @@ export interface RadioMemoryMapUiField {
|
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Collect writable UI fields from a memory map, in declaration order.
|
|
16
|
+
* Channel-bound structs are skipped (use {@link collectChannelMemoryMapUiFields}).
|
|
16
17
|
*/
|
|
17
18
|
export declare function collectMemoryMapUiFields(memoryMap: RadioMemoryMap): RadioMemoryMapUiField[];
|
|
19
|
+
/**
|
|
20
|
+
* Collect per-channel UI fields from the `channelBindings.records` struct.
|
|
21
|
+
* Skips reserved fields, fields without `ui`, and fields bound onto RadioChannel.
|
|
22
|
+
*/
|
|
23
|
+
export declare function collectChannelMemoryMapUiFields(memoryMap: RadioMemoryMap): RadioMemoryMapUiField[];
|
|
24
|
+
/**
|
|
25
|
+
* Format a decoded memory-map field value for read-only display (e.g. channel table).
|
|
26
|
+
*/
|
|
27
|
+
export declare function formatMemoryMapFieldValue(value: RadioSettingValue | undefined, field: Pick<RadioMemoryMapUiField, 'value' | 'fieldId'>): string;
|
|
18
28
|
/**
|
|
19
29
|
* Group UI fields by their ui.group key, preserving first-seen group order.
|
|
20
30
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-map-ui.d.ts","sourceRoot":"","sources":["../../src/memory/memory-map-ui.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"memory-map-ui.d.ts","sourceRoot":"","sources":["../../src/memory/memory-map-ui.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,qBAAqB,EACrB,uBAAuB,EACvB,iBAAiB,EAClB,MAAM,4BAA4B,CAAC;AAEpC;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,EAAE,EAAE,qBAAqB,CAAC;IAC1B,KAAK,CAAC,EAAE,uBAAuB,CAAC;CACjC;AAyBD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,cAAc,GAAG,qBAAqB,EAAE,CA8C3F;AAED;;;GAGG;AACH,wBAAgB,+BAA+B,CAAC,SAAS,EAAE,cAAc,GAAG,qBAAqB,EAAE,CA+BlG;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,iBAAiB,GAAG,SAAS,EACpC,KAAK,EAAE,IAAI,CAAC,qBAAqB,EAAE,OAAO,GAAG,SAAS,CAAC,GACtD,MAAM,CAkCR;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,qBAAqB,EAAE,CAAC,CAc5G"}
|
|
@@ -1,5 +1,25 @@
|
|
|
1
|
+
function channelBoundFieldIds(memoryMap) {
|
|
2
|
+
const bindings = memoryMap.channelBindings;
|
|
3
|
+
const ids = new Set();
|
|
4
|
+
if (!bindings) {
|
|
5
|
+
return ids;
|
|
6
|
+
}
|
|
7
|
+
for (const key of [
|
|
8
|
+
bindings.receiveFrequency,
|
|
9
|
+
bindings.transmitFrequency,
|
|
10
|
+
bindings.receiveTone,
|
|
11
|
+
bindings.transmitTone,
|
|
12
|
+
bindings.nameField,
|
|
13
|
+
]) {
|
|
14
|
+
if (key) {
|
|
15
|
+
ids.add(key);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return ids;
|
|
19
|
+
}
|
|
1
20
|
/**
|
|
2
21
|
* Collect writable UI fields from a memory map, in declaration order.
|
|
22
|
+
* Channel-bound structs are skipped (use {@link collectChannelMemoryMapUiFields}).
|
|
3
23
|
*/
|
|
4
24
|
export function collectMemoryMapUiFields(memoryMap) {
|
|
5
25
|
const fields = [];
|
|
@@ -37,6 +57,65 @@ export function collectMemoryMapUiFields(memoryMap) {
|
|
|
37
57
|
}
|
|
38
58
|
return fields;
|
|
39
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Collect per-channel UI fields from the `channelBindings.records` struct.
|
|
62
|
+
* Skips reserved fields, fields without `ui`, and fields bound onto RadioChannel.
|
|
63
|
+
*/
|
|
64
|
+
export function collectChannelMemoryMapUiFields(memoryMap) {
|
|
65
|
+
const bindings = memoryMap.channelBindings;
|
|
66
|
+
if (!bindings) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
const struct = memoryMap.structs.find((entry) => entry.id === bindings.records);
|
|
70
|
+
if (!struct) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
const boundIds = channelBoundFieldIds(memoryMap);
|
|
74
|
+
const fields = [];
|
|
75
|
+
for (const field of struct.fields) {
|
|
76
|
+
if (field.reserved || !field.ui || boundIds.has(field.id)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
fields.push({
|
|
80
|
+
path: field.id,
|
|
81
|
+
structId: struct.id,
|
|
82
|
+
fieldId: field.id,
|
|
83
|
+
ui: field.ui,
|
|
84
|
+
value: field.value,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return fields;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Format a decoded memory-map field value for read-only display (e.g. channel table).
|
|
91
|
+
*/
|
|
92
|
+
export function formatMemoryMapFieldValue(value, field) {
|
|
93
|
+
if (value === undefined || value === null) {
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
if (field.value?.kind === 'enum' && typeof value === 'string') {
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
if (field.value?.kind === 'boolean' || typeof value === 'boolean') {
|
|
100
|
+
if (field.fieldId === 'wide') {
|
|
101
|
+
return value ? 'Wide' : 'Narrow';
|
|
102
|
+
}
|
|
103
|
+
if (field.fieldId === 'scan') {
|
|
104
|
+
return value ? 'On' : 'Off';
|
|
105
|
+
}
|
|
106
|
+
return value ? 'On' : 'Off';
|
|
107
|
+
}
|
|
108
|
+
if (field.fieldId === 'lowpower' && typeof value === 'number') {
|
|
109
|
+
return value === 0 ? 'High' : 'Low';
|
|
110
|
+
}
|
|
111
|
+
if (field.fieldId === 'scode' && typeof value === 'number') {
|
|
112
|
+
return String(value + 1);
|
|
113
|
+
}
|
|
114
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
115
|
+
return String(value);
|
|
116
|
+
}
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
40
119
|
/**
|
|
41
120
|
* Group UI fields by their ui.group key, preserving first-seen group order.
|
|
42
121
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-map-ui.js","sourceRoot":"","sources":["../../src/memory/memory-map-ui.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"memory-map-ui.js","sourceRoot":"","sources":["../../src/memory/memory-map-ui.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n RadioMemoryMap,\n RadioMemoryMapFieldUi,\n RadioMemoryMapValueKind,\n RadioSettingValue,\n} from '@springfield/ham-radio-api';\n\n/**\n * Flattened field descriptor for schema-driven settings UI.\n */\nexport interface RadioMemoryMapUiField {\n /** Dot path into decoded settings, e.g. settings.squelch or pttid.0.code */\n path: string;\n structId: string;\n fieldId: string;\n arrayIndex?: number;\n ui: RadioMemoryMapFieldUi;\n value?: RadioMemoryMapValueKind;\n}\n\nfunction channelBoundFieldIds(memoryMap: RadioMemoryMap): Set<string> {\n const bindings = memoryMap.channelBindings;\n const ids = new Set<string>();\n\n if (!bindings) {\n return ids;\n }\n\n for (const key of [\n bindings.receiveFrequency,\n bindings.transmitFrequency,\n bindings.receiveTone,\n bindings.transmitTone,\n bindings.nameField,\n ]) {\n if (key) {\n ids.add(key);\n }\n }\n\n return ids;\n}\n\n/**\n * Collect writable UI fields from a memory map, in declaration order.\n * Channel-bound structs are skipped (use {@link collectChannelMemoryMapUiFields}).\n */\nexport function collectMemoryMapUiFields(memoryMap: RadioMemoryMap): RadioMemoryMapUiField[] {\n const fields: RadioMemoryMapUiField[] = [];\n const channelStructIds = new Set<string>();\n\n if (memoryMap.channelBindings) {\n channelStructIds.add(memoryMap.channelBindings.records);\n\n if (memoryMap.channelBindings.names) {\n channelStructIds.add(memoryMap.channelBindings.names);\n }\n }\n\n for (const struct of memoryMap.structs) {\n if (channelStructIds.has(struct.id)) {\n continue;\n }\n\n const count = struct.count ?? 1;\n\n for (let index = 0; index < count; index += 1) {\n for (const field of struct.fields) {\n if (field.reserved || !field.ui) {\n continue;\n }\n\n const path =\n count > 1 ? `${struct.id}.${index}.${field.id}` : `${struct.id}.${field.id}`;\n\n const label =\n count > 1 && field.ui.label.indexOf('%') === -1\n ? `${field.ui.label} ${index + 1}`\n : field.ui.label;\n\n fields.push({\n path,\n structId: struct.id,\n fieldId: field.id,\n arrayIndex: count > 1 ? index : undefined,\n ui: { ...field.ui, label },\n value: field.value,\n });\n }\n }\n }\n\n return fields;\n}\n\n/**\n * Collect per-channel UI fields from the `channelBindings.records` struct.\n * Skips reserved fields, fields without `ui`, and fields bound onto RadioChannel.\n */\nexport function collectChannelMemoryMapUiFields(memoryMap: RadioMemoryMap): RadioMemoryMapUiField[] {\n const bindings = memoryMap.channelBindings;\n\n if (!bindings) {\n return [];\n }\n\n const struct = memoryMap.structs.find((entry) => entry.id === bindings.records);\n\n if (!struct) {\n return [];\n }\n\n const boundIds = channelBoundFieldIds(memoryMap);\n const fields: RadioMemoryMapUiField[] = [];\n\n for (const field of struct.fields) {\n if (field.reserved || !field.ui || boundIds.has(field.id)) {\n continue;\n }\n\n fields.push({\n path: field.id,\n structId: struct.id,\n fieldId: field.id,\n ui: field.ui,\n value: field.value,\n });\n }\n\n return fields;\n}\n\n/**\n * Format a decoded memory-map field value for read-only display (e.g. channel table).\n */\nexport function formatMemoryMapFieldValue(\n value: RadioSettingValue | undefined,\n field: Pick<RadioMemoryMapUiField, 'value' | 'fieldId'>,\n): string {\n if (value === undefined || value === null) {\n return '';\n }\n\n if (field.value?.kind === 'enum' && typeof value === 'string') {\n return value;\n }\n\n if (field.value?.kind === 'boolean' || typeof value === 'boolean') {\n if (field.fieldId === 'wide') {\n return value ? 'Wide' : 'Narrow';\n }\n\n if (field.fieldId === 'scan') {\n return value ? 'On' : 'Off';\n }\n\n return value ? 'On' : 'Off';\n }\n\n if (field.fieldId === 'lowpower' && typeof value === 'number') {\n return value === 0 ? 'High' : 'Low';\n }\n\n if (field.fieldId === 'scode' && typeof value === 'number') {\n return String(value + 1);\n }\n\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n\n return '';\n}\n\n/**\n * Group UI fields by their ui.group key, preserving first-seen group order.\n */\nexport function groupMemoryMapUiFields(fields: RadioMemoryMapUiField[]): Map<string, RadioMemoryMapUiField[]> {\n const groups = new Map<string, RadioMemoryMapUiField[]>();\n\n for (const field of fields) {\n const existing = groups.get(field.ui.group);\n\n if (existing) {\n existing.push(field);\n } else {\n groups.set(field.ui.group, [field]);\n }\n }\n\n return groups;\n}\n"]}
|
|
@@ -375,6 +375,40 @@
|
|
|
375
375
|
"expect": {
|
|
376
376
|
"$ref": "#/definitions/expect"
|
|
377
377
|
},
|
|
378
|
+
"chunkSize": {
|
|
379
|
+
"type": "integer",
|
|
380
|
+
"description": "Override memoryConfig.chunkSize for this write (UV-5R clone writes 16-byte blocks)",
|
|
381
|
+
"minimum": 1
|
|
382
|
+
},
|
|
383
|
+
"delay": {
|
|
384
|
+
"type": "integer",
|
|
385
|
+
"description": "Milliseconds to wait after each accepted block (UV-5R clone waits 50ms)",
|
|
386
|
+
"minimum": 0
|
|
387
|
+
},
|
|
388
|
+
"skip": {
|
|
389
|
+
"type": "array",
|
|
390
|
+
"description": "Inclusive radio-address ranges that must not be uploaded",
|
|
391
|
+
"items": {
|
|
392
|
+
"type": "object",
|
|
393
|
+
"required": [
|
|
394
|
+
"startAddress",
|
|
395
|
+
"endAddress"
|
|
396
|
+
],
|
|
397
|
+
"additionalProperties": false,
|
|
398
|
+
"properties": {
|
|
399
|
+
"startAddress": {
|
|
400
|
+
"type": "integer",
|
|
401
|
+
"description": "Inclusive start address of a skipped range",
|
|
402
|
+
"minimum": 0
|
|
403
|
+
},
|
|
404
|
+
"endAddress": {
|
|
405
|
+
"type": "integer",
|
|
406
|
+
"description": "Inclusive end address of a skipped range",
|
|
407
|
+
"minimum": 0
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
},
|
|
378
412
|
"timeout": {
|
|
379
413
|
"type": "integer",
|
|
380
414
|
"description": "Timeout in milliseconds for each chunk exchange",
|
package/package.json
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
RadioMemoryMap,
|
|
3
|
+
RadioMemoryMapFieldUi,
|
|
4
|
+
RadioMemoryMapValueKind,
|
|
5
|
+
RadioSettingValue,
|
|
6
|
+
} from '@springfield/ham-radio-api';
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
9
|
* Flattened field descriptor for schema-driven settings UI.
|
|
@@ -13,8 +18,32 @@ export interface RadioMemoryMapUiField {
|
|
|
13
18
|
value?: RadioMemoryMapValueKind;
|
|
14
19
|
}
|
|
15
20
|
|
|
21
|
+
function channelBoundFieldIds(memoryMap: RadioMemoryMap): Set<string> {
|
|
22
|
+
const bindings = memoryMap.channelBindings;
|
|
23
|
+
const ids = new Set<string>();
|
|
24
|
+
|
|
25
|
+
if (!bindings) {
|
|
26
|
+
return ids;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const key of [
|
|
30
|
+
bindings.receiveFrequency,
|
|
31
|
+
bindings.transmitFrequency,
|
|
32
|
+
bindings.receiveTone,
|
|
33
|
+
bindings.transmitTone,
|
|
34
|
+
bindings.nameField,
|
|
35
|
+
]) {
|
|
36
|
+
if (key) {
|
|
37
|
+
ids.add(key);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return ids;
|
|
42
|
+
}
|
|
43
|
+
|
|
16
44
|
/**
|
|
17
45
|
* Collect writable UI fields from a memory map, in declaration order.
|
|
46
|
+
* Channel-bound structs are skipped (use {@link collectChannelMemoryMapUiFields}).
|
|
18
47
|
*/
|
|
19
48
|
export function collectMemoryMapUiFields(memoryMap: RadioMemoryMap): RadioMemoryMapUiField[] {
|
|
20
49
|
const fields: RadioMemoryMapUiField[] = [];
|
|
@@ -64,6 +93,85 @@ export function collectMemoryMapUiFields(memoryMap: RadioMemoryMap): RadioMemory
|
|
|
64
93
|
return fields;
|
|
65
94
|
}
|
|
66
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Collect per-channel UI fields from the `channelBindings.records` struct.
|
|
98
|
+
* Skips reserved fields, fields without `ui`, and fields bound onto RadioChannel.
|
|
99
|
+
*/
|
|
100
|
+
export function collectChannelMemoryMapUiFields(memoryMap: RadioMemoryMap): RadioMemoryMapUiField[] {
|
|
101
|
+
const bindings = memoryMap.channelBindings;
|
|
102
|
+
|
|
103
|
+
if (!bindings) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const struct = memoryMap.structs.find((entry) => entry.id === bindings.records);
|
|
108
|
+
|
|
109
|
+
if (!struct) {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const boundIds = channelBoundFieldIds(memoryMap);
|
|
114
|
+
const fields: RadioMemoryMapUiField[] = [];
|
|
115
|
+
|
|
116
|
+
for (const field of struct.fields) {
|
|
117
|
+
if (field.reserved || !field.ui || boundIds.has(field.id)) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fields.push({
|
|
122
|
+
path: field.id,
|
|
123
|
+
structId: struct.id,
|
|
124
|
+
fieldId: field.id,
|
|
125
|
+
ui: field.ui,
|
|
126
|
+
value: field.value,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return fields;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Format a decoded memory-map field value for read-only display (e.g. channel table).
|
|
135
|
+
*/
|
|
136
|
+
export function formatMemoryMapFieldValue(
|
|
137
|
+
value: RadioSettingValue | undefined,
|
|
138
|
+
field: Pick<RadioMemoryMapUiField, 'value' | 'fieldId'>,
|
|
139
|
+
): string {
|
|
140
|
+
if (value === undefined || value === null) {
|
|
141
|
+
return '';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (field.value?.kind === 'enum' && typeof value === 'string') {
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (field.value?.kind === 'boolean' || typeof value === 'boolean') {
|
|
149
|
+
if (field.fieldId === 'wide') {
|
|
150
|
+
return value ? 'Wide' : 'Narrow';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (field.fieldId === 'scan') {
|
|
154
|
+
return value ? 'On' : 'Off';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return value ? 'On' : 'Off';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (field.fieldId === 'lowpower' && typeof value === 'number') {
|
|
161
|
+
return value === 0 ? 'High' : 'Low';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (field.fieldId === 'scode' && typeof value === 'number') {
|
|
165
|
+
return String(value + 1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
169
|
+
return String(value);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return '';
|
|
173
|
+
}
|
|
174
|
+
|
|
67
175
|
/**
|
|
68
176
|
* Group UI fields by their ui.group key, preserving first-seen group order.
|
|
69
177
|
*/
|
|
@@ -375,6 +375,40 @@
|
|
|
375
375
|
"expect": {
|
|
376
376
|
"$ref": "#/definitions/expect"
|
|
377
377
|
},
|
|
378
|
+
"chunkSize": {
|
|
379
|
+
"type": "integer",
|
|
380
|
+
"description": "Override memoryConfig.chunkSize for this write (UV-5R clone writes 16-byte blocks)",
|
|
381
|
+
"minimum": 1
|
|
382
|
+
},
|
|
383
|
+
"delay": {
|
|
384
|
+
"type": "integer",
|
|
385
|
+
"description": "Milliseconds to wait after each accepted block (UV-5R clone waits 50ms)",
|
|
386
|
+
"minimum": 0
|
|
387
|
+
},
|
|
388
|
+
"skip": {
|
|
389
|
+
"type": "array",
|
|
390
|
+
"description": "Inclusive radio-address ranges that must not be uploaded",
|
|
391
|
+
"items": {
|
|
392
|
+
"type": "object",
|
|
393
|
+
"required": [
|
|
394
|
+
"startAddress",
|
|
395
|
+
"endAddress"
|
|
396
|
+
],
|
|
397
|
+
"additionalProperties": false,
|
|
398
|
+
"properties": {
|
|
399
|
+
"startAddress": {
|
|
400
|
+
"type": "integer",
|
|
401
|
+
"description": "Inclusive start address of a skipped range",
|
|
402
|
+
"minimum": 0
|
|
403
|
+
},
|
|
404
|
+
"endAddress": {
|
|
405
|
+
"type": "integer",
|
|
406
|
+
"description": "Inclusive end address of a skipped range",
|
|
407
|
+
"minimum": 0
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
},
|
|
378
412
|
"timeout": {
|
|
379
413
|
"type": "integer",
|
|
380
414
|
"description": "Timeout in milliseconds for each chunk exchange",
|