diff --git a/bindings/node/src/radio.rs b/bindings/node/src/radio.rs index cda7f2f..e0071a0 100644 --- a/bindings/node/src/radio.rs +++ b/bindings/node/src/radio.rs @@ -40,7 +40,6 @@ impl NodeRF24 { })? .find(|chip| { if let Ok(chip) = chip { - println!("{:?}", chip.path()); if chip .path() .to_string_lossy() diff --git a/cspell.config.yml b/cspell.config.yml index 82797e5..e0c84b5 100644 --- a/cspell.config.yml +++ b/cspell.config.yml @@ -12,6 +12,7 @@ words: - Doherty - DYNPD - eabi + - endl - fontawesome - gnueabihf - gpio diff --git a/examples/node/ts/acknowledgementPayloads.ts b/examples/node/ts/acknowledgementPayloads.ts new file mode 100644 index 0000000..b7fd848 --- /dev/null +++ b/examples/node/ts/acknowledgementPayloads.ts @@ -0,0 +1,173 @@ +import * as readline from "readline/promises"; +import * as fs from "fs"; +import * as timer from "timers/promises"; +import { RF24, PaLevel } from "@rf24/rf24"; + +const io = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +type AppState = { + radio: RF24; + counter: number; +}; + +console.log(module.filename); + +export async function setup(): Promise { + // The radio's CE Pin uses a GPIO number. + // On Linux, consider the device path `/dev/gpiochip`: + // - `` is the gpio chip's identifying number. + // Using RPi4 (or earlier), this number is `0` (the default). + // Using the RPi5, this number is actually `4`. + // The radio's CE pin must connected to a pin exposed on the specified chip. + const CE_PIN = 22; // for GPIO22 + // try detecting RPi5 first; fall back to default + const DEV_GPIO_CHIP = fs.existsSync("/dev/gpiochip4") ? 4 : 0; + + // The radio's CSN Pin corresponds the SPI bus's CS pin (aka CE pin). + // On Linux, consider the device path `/dev/spidev.`: + // - `` is the SPI bus number (defaults to `0`) + // - `` is the CSN pin (must be unique for each device on the same SPI bus) + const CSN_PIN = 0; // aka CE0 for SPI bus 0 (/dev/spidev0.0) + + // create a radio object for the specified hard ware config: + const radio = new RF24(CE_PIN, CSN_PIN, { + devGpioChip: DEV_GPIO_CHIP, + }); + + // initialize the nRF24L01 on the spi bus + radio.begin(); + + // For this example, we will use different addresses + // An address needs to be a buffer object (bytearray) + const address = [Buffer.from("1Node"), Buffer.from("2Node")]; + + // to use different addresses on a pair of radios, we need a variable to + // uniquely identify which address this radio will use to transmit + // 0 uses address[0] to transmit, 1 uses address[1] to transmit + const radioNumber = Number( + (await io.question( + "Which radio is this? Enter '1' or '0' (default is '0') ", + )) == "1", + ); + console.log(`radioNumber is ${radioNumber}`); + //set TX address of RX node into the TX pipe + radio.openTxPipe(address[radioNumber]); // always uses pipe 0 + // set RX address of TX node into an RX pipe + radio.openRxPipe(1, address[1 - radioNumber]); // using pipe 1 + + // set the Power Amplifier level to -12 dBm since this test example is + // usually run with nRF24L01 transceivers in close proximity of each other + radio.setPaLevel(PaLevel.Low); // PaLevel.Max is default + + radio.allowAckPayloads(true); + radio.setDynamicPayloads(true); + + return { radio: radio, counter: 0 }; +} + +/** + * The transmitting node's behavior. + * @param count The number of payloads to send + */ +export async function master(example: AppState, count: number | null) { + example.radio.stopListening(); + // we'll use a DataView object to store our string and number into a bytearray buffer + const outgoing = Buffer.from("Hello \0."); + for (let i = 0; i < (count || 5); i++) { + outgoing.writeUint8(example.counter, 7); + const start = process.hrtime.bigint(); + const result = example.radio.send(outgoing); + const end = process.hrtime.bigint(); + if (result) { + const elapsed = (end - start) / BigInt(1000); + process.stdout.write( + `Transmission successful! Time to Transmit: ${elapsed} us. Sent: ` + + `${outgoing.subarray(0, 6).toString()}${example.counter} `, + ); + example.counter += 1; + if (example.radio.available()) { + const incoming = example.radio.read(); + const counter = incoming.readUint8(7); + console.log( + ` Received: ${incoming.subarray(0, 6).toString()}${counter}`, + ); + } else { + console.log("Received an empty ACK packet"); + } + } else { + console.log("Transmission failed or timed out!"); + } + await timer.setTimeout(1000); + } +} + +/** + * The receiving node's behavior. + * @param duration The timeout duration (in seconds) to listen after receiving a payload. + */ +export function slave(example: AppState, duration: number | null) { + example.radio.startListening(); + // we'll use a DataView object to store our string and number into a bytearray buffer + const outgoing = Buffer.from("World \0."); + outgoing.writeUint8(example.counter, 7); + example.radio.writeAckPayload(1, outgoing); + let timeout = Date.now() + (duration || 6) * 1000; + while (Date.now() < timeout) { + const hasRx = example.radio.availablePipe(); + if (hasRx.available) { + const incoming = example.radio.read(); + const counter = incoming.readUint8(7); + console.log( + `Received ${incoming.length} bytes on pipe ${hasRx.pipe}: ` + + `${incoming.subarray(0, 6).toString()}${counter} Sent: ` + + `${outgoing.subarray(0, 6).toString()}${example.counter}`, + ); + example.counter = counter; + outgoing.writeUint8(counter + 1, 7); + example.radio.writeAckPayload(1, outgoing); + timeout = Date.now() + (duration || 6) * 1000; + } + } + example.radio.stopListening(); // flushes TX FIFO when ACK payloads are enabled +} + +/** + * This function prompts the user and performs the specified role for the radio. + */ +export async function setRole(example: AppState): Promise { + const prompt = + "*** Enter 'T' to transmit\n" + + "*** Enter 'R' to receive\n" + + "*** Enter 'Q' to quit\n"; + const input = (await io.question(prompt)).split(" "); + let param: number | null = null; + if (input.length > 1) { + param = Number(input[1]); + } + switch (input[0].charAt(0).toLowerCase()) { + case "t": + await master(example, param); + return true; + case "r": + slave(example, param); + return true; + default: + console.log(`'${input[0].charAt(0)}' is an unrecognized input`); + return true; + case "q": + example.radio.powerDown(); + return false; + } +} + +export async function main() { + const example = await setup(); + while (await setRole(example)); + io.close(); + example.radio.powerDown(); +} + +main(); diff --git a/examples/node/ts/gettingStarted.ts b/examples/node/ts/gettingStarted.ts index 196f80f..2042059 100644 --- a/examples/node/ts/gettingStarted.ts +++ b/examples/node/ts/gettingStarted.ts @@ -8,14 +8,14 @@ const io = readline.createInterface({ output: process.stdout, }); -type ExampleStates = { +type AppState = { radio: RF24; - payload: DataView; + payload: Buffer; }; console.log(module.filename); -export async function setup(): Promise { +export async function setup(): Promise { // The radio's CE Pin uses a GPIO number. // On Linux, consider the device path `/dev/gpiochip`: // - `` is the gpio chip's identifying number. @@ -67,8 +67,8 @@ export async function setup(): Promise { const payloadLength = 4; radio.setPayloadLength(payloadLength); // we'll use a DataView object to store our float number into a bytearray buffer - const payload = new DataView(new ArrayBuffer(payloadLength)); - payload.setFloat32(0, 0.0, true); // true means using little endian + const payload = Buffer.alloc(payloadLength); + payload.writeFloatLE(0.0, 0); return { radio: radio, payload: payload }; } @@ -77,16 +77,16 @@ export async function setup(): Promise { * The transmitting node's behavior. * @param count The number of payloads to send */ -export async function master(example: ExampleStates, count: number = 5) { - example.radio.startListening(); - for (let i = 0; i < count; i++) { +export async function master(example: AppState, count: number | null) { + example.radio.stopListening(); + for (let i = 0; i < (count || 5); i++) { const start = process.hrtime.bigint(); - const result = example.radio.send(Buffer.from(example.payload.buffer)); + const result = example.radio.send(example.payload); const end = process.hrtime.bigint(); if (result) { const elapsed = (end - start) / BigInt(1000); - console.log(`Transmission successful! Time to Transmit: ${elapsed} ms`); - example.payload.setFloat32(0, example.payload.getFloat32(0) + 0.01); + console.log(`Transmission successful! Time to Transmit: ${elapsed} us`); + example.payload.writeFloatLE(example.payload.readFloatLE(0) + 0.01, 0); } else { console.log("Transmission failed or timed out!"); } @@ -98,19 +98,19 @@ export async function master(example: ExampleStates, count: number = 5) { * The receiving node's behavior. * @param duration The timeout duration (in seconds) to listen after receiving a payload. */ -export function slave(example: ExampleStates, duration: number = 6) { +export function slave(example: AppState, duration: number | null) { example.radio.startListening(); - let timeout = Date.now() + duration * 1000; + let timeout = Date.now() + (duration || 6) * 1000; while (Date.now() < timeout) { const hasRx = example.radio.availablePipe(); if (hasRx.available) { - const received = example.radio.read(); - example.payload = new DataView(received.buffer); - const data = example.payload.getFloat32(0, true); // true means little endian + const incoming = example.radio.read(); + example.payload = incoming; + const data = incoming.readFloatLE(0); console.log( - `Received ${received.length} bytes on pipe ${hasRx.pipe}: ${data}`, + `Received ${incoming.length} bytes on pipe ${hasRx.pipe}: ${data}`, ); - timeout = Date.now() + duration * 1000; + timeout = Date.now() + (duration || 6) * 1000; } } example.radio.stopListening(); @@ -119,21 +119,25 @@ export function slave(example: ExampleStates, duration: number = 6) { /** * This function prompts the user and performs the specified role for the radio. */ -export async function setRole(example: ExampleStates): Promise { +export async function setRole(example: AppState): Promise { const prompt = "*** Enter 'T' to transmit\n" + "*** Enter 'R' to receive\n" + "*** Enter 'Q' to quit\n"; - const input = await io.question(prompt); - switch (input.toLowerCase()[0]) { + const input = (await io.question(prompt)).split(" "); + let param: number | null = null; + if (input.length > 1) { + param = Number(input[1]); + } + switch (input[0].charAt(0).toLowerCase()) { case "t": - await master(example); + await master(example, param); return true; case "r": - slave(example); + slave(example, param); return true; default: - console.log(`'${input[0]}' is an unrecognized input`); + console.log(`'${input[0].charAt(0)}' is an unrecognized input`); return true; case "q": example.radio.powerDown(); diff --git a/examples/node/ts/scanner.ts b/examples/node/ts/scanner.ts new file mode 100644 index 0000000..fb144af --- /dev/null +++ b/examples/node/ts/scanner.ts @@ -0,0 +1,222 @@ +import * as readline from "readline/promises"; +import * as fs from "fs"; +import * as timer from "timers/promises"; +import { RF24, DataRate, FifoState } from "@rf24/rf24"; + +const io = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +type AppState = { + radio: RF24; +}; + +const CHANNELS = 126; + +console.log(module.filename); +console.log( + "!!!Make sure the terminal is wide enough for 126 characters on 1 line." + + " If this line is wrapped, then the output will look bad!", +); + +export async function setup(): Promise { + // The radio's CE Pin uses a GPIO number. + // On Linux, consider the device path `/dev/gpiochip`: + // - `` is the gpio chip's identifying number. + // Using RPi4 (or earlier), this number is `0` (the default). + // Using the RPi5, this number is actually `4`. + // The radio's CE pin must connected to a pin exposed on the specified chip. + const CE_PIN = 22; // for GPIO22 + // try detecting RPi5 first; fall back to default + const DEV_GPIO_CHIP = fs.existsSync("/dev/gpiochip4") ? 4 : 0; + + // The radio's CSN Pin corresponds the SPI bus's CS pin (aka CE pin). + // On Linux, consider the device path `/dev/spidev.`: + // - `` is the SPI bus number (defaults to `0`) + // - `` is the CSN pin (must be unique for each device on the same SPI bus) + const CSN_PIN = 0; // aka CE0 for SPI bus 0 (/dev/spidev0.0) + + // create a radio object for the specified hard ware config: + const radio = new RF24(CE_PIN, CSN_PIN, { + devGpioChip: DEV_GPIO_CHIP, + }); + + // initialize the nRF24L01 on the spi bus + radio.begin(); + + // This is the worst possible configuration. + // The intention here is to pick up as much noise as possible. + radio.setAddressLength(2); + + // For this example, we will use the worst possible addresses + const address = [ + Buffer.from([0x55, 0x55]), + Buffer.from([0xaa, 0xaa]), + Buffer.from([0xa0, 0xaa]), + Buffer.from([0x0a, 0xaa]), + Buffer.from([0xa5, 0xaa]), + Buffer.from([0x5a, 0xaa]), + ]; + for (let pipe = 0; pipe < address.length; pipe++) { + radio.openRxPipe(pipe, address[pipe]); + } + + // Ask user for desired data rate (default to 1 Mbps) + const dRatePrompt = + "Select the desired DataRate: (defaults to 1 Mbps)\n" + + "1. 1 Mbps\n2. 2 Mbps\n3. 250 Kbps\n"; + const answer = parseInt(await io.question(dRatePrompt)); + const index = isNaN(answer) ? 0 : answer; + + if (index == 2) { + radio.setDataRate(DataRate.Mbps2); + console.log(`Data Rate is 2 Mbps`); + } + if (index == 3) { + radio.setDataRate(DataRate.Kbps250); + console.log(`Data Rate is 250 Kbps`); + } else { + radio.setDataRate(DataRate.Mbps1); + console.log(`Data Rate is 1 Mbps`); + } + + return { + radio: radio, + }; +} + +/** + * Prints the vertical header for all the channels + */ +export function printHeader() { + let hundreds = ""; + let tens = ""; + let ones = ""; + let divider = ""; + for (let i = 0; i < CHANNELS; i++) { + hundreds += Math.floor(i / 100).toString(); + tens += (Math.floor(i / 10) % 10).toString(); + ones += (i % 10).toString(); + divider += "~"; + } + console.log(hundreds); + console.log(tens); + console.log(ones); + console.log(divider); +} + +/** + * The scanner behavior. + */ +export async function scan(example: AppState, duration: number | null) { + printHeader(); + const caches = []; + for (let i = 0; i < CHANNELS; i++) { + caches.push(0); + } + let sweeps = 0; + let channel = 0; + + const timeout = Date.now() + (duration || 30) * 1000; + while (Date.now() < timeout) { + example.radio.setChannel(channel); + example.radio.startListening(); + await timer.setTimeout(0.13); // needs to be at least 130 microseconds + const rpd = example.radio.rpd; + example.radio.stopListening(); + const foundSignal = example.radio.available(); + + caches[channel] += Number(foundSignal || rpd || example.radio.rpd); + + if (foundSignal) { + example.radio.flushRx(); // discard any packets (noise) saved in RX FIFO + } + const total = caches[channel]; + process.stdout.write(total > 0 ? total.toString(16) : "-"); + + channel += 1; + let endl = false; + if (channel >= CHANNELS) { + channel = 0; + sweeps += 1; + } + if (sweeps > 15) { + endl = true; + sweeps = 0; + // reset total signal counts for all channels + for (let i = 0; i < CHANNELS; i++) { + caches[i] = 0; + } + } + if (channel == 0) { + process.stdout.write(endl ? "\n" : "\r"); + } + } + + // finish printing current cache of signals + for (let i = channel; i < CHANNELS; i++) { + const total = caches[i]; + process.stdout.write(total > 0 ? total.toString(16) : "-"); + } +} + +/** + * Sniff ambient noise and print it out as hexadecimal string. + */ +export function noise(example: AppState, duration: number | null) { + const timeout = Date.now() + (duration || 10) * 1000; + example.radio.startListening(); + while ( + example.radio.isListening || + example.radio.getFifoState(false) != FifoState.Empty + ) { + const payload = example.radio.read(); + const hexArray = []; + for (let i = 0; i < payload.length; i++) { + hexArray.push(payload[i].toString(16).padStart(2, "0")); + } + console.log(hexArray.join(" ")); + if (Date.now() > timeout && example.radio.isListening) { + example.radio.stopListening(); + } + } +} + +/** + * This function prompts the user and performs the specified role for the radio. + */ +export async function setRole(example: AppState): Promise { + const prompt = + "*** Enter 'S' to scan\n" + + "*** Enter 'N' to print noise\n" + + "*** Enter 'Q' to quit\n"; + const input = (await io.question(prompt)).split(" "); + let param: number | null = null; + if (input.length > 1) { + param = Number(input[1]); + } + switch (input[0].charAt(0).toLowerCase()) { + case "s": + await scan(example, param); + return true; + case "n": + noise(example, param); + return true; + default: + console.log(`'${input[0].charAt(0)}' is an unrecognized input`); + return true; + case "q": + example.radio.powerDown(); + return false; + } +} + +export async function main() { + const example = await setup(); + while (await setRole(example)); + io.close(); + example.radio.powerDown(); +} + +main();