Skip to content

Commit 880bd98

Browse files
vsky279marcelstoer
authored andcommitted
Somfy/TELIS driver (#1521)
1 parent 101eb20 commit 880bd98

File tree

5 files changed

+439
-0
lines changed

5 files changed

+439
-0
lines changed

app/include/sections.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
#define _SECTIONS_H_
33

44
#define TEXT_SECTION_ATTR __attribute__((section(".text")))
5+
#define RAM_CONST_SECTION_ATTR __attribute((section(".data")))
56

67
#endif

app/include/user_modules.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
//#define LUA_USE_MODULES_RTCTIME
5454
//#define LUA_USE_MODULES_SIGMA_DELTA
5555
//#define LUA_USE_MODULES_SNTP
56+
//#define LUA_USE_MODULES_SOMFY
5657
#define LUA_USE_MODULES_SPI
5758
//#define LUA_USE_MODULES_STRUCT
5859
//#define LUA_USE_MODULES_SWITEC

app/modules/somfy.c

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
// ***************************************************************************
2+
// Somfy module for ESP8266 with NodeMCU
3+
//
4+
// Written by Lukas Voborsky, @voborsky
5+
// based on https://github.com/Nickduino/Somfy_Remote
6+
// Somfy protocol description: https://pushstack.wordpress.com/somfy-rts-protocol/
7+
// and discussion: https://forum.arduino.cc/index.php?topic=208346.0
8+
//
9+
// MIT license, http://opensource.org/licenses/MIT
10+
// ***************************************************************************
11+
12+
//#define NODE_DEBUG
13+
14+
#include "os_type.h"
15+
#include "osapi.h"
16+
#include "sections.h"
17+
18+
#include "module.h"
19+
#include "lauxlib.h"
20+
#include "lmem.h"
21+
#include "platform.h"
22+
#include "hw_timer.h"
23+
#include "user_interface.h"
24+
25+
#define SYMBOL 640 // symbol width in microseconds
26+
#define SOMFY_UP 0x2
27+
#define SOMFY_STOP 0x1
28+
#define SOMFY_DOWN 0x4
29+
#define SOMFY_PROG 0x8
30+
31+
#define DIRECT_WRITE_LOW(pin) (GPIO_OUTPUT_SET(GPIO_ID_PIN(pin_num[pin]), 0))
32+
#define DIRECT_WRITE_HIGH(pin) (GPIO_OUTPUT_SET(GPIO_ID_PIN(pin_num[pin]), 1))
33+
34+
static const os_param_t TIMER_OWNER = 0x736f6d66; // "somf"
35+
static task_handle_t done_taskid;
36+
37+
static uint8_t pin;
38+
static uint8_t frame[7];
39+
static uint8_t sync;
40+
static uint8_t repeat;
41+
42+
//static uint32_t delay[10] = {9415, 89565, 4*SYMBOL, 4*SYMBOL, 4*SYMBOL, 4550, SYMBOL, SYMBOL, SYMBOL, 30415}; // in us
43+
// the `delay` array of constants must be in RAM as it is accessed from the timer interrupt
44+
static const RAM_CONST_SECTION_ATTR uint32_t delay[10] = {US_TO_RTC_TIMER_TICKS(9415), US_TO_RTC_TIMER_TICKS(89565), US_TO_RTC_TIMER_TICKS(4*SYMBOL), US_TO_RTC_TIMER_TICKS(4*SYMBOL), US_TO_RTC_TIMER_TICKS(4*SYMBOL), US_TO_RTC_TIMER_TICKS(4550), US_TO_RTC_TIMER_TICKS(SYMBOL), US_TO_RTC_TIMER_TICKS(SYMBOL), US_TO_RTC_TIMER_TICKS(SYMBOL), US_TO_RTC_TIMER_TICKS(30415)}; // in ticks (no need to recalculate)
45+
46+
static uint8_t repeatindex;
47+
static uint8_t signalindex;
48+
static uint8_t subindex;
49+
static uint8_t bitcondition;
50+
51+
int lua_done_ref; // callback when transmission is done
52+
53+
void buildFrame(uint8_t *frame, uint64_t remote, uint8_t button, uint16_t code) {
54+
// NODE_DBG("remote: %x\n", remote);
55+
// NODE_DBG("button: %x\n", button);
56+
// NODE_DBG("rolling code: %x\n", code);
57+
frame[0] = 0xA7; // Encryption key. Doesn't matter much
58+
frame[1] = button << 4; // Which button did you press? The 4 LSB will be the checksum
59+
frame[2] = code >> 8; // Rolling code (big endian)
60+
frame[3] = code; // Rolling code
61+
frame[4] = remote >> 16; // Remote address
62+
frame[5] = remote >> 8; // Remote address
63+
frame[6] = remote; // Remote address
64+
// frame[7] = 0x80;
65+
// frame[8] = 0x0;
66+
// frame[9] = 0x0;
67+
68+
// NODE_DBG("Frame:\t\t\t%02x %02x %02x %02x %02x %02x %02x\n", frame[0], frame[1], frame[2], frame[3], frame[4], frame[5], frame[6]);
69+
// Checksum calculation: a XOR of all the nibbles
70+
uint8_t checksum = 0;
71+
for(uint8_t i = 0; i < 7; i++) {
72+
checksum = checksum ^ frame[i] ^ (frame[i] >> 4);
73+
}
74+
checksum &= 0b1111; // We keep the last 4 bits only
75+
76+
//Checksum integration
77+
frame[1] |= checksum; // If a XOR of all the nibbles is equal to 0, the blinds will consider the checksum ok.
78+
// NODE_DBG("With checksum:\t%02x %02x %02x %02x %02x %02x %02x\n", frame[0], frame[1], frame[2], frame[3], frame[4], frame[5], frame[6]);
79+
80+
// Obfuscation: a XOR of all the uint8_ts
81+
for(uint8_t i = 1; i < 7; i++) {
82+
frame[i] ^= frame[i-1];
83+
}
84+
// NODE_DBG("Obfuscated:\t\t%02x %02x %02x %02x %02x %02x %02x\n", frame[0], frame[1], frame[2], frame[3], frame[4], frame[5], frame[6]);
85+
}
86+
87+
static void somfy_transmissionDone (task_param_t arg)
88+
{
89+
lua_State *L = lua_getstate();
90+
lua_rawgeti (L, LUA_REGISTRYINDEX, lua_done_ref);
91+
luaL_unref (L, LUA_REGISTRYINDEX, lua_done_ref);
92+
lua_done_ref = LUA_NOREF;
93+
lua_call (L, 0, 0);
94+
}
95+
96+
static void ICACHE_RAM_ATTR sendCommand(os_param_t p) {
97+
(void) p;
98+
// NODE_DBG("%d\t%d\n", signalindex, subindex);
99+
switch (signalindex) {
100+
case 0:
101+
subindex = 0;
102+
if(sync == 2) { // Only with the first frame.
103+
//Wake-up pulse & Silence
104+
DIRECT_WRITE_HIGH(pin);
105+
signalindex++;
106+
// delayMicroseconds(9415);
107+
break;
108+
} else {
109+
signalindex++; signalindex++; //no break means: go directly to step 3
110+
}
111+
case 1:
112+
//Wake-up pulse & Silence
113+
DIRECT_WRITE_LOW(pin);
114+
signalindex++;
115+
// delayMicroseconds(89565);
116+
break;
117+
case 2:
118+
signalindex++;
119+
// no break means go directly to step 3
120+
// a "useless" step to allow repeating the hardware sync w/o the silence after wake-up pulse
121+
case 3:
122+
// Hardware sync: two sync for the first frame, seven for the following ones.
123+
DIRECT_WRITE_HIGH(pin);
124+
signalindex++;
125+
// delayMicroseconds(4*SYMBOL);
126+
break;
127+
case 4:
128+
DIRECT_WRITE_LOW(pin);
129+
subindex++;
130+
if (subindex < sync) {signalindex--;} else {signalindex++;}
131+
// delayMicroseconds(4*SYMBOL);
132+
break;
133+
case 5:
134+
// Software sync
135+
DIRECT_WRITE_HIGH(pin);
136+
signalindex++;
137+
// delayMicroseconds(4550);
138+
break;
139+
case 6:
140+
DIRECT_WRITE_LOW(pin);
141+
signalindex++;
142+
subindex=0;
143+
// delayMicroseconds(SYMBOL);
144+
break;
145+
case 7:
146+
//Data: bits are sent one by one, starting with the MSB.
147+
bitcondition = ((frame[subindex/8] >> (7 - (subindex%8))) & 1) == 1;
148+
if(bitcondition) {
149+
DIRECT_WRITE_LOW(pin);
150+
}
151+
else {
152+
DIRECT_WRITE_HIGH(pin);
153+
}
154+
signalindex++;
155+
// delayMicroseconds(SYMBOL);
156+
break;
157+
case 8:
158+
//Data: bits are sent one by one, starting with the MSB.
159+
if(bitcondition) {
160+
DIRECT_WRITE_HIGH(pin);
161+
}
162+
else {
163+
DIRECT_WRITE_LOW(pin);
164+
}
165+
166+
if (subindex<56) {
167+
subindex++;
168+
signalindex--;
169+
}
170+
else {
171+
signalindex++;
172+
}
173+
// delayMicroseconds(SYMBOL);
174+
break;
175+
case 9:
176+
DIRECT_WRITE_LOW(pin);
177+
signalindex++;
178+
// delayMicroseconds(30415); // Inter-frame silence
179+
break;
180+
case 10:
181+
repeatindex++;
182+
if (repeatindex<repeat) {
183+
DIRECT_WRITE_HIGH(pin); //start repeat from step 3, but don't wait as after step 1
184+
signalindex=4; subindex=0; sync=7;
185+
} else {
186+
platform_hw_timer_close(TIMER_OWNER);
187+
if (lua_done_ref != LUA_NOREF) {
188+
task_post_low (done_taskid, (task_param_t)0);
189+
}
190+
}
191+
break;
192+
}
193+
if (signalindex<10) {
194+
platform_hw_timer_arm_ticks(TIMER_OWNER, delay[signalindex-1]);
195+
}
196+
}
197+
198+
static int somfy_lua_sendcommand(lua_State* L) { // pin, remote, command, rolling_code, num_repeat, callback
199+
if (!lua_isnumber(L, 4)) {
200+
return luaL_error(L, "wrong arg range");
201+
}
202+
pin = luaL_checkinteger(L, 1);
203+
uint64_t remote = luaL_checkinteger(L, 2);
204+
uint8_t cmd = luaL_checkinteger(L, 3);
205+
uint16_t code = luaL_checkinteger(L, 4);
206+
repeat=luaL_optint( L, 5, 2 );
207+
208+
luaL_argcheck(L, platform_gpio_exists(pin), 1, "Invalid pin");
209+
210+
luaL_unref(L, LUA_REGISTRYINDEX, lua_done_ref);
211+
if (!lua_isnoneornil(L, 6)) {
212+
lua_pushvalue(L, 6);
213+
lua_done_ref = luaL_ref(L, LUA_REGISTRYINDEX);
214+
} else {
215+
lua_done_ref = LUA_NOREF;
216+
}
217+
218+
MOD_CHECK_ID(gpio, pin);
219+
platform_gpio_mode(pin, PLATFORM_GPIO_OUTPUT, PLATFORM_GPIO_PULLUP);
220+
221+
buildFrame(frame, remote, cmd, code);
222+
223+
if (!platform_hw_timer_init(TIMER_OWNER, FRC1_SOURCE, TRUE)) {
224+
// Failed to init the timer
225+
luaL_error(L, "Unable to initialize timer");
226+
}
227+
platform_hw_timer_set_func(TIMER_OWNER, sendCommand, 0);
228+
sync=2;
229+
signalindex=0; repeatindex=0;
230+
sendCommand(0);
231+
return 0;
232+
}
233+
234+
static const LUA_REG_TYPE somfy_map[] = {
235+
{ LSTRKEY( "UP" ), LNUMVAL( SOMFY_UP ) },
236+
{ LSTRKEY( "DOWN" ), LNUMVAL( SOMFY_DOWN ) },
237+
{ LSTRKEY( "PROG" ), LNUMVAL( SOMFY_PROG ) },
238+
{ LSTRKEY( "STOP" ), LNUMVAL( SOMFY_STOP ) },
239+
{ LSTRKEY( "sendcommand" ), LFUNCVAL(somfy_lua_sendcommand)},
240+
{ LNILKEY, LNILVAL}
241+
};
242+
243+
int luaopen_somfy( lua_State *L ) {
244+
done_taskid = task_get_id((task_callback_t) somfy_transmissionDone);
245+
return 0;
246+
}
247+
248+
NODEMCU_MODULE(SOMFY, "somfy", somfy_map, luaopen_somfy);

docs/en/modules/somfy.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Somfy module
2+
| Since | Origin / Contributor | Maintainer | Source |
3+
| :----- | :-------------------- | :---------- | :------ |
4+
| 2016-09-27 | [vsky279](https://github.com/vsky279) | [vsky279](https://github.com/vsky279) | [somfy.c](../../../app/modules/somfy.c)|
5+
6+
This module provides a simple interface to control Somfy blinds via an RF transmitter (433.42 MHz). It is based on [Nickduino Somfy Remote Arduino skecth](https://github.com/Nickduino/Somfy_Remote).
7+
8+
The hardware used is the standard 433 MHz RF transmitter. Unfortunately these chips are usually transmitting at he frequency of 433.92MHz so the crystal resonator should be replaced with the 433.42 MHz resonator though some reporting that it is working even with the original crystal.
9+
10+
To understand details of the Somfy protocol please refer to [Somfy RTS protocol](https://pushstack.wordpress.com/somfy-rts-protocol/) and also discussion [here](https://forum.arduino.cc/index.php?topic=208346.0).
11+
12+
The module is using hardware timer so it cannot be used at the same time with other NodeMCU modules using the hardware timer, i.e. `sigma delta`, `pcm`, `perf`, or `pwm` modules.
13+
14+
## somfy.sendcommand()
15+
16+
Builds an frame defined by Somfy protocol and sends it to the RF transmitter.
17+
18+
#### Syntax
19+
`somfy.sendcommand(pin, remote_address, command, rolling_code, repeat_count, call_back)`
20+
21+
#### Parameters
22+
- `pin` GPIO pin the RF transmitter is connected to.
23+
- `remote_address` address of the remote control. The device to be controlled is programmed with the addresses of the remote controls it should listen to.
24+
- `command` command to be transmitted. Can be one of `somfy.SOMFY_UP`, `somfy.SOMFY_DOWN`, `somfy.SOMFY_PROG`, `somfy.SOMFY_STOP`
25+
- `rolling_code` The rolling code is increased every time a button is pressed. The receiver only accepts command if the rolling code is above the last received code and is not to far ahead of the last received code. This window is in the order of a 100 big. The rolling code needs to be stored in the EEPROM (i.e. filesystem) to survive the ESP8266 reset.
26+
- `repeat_count` how many times the command is repeated
27+
- `call_back` a function to be called after the command is transmitted. Allows chaining commands to set the blinds to a defined position.
28+
29+
My original remote is [TELIS 4 MODULIS RTS](https://www.somfy.co.uk/products/1810765/telis-4-modulis-rts). This remote is working with the additional info - additional 56 bits that follow data (shortening the Inter-frame gap). It seems that the scrumbling alhorithm has not been revealed yet.
30+
31+
When I send the `somfy.DOWN` command, repeating the frame twice (which seems to be the standard for a short button press), i.e. `repeat_count` equal to 2, the blinds go only 1 step down. This corresponds to the movement of the wheel on the original remote. The down button on the original remote sends also `somfy.DOWN` command but the additional info is different and this makes the blinds go full down. Fortunately it seems that repeating the frame 16 times makes the blinds go fully down.
32+
33+
#### Returns
34+
nil
35+
36+
#### Example
37+
To start with controlling your Somfy blinds you need to
38+
- Choose an arbitrary remote address (different from your existing remote) - `123` in this example
39+
- Choose a starting point for the rolling code. Any unsigned int works, 1 is a good start
40+
- Long-press the program button of your existing remote control until your blind goes up and down slightly
41+
- execute `somfy.sendcommand(4, 123, somfy.PROG, 1, 2)` - the blinds will react and your ESP8266 remote control is now registered
42+
- running `somfy.sendcommand(4, 123, somfy.DOWN, 2, 16)` - fully closes the blinds
43+
44+
For more elaborated example please refer to [`somfy.lua`](../../../lua_examples/somfy.lua).

0 commit comments

Comments
 (0)