vad7 3 лет назад
Сommit
4fe19b28f5
10 измененных файлов с 1554 добавлено и 0 удалено
  1. 20 0
      README.md
  2. 5 0
      addr.txt
  3. 3 0
      addr1.txt
  4. 4 0
      addr_sniff.txt
  5. 20 0
      application.fam
  6. 533 0
      lib/nrf24/nrf24.c
  7. 376 0
      lib/nrf24/nrf24.h
  8. 560 0
      nrf24scan.c
  9. 33 0
      nrf24scan.h
  10. BIN
      nrf24scan_10px.png

+ 20 - 0
README.md

@@ -0,0 +1,20 @@
+#  Scanner NRF24 scanner with logging and resend ability for Flipper Zero
+
+An [NRF24](https://www.sparkfun.com/datasheets/Components/SMD/nRF24L01Pluss_Preliminary_Product_Specification_v1_0.pdf) driver for the [Flipper Zero](https://flipperzero.one/) device. The NRF24 is a popular line of 2.4GHz radio transceivers from Nordic Semiconductors. This library is not currently complete, but functional.
+
+## PinOut from from NoComp/Frog
+<img src="https://media.discordapp.net/attachments/937479784726949900/994495234618687509/unknown.png?width=567&height=634">
+
+# Mousejack / NRF24 pinout by UberGuidoZ
+2/A7 on FZ goes to MOSI/6 on nrf24l01<br>
+3/A6 on FZ goes to MISO/7 on nrf24l01<br>
+4/A4 on FZ goes to CSN/4 on nrf24l01<br>
+5/B3 on FZ goes to SCK/5 on nrf24l01<br>
+6/B2 on FZ goes to CE/3 on nrf24l01<br>
+8/GND on FZ goes to GND/1 on nrf24l01<br>
+9/3V3 on FZ goes to VCC/2 on nrf24l01<br>
+IRQ/8 is left disconnected on nrf24l01
+![NRF_Pins](https://user-images.githubusercontent.com/57457139/178093717-39effd5c-ebe2-4253-b13c-70517d7902f9.png)
+If the nRF module is acting a bit flakey, try adding a capacitor to the vcc/gnd lines! I've not tried the Plus model so it may have a bigger need for a cap. Otherwise, I haven't had any major issues. Anything from a 3.3 uF to 10 uF should do. (Watch your positive/negative placement! Negative to ground.) I learned if you wanna get fancy, include a 0.1 uF cap in parallel. The 3.3 uF to 10 uF will respond to slow freq changes while the 0.1 uF will respond to the high freq switching spikes that the larger one cannot. That said, a single 10 uF will likely suffice for the Mousejack attack. ¯\\\_(ツ)_/¯
+![NRF_Capacitor](https://user-images.githubusercontent.com/57457139/178169959-d030f9a6-d2ac-46af-af8b-470ff092c8a7.jpg)
+

+ 5 - 0
addr.txt

@@ -0,0 +1,5 @@
+1
+120
+C8C801
+C8C802
+03

+ 3 - 0
addr1.txt

@@ -0,0 +1,3 @@
+1
+120
+C8C801

+ 4 - 0
addr_sniff.txt

@@ -0,0 +1,4 @@
+1
+0
+0055
+00AA

+ 20 - 0
application.fam

@@ -0,0 +1,20 @@
+App(
+    appid="Nrf24_Scanner",
+    name="[NRF24] Scanner",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="nrf24scan_app",
+    cdefines=["APP_NRF24SCAN"],
+    requires=["gui"],
+    stack_size=2 * 1024,
+    order=60,
+    fap_icon="nrf24scan_10px.png",
+    fap_category="GPIO",
+    fap_private_libs=[
+        Lib(
+            name="nrf24",
+            sources=[
+                "nrf24.c",
+            ],
+        ),
+    ],
+)

+ 533 - 0
lib/nrf24/nrf24.c

@@ -0,0 +1,533 @@
+#include "nrf24.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <furi_hal_resources.h>
+#include <assert.h>
+#include <string.h>
+
+void nrf24_init() {
+    furi_hal_spi_bus_handle_init(nrf24_HANDLE);
+    furi_hal_spi_acquire(nrf24_HANDLE);
+    furi_hal_gpio_init(nrf24_CE_PIN, GpioModeOutputPushPull, GpioPullUp, GpioSpeedVeryHigh);
+    furi_hal_gpio_write(nrf24_CE_PIN, false);
+}
+
+void nrf24_deinit() {
+    furi_hal_spi_release(nrf24_HANDLE);
+    furi_hal_spi_bus_handle_deinit(nrf24_HANDLE);
+    furi_hal_gpio_write(nrf24_CE_PIN, false);
+    furi_hal_gpio_init(nrf24_CE_PIN, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
+}
+
+void nrf24_spi_trx(
+    FuriHalSpiBusHandle* handle,
+    uint8_t* tx,
+    uint8_t* rx,
+    uint8_t size,
+    uint32_t timeout) {
+    UNUSED(timeout);
+    furi_hal_gpio_write(handle->cs, false);
+    furi_hal_spi_bus_trx(handle, tx, rx, size, nrf24_TIMEOUT);
+    furi_hal_gpio_write(handle->cs, true);
+}
+
+uint8_t nrf24_write_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t data) {
+    uint8_t tx[2] = {W_REGISTER | (REGISTER_MASK & reg), data};
+    uint8_t rx[2] = {0};
+    nrf24_spi_trx(handle, tx, rx, 2, nrf24_TIMEOUT);
+
+
+    FURI_LOG_D("NRF_WR", " #%02X=%02X", reg, data);
+
+
+
+    return rx[0];
+}
+
+uint8_t
+    nrf24_write_buf_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* data, uint8_t size) {
+    uint8_t tx[size + 1];
+    uint8_t rx[size + 1];
+    memset(rx, 0, size + 1);
+    tx[0] = W_REGISTER | (REGISTER_MASK & reg);
+    memcpy(&tx[1], data, size);
+    nrf24_spi_trx(handle, tx, rx, size + 1, nrf24_TIMEOUT);
+
+
+
+    FURI_LOG_D("NRF_WR", " #%02X(%02X)=0x%02X%02X%02X%02X%02X", reg, size, data[0], data[1], data[2], data[3], data[4] );
+
+
+
+    return rx[0];
+}
+
+uint8_t nrf24_read_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* data, uint8_t size) {
+    uint8_t tx[size + 1];
+    uint8_t rx[size + 1];
+    memset(rx, 0, size + 1);
+    tx[0] = R_REGISTER | (REGISTER_MASK & reg);
+    memset(&tx[1], 0, size);
+    nrf24_spi_trx(handle, tx, rx, size + 1, nrf24_TIMEOUT);
+    memcpy(data, &rx[1], size);
+    return rx[0];
+}
+
+uint8_t nrf24_flush_rx(FuriHalSpiBusHandle* handle) {
+    uint8_t tx[] = {FLUSH_RX};
+    uint8_t rx[] = {0};
+    nrf24_spi_trx(handle, tx, rx, 1, nrf24_TIMEOUT);
+    return rx[0];
+}
+
+uint8_t nrf24_flush_tx(FuriHalSpiBusHandle* handle) {
+    uint8_t tx[] = {FLUSH_TX};
+    uint8_t rx[] = {0};
+    nrf24_spi_trx(handle, tx, rx, 1, nrf24_TIMEOUT);
+    return rx[0];
+}
+
+uint8_t nrf24_get_maclen(FuriHalSpiBusHandle* handle) {
+    uint8_t maclen;
+    nrf24_read_reg(handle, REG_SETUP_AW, &maclen, 1);
+    maclen &= 3;
+    return maclen + 2;
+}
+
+uint8_t nrf24_set_maclen(FuriHalSpiBusHandle* handle, uint8_t maclen) {
+    assert(maclen > 1 && maclen < 6);
+    uint8_t status = 0;
+    status = nrf24_write_reg(handle, REG_SETUP_AW, maclen - 2);
+    return status;
+}
+
+uint8_t nrf24_status(FuriHalSpiBusHandle* handle) {
+    uint8_t status;
+    uint8_t tx[] = {R_REGISTER | (REGISTER_MASK & REG_STATUS)};
+    nrf24_spi_trx(handle, tx, &status, 1, nrf24_TIMEOUT);
+    return status;
+}
+
+uint32_t nrf24_get_rate(FuriHalSpiBusHandle* handle) {
+    uint8_t setup = 0;
+    uint32_t rate = 0;
+    nrf24_read_reg(handle, REG_RF_SETUP, &setup, 1);
+    setup &= 0x28;
+    if(setup == 0x20)
+        rate = 250000; // 250kbps
+    else if(setup == 0x08)
+        rate = 2000000; // 2Mbps
+    else if(setup == 0x00)
+        rate = 1000000; // 1Mbps
+
+    return rate;
+}
+
+uint8_t nrf24_set_rate(FuriHalSpiBusHandle* handle, uint32_t rate) {
+    uint8_t r6 = 0;
+    uint8_t status = 0;
+    if(!rate) rate = 2000000;
+
+    nrf24_read_reg(handle, REG_RF_SETUP, &r6, 1); // RF_SETUP register
+    r6 = r6 & (~0x28); // Clear rate fields.
+    if(rate == 2000000)
+        r6 = r6 | 0x08;
+    else if(rate == 1000000)
+        r6 = r6;
+    else if(rate == 250000)
+        r6 = r6 | 0x20;
+
+    status = nrf24_write_reg(handle, REG_RF_SETUP, r6); // Write new rate.
+    return status;
+}
+
+uint8_t nrf24_get_chan(FuriHalSpiBusHandle* handle) {
+    uint8_t channel = 0;
+    nrf24_read_reg(handle, REG_RF_CH, &channel, 1);
+    return channel;
+}
+
+uint8_t nrf24_set_chan(FuriHalSpiBusHandle* handle, uint8_t chan) {
+    uint8_t status;
+    status = nrf24_write_reg(handle, REG_RF_CH, chan);
+    return status;
+}
+
+uint8_t nrf24_get_src_mac(FuriHalSpiBusHandle* handle, uint8_t* mac) {
+    uint8_t size = 0;
+    uint8_t status = 0;
+    size = nrf24_get_maclen(handle);
+    status = nrf24_read_reg(handle, REG_RX_ADDR_P0, mac, size);
+    return status;
+}
+
+uint8_t nrf24_set_src_mac(FuriHalSpiBusHandle* handle, uint8_t* mac, uint8_t size) {
+    uint8_t status = 0;
+    uint8_t clearmac[] = {0, 0, 0, 0, 0};
+    nrf24_set_maclen(handle, size);
+    nrf24_write_buf_reg(handle, REG_RX_ADDR_P0, clearmac, 5);
+    status = nrf24_write_buf_reg(handle, REG_RX_ADDR_P0, mac, size);
+    return status;
+}
+
+uint8_t nrf24_get_dst_mac(FuriHalSpiBusHandle* handle, uint8_t* mac) {
+    uint8_t size = 0;
+    uint8_t status = 0;
+    size = nrf24_get_maclen(handle);
+    status = nrf24_read_reg(handle, REG_TX_ADDR, mac, size);
+    return status;
+}
+
+uint8_t nrf24_set_dst_mac(FuriHalSpiBusHandle* handle, uint8_t* mac, uint8_t size) {
+    uint8_t status = 0;
+    uint8_t clearmac[] = {0, 0, 0, 0, 0};
+    nrf24_set_maclen(handle, size);
+    nrf24_write_buf_reg(handle, REG_TX_ADDR, clearmac, 5);
+    status = nrf24_write_buf_reg(handle, REG_TX_ADDR, mac, size);
+    return status;
+}
+
+uint8_t nrf24_get_packetlen(FuriHalSpiBusHandle* handle) {
+    uint8_t len = 0;
+    nrf24_read_reg(handle, RX_PW_P0, &len, 1);
+    return len;
+}
+
+uint8_t nrf24_set_packetlen(FuriHalSpiBusHandle* handle, uint8_t len) {
+    uint8_t status = 0;
+    status = nrf24_write_reg(handle, RX_PW_P0, len);
+    return status;
+}
+
+uint8_t
+    nrf24_rxpacket(FuriHalSpiBusHandle* handle, uint8_t* packet, uint8_t* packetsize, bool full) {
+    uint8_t status = 0;
+    uint8_t size = 0;
+    uint8_t tx_pl_wid[] = {R_RX_PL_WID, 0};
+    uint8_t rx_pl_wid[] = {0, 0};
+    uint8_t tx_cmd[33] = {0}; // 32 max payload size + 1 for command
+    uint8_t tmp_packet[33] = {0};
+
+    status = nrf24_status(handle);
+
+    if(status & 0x40) {
+        if(full)
+            size = nrf24_get_packetlen(handle);
+        else {
+            nrf24_spi_trx(handle, tx_pl_wid, rx_pl_wid, 2, nrf24_TIMEOUT);
+            size = rx_pl_wid[1];
+        }
+
+        tx_cmd[0] = R_RX_PAYLOAD;
+        nrf24_spi_trx(handle, tx_cmd, tmp_packet, size + 1, nrf24_TIMEOUT);
+        nrf24_write_reg(handle, REG_STATUS, 0x50); // clear RX_DR, MAX_RT.
+        memcpy(packet, &tmp_packet[1], size);
+    } else if(status == 0 || (status & 0x11)) {
+        nrf24_flush_rx(handle);
+        nrf24_write_reg(handle, REG_STATUS, 0x50); // clear RX_DR, MAX_RT.
+    }
+
+    *packetsize = size;
+    return status;
+}
+
+uint8_t nrf24_txpacket(FuriHalSpiBusHandle* handle, uint8_t* payload, uint8_t size, bool ack) {
+    uint8_t status = 0;
+    uint8_t tx[size + 1];
+    uint8_t rx[size + 1];
+    memset(tx, 0, size + 1);
+    memset(rx, 0, size + 1);
+
+    if(!ack)
+        tx[0] = W_TX_PAYLOAD_NOACK;
+    else
+        tx[0] = W_TX_PAYLOAD;
+
+    memcpy(&tx[1], payload, size);
+    nrf24_spi_trx(handle, tx, rx, size + 1, nrf24_TIMEOUT);
+    nrf24_set_tx_mode(handle);
+
+    while(!(status & (TX_DS | MAX_RT))) status = nrf24_status(handle);
+
+    if(status & MAX_RT) nrf24_flush_tx(handle);
+
+    nrf24_set_idle(handle);
+    nrf24_write_reg(handle, REG_STATUS, TX_DS | MAX_RT);
+    return status & TX_DS;
+}
+
+uint8_t nrf24_power_up(FuriHalSpiBusHandle* handle) {
+    uint8_t status = 0;
+    uint8_t cfg = 0;
+    nrf24_read_reg(handle, REG_CONFIG, &cfg, 1);
+    cfg = cfg | 2;
+    status = nrf24_write_reg(handle, REG_CONFIG, cfg);
+    furi_delay_ms(1000);
+    return status;
+}
+
+uint8_t nrf24_set_idle(FuriHalSpiBusHandle* handle) {
+    uint8_t status = 0;
+    uint8_t cfg = 0;
+    nrf24_read_reg(handle, REG_CONFIG, &cfg, 1);
+    cfg &= 0xfc; // clear bottom two bits to power down the radio
+    status = nrf24_write_reg(handle, REG_CONFIG, cfg);
+    //nr204_write_reg(handle, REG_EN_RXADDR, 0x0);
+    furi_hal_gpio_write(nrf24_CE_PIN, false);
+    return status;
+}
+
+uint8_t nrf24_set_rx_mode(FuriHalSpiBusHandle* handle) {
+    uint8_t status = 0;
+    uint8_t cfg = 0;
+    //status = nrf24_write_reg(handle, REG_CONFIG, 0x0F); // enable 2-byte CRC, PWR_UP, and PRIM_RX
+    nrf24_read_reg(handle, REG_CONFIG, &cfg, 1);
+    cfg |= 0x03; // PWR_UP, and PRIM_RX
+    status = nrf24_write_reg(handle, REG_CONFIG, cfg);
+    //nr204_write_reg(REG_EN_RXADDR, 0x03) // Set RX Pipe 0 and 1
+    furi_hal_gpio_write(nrf24_CE_PIN, true);
+    furi_delay_ms(2000);
+    return status;
+}
+
+uint8_t nrf24_set_tx_mode(FuriHalSpiBusHandle* handle) {
+    uint8_t status = 0;
+    uint8_t cfg = 0;
+    furi_hal_gpio_write(nrf24_CE_PIN, false);
+    nrf24_write_reg(handle, REG_STATUS, 0x30);
+    //status = nrf24_write_reg(handle, REG_CONFIG, 0x0E); // enable 2-byte CRC, PWR_UP
+    nrf24_read_reg(handle, REG_CONFIG, &cfg, 1);
+    cfg &= 0xfe; // disable PRIM_RX
+    cfg |= 0x02; // PWR_UP
+    status = nrf24_write_reg(handle, REG_CONFIG, cfg);
+    furi_hal_gpio_write(nrf24_CE_PIN, true);
+    furi_delay_ms(2);
+    return status;
+}
+
+void nrf24_configure(
+    FuriHalSpiBusHandle* handle,
+    uint8_t rate,
+    uint8_t* srcmac,
+    uint8_t* dstmac,
+    uint8_t maclen,
+    uint8_t channel,
+    bool noack,
+    bool disable_aa) {
+    assert(channel <= 125);
+    assert(rate == 1 || rate == 2);
+    if(rate == 2)
+        rate = 8; // 2Mbps
+    else
+        rate = 0; // 1Mbps
+
+    nrf24_write_reg(handle, REG_CONFIG, 0x00); // Stop nRF
+    nrf24_set_idle(handle);
+    nrf24_write_reg(handle, REG_STATUS, 0x70); // clear interrupts
+    if(disable_aa)
+        nrf24_write_reg(handle, REG_EN_AA, 0x00); // Disable Shockburst
+    else
+        nrf24_write_reg(handle, REG_EN_AA, 0x1F); // Enable Shockburst
+
+    nrf24_write_reg(handle, REG_DYNPD, 0x3F); // enable dynamic payload length on all pipes
+    if(noack)
+        nrf24_write_reg(handle, REG_FEATURE, 0x05); // disable payload-with-ack, enable noack
+    else {
+        nrf24_write_reg(handle, REG_CONFIG, 0x0C); // 2 byte CRC
+        nrf24_write_reg(handle, REG_FEATURE, 0x07); // enable dyn payload and ack
+        nrf24_write_reg(
+            handle, REG_SETUP_RETR, 0x1f); // 15 retries for AA, 500us auto retransmit delay
+    }
+
+    nrf24_set_idle(handle);
+    nrf24_flush_rx(handle);
+    nrf24_flush_tx(handle);
+
+    if(maclen) nrf24_set_maclen(handle, maclen);
+    if(srcmac) nrf24_set_src_mac(handle, srcmac, maclen);
+    if(dstmac) nrf24_set_dst_mac(handle, dstmac, maclen);
+
+    nrf24_write_reg(handle, REG_RF_CH, channel);
+    nrf24_write_reg(handle, REG_RF_SETUP, rate);
+    furi_delay_ms(200);
+}
+
+void nrf24_init_promisc_mode(FuriHalSpiBusHandle* handle, uint8_t channel, uint8_t rate) {
+    //uint8_t preamble[] = {0x55, 0x00}; // little endian
+    uint8_t preamble[] = {0xAA, 0x00}; // little endian
+    //uint8_t preamble[] = {0x00, 0x55}; // little endian
+    //uint8_t preamble[] = {0x00, 0xAA}; // little endian
+    nrf24_write_reg(handle, REG_CONFIG, 0x00); // Stop nRF
+    nrf24_write_reg(handle, REG_STATUS, 0x70); // clear interrupts
+    nrf24_write_reg(handle, REG_DYNPD, 0x0); // disable shockburst
+    nrf24_write_reg(handle, REG_EN_AA, 0x00); // Disable Shockburst
+    nrf24_write_reg(handle, REG_FEATURE, 0x05); // disable payload-with-ack, enable noack
+    nrf24_set_maclen(handle, 2); // shortest address
+    nrf24_set_src_mac(handle, preamble, 2); // set src mac to preamble bits to catch everything
+    nrf24_set_packetlen(handle, 32); // set max packet length
+    nrf24_set_idle(handle);
+    nrf24_flush_rx(handle);
+    nrf24_flush_tx(handle);
+    nrf24_write_reg(handle, REG_RF_CH, channel);
+    nrf24_write_reg(handle, REG_RF_SETUP, rate);
+
+    // prime for RX, no checksum
+    nrf24_write_reg(handle, REG_CONFIG, 0x03); // PWR_UP and PRIM_RX, disable AA and CRC
+    furi_hal_gpio_write(nrf24_CE_PIN, true);
+    furi_delay_ms(100);
+}
+
+void hexlify(uint8_t* in, uint8_t size, char* out) {
+    memset(out, 0, size * 2);
+    for(int i = 0; i < size; i++)
+        snprintf(out + strlen(out), sizeof(out + strlen(out)), "%02X", in[i]);
+}
+
+uint64_t bytes_to_int64(uint8_t* bytes, uint8_t size, bool bigendian) {
+    uint64_t ret = 0;
+    for(int i = 0; i < size; i++)
+        if(bigendian)
+            ret |= bytes[i] << ((size - 1 - i) * 8);
+        else
+            ret |= bytes[i] << (i * 8);
+
+    return ret;
+}
+
+void int64_to_bytes(uint64_t val, uint8_t* out, bool bigendian) {
+    for(int i = 0; i < 8; i++) {
+        if(bigendian)
+            out[i] = (val >> ((7 - i) * 8)) & 0xff;
+        else
+            out[i] = (val >> (i * 8)) & 0xff;
+    }
+}
+
+uint32_t bytes_to_int32(uint8_t* bytes, bool bigendian) {
+    uint32_t ret = 0;
+    for(int i = 0; i < 4; i++)
+        if(bigendian)
+            ret |= bytes[i] << ((3 - i) * 8);
+        else
+            ret |= bytes[i] << (i * 8);
+
+    return ret;
+}
+
+void int32_to_bytes(uint32_t val, uint8_t* out, bool bigendian) {
+    for(int i = 0; i < 4; i++) {
+        if(bigendian)
+            out[i] = (val >> ((3 - i) * 8)) & 0xff;
+        else
+            out[i] = (val >> (i * 8)) & 0xff;
+    }
+}
+
+uint64_t bytes_to_int16(uint8_t* bytes, bool bigendian) {
+    uint16_t ret = 0;
+    for(int i = 0; i < 2; i++)
+        if(bigendian)
+            ret |= bytes[i] << ((1 - i) * 8);
+        else
+            ret |= bytes[i] << (i * 8);
+
+    return ret;
+}
+
+void int16_to_bytes(uint16_t val, uint8_t* out, bool bigendian) {
+    for(int i = 0; i < 2; i++) {
+        if(bigendian)
+            out[i] = (val >> ((1 - i) * 8)) & 0xff;
+        else
+            out[i] = (val >> (i * 8)) & 0xff;
+    }
+}
+
+// handle iffyness with preamble processing sometimes being a bit (literally) off
+void alt_address_old(uint8_t* packet, uint8_t* altaddr) {
+    uint8_t macmess_hi_b[4];
+    uint8_t macmess_lo_b[2];
+    uint32_t macmess_hi;
+    uint16_t macmess_lo;
+    uint8_t preserved;
+
+    // get first 6 bytes into 32-bit and 16-bit variables
+    memcpy(macmess_hi_b, packet, 4);
+    memcpy(macmess_lo_b, packet + 4, 2);
+
+    macmess_hi = bytes_to_int32(macmess_hi_b, true);
+
+    //preserve least 7 bits from hi that will be shifted down to lo
+    preserved = macmess_hi & 0x7f;
+    macmess_hi >>= 7;
+
+    macmess_lo = bytes_to_int16(macmess_lo_b, true);
+    macmess_lo >>= 7;
+    macmess_lo = (preserved << 9) | macmess_lo;
+    int32_to_bytes(macmess_hi, macmess_hi_b, true);
+    int16_to_bytes(macmess_lo, macmess_lo_b, true);
+    memcpy(altaddr, &macmess_hi_b[1], 3);
+    memcpy(altaddr + 3, macmess_lo_b, 2);
+}
+
+bool validate_address(uint8_t* addr) {
+    uint8_t bad[][3] = {{0x55, 0x55}, {0xAA, 0xAA}, {0x00, 0x00}, {0xFF, 0xFF}};
+    for(int i = 0; i < 4; i++)
+        for(int j = 0; j < 2; j++)
+            if(!memcmp(addr + j * 2, bad[i], 2)) return false;
+
+    return true;
+}
+
+bool nrf24_sniff_address(FuriHalSpiBusHandle* handle, uint8_t maclen, uint8_t* address) {
+    bool found = false;
+    uint8_t packet[32] = {0};
+    uint8_t packetsize;
+    //char printit[65];
+    uint8_t status = 0;
+    status = nrf24_rxpacket(handle, packet, &packetsize, true);
+    if(status & 0x40) {
+        if(validate_address(packet)) {
+            for(int i = 0; i < maclen; i++) address[i] = packet[maclen - 1 - i];
+
+            /*
+            alt_address(packet, packet);
+
+            for(i = 0; i < maclen; i++)
+                address[i + 5] = packet[maclen - 1 - i];
+            */
+
+            //memcpy(address, packet, maclen);
+            //hexlify(packet, packetsize, printit);
+            found = true;
+        }
+    }
+
+    return found;
+}
+
+uint8_t nrf24_find_channel(
+    FuriHalSpiBusHandle* handle,
+    uint8_t* srcmac,
+    uint8_t* dstmac,
+    uint8_t maclen,
+    uint8_t rate,
+    uint8_t min_channel,
+    uint8_t max_channel,
+    bool autoinit) {
+    uint8_t ping_packet[] = {0x0f, 0x0f, 0x0f, 0x0f}; // this can be anything, we just need an ack
+    uint8_t ch = max_channel + 1; // means fail
+    nrf24_configure(handle, rate, srcmac, dstmac, maclen, 2, false, false);
+    for(ch = min_channel; ch <= max_channel + 1; ch++) {
+        nrf24_write_reg(handle, REG_RF_CH, ch);
+        if(nrf24_txpacket(handle, ping_packet, 4, true)) break;
+    }
+
+    if(autoinit) {
+        FURI_LOG_D("nrf24", "initializing radio for channel %d", ch);
+        nrf24_configure(handle, rate, srcmac, dstmac, maclen, ch, false, false);
+        return ch;
+    }
+
+    return ch;
+}

+ 376 - 0
lib/nrf24/nrf24.h

@@ -0,0 +1,376 @@
+#pragma once
+#include <stdbool.h>
+#include <stdint.h>
+#include <furi_hal_spi.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define R_REGISTER 0x00
+#define W_REGISTER 0x20
+#define REGISTER_MASK 0x1F
+#define ACTIVATE 0x50
+#define R_RX_PL_WID 0x60
+#define R_RX_PAYLOAD 0x61
+#define W_TX_PAYLOAD 0xA0
+#define W_TX_PAYLOAD_NOACK 0xB0
+#define W_ACK_PAYLOAD 0xA8
+#define FLUSH_TX 0xE1
+#define FLUSH_RX 0xE2
+#define REUSE_TX_PL 0xE3
+#define RF24_NOP 0xFF
+
+#define REG_CONFIG 0x00
+#define REG_EN_AA 0x01
+#define REG_EN_RXADDR 0x02
+#define REG_SETUP_AW 0x03
+#define REG_SETUP_RETR 0x04
+#define REG_DYNPD 0x1C
+#define REG_FEATURE 0x1D
+#define REG_RF_SETUP 0x06
+#define REG_STATUS 0x07
+#define REG_RX_ADDR_P0 0x0A
+#define REG_RX_ADDR_P1 0x0B
+#define REG_RX_ADDR_P2 0x0C
+#define REG_RX_ADDR_P3 0x0D
+#define REG_RX_ADDR_P4 0x0E
+#define REG_RX_ADDR_P5 0x0F
+#define REG_RF_CH 0x05
+#define REG_TX_ADDR 0x10
+
+#define RX_PW_P0 0x11
+#define RX_PW_P1 0x12
+#define RX_PW_P2 0x13
+#define RX_PW_P3 0x14
+#define RX_PW_P4 0x15
+#define RX_PW_P5 0x16
+#define TX_DS 0x20
+#define MAX_RT 0x10
+
+#define nrf24_TIMEOUT 500
+#define nrf24_CE_PIN &gpio_ext_pb2
+#define nrf24_HANDLE &furi_hal_spi_bus_handle_external
+
+/* Low level API */
+
+/** Write device register
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      reg     - register
+ * @param      data    - data to write
+ *
+ * @return     device status
+ */
+uint8_t nrf24_write_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t data);
+
+/** Write buffer to device register
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      reg     - register
+ * @param      data    - data to write
+ * @param      size    - size of data to write
+ *
+ * @return     device status
+ */
+uint8_t nrf24_write_buf_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* data, uint8_t size);
+
+/** Read device register
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      reg     - register
+ * @param[out] data    - pointer to data
+ *
+ * @return     device status
+ */
+uint8_t nrf24_read_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* data, uint8_t size);
+
+/** Power up the radio for operation
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_power_up(FuriHalSpiBusHandle* handle);
+
+/** Power down the radio
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_idle(FuriHalSpiBusHandle* handle);
+
+/** Sets the radio to RX mode
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_rx_mode(FuriHalSpiBusHandle* handle);
+
+/** Sets the radio to TX mode
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_tx_mode(FuriHalSpiBusHandle* handle);
+
+/*=============================================================================================================*/
+
+/* High level API */
+
+/** Must call this before using any other nrf24 API
+ * 
+ */
+void nrf24_init();
+
+/** Must call this when we end using nrf24 device
+ * 
+ */
+void nrf24_deinit();
+
+/** Send flush rx command
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ *
+ * @return     device status
+ */
+uint8_t nrf24_flush_rx(FuriHalSpiBusHandle* handle);
+
+/** Send flush tx command
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ *
+ * @return     device status
+ */
+uint8_t nrf24_flush_tx(FuriHalSpiBusHandle* handle);
+
+/** Gets the RX packet length in data pipe 0
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     packet length in data pipe 0
+ */
+uint8_t nrf24_get_packetlen(FuriHalSpiBusHandle* handle);
+
+/** Sets the RX packet length in data pipe 0
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      len - length to set
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_packetlen(FuriHalSpiBusHandle* handle, uint8_t len);
+
+/** Gets configured length of MAC address
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     MAC address length
+ */
+uint8_t nrf24_get_maclen(FuriHalSpiBusHandle* handle);
+
+/** Sets configured length of MAC address
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      maclen - length to set MAC address to, must be greater than 1 and less than 6
+ * 
+ * @return     MAC address length
+ */
+uint8_t nrf24_set_maclen(FuriHalSpiBusHandle* handle, uint8_t maclen);
+
+/** Gets the current status flags from the STATUS register
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     status flags
+ */
+uint8_t nrf24_status(FuriHalSpiBusHandle* handle);
+
+/** Gets the current transfer rate
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     transfer rate in bps
+ */
+uint32_t nrf24_get_rate(FuriHalSpiBusHandle* handle);
+
+/** Sets the transfer rate
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      rate - the transfer rate in bps
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_rate(FuriHalSpiBusHandle* handle, uint32_t rate);
+
+/** Gets the current channel
+ * In nrf24, the channel number is multiplied times 1MHz and added to 2400MHz to get the frequency
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     channel
+ */
+uint8_t nrf24_get_chan(FuriHalSpiBusHandle* handle);
+
+/** Sets the channel
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      frequency - the frequency in hertz
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_chan(FuriHalSpiBusHandle* handle, uint8_t chan);
+
+/** Gets the source mac address
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param[out] mac - the source mac address
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_get_src_mac(FuriHalSpiBusHandle* handle, uint8_t* mac);
+
+/** Sets the source mac address
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      mac - the mac address to set
+ * @param      size - the size of the mac address (2 to 5)
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_src_mac(FuriHalSpiBusHandle* handle, uint8_t* mac, uint8_t size);
+
+/** Gets the dest mac address
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param[out] mac - the source mac address
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_get_dst_mac(FuriHalSpiBusHandle* handle, uint8_t* mac);
+
+/** Sets the dest mac address
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      mac - the mac address to set
+ * @param      size - the size of the mac address (2 to 5)
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_dst_mac(FuriHalSpiBusHandle* handle, uint8_t* mac, uint8_t size);
+
+/** Reads RX packet
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param[out] packet - the packet contents
+ * @param[out] packetsize - size of the received packet
+ * @param      full - boolean set to true, packet length is determined by RX_PW_P0 register, false it is determined by dynamic payload length command
+ * 
+ * @return     device status
+ */
+uint8_t
+    nrf24_rxpacket(FuriHalSpiBusHandle* handle, uint8_t* packet, uint8_t* packetsize, bool full);
+
+/** Sends TX packet
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      packet - the packet contents
+ * @param      size - packet size
+ * @param      ack - boolean to determine whether an ACK is required for the packet or not
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_txpacket(FuriHalSpiBusHandle* handle, uint8_t* payload, uint8_t size, bool ack);
+
+/** Configure the radio
+ * This is not comprehensive, but covers a lot of the common configuration options that may be changed
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      rate - transfer rate in Mbps (1 or 2)
+ * @param      srcmac - source mac address
+ * @param      dstmac - destination mac address
+ * @param      maclen - length of mac address
+ * @param      channel - channel to tune to
+ * @param      noack - if true, disable auto-acknowledge
+ * @param      disable_aa - if true, disable ShockBurst
+ * 
+ */
+void nrf24_configure(
+    FuriHalSpiBusHandle* handle,
+    uint8_t rate,
+    uint8_t* srcmac,
+    uint8_t* dstmac,
+    uint8_t maclen,
+    uint8_t channel,
+    bool noack,
+    bool disable_aa);
+
+/** Configures the radio for "promiscuous mode" and primes it for rx
+ * This is not an actual mode of the nrf24, but this function exploits a few bugs in the chip that allows it to act as if it were.
+ * See http://travisgoodspeed.blogspot.com/2011/02/promiscuity-is-nrf24l01s-duty.html for details.
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      channel - channel to tune to
+ * @param      rate - transfer rate in Mbps (1 or 2) 
+ */
+void nrf24_init_promisc_mode(FuriHalSpiBusHandle* handle, uint8_t channel, uint8_t rate);
+
+/** Listens for a packet and returns first possible address sniffed
+ * Call this only after calling nrf24_init_promisc_mode
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      maclen - length of target mac address
+ * @param[out] addresses - sniffed address
+ * 
+ * @return     success
+ */
+bool nrf24_sniff_address(FuriHalSpiBusHandle* handle, uint8_t maclen, uint8_t* address);
+
+/** Sends ping packet on each channel for designated tx mac looking for ack
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      srcmac - source address
+ * @param      dstmac - destination address
+ * @param      maclen - length of address
+ * @param      rate - transfer rate in Mbps (1 or 2) 
+ * @param      min_channel - channel to start with
+ * @param      max_channel - channel to end at
+ * @param      autoinit - if true, automatically configure radio for this channel
+ * 
+ * @return     channel that the address is listening on, if this value is above the max_channel param, it failed
+ */
+uint8_t nrf24_find_channel(
+    FuriHalSpiBusHandle* handle,
+    uint8_t* srcmac,
+    uint8_t* dstmac,
+    uint8_t maclen,
+    uint8_t rate,
+    uint8_t min_channel,
+    uint8_t max_channel,
+    bool autoinit);
+
+/** Converts 64 bit value into uint8_t array
+ * @param      val  - 64-bit integer
+ * @param[out] out - bytes out
+ * @param      bigendian - if true, convert as big endian, otherwise little endian
+ */
+void int64_to_bytes(uint64_t val, uint8_t* out, bool bigendian);
+
+/** Converts 32 bit value into uint8_t array
+ * @param      val  - 32-bit integer
+ * @param[out] out - bytes out
+ * @param      bigendian - if true, convert as big endian, otherwise little endian
+ */
+void int32_to_bytes(uint32_t val, uint8_t* out, bool bigendian);
+
+/** Converts uint8_t array into 32 bit value
+ * @param      bytes  - uint8_t array
+ * @param      bigendian - if true, convert as big endian, otherwise little endian
+ * 
+ * @return     32-bit value
+ */
+uint32_t bytes_to_int32(uint8_t* bytes, bool bigendian);
+
+#ifdef __cplusplus
+}
+#endif

