Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Somfy/TELIS driver #1521

Merged
merged 6 commits into from
Oct 13, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/include/sections.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
#define _SECTIONS_H_

#define TEXT_SECTION_ATTR __attribute__((section(".text")))
#define RAM_CONST_SECTION_ATTR __attribute((section(".data")))

#endif
1 change: 1 addition & 0 deletions app/include/user_modules.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
//#define LUA_USE_MODULES_RTCTIME
//#define LUA_USE_MODULES_SIGMA_DELTA
//#define LUA_USE_MODULES_SNTP
//#define LUA_USE_MODULES_SOMFY
#define LUA_USE_MODULES_SPI
//#define LUA_USE_MODULES_STRUCT
//#define LUA_USE_MODULES_SWITEC
Expand Down
248 changes: 248 additions & 0 deletions app/modules/somfy.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
// ***************************************************************************
// Somfy module for ESP8266 with NodeMCU
//
// Written by Lukas Voborsky, @voborsky
// based on https://github.com/Nickduino/Somfy_Remote
// Somfy protocol description: https://pushstack.wordpress.com/somfy-rts-protocol/
// and discussion: https://forum.arduino.cc/index.php?topic=208346.0
//
// MIT license, http://opensource.org/licenses/MIT
// ***************************************************************************

//#define NODE_DEBUG

#include "os_type.h"
#include "osapi.h"
#include "sections.h"

#include "module.h"
#include "lauxlib.h"
#include "lmem.h"
#include "platform.h"
#include "hw_timer.h"
#include "user_interface.h"

#define SYMBOL 640 // symbol width in microseconds
#define SOMFY_UP 0x2
#define SOMFY_STOP 0x1
#define SOMFY_DOWN 0x4
#define SOMFY_PROG 0x8

#define DIRECT_WRITE_LOW(pin) (GPIO_OUTPUT_SET(GPIO_ID_PIN(pin_num[pin]), 0))
#define DIRECT_WRITE_HIGH(pin) (GPIO_OUTPUT_SET(GPIO_ID_PIN(pin_num[pin]), 1))

static const os_param_t TIMER_OWNER = 0x736f6d66; // "somf"
static task_handle_t done_taskid;

static uint8_t pin;
static uint8_t frame[7];
static uint8_t sync;
static uint8_t repeat;

//static uint32_t delay[10] = {9415, 89565, 4*SYMBOL, 4*SYMBOL, 4*SYMBOL, 4550, SYMBOL, SYMBOL, SYMBOL, 30415}; // in us
// the `delay` array of constants must be in RAM as it is accessed from the timer interrupt
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)

static uint8_t repeatindex;
static uint8_t signalindex;
static uint8_t subindex;
static uint8_t bitcondition;

int lua_done_ref; // callback when transmission is done

void buildFrame(uint8_t *frame, uint64_t remote, uint8_t button, uint16_t code) {
// NODE_DBG("remote: %x\n", remote);
// NODE_DBG("button: %x\n", button);
// NODE_DBG("rolling code: %x\n", code);
frame[0] = 0xA7; // Encryption key. Doesn't matter much
frame[1] = button << 4; // Which button did you press? The 4 LSB will be the checksum
frame[2] = code >> 8; // Rolling code (big endian)
frame[3] = code; // Rolling code
frame[4] = remote >> 16; // Remote address
frame[5] = remote >> 8; // Remote address
frame[6] = remote; // Remote address
// frame[7] = 0x80;
// frame[8] = 0x0;
// frame[9] = 0x0;

// 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]);
// Checksum calculation: a XOR of all the nibbles
uint8_t checksum = 0;
for(uint8_t i = 0; i < 7; i++) {
checksum = checksum ^ frame[i] ^ (frame[i] >> 4);
}
checksum &= 0b1111; // We keep the last 4 bits only

//Checksum integration
frame[1] |= checksum; // If a XOR of all the nibbles is equal to 0, the blinds will consider the checksum ok.
// 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]);

// Obfuscation: a XOR of all the uint8_ts
for(uint8_t i = 1; i < 7; i++) {
frame[i] ^= frame[i-1];
}
// 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]);
}

