BRmesh app bluetooth lights

@slashgob Yes these bulbs would work with the hub I mentioned above and although you would need to rely on a cloud provider at the moment the integration is possible. Hopefully at some point I can spend some time figuring out a local integration of the hub or reverse engineering of the protocol

I emailed Broadlink support to ask about any open API information they may be able to share and got the response…

“Hi Customer,
There are also don’t have a open API, thanks!”

@Tdubs3 I have the hub, smart switches and bulbs all working great with the Broadlink app and Google, no problems there. I would just love to be able to address them from within HA.

If there is anything I can do to help, please let me know.

@slashgob Thanks for the support, if I can think of anything I’ll let you know.

I had a quick chance the other night to try and get Xiaomi paired with the GW4C and while I could get it to connect with the china server it does not add the devices to your account but let’s you control them using AI voice commands which is not helpful. Hopefully I can get some more technical assistance on GitHub to try and port the protocol to Bluetooth

hey guys… not sure if this the right thread or not but it is the best match I could find. I have a couple LED floodlights that connect through the BRmesh app (so communicating via bluetooth). Do you know any way to integrate those type of devices to HA? I was thinking there should be a way to connect via bluetooth directly to the devices but can seem to sort out how…

I have spent my evening on analyzing the things, and here are some points:

  1. BRmesh is basically a facelifted version of Broadlink BLE Light App, original app works too!
    BroadLink BLE App
  2. Apps really use BT Mesh to talk with the lights, so iOS bluetooth tracing would not work, requires h/w analyzer, if anyone wants to go this path No, it is a fake “mesh”.
  3. As a possible approach to understanding the protocol one can try reverse engineering these 2 Android apps (think JADX, produces nice, clear code)

I guess some kind of SDK exists, as the package inside both apps is called “cn.com.broadlink.blelight”

TLDR: Integration is very doable, app contains all the required structures and code, easy reversable. Someone, who is good in bluez mesh coding should take a look.

Android log of BRmesh adding new light:

jyq_helper: getPayloadWithInnerRetry---> payload:000000000000000000000000,  key: 
jyq_helper: getPayloadWithInnerRetry---> mSendCnt:13,  sSendSeq:61,  seq:61
jyq_jni : redmi--000
jyq_jni : redmi--111
jyq_jni : redmi--222
jyq_jni : package forward_flag: 0 
jyq_jni : package header: 003dff3c 
jyq_jni : package payload: 000000000000000000000000 
jyq_jni : redmi--333
jyq_jni : redmi--444
BluetoothAdapter: STATE_ON
BluetoothLeAdvertiser: Stop AdvertingSet
jyq_helper: onLeScan: null - 62
BtGatt.GattService: [GSIM LOG]: gsimLogHandler, msg: MESSAGE_ADV_SET_STOP, appName: com.brgd.brblmesh, id: 0, isLegacy: true
jyq_helper: send---> payload:5e0b84f85e367bc45e367bc45e367bc4, calculatedPayload:6db64368931d0d385c42c63e5f0f65ca0a67aa63130b271e, len:24
BtGatt.GattService: onScanResult to scannerId: 10- eventType=0x10, addressType=1, address=112233_6, primaryPhy=1, secondaryPhy=0, advertisingSid=0xff, txPower=127, rssi=-79, periodicAdvInt=0x0
jyq_helper: onLeScan: [null - 11:22:33:44:55:66] old protocol: len: 16, data: 4e6c7a5cec0bf1198888a1a85e367bc4
jyq_add_dev: init: [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254]
jyq_helper: onLeScan: null - 62
jyq_add_dev: poll: [2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254]
jyq_helper: getPayloadWithInnerRetry---> payload:ec0bf1198888010137343034,  key: 5e367bc4
jyq_helper: getPayloadWithInnerRetry---> mSendCnt:10,  sSendSeq:58,  seq:58
jyq_helper: onLeScan: [null - 11:22:33:44:55:66] old protocol: len: 16, data: 6e6d7a934335303033013a3b37343034
jyq_jni : BLE_FASTCON_CONTROL_HEADER_TYPE_HEARTBEAT: short_add(1), group_add(0), version(4.4.0.3850.53)
jyq_helper: onHeartBeat: 1, 0, 4.4.0.3850.53
jyq_helper: onHeartBeat.mOnHearBeatCallback: com.brgd.brblmesh.Main.Activity.ScanDeviceActivity

Someone even done native code analysis:

And Rust implementation:

