@tx5dr/contracts 1.7.11 → 2.0.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 +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/schema/__tests__/android-operator-audio.schema.test.d.ts +2 -0
- package/dist/schema/__tests__/android-operator-audio.schema.test.d.ts.map +1 -0
- package/dist/schema/__tests__/android-operator-audio.schema.test.js +27 -0
- package/dist/schema/__tests__/android-operator-audio.schema.test.js.map +1 -0
- package/dist/schema/__tests__/assisted-queue.schema.test.d.ts +2 -0
- package/dist/schema/__tests__/assisted-queue.schema.test.d.ts.map +1 -0
- package/dist/schema/__tests__/assisted-queue.schema.test.js +113 -0
- package/dist/schema/__tests__/assisted-queue.schema.test.js.map +1 -0
- package/dist/schema/__tests__/audio-sidecar.schema.test.d.ts +2 -0
- package/dist/schema/__tests__/audio-sidecar.schema.test.d.ts.map +1 -0
- package/dist/schema/__tests__/audio-sidecar.schema.test.js +42 -0
- package/dist/schema/__tests__/audio-sidecar.schema.test.js.map +1 -0
- package/dist/schema/__tests__/audio.schema.test.js +92 -1
- package/dist/schema/__tests__/audio.schema.test.js.map +1 -1
- package/dist/schema/__tests__/diagnostics.schema.test.d.ts +2 -0
- package/dist/schema/__tests__/diagnostics.schema.test.d.ts.map +1 -0
- package/dist/schema/__tests__/diagnostics.schema.test.js +31 -0
- package/dist/schema/__tests__/diagnostics.schema.test.js.map +1 -0
- package/dist/schema/__tests__/logbook-health.schema.test.d.ts +2 -0
- package/dist/schema/__tests__/logbook-health.schema.test.d.ts.map +1 -0
- package/dist/schema/__tests__/logbook-health.schema.test.js +104 -0
- package/dist/schema/__tests__/logbook-health.schema.test.js.map +1 -0
- package/dist/schema/__tests__/observability.schema.test.d.ts +2 -0
- package/dist/schema/__tests__/observability.schema.test.d.ts.map +1 -0
- package/dist/schema/__tests__/observability.schema.test.js +12 -0
- package/dist/schema/__tests__/observability.schema.test.js.map +1 -0
- package/dist/schema/__tests__/operator.schema.test.js +9 -3
- package/dist/schema/__tests__/operator.schema.test.js.map +1 -1
- package/dist/schema/__tests__/plugin-market.schema.test.js +43 -1
- package/dist/schema/__tests__/plugin-market.schema.test.js.map +1 -1
- package/dist/schema/__tests__/radio-power-support.test.js +12 -0
- package/dist/schema/__tests__/radio-power-support.test.js.map +1 -1
- package/dist/schema/__tests__/radio-profile.schema.test.d.ts +2 -0
- package/dist/schema/__tests__/radio-profile.schema.test.d.ts.map +1 -0
- package/dist/schema/__tests__/radio-profile.schema.test.js +36 -0
- package/dist/schema/__tests__/radio-profile.schema.test.js.map +1 -0
- package/dist/schema/__tests__/radio.schema.test.js +41 -1
- package/dist/schema/__tests__/radio.schema.test.js.map +1 -1
- package/dist/schema/__tests__/realtime-settings.schema.test.js +2 -1
- package/dist/schema/__tests__/realtime-settings.schema.test.js.map +1 -1
- package/dist/schema/__tests__/remote-access.schema.test.d.ts +2 -0
- package/dist/schema/__tests__/remote-access.schema.test.d.ts.map +1 -0
- package/dist/schema/__tests__/remote-access.schema.test.js +18 -0
- package/dist/schema/__tests__/remote-access.schema.test.js.map +1 -0
- package/dist/schema/android-operator-audio.schema.d.ts +174 -0
- package/dist/schema/android-operator-audio.schema.d.ts.map +1 -0
- package/dist/schema/android-operator-audio.schema.js +38 -0
- package/dist/schema/android-operator-audio.schema.js.map +1 -0
- package/dist/schema/api-response.schema.d.ts +268 -44
- package/dist/schema/api-response.schema.d.ts.map +1 -1
- package/dist/schema/audio-sidecar.schema.d.ts +75 -0
- package/dist/schema/audio-sidecar.schema.d.ts.map +1 -1
- package/dist/schema/audio-sidecar.schema.js +32 -0
- package/dist/schema/audio-sidecar.schema.js.map +1 -1
- package/dist/schema/audio.schema.d.ts +4749 -3
- package/dist/schema/audio.schema.d.ts.map +1 -1
- package/dist/schema/audio.schema.js +101 -2
- package/dist/schema/audio.schema.js.map +1 -1
- package/dist/schema/auth.schema.d.ts +171 -12
- package/dist/schema/auth.schema.d.ts.map +1 -1
- package/dist/schema/auth.schema.js +65 -1
- package/dist/schema/auth.schema.js.map +1 -1
- package/dist/schema/cpu-profile.schema.d.ts +2 -2
- package/dist/schema/cw-decoder.schema.d.ts +74 -74
- package/dist/schema/cw-keyer.schema.d.ts +20 -14
- package/dist/schema/cw-keyer.schema.d.ts.map +1 -1
- package/dist/schema/cw-keyer.schema.js +3 -0
- package/dist/schema/cw-keyer.schema.js.map +1 -1
- package/dist/schema/desktop-https.schema.d.ts +6 -6
- package/dist/schema/device-ui.schema.d.ts +6 -6
- package/dist/schema/diagnostics.schema.d.ts +163 -0
- package/dist/schema/diagnostics.schema.d.ts.map +1 -0
- package/dist/schema/diagnostics.schema.js +46 -0
- package/dist/schema/diagnostics.schema.js.map +1 -0
- package/dist/schema/ft8.schema.d.ts +12 -2
- package/dist/schema/ft8.schema.d.ts.map +1 -1
- package/dist/schema/ft8.schema.js +8 -0
- package/dist/schema/ft8.schema.js.map +1 -1
- package/dist/schema/logbook.schema.d.ts +1940 -18
- package/dist/schema/logbook.schema.d.ts.map +1 -1
- package/dist/schema/logbook.schema.js +158 -1
- package/dist/schema/logbook.schema.js.map +1 -1
- package/dist/schema/mode.schema.d.ts +2 -6
- package/dist/schema/mode.schema.d.ts.map +1 -1
- package/dist/schema/mode.schema.js +2 -6
- package/dist/schema/mode.schema.js.map +1 -1
- package/dist/schema/observability.schema.d.ts +66 -0
- package/dist/schema/observability.schema.d.ts.map +1 -0
- package/dist/schema/observability.schema.js +20 -0
- package/dist/schema/observability.schema.js.map +1 -0
- package/dist/schema/operator.schema.d.ts +6 -6
- package/dist/schema/operator.schema.d.ts.map +1 -1
- package/dist/schema/operator.schema.js +6 -1
- package/dist/schema/operator.schema.js.map +1 -1
- package/dist/schema/plugin.schema.d.ts +258 -78
- package/dist/schema/plugin.schema.d.ts.map +1 -1
- package/dist/schema/plugin.schema.js +57 -2
- package/dist/schema/plugin.schema.js.map +1 -1
- package/dist/schema/process-monitor.schema.d.ts +132 -132
- package/dist/schema/pskreporter.schema.d.ts +1 -3
- package/dist/schema/pskreporter.schema.d.ts.map +1 -1
- package/dist/schema/pskreporter.schema.js +1 -3
- package/dist/schema/pskreporter.schema.js.map +1 -1
- package/dist/schema/qso.schema.d.ts +7 -0
- package/dist/schema/qso.schema.d.ts.map +1 -1
- package/dist/schema/qso.schema.js +7 -0
- package/dist/schema/qso.schema.js.map +1 -1
- package/dist/schema/radio-capability.schema.d.ts +31 -40
- package/dist/schema/radio-capability.schema.d.ts.map +1 -1
- package/dist/schema/radio-capability.schema.js +7 -15
- package/dist/schema/radio-capability.schema.js.map +1 -1
- package/dist/schema/radio-power-support.d.ts.map +1 -1
- package/dist/schema/radio-power-support.js +3 -0
- package/dist/schema/radio-power-support.js.map +1 -1
- package/dist/schema/radio-power.schema.d.ts +87 -13
- package/dist/schema/radio-power.schema.d.ts.map +1 -1
- package/dist/schema/radio-power.schema.js +15 -8
- package/dist/schema/radio-power.schema.js.map +1 -1
- package/dist/schema/radio-profile.schema.d.ts +1240 -97
- package/dist/schema/radio-profile.schema.d.ts.map +1 -1
- package/dist/schema/radio.schema.d.ts +271 -48
- package/dist/schema/radio.schema.d.ts.map +1 -1
- package/dist/schema/radio.schema.js +44 -5
- package/dist/schema/radio.schema.js.map +1 -1
- package/dist/schema/realtime.schema.d.ts +60 -52
- package/dist/schema/realtime.schema.d.ts.map +1 -1
- package/dist/schema/realtime.schema.js +9 -1
- package/dist/schema/realtime.schema.js.map +1 -1
- package/dist/schema/slot-info.schema.d.ts +15 -9
- package/dist/schema/slot-info.schema.d.ts.map +1 -1
- package/dist/schema/slot-info.schema.js +15 -9
- package/dist/schema/slot-info.schema.js.map +1 -1
- package/dist/schema/spectrum.schema.d.ts +12 -12
- package/dist/schema/station-info.schema.d.ts +5 -0
- package/dist/schema/station-info.schema.d.ts.map +1 -1
- package/dist/schema/station-info.schema.js +5 -0
- package/dist/schema/station-info.schema.js.map +1 -1
- package/dist/schema/system.schema.d.ts +56 -6
- package/dist/schema/system.schema.d.ts.map +1 -1
- package/dist/schema/system.schema.js +22 -0
- package/dist/schema/system.schema.js.map +1 -1
- package/dist/schema/transmission.schema.d.ts +1 -7
- package/dist/schema/transmission.schema.d.ts.map +1 -1
- package/dist/schema/transmission.schema.js +2 -7
- package/dist/schema/transmission.schema.js.map +1 -1
- package/dist/schema/update.schema.d.ts +3 -0
- package/dist/schema/update.schema.d.ts.map +1 -1
- package/dist/schema/update.schema.js +1 -0
- package/dist/schema/update.schema.js.map +1 -1
- package/dist/schema/voice-keyer.schema.d.ts +16 -16
- package/dist/schema/websocket.schema.d.ts +3582 -555
- package/dist/schema/websocket.schema.d.ts.map +1 -1
- package/dist/schema/websocket.schema.js +160 -3
- package/dist/schema/websocket.schema.js.map +1 -1
- package/package.json +6 -1
- package/src/index.ts +3 -0
- package/src/schema/__tests__/android-operator-audio.schema.test.ts +31 -0
- package/src/schema/__tests__/assisted-queue.schema.test.ts +121 -0
- package/src/schema/__tests__/audio-sidecar.schema.test.ts +48 -0
- package/src/schema/__tests__/audio.schema.test.ts +102 -0
- package/src/schema/__tests__/diagnostics.schema.test.ts +32 -0
- package/src/schema/__tests__/logbook-health.schema.test.ts +117 -0
- package/src/schema/__tests__/observability.schema.test.ts +15 -0
- package/src/schema/__tests__/operator.schema.test.ts +11 -3
- package/src/schema/__tests__/plugin-market.schema.test.ts +49 -0
- package/src/schema/__tests__/radio-power-support.test.ts +14 -0
- package/src/schema/__tests__/radio-profile.schema.test.ts +44 -0
- package/src/schema/__tests__/radio.schema.test.ts +49 -1
- package/src/schema/__tests__/realtime-settings.schema.test.ts +2 -1
- package/src/schema/__tests__/remote-access.schema.test.ts +22 -0
- package/src/schema/android-operator-audio.schema.ts +49 -0
- package/src/schema/audio-sidecar.schema.ts +35 -0
- package/src/schema/audio.schema.ts +115 -2
- package/src/schema/auth.schema.ts +79 -1
- package/src/schema/cw-keyer.schema.ts +5 -0
- package/src/schema/diagnostics.schema.ts +55 -0
- package/src/schema/ft8.schema.ts +8 -0
- package/src/schema/logbook.schema.ts +204 -1
- package/src/schema/mode.schema.ts +2 -6
- package/src/schema/observability.schema.ts +26 -0
- package/src/schema/operator.schema.ts +7 -1
- package/src/schema/plugin.schema.ts +67 -2
- package/src/schema/pskreporter.schema.ts +1 -3
- package/src/schema/qso.schema.ts +7 -0
- package/src/schema/radio-capability.schema.ts +7 -15
- package/src/schema/radio-power-support.ts +3 -0
- package/src/schema/radio-power.schema.ts +15 -8
- package/src/schema/radio.schema.ts +55 -5
- package/src/schema/realtime.schema.ts +9 -1
- package/src/schema/slot-info.schema.ts +16 -10
- package/src/schema/station-info.schema.ts +5 -0
- package/src/schema/system.schema.ts +29 -0
- package/src/schema/transmission.schema.ts +2 -7
- package/src/schema/update.schema.ts +1 -0
- package/src/schema/websocket.schema.ts +223 -6
- package/test/radio-capability-schema.test.ts +44 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CreateLogbookBackupRequestSchema,
|
|
5
|
+
LogBookInfoSchema,
|
|
6
|
+
LogbookHealthSchema,
|
|
7
|
+
RestoreLogbookRequestSchema,
|
|
8
|
+
WSMessageSchema,
|
|
9
|
+
WSMessageType,
|
|
10
|
+
} from '../../index.js';
|
|
11
|
+
|
|
12
|
+
const loadingHealth = {
|
|
13
|
+
state: 'loading' as const,
|
|
14
|
+
readable: false,
|
|
15
|
+
writable: false,
|
|
16
|
+
issues: [],
|
|
17
|
+
updatedAt: 1_775_394_000_000,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe('logbook health contracts', () => {
|
|
21
|
+
it('requires every logbook listing to expose health', () => {
|
|
22
|
+
const info = {
|
|
23
|
+
id: 'logbook-N0CALL',
|
|
24
|
+
name: 'N0CALL QSO Log',
|
|
25
|
+
fileName: 'N0CALL.adi',
|
|
26
|
+
storageKind: 'managed' as const,
|
|
27
|
+
createdAt: 1,
|
|
28
|
+
lastUsed: 2,
|
|
29
|
+
isActive: true,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
expect(LogBookInfoSchema.safeParse(info).success).toBe(false);
|
|
33
|
+
const parsed = LogBookInfoSchema.parse({ ...info, health: loadingHealth });
|
|
34
|
+
expect(parsed.health).toEqual(loadingHealth);
|
|
35
|
+
expect(parsed).not.toHaveProperty('filePath');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('rejects path-bearing fields in backup and restore mutations', () => {
|
|
39
|
+
expect(CreateLogbookBackupRequestSchema.safeParse({
|
|
40
|
+
filePath: '/tmp/escape.adi',
|
|
41
|
+
}).success).toBe(false);
|
|
42
|
+
expect(RestoreLogbookRequestSchema.safeParse({
|
|
43
|
+
preflightToken: 'preflight-token-1',
|
|
44
|
+
confirmation: 'logbook-N0CALL',
|
|
45
|
+
artifactPath: '../another.adi',
|
|
46
|
+
}).success).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('rejects negative affected record and byte counts', () => {
|
|
50
|
+
expect(LogbookHealthSchema.safeParse({
|
|
51
|
+
...loadingHealth,
|
|
52
|
+
issues: [{
|
|
53
|
+
code: 'OPAQUE_RECORDS',
|
|
54
|
+
message: 'One record was skipped',
|
|
55
|
+
affectedRecords: -1,
|
|
56
|
+
affectedBytes: -1,
|
|
57
|
+
occurredAt: loadingHealth.updatedAt,
|
|
58
|
+
}],
|
|
59
|
+
}).success).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('validates health and write-failure websocket payloads', () => {
|
|
63
|
+
expect(WSMessageSchema.safeParse({
|
|
64
|
+
type: WSMessageType.LOGBOOK_HEALTH_CHANGED,
|
|
65
|
+
timestamp: new Date().toISOString(),
|
|
66
|
+
data: { logBookId: 'logbook-N0CALL', health: loadingHealth },
|
|
67
|
+
}).success).toBe(true);
|
|
68
|
+
expect(WSMessageSchema.safeParse({
|
|
69
|
+
type: WSMessageType.LOGBOOK_WRITE_FAILED,
|
|
70
|
+
timestamp: new Date().toISOString(),
|
|
71
|
+
data: {
|
|
72
|
+
logBookId: 'logbook-N0CALL',
|
|
73
|
+
operatorId: 'operator-1',
|
|
74
|
+
attemptId: 'attempt-12345678',
|
|
75
|
+
unsavedCount: 1,
|
|
76
|
+
error: {
|
|
77
|
+
code: 'LOGBOOK_WRITE_FAILED',
|
|
78
|
+
message: 'No space left on device',
|
|
79
|
+
systemCode: 'ENOSPC',
|
|
80
|
+
occurredAt: loadingHealth.updatedAt,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
}).success).toBe(true);
|
|
84
|
+
expect(WSMessageSchema.safeParse({
|
|
85
|
+
type: WSMessageType.LOGBOOK_WRITE_FAILED,
|
|
86
|
+
timestamp: new Date().toISOString(),
|
|
87
|
+
data: {
|
|
88
|
+
logBookId: 'logbook-N0CALL',
|
|
89
|
+
qsoRecord: {
|
|
90
|
+
id: 'private-qso',
|
|
91
|
+
callsign: 'N0CALL',
|
|
92
|
+
frequency: 14_074_000,
|
|
93
|
+
mode: 'FT8',
|
|
94
|
+
startTime: loadingHealth.updatedAt,
|
|
95
|
+
messageHistory: ['CQ N0CALL AA00'],
|
|
96
|
+
},
|
|
97
|
+
error: {
|
|
98
|
+
code: 'LOGBOOK_WRITE_FAILED',
|
|
99
|
+
message: 'write failed',
|
|
100
|
+
occurredAt: loadingHealth.updatedAt,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
}).success).toBe(false);
|
|
104
|
+
expect(WSMessageSchema.safeParse({
|
|
105
|
+
type: WSMessageType.LOGBOOK_WRITE_FAILED,
|
|
106
|
+
timestamp: new Date().toISOString(),
|
|
107
|
+
data: {
|
|
108
|
+
logBookId: 'logbook-N0CALL',
|
|
109
|
+
error: {
|
|
110
|
+
code: 'ARBITRARY_ERROR',
|
|
111
|
+
message: 'Unstable error code',
|
|
112
|
+
occurredAt: loadingHealth.updatedAt,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
}).success).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
OBSERVABILITY_NOTICE_VERSION,
|
|
4
|
+
UpdateObservabilitySettingsSchema,
|
|
5
|
+
} from '../observability.schema.js';
|
|
6
|
+
|
|
7
|
+
describe('observability settings schema', () => {
|
|
8
|
+
it('requires acknowledgment of the current notice', () => {
|
|
9
|
+
expect(UpdateObservabilitySettingsSchema.parse({
|
|
10
|
+
enabled: true,
|
|
11
|
+
noticeVersion: OBSERVABILITY_NOTICE_VERSION,
|
|
12
|
+
})).toEqual({ enabled: true, noticeVersion: 1 });
|
|
13
|
+
expect(() => UpdateObservabilitySettingsSchema.parse({ enabled: true, noticeVersion: 0 })).toThrow();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -2,7 +2,8 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import {
|
|
3
3
|
CreateRadioOperatorRequestSchema,
|
|
4
4
|
UpdateRadioOperatorRequestSchema,
|
|
5
|
-
sanitizeCallsignInput
|
|
5
|
+
sanitizeCallsignInput,
|
|
6
|
+
sanitizeGridInput,
|
|
6
7
|
} from '../../index.js';
|
|
7
8
|
import { MODES } from '../mode.schema.js';
|
|
8
9
|
|
|
@@ -11,19 +12,26 @@ describe('operator callsign normalization', () => {
|
|
|
11
12
|
expect(sanitizeCallsignInput(' bg5abc/p ')).toBe('BG5ABC/P');
|
|
12
13
|
});
|
|
13
14
|
|
|
14
|
-
it('
|
|
15
|
+
it('uppercases grids and removes whitespace', () => {
|
|
16
|
+
expect(sanitizeGridInput(' ol 63aa ')).toBe('OL63AA');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('normalizes callsigns and grids in create and update payloads', () => {
|
|
15
20
|
const createPayload = CreateRadioOperatorRequestSchema.parse({
|
|
16
21
|
myCallsign: 'bg5abc/p',
|
|
17
|
-
myGrid: '
|
|
22
|
+
myGrid: 'ol63aa',
|
|
18
23
|
frequency: 1000,
|
|
19
24
|
transmitCycles: [0],
|
|
20
25
|
mode: MODES.FT8,
|
|
21
26
|
});
|
|
22
27
|
const updatePayload = UpdateRadioOperatorRequestSchema.parse({
|
|
23
28
|
myCallsign: 'vk9/bg3yza',
|
|
29
|
+
myGrid: 'pm01',
|
|
24
30
|
});
|
|
25
31
|
|
|
26
32
|
expect(createPayload.myCallsign).toBe('BG5ABC/P');
|
|
33
|
+
expect(createPayload.myGrid).toBe('OL63AA');
|
|
27
34
|
expect(updatePayload.myCallsign).toBe('VK9/BG3YZA');
|
|
35
|
+
expect(updatePayload.myGrid).toBe('PM01');
|
|
28
36
|
});
|
|
29
37
|
});
|
|
@@ -3,8 +3,11 @@ import {
|
|
|
3
3
|
PluginMarketCatalogResponseSchema,
|
|
4
4
|
PluginMarketCatalogSchema,
|
|
5
5
|
PluginMarketChannelSchema,
|
|
6
|
+
PluginManifestSchema,
|
|
6
7
|
PluginPermissionSchema,
|
|
8
|
+
PluginsConfigSchema,
|
|
7
9
|
PluginSourceSchema,
|
|
10
|
+
PluginStatusSchema,
|
|
8
11
|
} from '../../index.js';
|
|
9
12
|
|
|
10
13
|
describe('plugin market schema', () => {
|
|
@@ -18,6 +21,8 @@ describe('plugin market schema', () => {
|
|
|
18
21
|
name: 'heartbeat-demo',
|
|
19
22
|
title: 'Heartbeat Demo',
|
|
20
23
|
description: 'Example timer and quick-action plugin.',
|
|
24
|
+
readmeMarkdown: '# Heartbeat Demo\n\nExample timer and quick-action plugin.',
|
|
25
|
+
readmeSourceUrl: 'https://github.com/boybook/tx-5dr-plugins/blob/main/heartbeat-demo/README.md',
|
|
21
26
|
locales: {
|
|
22
27
|
en: {
|
|
23
28
|
pluginName: 'Heartbeat Demo',
|
|
@@ -53,6 +58,8 @@ describe('plugin market schema', () => {
|
|
|
53
58
|
|
|
54
59
|
expect(catalog.channel).toBe('stable');
|
|
55
60
|
expect(catalog.plugins[0]?.name).toBe('heartbeat-demo');
|
|
61
|
+
expect(catalog.plugins[0]?.readmeMarkdown).toContain('# Heartbeat Demo');
|
|
62
|
+
expect(catalog.plugins[0]?.readmeSourceUrl).toContain('/heartbeat-demo/README.md');
|
|
56
63
|
});
|
|
57
64
|
|
|
58
65
|
it('rejects unsupported channels', () => {
|
|
@@ -60,9 +67,51 @@ describe('plugin market schema', () => {
|
|
|
60
67
|
});
|
|
61
68
|
|
|
62
69
|
it('accepts host settings permissions', () => {
|
|
70
|
+
expect(PluginPermissionSchema.parse('plugin:event-bus')).toBe('plugin:event-bus');
|
|
63
71
|
expect(PluginPermissionSchema.parse('host:hamlib')).toBe('host:hamlib');
|
|
72
|
+
expect(PluginPermissionSchema.parse('operator:transmit-control')).toBe('operator:transmit-control');
|
|
64
73
|
expect(PluginPermissionSchema.parse('settings:ft8')).toBe('settings:ft8');
|
|
65
74
|
expect(PluginPermissionSchema.parse('settings:ntp')).toBe('settings:ntp');
|
|
75
|
+
expect(() => PluginPermissionSchema.parse('operator:transmit')).toThrow();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('accepts transmit-control permission and auto-call status in plugin snapshots', () => {
|
|
79
|
+
const manifest = PluginManifestSchema.parse({
|
|
80
|
+
name: 'autocall-demo',
|
|
81
|
+
version: '1.0.0',
|
|
82
|
+
type: 'utility',
|
|
83
|
+
permissions: ['operator:transmit-control'],
|
|
84
|
+
});
|
|
85
|
+
expect(manifest.permissions).toEqual(['operator:transmit-control']);
|
|
86
|
+
|
|
87
|
+
const status = PluginStatusSchema.parse({
|
|
88
|
+
name: 'autocall-demo',
|
|
89
|
+
version: '1.0.0',
|
|
90
|
+
type: 'utility',
|
|
91
|
+
isBuiltIn: false,
|
|
92
|
+
loaded: true,
|
|
93
|
+
enabled: true,
|
|
94
|
+
errorCount: 0,
|
|
95
|
+
permissions: ['operator:transmit-control'],
|
|
96
|
+
capabilities: ['auto_call_control'],
|
|
97
|
+
autoCallEnabledOperatorIds: ['operator-1'],
|
|
98
|
+
pausedOperatorIds: ['operator-2'],
|
|
99
|
+
});
|
|
100
|
+
expect(status.autoCallEnabledOperatorIds).toEqual(['operator-1']);
|
|
101
|
+
expect(status.pausedOperatorIds).toEqual(['operator-2']);
|
|
102
|
+
expect(status.capabilities).toContain('auto_call_control');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('accepts persisted operator plugin pause state with defaults', () => {
|
|
106
|
+
const emptyConfig = PluginsConfigSchema.parse({});
|
|
107
|
+
expect(emptyConfig.operatorPluginPauses).toEqual({});
|
|
108
|
+
|
|
109
|
+
const config = PluginsConfigSchema.parse({
|
|
110
|
+
operatorPluginPauses: {
|
|
111
|
+
'operator-1': ['scheduled-cq-autocall'],
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
expect(config.operatorPluginPauses['operator-1']).toEqual(['scheduled-cq-autocall']);
|
|
66
115
|
});
|
|
67
116
|
|
|
68
117
|
it('accepts the route response envelope', () => {
|
|
@@ -23,4 +23,18 @@ describe('radio power support', () => {
|
|
|
23
23
|
}),
|
|
24
24
|
).not.toThrow();
|
|
25
25
|
});
|
|
26
|
+
|
|
27
|
+
it('does not expose physical power controls for TCI radios', () => {
|
|
28
|
+
const decision = decidePowerSupport({
|
|
29
|
+
type: 'tci',
|
|
30
|
+
tci: { host: '127.0.0.1', port: 40001, receiver: 0, trx: 0, vfo: 0, audioEnabled: true, audioSampleRate: 12000 },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(decision).toMatchObject({
|
|
34
|
+
canPowerOn: false,
|
|
35
|
+
canPowerOff: false,
|
|
36
|
+
supportedStates: [],
|
|
37
|
+
reason: 'model-unsupported',
|
|
38
|
+
});
|
|
39
|
+
});
|
|
26
40
|
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
CreateProfileRequestSchema,
|
|
4
|
+
RadioProfileSchema,
|
|
5
|
+
UpdateProfileRequestSchema,
|
|
6
|
+
} from '../radio-profile.schema.js';
|
|
7
|
+
|
|
8
|
+
const audio = {
|
|
9
|
+
inputDeviceName: 'Headset microphone',
|
|
10
|
+
inputRouteKey: 'android:wired-headset:input',
|
|
11
|
+
outputDeviceName: 'Headset headphones',
|
|
12
|
+
outputRouteKey: 'android:wired-headset:output',
|
|
13
|
+
inputSampleRate: 48000,
|
|
14
|
+
outputSampleRate: 48000,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe('radio profile audio route identity', () => {
|
|
18
|
+
it('preserves stable route keys through create and update requests', () => {
|
|
19
|
+
expect(CreateProfileRequestSchema.parse({
|
|
20
|
+
name: 'Android 3.5 mm',
|
|
21
|
+
radio: { type: 'none' },
|
|
22
|
+
audio,
|
|
23
|
+
}).audio).toMatchObject(audio);
|
|
24
|
+
|
|
25
|
+
expect(UpdateProfileRequestSchema.parse({ audio }).audio).toMatchObject(audio);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('loads stable route keys while accepting legacy name-only profiles', () => {
|
|
29
|
+
const baseProfile = {
|
|
30
|
+
id: 'profile-1',
|
|
31
|
+
name: 'Android 3.5 mm',
|
|
32
|
+
radio: { type: 'none' as const },
|
|
33
|
+
audioLockedToRadio: false,
|
|
34
|
+
createdAt: 1,
|
|
35
|
+
updatedAt: 2,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
expect(RadioProfileSchema.parse({ ...baseProfile, audio }).audio).toMatchObject(audio);
|
|
39
|
+
expect(RadioProfileSchema.parse({
|
|
40
|
+
...baseProfile,
|
|
41
|
+
audio: { inputDeviceName: 'Legacy USB Audio' },
|
|
42
|
+
}).audio).toEqual({ inputDeviceName: 'Legacy USB Audio' });
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { PresetFrequencySchema } from '../radio.schema.js';
|
|
2
|
+
import { DigitalModeRadioModePreferenceSchema, HamlibConfigSchema, PresetFrequencySchema, RadioInfoSchema } from '../radio.schema.js';
|
|
3
|
+
|
|
4
|
+
describe('HamlibConfigSchema digital mode radio mode preference', () => {
|
|
5
|
+
it('accepts legacy configs without a digital mode radio mode preference', () => {
|
|
6
|
+
const parsed = HamlibConfigSchema.parse({ type: 'none' });
|
|
7
|
+
|
|
8
|
+
expect(parsed.digitalModeRadioMode).toBeUndefined();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('accepts supported FT8/FT4 radio mode preferences', () => {
|
|
12
|
+
expect(DigitalModeRadioModePreferenceSchema.parse('none')).toBe('none');
|
|
13
|
+
expect(DigitalModeRadioModePreferenceSchema.parse('usb')).toBe('usb');
|
|
14
|
+
expect(DigitalModeRadioModePreferenceSchema.parse('usb-data')).toBe('usb-data');
|
|
15
|
+
expect(HamlibConfigSchema.parse({ type: 'serial', digitalModeRadioMode: 'usb-data' }).digitalModeRadioMode).toBe('usb-data');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('rejects invalid FT8/FT4 radio mode preferences', () => {
|
|
19
|
+
expect(() => HamlibConfigSchema.parse({ type: 'none', digitalModeRadioMode: 'lsb-data' })).toThrow();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('HamlibConfigSchema TCI connection', () => {
|
|
24
|
+
it('accepts TCI configs and applies SunSDR defaults', () => {
|
|
25
|
+
const parsed = HamlibConfigSchema.parse({
|
|
26
|
+
type: 'tci',
|
|
27
|
+
tci: {},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(parsed.tci).toMatchObject({
|
|
31
|
+
host: '127.0.0.1',
|
|
32
|
+
port: 40001,
|
|
33
|
+
receiver: 0,
|
|
34
|
+
trx: 0,
|
|
35
|
+
vfo: 0,
|
|
36
|
+
audioEnabled: true,
|
|
37
|
+
audioSampleRate: 12000,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('accepts TCI as a reported radio connection type', () => {
|
|
42
|
+
const parsed = RadioInfoSchema.parse({
|
|
43
|
+
manufacturer: 'Expert Electronics',
|
|
44
|
+
model: 'SunSDR',
|
|
45
|
+
connectionType: 'tci',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(parsed.connectionType).toBe('tci');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
3
51
|
|
|
4
52
|
describe('PresetFrequencySchema repeater DUP fields', () => {
|
|
5
53
|
it('accepts existing presets without repeater DUP fields', () => {
|
|
@@ -9,9 +9,10 @@ import {
|
|
|
9
9
|
} from '../realtime.schema.js';
|
|
10
10
|
|
|
11
11
|
describe('Realtime transport schemas', () => {
|
|
12
|
-
it('accepts
|
|
12
|
+
it('accepts realtime network transports plus the Android native diagnostics transport', () => {
|
|
13
13
|
expect(RealtimeTransportKindSchema.parse('rtc-data-audio')).toBe('rtc-data-audio');
|
|
14
14
|
expect(RealtimeTransportKindSchema.parse('ws-compat')).toBe('ws-compat');
|
|
15
|
+
expect(RealtimeTransportKindSchema.parse('android-native')).toBe('android-native');
|
|
15
16
|
const retiredTransport = 'live' + 'kit';
|
|
16
17
|
expect(() => RealtimeTransportKindSchema.parse(retiredTransport)).toThrow();
|
|
17
18
|
expect(() => RealtimeSessionRequestSchema.parse({
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
isSecureRemoteAccessOrigin,
|
|
4
|
+
normalizeRemoteAccessOrigin,
|
|
5
|
+
} from '../auth.schema.js';
|
|
6
|
+
|
|
7
|
+
describe('remote access origins', () => {
|
|
8
|
+
it('normalizes exact browser origins and rejects paths or credentials', () => {
|
|
9
|
+
expect(normalizeRemoteAccessOrigin('https://radio.example.com/')).toBe('https://radio.example.com');
|
|
10
|
+
expect(normalizeRemoteAccessOrigin('http://10.8.0.2:8076')).toBe('http://10.8.0.2:8076');
|
|
11
|
+
expect(normalizeRemoteAccessOrigin('https://radio.example.com/tx5dr')).toBeNull();
|
|
12
|
+
expect(normalizeRemoteAccessOrigin('https://user:pass@radio.example.com')).toBeNull();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('requires HTTPS for public hosts while allowing private-network HTTP', () => {
|
|
16
|
+
expect(isSecureRemoteAccessOrigin('https://radio.example.com')).toBe(true);
|
|
17
|
+
expect(isSecureRemoteAccessOrigin('http://radio.example.com')).toBe(false);
|
|
18
|
+
expect(isSecureRemoteAccessOrigin('http://203.0.113.10:8076')).toBe(false);
|
|
19
|
+
expect(isSecureRemoteAccessOrigin('http://192.168.1.20:8076')).toBe(true);
|
|
20
|
+
expect(isSecureRemoteAccessOrigin('http://100.64.0.2:8076')).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const AndroidOperatorAudioDeviceSchema = z.object({
|
|
4
|
+
id: z.string(),
|
|
5
|
+
name: z.string(),
|
|
6
|
+
kind: z.string(),
|
|
7
|
+
socketPath: z.string(),
|
|
8
|
+
sampleRate: z.number().int().positive(),
|
|
9
|
+
connected: z.boolean().optional(),
|
|
10
|
+
});
|
|
11
|
+
export type AndroidOperatorAudioDevice = z.infer<typeof AndroidOperatorAudioDeviceSchema>;
|
|
12
|
+
|
|
13
|
+
export const AndroidOperatorAudioCaptureStateSchema = z.enum(['idle', 'preparing', 'capturing', 'error']);
|
|
14
|
+
export type AndroidOperatorAudioCaptureState = z.infer<typeof AndroidOperatorAudioCaptureStateSchema>;
|
|
15
|
+
|
|
16
|
+
export const AndroidOperatorAudioMonitorStateSchema = z.enum(['idle', 'starting', 'playing', 'paused-for-ptt', 'error']);
|
|
17
|
+
export type AndroidOperatorAudioMonitorState = z.infer<typeof AndroidOperatorAudioMonitorStateSchema>;
|
|
18
|
+
|
|
19
|
+
export const AndroidOperatorAudioStatusSchema = z.object({
|
|
20
|
+
available: z.boolean(),
|
|
21
|
+
captureState: AndroidOperatorAudioCaptureStateSchema,
|
|
22
|
+
monitorState: AndroidOperatorAudioMonitorStateSchema,
|
|
23
|
+
participantIdentity: z.string().nullable(),
|
|
24
|
+
inputLevel: z.number().min(0).max(1),
|
|
25
|
+
inputPeak: z.number().min(0).max(1),
|
|
26
|
+
rawInputLevel: z.number().min(0).max(1).optional(),
|
|
27
|
+
rawInputPeak: z.number().min(0).max(1).optional(),
|
|
28
|
+
inputSilenced: z.boolean(),
|
|
29
|
+
micGainDb: z.number(),
|
|
30
|
+
micGainMinDb: z.number(),
|
|
31
|
+
micGainMaxDb: z.number(),
|
|
32
|
+
monitorGainDb: z.number(),
|
|
33
|
+
monitorGainMinDb: z.number(),
|
|
34
|
+
monitorGainMaxDb: z.number(),
|
|
35
|
+
micDevice: AndroidOperatorAudioDeviceSchema.nullable(),
|
|
36
|
+
speakerDevice: AndroidOperatorAudioDeviceSchema.nullable(),
|
|
37
|
+
lastError: z.string().nullable(),
|
|
38
|
+
});
|
|
39
|
+
export type AndroidOperatorAudioStatus = z.infer<typeof AndroidOperatorAudioStatusSchema>;
|
|
40
|
+
|
|
41
|
+
export const AndroidOperatorAudioGainUpdateSchema = z.object({
|
|
42
|
+
micGainDb: z.number(),
|
|
43
|
+
});
|
|
44
|
+
export type AndroidOperatorAudioGainUpdate = z.infer<typeof AndroidOperatorAudioGainUpdateSchema>;
|
|
45
|
+
|
|
46
|
+
export const AndroidOperatorAudioMonitorGainUpdateSchema = z.object({
|
|
47
|
+
monitorGainDb: z.number(),
|
|
48
|
+
});
|
|
49
|
+
export type AndroidOperatorAudioMonitorGainUpdate = z.infer<typeof AndroidOperatorAudioMonitorGainUpdateSchema>;
|
|
@@ -29,6 +29,29 @@ export const AudioSidecarErrorSchema = z.object({
|
|
|
29
29
|
|
|
30
30
|
export type AudioSidecarError = z.infer<typeof AudioSidecarErrorSchema>;
|
|
31
31
|
|
|
32
|
+
export const AudioSidecarPhaseSchema = z.enum([
|
|
33
|
+
'startStream',
|
|
34
|
+
'startOutput',
|
|
35
|
+
'runtime',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
export const AudioSidecarClassificationSchema = z.enum([
|
|
39
|
+
'startup-failure',
|
|
40
|
+
'runtime-loss',
|
|
41
|
+
'sample-rate-fallback',
|
|
42
|
+
'sample-rate-fallback-failed',
|
|
43
|
+
'device-unavailable',
|
|
44
|
+
'disabled',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
export const AudioSidecarFallbackSchema = z.object({
|
|
48
|
+
active: z.boolean(),
|
|
49
|
+
fromSampleRate: z.number().int().positive().nullable(),
|
|
50
|
+
toSampleRate: z.number().int().positive().nullable(),
|
|
51
|
+
persisted: z.boolean().optional(),
|
|
52
|
+
reason: z.string().optional(),
|
|
53
|
+
});
|
|
54
|
+
|
|
32
55
|
/**
|
|
33
56
|
* AudioSidecar 状态变化载荷。
|
|
34
57
|
* 前端以此渲染 RadioControl 中的音频副状态 spinner + Popover。
|
|
@@ -48,6 +71,18 @@ export const AudioSidecarStatusPayloadSchema = z.object({
|
|
|
48
71
|
lastError: AudioSidecarErrorSchema.nullable(),
|
|
49
72
|
/** 当前目标设备名(来自 AudioConfig.inputDeviceName) */
|
|
50
73
|
deviceName: z.string().nullable(),
|
|
74
|
+
/** 当前失败/重试阶段 */
|
|
75
|
+
phase: AudioSidecarPhaseSchema.optional(),
|
|
76
|
+
/** 机器可读的失败分类,用于 UI 展示和诊断 */
|
|
77
|
+
classification: AudioSidecarClassificationSchema.optional(),
|
|
78
|
+
/** 具体受影响的输入/输出设备名 */
|
|
79
|
+
affectedDeviceName: z.string().nullable().optional(),
|
|
80
|
+
/** 失败时使用的采样率 */
|
|
81
|
+
sampleRate: z.number().int().positive().nullable().optional(),
|
|
82
|
+
/** 自动降级状态 */
|
|
83
|
+
fallback: AudioSidecarFallbackSchema.optional(),
|
|
84
|
+
/** 当前重试原因摘要 */
|
|
85
|
+
retryReason: z.string().optional(),
|
|
51
86
|
});
|
|
52
87
|
|
|
53
88
|
export type AudioSidecarStatusPayload = z.infer<typeof AudioSidecarStatusPayloadSchema>;
|
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
+
export const AudioDeviceBackendSchema = z.enum([
|
|
4
|
+
'rtaudio',
|
|
5
|
+
'android',
|
|
6
|
+
'icom-wlan',
|
|
7
|
+
'openwebrx',
|
|
8
|
+
'tci',
|
|
9
|
+
'network',
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
export const AudioDeviceKindSchema = z.enum([
|
|
13
|
+
'usb',
|
|
14
|
+
'wired-headset',
|
|
15
|
+
'wired-headphones',
|
|
16
|
+
'analog-line',
|
|
17
|
+
'builtin-mic',
|
|
18
|
+
'builtin-speaker',
|
|
19
|
+
'network',
|
|
20
|
+
'virtual',
|
|
21
|
+
'unknown',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export const AudioDeviceTransportSchema = z.enum([
|
|
25
|
+
'usb',
|
|
26
|
+
'analog',
|
|
27
|
+
'builtin',
|
|
28
|
+
'network',
|
|
29
|
+
'virtual',
|
|
30
|
+
'unknown',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
export const AudioDeviceConnectorSchema = z.enum([
|
|
34
|
+
'usb',
|
|
35
|
+
'3.5mm',
|
|
36
|
+
'builtin',
|
|
37
|
+
'network',
|
|
38
|
+
'virtual',
|
|
39
|
+
'unknown',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
export const AudioDeviceRouteStateSchema = z.enum([
|
|
43
|
+
'idle',
|
|
44
|
+
'opening',
|
|
45
|
+
'verified',
|
|
46
|
+
'lost',
|
|
47
|
+
'unavailable',
|
|
48
|
+
'failed',
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
export const AudioDeviceCapabilitiesSchema = z.object({
|
|
52
|
+
sampleRates: z.array(z.number().int().positive()).optional(),
|
|
53
|
+
bufferSizes: z.array(z.number().int().positive()).optional(),
|
|
54
|
+
channelCounts: z.array(z.number().int().positive()).optional(),
|
|
55
|
+
sampleFormats: z.array(z.enum(['float32', 'int16'])).optional(),
|
|
56
|
+
channelModes: z.array(z.enum(['mono', 'left', 'right', 'both'])).optional(),
|
|
57
|
+
sampleRateConfigurable: z.boolean().optional(),
|
|
58
|
+
bufferSizeConfigurable: z.boolean().optional(),
|
|
59
|
+
sampleFormatConfigurable: z.boolean().optional(),
|
|
60
|
+
channelModeConfigurable: z.boolean().optional(),
|
|
61
|
+
outputRouteAck: z.boolean().optional(),
|
|
62
|
+
fixedSampleRate: z.number().int().positive().optional(),
|
|
63
|
+
fixedBufferSize: z.number().int().positive().optional(),
|
|
64
|
+
fixedChannelCount: z.number().int().positive().optional(),
|
|
65
|
+
});
|
|
66
|
+
|
|
3
67
|
// 音频设备信息
|
|
4
68
|
export const AudioDeviceSchema = z.object({
|
|
5
69
|
id: z.string(),
|
|
@@ -12,6 +76,26 @@ export const AudioDeviceSchema = z.object({
|
|
|
12
76
|
availability: z.enum(['available', 'cached', 'active']).optional(),
|
|
13
77
|
isActiveByTx5dr: z.boolean().optional(),
|
|
14
78
|
lastSeenAt: z.number().int().positive().optional(),
|
|
79
|
+
backend: AudioDeviceBackendSchema.optional(),
|
|
80
|
+
kind: AudioDeviceKindSchema.optional(),
|
|
81
|
+
routeKey: z.string().min(1).optional(),
|
|
82
|
+
transport: AudioDeviceTransportSchema.optional(),
|
|
83
|
+
connector: AudioDeviceConnectorSchema.optional(),
|
|
84
|
+
clientConnected: z.boolean().optional(),
|
|
85
|
+
routeVerified: z.boolean().optional(),
|
|
86
|
+
routeState: AudioDeviceRouteStateSchema.optional(),
|
|
87
|
+
failureReason: z.string().min(1).optional(),
|
|
88
|
+
capabilities: AudioDeviceCapabilitiesSchema.optional(),
|
|
89
|
+
/** Human-readable secondary label (radio identity · VID:PID · USB path). */
|
|
90
|
+
detail: z.string().optional(),
|
|
91
|
+
/** Stable hardware identity when available (e.g. usb:1-7.2.4). */
|
|
92
|
+
hardwareId: z.string().optional(),
|
|
93
|
+
vendorId: z.string().optional(),
|
|
94
|
+
productId: z.string().optional(),
|
|
95
|
+
serialNumber: z.string().optional(),
|
|
96
|
+
usbPath: z.string().optional(),
|
|
97
|
+
alsaCard: z.number().int().nonnegative().optional(),
|
|
98
|
+
alsaCardId: z.string().optional(),
|
|
15
99
|
});
|
|
16
100
|
|
|
17
101
|
export const AudioDeviceResolutionStatusSchema = z.enum([
|
|
@@ -23,6 +107,9 @@ export const AudioDeviceResolutionStatusSchema = z.enum([
|
|
|
23
107
|
|
|
24
108
|
export const AudioDeviceResolutionSchema = z.object({
|
|
25
109
|
configuredDeviceName: z.string().nullable(),
|
|
110
|
+
configuredRouteKey: z.string().nullable().optional(),
|
|
111
|
+
configuredDeviceId: z.string().nullable().optional(),
|
|
112
|
+
configuredHardwareId: z.string().nullable().optional(),
|
|
26
113
|
configuredDevice: AudioDeviceSchema.nullable(),
|
|
27
114
|
effectiveDevice: AudioDeviceSchema.nullable(),
|
|
28
115
|
status: AudioDeviceResolutionStatusSchema,
|
|
@@ -37,6 +124,13 @@ export const AudioDeviceResolutionSetSchema = z.object({
|
|
|
37
124
|
export const AudioOutputSampleFormatSchema = z.enum(['float32', 'int16']);
|
|
38
125
|
export const AudioOutputChannelModeSchema = z.enum(['mono', 'left', 'right', 'both']);
|
|
39
126
|
|
|
127
|
+
/**
|
|
128
|
+
* How USB/ACC capture PCM should be interpreted before the internal 12 kHz AF ring.
|
|
129
|
+
* - af: already-demodulated SSB baseband (default)
|
|
130
|
+
* - icom-12k-if: Icom ACC/USB Output Select=IF (~12 kHz IF); software SSB demod required
|
|
131
|
+
*/
|
|
132
|
+
export const AudioInputSignalTypeSchema = z.enum(['af', 'icom-12k-if']);
|
|
133
|
+
|
|
40
134
|
// 音频设备列表响应
|
|
41
135
|
export const AudioDevicesResponseSchema = z.object({
|
|
42
136
|
inputDevices: z.array(AudioDeviceSchema),
|
|
@@ -48,13 +142,25 @@ export const AudioDevicesResponseSchema = z.object({
|
|
|
48
142
|
// 音频设备设置请求
|
|
49
143
|
export const AudioDeviceSettingsSchema = z.object({
|
|
50
144
|
inputDeviceName: z.string().optional(), // 使用设备名称而非ID
|
|
51
|
-
outputDeviceName: z.string().optional(), // 使用设备名称而非ID
|
|
145
|
+
outputDeviceName: z.string().optional(), // 使用设备名称而非ID
|
|
146
|
+
inputRouteKey: z.string().min(1).nullable().optional(),
|
|
147
|
+
outputRouteKey: z.string().min(1).nullable().optional(),
|
|
148
|
+
/** Runtime device id (e.g. input-130). Preferred over name when unique. */
|
|
149
|
+
inputDeviceId: z.string().optional(),
|
|
150
|
+
outputDeviceId: z.string().optional(),
|
|
151
|
+
/** Stable hardware id when available (e.g. usb:1-7.2.4). Preferred over runtime id. */
|
|
152
|
+
inputHardwareId: z.string().optional(),
|
|
153
|
+
outputHardwareId: z.string().optional(),
|
|
52
154
|
inputSampleRate: z.number().optional(),
|
|
53
155
|
outputSampleRate: z.number().optional(),
|
|
54
156
|
inputBufferSize: z.number().optional(),
|
|
55
157
|
outputBufferSize: z.number().optional(),
|
|
56
158
|
outputSampleFormat: AudioOutputSampleFormatSchema.optional(),
|
|
57
159
|
outputChannelMode: AudioOutputChannelModeSchema.optional(),
|
|
160
|
+
/** Capture signal interpretation. Defaults to demodulated AF baseband. */
|
|
161
|
+
inputSignalType: AudioInputSignalTypeSchema.optional(),
|
|
162
|
+
/** IF center frequency in Hz when inputSignalType is icom-12k-if (default 12000). */
|
|
163
|
+
ifCenterHz: z.number().positive().max(24000).optional(),
|
|
58
164
|
sampleRate: z.number().optional(),
|
|
59
165
|
bufferSize: z.number().optional(),
|
|
60
166
|
});
|
|
@@ -69,7 +175,7 @@ export const AudioDeviceSettingsResponseSchema = z.object({
|
|
|
69
175
|
|
|
70
176
|
export const AudioSettingsResolveRequestSchema = z.object({
|
|
71
177
|
audio: AudioDeviceSettingsSchema,
|
|
72
|
-
radioType: z.enum(['none', 'network', 'serial', 'icom-wlan']).optional(),
|
|
178
|
+
radioType: z.enum(['none', 'network', 'serial', 'icom-wlan', 'tci']).optional(),
|
|
73
179
|
});
|
|
74
180
|
|
|
75
181
|
export const AudioSettingsResolveResponseSchema = z.object({
|
|
@@ -110,11 +216,18 @@ export const VolumeGainSchema = z.object({
|
|
|
110
216
|
|
|
111
217
|
// 导出类型
|
|
112
218
|
export type AudioDevice = z.infer<typeof AudioDeviceSchema>;
|
|
219
|
+
export type AudioDeviceBackend = z.infer<typeof AudioDeviceBackendSchema>;
|
|
220
|
+
export type AudioDeviceKind = z.infer<typeof AudioDeviceKindSchema>;
|
|
221
|
+
export type AudioDeviceTransport = z.infer<typeof AudioDeviceTransportSchema>;
|
|
222
|
+
export type AudioDeviceConnector = z.infer<typeof AudioDeviceConnectorSchema>;
|
|
223
|
+
export type AudioDeviceRouteState = z.infer<typeof AudioDeviceRouteStateSchema>;
|
|
224
|
+
export type AudioDeviceCapabilities = z.infer<typeof AudioDeviceCapabilitiesSchema>;
|
|
113
225
|
export type AudioDeviceResolutionStatus = z.infer<typeof AudioDeviceResolutionStatusSchema>;
|
|
114
226
|
export type AudioDeviceResolution = z.infer<typeof AudioDeviceResolutionSchema>;
|
|
115
227
|
export type AudioDeviceResolutionSet = z.infer<typeof AudioDeviceResolutionSetSchema>;
|
|
116
228
|
export type AudioOutputSampleFormat = z.infer<typeof AudioOutputSampleFormatSchema>;
|
|
117
229
|
export type AudioOutputChannelMode = z.infer<typeof AudioOutputChannelModeSchema>;
|
|
230
|
+
export type AudioInputSignalType = z.infer<typeof AudioInputSignalTypeSchema>;
|
|
118
231
|
export type AudioDevicesResponse = z.infer<typeof AudioDevicesResponseSchema>;
|
|
119
232
|
export type AudioDeviceSettings = z.infer<typeof AudioDeviceSettingsSchema>;
|
|
120
233
|
export type AudioDeviceSettingsResponse = z.infer<typeof AudioDeviceSettingsResponseSchema>;
|