Custom firmware ESPHome-Xiaomi_bslamp2

If you can put up the miio_fw* files for download somewhere, I can take a look at them.
I still have a lamp using the original firmware, which I will extract before modding that one, but that one first needs some soldering to get there.

Funny find of the day: while measuring the GPIO outputs for RGB white (R=255, G=255, B=255) over a brightness range of 1 - 255, I noticed something unexpected in the measurements:

There is a weird little bump in the graph!

I measured these BTW, to find an R:G:B ratio to work with for the LEDs.
Below you can see the analysis of this.

I found a 1.0 : 2.10 : 0.78 ratio. It’s not fully static across the whole brightness spectrum as you can see in the graph (especially the green channel does rise significantly over brightnesses 1 - 100), but I will simply assume that keeping it static in my code will work.

I’ve never used this device, have no intention of using it, but wanted to stop by and say - great job!

I love the maker community and what people are able to do when they put their minds to it.

5 Likes

Peculiar findings, mmakaay. One would believe that driving a led was a bit more straightforward. I was wrong when remembering that GPIO4 was linked to the “Xiaomi test function” It is GPIO25. I suppose it is linked to the entry “test” when looking at the original firmware, and I wonder what it does test. If it cycles through the different default values of the lamp it could be easier measuring.

Ah, GPIO 25 is the one that I found to be connected, but which I could not relate it to base functionality of the lamp. It’s pulled up bij default, so maybe pulling it to ground might be the way to trigger the test partition. I will try that.

It wouldn’t be to useful for measuring though. I can easily automate the lamp settings, it’s the reading that I do manually. I did solder some wires to the RGB pins and hooked those up to my logic analyzer. That maken is very easy to get a pwm reading. However, from there on it is still manual work.
I am requesting the urge to build my own logic analyzer :laughing:

Just for fun, a picture of Project Frankenstein :laughing:

1 Like

“Beware; for I am fearless, and therefore powerful.” -Frankenstein- :grin::+1:t2:

1 Like

The firmware is coming along nicely. The main thing I want to fix for RGB, is the balance of the white color in the middle of the RGB color selection. There’s a hint of red in there, which can easily be aproved upon.
I took a quick video to show the responsiveness of this ESPHome-based firmware. I’m very happy with it. The connection stability fix works like a charm and the lamp has been working solid for the last few days.

Upcoming: a bit more of RGB hacking and then I’ll continue with the front touch panel integration.

5 Likes

That’s just marvellous. I’m tipping my hat to your work and dedication, mmakaay! I had a quick look at the Yeelight forum here, and there are quite a few irritated people over there. Yeelight just came up with a version 43 - special edition also. Supposedly gonna fix LAN control option from resetting…? There is something smelling a bit off of the whole thing :thinking:
When you bring this together I would not be surprised if you’ll be one of the most popular people around. :grin:

2 Likes

I will bring this together for sure, being popular is just a nice side-effect :smile:

I just finished tracking and fixing a bug with transitioning the light from ON to OFF. When turning the light on, I could get a nice transitioning from of to the required color+brightness. However, when turning the light off, it simply stayed on during the transitioning period, to eventually turn off abruptly. Very annoying, because it mainly feels as an unresponsive lamp.

I found that this was not an issue with my code, but something in the ESPHome light component code.
I filed a pull request and hope that it will be accepted:

Note: if this code is not accepted or if accepting it takes too long, then I have a work-around available that archieves the same thing from within my code.

Update:

In my codebase, I implemented a work-around for the reported issue, which automatically triggers as long as the big is not fixed in the upstream code. This means that the RGB transitions now all look good (between colors, brightnesses and ON/OFF switching) and will remain to do so when this bug gets fixed.

Here’s a new video of the lamp, which shows the new transitioning in action:

Note that these transitions are optional. If you prefer to have no transitions (like shown in my previous video a few posts up), you can configure this by setting the transition time to 0s.

1 Like

I moved the repository code to a proper ESPHome component format. This makes installation a lot easier and cleaner. It also allows me to make the configuration a clean yaml config, instead of having to inject objects using an ugly lambda function. Here’s what the component configuration now looks like:

light:
  - platform: yeelight_bs2
    name: ${friendly_name} Custom Light
    red: led_r
    green: led_g
    blue: led_b
    white: led_w
    master1: master1
    master2: master2
    default_transition_length: ${transition_length}
    effects:
      - random:
          name: "Slow Random"
          transition_length: 30s
          update_interval: 30s
      - random:
          name: "Fast Random"
          transition_length: 3s
          update_interval: 4s

Full details are in the updated repo.

As you can see, I still need component definitions for the various GPIOs. I want to modify the component to make these bulit-in defaults (I presume these pinouts do not differ between devices) to make the configuration even cleaner.

I’m following this thread with great interest (and by the way, phenomenal effort @mmakaay and others!). I have the Bedside Lamp D2, the smaller version, and I’m hoping some of this great hacking can be used there as well. Interesting to see that it is an ESP32 - I’m using an external ESP32 (with ESPHome) for BLE presence detection in my bedroom. Would be nice to have an all-in-one instead :slight_smile:

1 Like

Yeah, for my own lamp I am also opting for adding one or two microwave presence detection sensors under the hood. I just have to see if they don’t show too much when the light is on.

I don’t have a D2, but it’d be interesting so see what the differences would be. I would not be surprised if at least the light settings were the same and therefore reusable.

As for my progress: really happy now about my RGB implementation. Transitions go smooth and the white mix is more towards white than on the original lamp firmware.
Going back to the white light implementation now. Transitions don’t look good there, so I’ve got some work to do there.

Fun fact: While testing white light transitions, I could get the original firmware to crash the network connection quickly, making the device unavailable in Home Assistant. Doing the same on the new firmware is rock solid. Looking forward to having a stable device!

3 Likes

I did new detailed measurements for the white light mode, now using digital PWM measurements as suggested earlier in this thread by @thatthing (so thanks for encouraging that).

PWM duty cycles

I found that the RGB GPIO’s are all driven using a 3 kHz duty cycle, while the extra GPIO that is used in the white light mode is driven using a 10 kHz duty cycle. So there’s already something to work with in the white light mode code.

GPIO duty cycle levels for various color settings

I just finished measuring all the possible temperature options for 100% and 1% brightness, and I found that the “stepping” behavior that I saw earlier during my voltage-based measurements is even more prevalent than I figured before. In the graphs from below, you can see that I colored pairs of measurements yellow or orange. All measurements within such colored range of white light temperatures yield the exact same output values.

Test the duty cycle values between 1% and 100%

While the two graphs from above show the required outputs at 1% and 100%, I was not sure yet whether for in-between brightness values, the GPIO level would scale linearly with the brightness. To check this, I did some measurements:

afbeelding

afbeelding

afbeelding

Good news in this department. It looks like we can safely come up with GPIO outputs for any brightness level, by determining the levels for brightness 1% and those for brightness 100%, and interpolating to the required level based on the actual brightness.

Test white colors transitioning behavior

I also checked the behavior of the GPIO pins when transitioning between color temperatures, since transitioning does not look good in my current code base. I did a transitioning from warm (588) to cold (153) light and one from a bright white in the middle (300) to cold (153) light.

I checked these two specifically, because my light does show the bright white in the middle while going from warm to cold, whereas the original device does not do that, making it a smoother ride.

These are the results:

afbeelding

afbeelding

What I found is that all parameters (RGB+W) are modified in a linear way between the starting point and end point. So that is a nice find. It means that I can work with the start and end values as determined by the logic from above, and that the actual transitioning is straight forward. I think I might have to override some of the base functionality of the ESPHome light code to handle this, but this isn’t going to be rocket science.

Let’s get cracking on that code

I think I learned enough to redo the white light handling. Oh joy!! :smiley:

Update:

I’ve translated the measurements into a class that impements the white light color space
Setting the various temperatures and brightnesses look good on the device.
Tomorrow, I will look at the transitions within this white light color space, because the default transitioning does not cut it.
After that, there’s another fun one to tackle: transitioning between white and RGB light.

Hoping to get a touch of C help. I’m a JS/Python/SQL developer so C feels like an odd and strange place to me.

What’s the best approach to check a given byte array against another? I’m able to successfully decode the i2c responses from the lamp when the int pin is pulled high. But I don’t know enough C to approach the problem in a sane way. Below is my custom library that I’ve developed to interface with the touch panel. Happy for someone to build a POC and I can finish it off. I managed to brick my lamp and my baby lost my solder reel so I’ve been waiting for parts to do a hardware flash again.

Thanks,

#include "esphome.h"

volatile byte readyCmd[7]={0x01,0x00,0x00,0x00,0x00,0x00,0x01};
volatile byte power_touch[3]={0x01,0x01,0x03};
volatile byte power_release[3]={0x01,0x03,0x04};

//char myString[] = "This is the first line"
String touch_nibble = "";

class YeeFrontPanel : public PollingComponent, public Sensor {
public:
  // constructor
  YeeFrontPanel() : PollingComponent(10) { }

  float get_setup_priority() const override { return esphome::setup_priority::BUS; }

  void setup() override {
    // This will be called by App.setup()
    pinMode(16, INPUT);
    Wire.begin(21,19); 
  }

  void update() override {
    // This will be called every "update_interval" milliseconds.
    if (digitalRead(16)){
      return;
    };    
    ESP_LOGD("yee_touch_panel", "irq pin high");
    Wire.beginTransmission(0x2C);
    for( int i = 0; i<sizeof(readyCmd); i++){
      Wire.write(readyCmd[i]);
      }
    Wire.endTransmission();
    Wire.requestFrom(0x2C,7);
    
    
    while(Wire.available())    // slave may send less than requested
    { 
      char c = Wire.read();    // receive a byte as character
      touch_nibble += String(c);
      //Serial.print(c);         // print the character
      ESP_LOGD("yee_touch_panel", "return_string: %d", c);
    }
    // char touch_read[] = Wire.read();
    // char touch_nibble[] = "";
    // for(int i = 0; i<sizeof(touch_read); i++){
    //   touch_nibble.concat(String(touch_read[i]));
    // }
    return;
  }
};

