Skip to content

Commit b5b55fe

Browse files
Unify hardware driver HAL and consolidate HackRF under src/drivers (#241)
* Initial plan * feat: unify hardware driver HAL and move HackRF to src/drivers/hackrf Co-authored-by: alexthemitchell <5687158+alexthemitchell@users.noreply.github.com> * docs: update drivers README with HAL unification details Co-authored-by: alexthemitchell <5687158+alexthemitchell@users.noreply.github.com> * fix: resolve TypeScript type errors in driver exports and conformance tests Co-authored-by: alexthemitchell <5687158+alexthemitchell@users.noreply.github.com> * fix: improve null safety check in conformance test sample processing Co-authored-by: alexthemitchell <5687158+alexthemitchell@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexthemitchell <5687158+alexthemitchell@users.noreply.github.com> Co-authored-by: Alex Mitchell <alex+github@alexmitchelltech.com>
1 parent f895b19 commit b5b55fe

25 files changed

+366
-16
lines changed

jest.config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ const config: Config = {
2525
functions: 66,
2626
lines: 64,
2727
},
28-
// HackRF implementation - updated to current coverage
29-
"./src/hackrf/HackRFOne.ts": {
28+
// HackRF implementation - updated paths after moving to src/drivers/hackrf
29+
"./src/drivers/hackrf/HackRFOne.ts": {
3030
statements: 76,
3131
branches: 60,
3232
functions: 95,
3333
lines: 75,
3434
},
35-
"./src/hackrf/HackRFOneAdapter.ts": {
35+
"./src/drivers/hackrf/HackRFOneAdapter.ts": {
3636
statements: 96,
3737
branches: 83,
3838
functions: 93,

src/drivers/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,15 +138,19 @@ registerBuiltinDrivers();
138138

139139
## Built-in Drivers
140140

141+
All built-in drivers are located in subdirectories under `src/drivers/`:
142+
141143
### HackRF One
142144

145+
- **Location**: `src/drivers/hackrf/`
143146
- **ID**: `hackrf-one`
144147
- **Frequency Range**: 1 MHz - 6 GHz
145148
- **Sample Rates**: 1-20 MHz
146149
- **USB**: VID 0x1d50, PID 0x6089
147150

148151
### RTL-SDR
149152

153+
- **Location**: `src/models/` (planned to be moved to `src/drivers/rtlsdr/`)
150154
- **ID**: `rtl-sdr`
151155
- **Frequency Range**: 24 MHz - 1.7 GHz
152156
- **Sample Rates**: 225 kHz - 3.2 MHz
@@ -391,11 +395,26 @@ All components are fully tested:
391395
- `DeviceDiscovery.test.ts` - 14 tests
392396
- `DriverHotReload.test.ts` - 14 tests
393397
- `registerBuiltinDrivers.test.ts` - 16 tests
398+
- `DriverConformance.test.ts` - 36 tests (validates HAL contract)
399+
400+
### Conformance Tests
401+
402+
The `DriverConformance.test.ts` suite validates that all drivers properly implement the `ISDRDevice` HAL interface. Every driver must pass these tests to ensure:
403+
404+
- Correct interface implementation
405+
- Consistent capability reporting
406+
- Proper lifecycle management
407+
- Standard sample processing
408+
- Memory management compliance
394409

395410
Run tests:
396411

397412
```bash
413+
# All driver tests
398414
npm test -- src/drivers/__tests__
415+
416+
# Conformance tests only
417+
npm test -- src/drivers/__tests__/DriverConformance.test.ts
399418
```
400419

401420
## Migration Guide
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
/**
2+
* Driver Conformance Tests
3+
*
4+
* This test suite validates that all SDR drivers properly implement the ISDRDevice
5+
* interface and conform to the Hardware Abstraction Layer (HAL) contract.
6+
*
7+
* Every driver MUST pass these conformance tests to ensure consistent behavior
8+
* across different hardware implementations.
9+
*/
10+
11+
import type { ISDRDevice } from "../../models/SDRDevice";
12+
import { HackRFOneAdapter } from "../hackrf/HackRFOneAdapter";
13+
import { RTLSDRDeviceAdapter } from "../../models/RTLSDRDeviceAdapter";
14+
15+
/**
16+
* Create a mock USB device for testing
17+
*/
18+
function createMockUSBDevice(vendorId: number, productId: number): USBDevice {
19+
// Create mock endpoints for bulk transfers
20+
const mockEndpointIn: USBEndpoint = {
21+
endpointNumber: 1,
22+
direction: "in",
23+
type: "bulk",
24+
packetSize: 16384,
25+
};
26+
27+
const mockEndpointOut: USBEndpoint = {
28+
endpointNumber: 2,
29+
direction: "out",
30+
type: "bulk",
31+
packetSize: 16384,
32+
};
33+
34+
// Create a mock interface with both endpoints
35+
const mockInterface: USBInterface = {
36+
interfaceNumber: 0,
37+
alternate: {
38+
alternateSetting: 0,
39+
interfaceClass: 0xff, // Vendor-specific
40+
interfaceSubclass: 0,
41+
interfaceProtocol: 0,
42+
interfaceName: "HackRF Interface",
43+
endpoints: [mockEndpointIn, mockEndpointOut],
44+
},
45+
alternates: [],
46+
claimed: true, // Pre-claimed for testing
47+
};
48+
49+
const mockConfiguration: USBConfiguration = {
50+
configurationValue: 1,
51+
configurationName: "HackRF Configuration",
52+
interfaces: [mockInterface],
53+
};
54+
55+
const mockDevice = {
56+
opened: true, // Pre-opened for testing
57+
vendorId,
58+
productId,
59+
productName: "Mock Device",
60+
manufacturerName: "Mock",
61+
serialNumber: "TEST123",
62+
usbVersionMajor: 2,
63+
usbVersionMinor: 0,
64+
usbVersionSubminor: 0,
65+
deviceClass: 0xff,
66+
deviceSubclass: 0,
67+
deviceProtocol: 0,
68+
deviceVersionMajor: 1,
69+
deviceVersionMinor: 0,
70+
deviceVersionSubminor: 0,
71+
configurations: [mockConfiguration],
72+
configuration: mockConfiguration,
73+
controlTransferOut: jest.fn().mockResolvedValue({
74+
status: "ok",
75+
bytesWritten: 0,
76+
} as USBOutTransferResult),
77+
controlTransferIn: jest.fn().mockResolvedValue({
78+
data: new DataView(new ArrayBuffer(256)),
79+
status: "ok",
80+
} as USBInTransferResult),
81+
transferIn: jest.fn().mockResolvedValue({
82+
data: new DataView(new ArrayBuffer(4096)),
83+
status: "ok",
84+
} as USBInTransferResult),
85+
transferOut: jest.fn().mockResolvedValue({
86+
status: "ok",
87+
bytesWritten: 4096,
88+
} as USBOutTransferResult),
89+
open: jest.fn().mockResolvedValue(undefined),
90+
close: jest.fn().mockResolvedValue(undefined),
91+
reset: jest.fn().mockResolvedValue(undefined),
92+
clearHalt: jest.fn().mockResolvedValue(undefined),
93+
selectConfiguration: jest.fn().mockResolvedValue(undefined),
94+
selectAlternateInterface: jest.fn().mockResolvedValue(undefined),
95+
claimInterface: jest.fn().mockResolvedValue(undefined),
96+
releaseInterface: jest.fn().mockResolvedValue(undefined),
97+
forget: jest.fn().mockResolvedValue(undefined),
98+
} as unknown as USBDevice;
99+
100+
return mockDevice;
101+
}
102+
103+
/**
104+
* Test suite factory for driver conformance tests
105+
*/
106+
function createDriverConformanceTests(
107+
driverName: string,
108+
createDriver: (usbDevice: USBDevice) => ISDRDevice,
109+
vendorId: number,
110+
productId: number,
111+
): void {
112+
describe(`${driverName} HAL Conformance`, () => {
113+
let device: ISDRDevice;
114+
let mockUSBDevice: USBDevice;
115+
116+
beforeEach(() => {
117+
mockUSBDevice = createMockUSBDevice(vendorId, productId);
118+
device = createDriver(mockUSBDevice);
119+
});
120+
121+
afterEach(async () => {
122+
if (device.isOpen()) {
123+
await device.close();
124+
}
125+
});
126+
127+
describe("Device Information", () => {
128+
it("should provide device info", async () => {
129+
const info = await device.getDeviceInfo();
130+
expect(info).toBeDefined();
131+
expect(info.type).toBeDefined();
132+
expect(info.vendorId).toBe(vendorId);
133+
expect(info.productId).toBe(productId);
134+
});
135+
136+
it("should provide device capabilities", () => {
137+
const capabilities = device.getCapabilities();
138+
expect(capabilities).toBeDefined();
139+
expect(capabilities.minFrequency).toBeGreaterThan(0);
140+
expect(capabilities.maxFrequency).toBeGreaterThan(
141+
capabilities.minFrequency,
142+
);
143+
expect(Array.isArray(capabilities.supportedSampleRates)).toBe(true);
144+
expect(capabilities.supportedSampleRates.length).toBeGreaterThan(0);
145+
});
146+
});
147+
148+
describe("Device Lifecycle", () => {
149+
it("should have isOpen method", () => {
150+
expect(typeof device.isOpen).toBe("function");
151+
expect(typeof device.isOpen()).toBe("boolean");
152+
});
153+
154+
it("should have open and close methods", () => {
155+
expect(typeof device.open).toBe("function");
156+
expect(typeof device.close).toBe("function");
157+
});
158+
159+
it("should have isReceiving method", () => {
160+
expect(typeof device.isReceiving).toBe("function");
161+
expect(typeof device.isReceiving()).toBe("boolean");
162+
});
163+
});
164+
165+
describe("Frequency Control", () => {
166+
it("should have setFrequency and getFrequency methods", () => {
167+
expect(typeof device.setFrequency).toBe("function");
168+
expect(typeof device.getFrequency).toBe("function");
169+
});
170+
171+
it("should define valid frequency range in capabilities", () => {
172+
const capabilities = device.getCapabilities();
173+
expect(capabilities.minFrequency).toBeGreaterThan(0);
174+
expect(capabilities.maxFrequency).toBeGreaterThan(
175+
capabilities.minFrequency,
176+
);
177+
});
178+
});
179+
180+
describe("Sample Rate Control", () => {
181+
it("should have setSampleRate and getSampleRate methods", () => {
182+
expect(typeof device.setSampleRate).toBe("function");
183+
expect(typeof device.getSampleRate).toBe("function");
184+
});
185+
186+
it("should have getUsableBandwidth method", () => {
187+
expect(typeof device.getUsableBandwidth).toBe("function");
188+
});
189+
190+
it("should define supported sample rates in capabilities", () => {
191+
const capabilities = device.getCapabilities();
192+
expect(Array.isArray(capabilities.supportedSampleRates)).toBe(true);
193+
expect(capabilities.supportedSampleRates.length).toBeGreaterThan(0);
194+
});
195+
});
196+
197+
describe("Gain Control", () => {
198+
it("should have setLNAGain method", () => {
199+
expect(typeof device.setLNAGain).toBe("function");
200+
});
201+
202+
it("should have setAmpEnable method", () => {
203+
expect(typeof device.setAmpEnable).toBe("function");
204+
});
205+
206+
it("should declare amp and antenna support in capabilities", () => {
207+
const capabilities = device.getCapabilities();
208+
expect(typeof capabilities.supportsAmpControl).toBe("boolean");
209+
expect(typeof capabilities.supportsAntennaControl).toBe("boolean");
210+
});
211+
});
212+
213+
describe("Sample Processing", () => {
214+
it("should parse samples", () => {
215+
// Create test data
216+
const testData = new DataView(new ArrayBuffer(1024));
217+
218+
const samples = device.parseSamples(testData);
219+
expect(Array.isArray(samples)).toBe(true);
220+
221+
// Each sample should have I and Q components
222+
if (samples.length > 0 && samples[0]) {
223+
const firstSample = samples[0];
224+
expect(firstSample).toBeDefined();
225+
expect(firstSample).toHaveProperty("I");
226+
expect(firstSample).toHaveProperty("Q");
227+
expect(typeof firstSample.I).toBe("number");
228+
expect(typeof firstSample.Q).toBe("number");
229+
}
230+
});
231+
});
232+
233+
describe("Memory Management", () => {
234+
it("should provide memory info", () => {
235+
const memInfo = device.getMemoryInfo();
236+
expect(memInfo).toBeDefined();
237+
expect(memInfo.totalBufferSize).toBeGreaterThanOrEqual(0);
238+
expect(memInfo.usedBufferSize).toBeGreaterThanOrEqual(0);
239+
expect(memInfo.activeBuffers).toBeGreaterThanOrEqual(0);
240+
});
241+
242+
it("should clear buffers", () => {
243+
expect(() => device.clearBuffers()).not.toThrow();
244+
});
245+
});
246+
247+
describe("Device Reset", () => {
248+
it("should have reset method", () => {
249+
expect(typeof device.reset).toBe("function");
250+
});
251+
});
252+
});
253+
}
254+
255+
// Run conformance tests for all built-in drivers
256+
describe("Hardware Driver HAL Conformance Tests", () => {
257+
createDriverConformanceTests(
258+
"HackRF One",
259+
(usbDevice) => new HackRFOneAdapter(usbDevice),
260+
0x1d50, // HackRF vendor ID
261+
0x6089, // HackRF product ID
262+
);
263+
264+
createDriverConformanceTests(
265+
"RTL-SDR",
266+
(usbDevice) => new RTLSDRDeviceAdapter(usbDevice),
267+
0x0bda, // RTL-SDR vendor ID
268+
0x2838, // RTL-SDR product ID
269+
);
270+
});
271+
272+
/**
273+
* Additional HAL interface validation tests
274+
*/
275+
describe("HAL Interface Validation", () => {
276+
it("should export ISDRDevice interface from drivers module", () => {
277+
// This is a type-checking test - it validates at compile time
278+
const checkInterface = (device: ISDRDevice): void => {
279+
// Required methods
280+
expect(typeof device.getDeviceInfo).toBe("function");
281+
expect(typeof device.getCapabilities).toBe("function");
282+
expect(typeof device.open).toBe("function");
283+
expect(typeof device.close).toBe("function");
284+
expect(typeof device.isOpen).toBe("function");
285+
expect(typeof device.setFrequency).toBe("function");
286+
expect(typeof device.getFrequency).toBe("function");
287+
expect(typeof device.setSampleRate).toBe("function");
288+
expect(typeof device.getSampleRate).toBe("function");
289+
expect(typeof device.getUsableBandwidth).toBe("function");
290+
expect(typeof device.setLNAGain).toBe("function");
291+
expect(typeof device.setAmpEnable).toBe("function");
292+
expect(typeof device.receive).toBe("function");
293+
expect(typeof device.stopRx).toBe("function");
294+
expect(typeof device.isReceiving).toBe("function");
295+
expect(typeof device.parseSamples).toBe("function");
296+
expect(typeof device.getMemoryInfo).toBe("function");
297+
expect(typeof device.clearBuffers).toBe("function");
298+
expect(typeof device.reset).toBe("function");
299+
};
300+
301+
// Create a mock device to test
302+
const mockDevice = createMockUSBDevice(0x1d50, 0x6089);
303+
const hackrf = new HackRFOneAdapter(mockDevice);
304+
checkInterface(hackrf);
305+
});
306+
307+
it("should have consistent capabilities structure", () => {
308+
const mockDevice = createMockUSBDevice(0x1d50, 0x6089);
309+
const hackrf = new HackRFOneAdapter(mockDevice);
310+
const caps = hackrf.getCapabilities();
311+
312+
// Required fields
313+
expect(caps).toHaveProperty("minFrequency");
314+
expect(caps).toHaveProperty("maxFrequency");
315+
expect(caps).toHaveProperty("supportedSampleRates");
316+
expect(caps).toHaveProperty("supportsAmpControl");
317+
expect(caps).toHaveProperty("supportsAntennaControl");
318+
319+
// Type validation
320+
expect(typeof caps.minFrequency).toBe("number");
321+
expect(typeof caps.maxFrequency).toBe("number");
322+
expect(Array.isArray(caps.supportedSampleRates)).toBe(true);
323+
expect(typeof caps.supportsAmpControl).toBe("boolean");
324+
expect(typeof caps.supportsAntennaControl).toBe("boolean");
325+
});
326+
});

0 commit comments

Comments
 (0)