static void somfy_transmissionDone (task_param_t arg)
{
lua_State *L = lua_getstate();
lua_rawgeti (L, LUA_REGISTRYINDEX, lua_done_ref);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a subtle race condition here -- if lua_done_ref is not nil when the transmission ends, the task will be queued. But it might be made null by another call to send command in the mean time.

Worse you need to set the lua_done_ref to LUA_NOREF before doing the lua_call so that if the callback tries to send another command it will all still work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Philip, I am not sure I got the point. If I modify the code as follows

static void somfy_transmissionDone (task_param_t arg)
{
    lua_State *L = lua_getstate();
    lua_rawgeti (L, LUA_REGISTRYINDEX, lua_done_ref);
    lua_done_ref = LUA_NOREF;
    lua_call (L, 0, 0);
    luaL_unref (L, LUA_REGISTRYINDEX, lua_done_ref);
}

Will the luaL_unref correctly unreference the callback function (when lua_done_ref is LUA_NOREF at the moment)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, it will indeed not. What you want to do is luaL_unref() as soon as you've done the lua_rawgeti(), or well, anywhere before you lua_call() at least. So something like

    lua_State *L = lua_getstate();
    lua_rawgeti (L, LUA_REGISTRYINDEX, lua_done_ref);
    luaL_unref (L, LUA_REGISTRYINDEX, lua_done_ref);
    lua_done_ref = LUA_NOREF;
    lua_call (L, 0, 0);

This way, the callback function is able to issue another somfy command successfully (since lua_done_ref is LUA_NOREF at that point), and the callback itself can become available for gc as soon as the lua_call() returns. While the callback is on the stack (after lua_rawgeti()), the stack itself holds a reference on it and thus ensures it doesn't get gc'd.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Johny. It's fixed now.

I guess we have the same issue in gpio.c. According to the same logic it should look like:

static void seroutasync_done (task_param_t arg)
{
  lua_State *L = lua_getstate();
  luaM_freearray(L, serout.delay_table, serout.tablelen, uint32);
  if (serout.lua_done_ref != LUA_REFNIL) { // we're here so serout.lua_done_ref != LUA_NOREF
    lua_rawgeti (L, LUA_REGISTRYINDEX, serout.lua_done_ref);
    luaL_unref (L, LUA_REGISTRYINDEX, serout.lua_done_ref);
    serout.lua_done_ref = LUA_NOREF;
    if (lua_pcall(L, 0, 0, 0)) {
      // Uncaught Error. Print instead of sudden reset
      luaL_error(L, "error: %s", lua_tostring(L, -1));
    }
  }
}

Can it be fixed in this PR or should there be another one?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make it another one, once this is settled.

luaL_unref (L, LUA_REGISTRYINDEX, lua_done_ref);
lua_done_ref = LUA_NOREF;
lua_call (L, 0, 0);
}

static void ICACHE_RAM_ATTR sendCommand(os_param_t p) {
(void) p;
// NODE_DBG("%d\t%d\n", signalindex, subindex);
switch (signalindex) {
case 0:
subindex = 0;
if(sync == 2) { // Only with the first frame.
//Wake-up pulse & Silence
DIRECT_WRITE_HIGH(pin);
signalindex++;
// delayMicroseconds(9415);
break;
} else {
signalindex++; signalindex++; //no break means: go directly to step 3
}
case 1:
//Wake-up pulse & Silence
DIRECT_WRITE_LOW(pin);
signalindex++;
// delayMicroseconds(89565);
break;
case 2:
signalindex++;
// no break means go directly to step 3
// a "useless" step to allow repeating the hardware sync w/o the silence after wake-up pulse
case 3:
// Hardware sync: two sync for the first frame, seven for the following ones.
DIRECT_WRITE_HIGH(pin);
signalindex++;
// delayMicroseconds(4*SYMBOL);
break;
case 4:
DIRECT_WRITE_LOW(pin);
subindex++;
if (subindex < sync) {signalindex--;} else {signalindex++;}
// delayMicroseconds(4*SYMBOL);
break;
case 5:
// Software sync
DIRECT_WRITE_HIGH(pin);
signalindex++;
// delayMicroseconds(4550);
break;
case 6:
DIRECT_WRITE_LOW(pin);
signalindex++;
subindex=0;
// delayMicroseconds(SYMBOL);
break;
case 7:
//Data: bits are sent one by one, starting with the MSB.
bitcondition = ((frame[subindex/8] >> (7 - (subindex%8))) & 1) == 1;
if(bitcondition) {
DIRECT_WRITE_LOW(pin);
}
else {
DIRECT_WRITE_HIGH(pin);
}
signalindex++;
// delayMicroseconds(SYMBOL);
break;
case 8:
//Data: bits are sent one by one, starting with the MSB.
if(bitcondition) {
DIRECT_WRITE_HIGH(pin);
}
else {
DIRECT_WRITE_LOW(pin);
}

if (subindex<56) {
subindex++;
signalindex--;
}
else {
signalindex++;
}
// delayMicroseconds(SYMBOL);
break;
case 9:
DIRECT_WRITE_LOW(pin);
signalindex++;
// delayMicroseconds(30415); // Inter-frame silence
break;
case 10:
repeatindex++;
if (repeatindex<repeat) {
DIRECT_WRITE_HIGH(pin); //start repeat from step 3, but don't wait as after step 1
signalindex=4; subindex=0; sync=7;
} else {
platform_hw_timer_close(TIMER_OWNER);
if (lua_done_ref != LUA_NOREF) {
task_post_low (done_taskid, (task_param_t)0);
}
}
break;
}
if (signalindex<10) {
platform_hw_timer_arm_ticks(TIMER_OWNER, delay[signalindex-1]);
}
}

