Compare commits

..

13 Commits

11 changed files with 572 additions and 31 deletions

10
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
]
}

177
README.md
View File

@@ -1,4 +1,179 @@
# RFPowerView
A library for receiving and sending PowerView packets via an nRF24L01 module
Library for sending and receiving PowerView packets so that blinds can be controlled programatically. PowerView is a brand of remote controlled blinds developed by Hunter Douglas (also known as Luxaflex in some regions). This library is intended to be used on a microcontroller like an ESP8266 in conjunction with a nRF24L01 transceiver module. The implementation has been entirely reverse engineered by sniffing packets sent from a PowerView Hub or Pebble remote.
## Supported commands
- Open
- Close
- Stop
- Open slowly
- Close slowly
- Move to saved position
- Move to position
- Query position
- Query battery level
## Protocol
A PowerView packet consists of a header (which contains a variable length address section), a variable length payload, and a two byte checksum at the end. Each packet is limited to being at most 32 bytes due to limitations of the nRF24L01 module.
### Header
| Byte Offset | Size (Bits) | Name | Description | Valid Values |
|---|---|---|---|---|
| 0 | 8 | ID Byte | Identifies the start of a new packet. | `0xCO` |
| 1 | 8 | Length | Specifies how many bytes of header and data come after this byte (excludes checksum bytes). | `0x0A` - `0x1D` |
| 2 | 8 | Unknown | Could indicate the type of sender | `0x00` (from hub or remote), `0x01` (from blind) |
| 3 | 8 | Fixed | This byte is always the same. | `0x05` |
| 4 | 8 | Rolling Code 1 | This byte is incremented by 0x01 each time a packet is sent. | `0x00`-`0xFF` |
| 5 | 16 | Fixed | These two bytes are always the same. | `0xFFFF` |
| 7 | 16 | Physical Source ID | The ID of device that actually sent the packet (can be different to Logical Source ID when a repeater is being used) | `0x0000`-`0xFFFF` |
| 9 | 8 | Fixed | This byte is always the same | `0x86` |
| 10 | 8 | Address Type | Indicates which type of address this packet is using. | `0x04` (Broadcast), `0x05` (Unicast), `0x06` (Groups) |
| 11 | 8 | Rolling Code 2 | This byte is incremented by 0x01 each time a packet is sent. | `0x00`-`0xFF` |
| 12 | Varies | Address | This section is a variable length. | Broadcast, Unicast, or Groups section (see next section) |
#### Broadcast address
| Byte Offset | Size (Bits) | Name | Description | Valid Values |
|---|---|---|---|---|
| 0 | 16 | Logical Source ID | The ID of device where the packet originated (hubs seem to always be 0x0000) | `0x0000`-`0xFFFF` |
Note: this type of address seems to be used when a hub is activating a scene (and multiple blinds could be included in the scene).
#### Unicast address
| Byte Offset | Size (Bits) | Name | Description | Valid Values |
|---|---|---|---|---|
| 0 | 16 | Destination ID | The ID of device that should process the packet | `0x0000`-`0xFFFF` |
| 2 | 16 | Logical Source ID | The ID of device where the packet originated (hubs seem to always be 0x0000) | `0x0000`-`0xFFFF` |
Note: this type of address is used when a hub wants to communicate with a specific blind or when a blind is communicating with a hub.
#### Groups address
| Byte Offset | Size (Bits) | Name | Description | Valid Values |
|---|---|---|---|---|
| 0-5 | 8 | Group Number | Devices assigned to the specified group numbers should process the packet. | `0x01`-`0x06` |
| n | 8 | Terminator | Marker to stop processing group numbers | `0x00` |
| n+1 | 16 | Logical Source ID | The ID of device where the packet originated | `0x0000`-`0xFFFF` |
Note: this type of address is used by pebble remotes. The Group Number corresponds to the the numbered buttons on the pebble remote. When multiple group numbers are included, generally they are in ascending order.
### Payload
This section begins at the next byte following the variable length address section. I've observed two types of payloads: simple and "fields".
### Simple payload
| Byte Offset | Size (Bits) | Name | Description | Valid Values |
|---|---|---|---|---|
| 0 | 8 | Packet Type | Indicates the payload is a simple command | `0x52` |
| 1 | 8 | Command | What should the blind do | `0x55` (Open), `0x44` (Close), `0x53` (Stop), `0x52` (Open Slowly), `0x4C` (Close Slowly), `0x48` (Move to saved position) |
| 2 | 8 | End | Marker to indicate end of data | `0x00` |
Note: this type of payload is used by both the hub and pebble remotes. Used for commands like "Open", "Close", or "Stop"
#### Fields payload
| Byte Offset | Size (Bits) | Name | Description | Valid Values |
|---|---|---|---|---|
| 0 | 8 | Field Mode | Indicates the payloads contains "fields" | `0x3F` (fields without values), `0x21` (fields with values) |
| 1 | 8 | Fixed | This byte is always the same. | `0x5A` |
| 2 | varies | Field data | This section is a variable length depending on the number of fields the payload contains | Field data (see next section) |
Note: this type of payload is used by the hub. If sent to a blind without values, it will prompt the blind to respond with the value of those fields. If sent to a blind with values, it will prompt the blind to "apply" the value to the fields (eg. by moving to a position). A single payload can contain multiple fields.
#### Field data
| Byte Offset | Size (Bits) | Name | Description | Valid Values |
|---|---|---|---|---|
| 0 | 8 | Field Length | Indicates how many bytes are in this field | `0x02`-`0x04` |
| 1 | 8 | Field Mode | Seems to always be the same as the first Field Mode | `0x3F` (fields without values), `0x21` (fields with values) |
| 2 | 8 | Field ID | The field ID | `0x00`-`0xFF`, eg. `0x50` is Position, `0x42` is Battery level |
| 3 (if length=3) | 8 | Field Value | The value of the field | `0x00`-`0xFF` |
| 3 (if length=4) | 16 | Field Value | The value of the field | `0x0000`-`0xFFFF` |
### Checksum
| Byte Offset | Size (Bits) | Name | Description | Valid Values |
|---|---|---|---|---|
| 0 | 16 | Checksum | A CRC16 checksum of the packet data | `0x0000`-`0xFFFF` |
## Examples
### Open blinds in group 4
`C01100056CFFFF369E86063C0400369E525500B988`
| Name | Value | Notes |
|---|---|---|
| Length | `0x11` | |
| Rolling Code 1 | `0x6C` | |
| Rolling Code 2 | `0x3C` | |
| Physical Source ID | `0x369E` | ID of pebble remote |
| Address type | `0x06` | Using Groups address type |
| Logical Source ID | `0x369E` | ID of pebble remote (same as Physical Source ID) |
| Groups | 4 | Only a single group specified |
| Checksum | `0xB988` | |
### Hub requesting values from a blind
`C019000592FFFF72CB85054E4EF100003F5A023F50023F4D023F54C9F3`
| Name | Value | Notes |
|---|---|---|
| Length | `0x19` | |
| Rolling Code 1 | `0x92` | |
| Rolling Code 2 | `0x4E` | |
| Physical Source ID | `0x72CB` | ID of repeater |
| Address type | `0x05` | Using Unicast address type |
| Destination ID | `0x4EF1` | ID of blind |
| Logical Source ID | `0x0000` | ID of hub (different than Physical Source ID) |
| Field 1 | `023F50` | Field of 2 bytes, ID = `0x50` (position) |
| Field 2 | `023F4D` | Field of 2 bytes, ID = `0x4D` (unknown) |
| Field 3 | `023F54` | Field of 2 bytes, ID = `0x54` (unknown) |
| Checksum | `0xC9F3` | |
### Blind reporting position value to hub
`C0151005E0FFFF4EF186051A00004EF1215A04215040016670`
| Name | Value | Notes |
|---|---|---|
| Length | `0x15` | |
| Rolling Code 1 | `0xE0` | |
| Rolling Code 2 | `0x1A` | |
| Physical Source ID | `0x4EF1` | ID of blind |
| Address type | `0x05` | Using Unicast address type |
| Destination ID | `0x0000` | ID of hub |
| Logical Source ID | `0x4EF1` | ID of blind |
| Field 1 | `0421504001` | Field of 4 bytes, ID = `0x50` (position), value = `0x4001` |
| Checksum | `0x6670` | |
### Blind reporting battery level value to hub
`C014100558FFFF4EF18605C100004EF1215A0321429DEC23`
| Name | Value | Notes |
|---|---|---|
| Length | `0x14` | |
| Rolling Code 1 | `0x58` | |
| Rolling Code 2 | `0xC1` | |
| Physical Source ID | `0x4EF1` | ID of blind |
| Address type | `0x05` | Using Unicast address type |
| Destination ID | `0x0000` | ID of hub |
| Logical Source ID | `0x4EF1` | ID of blind |
| Field 1 | `0321429D` | Field of 3 bytes, ID = `0x42` (battery level), value = `0x9D` |
| Checksum | `0xEC23` | |
### Hub activating a scene
`C00F0005A1FFFF00008604FF000053471B446B`
| Name | Value | Notes |
|---|---|---|
| Length | `0x0F` | |
| Rolling Code 1 | `0xA1` | |
| Rolling Code 2 | `0xFF` | |
| Physical Source ID | `0x0000` | ID of hub |
| Address type | `0x04` | Using Broadcast address type |
| Logical Source ID | `0x0000` | ID of hub |
| Scene ID | `1B` | |
| Checksum | `0x446B` | |
Note: activating scenes is not supported by the library (yet!)

