BRmesh app bluetooth lights

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.

No, usually just plug USB again and it should ask you if you want to trust the computer.

“adb devices” should return a list with your tablet.

oh sorry so maybe that is where I am not getting it. I have not used adb before. I was trying to run the adb command on the same android device in a terminal window. So I am supposed to connect android to a separate computer via USB and the run adb command on that other computer?

Yes. And install Android SDK on that computer, so you have “adb” utility.
Easiest is just to install Android Studio and SDK from it’s menu.

ok thanks. Got that part working and I now have the key. I thought I knew how to get this NodeJS script working in HA with MQTT but apparently I do not. I had a MQTT server running already for other purposes so what do I do after I edit that .js file? How do I integrate this into HA? Also you had my_key = four sets of fields AA BB CC DD. I am assuming I break up the key into those four groups and entered it as such into the js?

also for the mqtt server IP… if I am running this on the same host as matt would I just use the localhost ip? like 127.0.0.1?

Then 127.0.0.1 as your MQTT server.
4 bytes of the key. In the Github example:

jyq_helper: getPayloadWithInnerRetry---> payload:220300000000000000000000,  key: b2fd16aa

It means put the key as:

let my_key = [0xb2, 0xfd, 0x16, 0xaa];

Same log entry suggests your device ID of this specific LED is “03”.

After you are done with editing - just run the script with:

node mqtt2brMesh.js

and see what happens, it should be able to connect to your MQTT server.

Then you need to tell HA there is a new device, run in a separate shell session:

mosquitto_pub -h 127.0.0.1 -t 'homeassistant/light/brMesh3/config' -m '{ "name":"brMesh3", "schema":"json", "command_topic":"brMesh/3/set", "rgb":"true", "brightness":"true", "optimistic":"true", "color_
temp":"false","effect":"false",  "color_temp":"true","device":{"identifiers":["brmesh2mqtt_3"], "manufacturer":"MELPO","model":"RGB light"}, "unique_id":"brmesh2mqtt_3"}'

In this case we are sending config packet to MQTT server 127.0.0.1, asvertising brMesh controlled LED light with ID “3”, edit to match your setup. Note that this “3” is repeated 5 times in the command.
Later I would do so that the gateway would publish this on itself.

Now you should be able too see the light in HA and try to control it, when you do go back to the session wiht node/brMesh2Mqtt and see what happens there.

When you would make sure it works - you can autostart the gateway in a screen session, like:

screen -dmS brMesh2MQTT bash -c 'while true; do node /full/path/to/your/brMeshMQTT.js; done'
1 Like

I got the mqtt credentials sorted I think. Where is the new device supposed to show up in HASS? I see the connection in mqtt but I don’t see the device name showing up in HA

Add it manually to any dashboard.
Edit dashboard → Edit card → Start typing device name, eg. brMesh…