(this document can be found here: https://gist.github.com/vmedea/434694c11092261fcac401b7a4b9a741)
Contents:
- Nanlite RF protocol (V1.0)
- Nanlite RF protocol (V2.0) -- TODO
- Bluetooth LE ("app protocol")
This document describes the RF remote control protocol used by at least the Nanlite PavoTubeII6C RGBWW photography lights, and probably more lighting equipment by the same vendor (I do not have access to any other so cannot verify this).
The lights use a Nordic nRF24L0 single chip 2.4 GHz transceiver module to receive updates of the current light mode and settings.
-
The nRF channel used (
RF_CH
) is always0x73
. -
All packets are four bytes.
-
The 5-byte address is generated from the configured channel as
0x00 0x00 0x00 0xAA 0xBB
, where 0xAABB is the channel number between 1 and 511. -
RF_SETUP
is0x07
(2 Mbps, RF output power 0 dBm, setup LNA gain). -
CONFIG
is0x0f
(Enable CRC, 2-byte CRC, POWER UP, PRX).
Four known kinds of packets exist:
-
Type 1: Set intensity (independent of mode).
-
Type 2: Set CCT color temperature + intensity.
-
Type 3: Set HSI hue, saturation and intensity.
-
Type 4: Set CCT color temperature, intensity and G/M (Green/Magenta).
These are described in the individual sections below. The numbering is arbitrary. Although all four bytes, the packet layouts are completely different and they are recognized by matching certain requirements on how the byte values relate.
The RC-1 remote control can only set color temperature and intensity, not hue/saturation/intensity. The other packets are likely used by the WiFi control box.
Set intensity of the light independent of mode.
pkt[0] = I (0..100)
pkt[1] = 0x19
With the following requirements:
pkt[2] == pkt[3]
pkt[2]
and pkt[3]
, besides having to match, can have any value, with no known effect.
Switch the light to CCT mode, and set color temperature + intensity.
pkt[0] = I (0..100)
pkt[1] = CT (0..100, linearly maps to 2700K..7500K)
With the following requirements:
(pkt[0] + pkt[1]) == pkt[2]
(pkt[2] ^ 0xff) == pkt[3]
Switch the light to HSI mode, and set hue, saturation and intensity.
pkt[0] = 0xf0 | Hmsb
pkt[1] = I (0..100)
pkt[2] = Hlsb
pkt[3] = S (0..100)
With the following requirements:
pkt[0] == 0xf0 || pkt[0] == 0xf1
The hue H is computed as ((pkt[0] << 8) | pkt[2]) & 0xfff
, and must be in the range 0..360. The the upper 4 bits of pkt[0]
must be set, so it can only ever be 0xf0
or 0xf1
.
Switch the light to CCT mode, and set color temperature + intensity, as well as G/M.
pkt[0] = I (0..100)
pkt[1] = CT (0..100, linearly maps to 2700K..7500K)
pkt[2] = G/M (0..100, linearly maps to -100..100)
With the following requirements:
(pkt[0] + pkt[1]) == pkt[3]
(pkt[2] ^ 0xff) != pkt[3]
Newer devices (and firmwares) have a setting for a V2 radio protocol. Its specifics are unknown to me.
Newer firmware versions also introduced a protocol that communicates over Bluetooth LE, which is used to communicate with the vendor app. It's also referred to as the "app protocol" in places like change logs.
Note that Bluetooth has higher latency, is less reliable (at least in the form used here), has shorter range, and is much more complicated than the 2.4Ghz radio protocol. The reason one would use it is that BLE hardware is very common in mobile devices, and offers some functionality not possible with the radio protocol (for example: setting effect parameters and reading back values).
The following types are used throughout the document:
uint8
an unsigned byte.uint16_be
an unsigned 16-bit word (big-endian).int16_be
a signed 16-bit word (two's complement, big-endian).uint32_be
an unsigned 32-bit word (big-endian).int32_be
a signed 32-bit word ((two's complement, big-endian).X[Y]
an array of type X of size Y. Size can be left out if it is variable and implied by the context.
All integer fields in the protocol appear to be big-endian.
The Bluetooth GATT stack is provided by a USR IOT WH-BLE 102 module, which provides a serial UART to the device, using the following characteristics:
Service UUID: 0003cdd0-0000-1000-8000-00805f9b0131
UART_TX 0003cdd1-0000-1000-8000-00805f9b0131
UART_RX 0003cdd2-0000-1000-8000-00805f9b0131
To send to the device, send up to 20 bytes at a time to UART_RX
. To receive from the device, subscribe to notifications from UART_TX
using its notification handle (UUID 00002902-0000-1000-8000-00805f9b34fb
, which will have the characteristic handle after UART_TX
). See the example session below.
Larger command packets can span multiple 20-byte BLE packets but the timing seems to be important: the packet is only processed if everything arrives at once, not if there is significant delay between fragments. Conversely, if two packets arrive in too quick a succession they are seen as one packet and the second packet will be ignored.
What seems to achieve most stability in my experiments is to send the first fragments of a packet as write_command
(will have no response), then the last fragment with write_request
, then wait for the write_response
before sending the next packet. A fixed delay (if long enough) will also work. If you want to be more sure that a packet was processed, send it multiple times. The sequence number will make sure it is only processed at most once.
I've also noticed that responses larger than 20 bytes seem to be truncated. The device will never send back anything larger. I'm not sure if this is a problem with the BLE module, or the device firmware, or my own mistake.
From the perspective of the serial line, the protocol looks like:
fa55001101 0320010100320004 01bd0d0a
Application-level packets are enclosed in this envelope with a five-byte header, and a four byte trailer. The header consists of:
(byte offset, type, name, description)
0x00 uint8 magic1 Always 0xfa.
0x01 uint8 magic2 Always 0x55.
0x02 uint16_be pkt_size Size of packet in bytes (including header and trailer).
0x04 uint8 class Packet class. Always 0x01 for packets described here.
The trailer consists of:
(byte offset, type, name, description)
0x00 uint16_be checksum Checksum of packet. The sum of all the bytes in the packets (including
header, but not trailer) plus 0x01.
0x02 uint8 magic3 Always 0x0d.
0x03 uint8 magic4 Always 0x0a.
Magic values inside packets are not escaped inside this envelope, as one would normally expect for line synchronization in SLIP-like protocols. Reply packets will have the same format.
Besides these packets, the application has been seen to send 0x01
periodically, probably a keep-alive. The device occasionally sends +++
spuriously (may be a failed AT break — the BLE module uses a AT command set for setup).
This section describes the application level protocol, inside the serial protocol wrapping.
An example of the simplest packet would be: 03 20 01 01 00 32 00 04
. This sets the brightness parameter to 50.
An example of an extended packet is 23 24 00 04 01 01 01 08 00 0b 03 01 00 00 64
. This switches the device to HSI lighting mode.
Packets are prefixed by a header:
(byte offset, type, name, description)
0x00 uint8 seq Increasing sequence number. this is used to ignore duplicated packets, as well as reproduced in replies.
0x01 uint8 flags Bit field specifying packet layout and requested parameter operations:
00000001 read_req Request parameter read.
00000010 read_resp Response to parameter read.
00000100 extpkt Extended packet.
00100000 write_req Request parameter write.
Packets can have two known formats:
- Short packets which are always 8 bytes.
- Extended packets which can be any size. These are marked by the
extpkt
flag.
Unknown packets, commands or parameter accesses are ignored.
Response packets will only be sent if requested with req_read
. These will generally have the same format as the requesting command packet.
Short packets have a fixed format, specifying:
(byte offset, type, name, description)
0x00 uint8 seq (see "Header" section)
0x01 uint8 flags (see "Header" section)
0x02 uint8 pgroup Parameter group.
0x03 uint8 param Parameter number within group.
0x04 uint8[4] payload Parameter-specific payload. Generally one of:
uint8 | padding[3]
uint16_be | padding[2]
uint16_be[2]
uint32_be
-
To write a parameter, set
write_req
inflags
(0x20
). The parameter-specific payload is written to the specified parameter. The value of padding bytes seems to be ignored. But the app consistently uses0x00 0x04
.- There will be no reply unless
read_req
is set as well (0x21
), in which case the parameter is written and a (confirmation?) response is sent, with thewrite_req
flag set but notread_resp
(0x20
).
- There will be no reply unless
-
To read a parameter, set
read_req
inflags
(0x01
). The payload is ignored. The reply will haveread_resp
flag set.
Extended packets have the following header:
(byte offset, type, name, description)
0x00 uint8 seq (see "Header" section)
0x01 uint8 flags (see "Header" section)
0x02 uint16_be unk1 Always 0x0004?
0x04 uint16_be unk2 Always 0x0101?
0x05 uint8 opcode One of:
0x01 set_mode Set lighting mode.
0x04 animate Animate?
0x06 uint8[] payload Command-dependent variable payload.
To send a command, set the write_req
and extpkt
bits in flags
(0x24
). See the "Commands" section below for specifics on the payload.
There are tons of different parameters that can be set, it's surprising how a device like this has so many, though, it's bound to be a more general protocol adapted to different devices and not all may apply to this one.
General light parameters.
(param, payload type, description)
0x01 uint16_be Brightness (any mode). Range 0..100.
0x02 uint16_be[2] Animation parameters?
0x03 uint16_be Color temperature (CCT). Range 2700..7500.
0x04 int16_be Green/Magenta (CCT). Range -100..100.
0x05 uint16_be Hue (HSI). Range 0..359.
0x0c uint8 Saturation (HSI). Range 0..100.
0x64 uint32_be Set ??
0x65 N/A Disable ??
Probing has shown that parameter IDs in this group run up to 0x65
(101). Most of these will be effect parameters. I've not looked into these modes as the built-in effects aren't relevant to our use case. Please let me know if you figure any out, though.
Probing up to group 0x0c
using parameter reads has shown that the following other groups have values that can be read:
0x03
range0x01..0x06
0x05
range0x00..0xff
0x09
range0x00..0xff
0x0a
range0x00..0xff
I've seen parameter 0x0c:0x00
being read in captures, but without response. I'm not sure what any of these are.
Some extended packets do more than parameter setting. This section describes known application command packets and their payload.
This command sets the device mode, and full initial parameters for that mode.
(byte offset, type, name, description)
0x00 uint8 ssize Size of payload in bytes (including this byte)
0x01 uint16_be brightness Brightness value 0..100 (mode independent).
0x03 uint8 mode Lighting mode.
0x04 uint8 submode Lighting submode (values depend on mode).
0x05 uint8[] params Concatenated parameters, depending on mode and submode.
The following sections describe mode:submode
combinations observed.
Both of these switch to CCT (Correlated Color Temperature) mode. I've seen no functional difference between either. Params (6 bytes) are:
(byte offset, type, name, description)
0x00 uint8 unk1 Always 0x01? No visible effect.
0x01 uint16_be color_temp Color temperature.
0x03 uint16_be green_mag Green/Magenta.
0x05 uint8 unk2 Always 0x01? No visible effect.
Switch to HSI (Hue Saturation Intensity) mode. Params (3 bytes) are:
(byte offset, type, name, description)
0x00 uint16_be hue Hue.
0x02 uint8 sat Saturation.
This sets effects like "hue loop", "lightning", "police car", etc. The submode
specifies the specific effect. The parameters also depend on the specific effect.
I've not investigated specifics, but here are some example packets setting different modes:
(mode:submode, params)
02:02 010e10000001 (CCT)
03:01 012b58 (HSI)
07:04 000001686408020103e8322832
07:05 0c8015e0000008020103e8322832
07:06 00280115e0000000006408020103e8322832
07:08 0115e0000000006428
07:09 0115e00206030601
07:0a 040232
07:0c 15e0000032
07:0e 070903320000001e003c005a0078009664646464646464
07:10 070903323215e0000002580000001e003c005a0078009664646464646464
(byte offset, type, name, description)
0x00 uint8 pgroup Parameter group.
0x01 uint8 param Parameter number within group.
0x02 uint8[4] payload Parameter-specific payload.
I'm not actually sure what this does I've only seen 010200000002
, which animates the brightness (see below). The parameter group and number seems to match with the parameters used in short packets, but it definitely only works for a subset.
(byte offset, type, name, description)
0x00 uint8 pgroup 0x01
0x01 uint8 param 0x02
0x02 uint16_be brightness Target brightness value
0x04 uint16_be time Time in seconds to reach that value
You can follow these steps on a typical Linux system with Bluetooth 4.1+ stack and the necessary tools installed:
- Find the device advertisements. Nanlite devices will start with
..NL
. The other numbers probably specifies the model and configured channel number:
$ hciconfig hci0 up
$ hcitool lescan
9C:A5:xx:xx:xx:xx ..NL 004 00000A
- Connect using
gatttool
:
$ gatttool -i hci0 -b 9C:A5:xx:xx:xx:xx -I
[9C:A5:xx:xx:xx:xx][LE]> connect
Attempting to connect to 9C:A5:xx:xx:xx:xx
Connection successful
[9C:A5:xx:xx:xx:xx][LE]> primary
attr handle: 0x0001, end grp handle: 0x0004 uuid: 00001801-0000-1000-8000-00805f9b34fb
attr handle: 0x0005, end grp handle: 0x000b uuid: 00001800-0000-1000-8000-00805f9b34fb
attr handle: 0x000c, end grp handle: 0x0011 uuid: 0003cdd0-0000-1000-8000-00805f9b0131
[9C:A5:xx:xx:xx:xx][LE]> char-desc
handle: 0x0001, uuid: 00002800-0000-1000-8000-00805f9b34fb
handle: 0x0002, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0003, uuid: 00002a05-0000-1000-8000-00805f9b34fb
handle: 0x0004, uuid: 00002902-0000-1000-8000-00805f9b34fb
handle: 0x0005, uuid: 00002800-0000-1000-8000-00805f9b34fb
handle: 0x0006, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0007, uuid: 00002a00-0000-1000-8000-00805f9b34fb
handle: 0x0008, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0009, uuid: 00002a01-0000-1000-8000-00805f9b34fb
handle: 0x000a, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x000b, uuid: 00002a04-0000-1000-8000-00805f9b34fb
handle: 0x000c, uuid: 00002800-0000-1000-8000-00805f9b34fb
handle: 0x000d, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x000e, uuid: 0003cdd1-0000-1000-8000-00805f9b0131 tx
handle: 0x000f, uuid: 00002902-0000-1000-8000-00805f9b34fb subscribe for read
handle: 0x0010, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0011, uuid: 0003cdd2-0000-1000-8000-00805f9b0131 rx
- Subscribe to notifications:
[9C:A5:xx:xx:xx:xx][LE]> char-write-req 0x0f 0100
Characteristic value was written successfully
- Set the brightness to 50%:
> char-write-req 0x11 fa55001101032001010032000401bd0d0a
Characteristic value was written successfully
- Set the brightness to 25%:
> char-write-req 0x11 fa55001101082001010019000401a90d0a
Characteristic value was written successfully
- Read the current brightness:
> char-write-req 0x11 fa550011010601010100000000016b0d0a
Characteristic value was written successfully
Notification handle = 0x000e value: fa 55 00 11 01 06 02 01 01 00 32 00 00 01 9e 0d 0a
- It looks like the Bluetooth module is on when the device is off and charging. It doesn't respond to (nor likely, process) any application-level commands in this case, but will appear in advertisements, and respond to Bluetooth queries.
SPDX-License-Identifier: CC-BY-4.0
I have provided this information in the hope it is useful, and have tried to be as careful as possible, but I cannot be held responsible if use of this information causes damage to your device.