Add version 0.1

This commit is contained in:
Erik Borowski
2024-11-29 23:51:53 +01:00
parent e6dcaac5ab
commit 0071af40a6
10 changed files with 960 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
config.h
.DS_Store
.vscode

24
Lightbar.ino Normal file
View File

@ -0,0 +1,24 @@
#include "config.h"
#include "lightbar.h"
#include "mqtt.h"
Lightbar lightbar(INCOMING_SERIAL, OUTGOING_SERIAL, RADIO_PIN_CE, RADIO_PIN_CSN);
MQTT mqtt(&lightbar, WIFI_SSID, WIFI_PASSWORD, MQTT_SERVER, MQTT_PORT, MQTT_USER, MQTT_PASSWORD, MQTT_ROOT_TOPIC, HOME_ASSISTANT_DISCOVERY, HOME_ASSISTANT_DISCOVERY_PREFIX, HOME_ASSISTANT_DEVICE_NAME);
void setup()
{
Serial.begin(115200);
Serial.println("##########################################");
Serial.println("# LIGHTBAR2MQTT (Version 0.1) #");
Serial.println("# https://github.com/ebinf/lightbar2mqtt #");
Serial.println("##########################################");
lightbar.setup();
mqtt.setup();
}
void loop()
{
mqtt.loop();
lightbar.loop();
}

138
README.md Normal file
View File