1 Like

Fastcon BLE is not compatible with mentioned BT mesh implementations.
BLE scanner app won’t even see my lights, as they do not broadcast any services.

Broadlink Fastcon is something completly different and it uses “advertisment” frames for comms, no fancy UUID based stuff at all. I guess it is not a “bluetooth mesh” in classic understanding (and I have no idea if devices retransmit frames)

As we have (presumably) working Rust code, can re-implement it with something else, may be MQTT connectable.

ADDED:

It seems a quick dirty hack might be possible:

  1. Use Android phone to setup your “mesh”
  2. Use adb logcat to see which key was generated for it (key btw is just 4 bytes, not very secure imho)
  3. Use Moody’s code (or rewrite it) to build BLE advertisment packets
  4. Send them using system(), invoking btmgmt add-adv -d hexdatahexdata...... 1

I would try that on the weekend.

I can confirm that sending commands to the light using btmgmt works.
I was able to replay a packet, changing LED’s color.

1 Like

Well, it works.

HA video: brMesh HomeAssistant integration PoC - YouTube

Proof of concept code: GitHub - ArcadeMachinist/brMeshMQTT: MQTT to brMesh (Broadlink Fastcon) gateway

I hope someone adapts it to a pretty HomeAssistant plugin, that would use DBus to communicate with the BT adapter. Or use ESPhome’s ability to broadcast advertisment frames. For now I’m using shell to invoke btmgmt, which is a very dirty solution.

Time permitting, may be I would do it myself. Thinking about something like Zigbee2MQTT, as things like mesh config, device grouping, etc would require it’s own web interface, unreasonable to do it inside HA instance.

As a note: there are LEDs that come with brLight app, while the App UI looks exactly the same as brMesh (may be designed by the same team) - the underlying BT comms are not compatible. Lights that come with brLight app really DO advertise as full fledge BT Mesh devices, but they are not fastcon and as such are not handled by this gateway.

the code below is a working example for the ESP32 compiled with Arduino IDE. It turns one light on/off once a second.Tested with this light → Amazon.de
The code 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();
  }
}
2 Likes

Nice. I would make a dedicated ESP32/MQTT node for sending these broadcasts.
Now I run my Python code on the same machine with HA.
brMesh is advertished as mesh, but in fact it is not. So if HA host with BT stich is too far away - it won’t work.
Placing dedicated ESP32 sender near the far away lights would solve the problem.

Is that really necessary? Couldn’t it be in-cooperated to a existing esphome node? :thinking:

People do lots of advanced bluetooth stuff in esphome, for example :point_down:

This looks very promising indeed. Will that implementation work with any of the lights that are using the BRmesh app or is it specific to the brand you tried it with? I have a set of these lights and am hoping that there is a path to get them in HA eventually… https://www.amazon.ca/dp/B01N6PKKBP?psc=1&ref=ppx_yo2ov_dt_b_product_details

Any lights, that are supported by brMesh.
In fact I have just installed 2 of the lights, you are mentioning above, today.
(amazon.com/dp/B09QHQBZC9)
Works perfectly.

That’s great to hear. Are there instructions on how to install this on HA? I am running it on a Debian box right now.

See README in the GitHub above.
Choose NodeJS version, if you are not comfortable reocmpiling BlueZ from source.

hey there… so your instructions looks like they are using Android phone to figure out what the key is. Is there a way to do that using the iPhone app (or a different way by using the Debian box directly?)

Not a way that I know of.
Also iOS app does not expose the key in logs.
But you can borrow Android from someone then, do “Share home” from your iPhone via QR code to Android and then get the same key, so you won’t have to re-learn all the lights again.

The whole thing can be theoretically done from the gateway, but I do not have time to code it right now.

Ok so I have an android tablet. I installed BRmesh on there and can control the light. I installed adb on it but when I run that command I get waiting for device but nothing happens. I switch to BRmesh and do some different commands etc but when I go back to the terminal window it is still just sitting there on waiting for device. I must be missing something… is there something else I need to do?

You need to enable “Developer options” on your Android tablet and then enable “USB debug”.

  1. Go to “Settings”
  2. Tap “About device” or “About phone”
  3. Tap “Software information”
  4. Tap “Build number” seven times.

You would now have one extra menu item called “Developer options”, make sure USB debug is enabled.

Thanks. I did go through that previously and turned on USB debug. Do I need to reboot the tablet after turning on USB debug? It didn’t seem to make any difference for me.