static int somfy_lua_sendcommand(lua_State* L) { // pin, remote, command, rolling_code, num_repeat, callback
if (!lua_isnumber(L, 4)) {
return luaL_error(L, "wrong arg range");
}
pin = luaL_checkinteger(L, 1);
uint64_t remote = luaL_checkinteger(L, 2);
uint8_t cmd = luaL_checkinteger(L, 3);
uint16_t code = luaL_checkinteger(L, 4);
repeat=luaL_optint( L, 5, 2 );

luaL_argcheck(L, platform_gpio_exists(pin), 1, "Invalid pin");

luaL_unref(L, LUA_REGISTRYINDEX, lua_done_ref);
if (!lua_isnoneornil(L, 6)) {
lua_pushvalue(L, 6);
lua_done_ref = luaL_ref(L, LUA_REGISTRYINDEX);
Copy link
Member

@pjsg pjsg Oct 7, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to do luaL_unref(L, LUA_REGISTRYINDEX, lua_done_ref) first (just in case there is still a reference) (actually in both cases -- so do it a few more lines up). Doing an unref on a LUA_NOREF is perfectly OK

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Le voila. To be fixed in the gpio.c too.

} else {
lua_done_ref = LUA_NOREF;
}

MOD_CHECK_ID(gpio, pin);
platform_gpio_mode(pin, PLATFORM_GPIO_OUTPUT, PLATFORM_GPIO_PULLUP);

buildFrame(frame, remote, cmd, code);

if (!platform_hw_timer_init(TIMER_OWNER, FRC1_SOURCE, TRUE)) {
// Failed to init the timer
luaL_error(L, "Unable to initialize timer");
}
platform_hw_timer_set_func(TIMER_OWNER, sendCommand, 0);
sync=2;
signalindex=0; repeatindex=0;
sendCommand(0);
return 0;
}

static const LUA_REG_TYPE somfy_map[] = {
{ LSTRKEY( "UP" ), LNUMVAL( SOMFY_UP ) },
{ LSTRKEY( "DOWN" ), LNUMVAL( SOMFY_DOWN ) },
{ LSTRKEY( "PROG" ), LNUMVAL( SOMFY_PROG ) },
{ LSTRKEY( "STOP" ), LNUMVAL( SOMFY_STOP ) },
{ LSTRKEY( "sendcommand" ), LFUNCVAL(somfy_lua_sendcommand)},
{ LNILKEY, LNILVAL}
};

int luaopen_somfy( lua_State *L ) {
done_taskid = task_get_id((task_callback_t) somfy_transmissionDone);
return 0;
}

NODEMCU_MODULE(SOMFY, "somfy", somfy_map, luaopen_somfy);
44 changes: 44 additions & 0 deletions docs/en/modules/somfy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Somfy module
| Since | Origin / Contributor | Maintainer | Source |
| :----- | :-------------------- | :---------- | :------ |
| 2016-09-27 | [vsky279](https://github.com/vsky279) | [vsky279](https://github.com/vsky279) | [somfy.c](../../../app/modules/somfy.c)|

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).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a reference to the device that you need -- the transmitter appears to be an odd frequency....

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I've added:

The hardware used is the standard 433 MHz RF transmitter (on eBay usually sold also with the receiver). 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.


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.

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).

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.

## somfy.sendcommand()

Builds an frame defined by Somfy protocol and sends it to the RF transmitter.

#### Syntax
`somfy.sendcommand(pin, remote_address, command, rolling_code, repeat_count, call_back)`

#### Parameters
- `pin` GPIO pin the RF transmitter is connected to.
- `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.
- `command` command to be transmitted. Can be one of `somfy.SOMFY_UP`, `somfy.SOMFY_DOWN`, `somfy.SOMFY_PROG`, `somfy.SOMFY_STOP`
- `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.
- `repeat_count` how many times the command is repeated
- `call_back` a function to be called after the command is transmitted. Allows chaining commands to set the blinds to a defined position.

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.

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.

#### Returns
nil

#### Example
To start with controlling your Somfy blinds you need to
- Choose an arbitrary remote address (different from your existing remote) - `123` in this example
- Choose a starting point for the rolling code. Any unsigned int works, 1 is a good start
- Long-press the program button of your existing remote control until your blind goes up and down slightly
- execute `somfy.sendcommand(4, 123, somfy.PROG, 1, 2)` - the blinds will react and your ESP8266 remote control is now registered
- running `somfy.sendcommand(4, 123, somfy.DOWN, 2, 16)` - fully closes the blinds

For more elaborated example please refer to [`somfy.lua`](../../../lua_examples/somfy.lua).
Loading