Reverse Engineering Blisslights 2.0 Bluetooth Star Projector?

Hi all. Sorry in advance for the huge post.

I recently got one of those Blisslights Star Projectors (the 2.0 version with Bluetooth) and I’m trying to reverse engineer the commands that their official app sends to the device so I can hopefully integrate it into Home Assistant somehow… As of right now the device does not remember its last state while on, so my only current way to integrate it into HA (a smart plug) allows essentially only one light mode, the default when it turns on.

I followed this wonderful guide to get myself started trying to figure out what the commands being sent from the Blisslights app to the projector are, but I think I may have exhausted my abilities at this point.

Following the guide, I found the device on the nRF app, found 4 characteristics used to communicate with the device:

  1. 1911 Notifications
  2. 1912 Commands
  3. 1913 OTA (Assuming over the air update stuff)
  4. 1914 Pair

I sniffed some bluetooth packets using my Android phone while sending some commands through the app, and the order of notable events essentially was:

  1. Phone sends write request 0cc4c38bb5649aae797759a5daece26f02 (this changes every time, and there’s zero pattern for each command) through 1914, to pair the device I’m assuming.
  2. Projector sends reply (WireShark doesn’t say what it is)
  3. Phone sends a read request through 1914
  4. Projector sends back 0da3fe0961edf42718cc2967034afe3748 (again, no pattern I can see between different times)
  5. Phone sends 01 request to projector through 1911
  6. Projector sends back c8400d0000863cc4199b7cbc12bf635628b96926
  7. Phone sends command c7c5207b64ca9fe414e210c249a8da606a863ef5 to change lights to stars only through 1912

Looking through more logs, it’s clear that the app sometimes sends the 01 write request through 1911 prior to sending a command through 1912, but also seems to have the capability to send multiple commands through 1912 after one another.

Here is a list of commands that I sent (alternating between Nebula only and Stars only):

Value: c7c5207b64ca9fe414e210c249a8da606a863ef5
Value: c8c5204961239e7c67c70045b40acc799edb6625
Value: c9c52013faf771b29ef4a9353f950820b32acd07
Value: cac52026b5d07933491a23bd6b701de747c84f48
Value: cbc5208c56ab84173ac338d4a85e7be6b1f5a46a
Value: ccc520ed45b4a35ed54a7b37586d83805009f975
Value: cdc520f6ea193cb83cd19e5674cd2e0dc4a726e4
Value: cec52068def24e42073b55cbda8670d0fc600eae
Value: cfc5209d4305920e223c6b706816d2791a7d5270
Value: d0c5203ec99c4ef2f921413774bff5bccc905c45
Value: d1c5203d12cc74466fcc5de3ae23b66f3622b390
Value: d2c520dd124ab3885620bfe4d5e4b341eaf8f249
Value: d3c52094d43160b6c61f57f10ec5920f471edba0
Value: d4c52011765f7a7b727c068046d7a2dbc27e2947
Value: d5c520ff5aeb8fa08f78cf22cc4e77729e9dbe77
Value: d6c5205edc055bbb3fbad78c3b12e9a26767c6e1

Value: 87159869260c326c66a8073abb8e75a1d403a21f
Value: 881598b6a79412cc660045fa9f676921e99a5572
Value: 891598419aacaba126ccc59c3f943afb7f6a17e9
Value: 8a1598b9b62f598d0e6869a740c2b74494aa9e37
Value: 8b1598e15e347c2d1ad77cca5609eddaf394cdcb
Value: 8c15981f22dde086ea2bd4c666b6f9d860531c96
Value: 8d1598c1460c1ae3423d84c135df7962c7765965

Unfortunately, these commands seem to have no pattern whatsoever, which makes it rather difficult to reverse engineer them. I can tell the first two digits are incrementing each command, and the next 4 digits after that always stay the same (unless I log out and in, then they change). Other than that, zero patterns I can see.

A couple of other notes:

  • I’ve tried simply writing all those commands into 1912 to no avail
  • I’ve tried recreating every step I laid out above to no avail
  • When I send 01 to 1911 through the nRF app, I get a response only if I’ve already “paired” the device by sending that same long string of digits to 1914 in step 1. Although the actual letters/numbers always change, I can still use that one from above to get a response every time. If I don’t do that step, I get no response back when sending 01 to 1911.
  • If I open the Blisslights app and connect, then switch to the nRF app, the app will already be connected to the projector and I won’t need to do step 1, I will get a response by sending 01 to 1911 every time.

The only way I ever managed to change the lights successfully using the nRF app was by doing step 5 and step 7 only. I am thinking this worked because I switched from the Blisslights app right into the nRF app, and thus didn’t need to do step 1 as my last note bullet says. The nRF log said I received a response back when sending 01 to 1911. Unfortunately, I can no longer repeat it - it only worked once for a couple of commands and never again.

