Fake BLE broadcasting from nrf24l01 to ESP32

I found this a while back https://d.lij.uno/misc-nrf24-ble.html and I used that as a starting point to update the sensors I made years ago to send fake ble and receive the data with a esp32
the battery life can be really good, before I updated the code to send fake ble I got a few years and millions of messages sent from a attiny13 nrf24l01 sensor with cr2032 battery, I hope this will last a long time too.

below are a example of a simplified code that just send fixed values and you can modify it to send whatever values you like.

ble_32.yaml

#swedude
esphome:
  name: ble-32-nrf
  platform: ESP32
  board: esp32dev
     
wifi:
  fast_connect: true 
  ssid: "xxxxx"
  password: "xxxxxx"

# Enable logging
logger:
  level: DEBUG
# Enable Home Assistant API
api:

ota:

esp32_ble_tracker:
  scan_parameters:
    active: false
    duration: 32s
    interval: 320ms
    window: 310ms     

  on_ble_advertise:
    - mac_address: '66:55:44:33:22:11'
      then:
        - lambda: |-
            ESP_LOGD("ble_adv", "New BLE device");
            ESP_LOGD("ble_adv", "  address: %s", x.address_str().c_str());
            ESP_LOGD("ble_adv", "  name: %s", x.get_name().c_str());
            ESP_LOGD("ble_adv", "  Advertised manufacturer data:");
            for (auto data : x.get_manufacturer_datas()) {
                ESP_LOGD("ble_adv", "    - %s: (length %i)", data.uuid.to_string().c_str(), data.data.size()); 
            }

  on_ble_manufacturer_data_advertise:
    - mac_address: '66:55:44:33:22:11'
      manufacturer_id: '0201'
      then:
        - lambda: |-        
            struct __attribute__((packed)) dataStruct {
            int16_t temp;
            int16_t humidity;
            uint32_t counter;
            } myData;
            
            memcpy(&myData, &*x.begin(), sizeof(myData)); //https://stackoverflow.com/questions/60505975/copying-stdvectorunsigned-char-to-void-buffer-using-memcpy
                                  
            id(rf24_temp_).publish_state(myData.temp/10.0);
            id(rf24_humidity_).publish_state(myData.humidity/10.0);
            id(rf24_counter_).publish_state(myData.counter);

sensor:
  - platform: template
    name: "rf24_temp"
    id: rf24_temp_
    update_interval: never
    unit_of_measurement: °C
    accuracy_decimals: 2

  - platform: template
    name: "rf24_humidity"
    unit_of_measurement: '%'
    id: rf24_humidity_
    update_interval: never
    accuracy_decimals: 2
    
  - platform: template
    name: "rf24_counter"
    id: rf24_counter_
    update_interval: never
    unit_of_measurement: ' '
    accuracy_decimals: 0

arduino code

// Inspired by http://dmitry.gr/index.php?r=05.Projects&proj=11.%20Bluetooth%20LE%20fakery
//code from https://d.lij.uno/misc-nrf24-ble.html
//adapted for esphome swedude
#include "SPI.h"  // SPI in Arduino Uno/Nano: MOSI pin 11, MISO pin 12, SCK pin 13
#define PIN_CE  9 // chip enable
#define PIN_CSN 10   // chip select (for SPI)

// The MAC address of BLE advertizer -- just make one up
#define MY_MAC_0  0x11
#define MY_MAC_1  0x22
#define MY_MAC_2  0x33
#define MY_MAC_3  0x44
#define MY_MAC_4  0x55
#define MY_MAC_5  0x66


struct __attribute__((packed)) dataStruct {
  int16_t temp;
  int16_t humidity;
  uint32_t counter;
} myData;


uint8_t buf[32];
static const uint8_t chRf[] = {2, 26, 80};
static const uint8_t chLe[] = {37, 38, 39};
uint8_t ch = 0;  // RF channel for frequency hopping

void btLeCrc(const uint8_t* data, uint8_t len, uint8_t* dst) {
  // implementing CRC with LFSR
  uint8_t v, t, d;

  while (len--) {
    d = *data++;
    for (v = 0; v < 8; v++, d >>= 1) {
      t = dst[0] >> 7;
      dst[0] <<= 1;
      if (dst[1] & 0x80) dst[0] |= 1;
      dst[1] <<= 1;
      if (dst[2] & 0x80) dst[1] |= 1;
      dst[2] <<= 1;

      if (t != (d & 1)) {
        dst[2] ^= 0x5B;
        dst[1] ^= 0x06;
      }
    }
  }
}