+ 560 - 0
nrf24scan.c

@@ -0,0 +1,560 @@
+//
+// Written by vad7, 20.11.2022.
+// ver. 1.0
+//
+#include "nrf24scan.h"
+
+#include <furi.h>
+#include <furi_hal.h>
+#include <gui/gui.h>
+#include <dialogs/dialogs.h>
+#include <input/input.h>
+#include <notification/notification_messages.h>
+#include <stdlib.h>
+#include <dolphin/dolphin.h>
+#include <nrf24.h>
+
+#define TAG "nrf24scan"
+#define MAX_CHANNEL 125
+#define MAX_ADDR	6
+
+#define SCAN_APP_PATH_FOLDER "/ext/nrf24scan"
+#define ADDR_FILENAME "addr.txt"	// File format (1 parameter per line): 
+									// 0.25/1/2 - rate in Mbps
+									// 0..125 - default channel
+									// address P0 in hex (5 byte, LSB last)
+									// address P1 in hex (5 byte, LSB last)
+									// address P2, LSB in hex (1 byte)
+									// address P3, LSB in hex (1 byte)
+									// address P4, LSB in hex (1 byte)
+									// address P5, LSB in hex (1 byte)
+									// captured data in raw format, first byte = addr # 0..5
+									// ... up to MAX_LOG_RECORDS
+#define LOG_FILENAME "log.txt"
+#define MAX_LOG_RECORDS	99
+#define LOG_REC_SIZE	33				// max packet size
+#define VIEW_LOG_MAX_X	21
+
+Nrf24Scan* nrf24scan;
+uint8_t what_doing = 0; // 0 - setup, 1 - scanning
+char screen_buf[64];
+char addr_file_name[32];
+uint8_t NRF_rate; 		// 0 - 250Kbps, 1 - 1Mbps, 2 - 2Mbps
+uint8_t NRF_channel;	// 0..125
+struct {
+	uint8_t addr_P0[5];		// MSB first
+	uint8_t addr_P1[5];		// MSB first
+	uint8_t addr_P2;		// LSB only, MSB bytes equal addr_P1
+	uint8_t addr_P3;		// LSB only, MSB bytes equal addr_P1
+	uint8_t addr_P4;		// LSB only, MSB bytes equal addr_P1
+	uint8_t addr_P5;		// LSB only, MSB bytes equal addr_P1
+	uint8_t addr_len;		// 2..5
+	uint8_t addr_count;
+} addrs;
+int8_t log_to_file = 0;	// 0 - no, 1 - yes, 2 - new, -1 - only clear
+uint16_t log_arr_idx;
+uint16_t view_log_arr_idx = 0;
+uint16_t view_log_arr_x = 0;
+uint16_t last_packet_send = -1;
+uint8_t last_packet_send_st = 0;
+int16_t find_channel_period = 0; // sec
+uint8_t menu_selected = 0;
+#define menu_selected_max 5
+uint32_t start_time; 
+
+static uint8_t GetHexVal(char hex) {
+	return (uint8_t)hex - ((uint8_t)hex < 58 ? 48 : ((uint8_t)hex < 97 ? 55 : 87));
+}
+
+// Return num bytes in array
+static uint8_t ConvertHexToArray(char * hex, uint8_t * array, uint8_t maxlen) {
+	uint8_t len = 0;
+	do {
+		uint8_t ch = *hex++;
+		if(ch == 0) break;
+		if(ch < '0') continue;
+		*array++ = (GetHexVal(ch) << 4) + GetHexVal(*hex++);
+		len++;
+	} while(--maxlen);
+	return len;
+}
+
+static void add_to_str_hex_bytes(char *out, char *arr, int bytes)
+{
+	if(!bytes) return;
+	out += strlen(out);
+	do {
+		snprintf(out, 3, "%02X", *arr++);
+		out += 2;
+	} while(--bytes);
+}
+
+void clear_log()
+{
+	log_arr_idx = 0;
+	view_log_arr_idx = 0;
+	last_packet_send = -1;
+}
+
+void write_to_log_file(Storage* storage)
+{
+	if(log_arr_idx == 0) return;
+	Stream* file_stream = file_stream_alloc(storage);
+    FuriString* path = furi_string_alloc();
+    furi_string_set(path, SCAN_APP_PATH_FOLDER);
+	furi_string_cat(path, "/");
+	furi_string_cat(path, LOG_FILENAME);
+	char *txt = malloc(LOG_REC_SIZE * 2 + 2);
+	if(txt == NULL) {
+		FURI_LOG_E(TAG, "No memory to save log!");
+	} else {
+		if(file_stream_open(file_stream, furi_string_get_cstr(path), FSAM_READ_WRITE, FSOM_OPEN_APPEND)) {
+			for(int i = 0; i < log_arr_idx; i++) {
+				add_to_str_hex_bytes(txt, (char*)nrf24scan->log_arr + i * LOG_REC_SIZE, LOG_REC_SIZE);
+				strcat(txt, "\n");
+				int len = strlen(txt);
+				if(stream_write(file_stream, (uint8_t*)txt, len) != len) {
+					FURI_LOG_I(TAG, "Failed to write bytes to log!");
+					break;
+				}
+			}
+		} else {
+			FURI_LOG_E(TAG, "Failed to open log file");
+		}
+		free(txt);
+	}
+	file_stream_close(file_stream);
+	furi_string_free(path);
+	stream_free(file_stream);
+}
+
+static void render_callback(Canvas* const canvas, void* ctx) {
+	const PluginState* plugin_state = acquire_mutex((ValueMutex*)ctx, 25);
+	if(plugin_state == NULL) return;
+	//canvas_draw_frame(canvas, 0, 0, 128, 64); // border around the edge of the screen
+	if(what_doing == 0) {
+		canvas_set_font(canvas, FontSecondary); // 8x10 font
+		snprintf(screen_buf, sizeof(screen_buf), "Settings: %s", addr_file_name); 										// menu_selected = 0
+		canvas_draw_str(canvas, 10, 10, screen_buf);
+		snprintf(screen_buf, sizeof(screen_buf), "Channel: %d", NRF_channel); 											// menu_selected = 1
+		canvas_draw_str(canvas, 10, 20, screen_buf);
+		snprintf(screen_buf, sizeof(screen_buf), "Rate: %sbps",  NRF_rate == 2 ? "2M" : NRF_rate == 1 ? "1M" : "250K");	// menu_selected = 2
+		canvas_draw_str(canvas, 10, 30, screen_buf);
+		strcpy(screen_buf, "Find channel period: ");																	// menu_selected = 3
+		if(find_channel_period == 0) strcat(screen_buf, "off"); else snprintf(screen_buf + strlen(screen_buf), sizeof(screen_buf), "%d s", find_channel_period);
+		canvas_draw_str(canvas, 10, 40, screen_buf);
+		snprintf(screen_buf, sizeof(screen_buf), "Log: %s", log_to_file == 0 ? "No" : log_to_file == 1 ? "Yes" : log_to_file == 2 ? "New" : "Clear");// menu_selected = 4
+		canvas_draw_str(canvas, 10, 50, screen_buf);
+		snprintf(screen_buf, sizeof(screen_buf), "Ok - Start read (%dp)", addrs.addr_count);							// menu_selected = 5
+		canvas_draw_str(canvas, 10, 60,  screen_buf);				
+		canvas_draw_str(canvas, 0, menu_selected * 10 + 10, ">");
+	} else {
+		canvas_set_font(canvas, FontBatteryPercent); // 5x7 font, 9 lines
+		char ch2 = ' ';
+		screen_buf[0] = '\0';
+		if(view_log_arr_x == 0) { strcat(screen_buf, "  "); ch2 = '>';
+		} else {
+			snprintf(screen_buf, sizeof(screen_buf), "<%d", view_log_arr_x);
+			if(view_log_arr_x < VIEW_LOG_MAX_X) ch2 = '>';
+		} 
+		snprintf(screen_buf + strlen(screen_buf), sizeof(screen_buf), " Read %d channel... %c", NRF_channel, ch2);
+		canvas_draw_str(canvas, 0, 7, screen_buf);
+		if(log_arr_idx) {
+			uint16_t page = view_log_arr_idx & ~7;
+			for(uint8_t i = 0; i < 8 && page + i < log_arr_idx; i++) {
+				snprintf(screen_buf, sizeof(screen_buf), "%d: ", page + i + 1);
+				canvas_draw_str(canvas, 0, 14 + i * 7, screen_buf);
+				char *ptr = (char*)nrf24scan->log_arr + (page + i) * LOG_REC_SIZE + view_log_arr_x;
+				if(view_log_arr_x == 0) {
+					snprintf(screen_buf, sizeof(screen_buf), "%d-", *ptr + 1);
+					add_to_str_hex_bytes(screen_buf, ptr + 1, 10);
+				} else {
+					screen_buf[0] = '\0';
+					add_to_str_hex_bytes(screen_buf, ptr + 1, 11);
+				}
+				canvas_draw_str(canvas, 13, 14 + i * 7, screen_buf);
+			}
+			canvas_draw_str(canvas, 10, 14 + (view_log_arr_idx & 7) * 7, last_packet_send != view_log_arr_idx ? ">" : last_packet_send_st == 0 ? "!" : "+");
+		}
+	}
+	release_mutex((ValueMutex*)ctx, plugin_state);
+}
+
+static bool select_settings_file(Stream* stream) {
+    DialogsApp* dialogs = furi_record_open("dialogs");
+    bool result = false;
+    FuriString* path;
+    path = furi_string_alloc();
+    furi_string_set(path, SCAN_APP_PATH_FOLDER);
+
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(&browser_options, ".txt", NULL);
+    browser_options.hide_ext = false;
+
+    bool ret = dialog_file_browser_show(dialogs, path, path, &browser_options);
+
+    furi_record_close("dialogs");
+    if(ret) {
+        if(!file_stream_open(stream, furi_string_get_cstr(path), FSAM_READ, FSOM_OPEN_EXISTING)) {
+			file_stream_close(stream);
+            FURI_LOG_D(TAG, "Cannot open file \"%s\"", furi_string_get_cstr(path));
+        } else {
+			FURI_LOG_D(TAG, "Open file \"%s\"", furi_string_get_cstr(path));
+			strncpy(addr_file_name, furi_string_get_cstr(path) + sizeof(SCAN_APP_PATH_FOLDER), sizeof(addr_file_name));
+            result = true;
+        }
+    }
+    furi_string_free(path);
+    return result;
+}
+
+static bool load_settings_file(Stream* file_stream) {
+    size_t file_size = 0;
+    char* file_buf;
+    bool loaded = false;
+	file_size = stream_size(file_stream);
+	if(file_size == (size_t)0) {
+		FURI_LOG_D(TAG, "load failed. file_size: %d", file_size);
+		return loaded;
+	}
+	file_buf = malloc(file_size);
+	memset(file_buf, 0, file_size);
+	if(stream_read(file_stream, (uint8_t*)file_buf, file_size) == file_size) {
+		FURI_LOG_D(TAG, "Loading settings file");
+		char* line_ptr = file_buf;
+		int16_t line_num = 0;
+		memset((uint8_t*)&addrs, 0, sizeof(addrs));
+		while(line_ptr && line_ptr - file_buf < file_size) {
+			char* end_ptr = strstr((char*)line_ptr, "\n");
+			if(end_ptr == NULL) end_ptr = file_buf + file_size; else *end_ptr = '\0';
+			FURI_LOG_D(TAG, " L#%d: [%d]%s", line_num, end_ptr - line_ptr, line_ptr);
+			if(*line_ptr == '\r') {
+				line_ptr = end_ptr + 1;
+				continue;
+			}
+			if(end_ptr - line_ptr >= LOG_REC_SIZE * 2) { // data
+				if(log_arr_idx < MAX_LOG_RECORDS) {
+					ConvertHexToArray(line_ptr, nrf24scan->log_arr + log_arr_idx * LOG_REC_SIZE, LOG_REC_SIZE);
+					log_arr_idx++;
+					clear_log();
+				}
+			} else if(addrs.addr_count) {
+				ConvertHexToArray(line_ptr, addrs.addr_count==1? &addrs.addr_P1[0] : addrs.addr_count==2? &addrs.addr_P2 : addrs.addr_count==3? &addrs.addr_P3 : addrs.addr_count==4? &addrs.addr_P4: &addrs.addr_P5, addrs.addr_count==1? 5 : 1);
+				FURI_LOG_D(TAG, " +Addr_LSB: %s", line_ptr);
+				if(++addrs.addr_count == MAX_ADDR) break;
+			} else if(end_ptr - line_ptr < 6) { // Rate or Channel
+				if(line_num == 0) { // 1st line - Rate
+					NRF_rate = atoi(line_ptr);
+					FURI_LOG_D(TAG, " Rate: %d", NRF_rate);
+				} else if(line_num == 1) { // second line - Channel
+					NRF_channel = atoi(line_ptr);
+					FURI_LOG_D(TAG, " Ch: %d", NRF_channel);
+				}
+			} else if(end_ptr - line_ptr <= 5*2 + 1){ // addresses
+				addrs.addr_len = ConvertHexToArray(line_ptr, addrs.addr_P0, 5);
+				FURI_LOG_D(TAG, " +Addr(%d): %02X%02X%02X...", addrs.addr_len, addrs.addr_P0[0], addrs.addr_P0[1], addrs.addr_P0[2]);
+				loaded = true;
+				if(++addrs.addr_count == MAX_ADDR) break;
+			}
+			line_ptr = end_ptr + 1;
+			line_num++;
+		}
+	} else {
+		FURI_LOG_D(TAG, "load failed. file size: %d", file_size);
+	}
+	free(file_buf);
+    return loaded;
+}
+
+static void input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) {
+	furi_assert(event_queue);
+	PluginEvent event = {.type = EventTypeKey, .input = *input_event};
+	furi_message_queue_put(event_queue, &event, FuriWaitForever);
+}
+
+static void start_scanning() 
+{
+	uint8_t addr[5];
+	uint8_t erx_addr = (1<<0); // Enable RX_P0
+	if(addrs.addr_count == 0) return;
+	nrf24_write_reg(nrf24_HANDLE, REG_CONFIG, 0x00); // Stop nRF
+	nrf24_write_reg(nrf24_HANDLE, REG_STATUS, 0x70); // clear interrupts
+	nrf24_write_reg(nrf24_HANDLE, REG_DYNPD, 0x0); // disable shockburst
+	nrf24_write_reg(nrf24_HANDLE, REG_EN_AA, 0x00); // Disable Shockburst
+	nrf24_write_reg(nrf24_HANDLE, REG_FEATURE, 0x01); // Enables the W_TX_PAYLOAD_NOACK command, Disable Payload with ACK, Disable Dynamic Payload Length
+	nrf24_set_maclen(nrf24_HANDLE, addrs.addr_len);
+	for(int i = 0; i < addrs.addr_len; i++) addr[i] = addrs.addr_P0[addrs.addr_len - i - 1];
+    nrf24_write_buf_reg(nrf24_HANDLE, REG_RX_ADDR_P0, &addr[0], addrs.addr_len);
+	nrf24_write_reg(nrf24_HANDLE, RX_PW_P0, 32);
+	if(addrs.addr_count > 1) {
+		for(int i = 0; i < addrs.addr_len; i++) addr[i] = addrs.addr_P1[addrs.addr_len - i - 1];
+		nrf24_write_buf_reg(nrf24_HANDLE, REG_RX_ADDR_P1, &addr[0], addrs.addr_len);
+		nrf24_write_reg(nrf24_HANDLE, RX_PW_P1, 32);
+		erx_addr |= (1<<1); // Enable RX_P1
+	} else nrf24_write_reg(nrf24_HANDLE, RX_PW_P1, 0);
+	if(addrs.addr_count > 2) {
+		nrf24_write_buf_reg(nrf24_HANDLE, REG_RX_ADDR_P2, &addrs.addr_P2, 1);
+		nrf24_write_reg(nrf24_HANDLE, RX_PW_P2, 32);
+		erx_addr |= (1<<2); // Enable RX_P2
+	} else nrf24_write_reg(nrf24_HANDLE, RX_PW_P2, 0);
+	if(addrs.addr_count > 3) {
+		nrf24_write_buf_reg(nrf24_HANDLE, REG_RX_ADDR_P3, &addrs.addr_P3, 1);
+		nrf24_write_reg(nrf24_HANDLE, RX_PW_P3, 32);
+		erx_addr |= (1<<3); // Enable RX_P3
+	} else nrf24_write_reg(nrf24_HANDLE, RX_PW_P3, 0);
+	if(addrs.addr_count > 4) {
+		nrf24_write_buf_reg(nrf24_HANDLE, REG_RX_ADDR_P4, &addrs.addr_P4, 1);
+		nrf24_write_reg(nrf24_HANDLE, RX_PW_P4, 32);
+		erx_addr |= (1<<4); // Enable RX_P4
+	} else nrf24_write_reg(nrf24_HANDLE, RX_PW_P4, 0);
+	if(addrs.addr_count > 5) {
+		nrf24_write_buf_reg(nrf24_HANDLE, REG_RX_ADDR_P5, &addrs.addr_P5, 1);
+		nrf24_write_reg(nrf24_HANDLE, RX_PW_P5, 32);
+		erx_addr |= (1<<5); // Enable RX_P5
+	} else nrf24_write_reg(nrf24_HANDLE, RX_PW_P5, 0);
+	nrf24_write_reg(nrf24_HANDLE, REG_STATUS, 0x50); // clear RX_DR, MAX_RT.
+	nrf24_set_idle(nrf24_HANDLE);
+	nrf24_flush_rx(nrf24_HANDLE);
+	nrf24_flush_tx(nrf24_HANDLE);
+	nrf24_write_reg(nrf24_HANDLE, REG_EN_RXADDR, erx_addr);
+	nrf24_write_reg(nrf24_HANDLE, REG_RF_CH, NRF_channel);
+	nrf24_write_reg(nrf24_HANDLE, REG_RF_SETUP, NRF_rate);
+	// prime for RX, no checksum
+	nrf24_write_reg(nrf24_HANDLE, REG_CONFIG, 0x03); // PWR_UP and PRIM_RX, disable AA and CRC
+	furi_hal_gpio_write(nrf24_CE_PIN, true);
+	furi_delay_ms(100);
+
+	start_time = furi_get_tick();
+	FURI_LOG_D(TAG, "Start scan: Ch=%d Rate=%d", NRF_channel, NRF_rate);
+}
+
+bool nrf24_read_newpacket() {
+	if(nrf24scan->log_arr == NULL) return false;
+    bool found = false;
+    uint8_t packetsize;
+	uint8_t packet[32] = {0};
+    uint8_t status = nrf24_rxpacket(nrf24_HANDLE, packet, &packetsize, true);
+    if(status & 0x40) {
+		uint8_t *ptr = nrf24scan->log_arr + log_arr_idx * LOG_REC_SIZE;
+		*ptr++ = (status >> 1) & 7; // pipe #
+		memcpy(ptr, packet, packetsize);
+		if(packetsize < LOG_REC_SIZE) memset(ptr + packetsize, 0, LOG_REC_SIZE - packetsize);
+		if(log_arr_idx < MAX_LOG_RECORDS - 1) {
+			log_arr_idx++;
+		} else {
+			if(log_to_file > 0) {
+				write_to_log_file(nrf24scan->storage);
+				log_arr_idx = 0;
+			} else {
+				memmove(nrf24scan->log_arr, nrf24scan->log_arr + LOG_REC_SIZE, log_arr_idx * LOG_REC_SIZE);
+			}
+		}
+		FURI_LOG_D(TAG, "Found packet #%d pipe %d", log_arr_idx, (status >> 1) & 7);
+        found = true;
+    }
+	return found;
+}
+
+bool nrf24_send_packet()
+{
+	if(log_arr_idx == 0) return false;
+	last_packet_send_st = nrf24_txpacket(nrf24_HANDLE, nrf24scan->log_arr + view_log_arr_idx * LOG_REC_SIZE + 1, 32, false);
+	last_packet_send = view_log_arr_idx;
+	return last_packet_send_st;
+}
+
+void allocate_log_array() 
+{
+	nrf24scan->log_arr = malloc(LOG_REC_SIZE * MAX_LOG_RECORDS);
+	if(nrf24scan->log_arr == NULL) {
+		FURI_LOG_E(TAG, "Not enouch memory: %d", LOG_REC_SIZE * MAX_LOG_RECORDS);
+		strcpy(addr_file_name, "MEMORY LOW!");
+	}
+	clear_log();
+}
+
+int32_t nrf24scan_app(void* p) {
+	UNUSED(p);
+	nrf24scan = malloc(sizeof(Nrf24Scan));
+	nrf24scan->event_queue = furi_message_queue_alloc(8, sizeof(PluginEvent));
+	nrf24scan->plugin_state = malloc(sizeof(PluginState));
+	ValueMutex state_mutex;
+	if(!init_mutex(&state_mutex, nrf24scan->plugin_state, sizeof(PluginState))) {
+		furi_message_queue_free(nrf24scan->event_queue);
+		FURI_LOG_E(TAG, "cannot create mutex\r\n");
+		free(nrf24scan->plugin_state);
+		return 255;
+	}
+	memset((uint8_t*)&addrs, 0, sizeof(addrs));
+	nrf24_init();
+
+	// Set system callbacks
+	nrf24scan->view_port = view_port_alloc();
+	view_port_draw_callback_set(nrf24scan->view_port, render_callback, &state_mutex);
+	view_port_input_callback_set(nrf24scan->view_port, input_callback, nrf24scan->event_queue);
+
+	// Open GUI and register view_port
+	nrf24scan->gui = furi_record_open(RECORD_GUI);
+	gui_add_view_port(nrf24scan->gui, nrf24scan->view_port, GuiLayerFullscreen);
+	NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
+	nrf24scan->storage = furi_record_open(RECORD_STORAGE);
+	storage_common_mkdir(nrf24scan->storage, SCAN_APP_PATH_FOLDER);
+	Stream* file_stream = file_stream_alloc(nrf24scan->storage);
+    FuriString* path = furi_string_alloc();
+    furi_string_set(path, SCAN_APP_PATH_FOLDER);
+	furi_string_cat(path, "/");
+	furi_string_cat(path, ADDR_FILENAME);
+    if(file_stream_open(file_stream, furi_string_get_cstr(path), FSAM_READ, FSOM_OPEN_EXISTING)) {
+		if(load_settings_file(file_stream)) strcpy(addr_file_name, ADDR_FILENAME); else strcpy(addr_file_name, "LOAD ERROR");
+	} else {
+		strcpy(addr_file_name, "NONE");
+	}
+	file_stream_close(file_stream);
+	furi_string_free(path);
+	stream_free(file_stream);
+	allocate_log_array();
+
+	PluginEvent event;
+	for(bool processing = true; processing;) {
+		FuriStatus event_status = furi_message_queue_get(nrf24scan->event_queue, &event, 100);
+		PluginState* plugin_state = (PluginState*)acquire_mutex_block(&state_mutex);
+
+		if(event_status == FuriStatusOk) {
+			// press events
+			if(event.type == EventTypeKey) {
+				switch(event.input.key) {
+				case InputKeyUp:
+					if(event.input.type == InputTypePress || event.input.type == InputTypeRepeat) {
+						if(what_doing == 0) {
+							if(menu_selected > 0) menu_selected--; else menu_selected = menu_selected_max;
+						} else {
+							if(view_log_arr_idx > 0) view_log_arr_idx--;
+						}
+					}
+					break;
+				case InputKeyDown:
+					if(event.input.type == InputTypePress || event.input.type == InputTypeRepeat) {
+						if(what_doing == 0) {
+							if(menu_selected < menu_selected_max) menu_selected++; else menu_selected = 0;
+						} else {
+							if(view_log_arr_idx < log_arr_idx - 1) view_log_arr_idx++;
+						}
+					}
+					break;
+				case InputKeyRight:
+					if(event.input.type == InputTypePress || event.input.type == InputTypeRepeat) {
+						if(what_doing == 0) {
+							switch(menu_selected) {
+								case 1:
+									NRF_channel += event.input.type == InputTypeRepeat ? 10 : 1;
+									if(NRF_channel > MAX_CHANNEL) NRF_channel = 0;
+									break;
+								case 2:
+									NRF_rate++;
+									if(NRF_rate > 2) NRF_rate = 0;
+									break;
+								case 3:
+									find_channel_period += event.input.type == InputTypeRepeat ? 10 : 1;
+									break;
+								case 4:
+									if(++log_to_file > 2) log_to_file = -1;
+									break;
+							}
+						} else {
+							if(view_log_arr_x < VIEW_LOG_MAX_X) view_log_arr_x++;
+						}
+					}
+					break;
+				case InputKeyLeft:
+					if(event.input.type == InputTypePress || event.input.type == InputTypeRepeat) {
+						if(what_doing == 0) {
+							switch(menu_selected) {
+								case 1:
+									NRF_channel -= event.input.type == InputTypeRepeat ? 10 : 1;
+									if(NRF_channel > MAX_CHANNEL) NRF_channel = MAX_CHANNEL;
+									break;
+								case 2:
+									NRF_rate--;
+									if(NRF_rate > 2) NRF_rate = 2;
+									break;
+								case 3:
+									find_channel_period -= event.input.type == InputTypeRepeat ? 10 : 1;
+									if(find_channel_period < 0) find_channel_period = 0;
+									break;
+								case 4:
+									if(--log_to_file < -1) log_to_file = 2;
+									break;
+							}
+						} else {
+							if(view_log_arr_x > 0) view_log_arr_x--;
+						}
+					}
+					break;
+				case InputKeyOk:
+					if(event.input.type == InputTypePress) {
+						if(what_doing == 0) {
+							if(menu_selected == 0) { // File
+								file_stream = file_stream_alloc(nrf24scan->storage);
+								if(select_settings_file(file_stream)) {
+									if(!load_settings_file(file_stream)) {
+										strcpy(addr_file_name, "LOAD ERROR");
+									} else {
+										if(log_to_file > 0) write_to_log_file(nrf24scan->storage);
+										clear_log();
+									}
+									file_stream_close(file_stream);
+								}
+								stream_free(file_stream);
+							} else if(menu_selected == 5) {
+								what_doing = !what_doing;
+								if(what_doing) {
+									if(log_to_file == 2 || log_to_file == -1) clear_log();
+									start_scanning(); 
+								} else nrf24_set_idle(nrf24_HANDLE);
+							}
+						} else { // Send
+							nrf24_send_packet();
+						}
+					} else if(event.input.type == InputTypeLong) {
+						what_doing = !what_doing;
+						if(what_doing) {
+							if(log_to_file == 2 || log_to_file == -1) clear_log();
+							start_scanning(); 
+						} else nrf24_set_idle(nrf24_HANDLE);
+					}
+					break;
+				case InputKeyBack:
+					if(event.input.type == InputTypeLong) processing = false; else what_doing = 0;
+					nrf24_set_idle(nrf24_HANDLE);
+					break;
+				default:
+					break;
+				}
+			}
+		}
+
+		if(what_doing) {
+			nrf24_read_newpacket();
+			if(find_channel_period && furi_get_tick() - start_time >= (uint32_t)find_channel_period * 1000UL) {
+				if(++NRF_channel > MAX_CHANNEL) NRF_channel = 0;
+				start_scanning();
+			}
+		}
+
+		view_port_update(nrf24scan->view_port);
+		release_mutex(&state_mutex, plugin_state);
+	}
+
+	nrf24_deinit();
+	view_port_enabled_set(nrf24scan->view_port, false);
+	gui_remove_view_port(nrf24scan->gui, nrf24scan->view_port);
+	furi_record_close(RECORD_GUI);
+	furi_record_close(RECORD_NOTIFICATION);
+	furi_record_close(RECORD_STORAGE);
+	view_port_free(nrf24scan->view_port);
+	furi_message_queue_free(nrf24scan->event_queue);
+	free(nrf24scan->plugin_state);
+	if(nrf24scan->log_arr) free(nrf24scan->log_arr);
+	free(nrf24scan);
+	return 0;
+}

+ 33 - 0
nrf24scan.h

@@ -0,0 +1,33 @@
+#pragma once
+
+#include <gui/gui.h>
+#include <gui/view.h>
+#include <gui/view_dispatcher.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/dialog_ex.h>
+#include <toolbox/stream/file_stream.h>
+
+typedef enum {
+	EventTypeTick,
+	EventTypeKey,
+} EventType;
+
+typedef struct {
+	EventType type;
+	InputEvent input;
+} PluginEvent;
+
+typedef struct {
+	int x;
+	int y;
+} PluginState;
+
+typedef struct {
+    Gui* gui;
+    FuriMessageQueue* event_queue;
+    PluginState* plugin_state;
+    ViewPort* view_port;
+	Storage* storage;
+    uint8_t* log_arr;
+} Nrf24Scan;
+

BIN
nrf24scan_10px.png