I am thinking maybe the 1912 commands are encrypted or contain some code that expires, and maybe that’s why I’m not able to send them anymore? My knowledge of this stuff is very slim, so please excuse my ignorance here, and please excuse me if this isn’t the right forum for this, I’m not really sure where else to ask. Thanks in advance!

7 Likes

I’m about to poke around in my Sky Lite 2.0 too.
I just need to get the dang bits for those security screws…

Sadly I can’t find any internal pictures, would be good to know what controller this thing uses.

2 Likes

I used a Torx T15 screwdriver to open. chip is TLSR8250





2 Likes

It occurred to me to try looking at the binary representation of the commands. My thought is if they’re using bit masks for commands, it might make things clearer? There’s a bit of a pattern, certain quartets are incrementing by one bit and other quartets are identical across commands. I don’t have anything definitive yet, but I thought I’d share my line of reasoning! Here’s the first four commands converted to binary from the list above (view them so each is on a single line, if you can):

1100 0111 1100 0101 0010 0000 0111 1011 0110 0100 1100 1010 1001 1111 1110 0100 0001 0100 1110 0010 0001 0000 1100 0010 0100 1001 1010 1000 1101 1010 0110 0000 0110 1010 1000 0110 0011 1110 1111 0101
1100 1000 1100 0101 0010 0000 0100 1001 0110 0001 0010 0011 1001 1110 0111 1100 0110 0111 1100 0111 0000 0000 0100 0101 1011 0100 0000 1010 1100 1100 0111 1001 1001 1110 1101 1011 0110 0110 0010 0101
1100 1001 1100 0101 0010 0000 0001 0011 1111 1010 1111 0111 0111 0001 1011 0010 1001 1110 1111 0100 1010 1001 0011 0101 0011 1111 1001 0101 0000 1000 0010 0000 1011 0011 0010 1010 1100 1101 0000 0111
1100 1010 1100 0101 0010 0000 0010 0110 1011 0101 1101 0000 0111 1001 0011 0011 0100 1001 0001 1010 0010 0011 1011 1101 0110 1011 0111 0000 0001 1101 1110 0111 0100 0111 1100 1000 0100 1111 0100 1000
1 Like

Thanks for the pictures. I noticed there was what looked like UART pins so my curiosity took over, but had to wait a day to get some longer torx bits! For mine, it was a T10 security screw, so there might be some variation in different models? 3" long bits off amazon.

I was able to get connected to the UART pins no problem using some jumpers I had. 115000 baud, but not 100% sure if that’s from my FTDI adapter or the light. Here’s the output of powering up and then pressing a few of the physical buttons.

..3»               
....  

void app_gpio_normal_init(void) in sig mesh USEWORKMODE:0

 init IO to IIC with pull 

-----------------------mac:a4c1XXXXXXXX-----------------------
v001.13
ff030006000a00001001000000000001ffffffffff01ffffffffff01000000ffff01ffffff00ff0100ffffffff01ff00ffffff00ff00060dff01ffff00ffff01ff000000ff01ff0dff68ff00a0b0c0d0ff00a0b0c0d0ff00a0b0c0d0ff00a0b0c0d0ff00a0b0c0d0ff00a0b0c0d0ff00a0b0c0d0ff00a0b0c0d0ff00a0b0c0d0ff0000010000000000000000000000000000000000000102030405060708090a0b0c0d0e0f101112131668

 read flash allSceneData len:171 
nowMotor:ff 
nowBright:03 
lightOnTimer:00 
lightOffTimer:06 
defaultScene:00 
lastScene:0a 
loopTimer:00 
isLoop:00 
isConfig:10 

 should turn on light --------------- 

  scene 01 r:255 g:255 b:255 l:255 m:255 
  
 start 
 clock :32M 
MY_RF_POWER_INDEX:169
timer 10ms
rgblm stop

 s1 keyDown 

  scene 02 r:255 g:255 b:255 l:255 m:255 
rgblm stop

 s1 keyDown 

  scene 03 r:0 g:0 b:0 l:255 m:255 
rgblm stop

 s2 keyDown 
rgblm stop

 s3 keyDown 
rgblm stop
APPVERSION v001.13
APPVERSION v001.13

I tried entering commands, but it doesn’t seem to take what it outputs as commands. Or maybe it has to be booted into another mode to allow control over serial? But no idea how to go about that.

It looks like the first 9 bytes of the long string is the current state. I only saw it output that long string when first plugged in. So might not be very useful.

ff 03 00 06 00 0a 00 00 10

nowMotor:ff 
nowBright:03 
lightOnTimer:00 
lightOffTimer:06 
defaultScene:00 
lastScene:0a 
loopTimer:00 
isLoop:00 
isConfig:10 

Unfortunately sending commands over bluetooth doesn’t show much. It just outputs like this…

