@tx5dr/contracts 1.2.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/CLAUDE.md +41 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/dist/schema/__tests__/plugin-runtime-log.schema.test.d.ts +2 -0
- package/dist/schema/__tests__/plugin-runtime-log.schema.test.d.ts.map +1 -0
- package/dist/schema/__tests__/plugin-runtime-log.schema.test.js +78 -0
- package/dist/schema/__tests__/plugin-runtime-log.schema.test.js.map +1 -0
- package/dist/schema/ability.d.ts +85 -0
- package/dist/schema/ability.d.ts.map +1 -0
- package/dist/schema/ability.js +119 -0
- package/dist/schema/ability.js.map +1 -0
- package/dist/schema/api-response.schema.d.ts +1017 -0
- package/dist/schema/api-response.schema.d.ts.map +1 -0
- package/dist/schema/api-response.schema.js +137 -0
- package/dist/schema/api-response.schema.js.map +1 -0
- package/dist/schema/audio-sidecar.schema.d.ts +106 -0
- package/dist/schema/audio-sidecar.schema.d.ts.map +1 -0
- package/dist/schema/audio-sidecar.schema.js +47 -0
- package/dist/schema/audio-sidecar.schema.js.map +1 -0
- package/dist/schema/audio.schema.d.ts +227 -0
- package/dist/schema/audio.schema.d.ts.map +1 -0
- package/dist/schema/audio.schema.js +56 -0
- package/dist/schema/audio.schema.js.map +1 -0
- package/dist/schema/auth.schema.d.ts +738 -0
- package/dist/schema/auth.schema.d.ts.map +1 -0
- package/dist/schema/auth.schema.js +161 -0
- package/dist/schema/auth.schema.js.map +1 -0
- package/dist/schema/cycle.schema.d.ts +33 -0
- package/dist/schema/cycle.schema.d.ts.map +1 -0
- package/dist/schema/cycle.schema.js +23 -0
- package/dist/schema/cycle.schema.js.map +1 -0
- package/dist/schema/desktop-https.schema.d.ts +146 -0
- package/dist/schema/desktop-https.schema.d.ts.map +1 -0
- package/dist/schema/desktop-https.schema.js +31 -0
- package/dist/schema/desktop-https.schema.js.map +1 -0
- package/dist/schema/ft8.schema.d.ts +1107 -0
- package/dist/schema/ft8.schema.d.ts.map +1 -0
- package/dist/schema/ft8.schema.js +268 -0
- package/dist/schema/ft8.schema.js.map +1 -0
- package/dist/schema/hello.schema.d.ts +10 -0
- package/dist/schema/hello.schema.d.ts.map +1 -0
- package/dist/schema/hello.schema.js +5 -0
- package/dist/schema/hello.schema.js.map +1 -0
- package/dist/schema/logbook.schema.d.ts +1472 -0
- package/dist/schema/logbook.schema.d.ts.map +1 -0
- package/dist/schema/logbook.schema.js +256 -0
- package/dist/schema/logbook.schema.js.map +1 -0
- package/dist/schema/mode.schema.d.ts +130 -0
- package/dist/schema/mode.schema.d.ts.map +1 -0
- package/dist/schema/mode.schema.js +141 -0
- package/dist/schema/mode.schema.js.map +1 -0
- package/dist/schema/openwebrx.schema.d.ts +352 -0
- package/dist/schema/openwebrx.schema.d.ts.map +1 -0
- package/dist/schema/openwebrx.schema.js +139 -0
- package/dist/schema/openwebrx.schema.js.map +1 -0
- package/dist/schema/operator.schema.d.ts +883 -0
- package/dist/schema/operator.schema.d.ts.map +1 -0
- package/dist/schema/operator.schema.js +97 -0
- package/dist/schema/operator.schema.js.map +1 -0
- package/dist/schema/plugin.schema.d.ts +1579 -0
- package/dist/schema/plugin.schema.d.ts.map +1 -0
- package/dist/schema/plugin.schema.js +336 -0
- package/dist/schema/plugin.schema.js.map +1 -0
- package/dist/schema/process-monitor.schema.d.ts +282 -0
- package/dist/schema/process-monitor.schema.d.ts.map +1 -0
- package/dist/schema/process-monitor.schema.js +31 -0
- package/dist/schema/process-monitor.schema.js.map +1 -0
- package/dist/schema/pskreporter.schema.d.ts +185 -0
- package/dist/schema/pskreporter.schema.d.ts.map +1 -0
- package/dist/schema/pskreporter.schema.js +80 -0
- package/dist/schema/pskreporter.schema.js.map +1 -0
- package/dist/schema/qso.schema.d.ts +419 -0
- package/dist/schema/qso.schema.d.ts.map +1 -0
- package/dist/schema/qso.schema.js +137 -0
- package/dist/schema/qso.schema.js.map +1 -0
- package/dist/schema/radio-capability.schema.d.ts +625 -0
- package/dist/schema/radio-capability.schema.d.ts.map +1 -0
- package/dist/schema/radio-capability.schema.js +191 -0
- package/dist/schema/radio-capability.schema.js.map +1 -0
- package/dist/schema/radio-power-support.d.ts +45 -0
- package/dist/schema/radio-power-support.d.ts.map +1 -0
- package/dist/schema/radio-power-support.js +64 -0
- package/dist/schema/radio-power-support.js.map +1 -0
- package/dist/schema/radio-power.schema.d.ts +319 -0
- package/dist/schema/radio-power.schema.d.ts.map +1 -0
- package/dist/schema/radio-power.schema.js +87 -0
- package/dist/schema/radio-power.schema.js.map +1 -0
- package/dist/schema/radio-profile.schema.d.ts +2694 -0
- package/dist/schema/radio-profile.schema.d.ts.map +1 -0
- package/dist/schema/radio-profile.schema.js +71 -0
- package/dist/schema/radio-profile.schema.js.map +1 -0
- package/dist/schema/radio.schema.d.ts +1438 -0
- package/dist/schema/radio.schema.d.ts.map +1 -0
- package/dist/schema/radio.schema.js +314 -0
- package/dist/schema/radio.schema.js.map +1 -0
- package/dist/schema/realtime.schema.d.ts +1299 -0
- package/dist/schema/realtime.schema.d.ts.map +1 -0
- package/dist/schema/realtime.schema.js +184 -0
- package/dist/schema/realtime.schema.js.map +1 -0
- package/dist/schema/rigctld.schema.d.ts +151 -0
- package/dist/schema/rigctld.schema.d.ts.map +1 -0
- package/dist/schema/rigctld.schema.js +42 -0
- package/dist/schema/rigctld.schema.js.map +1 -0
- package/dist/schema/server-message-keys.d.ts +21 -0
- package/dist/schema/server-message-keys.d.ts.map +1 -0
- package/dist/schema/server-message-keys.js +22 -0
- package/dist/schema/server-message-keys.js.map +1 -0
- package/dist/schema/slot-info.schema.d.ts +696 -0
- package/dist/schema/slot-info.schema.d.ts.map +1 -0
- package/dist/schema/slot-info.schema.js +144 -0
- package/dist/schema/slot-info.schema.js.map +1 -0
- package/dist/schema/spectrum.schema.d.ts +716 -0
- package/dist/schema/spectrum.schema.d.ts.map +1 -0
- package/dist/schema/spectrum.schema.js +128 -0
- package/dist/schema/spectrum.schema.js.map +1 -0
- package/dist/schema/station-info.schema.d.ts +202 -0
- package/dist/schema/station-info.schema.d.ts.map +1 -0
- package/dist/schema/station-info.schema.js +32 -0
- package/dist/schema/station-info.schema.js.map +1 -0
- package/dist/schema/system.schema.d.ts +113 -0
- package/dist/schema/system.schema.d.ts.map +1 -0
- package/dist/schema/system.schema.js +95 -0
- package/dist/schema/system.schema.js.map +1 -0
- package/dist/schema/transmission.schema.d.ts +413 -0
- package/dist/schema/transmission.schema.d.ts.map +1 -0
- package/dist/schema/transmission.schema.js +78 -0
- package/dist/schema/transmission.schema.js.map +1 -0
- package/dist/schema/voice.schema.d.ts +38 -0
- package/dist/schema/voice.schema.d.ts.map +1 -0
- package/dist/schema/voice.schema.js +24 -0
- package/dist/schema/voice.schema.js.map +1 -0
- package/dist/schema/websocket.schema.d.ts +16916 -0
- package/dist/schema/websocket.schema.d.ts.map +1 -0
- package/dist/schema/websocket.schema.js +1050 -0
- package/dist/schema/websocket.schema.js.map +1 -0
- package/dist/utils/grid.d.ts +13 -0
- package/dist/utils/grid.d.ts.map +1 -0
- package/dist/utils/grid.js +71 -0
- package/dist/utils/grid.js.map +1 -0
- package/eslint.config.js +15 -0
- package/package.json +28 -0
- package/src/index.ts +152 -0
- package/src/schema/__tests__/plugin-runtime-log.schema.test.ts +90 -0
- package/src/schema/ability.ts +205 -0
- package/src/schema/api-response.schema.ts +187 -0
- package/src/schema/audio-sidecar.schema.ts +53 -0
- package/src/schema/audio.schema.ts +73 -0
- package/src/schema/auth.schema.ts +224 -0
- package/src/schema/cycle.schema.ts +25 -0
- package/src/schema/desktop-https.schema.ts +43 -0
- package/src/schema/ft8.schema.ts +362 -0
- package/src/schema/hello.schema.ts +7 -0
- package/src/schema/logbook.schema.ts +310 -0
- package/src/schema/mode.schema.ts +158 -0
- package/src/schema/openwebrx.schema.ts +160 -0
- package/src/schema/operator.schema.ts +112 -0
- package/src/schema/plugin.schema.ts +503 -0
- package/src/schema/process-monitor.schema.ts +41 -0
- package/src/schema/pskreporter.schema.ts +102 -0
- package/src/schema/qso.schema.ts +173 -0
- package/src/schema/radio-capability.schema.ts +244 -0
- package/src/schema/radio-power-support.ts +107 -0
- package/src/schema/radio-power.schema.ts +101 -0
- package/src/schema/radio-profile.schema.ts +91 -0
- package/src/schema/radio.schema.ts +385 -0
- package/src/schema/realtime.schema.ts +262 -0
- package/src/schema/rigctld.schema.ts +51 -0
- package/src/schema/server-message-keys.ts +26 -0
- package/src/schema/slot-info.schema.ts +162 -0
- package/src/schema/spectrum.schema.ts +166 -0
- package/src/schema/station-info.schema.ts +40 -0
- package/src/schema/system.schema.ts +121 -0
- package/src/schema/transmission.schema.ts +105 -0
- package/src/schema/voice.schema.ts +28 -0
- package/src/schema/websocket.schema.ts +1480 -0
- package/src/utils/grid.ts +94 -0
- package/test/mode-schema.test.ts +48 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ModeDescriptorSchema } from './mode.schema.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Candidate-ranking policy used when choosing which station to answer first.
|
|
6
|
+
*
|
|
7
|
+
* - `balanced`: combine multiple heuristics without strongly favoring one.
|
|
8
|
+
* - `dxcc_first`: prioritize entities that advance DXCC goals.
|
|
9
|
+
* - `new_callsign_first`: prioritize callsigns not yet worked before.
|
|
10
|
+
*/
|
|
11
|
+
export const TargetSelectionPriorityModeSchema = z.enum([
|
|
12
|
+
'balanced',
|
|
13
|
+
'dxcc_first',
|
|
14
|
+
'new_callsign_first',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Current work status of a resolved DXCC entity in local logbook context.
|
|
19
|
+
*
|
|
20
|
+
* - `current`: active/current DXCC entity.
|
|
21
|
+
* - `deleted`: historically valid but deleted DXCC entity.
|
|
22
|
+
* - `none`: no DXCC entity could be associated.
|
|
23
|
+
* - `unknown`: resolution has not completed or confidence is insufficient.
|
|
24
|
+
*/
|
|
25
|
+
export const DxccStatusSchema = z.enum([
|
|
26
|
+
'current',
|
|
27
|
+
'deleted',
|
|
28
|
+
'none',
|
|
29
|
+
'unknown',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
export const DxccSourceSchema = z.enum([
|
|
33
|
+
'resolver',
|
|
34
|
+
'adif',
|
|
35
|
+
'lotw',
|
|
36
|
+
'manual_override',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
export const DxccConfidenceSchema = z.enum([
|
|
40
|
+
'exception',
|
|
41
|
+
'prefix',
|
|
42
|
+
'heuristic',
|
|
43
|
+
'manual',
|
|
44
|
+
'unknown',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
export const SubdivisionConfidenceSchema = z.enum([
|
|
48
|
+
'high',
|
|
49
|
+
'low',
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
// 操作配置
|
|
53
|
+
export const OperatorConfigSchema = z.object({
|
|
54
|
+
id: z.string(),
|
|
55
|
+
mode: ModeDescriptorSchema,
|
|
56
|
+
myCallsign: z.string(),
|
|
57
|
+
myGrid: z.string(),
|
|
58
|
+
transmitCycles: z.array(z.number()),
|
|
59
|
+
autoReplyToCQ: z.boolean(),
|
|
60
|
+
maxQSOTimeoutCycles: z.number(),
|
|
61
|
+
maxCallAttempts: z.number(),
|
|
62
|
+
frequency: z.number(),
|
|
63
|
+
autoResumeCQAfterFail: z.boolean().default(false),
|
|
64
|
+
autoResumeCQAfterSuccess: z.boolean().default(false),
|
|
65
|
+
replyToWorkedStations: z.boolean().default(false),
|
|
66
|
+
prioritizeNewCalls: z.boolean().default(true),
|
|
67
|
+
targetSelectionPriorityMode: TargetSelectionPriorityModeSchema.default('dxcc_first'),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// QSO状态机上下文
|
|
71
|
+
export const QSOContextSchema = z.object({
|
|
72
|
+
config: OperatorConfigSchema,
|
|
73
|
+
targetCallsign: z.string().optional(),
|
|
74
|
+
targetGrid: z.string().optional(),
|
|
75
|
+
reportSent: z.number().optional(),
|
|
76
|
+
reportReceived: z.number().optional(),
|
|
77
|
+
actualFrequency: z.number().optional(), // 实际通联频率 (基础频率 + 偏移频率)
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// QSL 确认状态枚举(参考 ADIF 3.1.4 规范)
|
|
81
|
+
export const QslSentStatusSchema = z.enum(['Y', 'N', 'R', 'Q', 'I']).optional(); // Y=yes, N=no, R=requested, Q=queued, I=invalid
|
|
82
|
+
export const QslReceivedStatusSchema = z.enum(['Y', 'N', 'R', 'I', 'V']).optional(); // Y=yes, V=validated
|
|
83
|
+
export const QslSimpleStatusSchema = z.enum(['Y', 'N']).optional();
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Canonical persisted QSO record used by TX-5DR logbooks and plugin hooks.
|
|
87
|
+
*
|
|
88
|
+
* The schema intentionally spans multiple layers of information:
|
|
89
|
+
* - core contact details such as callsign, grid, frequency and timestamps;
|
|
90
|
+
* - message-level exchange details such as reports and raw message history;
|
|
91
|
+
* - DXCC enrichment and review metadata;
|
|
92
|
+
* - LoTW / QRZ confirmation state;
|
|
93
|
+
* - station-location metadata used when exporting ADIF-like records.
|
|
94
|
+
*/
|
|
95
|
+
export const QSORecordSchema = z.object({
|
|
96
|
+
id: z.string(),
|
|
97
|
+
callsign: z.string(), // 对方呼号
|
|
98
|
+
grid: z.string().optional(), // 对方网格定位
|
|
99
|
+
frequency: z.number(), // 频率
|
|
100
|
+
mode: z.string(), // 模式(FT8)
|
|
101
|
+
submode: z.string().optional(), // ADIF 子模式(如 FT4)
|
|
102
|
+
startTime: z.number(), // 开始时间
|
|
103
|
+
endTime: z.number().optional(), // 结束时间
|
|
104
|
+
reportSent: z.string().optional(), // 发送的信号报告
|
|
105
|
+
reportReceived: z.string().optional(), // 接收的信号报告
|
|
106
|
+
messageHistory: z.array(z.string()), // 内部消息历史(FT8 等数字模式)
|
|
107
|
+
comment: z.string().optional(), // 标准 ADIF COMMENT 字段
|
|
108
|
+
myCallsign: z.string().optional(), // 我的呼号(操作员呼号)
|
|
109
|
+
myGrid: z.string().optional(), // 我的网格定位(操作员网格)
|
|
110
|
+
qth: z.string().optional(), // 对方 QTH(地点,语音通联常用)
|
|
111
|
+
dxccId: z.number().int().positive().optional(),
|
|
112
|
+
dxccEntity: z.string().optional(),
|
|
113
|
+
dxccStatus: DxccStatusSchema.optional(),
|
|
114
|
+
countryCode: z.string().optional(),
|
|
115
|
+
cqZone: z.number().int().positive().optional(),
|
|
116
|
+
ituZone: z.number().int().positive().optional(),
|
|
117
|
+
dxccSource: DxccSourceSchema.optional(),
|
|
118
|
+
dxccConfidence: DxccConfidenceSchema.optional(),
|
|
119
|
+
dxccResolvedAt: z.number().optional(),
|
|
120
|
+
dxccResolverVersion: z.string().optional(),
|
|
121
|
+
dxccNeedsReview: z.boolean().optional(),
|
|
122
|
+
stationLocationId: z.string().optional(),
|
|
123
|
+
myDxccId: z.number().int().positive().optional(),
|
|
124
|
+
myCqZone: z.number().int().positive().optional(),
|
|
125
|
+
myItuZone: z.number().int().positive().optional(),
|
|
126
|
+
myState: z.string().optional(),
|
|
127
|
+
myCounty: z.string().optional(),
|
|
128
|
+
myIota: z.string().optional(),
|
|
129
|
+
|
|
130
|
+
// LoTW QSL 确认状态
|
|
131
|
+
lotwQslSent: QslSentStatusSchema,
|
|
132
|
+
lotwQslReceived: QslReceivedStatusSchema,
|
|
133
|
+
lotwQslSentDate: z.number().optional(), // 发送日期 (timestamp)
|
|
134
|
+
lotwQslReceivedDate: z.number().optional(), // 确认日期 (timestamp)
|
|
135
|
+
|
|
136
|
+
// QRZ QSL 确认状态
|
|
137
|
+
qrzQslSent: QslSimpleStatusSchema,
|
|
138
|
+
qrzQslReceived: QslSimpleStatusSchema,
|
|
139
|
+
qrzQslSentDate: z.number().optional(),
|
|
140
|
+
qrzQslReceivedDate: z.number().optional(),
|
|
141
|
+
|
|
142
|
+
// 附注(对应 ADIF NOTES 字段)
|
|
143
|
+
notes: z.string().optional(),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// 策略执行结果
|
|
147
|
+
export const StrategiesResultSchema = z.object({
|
|
148
|
+
stop: z.boolean().optional(),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
export type OperatorConfig = z.infer<typeof OperatorConfigSchema>;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Candidate-ranking policy used when choosing which station to answer first.
|
|
155
|
+
*/
|
|
156
|
+
export type TargetSelectionPriorityMode = z.infer<typeof TargetSelectionPriorityModeSchema>;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Current work status of a resolved DXCC entity in local logbook context.
|
|
160
|
+
*/
|
|
161
|
+
export type DxccStatus = z.infer<typeof DxccStatusSchema>;
|
|
162
|
+
export type DxccSource = z.infer<typeof DxccSourceSchema>;
|
|
163
|
+
export type DxccConfidence = z.infer<typeof DxccConfidenceSchema>;
|
|
164
|
+
export type QSOContext = z.infer<typeof QSOContextSchema>;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Canonical persisted QSO record used by TX-5DR logbooks and plugin hooks.
|
|
168
|
+
*
|
|
169
|
+
* Plugin authors will most commonly encounter this in completion hooks and
|
|
170
|
+
* logbook queries.
|
|
171
|
+
*/
|
|
172
|
+
export type QSORecord = z.infer<typeof QSORecordSchema>;
|
|
173
|
+
export type StrategiesResult = z.infer<typeof StrategiesResultSchema>;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 能力分类
|
|
5
|
+
* - antenna: 天线相关(天调等)
|
|
6
|
+
* - rf: 射频相关(发射功率、噪声抑制等)
|
|
7
|
+
* - audio: 音频相关(AF 增益、静噪、VOX 等)
|
|
8
|
+
* - operation: 操作过程相关(RIT/XIT、中继偏移、音调等)
|
|
9
|
+
* - system: 电台系统状态(锁定、电源等)
|
|
10
|
+
*/
|
|
11
|
+
export const CapabilityCategorySchema = z.enum(['antenna', 'rf', 'audio', 'operation', 'system']);
|
|
12
|
+
export type CapabilityCategory = z.infer<typeof CapabilityCategorySchema>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 能力值类型
|
|
16
|
+
* - boolean: 布尔开关
|
|
17
|
+
* - number: 数值(滑块/输入框)
|
|
18
|
+
* - enum: 枚举(下拉选择)
|
|
19
|
+
* - action: 纯动作按钮(无持久值,如手动调谐)
|
|
20
|
+
*/
|
|
21
|
+
export const CapabilityValueTypeSchema = z.enum(['boolean', 'number', 'enum', 'action']);
|
|
22
|
+
export type CapabilityValueType = z.infer<typeof CapabilityValueTypeSchema>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 能力更新模式
|
|
26
|
+
* - polling: 服务端定时轮询检测变化
|
|
27
|
+
* - event: 由电台事件驱动(如连接时一次性读取)
|
|
28
|
+
* - none: 不主动更新(action 类或只写能力)
|
|
29
|
+
*/
|
|
30
|
+
export const CapabilityUpdateModeSchema = z.enum(['polling', 'event', 'none']);
|
|
31
|
+
export type CapabilityUpdateMode = z.infer<typeof CapabilityUpdateModeSchema>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 枚举项可用值类型。
|
|
35
|
+
*/
|
|
36
|
+
export const CapabilityOptionValueSchema = z.union([z.string(), z.number()]);
|
|
37
|
+
export type CapabilityOptionValue = z.infer<typeof CapabilityOptionValueSchema>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 运行时能力值。
|
|
41
|
+
*/
|
|
42
|
+
export const CapabilityValueSchema = z.union([z.boolean(), z.number(), z.string()]);
|
|
43
|
+
export type CapabilityValue = z.infer<typeof CapabilityValueSchema>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 枚举项定义。
|
|
47
|
+
*/
|
|
48
|
+
export const CapabilityOptionSchema = z.object({
|
|
49
|
+
value: CapabilityOptionValueSchema,
|
|
50
|
+
label: z.string().optional(),
|
|
51
|
+
labelI18nKey: z.string().optional(),
|
|
52
|
+
});
|
|
53
|
+
export type CapabilityOption = z.infer<typeof CapabilityOptionSchema>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 前端展示模式。
|
|
57
|
+
*/
|
|
58
|
+
export const CapabilityDisplayModeSchema = z.enum(['percent', 'value']);
|
|
59
|
+
export type CapabilityDisplayMode = z.infer<typeof CapabilityDisplayModeSchema>;
|
|
60
|
+
|
|
61
|
+
export const CapabilityDisplayUnitSchema = z.enum(['Hz', 'kHz', 'toneHz', 'code', 'state']);
|
|
62
|
+
export type CapabilityDisplayUnit = z.infer<typeof CapabilityDisplayUnitSchema>;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 展示格式提示,由服务端下发给前端。
|
|
66
|
+
*/
|
|
67
|
+
export const CapabilityDisplaySchema = z.object({
|
|
68
|
+
mode: CapabilityDisplayModeSchema,
|
|
69
|
+
unit: CapabilityDisplayUnitSchema.optional(),
|
|
70
|
+
decimals: z.number().int().min(0).optional(),
|
|
71
|
+
signed: z.boolean().optional(),
|
|
72
|
+
});
|
|
73
|
+
export type CapabilityDisplay = z.infer<typeof CapabilityDisplaySchema>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 能力描述符
|
|
77
|
+
* 由服务端在运行时下发,作为当前连接会话的真源。
|
|
78
|
+
*/
|
|
79
|
+
export const CapabilityDescriptorSchema = z.object({
|
|
80
|
+
/** 全局唯一能力 ID,如 'tuner_switch', 'rf_power', 'lock_mode' */
|
|
81
|
+
id: z.string(),
|
|
82
|
+
|
|
83
|
+
/** 能力分类,用于前端面板分组渲染 */
|
|
84
|
+
category: CapabilityCategorySchema,
|
|
85
|
+
|
|
86
|
+
/** 能力值类型 */
|
|
87
|
+
valueType: CapabilityValueTypeSchema,
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 数值范围(仅 valueType='number' 时有效)
|
|
91
|
+
* 值可以是归一化范围(如 0-1),也可以是实际范围(如 -9999~9999 Hz)
|
|
92
|
+
*/
|
|
93
|
+
range: z.object({
|
|
94
|
+
min: z.number(),
|
|
95
|
+
max: z.number(),
|
|
96
|
+
step: z.number().optional(),
|
|
97
|
+
}).optional(),
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 数值能力的离散候选项(仅 valueType='number' 时有效)
|
|
101
|
+
* 例:RF 功率只允许若干固定挡位,但仍希望保留 slider 交互。
|
|
102
|
+
*/
|
|
103
|
+
discreteOptions: z.array(CapabilityOptionSchema).optional(),
|
|
104
|
+
|
|
105
|
+
/** 枚举项(仅 valueType='enum' 时有效) */
|
|
106
|
+
options: z.array(CapabilityOptionSchema).optional(),
|
|
107
|
+
|
|
108
|
+
/** 是否可读取当前值(false = 只写,UI 无初始值) */
|
|
109
|
+
readable: z.boolean(),
|
|
110
|
+
|
|
111
|
+
/** 是否可写入(false = 只读展示) */
|
|
112
|
+
writable: z.boolean(),
|
|
113
|
+
|
|
114
|
+
/** 服务端更新策略 */
|
|
115
|
+
updateMode: CapabilityUpdateModeSchema,
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 轮询间隔(ms),仅 updateMode='polling' 时有效。
|
|
119
|
+
*/
|
|
120
|
+
pollIntervalMs: z.number().optional(),
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 复合能力分组 ID。
|
|
124
|
+
* 同一 group 的描述符在面板中合并为一张卡片(如天调开关和手动调谐按钮)
|
|
125
|
+
*/
|
|
126
|
+
compoundGroup: z.string().optional(),
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 在复合能力组中的角色
|
|
130
|
+
* - switch: 布尔开关(主控制)
|
|
131
|
+
* - action: 动作按钮
|
|
132
|
+
*/
|
|
133
|
+
compoundRole: z.enum(['switch', 'action']).optional(),
|
|
134
|
+
|
|
135
|
+
/** 前端标签 i18n key,如 'radio:capability.tuner_switch.label' */
|
|
136
|
+
labelI18nKey: z.string(),
|
|
137
|
+
|
|
138
|
+
/** 前端描述文字 i18n key(可选) */
|
|
139
|
+
descriptionI18nKey: z.string().optional(),
|
|
140
|
+
|
|
141
|
+
/** 展示格式提示 */
|
|
142
|
+
display: CapabilityDisplaySchema.optional(),
|
|
143
|
+
|
|
144
|
+
/** 是否在 RadioControl 工具栏 surface 区域露出紧凑控件 */
|
|
145
|
+
hasSurfaceControl: z.boolean(),
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* surface 控件的分组 ID。
|
|
149
|
+
* 同一 surfaceGroup 的控件聚合为一个 Popover(如天调开关和调谐按钮)
|
|
150
|
+
*/
|
|
151
|
+
surfaceGroup: z.string().optional(),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
export type CapabilityDescriptor = z.infer<typeof CapabilityDescriptorSchema>;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 能力运行时状态
|
|
158
|
+
* 动态数据,通过 WebSocket 实时同步到前端。
|
|
159
|
+
*/
|
|
160
|
+
export const CapabilityStateSchema = z.object({
|
|
161
|
+
/** 能力 ID,与 CapabilityDescriptor.id 对应 */
|
|
162
|
+
id: z.string(),
|
|
163
|
+
|
|
164
|
+
/** 当前连接的电台是否支持此能力(探测结果) */
|
|
165
|
+
supported: z.boolean(),
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 当前值
|
|
169
|
+
* - boolean 类能力:true/false
|
|
170
|
+
* - number 类能力:数值(范围由 descriptor.range 定义)
|
|
171
|
+
* - enum 类能力:string/number(必须落在 descriptor.options 内)
|
|
172
|
+
* - action 类能力:始终为 null
|
|
173
|
+
*/
|
|
174
|
+
value: CapabilityValueSchema.nullable(),
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 附加元数据(能力特有信息)
|
|
178
|
+
* 例:tuner_switch 的 meta 可携带 { status: 'tuning' | 'idle' | 'success' | 'failed', swr?: number }
|
|
179
|
+
*/
|
|
180
|
+
meta: z.record(z.unknown()).optional(),
|
|
181
|
+
|
|
182
|
+
/** 最后更新时间戳(ms) */
|
|
183
|
+
updatedAt: z.number(),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
export type CapabilityState = z.infer<typeof CapabilityStateSchema>;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 能力列表快照(radioCapabilityList WS 消息 / REST 响应的 data 部分)
|
|
190
|
+
*/
|
|
191
|
+
export const CapabilityListSchema = z.object({
|
|
192
|
+
descriptors: z.array(CapabilityDescriptorSchema),
|
|
193
|
+
capabilities: z.array(CapabilityStateSchema),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
export type CapabilityList = z.infer<typeof CapabilityListSchema>;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 写命令负载(writeRadioCapability WS 命令的 data 部分)
|
|
200
|
+
*/
|
|
201
|
+
export const WriteCapabilityPayloadSchema = z.object({
|
|
202
|
+
/** 能力 ID */
|
|
203
|
+
id: z.string(),
|
|
204
|
+
/** 写入值(boolean/number/enum 类能力) */
|
|
205
|
+
value: CapabilityValueSchema.optional(),
|
|
206
|
+
/** 触发动作(action 类能力,传 true) */
|
|
207
|
+
action: z.boolean().optional(),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
export type WriteCapabilityPayload = z.infer<typeof WriteCapabilityPayloadSchema>;
|
|
211
|
+
|
|
212
|
+
// ============================================================
|
|
213
|
+
// 能力 ID 字面量联合类型(方便类型检查)
|
|
214
|
+
// ============================================================
|
|
215
|
+
|
|
216
|
+
export const CAPABILITY_IDS = [
|
|
217
|
+
'tuner_switch',
|
|
218
|
+
'tuner_tune',
|
|
219
|
+
'rf_power',
|
|
220
|
+
'af_gain',
|
|
221
|
+
'sql',
|
|
222
|
+
'mic_gain',
|
|
223
|
+
'compressor',
|
|
224
|
+
'compressor_level',
|
|
225
|
+
'monitor_gain',
|
|
226
|
+
'nb',
|
|
227
|
+
'nr',
|
|
228
|
+
'lock_mode',
|
|
229
|
+
'mute',
|
|
230
|
+
'vox',
|
|
231
|
+
'agc_mode',
|
|
232
|
+
'preamp',
|
|
233
|
+
'attenuator',
|
|
234
|
+
'mode_bandwidth',
|
|
235
|
+
'rit_offset',
|
|
236
|
+
'xit_offset',
|
|
237
|
+
'tuning_step',
|
|
238
|
+
'repeater_shift',
|
|
239
|
+
'repeater_offset',
|
|
240
|
+
'ctcss_tone',
|
|
241
|
+
'dcs_code',
|
|
242
|
+
] as const;
|
|
243
|
+
|
|
244
|
+
export type CapabilityId = (typeof CAPABILITY_IDS)[number];
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { HamlibConfig } from './radio.schema.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Radios known to support `rig_set_powerstat(RIG_POWER_ON)` via Hamlib.
|
|
5
|
+
* Matching happens on mfgName + modelName (case-insensitive, substring/regex).
|
|
6
|
+
*
|
|
7
|
+
* Because node-hamlib does not expose `rig_get_caps`, we cannot introspect
|
|
8
|
+
* per-model powerstat support at runtime. Instead we maintain a conservative
|
|
9
|
+
* allow-list of radios for which powerstat is verified to work; UI only
|
|
10
|
+
* surfaces the "power on" button for these models. The server still falls
|
|
11
|
+
* back to the actual Hamlib return value — an unsupported model receives a
|
|
12
|
+
* user-friendly error if the allow-list is bypassed.
|
|
13
|
+
*
|
|
14
|
+
* To add a new model: add an entry with the mfgName returned by
|
|
15
|
+
* `HamLib.getSupportedRigs()` and a pattern that matches its modelName.
|
|
16
|
+
*/
|
|
17
|
+
export interface PowerCapableRigEntry {
|
|
18
|
+
mfg: string | RegExp;
|
|
19
|
+
model: string | RegExp;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const POWER_CAPABLE_RIGS: ReadonlyArray<PowerCapableRigEntry> = [
|
|
23
|
+
// 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 },
|
|
31
|
+
|
|
32
|
+
// 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 },
|
|
36
|
+
|
|
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
|
+
];
|
|
43
|
+
|
|
44
|
+
export function isRigModelPowerCapable(mfgName: string, modelName: string): boolean {
|
|
45
|
+
return POWER_CAPABLE_RIGS.some((entry) => {
|
|
46
|
+
const mfgOk =
|
|
47
|
+
typeof entry.mfg === 'string'
|
|
48
|
+
? entry.mfg.toLowerCase() === mfgName.toLowerCase()
|
|
49
|
+
: entry.mfg.test(mfgName);
|
|
50
|
+
const modelOk =
|
|
51
|
+
typeof entry.model === 'string'
|
|
52
|
+
? entry.model.toLowerCase() === modelName.toLowerCase()
|
|
53
|
+
: entry.model.test(modelName);
|
|
54
|
+
return mfgOk && modelOk;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type PowerSupportReason =
|
|
59
|
+
| 'model-unsupported'
|
|
60
|
+
| 'network-mode-no-wake'
|
|
61
|
+
| 'none-mode';
|
|
62
|
+
|
|
63
|
+
export interface PowerSupportDecision {
|
|
64
|
+
/** Whether the UI should surface a "power on" control. */
|
|
65
|
+
canPowerOn: boolean;
|
|
66
|
+
/** Whether the UI should surface a "power off" control. */
|
|
67
|
+
canPowerOff: boolean;
|
|
68
|
+
/** Machine-readable reason when `canPowerOn` is false. */
|
|
69
|
+
reason?: PowerSupportReason;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Decide whether power control should be surfaced for a given Profile radio config.
|
|
74
|
+
*
|
|
75
|
+
* For serial mode, the caller must provide the rig's mfgName/modelName
|
|
76
|
+
* (resolved via `HamLib.getSupportedRigs()` server-side). For network mode
|
|
77
|
+
* the rig model is not known ahead of time (rigctld proxies any radio) —
|
|
78
|
+
* `power on` is never offered because a remote rigctld running on the radio
|
|
79
|
+
* is unreachable when the radio is powered off. `power off` is still offered
|
|
80
|
+
* in case the rigctld process runs on a separate host.
|
|
81
|
+
*/
|
|
82
|
+
export function decidePowerSupport(
|
|
83
|
+
config: HamlibConfig,
|
|
84
|
+
rigInfo?: { mfgName: string; modelName: string }
|
|
85
|
+
): PowerSupportDecision {
|
|
86
|
+
switch (config.type) {
|
|
87
|
+
case 'none':
|
|
88
|
+
return { canPowerOn: false, canPowerOff: false, reason: 'none-mode' };
|
|
89
|
+
case 'network':
|
|
90
|
+
return { canPowerOn: false, canPowerOff: true, reason: 'network-mode-no-wake' };
|
|
91
|
+
case 'icom-wlan':
|
|
92
|
+
// ICOM WLAN 的 CI-V-over-UDP 通道在电台关机后无法维持;即便已连接
|
|
93
|
+
// 发送 powerstat(off) 也不能可靠恢复。整体不暴露电源控制。
|
|
94
|
+
return { canPowerOn: false, canPowerOff: false, reason: 'model-unsupported' };
|
|
95
|
+
case 'serial': {
|
|
96
|
+
if (!rigInfo) {
|
|
97
|
+
return { canPowerOn: false, canPowerOff: false, reason: 'model-unsupported' };
|
|
98
|
+
}
|
|
99
|
+
const supported = isRigModelPowerCapable(rigInfo.mfgName, rigInfo.modelName);
|
|
100
|
+
return supported
|
|
101
|
+
? { canPowerOn: true, canPowerOff: true }
|
|
102
|
+
: { canPowerOn: false, canPowerOff: false, reason: 'model-unsupported' };
|
|
103
|
+
}
|
|
104
|
+
default:
|
|
105
|
+
return { canPowerOn: false, canPowerOff: false, reason: 'model-unsupported' };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { HamlibConfigSchema } from './radio.schema.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* High-level runtime state tracked by the RadioPowerController.
|
|
6
|
+
*
|
|
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; the engine is being started (if autoEngine).
|
|
10
|
+
* - `shutting_down`: engine is stopping in preparation for a power-off command.
|
|
11
|
+
* - `entering_standby`: powerstat(2) sent, waiting for radio to drop CAT link.
|
|
12
|
+
* - `failed`: last transition failed; UI should show an error + retry.
|
|
13
|
+
*/
|
|
14
|
+
export const RadioPowerStateSchema = z.enum([
|
|
15
|
+
'off',
|
|
16
|
+
'waking',
|
|
17
|
+
'awake',
|
|
18
|
+
'shutting_down',
|
|
19
|
+
'entering_standby',
|
|
20
|
+
'failed',
|
|
21
|
+
]);
|
|
22
|
+
export type RadioPowerState = z.infer<typeof RadioPowerStateSchema>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Finer-grained progress stage inside `waking` / `shutting_down`.
|
|
26
|
+
* UI uses this to show progress bar labels.
|
|
27
|
+
*/
|
|
28
|
+
export const RadioPowerStageSchema = z.enum([
|
|
29
|
+
'idle',
|
|
30
|
+
'sending_command',
|
|
31
|
+
'waiting_ready',
|
|
32
|
+
'waiting_standby_disconnect',
|
|
33
|
+
'starting_engine',
|
|
34
|
+
'stopping_engine',
|
|
35
|
+
'disconnecting',
|
|
36
|
+
]);
|
|
37
|
+
export type RadioPowerStage = z.infer<typeof RadioPowerStageSchema>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* WS event payload: server → client.
|
|
41
|
+
*/
|
|
42
|
+
export const RadioPowerStateEventSchema = z.object({
|
|
43
|
+
profileId: z.string().optional(),
|
|
44
|
+
state: RadioPowerStateSchema,
|
|
45
|
+
stage: RadioPowerStageSchema,
|
|
46
|
+
/** Translation key reference for an inline error, e.g. 'radio:power.error.timeout'. */
|
|
47
|
+
errorKey: z.string().optional(),
|
|
48
|
+
/** Free-form details for debugging; not user-facing. */
|
|
49
|
+
errorDetail: z.string().optional(),
|
|
50
|
+
});
|
|
51
|
+
export type RadioPowerStateEvent = z.infer<typeof RadioPowerStateEventSchema>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* REST request body: POST /api/radio/power
|
|
55
|
+
*/
|
|
56
|
+
/** Allowed target states for a power request. */
|
|
57
|
+
export const RadioPowerTargetSchema = z.enum(['on', 'off', 'standby', 'operate']);
|
|
58
|
+
export type RadioPowerTarget = z.infer<typeof RadioPowerTargetSchema>;
|
|
59
|
+
|
|
60
|
+
export const RadioPowerRequestSchema = z.object({
|
|
61
|
+
profileId: z.string().min(1),
|
|
62
|
+
state: RadioPowerTargetSchema,
|
|
63
|
+
/** Automatically start the engine after successful power-on. Defaults to true. */
|
|
64
|
+
autoEngine: z.boolean().optional().default(true),
|
|
65
|
+
});
|
|
66
|
+
export type RadioPowerRequest = z.infer<typeof RadioPowerRequestSchema>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* REST response: POST /api/radio/power
|
|
70
|
+
*/
|
|
71
|
+
export const RadioPowerResponseSchema = z.object({
|
|
72
|
+
success: z.boolean(),
|
|
73
|
+
state: RadioPowerStateSchema,
|
|
74
|
+
});
|
|
75
|
+
export type RadioPowerResponse = z.infer<typeof RadioPowerResponseSchema>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* REST response: GET /api/radio/power/support?profileId=xxx
|
|
79
|
+
* The server resolves mfgName/modelName internally via HamLib.getSupportedRigs().
|
|
80
|
+
*/
|
|
81
|
+
export const RadioPowerSupportInfoSchema = z.object({
|
|
82
|
+
profileId: z.string(),
|
|
83
|
+
canPowerOn: z.boolean(),
|
|
84
|
+
canPowerOff: z.boolean(),
|
|
85
|
+
/**
|
|
86
|
+
* States the user is allowed to switch *between* while the radio is connected.
|
|
87
|
+
* Empty when the radio is unsupported or not connected (UI then renders no dropdown).
|
|
88
|
+
*/
|
|
89
|
+
supportedStates: z.array(z.enum(['operate', 'standby', 'off'])).default([]),
|
|
90
|
+
reason: z.enum(['model-unsupported', 'network-mode-no-wake', 'none-mode']).optional(),
|
|
91
|
+
rigInfo: z
|
|
92
|
+
.object({
|
|
93
|
+
mfgName: z.string(),
|
|
94
|
+
modelName: z.string(),
|
|
95
|
+
})
|
|
96
|
+
.optional(),
|
|
97
|
+
});
|
|
98
|
+
export type RadioPowerSupportInfo = z.infer<typeof RadioPowerSupportInfoSchema>;
|
|
99
|
+
|
|
100
|
+
/** Convenience re-export so callers can validate a full config blob. */
|
|
101
|
+
export const RadioPowerProfileConfigSchema = HamlibConfigSchema;
|