@ -0,0 +1,138 @@
# lightbar2mqtt
Control your [Xiaomi Mi Computer Monitor Light Bar](https://www.mi.com/global/product/mi-computer-monitor-light-bar/) with MQTT and add it to Home Assistant! All you need is a ESP32, a nRF24 module and a light bar of course.
## Acknowledgements
This project is heavily based on the amazing reverse engineering work done by [lamperez](https://github.com/lamperez). Take a look at their [repository](https://github.com/lamperez/xiaomi-lightbar-nrf24) for more information on the protocol used, the reverse engineering process and a Python version running on Raspberry Pi. It is licensed under the GNU General Public License v3.0.
I also took some inspiration from [Ben Allen](https://github.com/benallen-dev) and their [repository](https://github.com/benallen-dev/xiaomi-lightbar), licensed under the MIT license.
This project would not have been possible without the work of these amazing people! Thank you!
## Features
- Control power state, brightness and color temperature via simple MQTT messages
- Receive state updates of the remote via MQTT as well
- Either use the controller aditionally or decouple light bar and remote to gain full control
- Integrates with Home Assistant either manually or with zero effort using the automatic discovery feature
- The light bar is represented as a `light` entity
- The remote is represented as a `sensor` entity
- Actions taken on the remote also trigger `device_automation`s. This allows you to trigger automations in Home Assistant based on actions taken on the remote
## Requirements
- a Xiaomi Mi Computer Monitor Light Bar, Model MJGJD**01**YL (without BLE/WiFi). The MJGJD**02**YL will not work!
- an ESP32
- a nRF24 module (tested with nRF24L01)
## Installation
### 1. Hardware
Connect the nRF24 module to the ESP32 as follows. At least these pins work for my combination of ESP32 and nRF24 module. You can change the CE & CSN pins in the `config.h` file if you need to. The SPI pins (SCK, MOSI, MISO) might be different on your ESP32 check the pinout of your ESP32!
| nRF24 | ESP32 |
| :---- | -----: |
| VCC | 3V3 |
| GND | GND |
| CE | Pin 4 |
| CSN | Pin 5 |
| SCK | Pin 18 |
| MOSI | Pin 23 |
| MISO | Pin 19 |
### 2. Software
1. Clone this repository
2. Copy the `config-example.h` file to `config.h` and adjust the settings to your needs.
3. Connect your ESP32 to your computer.
4. Open the Arduino IDE and install the required libraries:
- [Arduino_JSON](https://github.com/arduino-libraries/Arduino_JSON) by Arduino, _Version 0.2.0_
- [CRC](https://github.com/RobTillaart/CRC) by Rob Tillaart, _Version 1.0.3_
- [PubSubClient](https://pubsubclient.knolleary.net/) by Nick O'Leary, _Version 2.8_
- [RF24](https://nrf24.github.io/RF24/) by TMRh20, _Version 1.4.10_
5. Select your serial port and board. Upload the sketch to your ESP32.
6. Inspect the serial monitor (115200 baud). If everything is set up correctly, the ESP32 should connect to your WiFi and MQTT broker.
7. At this point, you probably don't know the serial of your remote. Just press/turn the remote and you should see the serial in the serial monitor. (E.g. `[Radio] Ignoring package with not matching serial: 0x7B7E12`) Copy it and paste it into the `config.h` file.
8. Upload the sketch again.
9. If your Home Assistant has the MQTT integration set up, the light bar should be discovered automatically.
10. Enjoy controlling your light bar via MQTT!
### 3. Pairing Light bar and ESP32 (optional)
If you opt to use different values for `INCOMING_SERIAL` and `OUTGOING_SERIAL` in the `config.h` file, you need to pair the light bar with the ESP32. To do this, power-cycle the light bar and within 10 seconds either press the "Pair" button in Home Assistant or send a message to the pair topic (see below). The light bar should blink a few times if the pairing was successful.
## Usage
### MQTT Topics
All MQTT topics are prefixed with a root topic. You can set this root topic in the `config.h` file. The default root topic is `lightbar2mqtt`. The root topic is followed by the MAC address of your ESP32 in the format `l2m_<MAC of your ESP32>`, e.g. `l2m_1234567890AB`. Just take a look at the serial monitor of your ESP32 to find out the used root topic and MAC address, it will be printed right after the startup message:
```text
...
[MQTT] Device ID: l2m_1234567890AB
[MQTT] Root Topic: lightbar2mqtt/l2m_12345679890AB
...
```
#### Light Bar
To control the light bar, you can send messages to the following topic: `<MQTT_ROOT_TOPIC>/l2m_<MAC of your ESP32>/lightbar/command` e.g. `lightbar2mqtt/l2m_1234567890AB/lightbar/command`. The payload should be a JSON object with the following keys:
- `state`: `"ON"` or `"OFF"`
- `brightness`: `0` (off) to `15` (full brightness)
- `color_temp`: Mireds `153` (cold) to `370` (warm)
Example:
```json
{
"state": "ON",
"brightness": 15,
"color_temp": 153
}
```
#### Remote
The remote sends its state to the following topic: `<MQTT_ROOT_TOPIC>/l2m_<MAC of your ESP32>/remote/state` e.g. `lightbar2mqtt/l2m_1234567890AB/remote/state`. The payload is a plain string with one the following values:
- `press`
- `turn_clockwise`
- `turn_counterclockwise`
- `hold`
- `press_turn_clockwise`
- `press_turn_counterclockwise`
#### Pairing
To pair the light bar with the ESP32, send a message to the following topic: `<MQTT_ROOT_TOPIC>/l2m_<MAC of your ESP32>/pair` e.g. `lightbar2mqtt/l2m_1234567890AB/pair`. The payload can be anything, it will be ignored.
Please note that the light bar needs to be power-cycled within 10 seconds _before_ sending the pairing message.
#### Availability
The ESP32 sends its availability to the following topic: `<MQTT_ROOT_TOPIC>/l2m_<MAC of your ESP32>/availability` e.g. `lightbar2mqtt/l2m_1234567890AB/availability`. The payload is either `online` or `offline`.
### Home Assistant
If your Home Assistant has the MQTT integration set up, the light bar should be discovered automatically.
Additionally, the above mentioned events from the remote are also available as `device_automation` triggers. You can use these triggers to create automations in Home Assistant based on the actions taken on the remote. To do so, create a new automation in Home Assistant and select "Device" as the trigger type. Select the light bar entity and the desired trigger, e.g. `"press" action`.
### Known Issues / Limitations
- The light bar does not send its state to the ESP32. This means that if you change the state of the light bar via the controller, the ESP32 will not know about it. This is a limitation of the protocol used by the light bar.
- There is no way of knowing whether the light bar is currently on or off. Therefore this project assumes that the light bar is on when the ESP32 starts. If you turn off the light bar (e.g. via the remote), this might lead to an inverted state in Home Assistant. Just turn the device on in Home Assistant and power-cycle the light bar to fix this.
- Sometimes actions taken on the remote are not recognized by the ESP32. When building automations in Home Assistant, don't rely on the remote events to be 100% accurate. Normally, the second or third try should work.
## Contributing
If you find a bug or have an idea for a new feature, feel free to open an issue or create a pull request. I'm happy to see this project grow and improve!
I've designed the code to be easily™ extendable. For example, it should be relatively easy to add support for multiple light bars or multiple remotes.
## License
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.

58
config-example.h Normal file
View File

@ -0,0 +1,58 @@
/* -- nRF24 --------------------------------------------------------------------------------------------------- */
// The pin number to which the nRF24's Chip Enable (CE) pin is connected.
#define RADIO_PIN_CE 4
// The pin number to which the nRF24's Chip Select Null (CSN) pin is connected.
#define RADIO_PIN_CSN 5
/* -- Light Bar ----------------------------------------------------------------------------------------------- */
// The serial of the remote to listen to.
// If you don't know the serial of your remote, just set this to any value and flash your controller. Once
// the controller is running, the serial of your remote will be printed to the console.
#define INCOMING_SERIAL 0x123456
// The serial, the controller should emulate.
// If set to the same value as INCOMING_SERIAL, the original remote will still control the ligtbar directly.
// To separate the light bar from the original remote, set this to a different value, e.g. 0xABCDEF.
#define OUTGOING_SERIAL 0xABCDEF
/* -- WiFi ---------------------------------------------------------------------------------------------------- */
// The SSID of the WiFi network to connect to.
#define WIFI_SSID "<Your WiFi>"
// The password of the WiFi network to connect to.
#define WIFI_PASSWORD "<Your Password>"
/* -- MQTT ---------------------------------------------------------------------------------------------------- */
// The IP address of the MQTT broker to connect to.
#define MQTT_SERVER "192.168.1.1"
// The port of the MQTT broker to connect to.
#define MQTT_PORT 1883
// The user to use to connect to the MQTT broker.
#define MQTT_USER "<Your User>"
// The password to use to connect to the MQTT broker.
#define MQTT_PASSWORD "<Your Password>"
// The root topic used for communicating with this controller.
// Normally, you don't need to change this. Only if you want to archive a specific structure in your MQTT broker.
// This has nothing to do with Home Assistant's discovery feature.
// Each controller will use its own unique subtopic consisting of the controllers MAC address. Therefore, you can
// have multiple controllers in your network without any conflicts.
#define MQTT_ROOT_TOPIC "lightbar2mqtt"
/* -- Home Assistant Device Discovery ------------------------------------------------------------------------- */
// Whether to send Home Assistant discovery messages.
#define HOME_ASSISTANT_DISCOVERY true
// The prefix to use for Home Assistant discovery messages.
// This must match the prefix used in your Home Assistant configuration. The default is "homeassistant".
#define HOME_ASSISTANT_DISCOVERY_PREFIX "homeassistant"
// The name of the device to use in Home Assistant.
// This is the name that will be displayed in the Home Assistant UI. Of course, you can change this in the UI
// later on. But if you want to have a specific name from the beginning or make it easier to identify the device,
// you can set it here.
#define HOME_ASSISTANT_DEVICE_NAME "Mi Computer Monitor Light Bar"

106
lightbar.cpp Normal file
View File

@ -0,0 +1,106 @@
#include "lightbar.h"
Lightbar::Lightbar(uint32_t incoming_serial, uint32_t outgoing_serial, uint8_t ce, uint8_t csn)
{
this->radio = new Radio(ce, csn);
this->radio->setOutgoingSerial(outgoing_serial);
this->incoming_serial = incoming_serial;
}
Lightbar::~Lightbar()
{
delete this->radio;
}
void Lightbar::setup()
{
this->radio->setup();
}
void Lightbar::sendRawCommand(Command command, byte options)
{
this->radio->sendCommand(command, options);
}
void Lightbar::sendRawCommand(Command command)
{
this->radio->sendCommand(command);
}
void Lightbar::onOff()
{
this->sendRawCommand(Lightbar::Command::ON_OFF);
onState = !onState;
}
void Lightbar::setOnOff(bool on)
{
if (onState != on)
this->onOff();
}
void Lightbar::brighter()
{
this->sendRawCommand(Lightbar::Command::BRIGHTER);
}
void Lightbar::dimmer()
{
this->sendRawCommand(Lightbar::Command::DIMMER);
}
void Lightbar::warmer()
{
this->sendRawCommand(Lightbar::Command::WARMER);
}
void Lightbar::cooler()
{
this->sendRawCommand(Lightbar::Command::COOLER);
}
void Lightbar::reset()
{
this->sendRawCommand(Lightbar::Command::RESET);
}
void Lightbar::pair()
{
this->sendRawCommand(Lightbar::Command::RESET);
}
void Lightbar::setTemperature(uint8_t value)
{
// Send max value first, then set to the desired value. See
// https://github.com/lamperez/xiaomi-lightbar-nrf24?tab=readme-ov-file#command-codes
// for details.
this->sendRawCommand(Lightbar::Command::COOLER, 0x0 - 16);
this->sendRawCommand(Lightbar::Command::WARMER, (byte)value);
}
void Lightbar::setMiredTemperature(uint mireds)
{
mireds = max(mireds, (uint)153);
mireds = min(mireds, (uint)370);
float amount = ((1 - ((mireds - 153) * 1.0 / (370 - 153) * 1.0)) * 15) + 0.5;
this->setTemperature((uint8_t)amount);
}
void Lightbar::setBrightness(uint8_t value)
{
// Send max value first, then set to the desired value. See
// https://github.com/lamperez/xiaomi-lightbar-nrf24?tab=readme-ov-file#command-codes
// for details.
this->sendRawCommand(Lightbar::Command::DIMMER, 0x0 - 16);
this->sendRawCommand(Lightbar::Command::BRIGHTER, (byte)value);
}
void Lightbar::loop()
{
this->radio->loop();
}
bool Lightbar::registerCommandListener(std::function<void(uint32_t, byte, byte)> callback)
{
return this->radio->addIncomingSerial(this->incoming_serial, callback);
}

49
lightbar.h Normal file
View File

@ -0,0 +1,49 @@
#ifndef LIGHTBAR_H
#define LIGHTBAR_H
#include "radio.h";
class Lightbar
{
public:
Lightbar(uint32_t incoming_serial, uint32_t outgoing_serial, uint8_t ce, uint8_t csn);
~Lightbar();
void setup();
enum Command
{
ON_OFF = 0x01,
COOLER = 0x02,
WARMER = 0x03,
BRIGHTER = 0x04,
DIMMER = 0x05,
RESET = 0x06
};
void sendRawCommand(Command command, byte options);
void sendRawCommand(Command command);
void onOff();
void brighter();
void dimmer();
void warmer();
void cooler();
void reset();
void pair();
void setOnOff(bool on);
void setTemperature(uint8_t value);
void setMiredTemperature(uint mireds);
void setBrightness(uint8_t value);
void loop();
bool registerCommandListener(std::function<void(uint32_t, byte, byte)> callback);
static void callback(uint32_t serial, byte command, byte options);
private:
Radio *radio;
bool onState = false;
uint32_t incoming_serial;
};
#endif

297
mqtt.cpp Normal file
View File

@ -0,0 +1,297 @@
#include "mqtt.h"
#include <Arduino_JSON.h>
#include <esp_mac.h>
MQTT::MQTT(Lightbar *lightbar, const char *wifiSsid, const char *wifiPassword, const char *mqttServer, int mqttPort, const char *mqttUser, const char *mqttPassword, const char *mqttRootTopic, bool homeAssistantAutoDiscovery, const char *homeAssistantAutoDiscoveryPrefix, const char *homeAssistantDeviceName)
{
this->wifiSsid = wifiSsid;
this->wifiPassword = wifiPassword;
this->mqttServer = mqttServer;
this->mqttPort = mqttPort;
this->mqttUser = mqttUser;
this->mqttPassword = mqttPassword;
this->mqttRootTopic = String(mqttRootTopic);
this->homeAssistantDiscovery = homeAssistantAutoDiscovery;
this->homeAssistantDiscoveryPrefix = String(homeAssistantAutoDiscoveryPrefix);
this->homeAssistantDeviceName = String(homeAssistantDeviceName);
this->wifiClient = new WiFiClient();
this->client = new PubSubClient(*this->wifiClient);
this->lightbar = lightbar;
this->lightbar->registerCommandListener(std::bind(&MQTT::sendAction, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
String mac = "";
unsigned char mac_base[6] = {0};
if (esp_efuse_mac_get_default(mac_base) == ESP_OK)
{
char buffer[13]; // 6*2 characters for hex + 5 characters for colons + 1 character for null terminator
sprintf(buffer, "%02X%02X%02X%02X%02X%02X", mac_base[0], mac_base[1], mac_base[2], mac_base[3], mac_base[4], mac_base[5]);
mac = buffer;
}
this->clientId = "l2m_" + mac;
}
MQTT::~MQTT()
{
delete this->client;
delete this->wifiClient;
}
void MQTT::onMessage(char *topic, byte *payload, unsigned int length)
{
Serial.print("[MQTT] New Message (");
Serial.print(topic);
Serial.print("): ");
for (int i = 0; i < length; i++)
{
Serial.print((char)payload[i]);
}
Serial.println();
if (!strcmp(topic, String(this->mqttRootTopic + "/" + this->clientId + "/pair").c_str()))
{
this->lightbar->pair();
return;
}
JSONVar command = JSON.parse(String(payload, length));
if (JSON.typeof(command) != "object")
return;
if (command.hasOwnProperty("state"))
{
const char *state = command["state"];
this->lightbar->setOnOff(strcmp(state, "ON"));
}
if (command.hasOwnProperty("brightness"))
{
this->lightbar->setBrightness((uint8_t)command["brightness"]);
}
if (command.hasOwnProperty("color_temp"))
{
this->lightbar->setMiredTemperature((uint)command["color_temp"]);
}
}
void MQTT::setupWifi()
{
Serial.print("[WiFi] Connecting to network \"");
Serial.print(this->wifiSsid);
Serial.print("\"...");
WiFi.begin(this->wifiSsid, this->wifiPassword);
WiFi.setHostname(this->clientId.c_str());
uint retries = 0;
while (WiFi.status() != WL_CONNECTED)
{
delay(1000);
Serial.print(".");
retries++;
if (retries > 60)
ESP.restart();
}
Serial.println();
Serial.println("[WiFi] connected!");
Serial.print("[WiFi] IP address: ");
Serial.println(WiFi.localIP());
}
void MQTT::setupMqtt()
{
this->client->setServer(this->mqttServer, this->mqttPort);
this->client->setCallback(std::bind(&MQTT::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
while (!this->client->connected())
{
Serial.println("[MQTT] Connecting to MQTT broker...");
uint retries = 0;
if (this->client->connect(this->clientId.c_str(), String(this->mqttRootTopic + "/" + this->clientId + "/availability").c_str(), 1, true, "offline"))
{
Serial.println("[MQTT] connected!");
this->client->publish(String(this->mqttRootTopic + "/" + this->clientId + "/availability").c_str(), "online", true);
this->client->subscribe(String(this->mqttRootTopic + "/" + this->clientId + "/lightbar/command").c_str());
this->client->subscribe(String(this->mqttRootTopic + "/" + this->clientId + "/pair").c_str());
}
else
{
Serial.print("[MQTT] Connection failed! rc=");
Serial.print(this->client->state());
Serial.println(" try again in 1 second");
while (WiFi.status() != WL_CONNECTED)
{
this->setupWifi();
}
delay(1000);
retries++;
if (retries > 60)
ESP.restart();
}
}
if (homeAssistantDiscovery)
this->sendHomeAssistantDiscoveryMessages();
}
void MQTT::setup()
{
Serial.print("[MQTT] Device ID: ");
Serial.println(this->clientId);
Serial.print("[MQTT] Root Topic: ");
Serial.println(this->mqttRootTopic + "/" + this->clientId);
this->setupWifi();
this->setupMqtt();
}
void MQTT::sendHomeAssistantDiscoveryMessages()
{
const String baseConfig = R"json(
"schema": "json",
"o": {
"name": "lightbar2mqtt",
"sw_version": "0.1",
"support_url": "https://github.com/ebinf/lightbar2mqtt"
},
"~": ")json" + this->mqttRootTopic +
"/" +
this->clientId +
R"json(",
"availability_topic": "~/availability",
"dev": {
"ids": ")json" + this->clientId +
R"json(",
"name": ")json" + homeAssistantDeviceName +
R"json(",
"mdl": "MJGJD01YL",
"mf": "Xiaomi"
},)json";
String rendevous_str = "{" +
baseConfig +
R"json(
"supported_color_modes": [
"color_temp"
],
"brightness": true,
"brightness_scale": 15,
"name": "Light bar",
"cmd_t": "~/lightbar/command",
"uniq_id": ")json" + this->clientId +
R"json(_lightbar",
"max_mireds": 370,
"min_mireds":153,
"p": "light"
)json" + "}";
this->client->beginPublish(String(homeAssistantDiscoveryPrefix + "/light/" + this->clientId + "/lightbar/config").c_str(), rendevous_str.length(), true);
this->client->print(rendevous_str);
this->client->endPublish();
rendevous_str = "{" +
baseConfig +
R"json(
"name": "Remote",
"state_topic": "~/remote/state",
"uniq_id": ")json" +
this->clientId + R"json(_remote",
"value_template": "{{ value }}",
"p": "sensor"
)json" + "}";
this->client->beginPublish(String(homeAssistantDiscoveryPrefix + "/sensor/" + this->clientId + "/remote/config").c_str(), rendevous_str.length(), true);
this->client->print(rendevous_str);
this->client->endPublish();
rendevous_str = "{" +
baseConfig +
R"json(
"name": "Pair",
"cmd_t": "~/pair",
"uniq_id": ")json" +
this->clientId + R"json(_pair",
"p": "button"
)json" + "}";
this->client->beginPublish(String(homeAssistantDiscoveryPrefix + "/button/" + this->clientId + "/pair/config").c_str(), rendevous_str.length(), true);
this->client->print(rendevous_str);
this->client->endPublish();
const char *commands[] = {
"press",
"turn_clockwise",
"turn_counterclockwise",
"press_turn_clockwise",
"press_turn_counterclockwise",
"hold"};
String cmd;
for (int i = 0; i < 6; i++)
{
cmd = commands[i];
rendevous_str = "{" +
baseConfig +
R"json(
"automation_type": "trigger",
"payload": ")json" + cmd +
R"json(",
"subtype": ")json" + cmd +
R"json(",
"type": "action",
"topic": "~/remote/state",
"p": "device_automation"
)json" + "}";
this->client->beginPublish(String(homeAssistantDiscoveryPrefix + "/device_automation/" + this->clientId + "/action_" + cmd + "/config").c_str(), rendevous_str.length(), true);
this->client->print(rendevous_str);
this->client->endPublish();
}
}
void MQTT::loop()
{
if (!this->client->connected())
{
Serial.println("[MQTT] connection lost!");
this->setupMqtt();
}
this->client->loop();
}
void MQTT::sendAction(uint32_t serial, byte command, byte options)
{
const char *action;
switch ((uint8_t)command)
{
case Lightbar::Command::ON_OFF:
action = "press";
break;
case Lightbar::Command::BRIGHTER:
action = "turn_clockwise";
break;
case Lightbar::Command::DIMMER:
action = "turn_counterclockwise";
break;
case Lightbar::Command::WARMER:
action = "press_turn_counterclockwise";
break;
case Lightbar::Command::COOLER:
action = "press_turn_clockwise";
break;
case Lightbar::Command::RESET:
action = "hold";
break;
default:
return;
}
this->client->publish(String(this->mqttRootTopic + "/" + this->clientId + "/remote/state").c_str(), action);
}

39
mqtt.h Normal file
View File

@ -0,0 +1,39 @@
#include <PubSubClient.h>
#include <WiFi.h>
#include "lightbar.h"
#ifndef MQTT_H
#define MQTT_H
class MQTT
{
public:
MQTT(Lightbar *lightbar, const char *wifiSsid, const char *wifiPassword, const char *mqttServer, int mqttPort, const char *mqttUser, const char *mqttPassword, const char *mqttRootTopic, bool homeAssistantAutoDiscovery, const char *homeAssistantAutoDiscoveryPrefix, const char *homeAssistantDeviceName);
~MQTT();
void setup();
void loop();
void onMessage(char *topic, byte *payload, unsigned int length);
void sendAction(uint32_t serial, byte command, byte options);
private:
WiFiClient *wifiClient;
PubSubClient *client;
String clientId;
Lightbar *lightbar;
const char *wifiSsid;
const char *wifiPassword;
const char *mqttServer;
int mqttPort = 1883;
const char *mqttUser = "";
const char *mqttPassword = "";
String mqttRootTopic = "lightbar2mqtt";
bool homeAssistantDiscovery = true;
String homeAssistantDiscoveryPrefix = "homeassistant";
String homeAssistantDeviceName = "Mi Computer Monitor Light Bar";
void setupWifi();
void setupMqtt();
void sendHomeAssistantDiscoveryMessages();
};
#endif

201
radio.cpp Normal file
View File

@ -0,0 +1,201 @@
#include "radio.h"
/*
* Package structure:
* 0 7: Preamble (see constant above)
* 8 10: Remote ID
* 11 11: Separator (0xFF)
* 12 12: Sequence counter
* 13 14: Command ID + options
* 15 16: CRC16 checksum
*/
Radio::Radio(uint8_t ce, uint8_t csn)
{
this->radio = RF24(ce, csn);
}
Radio::~Radio()
{
this->radio.stopListening();
this->radio.powerDown();
}
void Radio::setOutgoingSerial(uint32_t serial)
{
this->outgoing_serial = serial;
}
bool Radio::addIncomingSerial(uint32_t serial, std::function<void(uint32_t, byte, byte)> callback)
{
if (this->num_remotes >= MAX_REMOTES)
return false;
this->remotes[this->num_remotes].serial = serial;
this->remotes[this->num_remotes].last_received_package_id = 0;
this->remotes[this->num_remotes].callback = callback;
this->num_remotes++;
}
bool Radio::removeIncomingSerial(uint32_t serial)
{
for (int i = 0; i < this->num_remotes; i++)
{
if (this->remotes[i].serial == serial)
{
for (int j = i; j < this->num_remotes - 1; j++)
{
this->remotes[j] = this->remotes[j + 1];
}
this->num_remotes--;
return true;
}
}
return false;
}
void Radio::sendCommand(byte command, byte options)
{
byte data[17] = {0};
memcpy(data, Radio::preamble, sizeof(Radio::preamble));
data[8] = (this->outgoing_serial & 0xFF0000) >> 16;
data[9] = (this->outgoing_serial & 0x00FF00) >> 8;
data[10] = this->outgoing_serial & 0x0000FF;
data[11] = 0xFF;
data[12] = this->last_sent_package_id;
data[13] = command;
data[14] = options;
this->crc.restart();
this->crc.add(data, sizeof(data) - 2);
uint16_t checksum = this->crc.calc();
data[15] = (checksum & 0xFF00) >> 8;
data[16] = checksum & 0x00FF;
this->last_sent_package_id += 1;
Serial.print("[Radio] Sending command: 0x");
for (int i = 0; i < 17; i++)
{
Serial.print(data[i], HEX);
}
Serial.println();
this->radio.stopListening();
for (int i = 0; i < 20; i++)
{
this->radio.write(&data, sizeof(data), true);
delay(10);
}
this->radio.startListening();
}
void Radio::sendCommand(byte command)
{
return this->sendCommand(command, 0x0);
}
void Radio::setup()
{
uint retries = 0;
while (!this->radio.begin())
{
Serial.println("[Radio] nRF24 not responding! Is it wired correctly?");
delay(1000);
retries++;
if (retries > 60)
ESP.restart();
}
Serial.println("[Radio] Setting up radio...");
this->radio.failureDetected = false;
this->radio.openReadingPipe(0, Radio::address);
this->radio.setChannel(68);
this->radio.setDataRate(RF24_2MBPS);
this->radio.disableCRC();
this->radio.disableDynamicPayloads();
this->radio.setPayloadSize(17);
this->radio.setAutoAck(false);
this->radio.setRetries(15, 15);
this->radio.openWritingPipe(Radio::address);
this->radio.startListening();
Serial.println("[Radio] done!");
}
void Radio::loop()
{
if (this->radio.failureDetected)
{
Serial.println("[Radio] Failure detected!");
delay(1000);
this->setup();
delay(1000);
}
// Only continue if there is a package available.
if (!this->radio.available())
return;
// Read raw data, append a 5 and shift it. See
// https://github.com/lamperez/xiaomi-lightbar-nrf24?tab=readme-ov-file#baseband-packet-format
// on why that is necessary.
byte raw_data[18] = {0};
this->radio.read(&raw_data, sizeof(raw_data));
byte data[17] = {0x5};
for (int i = 0; i < 17; i++)
{
if (i == 0)
data[i] = 0x50 | raw_data[i] >> 5;
else
data[i] = ((raw_data[i - 1] >> 1) & 0x0F) << 4 | ((raw_data[i - 1] & 0x01) << 3) | raw_data[i] >> 5;
}
// Check if preamble matches. Ignore package otherwise.
if (memcmp(data, Radio::preamble, sizeof(Radio::preamble)))
return;
// Make sure the checksum of the package is correct.
this->crc.restart();
this->crc.add(data, sizeof(data) - 2);
uint16_t calculated_checksum = this->crc.calc();
uint16_t package_checksum = data[15] << 8 | data[16];
if (calculated_checksum != package_checksum)
{
Serial.println("[Radio] Ignoring pacakge with wrong checksum!");
return;
}
// Check if package is coming from a observed remote.
bool found = false;
uint32_t serial = data[8] << 16 | data[9] << 8 | data[10];
for (int i = 0; i < this->num_remotes; i++)
{
Remote remote = this->remotes[i];
if (serial != remote.serial)
continue;
found = true;
// Make sure the same package was not handled before.
uint8_t package_id = data[12];
if (package_id <= remote.last_received_package_id && package_id > remote.last_received_package_id - 64)
{
Serial.println("[Radio] Ignoring package with too low package number!");
continue;
}
remote.last_received_package_id = package_id;
Serial.println("[Radio] Package received!");
remote.callback(remote.serial, data[13], data[14]);
}
if (!found)
{
Serial.print("[Radio] Ignoring package with not matching serial: 0x");
Serial.print(serial, HEX);
Serial.println("");
}
}

45
radio.h Normal file
View File

@ -0,0 +1,45 @@
#ifndef RADIO_H
#define RADIO_H
#include <RF24.h>
#include <CRC16.h>
#define MAX_REMOTES 10
struct Remote
{
uint32_t serial;
uint8_t last_received_package_id;
std::function<void(uint32_t, byte command, byte options)> callback;
};
class Radio
{
public:
Radio(uint8_t ce, uint8_t csn);
~Radio();
void setup();
void sendCommand(byte command, byte options);
void sendCommand(byte command);
void loop();
void setOutgoingSerial(uint32_t serial);
bool addIncomingSerial(uint32_t serial, std::function<void(uint32_t, byte, byte)> callback);
bool removeIncomingSerial(uint32_t serial);
private:
RF24 radio;
uint8_t last_sent_package_id = 0;
uint32_t outgoing_serial;
Remote remotes[MAX_REMOTES];
uint8_t num_remotes = 0;
static const uint64_t address = 0xAAAAAAAAAAAA;
static constexpr byte preamble[8] = {0x53, 0x39, 0x14, 0xDD, 0x1C, 0x49, 0x34, 0x12};
// For details on how these parameters were chosen, see
// https://github.com/lamperez/xiaomi-lightbar-nrf24?tab=readme-ov-file#crc-checksum
CRC16 crc = CRC16(0x1021, 0xfffe, 0x0000, false, false);
};
#endif