Reverse engineering tools and documentation for controlling Nanlite FS-300B LED studio lights via BLE mesh, bypassing the official Nanlink app.
The Nanlite FS-300B is a 350W LED studio light that uses Bluetooth Low Energy (BLE) mesh for wireless control. Nanlite's official Nanlink app provisions the light into a BLE mesh network with encrypted communication.
This project provides:
- Protocol documentation - Complete breakdown of the vendor-specific BLE mesh commands
- Python CLI tool - Interactive controller using the mesh proxy protocol
- Key extraction guide - How to get your mesh keys from an Android device
- The Nanlink app is closed-source and cloud-connected
- No official API or local control option
- BLE mesh encryption prevents simple sniffing
- We want Home Assistant / MQTT integration
pip install bleak pycryptodomeSee Extracting Mesh Keys below. You need to extract these from your phone after pairing the light.
Edit nanlite_mesh.py and fill in your keys:
NID = 0x7A # Your Network ID
ENCRYPTION_KEY = bytes.fromhex("...") # Your EncryptionKey
PRIVACY_KEY = bytes.fromhex("...") # Your PrivacyKey
APP_KEY = bytes.fromhex("...") # Your AppKeyThen run:
python nanlite_mesh.pyThe mesh keys are provisioned when you first pair the light with the Nanlink app. They're stored on your phone and used to encrypt all BLE mesh traffic. You need to extract them to control the light independently.
This method extracts keys from the Nanlink app's logcat output in an Android bugreport.
- Android phone with the light already paired in Nanlink app
- USB debugging enabled (Settings → Developer Options → USB Debugging)
adbinstalled on your computer
-
Make sure the Nanlink app has been used recently (within the current boot cycle). The keys are logged when the app initializes the mesh.
-
Generate a bugreport:
adb bugreport
This creates a zip file like
bugreport-DEVICE-DATE.zip -
Extract and search the bugreport:
unzip bugreport-*.zip # The main log file is named like: # bugreport-DEVICE-DATE.txt
-
Search for mesh keys:
grep -i "encryptionkey\|privacykey\|appkey\|devicekey\|netkey" bugreport*.txt
Look for logcat entries from the Nanlink app or BLE mesh stack. You'll see entries like:
D NanMesh: EncryptionKey: 80EDA875409E2FF2B78310D4B45641C4 D NanMesh: PrivacyKey: F86F6FDD9FC1054722BB57C283214F57 D NanMesh: AppKey: 63964771734FBD76E3B40519D1D94A48 -
Also search for NID and AID:
grep -i "nid\|aid\|netkey" bugreport*.txt
The NID (Network ID) is 7 bits derived from the NetKey. The AID is derived from the AppKey.
If you can reproduce the key logging:
adb logcat -c # Clear existing logs
# Open Nanlink app and connect to the light
adb logcat | grep -i "key\|mesh"If the keys are hardcoded or you want to understand the key derivation:
-
Download the Nanlink APK:
# From your phone adb shell pm path com.nanguang.nanlinkforhome adb pull /path/to/base.apk nanlink.apk -
Decompile with jadx:
jadx -d nanlink_decompiled nanlink.apk
-
Search for mesh-related code:
grep -r "AppKey\|NetKey\|EncryptionKey" nanlink_decompiled/ -
Look at the provisioning logic in classes like:
MeshProvisionerServiceNanMeshManagerFeasyMeshConfig
If you can capture the BLE traffic during provisioning:
-
Enable HCI snoop log on Android:
- Settings → Developer Options → Enable Bluetooth HCI snoop log
- (Reboot may be required)
-
Pair the light in Nanlink app
-
Extract the snoop log:
adb pull /data/misc/bluetooth/logs/btsnoop_hci.log
-
Analyze in Wireshark with the BT Mesh dissector. You can see encrypted PDUs and, if you have known plaintexts (like brightness=100), attempt key recovery.
BLE Mesh key hierarchy:
- NetKey (128-bit) → Primary network key, provisioned per-network
- NID (7-bit) → Derived from NetKey, used to identify network
- EncryptionKey (128-bit) → Derived from NetKey, encrypts network layer
- PrivacyKey (128-bit) → Derived from NetKey, obfuscates network header
- AppKey (128-bit) → Application-level key, encrypts access layer
- AID (6-bit) → Derived from AppKey, identifies which AppKey was used
The Nanlite lights use standard BLE Mesh key derivation (k2, k4 functions from the BT Mesh spec).
All commands use the vendor-specific opcode C1 11 11 (0x1111 = Nanlite's Bluetooth SIG company ID).
After the opcode, commands have an 8-byte parameter block:
| Byte | Name | Description |
|---|---|---|
| 0 | rCode | Rolling counter 0-7 |
| 1 | funcCode | Bitfield: SET=0x20, QUERY=0x01 |
| 2 | typeCode | Command category (0x01=control, 0x07=fan, 0x0C=query-all) |
| 3 | optionCode | Command ID |
| 4-5 | value | Big-endian 16-bit value |
| 6-7 | channel | Big-endian 16-bit (0x0001 for direct control) |
| Code | Name | Value Range | Notes |
|---|---|---|---|
| 0x01 | DIM | 0-100 | Brightness percentage |
| 0x03 | CCT | 2700-6500 | Color temperature in Kelvin |
| 0x04 | GM | varies | Green-magenta tint |
| 0x28 | PC_MODE | 0-2 | Power constant mode |
| 0x60 | CCT_OUTPUT | 2 or 3 | 2=max output, 3=constant output |
Fan uses a different format with typeCode=0x07. The mode goes in optionCode:
- 0x00 = Fan off
- 0x01 = Fan on
Important: When fan is off, brightness is capped at 25%. The light doesn't auto-scale; it just limits output.
The light exposes a standard BLE Mesh Proxy service (UUID 0x1828). Communication flow:
- Connect to GATT
- Send Feasycom auth handshake on 0xFFF2:
$FSCCMD001$\r\n - Subscribe to 0x2ADE (Mesh Proxy Data Out)
- Write commands to 0x2ADD (Mesh Proxy Data In)
BLE Mesh has replay protection based on sequence numbers. The light rejects any message with seq ≤ last seen seq from that source address.
The script persists its sequence number to ~/.nanlite_seq to survive restarts. If you get no response, your seq might be behind - delete the file and restart (will use seq=50000).
nanlite_mesh.py- Interactive CLI controllerPROTOCOL.md- Detailed protocol documentation (if you want more)
- nanlite-bridge-esp32 - ESP32-C3 firmware that bridges Nanlite lights to MQTT/Home Assistant
MIT
Reverse engineered from the Nanlink Android app and btsnoop captures. No Nanlite proprietary code is included.