@tx5dr/contracts 1.7.2 → 1.7.4

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 (142) hide show
  1. package/dist/index.d.ts +5 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +7 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/schema/__tests__/ability.test.d.ts +2 -0
  6. package/dist/schema/__tests__/ability.test.d.ts.map +1 -0
  7. package/dist/schema/__tests__/ability.test.js +32 -0
  8. package/dist/schema/__tests__/ability.test.js.map +1 -0
  9. package/dist/schema/__tests__/audio.schema.test.js +4 -0
  10. package/dist/schema/__tests__/audio.schema.test.js.map +1 -1
  11. package/dist/schema/__tests__/cw-decoder.schema.test.d.ts +2 -0
  12. package/dist/schema/__tests__/cw-decoder.schema.test.d.ts.map +1 -0
  13. package/dist/schema/__tests__/cw-decoder.schema.test.js +114 -0
  14. package/dist/schema/__tests__/cw-decoder.schema.test.js.map +1 -0
  15. package/dist/schema/__tests__/device-ui.schema.test.d.ts +2 -0
  16. package/dist/schema/__tests__/device-ui.schema.test.d.ts.map +1 -0
  17. package/dist/schema/__tests__/device-ui.schema.test.js +111 -0
  18. package/dist/schema/__tests__/device-ui.schema.test.js.map +1 -0
  19. package/dist/schema/__tests__/operator.schema.test.d.ts +2 -0
  20. package/dist/schema/__tests__/operator.schema.test.d.ts.map +1 -0
  21. package/dist/schema/__tests__/operator.schema.test.js +23 -0
  22. package/dist/schema/__tests__/operator.schema.test.js.map +1 -0
  23. package/dist/schema/__tests__/process-monitor.schema.test.d.ts +2 -0
  24. package/dist/schema/__tests__/process-monitor.schema.test.d.ts.map +1 -0
  25. package/dist/schema/__tests__/process-monitor.schema.test.js +235 -0
  26. package/dist/schema/__tests__/process-monitor.schema.test.js.map +1 -0
  27. package/dist/schema/__tests__/radio.schema.test.d.ts +2 -0
  28. package/dist/schema/__tests__/radio.schema.test.d.ts.map +1 -0
  29. package/dist/schema/__tests__/radio.schema.test.js +143 -0
  30. package/dist/schema/__tests__/radio.schema.test.js.map +1 -0
  31. package/dist/schema/ability.d.ts +3 -1
  32. package/dist/schema/ability.d.ts.map +1 -1
  33. package/dist/schema/ability.js +11 -0
  34. package/dist/schema/ability.js.map +1 -1
  35. package/dist/schema/api-response.schema.d.ts +170 -3
  36. package/dist/schema/api-response.schema.d.ts.map +1 -1
  37. package/dist/schema/api-response.schema.js +22 -0
  38. package/dist/schema/api-response.schema.js.map +1 -1
  39. package/dist/schema/audio.schema.d.ts +369 -0
  40. package/dist/schema/audio.schema.d.ts.map +1 -1
  41. package/dist/schema/audio.schema.js +3 -0
  42. package/dist/schema/audio.schema.js.map +1 -1
  43. package/dist/schema/cpu-profile.schema.d.ts +6 -6
  44. package/dist/schema/cw-decoder.schema.d.ts +2501 -0
  45. package/dist/schema/cw-decoder.schema.d.ts.map +1 -0
  46. package/dist/schema/cw-decoder.schema.js +196 -0
  47. package/dist/schema/cw-decoder.schema.js.map +1 -0
  48. package/dist/schema/cw-keyer.schema.d.ts +368 -0
  49. package/dist/schema/cw-keyer.schema.d.ts.map +1 -0
  50. package/dist/schema/cw-keyer.schema.js +143 -0
  51. package/dist/schema/cw-keyer.schema.js.map +1 -0
  52. package/dist/schema/desktop-https.schema.js +1 -1
  53. package/dist/schema/desktop-https.schema.js.map +1 -1
  54. package/dist/schema/device-ui.schema.d.ts +1782 -0
  55. package/dist/schema/device-ui.schema.d.ts.map +1 -0
  56. package/dist/schema/device-ui.schema.js +156 -0
  57. package/dist/schema/device-ui.schema.js.map +1 -0
  58. package/dist/schema/logbook.schema.d.ts +2 -2
  59. package/dist/schema/mode.schema.d.ts +1 -0
  60. package/dist/schema/mode.schema.d.ts.map +1 -1
  61. package/dist/schema/mode.schema.js +8 -0
  62. package/dist/schema/mode.schema.js.map +1 -1
  63. package/dist/schema/operator.schema.d.ts +10 -10
  64. package/dist/schema/operator.schema.d.ts.map +1 -1
  65. package/dist/schema/operator.schema.js +6 -1
  66. package/dist/schema/operator.schema.js.map +1 -1
  67. package/dist/schema/plugin.schema.d.ts +653 -70
  68. package/dist/schema/plugin.schema.d.ts.map +1 -1
  69. package/dist/schema/plugin.schema.js +22 -2
  70. package/dist/schema/plugin.schema.js.map +1 -1
  71. package/dist/schema/process-monitor.schema.d.ts +3102 -42
  72. package/dist/schema/process-monitor.schema.d.ts.map +1 -1
  73. package/dist/schema/process-monitor.schema.js +54 -0
  74. package/dist/schema/process-monitor.schema.js.map +1 -1
  75. package/dist/schema/radio-capability.schema.d.ts +7 -7
  76. package/dist/schema/radio-capability.schema.d.ts.map +1 -1
  77. package/dist/schema/radio-capability.schema.js +41 -0
  78. package/dist/schema/radio-capability.schema.js.map +1 -1
  79. package/dist/schema/radio-power.schema.d.ts +6 -0
  80. package/dist/schema/radio-power.schema.d.ts.map +1 -1
  81. package/dist/schema/radio-profile.schema.d.ts +86 -0
  82. package/dist/schema/radio-profile.schema.d.ts.map +1 -1
  83. package/dist/schema/radio.schema.d.ts +166 -9
  84. package/dist/schema/radio.schema.d.ts.map +1 -1
  85. package/dist/schema/radio.schema.js +82 -0
  86. package/dist/schema/radio.schema.js.map +1 -1
  87. package/dist/schema/realtime.schema.d.ts +6 -6
  88. package/dist/schema/slot-info.schema.d.ts +136 -0
  89. package/dist/schema/slot-info.schema.d.ts.map +1 -1
  90. package/dist/schema/slot-info.schema.js +27 -2
  91. package/dist/schema/slot-info.schema.js.map +1 -1
  92. package/dist/schema/spectrum.schema.d.ts +24 -24
  93. package/dist/schema/voice-keyer.schema.d.ts +2 -2
  94. package/dist/schema/voice.schema.d.ts +1 -1
  95. package/dist/schema/voice.schema.d.ts.map +1 -1
  96. package/dist/schema/voice.schema.js +1 -1
  97. package/dist/schema/voice.schema.js.map +1 -1
  98. package/dist/schema/websocket.schema.d.ts +6066 -639
  99. package/dist/schema/websocket.schema.d.ts.map +1 -1
  100. package/dist/schema/websocket.schema.js +106 -4
  101. package/dist/schema/websocket.schema.js.map +1 -1
  102. package/dist/utils/__tests__/cwTiming.test.d.ts +2 -0
  103. package/dist/utils/__tests__/cwTiming.test.d.ts.map +1 -0
  104. package/dist/utils/__tests__/cwTiming.test.js +74 -0
  105. package/dist/utils/__tests__/cwTiming.test.js.map +1 -0
  106. package/dist/utils/callsign.d.ts +2 -0
  107. package/dist/utils/callsign.d.ts.map +1 -0
  108. package/dist/utils/callsign.js +8 -0
  109. package/dist/utils/callsign.js.map +1 -0
  110. package/dist/utils/cwTiming.d.ts +36 -0
  111. package/dist/utils/cwTiming.d.ts.map +1 -0
  112. package/dist/utils/cwTiming.js +155 -0
  113. package/dist/utils/cwTiming.js.map +1 -0
  114. package/package.json +1 -1
  115. package/src/index.ts +9 -0
  116. package/src/schema/__tests__/ability.test.ts +40 -0
  117. package/src/schema/__tests__/audio.schema.test.ts +4 -0
  118. package/src/schema/__tests__/cw-decoder.schema.test.ts +127 -0
  119. package/src/schema/__tests__/device-ui.schema.test.ts +122 -0
  120. package/src/schema/__tests__/operator.schema.test.ts +29 -0
  121. package/src/schema/__tests__/process-monitor.schema.test.ts +245 -0
  122. package/src/schema/__tests__/radio.schema.test.ts +160 -0
  123. package/src/schema/ability.ts +13 -0
  124. package/src/schema/api-response.schema.ts +22 -0
  125. package/src/schema/audio.schema.ts +3 -0
  126. package/src/schema/cw-decoder.schema.ts +234 -0
  127. package/src/schema/cw-keyer.schema.ts +177 -0
  128. package/src/schema/desktop-https.schema.ts +1 -1
  129. package/src/schema/device-ui.schema.ts +182 -0
  130. package/src/schema/mode.schema.ts +8 -0
  131. package/src/schema/operator.schema.ts +7 -1
  132. package/src/schema/plugin.schema.ts +28 -2
  133. package/src/schema/process-monitor.schema.ts +69 -0
  134. package/src/schema/radio-capability.schema.ts +41 -0
  135. package/src/schema/radio.schema.ts +97 -0
  136. package/src/schema/slot-info.schema.ts +33 -2
  137. package/src/schema/voice.schema.ts +1 -1
  138. package/src/schema/websocket.schema.ts +147 -4
  139. package/src/utils/__tests__/cwTiming.test.ts +91 -0
  140. package/src/utils/callsign.ts +9 -0
  141. package/src/utils/cwTiming.ts +203 -0
  142. package/test/decode-request-schema.test.ts +48 -0
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ CreateRadioOperatorRequestSchema,
4
+ UpdateRadioOperatorRequestSchema,
5
+ sanitizeCallsignInput
6
+ } from '../../index.js';
7
+ import { MODES } from '../mode.schema.js';
8
+
9
+ describe('operator callsign normalization', () => {
10
+ it('uppercases callsigns while preserving portable separators', () => {
11
+ expect(sanitizeCallsignInput(' bg5abc/p ')).toBe('BG5ABC/P');
12
+ });
13
+
14
+ it('normalizes callsigns in create and update payloads', () => {
15
+ const createPayload = CreateRadioOperatorRequestSchema.parse({
16
+ myCallsign: 'bg5abc/p',
17
+ myGrid: 'OL63',
18
+ frequency: 1000,
19
+ transmitCycles: [0],
20
+ mode: MODES.FT8,
21
+ });
22
+ const updatePayload = UpdateRadioOperatorRequestSchema.parse({
23
+ myCallsign: 'vk9/bg3yza',
24
+ });
25
+
26
+ expect(createPayload.myCallsign).toBe('BG5ABC/P');
27
+ expect(updatePayload.myCallsign).toBe('VK9/BG3YZA');
28
+ });
29
+ });
@@ -0,0 +1,245 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ProcessSnapshotSchema } from '../process-monitor.schema.js';
3
+
4
+ function createBaseSnapshot() {
5
+ return {
6
+ timestamp: 1,
7
+ uptimeSeconds: 10,
8
+ memory: {
9
+ heapUsed: 100,
10
+ heapTotal: 200,
11
+ rss: 300,
12
+ external: 20,
13
+ arrayBuffers: 10,
14
+ },
15
+ cpu: {
16
+ user: 10,
17
+ system: 5,
18
+ total: 15,
19
+ capacity: 800,
20
+ normalizedTotal: 1.875,
21
+ },
22
+ eventLoop: {
23
+ mean: 1,
24
+ p50: 1,
25
+ p99: 2,
26
+ },
27
+ };
28
+ }
29
+
30
+ describe('ProcessSnapshotSchema', () => {
31
+ it('accepts legacy snapshots without decode worker telemetry', () => {
32
+ expect(ProcessSnapshotSchema.parse(createBaseSnapshot()).decodeWorkers).toBeUndefined();
33
+ });
34
+
35
+ it('accepts decode worker telemetry snapshots', () => {
36
+ const parsed = ProcessSnapshotSchema.parse({
37
+ ...createBaseSnapshot(),
38
+ decodeWorkers: {
39
+ summary: {
40
+ status: 'ready',
41
+ workerCount: 2,
42
+ desiredWorkers: 2,
43
+ readyCount: 2,
44
+ busyCount: 1,
45
+ totalRss: 512,
46
+ totalCpu: 340,
47
+ nativeThreadsPerWorker: 4,
48
+ pendingJobs: 1,
49
+ activeJobs: 1,
50
+ restartAttempts: 0,
51
+ workerEntry: '/app/packages/server/dist/decode/decode-worker-entry.js',
52
+ workerMode: 'production',
53
+ },
54
+ workers: [
55
+ {
56
+ workerId: 1,
57
+ pid: 1234,
58
+ ready: true,
59
+ busy: true,
60
+ nativeThreads: 4,
61
+ uptimeSeconds: 30,
62
+ memory: {
63
+ heapUsed: 100,
64
+ heapTotal: 200,
65
+ rss: 256,
66
+ external: 20,
67
+ arrayBuffers: 10,
68
+ },
69
+ cpu: {
70
+ user: 300,
71
+ system: 40,
72
+ total: 340,
73
+ },
74
+ currentJob: {
75
+ jobId: 7,
76
+ slotId: 'FT8-1-0',
77
+ windowIdx: 0,
78
+ mode: 'FT8',
79
+ startedAt: 1,
80
+ elapsedMs: 1200,
81
+ requestAudioDurationMs: 11500,
82
+ },
83
+ lastSeenAt: 2,
84
+ },
85
+ ],
86
+ },
87
+ });
88
+
89
+ expect(parsed.decodeWorkers?.summary.workerCount).toBe(2);
90
+ expect(parsed.decodeWorkers?.workers[0].currentJob?.mode).toBe('FT8');
91
+ });
92
+
93
+ it('accepts decode worker unavailable snapshots without per-worker telemetry', () => {
94
+ const parsed = ProcessSnapshotSchema.parse({
95
+ ...createBaseSnapshot(),
96
+ decodeWorkers: {
97
+ summary: {
98
+ status: 'unavailable',
99
+ workerCount: 0,
100
+ desiredWorkers: 1,
101
+ readyCount: 0,
102
+ busyCount: 0,
103
+ totalRss: 0,
104
+ totalCpu: 0,
105
+ nativeThreadsPerWorker: 1,
106
+ pendingJobs: 2,
107
+ activeJobs: 0,
108
+ lastError: 'decode worker startup timed out',
109
+ lastFailureAt: 2,
110
+ restartAttempts: 3,
111
+ workerEntry: '/app/packages/server/dist/decode/decode-worker-entry.js',
112
+ workerMode: 'production',
113
+ },
114
+ workers: [],
115
+ },
116
+ });
117
+
118
+ expect(parsed.decodeWorkers?.summary.status).toBe('unavailable');
119
+ expect(parsed.decodeWorkers?.summary.lastError).toContain('startup');
120
+ });
121
+
122
+ it('accepts stopped decode worker snapshots without treating them as failures', () => {
123
+ const parsed = ProcessSnapshotSchema.parse({
124
+ ...createBaseSnapshot(),
125
+ decodeWorkers: {
126
+ summary: {
127
+ status: 'stopped',
128
+ workerCount: 0,
129
+ desiredWorkers: 0,
130
+ readyCount: 0,
131
+ busyCount: 0,
132
+ totalRss: 0,
133
+ totalCpu: 0,
134
+ nativeThreadsPerWorker: 1,
135
+ pendingJobs: 0,
136
+ activeJobs: 0,
137
+ restartAttempts: 0,
138
+ },
139
+ workers: [],
140
+ },
141
+ });
142
+
143
+ expect(parsed.decodeWorkers?.summary.status).toBe('stopped');
144
+ expect(parsed.decodeWorkers?.summary.lastError).toBeUndefined();
145
+ });
146
+
147
+ it('accepts generic worker pool telemetry while preserving decodeWorkers compatibility', () => {
148
+ const decodeWorkerTelemetry = {
149
+ summary: {
150
+ status: 'ready' as const,
151
+ workerCount: 1,
152
+ desiredWorkers: 1,
153
+ readyCount: 1,
154
+ busyCount: 0,
155
+ totalRss: 256,
156
+ totalCpu: 12,
157
+ nativeThreadsPerWorker: 2,
158
+ pendingJobs: 0,
159
+ activeJobs: 0,
160
+ },
161
+ workers: [
162
+ {
163
+ workerId: 1,
164
+ pid: 1234,
165
+ ready: true,
166
+ busy: false,
167
+ nativeThreads: 2,
168
+ uptimeSeconds: 30,
169
+ memory: {
170
+ heapUsed: 100,
171
+ heapTotal: 200,
172
+ rss: 256,
173
+ external: 20,
174
+ arrayBuffers: 10,
175
+ },
176
+ cpu: {
177
+ user: 10,
178
+ system: 2,
179
+ total: 12,
180
+ },
181
+ lastSeenAt: 2,
182
+ },
183
+ ],
184
+ };
185
+
186
+ const parsed = ProcessSnapshotSchema.parse({
187
+ ...createBaseSnapshot(),
188
+ decodeWorkers: decodeWorkerTelemetry,
189
+ workerPools: [
190
+ {
191
+ id: 'ft8-decode',
192
+ name: 'FT8 Decode Workers',
193
+ kind: 'decode',
194
+ ...decodeWorkerTelemetry,
195
+ },
196
+ {
197
+ id: 'cw-decoder',
198
+ name: 'CW Decoder Workers',
199
+ kind: 'cw-decoder',
200
+ summary: {
201
+ status: 'ready',
202
+ workerCount: 1,
203
+ desiredWorkers: 1,
204
+ readyCount: 1,
205
+ busyCount: 0,
206
+ totalRss: 512,
207
+ totalCpu: 4,
208
+ nativeThreadsPerWorker: 1,
209
+ pendingJobs: 0,
210
+ activeJobs: 0,
211
+ },
212
+ workers: [
213
+ {
214
+ workerId: 1,
215
+ pid: 5678,
216
+ ready: true,
217
+ busy: false,
218
+ nativeThreads: 1,
219
+ uptimeSeconds: 8,
220
+ memory: {
221
+ heapUsed: 120,
222
+ heapTotal: 220,
223
+ rss: 512,
224
+ external: 30,
225
+ arrayBuffers: 15,
226
+ },
227
+ cpu: {
228
+ user: 3,
229
+ system: 1,
230
+ total: 4,
231
+ },
232
+ lastSeenAt: 3,
233
+ },
234
+ ],
235
+ },
236
+ ],
237
+ });
238
+
239
+ expect(parsed.decodeWorkers?.summary.workerCount).toBe(1);
240
+ expect(parsed.workerPools?.map((pool) => pool.id)).toEqual(['ft8-decode', 'cw-decoder']);
241
+ expect(parsed.workerPools?.[0].workers[0].workerId).toBe(1);
242
+ expect(parsed.workerPools?.[1].summary.totalRss).toBe(512);
243
+ expect(parsed.workerPools?.[1].workers[0].pid).toBe(5678);
244
+ });
245
+ });
@@ -0,0 +1,160 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { PresetFrequencySchema } from '../radio.schema.js';
3
+
4
+ describe('PresetFrequencySchema repeater DUP fields', () => {
5
+ it('accepts existing presets without repeater DUP fields', () => {
6
+ const parsed = PresetFrequencySchema.parse({
7
+ band: '2m',
8
+ mode: 'VOICE',
9
+ radioMode: 'FM',
10
+ frequency: 145000000,
11
+ description: '145.000 MHz 2m FM',
12
+ });
13
+
14
+ expect(parsed.repeaterShift).toBeUndefined();
15
+ expect(parsed.repeaterOffsetHz).toBeUndefined();
16
+ });
17
+
18
+ it('accepts plus or minus DUP presets with positive Hz offsets', () => {
19
+ const parsed = PresetFrequencySchema.parse({
20
+ band: '2m',
21
+ mode: 'VOICE',
22
+ radioMode: 'FM',
23
+ frequency: 145000000,
24
+ repeaterShift: 'plus',
25
+ repeaterOffsetHz: 600000,
26
+ });
27
+
28
+ expect(parsed.repeaterShift).toBe('plus');
29
+ expect(parsed.repeaterOffsetHz).toBe(600000);
30
+ });
31
+
32
+ it('rejects DUP direction without a positive offset', () => {
33
+ expect(() => PresetFrequencySchema.parse({
34
+ band: '2m',
35
+ mode: 'VOICE',
36
+ radioMode: 'FM',
37
+ frequency: 145000000,
38
+ repeaterShift: 'minus',
39
+ })).toThrow();
40
+
41
+ expect(() => PresetFrequencySchema.parse({
42
+ band: '2m',
43
+ mode: 'VOICE',
44
+ radioMode: 'FM',
45
+ frequency: 145000000,
46
+ repeaterShift: 'minus',
47
+ repeaterOffsetHz: 0,
48
+ })).toThrow();
49
+ });
50
+
51
+ it('rejects invalid DUP directions', () => {
52
+ expect(() => PresetFrequencySchema.parse({
53
+ band: '2m',
54
+ mode: 'VOICE',
55
+ radioMode: 'FM',
56
+ frequency: 145000000,
57
+ repeaterShift: 'up',
58
+ repeaterOffsetHz: 600000,
59
+ })).toThrow();
60
+ });
61
+
62
+ it('rejects DUP presets outside VOICE FM', () => {
63
+ expect(() => PresetFrequencySchema.parse({
64
+ band: '20m',
65
+ mode: 'VOICE',
66
+ radioMode: 'USB',
67
+ frequency: 14270000,
68
+ repeaterShift: 'plus',
69
+ repeaterOffsetHz: 600000,
70
+ })).toThrow();
71
+ });
72
+ });
73
+
74
+ describe('PresetFrequencySchema tone squelch fields', () => {
75
+ it('accepts CTCSS tone presets with positive 0.1 Hz values', () => {
76
+ const parsed = PresetFrequencySchema.parse({
77
+ band: '2m',
78
+ mode: 'VOICE',
79
+ radioMode: 'FM',
80
+ frequency: 145000000,
81
+ toneMode: 'ctcss',
82
+ ctcssToneTenthsHz: 885,
83
+ });
84
+
85
+ expect(parsed.toneMode).toBe('ctcss');
86
+ expect(parsed.ctcssToneTenthsHz).toBe(885);
87
+ });
88
+
89
+ it('accepts DCS presets with positive codes', () => {
90
+ const parsed = PresetFrequencySchema.parse({
91
+ band: '2m',
92
+ mode: 'VOICE',
93
+ radioMode: 'FM',
94
+ frequency: 145000000,
95
+ toneMode: 'dcs',
96
+ dcsCode: 23,
97
+ });
98
+
99
+ expect(parsed.toneMode).toBe('dcs');
100
+ expect(parsed.dcsCode).toBe(23);
101
+ });
102
+
103
+ it('rejects tone squelch modes without required values', () => {
104
+ expect(() => PresetFrequencySchema.parse({
105
+ band: '2m',
106
+ mode: 'VOICE',
107
+ radioMode: 'FM',
108
+ frequency: 145000000,
109
+ toneMode: 'ctcss',
110
+ })).toThrow();
111
+
112
+ expect(() => PresetFrequencySchema.parse({
113
+ band: '2m',
114
+ mode: 'VOICE',
115
+ radioMode: 'FM',
116
+ frequency: 145000000,
117
+ toneMode: 'dcs',
118
+ })).toThrow();
119
+ });
120
+
121
+ it('rejects invalid tone squelch values and modes', () => {
122
+ expect(() => PresetFrequencySchema.parse({
123
+ band: '2m',
124
+ mode: 'VOICE',
125
+ radioMode: 'FM',
126
+ frequency: 145000000,
127
+ toneMode: 'ctcss',
128
+ ctcssToneTenthsHz: 0,
129
+ })).toThrow();
130
+
131
+ expect(() => PresetFrequencySchema.parse({
132
+ band: '2m',
133
+ mode: 'VOICE',
134
+ radioMode: 'FM',
135
+ frequency: 145000000,
136
+ toneMode: 'dcs',
137
+ dcsCode: -1,
138
+ })).toThrow();
139
+
140
+ expect(() => PresetFrequencySchema.parse({
141
+ band: '2m',
142
+ mode: 'VOICE',
143
+ radioMode: 'FM',
144
+ frequency: 145000000,
145
+ toneMode: 'tone',
146
+ ctcssToneTenthsHz: 885,
147
+ })).toThrow();
148
+ });
149
+
150
+ it('rejects tone squelch presets outside VOICE FM', () => {
151
+ expect(() => PresetFrequencySchema.parse({
152
+ band: '20m',
153
+ mode: 'VOICE',
154
+ radioMode: 'USB',
155
+ frequency: 14270000,
156
+ toneMode: 'ctcss',
157
+ ctcssToneTenthsHz: 885,
158
+ })).toThrow();
159
+ });
160
+ });
@@ -34,6 +34,8 @@ export type CapabilitySubject =
34
34
  | 'RigctldBridge'
