@tx5dr/contracts 2.0.0 → 2.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 +4 -1
- 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__/assisted-queue.schema.test.js +1 -0
- package/dist/schema/__tests__/assisted-queue.schema.test.js.map +1 -1
- package/dist/schema/__tests__/meter.schema.test.d.ts +2 -0
- package/dist/schema/__tests__/meter.schema.test.d.ts.map +1 -0
- package/dist/schema/__tests__/meter.schema.test.js +27 -0
- package/dist/schema/__tests__/meter.schema.test.js.map +1 -0
- package/dist/schema/__tests__/operator-context-report.schema.test.d.ts +2 -0
- package/dist/schema/__tests__/operator-context-report.schema.test.d.ts.map +1 -0
- package/dist/schema/__tests__/operator-context-report.schema.test.js +70 -0
- package/dist/schema/__tests__/operator-context-report.schema.test.js.map +1 -0
- package/dist/schema/__tests__/plugin-market.schema.test.js +107 -3
- package/dist/schema/__tests__/plugin-market.schema.test.js.map +1 -1
- package/dist/schema/__tests__/qso.schema.test.d.ts +2 -0
- package/dist/schema/__tests__/qso.schema.test.d.ts.map +1 -0
- package/dist/schema/__tests__/qso.schema.test.js +70 -0
- package/dist/schema/__tests__/qso.schema.test.js.map +1 -0
- package/dist/schema/__tests__/radio-power-support.test.js +4 -1
- package/dist/schema/__tests__/radio-power-support.test.js.map +1 -1
- package/dist/schema/__tests__/radio.schema.test.js +52 -1
- package/dist/schema/__tests__/radio.schema.test.js.map +1 -1
- package/dist/schema/__tests__/remote-access.schema.test.js +1 -1
- package/dist/schema/__tests__/remote-access.schema.test.js.map +1 -1
- package/dist/schema/__tests__/spectrum.schema.test.d.ts +2 -0
- package/dist/schema/__tests__/spectrum.schema.test.d.ts.map +1 -0
- package/dist/schema/__tests__/spectrum.schema.test.js +107 -0
- package/dist/schema/__tests__/spectrum.schema.test.js.map +1 -0
- package/dist/schema/__tests__/strategy-transmission.schema.test.d.ts +2 -0
- package/dist/schema/__tests__/strategy-transmission.schema.test.d.ts.map +1 -0
- package/dist/schema/__tests__/strategy-transmission.schema.test.js +95 -0
- package/dist/schema/__tests__/strategy-transmission.schema.test.js.map +1 -0
- package/dist/schema/api-response.schema.d.ts +235 -0
- package/dist/schema/api-response.schema.d.ts.map +1 -1
- package/dist/schema/api-response.schema.js +9 -0
- package/dist/schema/api-response.schema.js.map +1 -1
- package/dist/schema/auth.schema.d.ts +2 -2
- package/dist/schema/cpu-profile.schema.d.ts +2 -2
- package/dist/schema/device-ui.schema.d.ts +10 -10
- package/dist/schema/image-radio.schema.d.ts +2597 -0
- package/dist/schema/image-radio.schema.d.ts.map +1 -0
- package/dist/schema/image-radio.schema.js +283 -0
- package/dist/schema/image-radio.schema.js.map +1 -0
- package/dist/schema/logbook.schema.d.ts +203 -15
- package/dist/schema/logbook.schema.d.ts.map +1 -1
- package/dist/schema/logbook.schema.js +1 -0
- package/dist/schema/logbook.schema.js.map +1 -1
- package/dist/schema/mode.schema.d.ts +2 -0
- package/dist/schema/mode.schema.d.ts.map +1 -1
- package/dist/schema/mode.schema.js +16 -0
- package/dist/schema/mode.schema.js.map +1 -1
- package/dist/schema/openwebrx.schema.d.ts +2 -2
- package/dist/schema/operator.schema.d.ts +2676 -0
- package/dist/schema/operator.schema.d.ts.map +1 -1
- package/dist/schema/operator.schema.js +4 -19
- package/dist/schema/operator.schema.js.map +1 -1
- package/dist/schema/plugin.schema.d.ts +1780 -475
- package/dist/schema/plugin.schema.d.ts.map +1 -1
- package/dist/schema/plugin.schema.js +108 -12
- package/dist/schema/plugin.schema.js.map +1 -1
- package/dist/schema/qso.schema.d.ts +239 -1
- package/dist/schema/qso.schema.d.ts.map +1 -1
- package/dist/schema/qso.schema.js +61 -0
- package/dist/schema/qso.schema.js.map +1 -1
- package/dist/schema/radio-capability.schema.d.ts +1 -1
- package/dist/schema/radio-capability.schema.d.ts.map +1 -1
- package/dist/schema/radio-capability.schema.js +1 -0
- package/dist/schema/radio-capability.schema.js.map +1 -1
- package/dist/schema/radio-power.schema.d.ts +15 -0
- package/dist/schema/radio-power.schema.d.ts.map +1 -1
- package/dist/schema/radio-profile.schema.d.ts +251 -0
- package/dist/schema/radio-profile.schema.d.ts.map +1 -1
- package/dist/schema/radio-profile.schema.js +7 -0
- package/dist/schema/radio-profile.schema.js.map +1 -1
- package/dist/schema/radio.schema.d.ts +174 -3
- package/dist/schema/radio.schema.d.ts.map +1 -1
- package/dist/schema/radio.schema.js +31 -2
- package/dist/schema/radio.schema.js.map +1 -1
- package/dist/schema/rigctld.schema.d.ts +6 -6
- package/dist/schema/slot-info.schema.d.ts +13 -0
- package/dist/schema/slot-info.schema.d.ts.map +1 -1
- package/dist/schema/slot-info.schema.js +2 -1
- package/dist/schema/slot-info.schema.js.map +1 -1
- package/dist/schema/spectrum-config.schema.d.ts +638 -0
- package/dist/schema/spectrum-config.schema.d.ts.map +1 -0
- package/dist/schema/spectrum-config.schema.js +129 -0
- package/dist/schema/spectrum-config.schema.js.map +1 -0
- package/dist/schema/spectrum.schema.d.ts +1313 -20
- package/dist/schema/spectrum.schema.d.ts.map +1 -1
- package/dist/schema/spectrum.schema.js +72 -0
- package/dist/schema/spectrum.schema.js.map +1 -1
- package/dist/schema/transmission.schema.d.ts +7 -0
- package/dist/schema/transmission.schema.d.ts.map +1 -1
- package/dist/schema/tx-audio-input.schema.d.ts +8 -0
- package/dist/schema/tx-audio-input.schema.d.ts.map +1 -0
- package/dist/schema/tx-audio-input.schema.js +19 -0
- package/dist/schema/tx-audio-input.schema.js.map +1 -0
- package/dist/schema/voice.schema.d.ts +1 -1
- package/dist/schema/voice.schema.d.ts.map +1 -1
- package/dist/schema/voice.schema.js +1 -1
- package/dist/schema/voice.schema.js.map +1 -1
- package/dist/schema/websocket.schema.d.ts +27480 -2525
- package/dist/schema/websocket.schema.d.ts.map +1 -1
- package/dist/schema/websocket.schema.js +298 -11
- package/dist/schema/websocket.schema.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +4 -0
- package/src/schema/__tests__/assisted-queue.schema.test.ts +1 -0
- package/src/schema/__tests__/meter.schema.test.ts +28 -0
- package/src/schema/__tests__/operator-context-report.schema.test.ts +77 -0
- package/src/schema/__tests__/plugin-market.schema.test.ts +116 -2
- package/src/schema/__tests__/qso.schema.test.ts +91 -0
- package/src/schema/__tests__/radio-power-support.test.ts +4 -1
- package/src/schema/__tests__/radio.schema.test.ts +60 -1
- package/src/schema/__tests__/remote-access.schema.test.ts +1 -1
- package/src/schema/__tests__/spectrum.schema.test.ts +119 -0
- package/src/schema/__tests__/strategy-transmission.schema.test.ts +106 -0
- package/src/schema/api-response.schema.ts +11 -0
- package/src/schema/image-radio.schema.ts +342 -0
- package/src/schema/logbook.schema.ts +1 -0
- package/src/schema/mode.schema.ts +16 -0
- package/src/schema/operator.schema.ts +4 -19
- package/src/schema/plugin.schema.ts +119 -13
- package/src/schema/qso.schema.ts +71 -0
- package/src/schema/radio-capability.schema.ts +1 -0
- package/src/schema/radio-profile.schema.ts +7 -0
- package/src/schema/radio.schema.ts +35 -2
- package/src/schema/slot-info.schema.ts +2 -1
- package/src/schema/spectrum-config.schema.ts +156 -0
- package/src/schema/spectrum.schema.ts +89 -0
- package/src/schema/tx-audio-input.schema.ts +22 -0
- package/src/schema/voice.schema.ts +1 -1
- package/src/schema/websocket.schema.ts +383 -12
- package/test/image-radio-schema.test.ts +107 -0
- package/test/plugin-slot-schema.test.ts +31 -0
- package/CLAUDE.md +0 -41
package/src/schema/qso.schema.ts
CHANGED
|
@@ -49,6 +49,61 @@ export const SubdivisionConfidenceSchema = z.enum([
|
|
|
49
49
|
'low',
|
|
50
50
|
]);
|
|
51
51
|
|
|
52
|
+
/** Maximum UTF-8 JSON size of contest-owned data persisted with one QSO. */
|
|
53
|
+
export const CONTEST_QSO_ENVELOPE_MAX_BYTES = 8 * 1024;
|
|
54
|
+
|
|
55
|
+
const ContestQsoExchangeSchema = z.record(z.string(), z.string());
|
|
56
|
+
const ContestQsoAnnotationValueSchema = z.union([z.string(), z.number().finite(), z.boolean()]);
|
|
57
|
+
|
|
58
|
+
function durableContestQsoJson(value: unknown): string {
|
|
59
|
+
return JSON.stringify(value).replace(/[<>\u007f-\uffff]/g, character => (
|
|
60
|
+
`\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`
|
|
61
|
+
));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Versioned contest facts that must be committed atomically with their QSO.
|
|
66
|
+
*
|
|
67
|
+
* The shape is deliberately shallow and bounded. Contest plugins may choose
|
|
68
|
+
* their exchange and annotation keys, but cannot persist arbitrary object
|
|
69
|
+
* graphs or binary payloads in the logbook record.
|
|
70
|
+
*/
|
|
71
|
+
export const ContestQsoEnvelopeSchema = z.object({
|
|
72
|
+
schemaVersion: z.literal(1),
|
|
73
|
+
contestId: z.string().min(1),
|
|
74
|
+
editionId: z.string().min(1),
|
|
75
|
+
rulesetVersion: z.string().min(1),
|
|
76
|
+
sent: ContestQsoExchangeSchema,
|
|
77
|
+
received: ContestQsoExchangeSchema,
|
|
78
|
+
annotations: z.record(z.string(), ContestQsoAnnotationValueSchema).optional(),
|
|
79
|
+
}).strict().superRefine((value, context) => {
|
|
80
|
+
if (new TextEncoder().encode(durableContestQsoJson(value)).byteLength > CONTEST_QSO_ENVELOPE_MAX_BYTES) {
|
|
81
|
+
context.addIssue({
|
|
82
|
+
code: z.ZodIssueCode.custom,
|
|
83
|
+
message: `Contest QSO envelope must not exceed ${CONTEST_QSO_ENVELOPE_MAX_BYTES} UTF-8 JSON bytes`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Serialize a validated contest envelope as ASCII-only JSON for durable ADIF
|
|
90
|
+
* private fields. Escaping non-ASCII and angle brackets keeps legacy string
|
|
91
|
+
* ADIF readers byte-safe without changing the decoded data.
|
|
92
|
+
*/
|
|
93
|
+
export function serializeContestQsoEnvelope(value: ContestQsoEnvelope): string {
|
|
94
|
+
return durableContestQsoJson(ContestQsoEnvelopeSchema.parse(value));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Parse and validate an envelope read from a durable private field. */
|
|
98
|
+
export function parseContestQsoEnvelope(value: string): ContestQsoEnvelope | undefined {
|
|
99
|
+
try {
|
|
100
|
+
const parsed = ContestQsoEnvelopeSchema.safeParse(JSON.parse(value));
|
|
101
|
+
return parsed.success ? parsed.data : undefined;
|
|
102
|
+
} catch {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
52
107
|
// 操作配置
|
|
53
108
|
export const OperatorConfigSchema = z.object({
|
|
54
109
|
id: z.string(),
|
|
@@ -60,6 +115,7 @@ export const OperatorConfigSchema = z.object({
|
|
|
60
115
|
maxQSOTimeoutCycles: z.number(),
|
|
61
116
|
maxCallAttempts: z.number(),
|
|
62
117
|
frequency: z.number(),
|
|
118
|
+
maxConcurrentStreams: z.number().int().min(1).max(5).optional(),
|
|
63
119
|
autoResumeCQAfterFail: z.boolean().default(false),
|
|
64
120
|
autoResumeCQAfterSuccess: z.boolean().default(false),
|
|
65
121
|
replyToWorkedStations: z.boolean().default(false),
|
|
@@ -112,6 +168,8 @@ export const QSORecordSchema = z.object({
|
|
|
112
168
|
reportReceived: z.string().optional(), // 接收的信号报告
|
|
113
169
|
messageHistory: z.array(z.string()), // 内部消息历史(FT8 等数字模式)
|
|
114
170
|
comment: z.string().optional(), // 标准 ADIF COMMENT 字段
|
|
171
|
+
contestId: z.string().optional(), // ADIF CONTEST_ID
|
|
172
|
+
contestEntry: ContestQsoEnvelopeSchema.optional(), // TX-5DR atomic contest facts
|
|
115
173
|
myCallsign: z.string().optional(), // 我的呼号(操作员呼号)
|
|
116
174
|
myGrid: z.string().optional(), // 我的网格定位(操作员网格)
|
|
117
175
|
qth: z.string().optional(), // 对方 QTH(地点,语音通联常用)
|
|
@@ -148,8 +206,19 @@ export const QSORecordSchema = z.object({
|
|
|
148
206
|
|
|
149
207
|
// 附注(对应 ADIF NOTES 字段)
|
|
150
208
|
notes: z.string().optional(),
|
|
209
|
+
}).superRefine((value, context) => {
|
|
210
|
+
if (value.contestId && value.contestEntry && value.contestId !== value.contestEntry.contestId) {
|
|
211
|
+
context.addIssue({
|
|
212
|
+
code: z.ZodIssueCode.custom,
|
|
213
|
+
path: ['contestEntry', 'contestId'],
|
|
214
|
+
message: 'contestEntry.contestId must match contestId',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
151
217
|
});
|
|
152
218
|
|
|
219
|
+
/** Host persistence behavior for an automatically completed QSO. */
|
|
220
|
+
export const QSOPersistencePolicySchema = z.enum(['merge-nearby', 'preserve-distinct']);
|
|
221
|
+
|
|
153
222
|
// 策略执行结果
|
|
154
223
|
export const StrategiesResultSchema = z.object({
|
|
155
224
|
stop: z.boolean().optional(),
|
|
@@ -169,6 +238,7 @@ export type DxccStatus = z.infer<typeof DxccStatusSchema>;
|
|
|
169
238
|
export type DxccSource = z.infer<typeof DxccSourceSchema>;
|
|
170
239
|
export type DxccConfidence = z.infer<typeof DxccConfidenceSchema>;
|
|
171
240
|
export type QSOContext = z.infer<typeof QSOContextSchema>;
|
|
241
|
+
export type ContestQsoEnvelope = z.infer<typeof ContestQsoEnvelopeSchema>;
|
|
172
242
|
|
|
173
243
|
/**
|
|
174
244
|
* Canonical persisted QSO record used by TX-5DR logbooks and plugin hooks.
|
|
@@ -177,4 +247,5 @@ export type QSOContext = z.infer<typeof QSOContextSchema>;
|
|
|
177
247
|
* logbook queries.
|
|
178
248
|
*/
|
|
179
249
|
export type QSORecord = z.infer<typeof QSORecordSchema>;
|
|
250
|
+
export type QSOPersistencePolicy = z.infer<typeof QSOPersistencePolicySchema>;
|
|
180
251
|
export type StrategiesResult = z.infer<typeof StrategiesResultSchema>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { HamlibConfigSchema } from './radio.schema.js';
|
|
3
3
|
import { AudioDeviceSettingsSchema } from './audio.schema.js';
|
|
4
|
+
import { TxAudioInputSourcePolicySchema } from './tx-audio-input.schema.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Radio Profile Schema
|
|
@@ -14,9 +15,13 @@ export const RadioProfileSchema = z.object({
|
|
|
14
15
|
radio: HamlibConfigSchema, // 电台连接配置
|
|
15
16
|
audio: AudioDeviceSettingsSchema, // 音频设备配置
|
|
16
17
|
audioLockedToRadio: z.boolean(), // ICOM WLAN = true,音频由电台决定
|
|
18
|
+
/** Optional policy for the radio's physical TX audio input selector. */
|
|
19
|
+
txAudioInputSource: TxAudioInputSourcePolicySchema.optional(),
|
|
17
20
|
createdAt: z.number(),
|
|
18
21
|
updatedAt: z.number(),
|
|
19
22
|
description: z.string().optional(),
|
|
23
|
+
isVirtual: z.boolean().optional(),
|
|
24
|
+
readOnly: z.boolean().optional(),
|
|
20
25
|
});
|
|
21
26
|
|
|
22
27
|
export type RadioProfile = z.infer<typeof RadioProfileSchema>;
|
|
@@ -41,6 +46,7 @@ export const UpdateProfileRequestSchema = z.object({
|
|
|
41
46
|
radio: HamlibConfigSchema.optional(),
|
|
42
47
|
audio: AudioDeviceSettingsSchema.optional(),
|
|
43
48
|
audioLockedToRadio: z.boolean().optional(),
|
|
49
|
+
txAudioInputSource: TxAudioInputSourcePolicySchema.optional(),
|
|
44
50
|
description: z.string().optional(),
|
|
45
51
|
});
|
|
46
52
|
|
|
@@ -52,6 +58,7 @@ export type UpdateProfileRequest = z.infer<typeof UpdateProfileRequestSchema>;
|
|
|
52
58
|
export const ProfileListResponseSchema = z.object({
|
|
53
59
|
profiles: z.array(RadioProfileSchema),
|
|
54
60
|
activeProfileId: z.string().nullable(),
|
|
61
|
+
hasConfiguredProfiles: z.boolean().optional().default(false),
|
|
55
62
|
});
|
|
56
63
|
|
|
57
64
|
export type ProfileListResponse = z.infer<typeof ProfileListResponseSchema>;
|
|
@@ -14,15 +14,21 @@ export const ToneSquelchModeSchema = z.enum(['none', 'ctcss', 'dcs']);
|
|
|
14
14
|
/** Saved dial-frequency preset with band, digital mode and radio modulation. */
|
|
15
15
|
export const PresetFrequencySchema = z.object({
|
|
16
16
|
band: z.string(),
|
|
17
|
-
mode: z.string(), // 协议模式,如 FT8
|
|
17
|
+
mode: z.string(), // 协议模式,如 FT8、FT4、VOICE、SSTV、FAX
|
|
18
18
|
radioMode: z.string().optional(), // 电台调制模式,如 USB, LSB, AM, FM
|
|
19
|
-
frequency: z.number(),
|
|
19
|
+
frequency: z.number().int().positive().max(1_000_000_000), // 拨盘频率,单位 Hz(MHz 输入最多 6 位小数)
|
|
20
20
|
description: z.string().optional(),
|
|
21
21
|
repeaterShift: RepeaterShiftSchema.optional(), // 中继差频方向,默认 none
|
|
22
22
|
repeaterOffsetHz: z.number().int().positive().optional(), // 中继偏移,单位 Hz
|
|
23
23
|
toneMode: ToneSquelchModeSchema.optional(), // 哑音类型,默认 none
|
|
24
24
|
ctcssToneTenthsHz: z.number().int().positive().optional(), // CTCSS,单位 0.1Hz
|
|
25
25
|
dcsCode: z.number().int().positive().optional(), // DCS 码
|
|
26
|
+
region: z.enum(['global', 'iaru1', 'iaru2', 'iaru3']).optional(),
|
|
27
|
+
imagePurpose: z.enum(['activity', 'iss', 'weatherfax']).optional(),
|
|
28
|
+
audioCenterHz: z.number().int().positive().optional(),
|
|
29
|
+
assignedFrequency: z.number().int().positive().max(1_000_000_000).optional(),
|
|
30
|
+
faxEmission: z.enum(['J3C', 'F3C', 'F1C']).optional(),
|
|
31
|
+
carrierFrequency: z.number().positive().optional(), // Legacy field; use assignedFrequency for published WEFAX channels.
|
|
26
32
|
}).superRefine((preset, ctx) => {
|
|
27
33
|
const isVoiceFmPreset = preset.mode === 'VOICE' && preset.radioMode?.toUpperCase() === 'FM';
|
|
28
34
|
const hasRepeaterDuplex = preset.repeaterShift === 'minus' || preset.repeaterShift === 'plus';
|
|
@@ -195,9 +201,27 @@ export const IcomWlanConfigSchema = z.object({
|
|
|
195
201
|
/**
|
|
196
202
|
* TCI / SunSDR 连接配置Schema
|
|
197
203
|
*/
|
|
204
|
+
const TciWebSocketUrlSchema = z.string().url()
|
|
205
|
+
.refine((value) => /^wss?:\/\//i.test(value), 'TCI URL must use ws:// or wss://')
|
|
206
|
+
.refine((value) => {
|
|
207
|
+
const parsed = new URL(value);
|
|
208
|
+
return !parsed.username && !parsed.password;
|
|
209
|
+
}, 'TCI URL must not contain credentials');
|
|
210
|
+
|
|
198
211
|
export const TciConfigSchema = z.object({
|
|
199
212
|
host: z.string().default('127.0.0.1'),
|
|
200
213
|
port: z.number().int().min(1).max(65535).default(40001),
|
|
214
|
+
url: TciWebSocketUrlSchema.optional(),
|
|
215
|
+
dialect: z.enum([
|
|
216
|
+
'auto',
|
|
217
|
+
'expertsdr-1.4',
|
|
218
|
+
'expertsdr-1.5-1.8',
|
|
219
|
+
'expertsdr-1.9-2.0',
|
|
220
|
+
'aethersdr-1.5',
|
|
221
|
+
'thetis-2.0',
|
|
222
|
+
'generic-observed',
|
|
223
|
+
]).default('auto'),
|
|
224
|
+
autoDiscoverPorts: z.boolean().default(true),
|
|
201
225
|
receiver: z.number().int().min(0).default(0),
|
|
202
226
|
trx: z.number().int().min(0).default(0),
|
|
203
227
|
vfo: z.number().int().min(0).default(0),
|
|
@@ -384,6 +408,7 @@ export const SerialPortsResponseSchema = z.object({
|
|
|
384
408
|
export const TestResponseSchema = z.object({
|
|
385
409
|
success: z.boolean(),
|
|
386
410
|
message: z.string().optional(),
|
|
411
|
+
details: z.record(z.unknown()).optional(),
|
|
387
412
|
});
|
|
388
413
|
|
|
389
414
|
/**
|
|
@@ -497,6 +522,14 @@ export const CustomFrequencyPresetsSchema = z.object({
|
|
|
497
522
|
presets: z.array(PresetFrequencySchema).min(1),
|
|
498
523
|
});
|
|
499
524
|
|
|
525
|
+
/** TCI receiver DDS/IQ center-frequency control request. */
|
|
526
|
+
export const DdsFrequencyRequestSchema = z.object({
|
|
527
|
+
frequency: z.number().finite().nonnegative(),
|
|
528
|
+
receiver: z.number().int().nonnegative().optional(),
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
export type DdsFrequencyRequest = z.infer<typeof DdsFrequencyRequestSchema>;
|
|
532
|
+
|
|
500
533
|
// 导出类型
|
|
501
534
|
export type CustomFrequencyPresets = z.infer<typeof CustomFrequencyPresetsSchema>;
|
|
502
535
|
export type PresetFrequency = z.infer<typeof PresetFrequencySchema>;
|
|
@@ -96,7 +96,8 @@ export const FrameMessageSchema = z.object({
|
|
|
96
96
|
/** 基于日志本的分析结果(可选,仅在客户端定制化数据中提供) */
|
|
97
97
|
logbookAnalysis: LogbookAnalysisSchema.optional(),
|
|
98
98
|
/** 操作员ID(可选,仅 TX 帧使用,用于多操作员覆盖识别) */
|
|
99
|
-
operatorId: z.string().optional()
|
|
99
|
+
operatorId: z.string().optional(),
|
|
100
|
+
streamId: z.string().optional(),
|
|
100
101
|
});
|
|
101
102
|
|
|
102
103
|
export type FrameMessage = z.infer<typeof FrameMessageSchema>;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const SpectrumPresetIdSchema = z.enum(['responsive', 'balanced', 'fine']);
|
|
4
|
+
export const SpectrumPresetSchema = z.enum(['responsive', 'balanced', 'fine', 'custom']);
|
|
5
|
+
export type SpectrumPreset = z.infer<typeof SpectrumPresetSchema>;
|
|
6
|
+
export type SpectrumPresetId = z.infer<typeof SpectrumPresetIdSchema>;
|
|
7
|
+
|
|
8
|
+
export const SpectrumWindowFunctionSchema = z.enum([
|
|
9
|
+
'hann',
|
|
10
|
+
'hamming',
|
|
11
|
+
'blackman',
|
|
12
|
+
'blackmanHarris',
|
|
13
|
+
'none',
|
|
14
|
+
]);
|
|
15
|
+
export type SpectrumWindowFunction = z.infer<typeof SpectrumWindowFunctionSchema>;
|
|
16
|
+
|
|
17
|
+
export const SpectrumFftSizeSchema = z.union([
|
|
18
|
+
z.literal(1024),
|
|
19
|
+
z.literal(2048),
|
|
20
|
+
z.literal(4096),
|
|
21
|
+
z.literal(8192),
|
|
22
|
+
z.literal(16384),
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
export const SpectrumTargetSampleRateSchema = z.union([
|
|
26
|
+
z.literal(3000),
|
|
27
|
+
z.literal(4000),
|
|
28
|
+
z.literal(6000),
|
|
29
|
+
z.literal(8000),
|
|
30
|
+
z.literal(12000),
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
export const SpectrumCustomSettingsSchema = z.object({
|
|
34
|
+
analysisIntervalMs: z.number().int().min(50).max(1000),
|
|
35
|
+
fftSize: SpectrumFftSizeSchema,
|
|
36
|
+
targetSampleRate: SpectrumTargetSampleRateSchema,
|
|
37
|
+
windowFunction: SpectrumWindowFunctionSchema,
|
|
38
|
+
haloReduce: z.boolean(),
|
|
39
|
+
});
|
|
40
|
+
export type SpectrumCustomSettings = z.infer<typeof SpectrumCustomSettingsSchema>;
|
|
41
|
+
|
|
42
|
+
export const SpectrumRenderFrequencyRangeSchema = z.object({
|
|
43
|
+
min: z.number().finite(),
|
|
44
|
+
max: z.number().finite(),
|
|
45
|
+
}).refine((range) => range.max > range.min, {
|
|
46
|
+
message: 'Spectrum frequency range maximum must be greater than minimum',
|
|
47
|
+
path: ['max'],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const SpectrumRenderConfigSchema = z.object({
|
|
51
|
+
preset: SpectrumPresetSchema,
|
|
52
|
+
revision: z.number().int().nonnegative(),
|
|
53
|
+
analysisIntervalMs: z.number().int().positive(),
|
|
54
|
+
frameRateHz: z.number().positive(),
|
|
55
|
+
fftSize: z.number().int().positive(),
|
|
56
|
+
targetSampleRate: z.number().int().positive(),
|
|
57
|
+
fftWindowDurationMs: z.number().positive(),
|
|
58
|
+
frequencyResolutionHz: z.number().positive(),
|
|
59
|
+
frequencyRange: SpectrumRenderFrequencyRangeSchema,
|
|
60
|
+
displayBinCount: z.number().int().positive(),
|
|
61
|
+
windowFunction: SpectrumWindowFunctionSchema,
|
|
62
|
+
haloReduce: z.boolean(),
|
|
63
|
+
customSettings: SpectrumCustomSettingsSchema.optional(),
|
|
64
|
+
});
|
|
65
|
+
export type SpectrumRenderConfig = z.infer<typeof SpectrumRenderConfigSchema>;
|
|
66
|
+
|
|
67
|
+
export const TciIqSampleRateSchema = z.number().int().positive();
|
|
68
|
+
export type TciIqSampleRate = z.infer<typeof TciIqSampleRateSchema>;
|
|
69
|
+
|
|
70
|
+
export const TciSpectrumSettingsSchema = z.object({
|
|
71
|
+
fftSize: z.union([
|
|
72
|
+
z.literal(4096), z.literal(8192), z.literal(16384), z.literal(32768), z.literal(65536),
|
|
73
|
+
]),
|
|
74
|
+
displayBinCount: z.number().int().min(1024).max(16384),
|
|
75
|
+
analysisIntervalMs: z.number().int().min(20).max(1000),
|
|
76
|
+
});
|
|
77
|
+
export type TciSpectrumSettings = z.infer<typeof TciSpectrumSettingsSchema>;
|
|
78
|
+
|
|
79
|
+
export const TciSpectrumSettingsResponseSchema = z.object({
|
|
80
|
+
success: z.boolean(),
|
|
81
|
+
settings: TciSpectrumSettingsSchema,
|
|
82
|
+
});
|
|
83
|
+
export type TciSpectrumSettingsResponse = z.infer<typeof TciSpectrumSettingsResponseSchema>;
|
|
84
|
+
|
|
85
|
+
export const TciIqSpectrumSettingsResponseSchema = z.object({
|
|
86
|
+
success: z.boolean(),
|
|
87
|
+
configuredSampleRate: TciIqSampleRateSchema.nullable(),
|
|
88
|
+
appliedSampleRate: TciIqSampleRateSchema.nullable(),
|
|
89
|
+
supportedSampleRates: z.array(TciIqSampleRateSchema),
|
|
90
|
+
});
|
|
91
|
+
export type TciIqSpectrumSettingsResponse = z.infer<typeof TciIqSpectrumSettingsResponseSchema>;
|
|
92
|
+
|
|
93
|
+
export const SpectrumPresetDefinitionSchema = SpectrumRenderConfigSchema.omit({
|
|
94
|
+
revision: true,
|
|
95
|
+
});
|
|
96
|
+
export type SpectrumPresetDefinition = z.infer<typeof SpectrumPresetDefinitionSchema>;
|
|
97
|
+
|
|
98
|
+
export const SPECTRUM_PRESET_DEFINITIONS: Record<SpectrumPresetId, SpectrumPresetDefinition> = {
|
|
99
|
+
responsive: {
|
|
100
|
+
preset: 'responsive',
|
|
101
|
+
analysisIntervalMs: 100,
|
|
102
|
+
frameRateHz: 10,
|
|
103
|
+
fftSize: 2048,
|
|
104
|
+
targetSampleRate: 6000,
|
|
105
|
+
fftWindowDurationMs: (2048 / 6000) * 1000,
|
|
106
|
+
frequencyResolutionHz: 6000 / 2048,
|
|
107
|
+
frequencyRange: { min: 0, max: 3000 },
|
|
108
|
+
displayBinCount: 1025,
|
|
109
|
+
windowFunction: 'blackmanHarris',
|
|
110
|
+
haloReduce: false,
|
|
111
|
+
},
|
|
112
|
+
balanced: {
|
|
113
|
+
preset: 'balanced',
|
|
114
|
+
analysisIntervalMs: 150,
|
|
115
|
+
frameRateHz: 1000 / 150,
|
|
116
|
+
fftSize: 8192,
|
|
117
|
+
targetSampleRate: 6000,
|
|
118
|
+
fftWindowDurationMs: (8192 / 6000) * 1000,
|
|
119
|
+
frequencyResolutionHz: 6000 / 8192,
|
|
120
|
+
frequencyRange: { min: 0, max: 3000 },
|
|
121
|
+
displayBinCount: 4097,
|
|
122
|
+
windowFunction: 'blackmanHarris',
|
|
123
|
+
haloReduce: false,
|
|
124
|
+
},
|
|
125
|
+
fine: {
|
|
126
|
+
preset: 'fine',
|
|
127
|
+
analysisIntervalMs: 200,
|
|
128
|
+
frameRateHz: 5,
|
|
129
|
+
fftSize: 16384,
|
|
130
|
+
targetSampleRate: 6000,
|
|
131
|
+
fftWindowDurationMs: (16384 / 6000) * 1000,
|
|
132
|
+
frequencyResolutionHz: 6000 / 16384,
|
|
133
|
+
frequencyRange: { min: 0, max: 3000 },
|
|
134
|
+
displayBinCount: 8193,
|
|
135
|
+
windowFunction: 'blackmanHarris',
|
|
136
|
+
haloReduce: false,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export function getSpectrumPresetDefinition(preset: SpectrumPresetId): SpectrumPresetDefinition {
|
|
141
|
+
return { ...SPECTRUM_PRESET_DEFINITIONS[preset] };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const SpectrumSettingsUpdateRequestSchema = z.discriminatedUnion('preset', [
|
|
145
|
+
z.object({ preset: SpectrumPresetIdSchema }),
|
|
146
|
+
z.object({ preset: z.literal('custom'), settings: SpectrumCustomSettingsSchema }),
|
|
147
|
+
]);
|
|
148
|
+
export type SpectrumSettingsUpdateRequest = z.infer<typeof SpectrumSettingsUpdateRequestSchema>;
|
|
149
|
+
|
|
150
|
+
export const SpectrumSettingsResponseSchema = z.object({
|
|
151
|
+
success: z.boolean(),
|
|
152
|
+
message: z.string().optional(),
|
|
153
|
+
currentSettings: SpectrumRenderConfigSchema,
|
|
154
|
+
presets: z.array(SpectrumPresetDefinitionSchema),
|
|
155
|
+
});
|
|
156
|
+
export type SpectrumSettingsResponse = z.infer<typeof SpectrumSettingsResponseSchema>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { SpectrumPresetDefinitionSchema, SpectrumRenderConfigSchema } from './spectrum-config.schema.js';
|
|
2
3
|
|
|
3
4
|
export const SpectrumKindSchema = z.enum(['audio', 'radio-sdr', 'openwebrx-sdr']);
|
|
4
5
|
export type SpectrumKind = z.infer<typeof SpectrumKindSchema>;
|
|
@@ -26,10 +27,31 @@ export const SpectrumSessionFrequencyRangeModeSchema = z.enum([
|
|
|
26
27
|
]);
|
|
27
28
|
export type SpectrumSessionFrequencyRangeMode = z.infer<typeof SpectrumSessionFrequencyRangeModeSchema>;
|
|
28
29
|
|
|
30
|
+
export const SpectrumSessionViewModeSchema = z.enum(['wide', 'radio-center']);
|
|
31
|
+
export type SpectrumSessionViewMode = z.infer<typeof SpectrumSessionViewModeSchema>;
|
|
32
|
+
|
|
29
33
|
export const SpectrumFrequencyRangeSchema = z.object({
|
|
30
34
|
min: z.number(),
|
|
31
35
|
max: z.number(),
|
|
32
36
|
});
|
|
37
|
+
export const SpectrumViewportSchema = z.object({
|
|
38
|
+
min: z.number().finite(),
|
|
39
|
+
max: z.number().finite(),
|
|
40
|
+
displayBinCount: z.number().int().positive().max(16384),
|
|
41
|
+
}).refine((range) => range.max > range.min, {
|
|
42
|
+
message: 'Spectrum viewport maximum must be greater than minimum',
|
|
43
|
+
path: ['max'],
|
|
44
|
+
});
|
|
45
|
+
export type SpectrumViewport = z.infer<typeof SpectrumViewportSchema>;
|
|
46
|
+
|
|
47
|
+
export const SpectrumSessionViewportInteractionSchema = z.object({
|
|
48
|
+
enabled: z.boolean(),
|
|
49
|
+
canZoom: z.boolean(),
|
|
50
|
+
canPan: z.boolean(),
|
|
51
|
+
canTuneAtEdge: z.boolean(),
|
|
52
|
+
bounds: SpectrumFrequencyRangeSchema.nullable(),
|
|
53
|
+
});
|
|
54
|
+
export type SpectrumSessionViewportInteraction = z.infer<typeof SpectrumSessionViewportInteractionSchema>;
|
|
33
55
|
|
|
34
56
|
export const SpectrumBinaryFormatSchema = z.object({
|
|
35
57
|
type: z.literal('int16'),
|
|
@@ -43,13 +65,53 @@ export const SpectrumBinaryDataSchema = z.object({
|
|
|
43
65
|
format: SpectrumBinaryFormatSchema,
|
|
44
66
|
});
|
|
45
67
|
|
|
68
|
+
export const SpectrumLevelDomainSchema = z.enum(['dbfs', 'calibrated-db', 'raw']);
|
|
69
|
+
export type SpectrumLevelDomain = z.infer<typeof SpectrumLevelDomainSchema>;
|
|
70
|
+
|
|
71
|
+
export const SpectrumLevelUnitSchema = z.enum(['dBFS', 'dB', 'Level']);
|
|
72
|
+
export type SpectrumLevelUnit = z.infer<typeof SpectrumLevelUnitSchema>;
|
|
73
|
+
|
|
74
|
+
export const SpectrumLevelReferenceSchema = z.enum(['full-scale', 'device', 'none']);
|
|
75
|
+
export type SpectrumLevelReference = z.infer<typeof SpectrumLevelReferenceSchema>;
|
|
76
|
+
|
|
77
|
+
export const SpectrumLevelDescriptorSchema = z.object({
|
|
78
|
+
domain: SpectrumLevelDomainSchema,
|
|
79
|
+
unit: SpectrumLevelUnitSchema,
|
|
80
|
+
reference: SpectrumLevelReferenceSchema,
|
|
81
|
+
calibrated: z.boolean(),
|
|
82
|
+
min: z.number().finite(),
|
|
83
|
+
max: z.number().finite(),
|
|
84
|
+
}).refine((level) => level.max > level.min, {
|
|
85
|
+
message: 'Spectrum level maximum must be greater than minimum',
|
|
86
|
+
path: ['max'],
|
|
87
|
+
});
|
|
88
|
+
export type SpectrumLevelDescriptor = z.infer<typeof SpectrumLevelDescriptorSchema>;
|
|
89
|
+
|
|
90
|
+
/** A lower-resolution, wider-range representation derived from the same FFT. */
|
|
91
|
+
export const SpectrumFrameSupplementSchema = z.object({
|
|
92
|
+
frequencyRange: SpectrumFrequencyRangeSchema,
|
|
93
|
+
binaryData: SpectrumBinaryDataSchema,
|
|
94
|
+
meta: z.object({
|
|
95
|
+
sourceBinCount: z.number().int().positive(),
|
|
96
|
+
displayBinCount: z.number().int().positive(),
|
|
97
|
+
centerFrequency: z.number().optional(),
|
|
98
|
+
spanHz: z.number().optional(),
|
|
99
|
+
level: SpectrumLevelDescriptorSchema.optional(),
|
|
100
|
+
}),
|
|
101
|
+
});
|
|
102
|
+
export type SpectrumFrameSupplement = z.infer<typeof SpectrumFrameSupplementSchema>;
|
|
103
|
+
|
|
46
104
|
export const SpectrumFrameMetaSchema = z.object({
|
|
47
105
|
sourceBinCount: z.number().int().positive(),
|
|
48
106
|
displayBinCount: z.number().int().positive(),
|
|
107
|
+
/** Original source range before a per-client server-side projection. */
|
|
108
|
+
nativeFrequencyRange: SpectrumFrequencyRangeSchema.optional(),
|
|
49
109
|
centerFrequency: z.number().optional(),
|
|
50
110
|
spanHz: z.number().optional(),
|
|
51
111
|
profileId: z.string().nullable().optional(),
|
|
52
112
|
radioModel: z.string().optional(),
|
|
113
|
+
level: SpectrumLevelDescriptorSchema.optional(),
|
|
114
|
+
spectrumConfigRevision: z.number().int().nonnegative().optional(),
|
|
53
115
|
});
|
|
54
116
|
|
|
55
117
|
export const SpectrumFrameSchema = z.object({
|
|
@@ -58,6 +120,7 @@ export const SpectrumFrameSchema = z.object({
|
|
|
58
120
|
frequencyRange: SpectrumFrequencyRangeSchema,
|
|
59
121
|
binaryData: SpectrumBinaryDataSchema,
|
|
60
122
|
meta: SpectrumFrameMetaSchema,
|
|
123
|
+
supplement: SpectrumFrameSupplementSchema.optional(),
|
|
61
124
|
});
|
|
62
125
|
|
|
63
126
|
export const SpectrumSourceAvailabilitySchema = z.object({
|
|
@@ -76,6 +139,8 @@ export const SpectrumCapabilitiesSchema = z.object({
|
|
|
76
139
|
profileId: z.string().nullable(),
|
|
77
140
|
defaultKind: SpectrumKindSchema,
|
|
78
141
|
sources: z.array(SpectrumSourceAvailabilitySchema),
|
|
142
|
+
renderConfig: SpectrumRenderConfigSchema,
|
|
143
|
+
presets: z.array(SpectrumPresetDefinitionSchema),
|
|
79
144
|
});
|
|
80
145
|
|
|
81
146
|
export const SpectrumSessionControlIdSchema = z.enum([
|
|
@@ -120,7 +185,30 @@ export const SpectrumSessionPresetMarkerSchema = z.object({
|
|
|
120
185
|
});
|
|
121
186
|
export type SpectrumSessionPresetMarker = z.infer<typeof SpectrumSessionPresetMarkerSchema>;
|
|
122
187
|
|
|
188
|
+
export const SpectrumSessionFrequencyOverlaySchema = z.object({
|
|
189
|
+
id: z.string(),
|
|
190
|
+
label: z.string(),
|
|
191
|
+
lineFrequency: z.number().finite(),
|
|
192
|
+
rangeStartFrequency: z.number().finite(),
|
|
193
|
+
rangeEndFrequency: z.number().finite(),
|
|
194
|
+
variant: z.enum(['tx', 'rx', 'window']),
|
|
195
|
+
draggable: z.boolean(),
|
|
196
|
+
frequencyTarget: z.enum(['radio-frequency', 'operator-tx', 'split-frequency']).nullable(),
|
|
197
|
+
}).refine((overlay) => overlay.rangeEndFrequency >= overlay.rangeStartFrequency, {
|
|
198
|
+
message: 'Spectrum frequency overlay range is invalid',
|
|
199
|
+
path: ['rangeEndFrequency'],
|
|
200
|
+
});
|
|
201
|
+
export type SpectrumSessionFrequencyOverlay = z.infer<typeof SpectrumSessionFrequencyOverlaySchema>;
|
|
202
|
+
|
|
123
203
|
export const SpectrumSessionInteractionStateSchema = z.object({
|
|
204
|
+
viewMode: SpectrumSessionViewModeSchema.default('radio-center'),
|
|
205
|
+
viewport: SpectrumSessionViewportInteractionSchema.default({
|
|
206
|
+
enabled: false,
|
|
207
|
+
canZoom: false,
|
|
208
|
+
canPan: false,
|
|
209
|
+
canTuneAtEdge: false,
|
|
210
|
+
bounds: null,
|
|
211
|
+
}),
|
|
124
212
|
showTxMarkers: z.boolean(),
|
|
125
213
|
showRxMarkers: z.boolean(),
|
|
126
214
|
canDragTx: z.boolean(),
|
|
@@ -130,6 +218,7 @@ export const SpectrumSessionInteractionStateSchema = z.object({
|
|
|
130
218
|
frequencyGestureTarget: z.enum(['operator-tx', 'radio-frequency']).nullable(),
|
|
131
219
|
frequencyStepHz: z.number().int().positive().nullable(),
|
|
132
220
|
presetMarkers: z.array(SpectrumSessionPresetMarkerSchema),
|
|
221
|
+
frequencyOverlays: z.array(SpectrumSessionFrequencyOverlaySchema),
|
|
133
222
|
canDragVoiceOverlay: z.boolean(),
|
|
134
223
|
showVoiceOverlay: z.boolean(),
|
|
135
224
|
canLocalViewportZoom: z.boolean(),
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/** Normalized TX audio input routes used by the capability/profile layers. */
|
|
4
|
+
export const TxAudioInputSourceSchema = z.enum([
|
|
5
|
+
'mic',
|
|
6
|
+
'usb',
|
|
7
|
+
'network',
|
|
8
|
+
'accessory',
|
|
9
|
+
'line',
|
|
10
|
+
'spdif',
|
|
11
|
+
'mic+usb',
|
|
12
|
+
'mic+accessory',
|
|
13
|
+
]);
|
|
14
|
+
export type TxAudioInputSource = z.infer<typeof TxAudioInputSourceSchema>;
|
|
15
|
+
|
|
16
|
+
/** Profile policy. `auto` is resolved by the connection/provider. */
|
|
17
|
+
export const TxAudioInputSourcePolicySchema = z.enum([
|
|
18
|
+
'auto',
|
|
19
|
+
'unchanged',
|
|
20
|
+
...TxAudioInputSourceSchema.options,
|
|
21
|
+
]);
|
|
22
|
+
export type TxAudioInputSourcePolicy = z.infer<typeof TxAudioInputSourcePolicySchema>;
|
|
@@ -24,5 +24,5 @@ export type VoicePTTLock = z.infer<typeof VoicePTTLockSchema>;
|
|
|
24
24
|
* digital: FT8/FT4 等数字模式
|
|
25
25
|
* voice: 语音通联模式(SSB/FM/AM)
|
|
26
26
|
*/
|
|
27
|
-
export const EngineModeSchema = z.enum(['digital', 'voice', 'cw']);
|
|
27
|
+
export const EngineModeSchema = z.enum(['digital', 'voice', 'cw', 'image']);
|
|
28
28
|
export type EngineMode = z.infer<typeof EngineModeSchema>;
|