After creating the custom firmware for the Xiaomi Bedside Lamp 2, I did get quite some questions about a firmware for the Bedside Lamp D2. The D2 looks a bit like the bslamp2, but the lamp is lower, uses different controls and does not have LEDs in the controls.
The D2 is also known as “bslamp3” (as I found in the original device firmware serial output) and “YLCT01YL”.
Based on reports of people that already tried flashing the device with the bslamp2 firmware, it looks like the LED light circuitry matches that of the bslamp2, because they could control the light successfully from Home Assistant. Given that driving those LEDs was the biggest challenge while developing the bslamp2 firmware, this is very good news.
I did buy a D2 for development purposes (thanks to those who chipped in!) and have started the reverse engineering on this one.
First view after opening the device
What we see here is that there are a lot of similarities between the bslamp2 and the D2. The LED circuitry and connector to the LEDs themselves look very very much like what we’ve seen on bslamp2. The placing of the debug pads on the main board suggest that RX/TX/GND/GPIO0 might even be in the same spots. The big difference is that the connection to the controls unit is a 10 lines flat cable in this model.
On the left, the controls board contains the USB-C connector for powering the device, and there are two strips with capacitive sensor pads lined against the inside of the lamp’s bottom part. These are the pads that pick up a finger sliding along the lamp’s bottom sides for changing light color and intensity. The left strip also contains the power button capacitive sensor, at the front of the device.
Controls board
By removing two screws and opening up the two flat cable clips on the controls board, I could free the controls board from the unit.
Flipping it over, we get a good view at its control chip.
Not a big surprise there. This is the same chip as used in the bslamp2 controls board: Kungfu KF8TS2716. This chip handles the capacitive touch events and communicates those to the main board.
Flat cable between controls board and main board
By probing the flat cable, I found the following pin assignment:
- pin 1 : chip pin 28 (P0.1)
- pin 2 : chip pin 23 (P3.3)
- pin 3 : chip pin 24 (P3.2)
- pin 4 : chip pin 22 (P3.4)
- pin 5, 6, 7 : all connected to GND of the USB-C connector
- pin 8, 9, 10 : all connected to +5V of USB-C connector
I was happy to find that there weren’t actually 10 distinct lines at play here
Signalling between controls board and main board
The next step was of course to solder some leads to the debug pads, to make it possible to run them through a logic analyser. After doing so, I put the device on the pain bench to see what we’re working with here.
I was happy to see a familiar signal pattern, I2C! Based on the signals, I could derive the following pin assignments:
- pin 1 : Not used for signalling
- pin 2 : I2C SDA
- pin 3 : I2C SCL
- pin 4 : Changes state every time a new touch event is available
Note that pin 4 is not for example pulled low when a new event is available and then returns to high, but instead it toggles once for every new event.
Behaviour when touching multiple capacitive pads at the same time
From the signalling behaviour I could derive:
- that we can consider the controls to be segregated into three groups: left slider, power, right slider;
- that there will always be only one pad active per group. When touching a second pad while holding the first pad, no event is triggered until the first pad is released;
- that when going from one pad to the other within a group, there’s always a release event before the next press event is sent;
- that at most two touches can be active at the same time between the three groups. For example when touching the left slider and then the power button, both events will register. When also touching the right slider, no event is triggered. This goes for all possible combinations of the three groups.
I2C messages
Decoding the captured messages, I get the following information:
INITIALISATION:
After turning on the device, these messages are exchanged. I’m not sure if the pauses in there really matter, but I recorded them here in case they do.
read to 0x50 ack data: 0x07 0x00 0x00 0x00 0x00 0x00
--- 24ms pause
write to 0x27 ack data: 0x01
read to 0x27 ack data: 0x13
--- 3 ms pause
write to 0x27 ack data: 0x0F 0x00
write to 0x27 ack data: 0x11 0x1E
write to 0x27 ack data: 0x10 0x07
write to 0x27 ack data: 0x7F 0x83
write to 0x27 ack data: 0x7F 0x69
write to 0x27 ack data: 0x7F 0xBD
LEFT SLIDER, BACK (1) TO FRONT (7)
read to 0x50 ack data: 0x07 0x00 0x01 0x01 0x00 0x00 1 press
read to 0x50 ack data: 0x07 0x00 0x03 0x01 0x00 0x00 1 release
read to 0x50 ack data: 0x07 0x00 0x01 0x02 0x00 0x00
read to 0x50 ack data: 0x07 0x00 0x03 0x02 0x00 0x00
read to 0x50 ack data: 0x07 0x00 0x01 0x03 0x00 0x00
read to 0x50 ack data: 0x07 0x00 0x03 0x03 0x00 0x00
read to 0x50 ack data: 0x07 0x00 0x01 0x04 0x00 0x00
read to 0x50 ack data: 0x07 0x00 0x03 0x04 0x00 0x00
read to 0x50 ack data: 0x07 0x00 0x01 0x05 0x00 0x00
read to 0x50 ack data: 0x07 0x00 0x03 0x05 0x00 0x00
read to 0x50 ack data: 0x07 0x00 0x01 0x06 0x00 0x00
read to 0x50 ack data: 0x07 0x00 0x03 0x06 0x00 0x00
read to 0x50 ack data: 0x07 0x00 0x01 0x07 0x00 0x00 7 press
read to 0x50 ack data: 0x07 0x00 0x03 0x07 0x00 0x00 7 release
RIGHT SLIDER, BACK (1) TO FRONT (7)
read to 0x50 ack data: 0x07 0x00 0x00 0x00 0x01 0x01 1 press
read to 0x50 ack data: 0x07 0x00 0x00 0x00 0x03 0x01 1 release
read to 0x50 ack data: 0x07 0x00 0x00 0x00 0x01 0x02
read to 0x50 ack data: 0x07 0x00 0x00 0x00 0x03 0x02
read to 0x50 ack data: 0x07 0x00 0x00 0x00 0x01 0x03
read to 0x50 ack data: 0x07 0x00 0x00 0x00 0x03 0x03
read to 0x50 ack data: 0x07 0x00 0x00 0x00 0x01 0x04
read to 0x50 ack data: 0x07 0x00 0x00 0x00 0x03 0x04
read to 0x50 ack data: 0x07 0x00 0x00 0x00 0x01 0x05
read to 0x50 ack data: 0x07 0x00 0x00 0x00 0x03 0x05
read to 0x50 ack data: 0x07 0x00 0x00 0x00 0x01 0x06
read to 0x50 ack data: 0x07 0x00 0x00 0x00 0x03 0x06
read to 0x50 ack data: 0x07 0x00 0x00 0x00 0x01 0x07 7 press
read to 0x50 ack data: 0x07 0x00 0x00 0x00 0x03 0x07 7 release
POWER BUTTON
read to 0x50 ack data: 0x07 0x01 0x00 0x00 0x00 0x00 press
read to 0x50 ack data: 0x07 0x03 0x00 0x00 0x00 0x00 release
Knock sensor
The lamp supports turning it on by knocking twice on it (can be enabled via the accompanying Xiaomi app). Experimentation shows that the sensor that detects the knocking is probably some IMU, because serial output of the device shows messages about x,y,z. I’m not sure yet what exact sensor this is.
The sensor that is used, is connected to I2C and it uses channel 0x27. This means that the controls board is only on channel 0x50 (before I found out about the knock sensor, I assumed 0x27 was also used for the controls board).
When knock detection is enabled, I see a lot of traffic on the I2C bus. The MCU is reading from the knock sensor about every 20ms. Some captures of this traffic:
# Enable the sensor.
write to 0x27 ack data: 0x16 0x87
write to 0x27 ack data: 0x27 0x00
write to 0x27 ack data: 0x28 0x28
write to 0x27 ack data: 0x19 0x04
# Read the sensor.
write to 0x27 ack data: 0x02
read to 0x27 ack data: 0xA1 0x01 0xD0 0x02 0x14 0x50
--- pause 19ms
write to 0x27 ack data: 0x02
read to 0x27 ack data: 0x79 0x01 0x0C 0x03 0x3C 0x50
--- pause 19ms
write to 0x27 ack data: 0x02
read to 0x27 ack data: 0x25 0x02 0xA8 0x02 0x82 0x50
--- pause 19ms
write to 0x27 ack data: 0x02
read to 0x27 ack data: 0x83 0x01 0x0C 0x03 0xDC 0x4F
# Disable the sensor
write to 0x27 ack data: 0x16 0x00
write to 0x27 ack data: 0x19 0x00
The data that are read, are sets of 6 values as you can see above. When I assume that these values are to be interpreted individually, I can convert them to integers and graph them out. This results in the following picture for some data where I did a double knock:
The left three graphs are values 1, 3 and 5.
The middle three graphs (which look way more interesting) are values 2, 4 and 6.
On the right, there is a cumulative graph for values 2, 4 and 6. This might point at useful method to detect the knocking, since there are two peaks in that graph. I will have to generate some more data to be sure though.
The above measurements were done with the top of the lamp lying up-side-down. When I put the top straight up, the measurements became really messy. But after resetting the device, they were clean again. So my assumption based on this, is that the init sequence that I’ve seen on I2C is for letting the sensor calibrate to the current orientation. With the freshly booted device, the measurements looked like this in graphs:
It’s clearly visible that the values from these colums (2, 4 and 6) have a starting point from which the impact is measured. So detecting impact based on these data looks like I’ll have to detect the equilibrium value and compute the offset from that for the three measurements. I bet if I sum those, that I get a really clear “there was impact!” picture.
BTW: the graph was created by doing the cartoon character detection knock (dah ta ta daaaah dah… tam tam)
ESP32 pinout
After tracing the lines 1 - 4 of the flat cable, I found their connection to the ESP32 is as follows:
- pin 1 : Looks like it is used to feed 3V3 to the ESP32 (directly connected to the 3V3 input)
- pin 2 : SDA connected to GPIO 21
- pin 3 : SCL connected to GPIO 19
- pin 4 : INT connected to GPIO 16
The interrupt and I2C pins are the same as they are used on the bslamp2 board.
For the LED circuitry, I will assume for now that the pins used are the same as for the bslamp2. I’ve had reports of the LEDs more or less working when trying the bslamp2 firmware, so I’ll give that a go and see what needs fixing from there.
Debug pads, used for flashing
The debug pads have the same orientation as the bslamp2. Going from left to right, looking from the side of the board, the debug pads are: GND, TX, RX, GPIO0 and a small GND pad next to GPIO0.
Output from the original firmware serial logs
After soldering some leads to the debug pads and starting the device with the original firmware, the following interesting bits could be seen:
The lamp is sold as “Bedside Lamp D2”, but the internal name apparently is “bslamp3”, which is the folowup of “bslamp2”. So I’ve got to think about how to name the firmware module. Probably bslampd2, since that’s how the thing is known.
device model:yeelink.light.bslamp3
firmware version : 1.2.8_0020
Some GPIO information is logged.
(591) gpio: GPIO[26]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0
(591) gpio: GPIO[26]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0
(601) gpio: GPIO[33]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 <-- main switch in bslamp2
(611) gpio: GPIO[4]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 <-- main switch in bslamp2
(631) gpio: GPIO[16]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:3 <-- interrupt
(641) gpio: GPIO[23]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:1 <-- maybe knock sensor?
GPIO’s 5,12,13 and 14 are used in the bslamp2 for driving the LEDs. Those aren’t mentioned here. So it will be interesting to see how that goes.
This version of the lamp uses the same ESP32 single core chip as the bslamp2, including the efuse MAC address checksum issue that we’ve seen on bslamp2. This means that the work-around for this issue needs to be applied here too.
(931) system_api: Base MAC address is not set, read default base MAC address from BLK0 of EFUSE
(941) system_api: Base MAC address from BLK0 of EFUSE CRC error, efuse_crc = 0xc5; calc_crc = 0x4d
Of course the lamp is doing the phone home thing.
08:00:04.990 [I] ots:host resolving de.ots.io.mi.com...
08:00:04.990 [I] ots:resolved to 3.126.247.75.
08:00:05.000 [I] tls: connect to server de.ots.io.mi.com ,port is 443.
When changing colors, the firmware does log some pwm numbers, which is quite interesting. It looks like this lamp does use a cleaner scheme for driving the various colors. When changing between red, green and blue light, the reported pwm numbers are quite promising. Possibly, we don’t need the whole translation table stuff from the bslamp2 implementation to get correct colors here. I will have to play a bit more with this, but it looks good.
When varying between the lowest and highest light levels, the values go from respectively 51 to 1023.
08:00:00.330 [I] main, new pwm: [1023],[0],[0],[0] <-- red
13:24:15.110 [I] main, new pwm: [0],[1023],[0],[0] <-- green
13:24:16.420 [I] main, new pwm: [0],[0],[1023],[0] <-- blue
Note: unfortunately, the device firmware on the lamp got upgraded and the latest firmware does not log the pwm values If anybody can get me a dump of the firmware that does log the pwm values (v1.something, not the current v2.something), it might be useful for finding out how to drive the LEDs. Otherwise, I might have to buy a new lamp and make sure it’s not upgraded.
When double knocking on the device (after enabling knock gesture support via the app), the lamp state is toggled. Logging during this operation:
Knock Start
Knock done! 28 73 61
14:05:06.460 [I] knock done, static xyz:-85 -325 -700
Knock Start
Knock done! 39 27 92
Double knock!
And now the rest
This concludes my investigation so far. I think at this point there is enough information to start coding on the ESPHome firmware for the D2.