View File

@@ -1,7 +1,7 @@
#ifndef BUFFERFILLER_H
#define BUFFERFILLER_H
#include <Arduino.h>
#include <stdint.h>
#include "PacketCRC.h"
#include "PacketTypes.h"

View File

@@ -1,7 +1,7 @@
#ifndef PACKET_PARSER_H
#define PACKET_PARSER_H
#include <Arduino.h>
#include <vector>
#include "PacketTypes.h"
class PacketParser {

View File

@@ -1,7 +1,7 @@
#ifndef PACKET_TYPES_H
#define PACKET_TYPES_H
#include <Arduino.h>
#include <stdint.h>
#include <variant>
// Define packet types

View File

@@ -1,7 +1,7 @@
#ifndef RFPOWERVIEW_H
#define RFPOWERVIEW_H
#include <Arduino.h>
#include <stdint.h>
#include <RF24.h>
#include "PacketReceiver.h"
#include "PacketParser.h"

View File

@@ -1,27 +1,24 @@
{
"name": "RFPowerView",
"version": "0.0.3",
"description": "A library for receiving and sending PowerView packets via an nRF24L01 module",
"keywords": "powerview, hunterdouglas, luxaflex, rf, radio",
"repository": {
"type": "git",
"url": "https://github.com/mattyway/RFPowerView.git"
},
"authors":
[
{
"name": "Matt Way",
"email": "mattyway@gmail.com"
}
],
"license": "GPL-2.0-only",
"dependencies": {
"robtillaart/CRC": "1.0.2",
"nrf24/RF24": "1.4.8",
"rlogiacco/CircularBuffer": "1.3.3"
},
"frameworks": "arduino",
"platforms": [
"espressif8266"
]
}
"name": "RFPowerView",
"version": "0.0.3",
"description": "A library for receiving and sending PowerView packets via an nRF24L01 module",
"keywords": "powerview, hunterdouglas, luxaflex, rf, radio",
"repository": {
"type": "git",
"url": "https://github.com/mattyway/RFPowerView.git"
},
"authors": [
{
"name": "Matt Way",
"email": "mattyway@gmail.com"
}
],
"license": "GPL-2.0-only",
"dependencies": {
"robtillaart/CRC": "1.0.2",
"nrf24/RF24": "1.4.8",
"rlogiacco/CircularBuffer": "1.3.3"
},
"frameworks": "arduino",
"platforms": ["espressif8266"]
}

