BLE Mesh (brLight) lights

So.
I have bought 2 set of BLE lights on Amazon.
Flood lights and Recessed ceiling lights.

The control App in the pictures looked exactly the same for both.

It turned out while the GUI is exactly the same, the underlying protocol is completly different.

brMesh and BroadlinkBLE apps - Broadlink Fastcon, my floodlights
brLight - my ceiling lights

For the flood lights (MELPO flood lights https://amazon.com/dp/B07W6SHBV5) it turned out to be “Broadlink Fastcon”, while it says it is a BT Mesh, I highly doubt it, as the app just sends broadcast BT advertisment with “manufacturer data” containing encrypted command, and that’s it.

Integrating this lights (brMesh) has been solved and I made a quick demo, see:

While the ceiling lights (https://amazon.com/dp/B09PZWBYL9) seem to implement real BT mesh, with mesh provisioning, etc.

I wonder if someone (besides GitHub - dominikberse/homeassistant-bluetooth-mesh: Allows to use bluetooth mesh devices directly from homeassistant) started any research on the BLE Mesh lights? It seem to be a pretty standard thing.

If not, I guess I would invest some time in the BLE Mesh stuff too.

the code below is a working example for the ESP32 compiled with Arduino IDE. It turns one light on/off once a second.
It is based on java script code of @ArcadeMachinist.

Maybe this code helps finding a solution available in Home Assistant or ESPHome (maybe derived from BLEAdvertising class).
I’m not an expert in Home Assistant or ESPHome, so I can’t do it, sorry.

#include "BLEDevice.h"
#include "BLEUtils.h"
#include "BLEServer.h"
#include "BLEBeacon.h"

/*
 * see: 
 * https://circuitdigest.com/microcontroller-projects/esp32-based-bluetooth-ibeacon
 * https://community.home-assistant.io/t/brmesh-app-bluetooth-lights/473486/19
 * 
 */

// See the following for generating UUIDs:
// https://www.uuidgenerator.net/

BLEAdvertising *pAdvertising;   // BLE Advertisement type
#define BEACON_UUID "87b99b2c-90fd-11e9-bc42-526af7764f64" // UUID 1 128-Bit (may use linux tool uuidgen or random numbers via https://www.uuidgenerator.net/)

const uint8_t my_key[] = { 0x23, 0x15, 0x55, 0x38 };         // Your unique Mesh key - see README
const uint8_t default_key[] = { 0x5e, 0x36, 0x7b, 0xc4 };
uint8_t lightId = 1;

void setup() {
  Serial.begin(115200);
  Serial.printf("start ESP32 DEVICEID - %llX\n", ESP.getEfuseMac());

  // Create the BLE Device
  BLEDevice::init("ESP32 as iBeacon");

  pAdvertising = BLEDevice::getAdvertising();
  BLEDevice::startAdvertising();
}

void send(uint8_t* data, uint8_t dataLength);

uint8_t SEND_SEQ = 0;
uint8_t SEND_COUNT = 1;

const uint8_t BLE_CMD_RETRY_CNT = 1;
const uint8_t DEFAULT_BLE_FASTCON_ADDRESS[] = { 0xC1, 0xC2, 0xC3 };
const uint8_t addrLength = 3;

void dump(const uint8_t* data, int length)
{
  for (int i = 0; i < length; i++)
  {
    printf("%2.2X", data[i]);
  }
}

uint8_t package_ble_fastcon_body(int i, int i2, uint8_t sequence, uint8_t safe_key, int forward, const uint8_t* data, int length, const uint8_t* key, uint8_t*& payload)
{
  if (length > 12)
  {
    printf("data too long\n");
    payload = 0;
    return 0;
  }
  uint8_t payloadLength = 4 + 12;
  payload = (uint8_t*)malloc(payloadLength);
  payload[0] = (i2 & 0b1111) << 0 | (i & 0b111) << 4 | (forward & 0xff) << 7;
  payload[1] = sequence & 0xff;
  payload[2] = safe_key;
  payload[3] = 0; // checksum
  // fill payload with zeros
  for (int j = 4; j < payloadLength; j++) payload[j]=0;
  memcpy(payload + 4, data, length);

  uint8_t checksum = 0;
  for (int j = 0; j < length + 4; j++) 
  {
    if (j == 3) continue;
    checksum = (checksum + payload[j]) & 0xff;
  }
  payload[3] = checksum;
  for (int j = 0; j < 4; j++) {
    payload[j] = default_key[j & 3] ^ payload[j];
  }
  for (int j = 0; j < 12; j++) {
    payload[4 + j] = my_key[j & 3] ^ payload[4 + j];
  }
  return payloadLength;
}

uint8_t get_payload_with_inner_retry(int i, const uint8_t* data, int length, int i2, const uint8_t* key, int forward, uint8_t*& payload) {
  printf("data: "); dump(data, length); printf("\n");

  SEND_COUNT++;
  SEND_SEQ = SEND_COUNT;
  uint8_t safe_key = key[3];
  return package_ble_fastcon_body(i, i2, SEND_SEQ, safe_key, forward, data, length, key, payload);
}

void whiteningInit(uint8_t val, uint8_t* ctx)
{
  ctx[0] = 1;
  ctx[1] = (val >> 5) & 1;
  ctx[2] = (val >> 4) & 1;
  ctx[3] = (val >> 3) & 1;
  ctx[4] = (val >> 2) & 1;
  ctx[5] = (val >> 1) & 1;
  ctx[6] = val & 1;
}

void whiteningEncode(const uint8_t* data, int len, uint8_t* ctx, uint8_t* result)
{
  memcpy(result, data, len);
  for (int i = 0; i < len; i++) {
    int varC = ctx[3];
    int var14 = ctx[5];
    int var18 = ctx[6];
    int var10 = ctx[4];
    int var8 = var14 ^ ctx[2];
    int var4 = var10 ^ ctx[1];
    int _var = var18 ^ varC;
    int var0 = _var ^ ctx[0];

    int c = result[i];
    result[i] = ((c & 0x80) ^ ((var8 ^ var18) << 7))
      + ((c & 0x40) ^ (var0 << 6))
      + ((c & 0x20) ^ (var4 << 5))
      + ((c & 0x10) ^ (var8 << 4))
      + ((c & 0x08) ^ (_var << 3))
      + ((c & 0x04) ^ (var10 << 2))
      + ((c & 0x02) ^ (var14 << 1))
      + ((c & 0x01) ^ (var18 << 0));

    ctx[2] = var4;
    ctx[3] = var8;
    ctx[4] = var8 ^ varC;
    ctx[5] = var0 ^ var10;
    ctx[6] = var4 ^ var14;
    ctx[0] = var8 ^ var18;
    ctx[1] = var0;
  }
}

uint8_t reverse_8(uint8_t d)
{
  uint8_t result = 0;
  for (uint8_t k = 0; k < 8; k++) {
    result |= ((d >> k) & 1) << (7 - k);
  }
  return result;
}

uint16_t reverse_16(uint16_t d) {
  uint16_t result = 0;
  for (uint8_t k = 0; k < 16; k++) {
    result |= ((d >> k) & 1) << (15 - k);
  }
  return result;
}

uint16_t crc16(const uint8_t* addr, const uint8_t* data, uint8_t dataLength)
{
  uint16_t crc = 0xffff;

  for (int8_t i = addrLength - 1; i >= 0; i--) 
  {
    crc ^= addr[i] << 8;
    for (uint8_t ii = 0; ii < 4; ii++) {
      uint16_t tmp = crc << 1;

      if ((crc & 0x8000) !=0)
      {
        tmp ^= 0x1021;
      }

      crc = tmp << 1;
      if ((tmp & 0x8000) != 0)
      {
        crc ^= 0x1021;
      }
    }
  }

  for (uint8_t i = 0; i < dataLength; i++) {
    crc ^= reverse_8(data[i]) << 8;
    for (uint8_t ii = 0; ii < 4; ii++) {
      uint16_t tmp = crc << 1;

      if ((crc & 0x8000) != 0) 
      {
        tmp ^= 0x1021;
      }

      crc = tmp << 1;
      if ((tmp & 0x8000) != 0)
      {
        crc ^= 0x1021;
      }
    }
  }
  crc = ~reverse_16(crc) & 0xffff;
  return crc;
}


uint8_t get_rf_payload(const uint8_t* addr, const uint8_t* data, uint8_t dataLength, uint8_t*& rfPayload)
{

  uint8_t data_offset = 0x12;
  uint8_t inverse_offset = 0x0f;
  uint8_t result_data_size = data_offset + addrLength + dataLength+2;
  uint8_t* resultbuf = (uint8_t*)malloc(result_data_size);
  memset(resultbuf, 0, result_data_size);

  resultbuf[0x0f] = 0x71;
  resultbuf[0x10] = 0x0f;
  resultbuf[0x11] = 0x55;

  /*
  console.log("");
  console.log("get_rf_payload");
  console.log("------------------------");
  console.log("addr: " + Buffer.from(addr).toString('hex'));
  console.log("data: " + Buffer.from(data).toString('hex'));

  */
  printf("get_rf_payload\n");
  printf("addr: "); dump(addr, addrLength); printf("\n");
  printf("data: "); dump(data, dataLength); printf("\n");

  for (uint8_t j = 0; j < addrLength; j++) {
    resultbuf[data_offset + addrLength - j - 1] = addr[j];
  }

  for (int j = 0; j < dataLength; j++) {
    resultbuf[data_offset + addrLength + j] = data[j];
  }
  printf("inverse_offset: %d\n", inverse_offset);
  printf("inverse_offset addr.len + 3: %d\n", (inverse_offset + addrLength + 3));
  for (int i = inverse_offset; i < inverse_offset + addrLength + 3; i++) {
    resultbuf[i] = reverse_8(resultbuf[i]);
  }

  int crc = crc16(addr, data, dataLength);
  resultbuf[result_data_size-2] = crc & 0xff;
  resultbuf[result_data_size-1] = (crc >> 8) & 0xff;
  rfPayload = resultbuf;
  return result_data_size;
}

uint8_t do_generate_command(int i, const uint8_t* data, uint8_t length, const uint8_t* key, int forward, int use_default_adapter, int i2, uint8_t*& rfPayload)
{
  if (i2 < 0) i2 = 0;
  uint8_t* payload = 0;
  uint8_t* rfPayloadTmp = 0;
  uint8_t payloadLength = get_payload_with_inner_retry(i, data, length, i2, key, forward, payload);
  uint8_t rfPayloadLength = get_rf_payload(DEFAULT_BLE_FASTCON_ADDRESS, payload, payloadLength, rfPayloadTmp);
  free(payload);

  uint8_t ctx[7];
  whiteningInit(0x25, &ctx[0]);
  uint8_t* result = (uint8_t*)malloc(rfPayloadLength);
  printf("payload raw: "); dump(rfPayloadTmp, rfPayloadLength); printf("\n");
  whiteningEncode(rfPayloadTmp, rfPayloadLength, ctx, result);
  printf("payload cip: "); dump(result, rfPayloadLength); printf("\n");
  rfPayload = (uint8_t*)malloc(rfPayloadLength-15);
  memcpy(rfPayload, result + 15, rfPayloadLength - 15);
  free(result);
  free(rfPayloadTmp);
  return rfPayloadLength-15;
}


void single_control(uint16_t addr, const uint8_t* key, const uint8_t* data, uint8_t dataLength) 
{
  uint8_t* result= (uint8_t*)malloc(dataLength+2);
  result[0] = (uint8_t)(2 | ((sizeof(dataLength)+1) << 4));
  result[1] = addr & 0xff;
  memcpy(result + 2, data, dataLength);

  uint8_t ble_adv_data[] = { 0x02, 0x01, 0x1A, 0x1B, 0xFF, 0xF0, 0xFF };
  uint8_t* rfPayload = 0;
  uint8_t rfPayloadLength = do_generate_command(5, result, dataLength + 2, key, true /* forward ?*/, true /* use_default_adapter*/, 0, rfPayload);
  free(result);

  uint8_t* advPacket = (uint8_t*)malloc(rfPayloadLength + sizeof(ble_adv_data));
  memcpy(advPacket, ble_adv_data, sizeof(ble_adv_data));
  memcpy(advPacket+ sizeof(ble_adv_data), rfPayload, rfPayloadLength);
  free(rfPayload);

  send(advPacket, rfPayloadLength + sizeof(ble_adv_data));
  free(advPacket);
}

void Brightness(uint8_t id, uint8_t on, uint8_t brightness) 
{
  uint8_t command[1];
  command[0] = 0;
  if (on) command[0] = (brightness & 127);
  single_control(id, my_key, command, 1);
}

void send(uint8_t* data, uint8_t dataLength)
{
  printf("Adv-Cmd          : btmgmt -i 1 add-adv -d "); dump(data, dataLength); printf(" 1\n");
  printf("datalen=%d\n", dataLength); 
  
  BLEBeacon oBeacon = BLEBeacon();
  oBeacon.setManufacturerId(0xf0ff); // fake Apple 0x004C LSB (ENDIAN_CHANGE_U16!)
  oBeacon.setProximityUUID(BLEUUID(BEACON_UUID));
  oBeacon.setMajor(0);
  oBeacon.setMinor(0);
  BLEAdvertisementData oAdvertisementData = BLEAdvertisementData();
  BLEAdvertisementData oScanResponseData = BLEAdvertisementData();
  oAdvertisementData.setFlags(0x04); // BR_EDR_NOT_SUPPORTED 0x04
  std::string strServiceData = "";
  strServiceData += (char)(dataLength-4);     // Len  
  for (int i=4;i<dataLength;i++)
  {
    strServiceData += (char)data[i];
  }
  oAdvertisementData.addData(strServiceData);

  pAdvertising->setAdvertisementData(oAdvertisementData);
  pAdvertising->setScanResponseData(oScanResponseData);
  pAdvertising->start();
  delay(50);
  pAdvertising->stop();
}

bool isOn = false;
unsigned long lastMillis = 0;

void loop() 
{
  if (millis()-lastMillis > 500)    // every 500ms
  {
    isOn = !isOn;
    Brightness(lightId, isOn ? 1 : 0, isOn ? 127 : 0);
    lastMillis = millis();
  }
}
1 Like

This code is for brMesh lights only, it would not work with brLight.
App looks exactly the same, but the protocol is different.
brLight uses modified Sig mesh protocol, but requires some propietary stuff for the lights to join the mesh.

When not enrolled, they are detected by Nordic app and also Tuya detects them as BLE Smart Lock devices :slight_smile:

1 Like

Would be great if you could go into these issue - i really would love to control my brlight floodlights with HA.
If any help of an unexperienced user is needed, let me know, i am here :slight_smile:

1 Like