A few markers, @thatthing

  • Just a little thing, but the interrupt pin is pulled low, not high, when the panel has an event available.

  • For talking to the I2C bus, the cleanest implementation is to follow the ESPHome-way of doing this. This means that you write a component that dervies from the esphome::i2c::I2CDevice class (see the code on github]. It works with the I2C bus(es) as defined in the device yaml configuration file. This parent class provides some useful functions to work with the I2C bus, and it has a built-in option for handling communication timeouts.

  • I was wondering if the PollingComponent was the correct way to handle the interrupt pin state detection. Turns out it is, and that setting up interrupt handlers is not the way to go with ESPHome. So that was a good learning point for me.

  • About comparing arrays: since we’re looking at C++ and not plain C, I would opt for using the std::array class (some docs on this one). You can use the == equality operator for two std::array objects to test if they match. Another way would of course be to use C-style arrays as you did, and write a little loop that compares the bytes one at a time yourself.

  • For parsing the bytes as received from the front panel, I would probably not setup a mapping of all possible messages, but I would write a parser class that would implement more of a decision tree to find the message type. The structure would be a starter method that would look at the first byte, to call a method for processing the next byte or bail out with an error if the message cannot be parsed. Every followup function would do the same, until eventually a conclusion is reached about the resulting message code. There are other ways to implement such parser, but the important concept here would be the use of a decision tree instead of a lookup map.Using a tree takes the least steps to get from an input to a resulting message code. I made a drawing to show the paths for this:

As you can see, I suggested a computation based on the last two bytes for the slider’s touch and release events. There is a distinct pattern there, which could be used to translate the two bytes into the correct slider value. Forking the decision tree further would be feasible too of course. It might even be easier to detect the invalid cases that way.

Of course, in such tree-based parser, you don’t need four functions in succession to handle the first four bytes. The first function can check for the first four bytes to be 04, 04, 01, 00, and then call the next function or bail out based on the fifth byte. In pseudo code:

enum EventType {
    UnknownMessage,
    PowerTouch,
    PowerRelease,
    ColorTouch,
    ColorRelease,
    Level1Touch,
    Level1Release,
    // etc....
};

EventType parse(FrontPanelMessage message) {
    if (message[0] != 0x04) return UnknownMessage;
    if (message[1] != 0x04) return UnknownMessage;
    if (message[2] != 0x01) return UnknownMessage;
    if (message[3] != 0.00) return UnknownMessage;
    if (message[4] == 0x01) return parse_power_button_event(message);
    if (message[4] == 0x02) return parse_color_button_event(message);
    if (message[4] == 0x03) return parse_slider_touch_event(message);
    if (message[4] == 0x04) return parse_slider_release_event(message);
    return UnknownMessage;
}

EventType parse_power_button_event(FrontPanelMessage message) {
    if (message[5] == 0x01) return parse_power_button_touch_event(message);
    if (message[5] == 0x02) return parse_power_button_release_event(message);
    return UnknownMessage;
}

// etc....

As for storing the resulting event: for the slider events, it would be nice to have an actual event object, in which the slider level is stored as an integer value. That makes it easier to work with the resulting event (it prevents the need of a long switch statement to translate the event type to a slider level). Another way to handle this would be to provide a translation function from your code, that takes an event as input (e.g. Level12Touch) and would return its numerical value (12).

This was a bit of a brain dump, but I hope these pointers will help!

1 Like

In crypto we do a constant time array compare as follows:

XOR the arrays with eachother into a third array
AND all the bytes in the third array into a single byte
If that byte is 0, the 2 initial arrays were identical

I know the type of implementation, and I think you meant to OR the third array bytes:

Array 1 : [ 11110000, 10101010 ]
Array 2 : [ 11110000, 10101011 ]
XOR:      [ 00000000, 00000001 ] --> AND = 00000000, OR = 00000001

The two arrays are different, but using AND, the result would be 0, incorrectly indicating identical arrays.
Normally, you’d use a runtime OR-counter while looping over the two array, so you don’t have to generate an actual third array.

For the use case of @thatthing , this way of work is causing unneeded overhead, since the code would always loop over all the bytes in the arrays to come to a conclusion. Perfect for crypto purposes, since you don’t want timing leaks there (hence the constant time requirement).

For simply comparing two arrays using the same kind of XOR approch, I’d do something more efficient like this:

bool are_equal(byte* a, byte* b, size_t n)
{
    for (size_t pos = 0; p < n; p++)
        if (a[pos] ^ b[pos])
            return false;
    return true;
}

yes… or… it appears I am too sick right now to make code suggestions

1 Like

I, like others in this thread, don’t own any Yeelight products but had to say you are doing an absolutely fantastic job of reverse engineering this device @mmakaay! I found this thread after we were alerted on Twitter that Yeelight was silently removing LAN control by request of Xiaomi and wanted to see if any intrepid hackers on our forums were aware and looking into the problem. You have gone above and beyond to say the least. I will hold this thread up in the future as an excellent example of taking back control of our devices with brute force :laughing:! Please keep up the good work!!

4 Likes