uint8_t  swapbits(uint8_t a) {
  // reverse the bit order in a single byte
  uint8_t v = 0;
  if (a & 0x80) v |= 0x01;
  if (a & 0x40) v |= 0x02;
  if (a & 0x20) v |= 0x04;
  if (a & 0x10) v |= 0x08;
  if (a & 0x08) v |= 0x10;
  if (a & 0x04) v |= 0x20;
  if (a & 0x02) v |= 0x40;
  if (a & 0x01) v |= 0x80;
  return v;
}

void btLeWhiten(uint8_t* data, uint8_t len, uint8_t whitenCoeff) {
  // Implementing whitening with LFSR
  uint8_t  m;
  while (len--) {
    for (m = 1; m; m <<= 1) {
      if (whitenCoeff & 0x80) {
        whitenCoeff ^= 0x11;
        (*data) ^= m;
      }
      whitenCoeff <<= 1;
    }
    data++;
  }
}

static inline uint8_t btLeWhitenStart(uint8_t chan) {
  //the value we actually use is what BT'd use left shifted one...makes our life easier
  return swapbits(chan) | 2;
}

void btLePacketEncode(uint8_t* packet, uint8_t len, uint8_t chan) {
  // Assemble the packet to be transmitted
  // Length is of packet, including crc. pre-populate crc in packet with initial crc value!
  uint8_t i, dataLen = len - 3;
  btLeCrc(packet, dataLen, packet + dataLen);
  for (i = 0; i < 3; i++, dataLen++)
    packet[dataLen] = swapbits(packet[dataLen]);
  btLeWhiten(packet, len, btLeWhitenStart(chan));
  for (i = 0; i < len; i++)
    packet[i] = swapbits(packet[i]); // the byte order of the packet should be reversed as well

}

uint8_t spi_byte(uint8_t byte) {
  // using Arduino's SPI library; clock out one byte
  SPI.transfer(byte);
  return byte;
}

void nrf_cmd(uint8_t cmd, uint8_t data) {
  // Write to nRF24's register
  digitalWrite(PIN_CSN, LOW);
  spi_byte(cmd);
  spi_byte(data);
  digitalWrite(PIN_CSN, HIGH);
}

void nrf_simplebyte(uint8_t cmd) {
  // transfer only one byte
  digitalWrite(PIN_CSN, LOW);
  spi_byte(cmd);
  digitalWrite(PIN_CSN, HIGH);
}

void nrf_manybytes(uint8_t* data, uint8_t len) {
  // transfer several bytes in a row
  digitalWrite(PIN_CSN, LOW);
  do {
    spi_byte(*data++);
  } while (--len);
  digitalWrite(PIN_CSN, HIGH);
}


void setup() {
  pinMode(PIN_CSN, OUTPUT);
  pinMode(PIN_CE, OUTPUT);
  pinMode(11, OUTPUT);
  pinMode(13, OUTPUT);
  digitalWrite(PIN_CSN, HIGH);
  digitalWrite(PIN_CE, LOW);

  Serial.begin(9600);
  Serial.println("Start LE advertizing");
  SPI.begin();
  SPI.setBitOrder(MSBFIRST);

  // Now initialize nRF24L01+, setting general parameters
  nrf_cmd(0x20, 0x12);  //on, no crc, int on RX/TX done
  nrf_cmd(0x21, 0x00);  //no auto-acknowledge
  nrf_cmd(0x22, 0x00);  //no RX
  nrf_cmd(0x23, 0x02);  //4-byte address
  nrf_cmd(0x24, 0x00);  //no auto-retransmit
  nrf_cmd(0x26, 0x06);  //1MBps at 0dBm
  nrf_cmd(0x27, 0x3E);  //clear various flags
  nrf_cmd(0x3C, 0x00);  //no dynamic payloads
  nrf_cmd(0x3D, 0x00);  //no features
  nrf_cmd(0x31, 32);          //always RX 32 bytes
  nrf_cmd(0x22, 0x01);  //RX on pipe 0

  // Set access addresses (TX address in nRF24L01) to BLE advertising 0x8E89BED6
  // Remember that both bit and byte orders are reversed for BLE packet format
  buf[0] = 0x30;
  buf[1] = swapbits(0x8E);
  buf[2] = swapbits(0x89);
  buf[3] = swapbits(0xBE);
  buf[4] = swapbits(0xD6);
  nrf_manybytes(buf, 5);
  //buf[0] = 0x2A;    // set RX address in nRF24L01, doesn't matter because RX is ignored in this case
  //nrf_manybytes(buf, 5);
}