33
platformio.ini Normal file
View File

@@ -0,0 +1,33 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:nodemcuv2]
platform = espressif8266
board = nodemcuv2
framework = arduino
[env]
monitor_speed = 115200
test_build_src = true
test_framework = unity
lib_deps =
robtillaart/CRC @ 1.0.2
nrf24/RF24 @ 1.4.8
rlogiacco/CircularBuffer @ 1.3.3
[env:native]
platform = native
lib_deps =
${env.lib_deps}
ArduinoFake
build_src_filter =
+<**/*.cpp>
-<**/RFPowerView.cpp>
-<**/PacketReceiver.cpp>

29
test/hex_helper.h Normal file
View File

@@ -0,0 +1,29 @@
#ifndef HEX_HELPER_H
#define HEX_HELPER_H
#include <cstring> // for strlen
uint8_t* hex_string_to_array(const char* str) {
size_t len = strlen(str);
if (len % 2 != 0) {
// Cannot handle odd string length
return nullptr;
}
size_t data_len = len / 2;
uint8_t* data = new uint8_t[data_len];
// Convert each hexadecimal character pair to a byte
for (size_t i = 0; i < data_len; ++i) {
uint8_t high_nibble = str[i * 2] >= 'A' ? (str[i * 2] - 'A' + 10) : (str[i * 2] - '0');
uint8_t low_nibble = str[i * 2 + 1] >= 'A' ? (str[i * 2 + 1] - 'A' + 10) : (str[i * 2 + 1] - '0');
// Combine the nibbles into a byte
data[i] = (high_nibble << 4) | low_nibble;
}
// Return the pointer to the allocated data array
return data;
}
#endif // HEX_HELPER_H

