Custom firmware ESPHome-Xiaomi_bslamp2

A while ago, I looked at this disassembly video,. The device has its electronics spread out on different boards. One where the power comes in and that seems heavy on the buck converting:

afbeelding

and on a bit deeper down, which has a bit on it that looks promising:

afbeelding

The video is a bit too blurry though to see if that is really an ESP chip.
If its an ESP, then getting ESPHome working on it should not be too difficult. The backside of the second board has a load of debug pads:

afbeelding

The LEDs in the lamp look similar to the ones in the version 2 lamp. There are also 6 leads going to the LED board, so this might be a sign that the light mode control might be reusable.

I don’t own this device myself, so I’m unable to check out the details myself.

You’re right…

Just used this for a test:

void BSLEVELOutput::setup() {
  uint8_t level[] = { 0x02, 0x03, 0x5F, 0xE0, 0x64, 0x00, 0x00 };

  ESP_LOGCONFIG(TAG, "Setting up BSLEVELOutputComponent...");

  write_bytes_raw(level, 6);
}

And the bedside lamp 2 shows the bar lit up half way (o;

Sadly though esphome doesn’t support triggering on an event like on_control or on_brightness to set the level via a lambda call…

Thanks for testing! Really cool to see that it actually works. I hadn’t tried that myself yet.

Useful event handling is where the firmware comes in.
My idea is to add elements like events and services that are needed to control the device in a convenient way. It should for example allow one to either handle button presses locally or to let Home Assistant decide what to do with them. Linking the light level of the slider to changes in the brightness level of the lamp seem like a logical one to make possible (but if one wants to hook that the slider up to the volume of the sound system, that should be possible as well :wink: ).

What I’m currently trying to work out is how to organize my code for the extra components that are coming up next. The lamp is basically a set of components, and my gut feeling says these all would belong to the platform “yeelight_bs2”.
I would like to expose the slider as a monochromatic light component (with only on/off and brightness to control it). However, there is already a “light” component for my yeelight_bs2 platform and I don’t see a straight forward way to add a second “light” to the same platform.

Some routes that I will investigate:

  • not using “yeelight_bs2” as the platform, but introducing a “yeelight_bs2_front_panel” platform or so that can handle the light and touch functions; this way another light component can be introduced.
  • extending the configuration options for the yeelight_bs2.light so that the light component can support both the light outputs, and one can add two yeelight_bs.light components to the device’s yaml file (for example differentiating between the two using a output parameter that can be set to either lamp or panel).
  • not using “light” as the component type for the slider, but for example a custom “output”.
  • checking if adding a new component type (like “control_panel” with platform “yeelight_bs2”) is feasible and within the rules of the ESPHome ecosystem.

The first option keeps things best separated and it matches the way in which components are generally implemented. However, I don’t like that it requires another directory in the custom_components to support the front panel light. Having to add only a single repo to the custom_components is easier for the user IMO.

Tried a simple touch component (o;

[13:48:17][D][bstouch:026]: Interrupt detected
[13:48:17][D][bstouch:029]: Read: 04 03 02 00 1C 00, 1E
[13:48:17][D][bstouch:026]: Interrupt detected
[13:48:17][D][bstouch:029]: Read: 04 04 01 00 03 0B, 0F
[13:48:20][D][bstouch:026]: Interrupt detected
[13:48:20][D][bstouch:029]: Read: 04 04 01 00 04 0B, 10
[13:48:20][D][bstouch:026]: Interrupt detected
[13:48:20][D][bstouch:029]: Read: 04 04 01 00 03 05, 09
[13:48:20][D][bstouch:026]: Interrupt detected
[13:48:20][D][bstouch:029]: Read: 04 04 01 00 03 0E, 12
[13:48:21][D][bstouch:026]: Interrupt detected
[13:48:21][D][bstouch:029]: Read: 04 04 01 00 04 0E, 13

void BSTOUCH::loop() {
//  uint8_t ack[] = { 0x01, 0x00, 0x00, 0x00, 0x00, 0x01 };
  uint8_t data[7];

  if (this->irq_pin_->digital_read())
    return;

  ESP_LOGD(TAG, "Interrupt detected");

  read_bytes_raw(data, 7);
  ESP_LOGD(TAG, "Read: %02X %02X %02X %02X %02X %02X, %02X", data[0], data[1], data[2], data[3], data[4], data[5], data[6]);

  write_bytes_raw(ack, 7);
}

btw…is there already a cure for those reboots? Gettings disconnects every few minutes (o;

Tried a simple touch component

Funny , the original firmware does send the [1,0,0,0,0,1] byte sequence before reading, after the irq pin is pulled low, but looks like it also works without that command.

is there already a cure for those reboots?

I have fixed most of the reboots with a fix in the AsyncTCP library. It’s mentioned in the README.md file in the repo.
With this fix in place, my device is running very solid.

I still do see reboots, but those mainly turn up when I have the esphome log interface active. At that point, I have two connections open to the API server. My esphome log interface gets disconnected a lot and some of those disconnects will result in device reboots.
I opened up an issue on github for the api_server component. The way the component handles cleaning up of client connections is not correct I think, and this might cause various exceptions in the device.

But like I said: with my modified AsyncTCP code and with only Home Assistant connected to it, my device is running rock solid. I added an uptime sensor to monitor this, and the lamp on my desk (semi-production) has been running stable since the last firmware flash, about 19 hours ago.

Hmm…

Switching lamp on/off doesn’t work reliably…
In most cases the poll routine just sees either an on or off event…in rare case both…

Dunno if the esphome 16msecs poll interval is too slow…

BTW…the light can be controlled via ID, gives:

ID 'bedlight' of type yeelight::bs2::YeelightBS2LightState doesn't inherit from light::LightState. Please double check your ID is pointing to the correct value.

Needed to use a lambda function to toggle it…

              auto call = id(bedlight).toggle();
              call.perform();

Switching lamp on/off doesn’t work reliably… In most cases the poll routine just sees either an on or off event…in rare case both…Dunno if the esphome 16msecs poll interval is too slow…

I’m gonna play with the I2C code too now. Using an actual interrupt would be my preferred way of handling the interrupt line, but that’s not ESPHome-y as I understand. I did wonder if that could result in missed signals, like you seem to run into.

Is your code visible somewhere?

yeelight::bs2::YeelightBS2LightState doesn’t inherit from light::LightState

I disagree with that error message:

class YeelightBS2LightState : public light::LightState, public LightStateTransformerInspector {}

It inherits from LightState and another class. I think that might be an issue in ESPHome therefore.

Sadly though esphome doesn’t support triggering on an event like on_control or on_brightness to set the level via a lambda call…

Check out my latest commit, I added an on_brightness trigger for your pleasure :slight_smile: I tested it using the following config extension:

light:
  on_brightness:
    - lambda: |-
        ESP_LOGD("${name}", "Brightness changed to: %f", x);

and I saw this in my logging:

[17:04:37][D][bedside_lamp_office:072]: Brightness changed to: 0.010000
[17:04:37][D][bedside_lamp_office:072]: Brightness changed to: 0.020000
[17:04:37][D][bedside_lamp_office:072]: Brightness changed to: 0.030000
[17:04:37][D][bedside_lamp_office:072]: Brightness changed to: 0.040000
[17:04:37][D][bedside_lamp_office:072]: Brightness changed to: 0.050000
[17:04:37][D][bedside_lamp_office:072]: Brightness changed to: 0.060000
[17:04:37][D][bedside_lamp_office:072]: Brightness changed to: 0.070000
[17:04:37][D][bedside_lamp_office:072]: Brightness changed to: 0.080000
[17:04:37][D][bedside_lamp_office:072]: Brightness changed to: 0.090000
[17:04:37][D][bedside_lamp_office:072]: Brightness changed to: 0.100000
[17:04:37][D][bedside_lamp_office:072]: Brightness changed to: 0.110000
[17:04:37][D][bedside_lamp_office:072]: Brightness changed to: 0.120000
[17:04:37][D][bedside_lamp_office:072]: Brightness changed to: 0.130000
1 Like

Hello Maurice

Kull…will try out on_brightness tomorrow :wink:
Something really missing in esphome…

Not sure if real interrupts would be better…in your logic traces…was GPIO pulled higher after writing the acknowledge sequence?

Maybe the acknowledge write needs to come as the last bit on polling…maybe the whole ESP_LOGD takes up too much time :wink:

Gonna try further tomorrow morning :wink:

I restored my original firmware to test the I2C/INT behavior. When testing some button presses, I see various patterns in the interrupt line handling. Here are some screenshots to illustrate this:

afbeelding

afbeelding

afbeelding

afbeelding

Based on these probes, I think we can strike the theory that the interrupt line is pulled down, until the moment that the ESP sends the [1,0,0,0,0,1] byte sequence. I see the line go up before, during and after the I2C read and write operations. It would have been a lot more useful, had the line stayed low until the ESP acknowledges seeing it, but that’s not the case.

Looking at the timings, I think it is safe to say that the interrupt pin will be pulled low for 6ms or more.
For detecting this signal, a polling frequency of at least 167 Hz would be required.
@davorin mentioned a 16 ms poll interval for ESPHome (that is indeed the default). With the signalling time of only 6 ms, it is very easy to miss signals from the front panel. I think the loop frequency can be increased, but that won’t do any good if there are other parts of the code that blow up the cycle time of the main loop beyond 6 ms. The rule of thumb that I found in the ESPHome code even says that a component should not block the main loop for more than 20 - 30 ms. Well, that might seem okay for cases like handling human button presses, but such delay would detroy the detection of the front panel signalling :frowning:

I found an ESPHome feature request for interrupts, but Otto rejected the request because he did not see the use case for it. I think we have a use case here though. I’ll check if we can’t simply implement an interrupt handler for this specific device, since I don’t really see how to implement the signal detection within the framework’s standard loop.

Anyway, I have added a new document to the reverse engineering repository, in which I wrote up the things that I know so far about the front panel communication.

1 Like

I was able to reproduce the issue locally. I fixed the issue in commit 77, so now you should be able to use the light ID in your automation.

1 Like

Good morning (o;

Just tested polling continuously the i2c bus inside the poll routine…
doesn’t do anything…sometimes I even get a timeout when reading…seems the Kung Fu chip doesn’t like to send any data when no interrupt is active…

No interrupts…too bad…

Hmm…maybe the pulse_counter component might help where a GPIO pin feeds directly a counter?
Then in the poll() routine look if the counter has incremented?

Though that beats currently my knowledge of writing an own component yet (o;

cheers
richard

PS: What nice logic analyzer do you use for the traces?
Me just using a cheapo Digilent Digital Discovery (o;

Ah btw…just received my Xiaomi Mi Ceiling Light ( MJXDD01SYL )…and it uses now an ESP32 as well (o;

Well, there is some interrupt handling after all, buried deep down in the ESPHome code. I wondered how the rotary encoder would work without interrupts and the answer was: it doesn’t :wink: So I’m now working on implementing a binary sensor that makes use of this interrupt support. This might just work :slight_smile:

PS: What nice logic analyzer do you use for the traces?

I use a really basic 8 port Saleae clone from AZ-Delivery.
I use both the Saleae analyer software (Logic 2) and Pulseview.
I prefer Logic 2, because Pulseview often hangs with this analyzer device.

just received my Xiaomi Mi Ceiling Light

Cool! What kind of ESP32 chip did they use? It’s quite unreadable in the photo.

The pulse counter uses interrupts for 8266 only…for esp32 it is the hardware counter…

The ceiling light uses an ESPWROOM32D module…guess also a single core one as the others…
Nice thing is though you can pull out the module and just feed it with 3V3…add it to you Yeelight app and start testing (o;

Doing now the pin mapping…as you can’t beep through the connection from the 0.1" headers to the module…lots of electronics underneath (o;

Also a single core WROOM-32D. Not a big surprise, since they use it in other devices too and it’s not like the thing is underpowered for controlling a lamp :slight_smile:

Okay…think I got the most important GPIOs now…though this is maybe off-topic here (o;

GPIO19: Warm PWM @2kHz
GPIO21: Cold PWM @2kHz
GPIO22: Power supply on/off
GPIO23: Moonlight PWM @20kHz

Okay…some firmware backup first (o;

Update: Running ESPHome now…but also with the single core hack:

esphome:
  name: ceiling_light
  platform: ESP32
  board: esp32doit-devkit-v1
  platformio_options:
    platform: [email protected]
    platform_packages: |-4
          framework-arduinoespressif32 @ https://github.com/pauln/arduino-esp32.git#solo-no-mac-crc/1.0.4

1 Like

It’s hardly a hack :smile:
So this device only does color temperature and this moonlight mode lighting?
This might be a breeze to implement then. The Bedside Lamp 2 has a massively weird maping from color temperature to GPIO outputs, but I’ve got this feeling that with the warm and cold PWM split, the ESPHome light framework provides all the bits that you’d need.

Well the moonlight mode uses the warm LEDs…just at a higher frequency…so don’t see the point in having a separate light output (o;

Well the gamma needs correction…weird is…when decreasing brightness from 20% to 15%, the LEDs are still on…but when increasing from 0% to 15%, they are off…

Yes, one light output makes most sense IMO. That’s why I mixed the Bedside Lamp’s night light mode into the light output when brightness is at its lowest. That makes is really straight forward to use. I presume the moon light mode is kind of the night light mode, and that it could work in the same way.

Often LED light is cut off at a certain point, so you must find out the lowest duty cycle at which the LEDs still light up. Eventually, you want the lowest brightness in Home Assistant to match the lowest brightness of the device.
Maybe do some measurements like I did, where you go through various settings using the original firmware, and use the logic analyzer to find the duty cycles that are used? That might be the easiest way to get a good insight in how the LEDs are controlled.

I don’t know what the different light modes look like for this device, but maybe the 0% - 15% range could be used for the moonlight mode?

Well the original firmware has the LEDs much brighter…even at 1%…changed gamma_corret a little…much better…and good for me (o;

Anyway…back to bedside lamp 2 (o;

Have to do heavy reading over weekend on the pulse counter component and see how I could use it for the touch panel…