rgblm stop
rgblm stop

I’m assuming RGBLM stands for red, green, blue, laser, motion based on the scene output.

 s1 keyDown 

  scene 00 r:0 g:0 b:0 l:0 m:0 
rgblm stop

 turn off light --------------- 

 s1 keyDown 

 should turn on light --------------- 

  scene 01 r:255 g:255 b:255 l:255 m:255 
rgblm stop

 s1 keyDown 

  scene 02 r:255 g:255 b:255 l:255 m:255 
rgblm stop

 s1 keyDown 

  scene 03 r:0 g:0 b:0 l:255 m:255 

 s1 keyDown 

  scene 04 r:255 g:255 b:255 l:0 m:255 

 s1 keyDown 

  scene 05 r:0 g:255 b:255 l:255 m:255 

 s1 keyDown 

  scene 06 r:255 g:0 b:255 l:255 m:255 

 s1 keyDown 

  scene 08 r:255 g:255 b:0 l:255 m:255 

 s1 keyDown 

  scene 09 r:255 g:0 b:0 l:0 m:255 

 s1 keyDown 

  scene 0a r:255 g:13 b:255 l:104 m:255 
rgblm stop

 s1 keyDown 

  scene 00 r:0 g:0 b:0 l:0 m:0 
rgblm stop

 turn off light --------------- 

I’m familiar with serial ports from the enterprise IT realm, and I’m an engineer / developer by trade, but this is as deep as my knowledge goes in the circuit realm. There is some documentation from the bluetooth chip manufacturer on the chip. There’s mention of a single wire debug interface, and the pins on my unit are labeled TX, RST, RX, SWS, GND, so SWS might be part of that. But I’ve gone as far as I’m going to go with this one today.

Hopefully this will help someone else take this further. Would be great if we could add an ESP in to control it via HA.

3 Likes

Hi all, I had a quick go with my SkyLite 2.0 mostly to try bluetooth sniffing. I took the Mac OS PacketLogger approach and could see traffic, but all a bit to obfuscated for a quick job.

What does seem a lot more promising is pulling apart the android APK. jadx comes out with something pretty sane and this will do the job nicely - Apktool - decode apk ONLINE with open-source APKTOOL. App supports many different devices. Looks like it’s using telink mesh, which appears to involve AES. Have a sniff of com.quhwa.blisslights/mesh & com.telink

I think there’s enough to go on if someone’s sufficiently motivated/bored but it’s not a simple protocol.

1 Like

Small bump to this topic, but found it fascinating.
Although Bluetooth control was not something I’ve experienced with.

So I hardwired the thing!
Five wires from the different FX6402 and FX6404 (could not find any datasheets…) to an ESP32 (5x PWM outputs) running with ESPHome.
Cutting the 3.3v trace to the existing TLSR8250F512ET32 so it will not be powered up.

2 Likes

@mhagen94 Can you expand on what you did please. I’ve just acquired one of these with the intention of getting ESPHome control with an ESPxx device rather than Bluetooth.

I haven’t taken mine apart yet but that’s next. I have no problem in making changes to the PCB. Were you able to get all the functions controllable?

1 Like

Hi no big changes to the PCB, only cutting the 3.3v trace to the ic on the board so it would not power up.
Soldered 5 wires to the mosfets/drivers that are on the board and hooked them up to an ESP32 D1mini board.





ESPHome config

My ESPHome configs are inspired from frenck esphome setup

---
# Custom starlight

substitutions:
  label: startlight
  slug: startlight
  name: Startlight
  description: Startlight

packages:
  <<: !include_dir_named common
  board: !include boards/d1_mini32.yaml
  wifi: !include components/wifi.yaml

light:
  - platform: rgb
    name: "${name} Led"
    red: LED_R
    green: LED_G
    blue: LED_B
    restore_mode: RESTORE_DEFAULT_OFF

  - platform: monochromatic
    name: "${name} Laser"
    output: LASER
    icon: "mdi:laser-pointer"

fan:
  - platform: speed
    name: "${name} Motor"
    output: MOTOR

output:
  - platform: ledc
    pin: 23
    frequency: 19531Hz
    id: LED_R
  - platform: ledc
    pin: 19
    frequency: 19531Hz
    id: LED_G
  - platform: ledc
    pin: 18
    frequency: 19531Hz
    id: LED_B

  - platform: ledc
    pin: 22
    frequency: 19531Hz
    id: LASER
  - platform: ledc
    pin: 21
    frequency: 19531Hz
    id: MOTOR

@mhagen94 Thank you! I need to take my device appart to understand and prepare myself. So are those 6 wires all control or the supply for the ESP as well?

1 Like

Yes, the ESP is powered from the debug pads.
The yellow wire in my first photo.
I don’t have a good photo of that nor the esp, and the unit is hanging on my ceiling :sweat_smile:
But works great! Control over all the settings

