@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.
Files changed (138) hide show
  1. package/dist/index.d.ts +4 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +3 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/schema/__tests__/assisted-queue.schema.test.js +1 -0
  6. package/dist/schema/__tests__/assisted-queue.schema.test.js.map +1 -1
  7. package/dist/schema/__tests__/meter.schema.test.d.ts +2 -0
  8. package/dist/schema/__tests__/meter.schema.test.d.ts.map +1 -0
  9. package/dist/schema/__tests__/meter.schema.test.js +27 -0
  10. package/dist/schema/__tests__/meter.schema.test.js.map +1 -0
  11. package/dist/schema/__tests__/operator-context-report.schema.test.d.ts +2 -0
  12. package/dist/schema/__tests__/operator-context-report.schema.test.d.ts.map +1 -0
  13. package/dist/schema/__tests__/operator-context-report.schema.test.js +70 -0
  14. package/dist/schema/__tests__/operator-context-report.schema.test.js.map +1 -0
  15. package/dist/schema/__tests__/plugin-market.schema.test.js +107 -3
  16. package/dist/schema/__tests__/plugin-market.schema.test.js.map +1 -1
  17. package/dist/schema/__tests__/qso.schema.test.d.ts +2 -0
  18. package/dist/schema/__tests__/qso.schema.test.d.ts.map +1 -0
  19. package/dist/schema/__tests__/qso.schema.test.js +70 -0
  20. package/dist/schema/__tests__/qso.schema.test.js.map +1 -0
  21. package/dist/schema/__tests__/radio-power-support.test.js +4 -1
  22. package/dist/schema/__tests__/radio-power-support.test.js.map +1 -1
  23. package/dist/schema/__tests__/radio.schema.test.js +52 -1
  24. package/dist/schema/__tests__/radio.schema.test.js.map +1 -1
  25. package/dist/schema/__tests__/remote-access.schema.test.js +1 -1
  26. package/dist/schema/__tests__/remote-access.schema.test.js.map +1 -1
  27. package/dist/schema/__tests__/spectrum.schema.test.d.ts +2 -0
  28. package/dist/schema/__tests__/spectrum.schema.test.d.ts.map +1 -0
  29. package/dist/schema/__tests__/spectrum.schema.test.js +107 -0
  30. package/dist/schema/__tests__/spectrum.schema.test.js.map +1 -0
  31. package/dist/schema/__tests__/strategy-transmission.schema.test.d.ts +2 -0
  32. package/dist/schema/__tests__/strategy-transmission.schema.test.d.ts.map +1 -0
  33. package/dist/schema/__tests__/strategy-transmission.schema.test.js +95 -0
  34. package/dist/schema/__tests__/strategy-transmission.schema.test.js.map +1 -0
  35. package/dist/schema/api-response.schema.d.ts +235 -0
  36. package/dist/schema/api-response.schema.d.ts.map +1 -1
  37. package/dist/schema/api-response.schema.js +9 -0
  38. package/dist/schema/api-response.schema.js.map +1 -1
  39. package/dist/schema/auth.schema.d.ts +2 -2
  40. package/dist/schema/cpu-profile.schema.d.ts +2 -2
  41. package/dist/schema/device-ui.schema.d.ts +10 -10
  42. package/dist/schema/image-radio.schema.d.ts +2597 -0
  43. package/dist/schema/image-radio.schema.d.ts.map +1 -0
  44. package/dist/schema/image-radio.schema.js +283 -0
  45. package/dist/schema/image-radio.schema.js.map +1 -0
  46. package/dist/schema/logbook.schema.d.ts +203 -15
  47. package/dist/schema/logbook.schema.d.ts.map +1 -1
  48. package/dist/schema/logbook.schema.js +1 -0
  49. package/dist/schema/logbook.schema.js.map +1 -1
  50. package/dist/schema/mode.schema.d.ts +2 -0
  51. package/dist/schema/mode.schema.d.ts.map +1 -1
  52. package/dist/schema/mode.schema.js +16 -0
  53. package/dist/schema/mode.schema.js.map +1 -1
  54. package/dist/schema/openwebrx.schema.d.ts +2 -2
  55. package/dist/schema/operator.schema.d.ts +2676 -0
  56. package/dist/schema/operator.schema.d.ts.map +1 -1
  57. package/dist/schema/operator.schema.js +4 -19
  58. package/dist/schema/operator.schema.js.map +1 -1
  59. package/dist/schema/plugin.schema.d.ts +1780 -475
  60. package/dist/schema/plugin.schema.d.ts.map +1 -1
  61. package/dist/schema/plugin.schema.js +108 -12
  62. package/dist/schema/plugin.schema.js.map +1 -1
  63. package/dist/schema/qso.schema.d.ts +239 -1
  64. package/dist/schema/qso.schema.d.ts.map +1 -1
  65. package/dist/schema/qso.schema.js +61 -0
  66. package/dist/schema/qso.schema.js.map +1 -1
  67. package/dist/schema/radio-capability.schema.d.ts +1 -1
  68. package/dist/schema/radio-capability.schema.d.ts.map +1 -1
  69. package/dist/schema/radio-capability.schema.js +1 -0
  70. package/dist/schema/radio-capability.schema.js.map +1 -1
  71. package/dist/schema/radio-power.schema.d.ts +15 -0
  72. package/dist/schema/radio-power.schema.d.ts.map +1 -1
  73. package/dist/schema/radio-profile.schema.d.ts +251 -0
  74. package/dist/schema/radio-profile.schema.d.ts.map +1 -1
  75. package/dist/schema/radio-profile.schema.js +7 -0
  76. package/dist/schema/radio-profile.schema.js.map +1 -1
  77. package/dist/schema/radio.schema.d.ts +174 -3
  78. package/dist/schema/radio.schema.d.ts.map +1 -1
  79. package/dist/schema/radio.schema.js +31 -2
  80. package/dist/schema/radio.schema.js.map +1 -1
  81. package/dist/schema/rigctld.schema.d.ts +6 -6
  82. package/dist/schema/slot-info.schema.d.ts +13 -0
  83. package/dist/schema/slot-info.schema.d.ts.map +1 -1
  84. package/dist/schema/slot-info.schema.js +2 -1
  85. package/dist/schema/slot-info.schema.js.map +1 -1
  86. package/dist/schema/spectrum-config.schema.d.ts +638 -0
  87. package/dist/schema/spectrum-config.schema.d.ts.map +1 -0
  88. package/dist/schema/spectrum-config.schema.js +129 -0
  89. package/dist/schema/spectrum-config.schema.js.map +1 -0
  90. package/dist/schema/spectrum.schema.d.ts +1313 -20
  91. package/dist/schema/spectrum.schema.d.ts.map +1 -1
  92. package/dist/schema/spectrum.schema.js +72 -0
  93. package/dist/schema/spectrum.schema.js.map +1 -1
  94. package/dist/schema/transmission.schema.d.ts +7 -0
  95. package/dist/schema/transmission.schema.d.ts.map +1 -1
  96. package/dist/schema/tx-audio-input.schema.d.ts +8 -0
  97. package/dist/schema/tx-audio-input.schema.d.ts.map +1 -0
  98. package/dist/schema/tx-audio-input.schema.js +19 -0
  99. package/dist/schema/tx-audio-input.schema.js.map +1 -0
  100. package/dist/schema/voice.schema.d.ts +1 -1
  101. package/dist/schema/voice.schema.d.ts.map +1 -1
  102. package/dist/schema/voice.schema.js +1 -1
  103. package/dist/schema/voice.schema.js.map +1 -1
  104. package/dist/schema/websocket.schema.d.ts +27480 -2525
  105. package/dist/schema/websocket.schema.d.ts.map +1 -1
  106. package/dist/schema/websocket.schema.js +298 -11
  107. package/dist/schema/websocket.schema.js.map +1 -1
  108. package/package.json +1 -1
  109. package/src/index.ts +4 -0
  110. package/src/schema/__tests__/assisted-queue.schema.test.ts +1 -0
  111. package/src/schema/__tests__/meter.schema.test.ts +28 -0
  112. package/src/schema/__tests__/operator-context-report.schema.test.ts +77 -0
  113. package/src/schema/__tests__/plugin-market.schema.test.ts +116 -2
  114. package/src/schema/__tests__/qso.schema.test.ts +91 -0
  115. package/src/schema/__tests__/radio-power-support.test.ts +4 -1
  116. package/src/schema/__tests__/radio.schema.test.ts +60 -1
  117. package/src/schema/__tests__/remote-access.schema.test.ts +1 -1
  118. package/src/schema/__tests__/spectrum.schema.test.ts +119 -0
  119. package/src/schema/__tests__/strategy-transmission.schema.test.ts +106 -0
  120. package/src/schema/api-response.schema.ts +11 -0
  121. package/src/schema/image-radio.schema.ts +342 -0
  122. package/src/schema/logbook.schema.ts +1 -0
  123. package/src/schema/mode.schema.ts +16 -0
  124. package/src/schema/operator.schema.ts +4 -19
  125. package/src/schema/plugin.schema.ts +119 -13
  126. package/src/schema/qso.schema.ts +71 -0
  127. package/src/schema/radio-capability.schema.ts +1 -0
  128. package/src/schema/radio-profile.schema.ts +7 -0
  129. package/src/schema/radio.schema.ts +35 -2
  130. package/src/schema/slot-info.schema.ts +2 -1
  131. package/src/schema/spectrum-config.schema.ts +156 -0
  132. package/src/schema/spectrum.schema.ts +89 -0
  133. package/src/schema/tx-audio-input.schema.ts +22 -0
  134. package/src/schema/voice.schema.ts +1 -1
  135. package/src/schema/websocket.schema.ts +383 -12
  136. package/test/image-radio-schema.test.ts +107 -0
  137. package/test/plugin-slot-schema.test.ts +31 -0
  138. package/CLAUDE.md +0 -41
@@ -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>;
@@ -295,6 +295,7 @@ export const CAPABILITY_IDS = [
295
295
  'spectrum_rbw',
296
296
  'spectrum_during_tx',
297
297
  'spectrum_center_type',
298
+ 'tci_iq_sample_rate',
298
299
  'dcs_code',
299
300
  ] as const;
300
301
 
@@ -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, FT4
17
+ mode: z.string(), // 协议模式,如 FT8FT4、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>;