35
35
  | 'Engine'
36
36
  | 'ModeSwitch'
37
+ | 'CWDecoder'
38
+ | 'CWDecoderConfig'
37
39
  | 'SettingsDecodeWindows'
38
40
  | 'SettingsFrequencyPresets'
39
41
  | 'StationInfo';
@@ -59,6 +61,8 @@ export enum Permission {
59
61
  RIGCTLD_BRIDGE = 'rigctld:bridge',
60
62
  ENGINE_START_STOP = 'engine:start_stop',
61
63
  MODE_SWITCH = 'mode:switch',
64
+ CW_DECODER_CONTROL = 'cw:decoder_control',
65
+ CW_DECODER_CONFIG = 'cw:decoder_config',
62
66
  SETTINGS_DECODE_WINDOWS = 'settings:decode_windows',
63
67
  SETTINGS_FREQUENCY_PRESETS = 'settings:frequency_presets',
64
68
  STATION_UPDATE = 'station:update',
@@ -79,6 +83,8 @@ export const PERMISSION_RULE_MAP: Record<Permission, { action: AppAction; subjec
79
83
  [Permission.RIGCTLD_BRIDGE]: { action: 'execute', subject: 'RigctldBridge' },
80
84
  [Permission.ENGINE_START_STOP]: { action: 'execute', subject: 'Engine' },
81
85
  [Permission.MODE_SWITCH]: { action: 'execute', subject: 'ModeSwitch' },
86
+ [Permission.CW_DECODER_CONTROL]: { action: 'execute', subject: 'CWDecoder' },
87
+ [Permission.CW_DECODER_CONFIG]: { action: 'update', subject: 'CWDecoderConfig' },
82
88
  [Permission.SETTINGS_DECODE_WINDOWS]: { action: 'update', subject: 'SettingsDecodeWindows' },
83
89
  [Permission.SETTINGS_FREQUENCY_PRESETS]: { action: 'update', subject: 'SettingsFrequencyPresets' },
84
90
  [Permission.STATION_UPDATE]: { action: 'update', subject: 'StationInfo' },
@@ -181,6 +187,13 @@ export const PERMISSION_GROUPS: PermissionGroupDef[] = [
181
187
  },
182
188
  { key: 'engine', permissions: [Permission.ENGINE_START_STOP] },
183
189
  { key: 'mode', permissions: [Permission.MODE_SWITCH] },
190
+ {
191
+ key: 'cw',
192
+ permissions: [
193
+ Permission.CW_DECODER_CONTROL,
194
+ Permission.CW_DECODER_CONFIG,
195
+ ],
196
+ },
184
197
  {
185
198
  key: 'settings',
186
199
  permissions: [
@@ -115,6 +115,23 @@ export const DisconnectRadioResponseSchema = z.object({
115
115
  export const LastFrequencyResponseSchema = z.object({
116
116
  success: z.boolean(),
117
117
  lastFrequency: PresetFrequencySchema.nullable(),
118
+ lastVoiceFrequency: z.object({
119
+ frequency: z.number(),
120
+ radioMode: z.string().optional(),
121
+ band: z.string(),
122
+ description: z.string().optional(),
123
+ repeaterShift: z.enum(['none', 'minus', 'plus']).optional(),
124
+ repeaterOffsetHz: z.number().optional(),
125
+ toneMode: z.enum(['none', 'ctcss', 'dcs']).optional(),
126
+ ctcssToneTenthsHz: z.number().optional(),
127
+ dcsCode: z.number().optional(),
128
+ }).nullable().optional(),
129
+ lastCWFrequency: z.object({
130
+ frequency: z.number(),
131
+ radioMode: z.string().optional(),
132
+ band: z.string(),
133
+ description: z.string().optional(),
134
+ }).nullable().optional(),
118
135
  });
119
136
 
120
137
  /**
@@ -127,6 +144,11 @@ export const SetFrequencyResponseSchema = z.object({
127
144
  mode: z.string().optional(),
128
145
  band: z.string().optional(),
129
146
  radioMode: z.string().optional(),
147
+ repeaterShift: z.enum(['none', 'minus', 'plus']).optional(),
148
+ repeaterOffsetHz: z.number().optional(),
149
+ toneMode: z.enum(['none', 'ctcss', 'dcs']).optional(),
150
+ ctcssToneTenthsHz: z.number().optional(),
151
+ dcsCode: z.number().optional(),
130
152
  radioConnected: z.boolean().optional(),
131
153
  });
132
154
 
@@ -9,6 +9,9 @@ export const AudioDeviceSchema = z.object({
9
9
  sampleRate: z.number(),
10
10
  sampleRates: z.array(z.number().int().positive()).optional(),
11
11
  type: z.enum(['input', 'output']),
12
+ availability: z.enum(['available', 'cached', 'active']).optional(),
13
+ isActiveByTx5dr: z.boolean().optional(),
14
+ lastSeenAt: z.number().int().positive().optional(),
12
15
  });
13
16
 
14
17
  export const AudioDeviceResolutionStatusSchema = z.enum([