@mhagen94 Becoming clearer, thank you! Just to clarify;

  1. ESP power from the Yellow and White wires on the debug pads. (I missed the white wire!)
  2. Red, Green and Blue wires for LED control.
  3. Remaining Black for the Laser and White for the Motor?

Away being a grandparent next week, but will be track cutting and soldering the week after recovering!!!

When I started investigating, I was thinking of wiring up to the switch outputs and keeping the original functionality, but this (I think) will be far better.

1 Like
  1. ESP power from the Yellow and White wires on the debug pads. (I missed the white wire!)

yes

  1. Red, Green and Blue wires for LED control.

Yes, but the colors dont match the IC, just swapped them in the code.

  1. Remaining Black for the Laser and White for the Motor?

Exactly

When I started investigating, I was thinking of wiring up to the switch outputs and keeping the original functionality, but this (I think) will be far better.

Yeah, you could wire the buttons to the ESP as well, but my unit is hanging in the corner of the room, so control is through the HA UI. So I just left them out.

@mhagen94 Did you have to do anything else to control the motor? The nebula LEDs are all working, the star LASER is working but the motor isn’t. I have limited electronics knowledge and I can see the voltage across the motor terminal switching but only 1.5V.

EDIT: I might join the cut track and see what voltage is normally on those terminals, but later…

Hmmm, not sure what was going on? I rejoined the cut track and measured the motor drive voltage and 1.7V. Cut the track again and connected the ESP turned on the motor and 1.7V and all working!

All working now, thank you for the pictures, clarification and the YAML. Now to tinker…

So I got a wild hair tonight and started poking around the apk again and found some really interesting stuff.

For those that want to dig in as well, as mrchimpy pointed out:

  1. Download Blisslights apk
  2. Upload to apktool

I believe the most significant folders are:

  • /sources/com/quhwa
  • /sources/com/telink

This lead down a few rabbit holes, but something that I was always curious about was the QR code to share the light. What data was really in it? When I came across the code below, the format was finally clear. It’s a combination of json, bytes, hex, and gzip.

# from /sources/com/telink/bluetooth/light/qrcode/QRCodeGenerator.java
String bytesToHexString = GZIP.bytesToHexString(GZIP.compressed(json).getBytes(GZIP.GZIP_ENCODE));

So I of course had to see if I could reverse it, and it only took a few minutes and a Google search or two once I had a general idea of what to do. I can’t believe it was that easy.

From the Blisslights app, go to My Account > Share. Then on iOS at least, I took a screenshot, went to the photo, selected the QR code, and copied the underlying text which resulted in a 300ish character hex string. I’m pretty sure this will be part of what’s required to actually connect to the device.

import json
import zlib

def blisslights_decode_qr_code(share_qr_code_hex):
    share_qr_code_bytes_compressed = bytes.fromhex(share_qr_code_hex)
    share_qr_code_bytes_uncompressed = zlib.decompress(share_qr_code_bytes_compressed, 16+zlib.MAX_WBITS)
    share_qr_code_data = json.loads(share_qr_code_bytes_uncompressed)
    return share_qr_code_data

share_qr_code_decoded = blisslights_parse_qr_code('YOUR_LONG_CODE_HERE')

print(json.dumps(share_qr_code_decoded, indent=4))
{
    "n": "TN9CRYK111AAAA1",  <-- swapped some of my characters out for A/1
    "p": "123",
    "d": [
        {
            "a": 196,
            "h": "0",
            "pu": 4,
            "dt": "03",
            "v": "\u0004\u0003\u0002\u0001",
            "m": "3B:0F:AA:AA:FF:FF"  <-- swapped some of my characters out for A/F
        }
    ]
}

So the question then becomes, how do we send data? I found a Python lib Google put out for the Telink chips (well out of date, and archived) that mentions that you need a vendor ID, mac, mesh name, and mesh password. The “v” key in the JSON might be the vendor, which in that case could be 0x04030201, but I’m not sure if that’s right. If dimond works, it would be something like:

import dimond

network = dimond.dimond(0x0211, "00:11:22:33:44:55", "Meshname", "Meshpass", callback=callback)
network.connect()
network.send_packet(target, command, data)

But I’m not going to get into that tonight.

Some other notable things I found that supports what’s in the share code:

# from /sources/com/quhwa/mesh/constant/Constant.java
TELINK_MESH_FACTORY = "telink_mesh0";
MESH_PASSWORD = "123";

These files appear to have some of the command syntax:

  • /sources/com/quhwa/mesh/constant/CmdData.java
  • /sources/com/quhwa/mesh/lightstrip/StripCmdManager.java

So that’s the end of my adventure for this evening. Thought I’d share in case I don’t pick this up again for a while.