From 74538a769f3b50cefc42d4125d82e2b7f4ad7b41 Mon Sep 17 00:00:00 2001 From: Clifford Heath Date: Tue, 8 Sep 2020 15:27:26 +1000 Subject: [PATCH 1/5] Hacked node-speaker exports and other imports here to get examples working --- examples/fm_receiver.ts | 2 +- examples/fm_tone.ts | 2 +- package-lock.json | 18 +++++------------- package.json | 4 ++-- tsconfig.json | 3 ++- 5 files changed, 11 insertions(+), 18 deletions(-) diff --git a/examples/fm_receiver.ts b/examples/fm_receiver.ts index 4800147..3fda847 100644 --- a/examples/fm_receiver.ts +++ b/examples/fm_receiver.ts @@ -6,7 +6,7 @@ * (SDR - audio card) but it should be functional. */ -import { open } from '..' +import { open } from '../lib' import Speaker from 'speaker' async function main() { diff --git a/examples/fm_tone.ts b/examples/fm_tone.ts index e0f94cb..8c861b4 100644 --- a/examples/fm_tone.ts +++ b/examples/fm_tone.ts @@ -2,7 +2,7 @@ * Transmit a tone through FM radio. */ -import { open } from '..' +import { open } from '../lib' async function main() { const fs = 2e6 diff --git a/package-lock.json b/package-lock.json index 68cf7be..b68ef3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -811,18 +811,11 @@ "dev": true }, "@types/usb": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@types/usb/-/usb-1.5.1.tgz", - "integrity": "sha512-1qhcYMLJ0I2HcRG3G/nBcRZ0KrrTdGdUNcCkEVgcga4KMlDXWh6LZJjVA6MiWEDa+BOaQTEfGJfuNaQ71IQOpg==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@types/usb/-/usb-1.5.2.tgz", + "integrity": "sha512-mYGrpSQ+qYApVDQRXdjnbMpFxuO9o85YO6EfTAZwEWNZf4ptbWTT59g3WmyA/QmpecAD3ZRjdN0UkpZGcu/TVg==", "requires": { "@types/node": "*" - }, - "dependencies": { - "@types/node": { - "version": "14.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.0.tgz", - "integrity": "sha512-mikldZQitV94akrc4sCcSjtJfsTKt4p+e/s0AGscVA6XArQ9kFclP+ZiYUMnq987rc6QlYxXv/EivqlfSLxpKA==" - } } }, "@types/yargs": { @@ -4746,9 +4739,8 @@ "dev": true }, "speaker": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/speaker/-/speaker-0.5.2.tgz", - "integrity": "sha512-5jGAnNPhg1NSf2d8OAtmJsZUHtlK4zqvS6esyNXs5/+30sBNnjqO0XViVVUlXWynUL2rEWUxn/5a2kTduPpsjA==", + "version": "git+https://github.com/cjheath/node-speaker.git#09377c3544742a32afaa4e8aeff8a6ada33f0205", + "from": "git+https://github.com/cjheath/node-speaker.git", "dev": true, "requires": { "bindings": "^1.3.0", diff --git a/package.json b/package.json index 6a272eb..01eebd5 100644 --- a/package.json +++ b/package.json @@ -61,14 +61,14 @@ }, "dependencies": { "@types/node": "^13.13.15", - "@types/usb": "^1.5.1", + "@types/usb": "^1.5.2", "usb": "^1.6.3" }, "devDependencies": { "@types/jest": "^23.3.14", "coveralls": "^3.1.0", "jest": "^26.4.2", - "speaker": "^0.5.2", + "speaker": "https://github.com/cjheath/node-speaker.git", "ts-jest": "^26.2.0", "ts-node": "^9.0.0", "typedoc": "^0.17.0-3", diff --git a/tsconfig.json b/tsconfig.json index 81bee93..8c02c7e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,7 @@ "emitDecoratorMetadata": true }, "include": [ - "lib" + "lib", + "examples" ] } From 1370e8aa1ff2ac8b50c774d002b507e12572fd32 Mon Sep 17 00:00:00 2001 From: Clifford Heath Date: Wed, 6 Nov 2024 12:55:53 +1100 Subject: [PATCH 2/5] Added Speaker close event message --- examples/fm_receiver.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/fm_receiver.ts b/examples/fm_receiver.ts index 6367fe0..dc9001d 100644 --- a/examples/fm_receiver.ts +++ b/examples/fm_receiver.ts @@ -8,7 +8,6 @@ import { open } from '../lib' import Speaker from 'speaker' -// import Speaker = require('speaker') async function main() { const fs = 1200e3 @@ -50,7 +49,11 @@ async function main() { const signal = (x: Complex) => channelFilter(shifter(x)) console.error(`Receiving at ${(carrierFrequency/1e6).toFixed(2)}MHz...`) - process.on('SIGINT', () => device.requestStop()) + speaker.on('close', () => { console.error(`Speaker closed`); }); + process.on('SIGINT', () => { + speaker.end(); // Close the stream, triggering the Speaker close event + device.requestStop(); + } ) await device.receive(array => { const samples = array.length / 2 for (let n = 0; n < samples; n++) From d66d9ebd542159b8c1766aba5a8b800f06d12661 Mon Sep 17 00:00:00 2001 From: Clifford Heath Date: Wed, 11 Dec 2024 16:31:55 +1100 Subject: [PATCH 3/5] Working toward a production acceptance test for the HackRF, using a second one connected via an attenuator --- examples/acceptance_test.ts | 71 +++++++++++++++++++++++++++++ examples/fm_receiver.ts | 8 ++-- examples/fm_tone.ts | 12 +++-- examples/list_devices.ts | 15 +++++++ examples/scan_hackrfs.ts | 25 +++++++++++ examples/test_sequence.ts | 90 +++++++++++++++++++++++++++++++++++++ examples/tsconfig.json | 3 +- lib/index.ts | 4 ++ lib/scan.ts | 28 ++++++++++++ tsconfig.json | 2 +- 10 files changed, 249 insertions(+), 9 deletions(-) create mode 100644 examples/acceptance_test.ts create mode 100644 examples/list_devices.ts create mode 100644 examples/scan_hackrfs.ts create mode 100644 examples/test_sequence.ts create mode 100644 lib/scan.ts diff --git a/examples/acceptance_test.ts b/examples/acceptance_test.ts new file mode 100644 index 0000000..10fa133 --- /dev/null +++ b/examples/acceptance_test.ts @@ -0,0 +1,71 @@ +/* + * Configure a HackRF as test rig, another as test subject (DUT) and run tests interactively + */ + +import { DeviceInfo, listDevices, UsbBoardId, visible_hackrfs, scan_hackrfs } from '../lib' +import { run_test_sequence } from './test_sequence' + +function timeout(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function await_hackrf(device: string) { + process.stdout.write(`Please connect ${device}: `); + for (;;) { + for await (const device_change of scan_hackrfs()) { + if (device_change.added) { + console.log(`\nPlugged in: ${device_change.added.serialNumber}`) + process.stdout.write("\n"); + return device_change.added; + } + /* Ignore removals for now + else if (device_change.removed) { + console.log(`Unplugged: ${device_change.removed.serialNumber}`) + return undefined; + } + */ + } + + await timeout(1000); // Sleep for a second before trying again + } +} + +async function await_rig() +{ + // Do the initial scan to see what's connected + for await (const device_change of scan_hackrfs()) { scan_hackrfs(); } + + while (visible_hackrfs.length > 1) { + process.stdout.write(`${visible_hackrfs.length} HackRFs connected, unplug all but the test controller please\r`) + await timeout(1000); // Sleep for a second before trying again + for await (const device_change of scan_hackrfs()) { scan_hackrfs(); } + } + + if (visible_hackrfs.length == 1) + { + console.log(`Detected ${visible_hackrfs[0].serialNumber} as test controller`) + return visible_hackrfs[0]; + } + return await_hackrf("test controller HackRF"); +} + +async function await_dut() +{ + return await_hackrf("HackRF to be tested"); +} + +async function main() { + + var rig = await await_rig(); + for (;;) { + var dut = await await_dut(); + if (!dut) continue; + + console.log(`Detected ${dut.serialNumber} as DUT`) + + await run_test_sequence(rig, dut) + break; + } + process.exit(0) +} +main() diff --git a/examples/fm_receiver.ts b/examples/fm_receiver.ts index dc9001d..e694fa1 100644 --- a/examples/fm_receiver.ts +++ b/examples/fm_receiver.ts @@ -15,12 +15,13 @@ async function main() { const carrierFrequency = 101.7e6 const carrierDeviation = 75e3 - const device = await open() + const serial = process.argv[2] + const device = await open(serial) await device.setFrequency(carrierFrequency + tuneOffset) await device.setSampleRate(fs) await device.setAmpEnable(false) - await device.setLnaGain(24) - await device.setVgaGain(8) + await device.setLnaGain(32) + await device.setVgaGain(22) // Collect audio samples & play back const speaker = new Speaker({ sampleRate: 48000, channels: 1, bitDepth: 16 }) @@ -61,6 +62,7 @@ async function main() { }) speaker.destroy() console.error('\nDone, exiting') + process.exit(0) } main() diff --git a/examples/fm_tone.ts b/examples/fm_tone.ts index e42a6e5..2af3576 100644 --- a/examples/fm_tone.ts +++ b/examples/fm_tone.ts @@ -12,24 +12,28 @@ async function main() { const toneFrequency = 400 const toneAmplitude = .4 - const device = await open() + const serial = process.argv[2] + const device = await open(serial) await device.setFrequency(carrierFrequency) await device.setSampleRate(fs) await device.setAmpEnable(false) - await device.setTxVgaGain(30) + await device.setTxVgaGain(47) const tone = makeToneGeneratorF(fs, toneFrequency, toneAmplitude) const modulator = makeFrequencyMod(fs, carrierDeviation, carrierAmplitude) const signal = () => quantize( modulator(tone()) ) console.log(`Transmitting at ${(carrierFrequency/1e6).toFixed(2)}MHz...`) - process.on('SIGINT', () => device.requestStop()) + process.on('SIGINT', () => { device.requestStop(); }) + let block_count = 0; await device.transmit(array => { + block_count++; const samples = array.length / 2 for (let n = 0; n < samples; n++) array.set(signal(), n * 2) }) - console.log('\nDone, exiting') + console.log(`\nDone after ${block_count} blocks, exiting`); + process.exit(0) } main() diff --git a/examples/list_devices.ts b/examples/list_devices.ts new file mode 100644 index 0000000..9bd0a1d --- /dev/null +++ b/examples/list_devices.ts @@ -0,0 +1,15 @@ + +import { listDevices, UsbBoardId } from '../lib' + +async function main() { + let ld = listDevices(); + console.log(ld) + for await (const info of ld) { + console.log(info.device) + console.log(`Found ${info.usbBoardId} = ${UsbBoardId[info.usbBoardId]}`) + console.log(`Serial: ${info.serialNumber}`) + } + + process.exit(0) +} +main() diff --git a/examples/scan_hackrfs.ts b/examples/scan_hackrfs.ts new file mode 100644 index 0000000..3ae8f36 --- /dev/null +++ b/examples/scan_hackrfs.ts @@ -0,0 +1,25 @@ +/* + * Continuously scan for HackRF devices, announcing when each appears or disappears + */ + +import { DeviceInfo, listDevices, UsbBoardId, visible_hackrfs, scan_hackrfs } from '../lib' + +function timeout(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function main() { + for (;;) { + for await (const device_change of scan_hackrfs()) { + if (device_change.added) { + console.log(`Plugged in: ${device_change.added.serialNumber}`) + } else if (device_change.removed) { + console.log(`Unplugged: ${device_change.removed.serialNumber}`) + } + } + + await timeout(1000); // Sleep for a second before trying again + } + process.exit(0) +} +main() diff --git a/examples/test_sequence.ts b/examples/test_sequence.ts new file mode 100644 index 0000000..3a03e94 --- /dev/null +++ b/examples/test_sequence.ts @@ -0,0 +1,90 @@ +/* + * Testing sequence for the HackRF. + * + * Initially just test the noise floor at a range of frequencies. + * Noise floor is simplified to just the variance in the magnitude of the I/Q vectors. + */ +import { open, HackrfDevice, DeviceInfo, UsbBoardId } from '../lib' + +type Complex = [number, number] + +function timeout(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +/* + * Not really an FFT, not yet. But aspiring to be + */ +class FFT { + protected num_samples: number = 0; + protected sum_mag: number = 0; + protected sum_mag_squared: number = 0; + + constructor(_logSize: number) { + // Allocate array of size 2^_logSize + } + + accumulate(x: Complex): void { + const mag_squared = x[0]*x[0] + x[1]*x[1] + this.sum_mag_squared += mag_squared; + this.num_samples++; + } + + noise_floor(): number + { + return Math.sqrt(this.sum_mag_squared/this.num_samples); + } +} + +async function noise_floor_test(dut: HackrfDevice, frequency: number, sample_rate: number): Promise { + console.log(`Measuring noise floor at ${frequency}`) + + const num_seconds = 1; + + await dut.setFrequency(frequency) + await dut.setSampleRate(sample_rate) + await dut.setAmpEnable(false) + await dut.setLnaGain(32) + await dut.setVgaGain(48) + + var fft = new FFT(12); + var num_samples = 0; + await dut.receive((array): undefined | void | false => { + if (num_samples >= num_seconds*sample_rate) + return; // Discard overrun + const samples = array.length / 2 + for (let n = 0; n < samples; n++) + { + if (++num_samples == num_seconds*sample_rate) { // Collect 1 second of data + dut.requestStop(); + break; + } + const i = array[n * 2 + 0] / 127 + const q = array[n * 2 + 1] / 127 + fft.accumulate([i, q]) + } + }) + return fft.noise_floor(); +} + +export async function run_test_sequence(rig: DeviceInfo, dut: DeviceInfo) +{ + const device: HackrfDevice = await open(dut.serialNumber) + if (!device) + return `device ${dut.serialNumber} not available`; + + console.log(`Testing ${dut.serialNumber} using ${rig.serialNumber}:\n`); + + const frequencies = [80e6, 600e6, 2.4e9, 3.6e9] + var i: number; + for (i = 0; i < frequencies.length; i++) { + const frequency = frequencies[i]; + var noise_floor = await noise_floor_test(device, frequency, 1.2e6); + if (typeof(noise_floor) == 'string') + { + console.log("Failed to open DUT for testing"); + return; + } + console.log(`Noise floor at ${frequency/1e6} is ${Math.round(noise_floor*1280)/10}`) + } +} diff --git a/examples/tsconfig.json b/examples/tsconfig.json index 20dd146..7e65f7a 100644 --- a/examples/tsconfig.json +++ b/examples/tsconfig.json @@ -11,6 +11,7 @@ "strictNullChecks": true, "allowSyntheticDefaultImports": true, "experimentalDecorators": true, - "emitDecoratorMetadata": true + "emitDecoratorMetadata": true, + "outDir": "../dist" } } diff --git a/lib/index.ts b/lib/index.ts index ef996d0..667476c 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -20,3 +20,7 @@ export { DeviceInfo, HackrfDevice, listDevices, open, StreamOptions, defaultStreamOptions, } from './interface' + +export { + visible_hackrfs, scan_hackrfs +} from './scan' diff --git a/lib/scan.ts b/lib/scan.ts new file mode 100644 index 0000000..ee4663a --- /dev/null +++ b/lib/scan.ts @@ -0,0 +1,28 @@ +/* + * Continuously scan for HackRF devices, announcing when each appears or disappears + */ + +import { DeviceInfo, listDevices, UsbBoardId } from '.' + +/* + * This array contains all the HackRF devices that were visible on the last scan_hackrfs + */ +export var visible_hackrfs: Array; +visible_hackrfs = new Array; + +export async function* scan_hackrfs() { + var disappeared_hackrfs = [...visible_hackrfs]; // Copy devices that were visible previously + for await (const extant of listDevices()) { + var found = visible_hackrfs.find(e => e.usbBoardId == extant.usbBoardId && e.serialNumber == extant.serialNumber) + if (found) { // Still present, it's not disappeared + disappeared_hackrfs.splice(disappeared_hackrfs.indexOf(found), 1); + } else { // New device + visible_hackrfs.push(extant); + yield({ added: extant }); // New device found + } + } + for (const gone of disappeared_hackrfs) { + yield({ removed: gone }); // Device unplugged + visible_hackrfs.splice(visible_hackrfs.indexOf(gone), 1); + } +} diff --git a/tsconfig.json b/tsconfig.json index c22c35d..0291c08 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,7 @@ "declaration": true, "allowSyntheticDefaultImports": true, "experimentalDecorators": true, - "emitDecoratorMetadata": true + "emitDecoratorMetadata": true, }, "include": [ "lib", From 9ce27fe0442776ef4d3432bbb6b91f5f47f73424 Mon Sep 17 00:00:00 2001 From: Clifford Heath Date: Fri, 13 Dec 2024 12:51:19 +1100 Subject: [PATCH 4/5] Split off reception_test --- examples/acceptance_test.ts | 2 +- examples/reception_test.ts | 8 ++++++++ examples/test_sequence.ts | 32 ++++++++++++++++++-------------- 3 files changed, 27 insertions(+), 15 deletions(-) create mode 100644 examples/reception_test.ts diff --git a/examples/acceptance_test.ts b/examples/acceptance_test.ts index 10fa133..84f029c 100644 --- a/examples/acceptance_test.ts +++ b/examples/acceptance_test.ts @@ -64,7 +64,7 @@ async function main() { console.log(`Detected ${dut.serialNumber} as DUT`) await run_test_sequence(rig, dut) - break; + break; // REVISIT: Just one run for now :) } process.exit(0) } diff --git a/examples/reception_test.ts b/examples/reception_test.ts new file mode 100644 index 0000000..c98c45f --- /dev/null +++ b/examples/reception_test.ts @@ -0,0 +1,8 @@ +import { HackrfDevice } from '../lib' + +export async function reception_test(rig: HackrfDevice, dut: HackrfDevice, frequency: number): Promise { + console.log(`Testing reception at ${frequency}`) + + // REVISIT: Implement receiver test + return 0; +} diff --git a/examples/test_sequence.ts b/examples/test_sequence.ts index 3a03e94..8963444 100644 --- a/examples/test_sequence.ts +++ b/examples/test_sequence.ts @@ -6,6 +6,8 @@ */ import { open, HackrfDevice, DeviceInfo, UsbBoardId } from '../lib' +import { reception_test } from "./reception_test" + type Complex = [number, number] function timeout(ms: number) { @@ -36,7 +38,7 @@ class FFT { } } -async function noise_floor_test(dut: HackrfDevice, frequency: number, sample_rate: number): Promise { +async function noise_floor_test(dut: HackrfDevice, frequency: number, sample_rate: number): Promise { console.log(`Measuring noise floor at ${frequency}`) const num_seconds = 1; @@ -67,24 +69,26 @@ async function noise_floor_test(dut: HackrfDevice, frequency: number, sample_rat return fft.noise_floor(); } -export async function run_test_sequence(rig: DeviceInfo, dut: DeviceInfo) +export async function run_test_sequence(rig_info: DeviceInfo, dut_info: DeviceInfo) { - const device: HackrfDevice = await open(dut.serialNumber) - if (!device) - return `device ${dut.serialNumber} not available`; + const rig: HackrfDevice = await open(rig_info.serialNumber) + if (!rig) + return `rig ${rig_info.serialNumber} not available`; + + const dut: HackrfDevice = await open(dut_info.serialNumber) + if (!dut) + return `HackRF ${dut_info.serialNumber} not available`; - console.log(`Testing ${dut.serialNumber} using ${rig.serialNumber}:\n`); + console.log(`Testing ${dut_info.serialNumber} using ${rig_info.serialNumber}:\n`); const frequencies = [80e6, 600e6, 2.4e9, 3.6e9] - var i: number; - for (i = 0; i < frequencies.length; i++) { + for (let i = 0; i < frequencies.length; i++) { const frequency = frequencies[i]; - var noise_floor = await noise_floor_test(device, frequency, 1.2e6); - if (typeof(noise_floor) == 'string') - { - console.log("Failed to open DUT for testing"); - return; - } + var noise_floor = await noise_floor_test(dut, frequency, 1.2e6); console.log(`Noise floor at ${frequency/1e6} is ${Math.round(noise_floor*1280)/10}`) } + + var frequency = frequencies[0]; + var receive_result = await reception_test(rig, dut, frequency); + console.log(`Reception at ${frequency/1e6} returned ${receive_result}`) } From acfbea2ae7e2935a06c9ec16df3087c340fad2b1 Mon Sep 17 00:00:00 2001 From: Clifford Heath Date: Thu, 27 Mar 2025 21:06:13 +1100 Subject: [PATCH 5/5] Added board_rev reporting to list_devices.ts --- examples/list_devices.ts | 12 ++++++++---- lib/constants.ts | 12 ++++++++++++ lib/interface.ts | 13 ++++++++++++- package.json | 6 ++++++ 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/examples/list_devices.ts b/examples/list_devices.ts index 9bd0a1d..ba34790 100644 --- a/examples/list_devices.ts +++ b/examples/list_devices.ts @@ -3,11 +3,15 @@ import { listDevices, UsbBoardId } from '../lib' async function main() { let ld = listDevices(); - console.log(ld) for await (const info of ld) { - console.log(info.device) - console.log(`Found ${info.usbBoardId} = ${UsbBoardId[info.usbBoardId]}`) - console.log(`Serial: ${info.serialNumber}`) + console.log(info.device) // Show USB detail + console.log(`Found ${UsbBoardId[info.usbBoardId]} (PID 0x${info.usbBoardId.toString(16).padStart(4, '0')})`) + if (info.serialNumber) { + console.log(`Serial: ${info.serialNumber.replace(/^1+/,'')}`) + } + if (info.boardRev !== undefined) { + console.log(`Board Rev ${info.boardRev}`) + } } process.exit(0) diff --git a/lib/constants.ts b/lib/constants.ts index 1818e25..94e6741 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -170,6 +170,18 @@ export enum VendorRequest { OPERACAKE_GPIO_TEST = 35, CPLD_CHECKSUM = 36, UI_ENABLE = 37, + + OPERACAKE_SET_MODE = 38, + OPERACAKE_GET_MODE = 39, + OPERACAKE_SET_DWELL_TIMES = 40, + GET_M0_STATE = 41, + SET_TX_UNDERRUN_LIMIT = 42, + SET_RX_OVERRUN_LIMIT = 43, + GET_CLKIN_STATUS = 44, + BOARD_REV_READ = 45, + SUPPORTED_PLATFORM_READ = 46, + SET_LEDS = 47, + SET_USER_BIAS_T_OPTS = 48, } export enum TransceiverMode { diff --git a/lib/interface.ts b/lib/interface.ts index 8db188b..3c55c49 100644 --- a/lib/interface.ts +++ b/lib/interface.ts @@ -157,7 +157,8 @@ function detachKernelDrivers(handle: Device) { export interface DeviceInfo { device: Device - usbBoardId: number + usbBoardId: number // USB Product ID (PID) + boardRev?: number serialNumber?: string } @@ -179,6 +180,8 @@ export async function* listDevices() { device.open(false) info.serialNumber = await promisify(cb => device.getStringDescriptor(iSerialNumber, cb) )() as string + let hackRF = await HackrfDevice.open(device); + info.boardRev = await hackRF.getBoardRev() } catch (e) { } finally { device.close() @@ -921,4 +924,12 @@ export class HackrfDevice { data.subarray(i, i + chunkSize), cb as any) )() }) } + + /** + * @category Device info + */ + async getBoardRev() { + const buf = await this.controlTransferIn(VendorRequest.BOARD_REV_READ, 0, 0, 1) + return checkInLength(buf, 1).readUInt8() + } } diff --git a/package.json b/package.json index c340cb3..803e93f 100644 --- a/package.json +++ b/package.json @@ -70,5 +70,11 @@ "coveralls": "^3.1.1", "speaker": "^0.5.5", "typescript": "^5.6.3" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "speaker", + "usb" + ] } }