111
test/test_crc/test_crc.cpp Normal file
View File

@@ -0,0 +1,111 @@
#include <unity.h>
#include <Arduino.h>
#include "../hex_helper.h"
#include "PacketCRC.h"
void setUp()
{
}
void tearDown()
{
}
void run_calculate_test(const uint8_t *packet_data, const uint16_t expected_checksum)
{
PacketCRC packetCRC;
uint16_t actual_checksum = packetCRC.calculate(packet_data);
TEST_ASSERT_EQUAL_HEX16(expected_checksum, actual_checksum);
}
void test_calculate_withDataLengthOf15()
{
const uint8_t* packet_data = hex_string_to_array("C00F0005A1FFFF00008604FF000053471B");
const uint16_t expected_checksum = 0x446B;
run_calculate_test(packet_data, expected_checksum);
delete[] packet_data;
}
void test_calculate_withDataLengthOf17()
{
const uint8_t* packet_data = hex_string_to_array("C0110005D7FFFF369E8606B30400369E525300");
const uint16_t expected_checksum = 0xE04C;
run_calculate_test(packet_data, expected_checksum);
delete[] packet_data;
}
void test_calculate_withDataLengthOf19()
{
const uint8_t* packet_data = hex_string_to_array("C0130005DDFFFF00008605364EF100003F5A023F42");
const uint16_t expected_checksum = 0x9BD3;
run_calculate_test(packet_data, expected_checksum);
delete[] packet_data;
}
void test_calculate_withDataLengthOf20()
{
const uint8_t* packet_data = hex_string_to_array("C014100559FFFF4EF18605C200004EF1215A0321429C");
const uint16_t expected_checksum = 0x4A8B;
run_calculate_test(packet_data, expected_checksum);
delete[] packet_data;
}
void test_calculate_withDataLengthOf21()
{
const uint8_t* packet_data = hex_string_to_array("C0151005EAFFFF4EF186052400004EF121464E98070801");
const uint16_t expected_checksum = 0x9887;
run_calculate_test(packet_data, expected_checksum);
delete[] packet_data;
}
void test_calculate_withDataLengthOf25()
{
const uint8_t* packet_data = hex_string_to_array("C019000547FFFF000086053D4EF100003F5A023F50023F4D023F54");
const uint16_t expected_checksum = 0x8318;
run_calculate_test(packet_data, expected_checksum);
delete[] packet_data;
}
int runUnityTests(void)
{
UNITY_BEGIN();
RUN_TEST(test_calculate_withDataLengthOf15);
RUN_TEST(test_calculate_withDataLengthOf17);
RUN_TEST(test_calculate_withDataLengthOf19);
RUN_TEST(test_calculate_withDataLengthOf20);
RUN_TEST(test_calculate_withDataLengthOf21);
RUN_TEST(test_calculate_withDataLengthOf25);
return UNITY_END();
}
/**
* For Arduino framework
*/
void setup()
{
// Wait ~2 seconds before the Unity test runner
// establishes connection with a board Serial interface
delay(2000);
runUnityTests();
}
void loop() {}
int main(void)
{
return runUnityTests();
}

View File

