@tx5dr/contracts 1.3.0 → 1.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/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/schema/__tests__/radio-power-support.test.d.ts +2 -0
- package/dist/schema/__tests__/radio-power-support.test.d.ts.map +1 -0
- package/dist/schema/__tests__/radio-power-support.test.js +19 -0
- package/dist/schema/__tests__/radio-power-support.test.js.map +1 -0
- package/dist/schema/__tests__/realtime-settings.schema.test.d.ts +2 -0
- package/dist/schema/__tests__/realtime-settings.schema.test.d.ts.map +1 -0
- package/dist/schema/__tests__/realtime-settings.schema.test.js +34 -0
- package/dist/schema/__tests__/realtime-settings.schema.test.js.map +1 -0
- package/dist/schema/cpu-profile.schema.d.ts +95 -0
- package/dist/schema/cpu-profile.schema.d.ts.map +1 -0
- package/dist/schema/cpu-profile.schema.js +35 -0
- package/dist/schema/cpu-profile.schema.js.map +1 -0
- package/dist/schema/desktop-https.schema.d.ts +9 -0
- package/dist/schema/desktop-https.schema.d.ts.map +1 -1
- package/dist/schema/desktop-https.schema.js +3 -0
- package/dist/schema/desktop-https.schema.js.map +1 -1
- package/dist/schema/openwebrx.schema.d.ts +1 -1
- package/dist/schema/openwebrx.schema.d.ts.map +1 -1
- package/dist/schema/openwebrx.schema.js +1 -1
- package/dist/schema/openwebrx.schema.js.map +1 -1
- package/dist/schema/operator.schema.d.ts +38 -71
- package/dist/schema/operator.schema.d.ts.map +1 -1
- package/dist/schema/operator.schema.js +0 -5
- package/dist/schema/operator.schema.js.map +1 -1
- package/dist/schema/plugin.schema.d.ts +670 -136
- package/dist/schema/plugin.schema.d.ts.map +1 -1
- package/dist/schema/plugin.schema.js +37 -2
- package/dist/schema/plugin.schema.js.map +1 -1
- package/dist/schema/process-monitor.schema.d.ts +100 -0
- package/dist/schema/process-monitor.schema.d.ts.map +1 -1
- package/dist/schema/process-monitor.schema.js +8 -0
- package/dist/schema/process-monitor.schema.js.map +1 -1
- package/dist/schema/radio-capability.schema.d.ts +42 -2
- package/dist/schema/radio-capability.schema.d.ts.map +1 -1
- package/dist/schema/radio-capability.schema.js +18 -1
- package/dist/schema/radio-capability.schema.js.map +1 -1
- package/dist/schema/radio-power-support.d.ts +8 -0
- package/dist/schema/radio-power-support.d.ts.map +1 -1
- package/dist/schema/radio-power-support.js +38 -26
- package/dist/schema/radio-power-support.js.map +1 -1
- package/dist/schema/radio-power.schema.d.ts +10 -6
- package/dist/schema/radio-power.schema.d.ts.map +1 -1
- package/dist/schema/radio-power.schema.js +8 -6
- package/dist/schema/radio-power.schema.js.map +1 -1
- package/dist/schema/realtime.schema.d.ts +507 -417
- package/dist/schema/realtime.schema.d.ts.map +1 -1
- package/dist/schema/realtime.schema.js +85 -51
- package/dist/schema/realtime.schema.js.map +1 -1
- package/dist/schema/server-message-keys.d.ts +2 -2
- package/dist/schema/server-message-keys.d.ts.map +1 -1
- package/dist/schema/server-message-keys.js +2 -2
- package/dist/schema/server-message-keys.js.map +1 -1
- package/dist/schema/slot-info.schema.d.ts +6 -0
- package/dist/schema/slot-info.schema.d.ts.map +1 -1
- package/dist/schema/slot-info.schema.js +3 -1
- package/dist/schema/slot-info.schema.js.map +1 -1
- package/dist/schema/voice-keyer.schema.d.ts +164 -0
- package/dist/schema/voice-keyer.schema.d.ts.map +1 -0
- package/dist/schema/voice-keyer.schema.js +43 -0
- package/dist/schema/voice-keyer.schema.js.map +1 -0
- package/dist/schema/websocket.schema.d.ts +1565 -823
- package/dist/schema/websocket.schema.d.ts.map +1 -1
- package/dist/schema/websocket.schema.js +19 -6
- package/dist/schema/websocket.schema.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +4 -0
- package/src/schema/__tests__/radio-power-support.test.ts +26 -0
- package/src/schema/__tests__/realtime-settings.schema.test.ts +40 -0
- package/src/schema/cpu-profile.schema.ts +47 -0
- package/src/schema/desktop-https.schema.ts +3 -1
- package/src/schema/openwebrx.schema.ts +1 -1
- package/src/schema/operator.schema.ts +0 -5
- package/src/schema/plugin.schema.ts +43 -2
- package/src/schema/process-monitor.schema.ts +10 -0
- package/src/schema/radio-capability.schema.ts +25 -1
- package/src/schema/radio-power-support.ts +49 -26
- package/src/schema/radio-power.schema.ts +8 -6
- package/src/schema/realtime.schema.ts +96 -65
- package/src/schema/server-message-keys.ts +2 -2
- package/src/schema/slot-info.schema.ts +3 -1
- package/src/schema/voice-keyer.schema.ts +55 -0
- package/src/schema/websocket.schema.ts +31 -6
|
@@ -61,7 +61,7 @@ export type PluginPanelComponent = z.infer<typeof PluginPanelComponentSchema>;
|
|
|
61
61
|
* These values control both validation expectations and default frontend
|
|
62
62
|
* rendering in plugin settings UIs.
|
|
63
63
|
*/
|
|
64
|
-
export const PluginSettingTypeSchema = z.enum(['boolean', 'number', 'string', 'string[]', 'info']);
|
|
64
|
+
export const PluginSettingTypeSchema = z.enum(['boolean', 'number', 'string', 'string[]', 'object[]', 'info']);
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
67
|
* Supported generated-form field types for plugin settings.
|
|
@@ -81,6 +81,16 @@ export const PluginSettingOptionSchema = z.object({
|
|
|
81
81
|
*/
|
|
82
82
|
export type PluginSettingOption = z.infer<typeof PluginSettingOptionSchema>;
|
|
83
83
|
|
|
84
|
+
export const PluginObjectArrayFieldSchema = z.object({
|
|
85
|
+
key: z.string(),
|
|
86
|
+
type: z.enum(['string', 'number', 'boolean']).optional().default('string'),
|
|
87
|
+
label: z.string(),
|
|
88
|
+
description: z.string().optional(),
|
|
89
|
+
placeholder: z.string().optional(),
|
|
90
|
+
required: z.boolean().optional(),
|
|
91
|
+
});
|
|
92
|
+
export type PluginObjectArrayField = z.infer<typeof PluginObjectArrayFieldSchema>;
|
|
93
|
+
|
|
84
94
|
/**
|
|
85
95
|
* Persistence and UI scope for a plugin setting.
|
|
86
96
|
*
|
|
@@ -110,6 +120,10 @@ export const PluginSettingDescriptorSchema = z.object({
|
|
|
110
120
|
min: z.number().optional(),
|
|
111
121
|
max: z.number().optional(),
|
|
112
122
|
options: z.array(PluginSettingOptionSchema).optional(),
|
|
123
|
+
/** Field schema used by generated editors for `object[]` settings. */
|
|
124
|
+
itemFields: z.array(PluginObjectArrayFieldSchema).optional(),
|
|
125
|
+
/** Internal settings are persisted/injected but hidden from generated UIs. */
|
|
126
|
+
hidden: z.boolean().optional(),
|
|
113
127
|
/** 设置作用域:global(所有操作员共享)或 operator(每操作员独立),默认 global */
|
|
114
128
|
scope: PluginSettingScopeSchema.optional().default('global'),
|
|
115
129
|
});
|
|
@@ -212,7 +226,8 @@ export type PluginPanelWidth = z.infer<typeof PluginPanelWidthSchema>;
|
|
|
212
226
|
*
|
|
213
227
|
* Panels are passive containers rendered by the host. A plugin sends data into
|
|
214
228
|
* them through `ctx.ui.send(panelId, data)`. When `component` is `'iframe'`,
|
|
215
|
-
* the panel renders a custom UI page inside a sandboxed iframe instead.
|
|
229
|
+
* the panel renders a custom UI page inside a sandboxed iframe instead. Static
|
|
230
|
+
* manifest panels and runtime UI contributions use this same descriptor.
|
|
216
231
|
*/
|
|
217
232
|
export const PluginPanelDescriptorSchema = z.object({
|
|
218
233
|
id: z.string(),
|
|
@@ -220,6 +235,8 @@ export const PluginPanelDescriptorSchema = z.object({
|
|
|
220
235
|
component: PluginPanelComponentSchema,
|
|
221
236
|
/** Required when `component` is `'iframe'`. References a page id from `ui.pages`. */
|
|
222
237
|
pageId: z.string().optional(),
|
|
238
|
+
/** Optional string params forwarded to iframe panels as URL/init params. */
|
|
239
|
+
params: z.record(z.string(), z.string()).optional(),
|
|
223
240
|
/** Where the panel renders. Defaults to `'operator'` (operator card live-panel area). */
|
|
224
241
|
slot: PluginPanelSlotSchema.optional(),
|
|
225
242
|
/** Preferred width hint. Defaults to `'half'`. */
|
|
@@ -231,6 +248,29 @@ export const PluginPanelDescriptorSchema = z.object({
|
|
|
231
248
|
*/
|
|
232
249
|
export type PluginPanelDescriptor = z.infer<typeof PluginPanelDescriptorSchema>;
|
|
233
250
|
|
|
251
|
+
export const PluginUIPanelContributionTargetSchema = z.discriminatedUnion('kind', [
|
|
252
|
+
z.object({ kind: z.literal('global') }),
|
|
253
|
+
z.object({ kind: z.literal('operator'), operatorId: z.string() }),
|
|
254
|
+
]);
|
|
255
|
+
export type PluginUIPanelContributionTarget = z.infer<typeof PluginUIPanelContributionTargetSchema>;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* A normalized group of plugin UI panels.
|
|
259
|
+
*
|
|
260
|
+
* Static `PluginDefinition.panels` are emitted by the host as the reserved
|
|
261
|
+
* `manifest` group. Runtime groups are replaced by
|
|
262
|
+
* `ctx.ui.setPanelContributions(groupId, panels)` and cleared by publishing an
|
|
263
|
+
* empty panel list for the same group.
|
|
264
|
+
*/
|
|
265
|
+
export const PluginUIPanelContributionGroupSchema = z.object({
|
|
266
|
+
pluginName: z.string(),
|
|
267
|
+
groupId: z.string(),
|
|
268
|
+
source: z.enum(['manifest', 'runtime']),
|
|
269
|
+
instanceTarget: PluginUIPanelContributionTargetSchema.optional(),
|
|
270
|
+
panels: z.array(PluginPanelDescriptorSchema),
|
|
271
|
+
});
|
|
272
|
+
export type PluginUIPanelContributionGroup = z.infer<typeof PluginUIPanelContributionGroupSchema>;
|
|
273
|
+
|
|
234
274
|
// ===== 自定义 UI 页面 =====
|
|
235
275
|
|
|
236
276
|
/**
|
|
@@ -422,6 +462,7 @@ export const PluginSystemSnapshotSchema = z.object({
|
|
|
422
462
|
generation: z.number().int().nonnegative(),
|
|
423
463
|
plugins: z.array(PluginStatusSchema),
|
|
424
464
|
panelMeta: z.array(PluginPanelMetaPayloadSchema).optional().default([]),
|
|
465
|
+
panelContributions: z.array(PluginUIPanelContributionGroupSchema).optional().default([]),
|
|
425
466
|
lastError: z.string().optional(),
|
|
426
467
|
});
|
|
427
468
|
export type PluginSystemSnapshot = z.infer<typeof PluginSystemSnapshotSchema>;
|
|
@@ -12,6 +12,14 @@ export const ProcessCpuSchema = z.object({
|
|
|
12
12
|
user: z.number(),
|
|
13
13
|
system: z.number(),
|
|
14
14
|
total: z.number(),
|
|
15
|
+
capacity: z.number().positive().optional(),
|
|
16
|
+
normalizedTotal: z.number().optional(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const HostCpuSchema = z.object({
|
|
20
|
+
logicalCores: z.number().int().positive(),
|
|
21
|
+
availableParallelism: z.number().int().positive(),
|
|
22
|
+
totalUsage: z.number().optional(),
|
|
15
23
|
});
|
|
16
24
|
|
|
17
25
|
export const EventLoopDelaySchema = z.object({
|
|
@@ -25,6 +33,7 @@ export const ProcessSnapshotSchema = z.object({
|
|
|
25
33
|
uptimeSeconds: z.number(),
|
|
26
34
|
memory: ProcessMemorySchema,
|
|
27
35
|
cpu: ProcessCpuSchema,
|
|
36
|
+
hostCpu: HostCpuSchema.optional(),
|
|
28
37
|
eventLoop: EventLoopDelaySchema,
|
|
29
38
|
});
|
|
30
39
|
|
|
@@ -36,6 +45,7 @@ export const ProcessSnapshotHistorySchema = z.object({
|
|
|
36
45
|
|
|
37
46
|
export type ProcessMemory = z.infer<typeof ProcessMemorySchema>;
|
|
38
47
|
export type ProcessCpu = z.infer<typeof ProcessCpuSchema>;
|
|
48
|
+
export type HostCpu = z.infer<typeof HostCpuSchema>;
|
|
39
49
|
export type EventLoopDelay = z.infer<typeof EventLoopDelaySchema>;
|
|
40
50
|
export type ProcessSnapshot = z.infer<typeof ProcessSnapshotSchema>;
|
|
41
51
|
export type ProcessSnapshotHistory = z.infer<typeof ProcessSnapshotHistorySchema>;
|
|
@@ -42,6 +42,18 @@ export type CapabilityOptionValue = z.infer<typeof CapabilityOptionValueSchema>;
|
|
|
42
42
|
export const CapabilityValueSchema = z.union([z.boolean(), z.number(), z.string()]);
|
|
43
43
|
export type CapabilityValue = z.infer<typeof CapabilityValueSchema>;
|
|
44
44
|
|
|
45
|
+
export const CapabilityAvailabilitySchema = z.enum(['available', 'unavailable', 'unknown']);
|
|
46
|
+
export type CapabilityAvailability = z.infer<typeof CapabilityAvailabilitySchema>;
|
|
47
|
+
|
|
48
|
+
export const CapabilityAvailabilityReasonSchema = z.enum([
|
|
49
|
+
'runtime_error',
|
|
50
|
+
'busy',
|
|
51
|
+
'unsupported_by_current_mode',
|
|
52
|
+
'radio_reported_unavailable',
|
|
53
|
+
'unknown',
|
|
54
|
+
]);
|
|
55
|
+
export type CapabilityAvailabilityReason = z.infer<typeof CapabilityAvailabilityReasonSchema>;
|
|
56
|
+
|
|
45
57
|
/**
|
|
46
58
|
* 枚举项定义。
|
|
47
59
|
*/
|
|
@@ -161,9 +173,21 @@ export const CapabilityStateSchema = z.object({
|
|
|
161
173
|
/** 能力 ID,与 CapabilityDescriptor.id 对应 */
|
|
162
174
|
id: z.string(),
|
|
163
175
|
|
|
164
|
-
/**
|
|
176
|
+
/** 当前连接的电台型号/后端是否声明支持此能力 */
|
|
165
177
|
supported: z.boolean(),
|
|
166
178
|
|
|
179
|
+
/**
|
|
180
|
+
* 当前运行时是否可用。
|
|
181
|
+
* 兼容旧客户端:缺省时应按 supported=true 视为 available,supported=false 视为 unknown。
|
|
182
|
+
*/
|
|
183
|
+
availability: CapabilityAvailabilitySchema.optional(),
|
|
184
|
+
|
|
185
|
+
/** 当前不可用的机器可读原因 */
|
|
186
|
+
availabilityReason: CapabilityAvailabilityReasonSchema.optional(),
|
|
187
|
+
|
|
188
|
+
/** 最近一次运行时读写错误摘要 */
|
|
189
|
+
lastError: z.string().optional(),
|
|
190
|
+
|
|
167
191
|
/**
|
|
168
192
|
* 当前值
|
|
169
193
|
* - boolean 类能力:true/false
|
|
@@ -17,32 +17,40 @@ import type { HamlibConfig } from './radio.schema.js';
|
|
|
17
17
|
export interface PowerCapableRigEntry {
|
|
18
18
|
mfg: string | RegExp;
|
|
19
19
|
model: string | RegExp;
|
|
20
|
+
/**
|
|
21
|
+
* Physical powerstat targets that are safe to expose while CAT is connected.
|
|
22
|
+
* `on` is represented by canPowerOn and is not part of this connected-state list.
|
|
23
|
+
*/
|
|
24
|
+
supportedStates?: ReadonlyArray<'operate' | 'standby' | 'off'>;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
export const POWER_CAPABLE_RIGS: ReadonlyArray<PowerCapableRigEntry> = [
|
|
23
28
|
// Icom modern transceivers (CI-V 0x18 command)
|
|
24
|
-
{ mfg: /^icom$/i, model: /^IC-?705$/i },
|
|
25
|
-
{ mfg: /^icom$/i, model: /^IC-?7300$/i },
|
|
26
|
-
{ mfg: /^icom$/i, model: /^IC-?7610$/i },
|
|
27
|
-
{ mfg: /^icom$/i, model: /^IC-?7100$/i },
|
|
28
|
-
{ mfg: /^icom$/i, model: /^IC-?7851$/i },
|
|
29
|
-
{ mfg: /^icom$/i, model: /^IC-?9700$/i },
|
|
30
|
-
{ mfg: /^icom$/i, model: /^IC-?R8600$/i },
|
|
29
|
+
{ mfg: /^icom$/i, model: /^IC-?705$/i, supportedStates: ['off', 'standby', 'operate'] },
|
|
30
|
+
{ mfg: /^icom$/i, model: /^IC-?7300$/i, supportedStates: ['off', 'standby', 'operate'] },
|
|
31
|
+
{ mfg: /^icom$/i, model: /^IC-?7610$/i, supportedStates: ['off', 'standby', 'operate'] },
|
|
32
|
+
{ mfg: /^icom$/i, model: /^IC-?7100$/i, supportedStates: ['off', 'standby', 'operate'] },
|
|
33
|
+
{ mfg: /^icom$/i, model: /^IC-?7851$/i, supportedStates: ['off', 'standby', 'operate'] },
|
|
34
|
+
{ mfg: /^icom$/i, model: /^IC-?9700$/i, supportedStates: ['off', 'standby', 'operate'] },
|
|
35
|
+
{ mfg: /^icom$/i, model: /^IC-?R8600$/i, supportedStates: ['off', 'standby', 'operate'] },
|
|
31
36
|
|
|
32
37
|
// Kenwood
|
|
33
|
-
{ mfg: /^kenwood$/i, model: /^TS-590SG?$/i },
|
|
34
|
-
{ mfg: /^kenwood$/i, model: /^TS-890$/i },
|
|
35
|
-
{ mfg: /^kenwood$/i, model: /^TS-990$/i },
|
|
38
|
+
{ mfg: /^kenwood$/i, model: /^TS-590SG?$/i, supportedStates: ['off', 'standby'] },
|
|
39
|
+
{ mfg: /^kenwood$/i, model: /^TS-890$/i, supportedStates: ['off', 'standby'] },
|
|
40
|
+
{ mfg: /^kenwood$/i, model: /^TS-990$/i, supportedStates: ['off', 'standby'] },
|
|
36
41
|
|
|
37
|
-
// Yaesu
|
|
38
|
-
{ mfg: /^yaesu$/i, model: /^FT-991A?$/i },
|
|
39
|
-
{ mfg: /^yaesu$/i, model: /^FTDX-?10$/i },
|
|
40
|
-
{ mfg: /^yaesu$/i, model: /^FTDX-?101(MP|D)?$/i },
|
|
41
|
-
{ mfg: /^yaesu$/i, model: /^FT-?710$/i },
|
|
42
|
+
// Yaesu: FT-710 rejects powerstat(operate), so do not expose it by default.
|
|
43
|
+
{ mfg: /^yaesu$/i, model: /^FT-991A?$/i, supportedStates: ['off', 'standby'] },
|
|
44
|
+
{ mfg: /^yaesu$/i, model: /^FTDX-?10$/i, supportedStates: ['off', 'standby'] },
|
|
45
|
+
{ mfg: /^yaesu$/i, model: /^FTDX-?101(MP|D)?$/i, supportedStates: ['off', 'standby'] },
|
|
46
|
+
{ mfg: /^yaesu$/i, model: /^FT-?710$/i, supportedStates: ['off'] },
|
|
42
47
|
];
|
|
43
48
|
|
|
44
|
-
export function
|
|
45
|
-
|
|
49
|
+
export function findPowerCapableRigEntry(
|
|
50
|
+
mfgName: string,
|
|
51
|
+
modelName: string
|
|
52
|
+
): PowerCapableRigEntry | undefined {
|
|
53
|
+
return POWER_CAPABLE_RIGS.find((entry) => {
|
|
46
54
|
const mfgOk =
|
|
47
55
|
typeof entry.mfg === 'string'
|
|
48
56
|
? entry.mfg.toLowerCase() === mfgName.toLowerCase()
|
|
@@ -55,6 +63,10 @@ export function isRigModelPowerCapable(mfgName: string, modelName: string): bool
|
|
|
55
63
|
});
|
|
56
64
|
}
|
|
57
65
|
|
|
66
|
+
export function isRigModelPowerCapable(mfgName: string, modelName: string): boolean {
|
|
67
|
+
return findPowerCapableRigEntry(mfgName, modelName) !== undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
58
70
|
export type PowerSupportReason =
|
|
59
71
|
| 'model-unsupported'
|
|
60
72
|
| 'network-mode-no-wake'
|
|
@@ -65,6 +77,8 @@ export interface PowerSupportDecision {
|
|
|
65
77
|
canPowerOn: boolean;
|
|
66
78
|
/** Whether the UI should surface a "power off" control. */
|
|
67
79
|
canPowerOff: boolean;
|
|
80
|
+
/** Connected-state physical power targets the UI may offer. */
|
|
81
|
+
supportedStates: Array<'operate' | 'standby' | 'off'>;
|
|
68
82
|
/** Machine-readable reason when `canPowerOn` is false. */
|
|
69
83
|
reason?: PowerSupportReason;
|
|
70
84
|
}
|
|
@@ -85,23 +99,32 @@ export function decidePowerSupport(
|
|
|
85
99
|
): PowerSupportDecision {
|
|
86
100
|
switch (config.type) {
|
|
87
101
|
case 'none':
|
|
88
|
-
return { canPowerOn: false, canPowerOff: false, reason: 'none-mode' };
|
|
102
|
+
return { canPowerOn: false, canPowerOff: false, supportedStates: [], reason: 'none-mode' };
|
|
89
103
|
case 'network':
|
|
90
|
-
return {
|
|
104
|
+
return {
|
|
105
|
+
canPowerOn: false,
|
|
106
|
+
canPowerOff: true,
|
|
107
|
+
supportedStates: ['off'],
|
|
108
|
+
reason: 'network-mode-no-wake',
|
|
109
|
+
};
|
|
91
110
|
case 'icom-wlan':
|
|
92
111
|
// ICOM WLAN 的 CI-V-over-UDP 通道在电台关机后无法维持;即便已连接
|
|
93
112
|
// 发送 powerstat(off) 也不能可靠恢复。整体不暴露电源控制。
|
|
94
|
-
return { canPowerOn: false, canPowerOff: false, reason: 'model-unsupported' };
|
|
113
|
+
return { canPowerOn: false, canPowerOff: false, supportedStates: [], reason: 'model-unsupported' };
|
|
95
114
|
case 'serial': {
|
|
96
115
|
if (!rigInfo) {
|
|
97
|
-
return { canPowerOn: false, canPowerOff: false, reason: 'model-unsupported' };
|
|
116
|
+
return { canPowerOn: false, canPowerOff: false, supportedStates: [], reason: 'model-unsupported' };
|
|
98
117
|
}
|
|
99
|
-
const
|
|
100
|
-
return
|
|
101
|
-
? {
|
|
102
|
-
|
|
118
|
+
const entry = findPowerCapableRigEntry(rigInfo.mfgName, rigInfo.modelName);
|
|
119
|
+
return entry
|
|
120
|
+
? {
|
|
121
|
+
canPowerOn: true,
|
|
122
|
+
canPowerOff: true,
|
|
123
|
+
supportedStates: [...(entry.supportedStates ?? ['off'])],
|
|
124
|
+
}
|
|
125
|
+
: { canPowerOn: false, canPowerOff: false, supportedStates: [], reason: 'model-unsupported' };
|
|
103
126
|
}
|
|
104
127
|
default:
|
|
105
|
-
return { canPowerOn: false, canPowerOff: false, reason: 'model-unsupported' };
|
|
128
|
+
return { canPowerOn: false, canPowerOff: false, supportedStates: [], reason: 'model-unsupported' };
|
|
106
129
|
}
|
|
107
130
|
}
|
|
@@ -5,10 +5,10 @@ import { HamlibConfigSchema } from './radio.schema.js';
|
|
|
5
5
|
* High-level runtime state tracked by the RadioPowerController.
|
|
6
6
|
*
|
|
7
7
|
* - `off`: radio is known to be off (or was never connected).
|
|
8
|
-
* - `waking`: power-on command sent, waiting for the radio to respond.
|
|
9
|
-
* - `awake`: radio is responding;
|
|
10
|
-
* - `shutting_down`:
|
|
11
|
-
* - `entering_standby`:
|
|
8
|
+
* - `waking`: physical power-on command sent, waiting for the radio to respond.
|
|
9
|
+
* - `awake`: radio is physically responding; software engine startup is optional.
|
|
10
|
+
* - `shutting_down`: physical power-off command is being applied.
|
|
11
|
+
* - `entering_standby`: physical standby command is being applied.
|
|
12
12
|
* - `failed`: last transition failed; UI should show an error + retry.
|
|
13
13
|
*/
|
|
14
14
|
export const RadioPowerStateSchema = z.enum([
|
|
@@ -53,14 +53,15 @@ export type RadioPowerStateEvent = z.infer<typeof RadioPowerStateEventSchema>;
|
|
|
53
53
|
/**
|
|
54
54
|
* REST request body: POST /api/radio/power
|
|
55
55
|
*/
|
|
56
|
-
/** Allowed target states for a power request. */
|
|
56
|
+
/** Allowed physical radio target states for a power request. */
|
|
57
57
|
export const RadioPowerTargetSchema = z.enum(['on', 'off', 'standby', 'operate']);
|
|
58
58
|
export type RadioPowerTarget = z.infer<typeof RadioPowerTargetSchema>;
|
|
59
59
|
|
|
60
60
|
export const RadioPowerRequestSchema = z.object({
|
|
61
61
|
profileId: z.string().min(1),
|
|
62
|
+
/** Physical radio power target. This is not the TX-5DR software engine state. */
|
|
62
63
|
state: RadioPowerTargetSchema,
|
|
63
|
-
/** Automatically start the engine after successful power-on.
|
|
64
|
+
/** Automatically start the TX-5DR engine after successful physical power-on. */
|
|
64
65
|
autoEngine: z.boolean().optional().default(true),
|
|
65
66
|
});
|
|
66
67
|
export type RadioPowerRequest = z.infer<typeof RadioPowerRequestSchema>;
|
|
@@ -70,6 +71,7 @@ export type RadioPowerRequest = z.infer<typeof RadioPowerRequestSchema>;
|
|
|
70
71
|
*/
|
|
71
72
|
export const RadioPowerResponseSchema = z.object({
|
|
72
73
|
success: z.boolean(),
|
|
74
|
+
target: RadioPowerTargetSchema,
|
|
73
75
|
state: RadioPowerStateSchema,
|
|
74
76
|
});
|
|
75
77
|
export type RadioPowerResponse = z.infer<typeof RadioPowerResponseSchema>;
|
|
@@ -3,10 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
export const RealtimeScopeSchema = z.enum(['radio', 'openwebrx-preview']);
|
|
4
4
|
export type RealtimeScope = z.infer<typeof RealtimeScopeSchema>;
|
|
5
5
|
|
|
6
|
-
export const
|
|
7
|
-
export type RealtimeParticipantKind = z.infer<typeof RealtimeParticipantKindSchema>;
|
|
8
|
-
|
|
9
|
-
export const RealtimeTransportKindSchema = z.enum(['livekit', 'ws-compat']);
|
|
6
|
+
export const RealtimeTransportKindSchema = z.enum(['rtc-data-audio', 'ws-compat']);
|
|
10
7
|
export type RealtimeTransportKind = z.infer<typeof RealtimeTransportKindSchema>;
|
|
11
8
|
|
|
12
9
|
export const RealtimeSessionDirectionSchema = z.enum(['recv', 'send']);
|
|
@@ -27,10 +24,14 @@ export type RealtimeConnectivityErrorCode = z.infer<typeof RealtimeConnectivityE
|
|
|
27
24
|
|
|
28
25
|
export const RealtimeConnectivityHintsSchema = z.object({
|
|
29
26
|
signalingUrl: z.string(),
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
localUdpPort: z.number().int().min(1).max(65535),
|
|
28
|
+
publicCandidateEnabled: z.boolean(),
|
|
29
|
+
publicEndpoint: z.object({
|
|
30
|
+
host: z.string(),
|
|
31
|
+
port: z.number().int().min(1).max(65535),
|
|
32
|
+
}).nullable(),
|
|
33
|
+
iceServers: z.array(z.string()),
|
|
34
|
+
fallbackTransport: z.literal('ws-compat'),
|
|
34
35
|
});
|
|
35
36
|
export type RealtimeConnectivityHints = z.infer<typeof RealtimeConnectivityHintsSchema>;
|
|
36
37
|
|
|
@@ -45,14 +46,6 @@ export const RealtimeConnectivityIssueSchema = z.object({
|
|
|
45
46
|
});
|
|
46
47
|
export type RealtimeConnectivityIssue = z.infer<typeof RealtimeConnectivityIssueSchema>;
|
|
47
48
|
|
|
48
|
-
export const RealtimeTokenRequestSchema = z.object({
|
|
49
|
-
scope: RealtimeScopeSchema,
|
|
50
|
-
publish: z.boolean().optional(),
|
|
51
|
-
previewSessionId: z.string().optional(),
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
export type RealtimeTokenRequest = z.infer<typeof RealtimeTokenRequestSchema>;
|
|
55
|
-
|
|
56
49
|
export const RealtimeSessionRequestSchema = z.object({
|
|
57
50
|
scope: RealtimeScopeSchema,
|
|
58
51
|
direction: RealtimeSessionDirectionSchema,
|
|
@@ -62,30 +55,6 @@ export const RealtimeSessionRequestSchema = z.object({
|
|
|
62
55
|
|
|
63
56
|
export type RealtimeSessionRequest = z.infer<typeof RealtimeSessionRequestSchema>;
|
|
64
57
|
|
|
65
|
-
export const RealtimeParticipantMetadataSchema = z.object({
|
|
66
|
-
role: z.string(),
|
|
67
|
-
tokenId: z.string().nullable().optional(),
|
|
68
|
-
operatorIds: z.array(z.string()).optional(),
|
|
69
|
-
clientKind: z.string(),
|
|
70
|
-
participantKind: RealtimeParticipantKindSchema,
|
|
71
|
-
scope: RealtimeScopeSchema,
|
|
72
|
-
previewSessionId: z.string().optional(),
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
export type RealtimeParticipantMetadata = z.infer<typeof RealtimeParticipantMetadataSchema>;
|
|
76
|
-
|
|
77
|
-
export const RealtimeTokenResponseSchema = z.object({
|
|
78
|
-
url: z.string(),
|
|
79
|
-
roomName: z.string(),
|
|
80
|
-
token: z.string(),
|
|
81
|
-
participantIdentity: z.string(),
|
|
82
|
-
participantName: z.string(),
|
|
83
|
-
participantMetadata: RealtimeParticipantMetadataSchema,
|
|
84
|
-
connectivityHints: RealtimeConnectivityHintsSchema,
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
export type RealtimeTokenResponse = z.infer<typeof RealtimeTokenResponseSchema>;
|
|
88
|
-
|
|
89
58
|
export const RealtimeTransportOfferSchema = z.object({
|
|
90
59
|
transport: RealtimeTransportKindSchema,
|
|
91
60
|
direction: RealtimeSessionDirectionSchema,
|
|
@@ -93,7 +62,6 @@ export const RealtimeTransportOfferSchema = z.object({
|
|
|
93
62
|
token: z.string(),
|
|
94
63
|
participantIdentity: z.string().nullable().optional(),
|
|
95
64
|
participantName: z.string().nullable().optional(),
|
|
96
|
-
roomName: z.string().nullable().optional(),
|
|
97
65
|
});
|
|
98
66
|
|
|
99
67
|
export type RealtimeTransportOffer = z.infer<typeof RealtimeTransportOfferSchema>;
|
|
@@ -101,8 +69,74 @@ export type RealtimeTransportOffer = z.infer<typeof RealtimeTransportOfferSchema
|
|
|
101
69
|
export const RealtimeTransportPolicySchema = z.enum(['auto', 'force-compat']);
|
|
102
70
|
export type RealtimeTransportPolicy = z.infer<typeof RealtimeTransportPolicySchema>;
|
|
103
71
|
|
|
104
|
-
|
|
105
|
-
|
|
72
|
+
function isValidIpv4Literal(value: string): boolean {
|
|
73
|
+
const parts = value.split('.');
|
|
74
|
+
return parts.length === 4 && parts.every((part) => {
|
|
75
|
+
if (!/^\d{1,3}$/.test(part)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
const parsed = Number.parseInt(part, 10);
|
|
79
|
+
return parsed >= 0 && parsed <= 255;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isValidIpv6Literal(value: string): boolean {
|
|
84
|
+
if (!value.includes(':') || value.includes('[') || value.includes(']')) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
// WHATWG URL parsing gives us a portable IPv6 sanity check without Node-only APIs.
|
|
89
|
+
// The schema still forbids bracket notation because the setting is host-only.
|
|
90
|
+
const parsed = new URL(`http://[${value}]/`);
|
|
91
|
+
return parsed.hostname.length > 2;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isValidDnsHostname(value: string): boolean {
|
|
98
|
+
if (value.length > 253 || value.endsWith('.')) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
const labels = value.split('.');
|
|
102
|
+
return labels.every((label) => (
|
|
103
|
+
label.length >= 1
|
|
104
|
+
&& label.length <= 63
|
|
105
|
+
&& /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(label)
|
|
106
|
+
));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isValidRtcDataAudioPublicHost(value: string): boolean {
|
|
110
|
+
const host = value.trim();
|
|
111
|
+
if (!host || /\s/.test(host) || /[/?#@]/.test(host) || /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(host)) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
return isValidIpv4Literal(host) || isValidIpv6Literal(host) || isValidDnsHostname(host);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const RtcDataAudioPublicHostSchema = z.preprocess(
|
|
118
|
+
(value) => (typeof value === 'string' ? value.trim() : value),
|
|
119
|
+
z.union([
|
|
120
|
+
z.literal('').transform(() => null),
|
|
121
|
+
z.string().min(1).max(253).refine(isValidRtcDataAudioPublicHost, {
|
|
122
|
+
message: 'Public host must be an IP address or DNS hostname without scheme, port, or path',
|
|
123
|
+
}),
|
|
124
|
+
z.null(),
|
|
125
|
+
]),
|
|
126
|
+
);
|
|
127
|
+
export type RtcDataAudioPublicHost = z.infer<typeof RtcDataAudioPublicHostSchema>;
|
|
128
|
+
|
|
129
|
+
export const RtcDataAudioPublicUdpPortSchema = z.preprocess((value) => {
|
|
130
|
+
if (value === '' || value === undefined) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
if (typeof value === 'string') {
|
|
134
|
+
const parsed = Number.parseInt(value, 10);
|
|
135
|
+
return Number.isFinite(parsed) ? parsed : value;
|
|
136
|
+
}
|
|
137
|
+
return value;
|
|
138
|
+
}, z.number().int().min(1).max(65535).nullable());
|
|
139
|
+
export type RtcDataAudioPublicUdpPort = z.infer<typeof RtcDataAudioPublicUdpPortSchema>;
|
|
106
140
|
|
|
107
141
|
export const RealtimeSessionResponseSchema = z.object({
|
|
108
142
|
scope: RealtimeScopeSchema,
|
|
@@ -112,10 +146,8 @@ export const RealtimeSessionResponseSchema = z.object({
|
|
|
112
146
|
selectionReason: z.enum([
|
|
113
147
|
'client-override',
|
|
114
148
|
'server-policy',
|
|
115
|
-
'
|
|
116
|
-
'
|
|
117
|
-
'runtime-unavailable',
|
|
118
|
-
'default-livekit',
|
|
149
|
+
'default-rtc-data-audio',
|
|
150
|
+
'rtc-data-audio-unavailable',
|
|
119
151
|
]),
|
|
120
152
|
forcedCompatibilityMode: z.boolean(),
|
|
121
153
|
offers: z.array(RealtimeTransportOfferSchema).min(1),
|
|
@@ -125,34 +157,27 @@ export const RealtimeSessionResponseSchema = z.object({
|
|
|
125
157
|
export type RealtimeSessionResponse = z.infer<typeof RealtimeSessionResponseSchema>;
|
|
126
158
|
|
|
127
159
|
export const RealtimeSettingsSchema = z.object({
|
|
128
|
-
publicWsUrl: z.string().url().nullable().optional(),
|
|
129
160
|
transportPolicy: RealtimeTransportPolicySchema.optional(),
|
|
130
|
-
|
|
131
|
-
|
|
161
|
+
rtcDataAudioPublicHost: RtcDataAudioPublicHostSchema.optional(),
|
|
162
|
+
rtcDataAudioPublicUdpPort: RtcDataAudioPublicUdpPortSchema.optional(),
|
|
132
163
|
});
|
|
133
164
|
|
|
134
165
|
export type RealtimeSettings = z.infer<typeof RealtimeSettingsSchema>;
|
|
135
166
|
|
|
136
|
-
export const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
apiKeyPreview: z.string().nullable(),
|
|
144
|
-
createdAt: z.string().nullable(),
|
|
145
|
-
rotatedAt: z.string().nullable(),
|
|
167
|
+
export const RealtimeRtcDataAudioRuntimeSchema = z.object({
|
|
168
|
+
localUdpPort: z.number().int().min(1).max(65535),
|
|
169
|
+
publicCandidateEnabled: z.boolean(),
|
|
170
|
+
publicEndpoint: z.object({
|
|
171
|
+
host: z.string(),
|
|
172
|
+
port: z.number().int().min(1).max(65535),
|
|
173
|
+
}).nullable(),
|
|
146
174
|
});
|
|
147
|
-
export type
|
|
175
|
+
export type RealtimeRtcDataAudioRuntime = z.infer<typeof RealtimeRtcDataAudioRuntimeSchema>;
|
|
148
176
|
|
|
149
177
|
export const RealtimeSettingsRuntimeSchema = z.object({
|
|
150
|
-
liveKitEnabled: z.boolean(),
|
|
151
178
|
connectivityHints: RealtimeConnectivityHintsSchema,
|
|
152
179
|
radioReceiveTransport: RealtimeTransportKindSchema,
|
|
153
|
-
|
|
154
|
-
radioBridgeIssueCode: RealtimeConnectivityErrorCodeSchema.nullable(),
|
|
155
|
-
credentialStatus: RealtimeCredentialStatusSchema,
|
|
180
|
+
rtcDataAudio: RealtimeRtcDataAudioRuntimeSchema,
|
|
156
181
|
});
|
|
157
182
|
|
|
158
183
|
export type RealtimeSettingsRuntime = z.infer<typeof RealtimeSettingsRuntimeSchema>;
|
|
@@ -234,6 +259,10 @@ export const RealtimeVoiceTxServerIngressStatsSchema = z.object({
|
|
|
234
259
|
queueDepthFrames: z.number().int().nonnegative(),
|
|
235
260
|
queuedAudioMs: z.number().nonnegative(),
|
|
236
261
|
droppedFrames: z.number().int().nonnegative(),
|
|
262
|
+
staleDroppedFrames: z.number().int().nonnegative(),
|
|
263
|
+
underrunCount: z.number().int().nonnegative(),
|
|
264
|
+
plcFrames: z.number().int().nonnegative(),
|
|
265
|
+
jitterTargetMs: z.number().nonnegative(),
|
|
237
266
|
});
|
|
238
267
|
|
|
239
268
|
export type RealtimeVoiceTxServerIngressStats = z.infer<typeof RealtimeVoiceTxServerIngressStatsSchema>;
|
|
@@ -242,8 +271,10 @@ export const RealtimeVoiceTxServerOutputStatsSchema = z.object({
|
|
|
242
271
|
resampleMs: RealtimeVoiceTxMetricWindowSchema,
|
|
243
272
|
queueWaitMs: RealtimeVoiceTxMetricWindowSchema,
|
|
244
273
|
writeMs: RealtimeVoiceTxMetricWindowSchema,
|
|
274
|
+
serverPipelineMs: RealtimeVoiceTxMetricWindowSchema,
|
|
245
275
|
endToEndMs: RealtimeVoiceTxMetricWindowSchema,
|
|
246
276
|
outputBufferedMs: RealtimeVoiceTxMetricWindowSchema,
|
|
277
|
+
outputWriteIntervalMs: RealtimeVoiceTxMetricWindowSchema,
|
|
247
278
|
outputSampleRate: z.number().nullable(),
|
|
248
279
|
outputBufferSize: z.number().nullable(),
|
|
249
280
|
writeFailures: z.number().int().nonnegative(),
|
|
@@ -18,8 +18,8 @@ export enum ServerMessageKey {
|
|
|
18
18
|
/** 实时语音连接异常 */
|
|
19
19
|
REALTIME_CONNECTIVITY_WARNING = 'realtimeConnectivityWarning',
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/** 服务端实时语音服务异常 */
|
|
22
|
+
REALTIME_SERVICE_DOWN = 'realtimeServiceDown',
|
|
23
23
|
|
|
24
24
|
/** 实时语音链路无音频数据 */
|
|
25
25
|
REALTIME_NO_AUDIO = 'realtimeNoAudio',
|
|
@@ -148,7 +148,9 @@ export const SlotPackSchema = z.object({
|
|
|
148
148
|
/** 去重后的帧数 */
|
|
149
149
|
totalFramesAfterDedup: z.number().default(0),
|
|
150
150
|
/** 最后更新时间戳 */
|
|
151
|
-
lastUpdated: z.number().default(() => Date.now())
|
|
151
|
+
lastUpdated: z.number().default(() => Date.now()),
|
|
152
|
+
/** SlotPack 单调更新序号,用于客户端丢弃乱序旧包 */
|
|
153
|
+
updateSeq: z.number().int().nonnegative().optional()
|
|
152
154
|
}).default({}),
|
|
153
155
|
/** 解码历史(用于调试) */
|
|
154
156
|
decodeHistory: z.array(z.object({
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const VoiceKeyerSlotSchema = z.object({
|
|
4
|
+
id: z.string(),
|
|
5
|
+
index: z.number().int().min(1),
|
|
6
|
+
label: z.string(),
|
|
7
|
+
hasAudio: z.boolean(),
|
|
8
|
+
durationMs: z.number().int().nonnegative(),
|
|
9
|
+
updatedAt: z.number().nullable(),
|
|
10
|
+
repeatEnabled: z.boolean(),
|
|
11
|
+
repeatIntervalSec: z.number().int().min(1).max(300),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const VoiceKeyerPanelSchema = z.object({
|
|
15
|
+
callsign: z.string(),
|
|
16
|
+
slotCount: z.number().int().min(3).max(12),
|
|
17
|
+
maxSlotCount: z.number().int().min(12),
|
|
18
|
+
slots: z.array(VoiceKeyerSlotSchema),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const VoiceKeyerStatusSchema = z.object({
|
|
22
|
+
active: z.boolean(),
|
|
23
|
+
callsign: z.string().nullable(),
|
|
24
|
+
slotId: z.string().nullable(),
|
|
25
|
+
mode: z.enum(['idle', 'playing', 'repeat-waiting', 'stopping', 'error']),
|
|
26
|
+
repeating: z.boolean(),
|
|
27
|
+
startedBy: z.string().nullable(),
|
|
28
|
+
startedByLabel: z.string().nullable(),
|
|
29
|
+
nextRunAt: z.number().nullable(),
|
|
30
|
+
error: z.string().nullable(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const VoiceKeyerSlotUpdateSchema = z.object({
|
|
34
|
+
label: z.string().max(32).optional(),
|
|
35
|
+
repeatEnabled: z.boolean().optional(),
|
|
36
|
+
repeatIntervalSec: z.number().int().min(1).max(300).optional(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const VoiceKeyerPanelUpdateSchema = z.object({
|
|
40
|
+
slotCount: z.number().int().min(3).max(12),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const VoiceKeyerPlayRequestSchema = z.object({
|
|
44
|
+
callsign: z.string(),
|
|
45
|
+
slotId: z.string(),
|
|
46
|
+
repeat: z.boolean().optional(),
|
|
47
|
+
startImmediately: z.boolean().optional(),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export type VoiceKeyerSlot = z.infer<typeof VoiceKeyerSlotSchema>;
|
|
51
|
+
export type VoiceKeyerPanel = z.infer<typeof VoiceKeyerPanelSchema>;
|
|
52
|
+
export type VoiceKeyerStatus = z.infer<typeof VoiceKeyerStatusSchema>;
|
|
53
|
+
export type VoiceKeyerSlotUpdate = z.infer<typeof VoiceKeyerSlotUpdateSchema>;
|
|
54
|
+
export type VoiceKeyerPanelUpdate = z.infer<typeof VoiceKeyerPanelUpdateSchema>;
|
|
55
|
+
export type VoiceKeyerPlayRequest = z.infer<typeof VoiceKeyerPlayRequestSchema>;
|