void loop() {
  //-----------------add your own data here------------------
  myData.temp = 224;
  myData.humidity = 397;
  //-----------------------------------
  nrf_cmd(0x20, 0x12);  // TX on
  delayMicroseconds(2700);//time needed for nrf to wake upp 2000-3000us 3000+ needed on some clones...


  // Channel hopping
  for (ch = 0; ch < sizeof(chRf); ch++)
  {


    //nrf_simplebyte(0xE2); //Clear RX Fifo
    nrf_simplebyte(0xE1); //Clear TX Fifo
    nrf_cmd(0x27, 0x6E);  // Clear flags

    uint8_t i, L = 0;

    buf[L++] = 0x42;  //PDU type, given address is random; 0x42 for Android and 0x40 for iPhone
    buf[L++] = 15 + ((sizeof(myData)) + 4); // length of payload

    buf[L++] = MY_MAC_0;
    buf[L++] = MY_MAC_1;
    buf[L++] = MY_MAC_2;
    buf[L++] = MY_MAC_3;
    buf[L++] = MY_MAC_4;
    buf[L++] = MY_MAC_5;

    buf[L++] = 2;   //flags (LE-only, general discoverable mode)
    buf[L++] = 0x01;
    buf[L++] = 0x06;

    buf[L++] = 5;   // length of the name, including type byte
    buf[L++] = 0x08;
    //buf[L++] = 'N';
    buf[L++] = 'R';
    buf[L++] = 'F';
    buf[L++] = '2';
    buf[L++] = '4';

    buf[L++] = ((sizeof(myData)) + 3); // length of custom data, including type byte
    buf[L++] = 0xff;
    buf[L++] = 0x01;
    buf[L++] = 0x02;
    for (char i = 0; i < (sizeof(myData)); i++) {
      buf[L++] = (*((char*)(&myData) + i));
    }

    buf[L++] = 0x55;  //CRC start value: 0x555555
    buf[L++] = 0x55;
    buf[L++] = 0x55;

    nrf_cmd(0x25, chRf[ch]);
    btLePacketEncode(buf, L, chLe[ch]);
    digitalWrite(PIN_CSN, LOW);
    spi_byte(0xA0);
    for (i = 0 ; i < L ; i++) spi_byte(buf[i]);
    digitalWrite(PIN_CSN, HIGH);
    //nrf_cmd(0x20, 0x12);  // TX on
    digitalWrite(PIN_CE, HIGH); // Enable Chip

    delayMicroseconds (510);         //
    digitalWrite(PIN_CE, LOW);   // (in preparation of switching to RX quickly)
  }

  nrf_cmd(0x20, 0x00);   //sleep nrf
  delay(2000);// replace with sleep();
  myData.counter++;
}

I use a struct so you can change whatever data type you want to send just make the same changes on both sides and that you keep it at or below 8 bytes if you make the name even shorter you can make it bigger

struct __attribute__((packed)) dataStruct {
  int16_t temp;
  int16_t humidity;
  uint32_t counter;
} myData;

Let me know if you make something fun with it

1 Like

Interesting, is it possible to make ESPHome perform the BLE broadcasting?

Why?

  1. I feel it is possible to overcome this challenge soon AOJ-33B Tuya bluetooth blood pressure monitor
  2. I’m thinking of crafting an ESPHome YAML that can read its own RX and broadcast a BLE blood pressure payload, this will be ESP32-1
  3. This ESP32-1 will be embedded into the blood pressure monitor
  4. I will have another ESPHome BLE proxy (ESP32-2) listening for any BLE payload
  5. This makes it possible for my readings to go into HA directly
  6. ESPHome is easy to flash as all I need is just Chrome