@@ -0,0 +1,186 @@
#include <unity.h>
#include <Arduino.h>
#include "../hex_helper.h"
#include "PacketParser.h"
void setUp()
{
}
void tearDown()
{
}
void run_parse_test(const uint8_t *packet_data, Packet &packet)
{
PacketParser packetParser;
bool result = packetParser.parsePacket(packet_data, packet);
TEST_ASSERT_TRUE_MESSAGE(result, "Unable to parse packet_data");
}
void test_parse_stop()
{
const uint8_t* packet_data = hex_string_to_array("C01100056CFFFF369E86063C0400369E525300");
Packet packet;
run_parse_test(packet_data, packet);
TEST_ASSERT_TRUE(packet.type == PacketType::STOP);
delete[] packet_data;
}
void test_parse_close()
{
const uint8_t* packet_data = hex_string_to_array("C01100056CFFFF369E86063C0400369E524400");
Packet packet;
run_parse_test(packet_data, packet);
TEST_ASSERT_TRUE(packet.type == PacketType::CLOSE);
delete[] packet_data;
}
void test_parse_open()
{
const uint8_t* packet_data = hex_string_to_array("C01100056CFFFF369E86063C0400369E525500");
Packet packet;
run_parse_test(packet_data, packet);
TEST_ASSERT_TRUE(packet.type == PacketType::OPEN);
delete[] packet_data;
}
void test_parse_rolling_codes()
{
const uint8_t* packet_data = hex_string_to_array("C01100056CFFFF369E86063C0400369E525300");
Packet packet;
run_parse_test(packet_data, packet);
TEST_ASSERT_EQUAL_HEX8_MESSAGE(0x6C, packet.rollingCode1, "Rolling Code 1");
TEST_ASSERT_EQUAL_HEX8_MESSAGE(0x3C, packet.rollingCode2, "Rolling Code 2");
delete[] packet_data;
}
void test_broadcast_source_address()
{
const uint8_t* packet_data = hex_string_to_array("C01100056CFFFF369E86063C0400369E525300");
Packet packet;
run_parse_test(packet_data, packet);
auto header = std::get<GroupsHeader>(packet.header);
TEST_ASSERT_EQUAL_HEX16(0x369E, header.source);
delete[] packet_data;
}
void test_broadcast_single_group()
{
const uint8_t* packet_data = hex_string_to_array("C01100056CFFFF369E86063C0400369E525300");
Packet packet;
run_parse_test(packet_data, packet);
auto header = std::get<GroupsHeader>(packet.header);
TEST_ASSERT_EQUAL_MESSAGE(1, header.groups.size(), "Group count");
TEST_ASSERT_EQUAL_INT8_MESSAGE(4, header.groups[0], "Group at offset 0");
delete[] packet_data;
}
void test_broadcast_multiple_groups()
{
const uint8_t* packet_data = hex_string_to_array("C01100056CFFFF369E86063C03040500369E525300");
Packet packet;
run_parse_test(packet_data, packet);
auto header = std::get<GroupsHeader>(packet.header);
TEST_ASSERT_EQUAL_MESSAGE(3, header.groups.size(), "Group count");
TEST_ASSERT_EQUAL_INT8_MESSAGE(3, header.groups[0], "Group at offset 0");
TEST_ASSERT_EQUAL_INT8_MESSAGE(4, header.groups[1], "Group at offset 1");
TEST_ASSERT_EQUAL_INT8_MESSAGE(5, header.groups[2], "Group at offset 2");
delete[] packet_data;
}
void test_unicast_source_address()
{
const uint8_t* packet_data = hex_string_to_array("C019000592FFFF72CB85054E4EF100003F5A023F50023F4D023F54");
Packet packet;
run_parse_test(packet_data, packet);
auto header = std::get<UnicastHeader>(packet.header);
TEST_ASSERT_EQUAL_HEX16(0x0000, header.source);
delete[] packet_data;
}
void test_unicast_destination_address()
{
const uint8_t* packet_data = hex_string_to_array("C019000592FFFF72CB85054E4EF100003F5A023F50023F4D023F54");
Packet packet;
run_parse_test(packet_data, packet);
auto header = std::get<UnicastHeader>(packet.header);
TEST_ASSERT_EQUAL_HEX16(0x4EF1, header.destination);
delete[] packet_data;
}
int runUnityTests(void)
{
UNITY_BEGIN();
RUN_TEST(test_parse_stop);
RUN_TEST(test_parse_close);
RUN_TEST(test_parse_open);
RUN_TEST(test_parse_rolling_codes);
RUN_TEST(test_broadcast_source_address);
RUN_TEST(test_broadcast_single_group);
RUN_TEST(test_broadcast_multiple_groups);
RUN_TEST(test_unicast_source_address);
RUN_TEST(test_unicast_destination_address);
return UNITY_END();
}
/**
* For Arduino framework
*/
void setup()
{
// Wait ~2 seconds before the Unity test runner
// establishes connection with a board Serial interface
delay(2000);
runUnityTests();
}
void loop() {}
int main(void)
{
return runUnityTests();
}