From f890a06fc5f11f52718e3b09fe178c7559836270 Mon Sep 17 00:00:00 2001 From: Maxim Prokhorov Date: Sun, 23 Aug 2020 11:48:50 +0300 Subject: [PATCH] rfb refactoring - RF_... -> RFB_... - rework rcswitch integration, support variable length payload - rework rfbridge parser (not tested) - rework settings scanning routine when trying to match rf payload with the relay ID - update build tests --- code/espurna/board.cpp | 4 +- code/espurna/config/arduino.h | 2 +- code/espurna/config/dependencies.h | 5 - code/espurna/config/deprecated.h | 18 + code/espurna/config/general.h | 49 +- code/espurna/config/hardware.h | 18 +- code/espurna/config/types.h | 3 + code/espurna/config/webui.h | 2 +- code/espurna/main.cpp | 2 +- code/espurna/rfbridge.cpp | 1213 ++++++++++++++++++---------- code/espurna/rfbridge.h | 14 +- code/espurna/system.cpp | 2 +- code/espurna/utils.cpp | 2 +- code/espurna/utils.h | 2 +- code/test/build/nondefault.h | 4 +- code/test/build/rfbridge.h | 9 +- 16 files changed, 872 insertions(+), 477 deletions(-) diff --git a/code/espurna/board.cpp b/code/espurna/board.cpp index 18b04952..dc80c936 100644 --- a/code/espurna/board.cpp +++ b/code/espurna/board.cpp @@ -84,8 +84,8 @@ PROGMEM const char espurna_modules[] = #if RFM69_SUPPORT "RFM69 " #endif - #if RF_SUPPORT - "RF " + #if RFB_SUPPORT + "RFB " #endif #if RPN_RULES_SUPPORT "RPN_RULES " diff --git a/code/espurna/config/arduino.h b/code/espurna/config/arduino.h index f37b0f87..755433dd 100644 --- a/code/espurna/config/arduino.h +++ b/code/espurna/config/arduino.h @@ -207,7 +207,7 @@ //#define NTP_SUPPORT 0 //#define OTA_ARDUINOOTA_SUPPORT 1 //#define RFM69_SUPPORT 1 -//#define RF_SUPPORT 1 +//#define RFB_SUPPORT 1 //#define RPN_RULES_SUPPORT 0 //#define SCHEDULER_SUPPORT 0 //#define SPIFFS_SUPPORT 1 diff --git a/code/espurna/config/dependencies.h b/code/espurna/config/dependencies.h index 4d82d48c..de527968 100644 --- a/code/espurna/config/dependencies.h +++ b/code/espurna/config/dependencies.h @@ -49,11 +49,6 @@ #define MQTT_SUPPORT 1 #endif -#if RF_SUPPORT -#undef RELAY_SUPPORT -#define RELAY_SUPPORT 1 -#endif - #if LED_SUPPORT #undef BROKER_SUPPORT #define BROKER_SUPPORT 1 // If LED is enabled enable BROKER to supply status changes diff --git a/code/espurna/config/deprecated.h b/code/espurna/config/deprecated.h index 00327dda..d7950367 100644 --- a/code/espurna/config/deprecated.h +++ b/code/espurna/config/deprecated.h @@ -108,3 +108,21 @@ #warning "WIFI_FALLBACK_APMODE is deprecated! Please use WIFI_AP_MODE instead" #define WIFI_AP_MODE ((1 == WIFI_FALLBACK_APMODE) ? WiFiApMode::Fallback : WiFiApMode::Disabled) #endif + +#ifdef RFB_DIRECT +#warning "RFB_DIRECT is deprecated! Please use RFB_PROVIDER=RFB_PROVIDER_..." +#undef RFB_PROVIDER +#if RFB_DIRECT +#define RFB_PROVIDER RFB_PROVIDER_RCSWITCH +#else +#define RFB_PROVIDER RFB_PROVIDER_EFM8BB1 +#endif +#endif + +// TODO: RF_... -> RFB_... + +#ifdef RF_SUPPORT +#warning "RF_SUPPORT is deprecated! Please use RFB_SUPPORT" +#undef RFB_SUPPORT +#define RFB_SUPPORT RF_SUPPORT +#endif diff --git a/code/espurna/config/general.h b/code/espurna/config/general.h index 62d98d2a..fa770f31 100644 --- a/code/espurna/config/general.h +++ b/code/espurna/config/general.h @@ -1621,40 +1621,26 @@ // ----------------------------------------------------------------------------- -// MQTT RF BRIDGE +// RF BRIDGE // ----------------------------------------------------------------------------- -#ifndef RF_SUPPORT -#define RF_SUPPORT 0 +#ifndef RFB_SUPPORT +#define RFB_SUPPORT 0 #endif -#ifndef RF_DEBOUNCE -#define RF_DEBOUNCE 500 +#ifndef RFB_SEND_TIMES +#define RFB_SEND_TIMES 1 // How many times to send the message #endif -#ifndef RF_LEARN_TIMEOUT -#define RF_LEARN_TIMEOUT 60000 -#endif - -#ifndef RF_SEND_TIMES -#define RF_SEND_TIMES 4 // How many times to send the message -#endif - -#ifndef RF_SEND_DELAY -#define RF_SEND_DELAY 500 // Interval between sendings in ms -#endif - -#ifndef RF_RECEIVE_DELAY -#define RF_RECEIVE_DELAY 500 // Interval between recieving in ms (avoid debouncing) -#endif - -// Enable RCSwitch support +// - RFB_PROVIDER_EFM8BB1 +// Default option for the ITEAD_SONOFF_RFBRIDGE or any custom firmware implementing the protocol +// - RFB_PROVIDER_RCSWITCH // Originally implemented for SONOFF BASIC // https://tinkerman.cat/adding-rf-to-a-non-rf-itead-sonoff/ // Also possible to use with SONOFF RF BRIDGE, thanks to @wildwiz // https://github.com/xoseperez/espurna/wiki/Hardware-Itead-Sonoff-RF-Bridge---Direct-Hack -#ifndef RFB_DIRECT -#define RFB_DIRECT 0 +#ifndef RFB_PROVIDER +#define RFB_PROVIDER RFB_PROVIDER_RCSWITCH #endif #ifndef RFB_RX_PIN @@ -1665,6 +1651,21 @@ #define RFB_TX_PIN GPIO_NONE #endif +#ifndef RFB_LEARN_TIMEOUT +#define RFB_LEARN_TIMEOUT 15000 +#endif + +#ifndef RFB_SEND_DELAY +#define RFB_SEND_DELAY 500 // Interval between sendings in ms +#endif + +#ifndef RFB_RECEIVE_DELAY +#define RFB_RECEIVE_DELAY 500 // Interval between recieving in ms (avoid bouncing) +#endif + +#ifndef RFB_TRANSMIT_TIMES +#define RFB_TRANSMIT_TIMES 5 // How many times RCSwitch will repeat the message +#endif // ----------------------------------------------------------------------------- // IR Bridge diff --git a/code/espurna/config/hardware.h b/code/espurna/config/hardware.h index 10b14786..a2f4c9b6 100644 --- a/code/espurna/config/hardware.h +++ b/code/espurna/config/hardware.h @@ -789,16 +789,16 @@ #define LED1_PIN 13 #define LED1_PIN_INVERSE 1 - #define RF_SUPPORT 1 + #define RFB_SUPPORT 1 - // Only used when RFB_DIRECT=1 + // only used when RFB_PROVIDER is RCSWITCH #define RFB_RX_PIN 4 #define RFB_TX_PIN 5 // When using un-modified harware, ESPurna communicates with the secondary // MCU EFM8BB1 via UART at 19200 bps so we need to change the speed of // the port and remove UART noise on serial line - #if not RFB_DIRECT + #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1 #define SERIAL_BAUDRATE 19200 #define DEBUG_SERIAL_SUPPORT 0 #endif @@ -1373,9 +1373,9 @@ #define LIGHT_CH4_PIN 13 // WHITE // RF - #define RF_SUPPORT 1 - #define RFB_DIRECT 1 - #define RFB_RX_PIN 4 + #define RFB_SUPPORT 1 + #define RFB_PROVIDER RFB_PROVIDER_RCSWITCH + #define RFB_RX_PIN 4 #elif defined(MAGICHOME_ZJ_WFMN_C_11) @@ -2683,9 +2683,9 @@ #endif #define DALLAS_PIN 2 - #define RF_SUPPORT 1 - #define RFB_DIRECT 1 - #define RFB_RX_PIN 14 + #define RFB_SUPPORT 1 + #define RFB_PROVIDER RFB_PROVIDER_RCSWITCH + #define RFB_RX_PIN 14 #ifndef DIGITAL_SUPPORT #define DIGITAL_SUPPORT 1 diff --git a/code/espurna/config/types.h b/code/espurna/config/types.h index 93555758..952f866b 100644 --- a/code/espurna/config/types.h +++ b/code/espurna/config/types.h @@ -103,6 +103,9 @@ #define RELAY_PROVIDER_STM 4 #define RELAY_PROVIDER_MCP23S08 5 +#define RFB_PROVIDER_RCSWITCH 0 +#define RFB_PROVIDER_EFM8BB1 1 + #define RELAY_GROUP_SYNC_NORMAL 0 #define RELAY_GROUP_SYNC_INVERSE 1 #define RELAY_GROUP_SYNC_RECEIVEONLY 2 diff --git a/code/espurna/config/webui.h b/code/espurna/config/webui.h index b01894f7..801dabca 100644 --- a/code/espurna/config/webui.h +++ b/code/espurna/config/webui.h @@ -32,7 +32,7 @@ #endif #endif -#if RF_SUPPORT == 1 +#if RFB_SUPPORT == 1 #ifndef WEBUI_IMAGE #define WEBUI_IMAGE WEBUI_IMAGE_RFBRIDGE #else diff --git a/code/espurna/main.cpp b/code/espurna/main.cpp index 5c3ea914..7c8a6079 100644 --- a/code/espurna/main.cpp +++ b/code/espurna/main.cpp @@ -244,7 +244,7 @@ void setup() { #if I2C_SUPPORT i2cSetup(); #endif - #if RF_SUPPORT + #if RFB_SUPPORT rfbSetup(); #endif #if ALEXA_SUPPORT diff --git a/code/espurna/rfbridge.cpp b/code/espurna/rfbridge.cpp index cc691b43..b8b1a24d 100644 --- a/code/espurna/rfbridge.cpp +++ b/code/espurna/rfbridge.cpp @@ -8,9 +8,7 @@ Copyright (C) 2016-2019 by Xose Pérez #include "rfbridge.h" -#if RF_SUPPORT - -#include +#if RFB_SUPPORT #include "api.h" #include "relay.h" @@ -19,67 +17,300 @@ Copyright (C) 2016-2019 by Xose Pérez #include "ws.h" #include "utils.h" +BrokerBind(RfbridgeBroker); + +#include +#include +#include + // ----------------------------------------------------------------------------- -// DEFINITIONS +// GLOBALS TO THE MODULE // ----------------------------------------------------------------------------- -// EFM8 Protocol - -#define RF_MESSAGE_SIZE 9 -#define RF_MAX_MESSAGE_SIZE (112+4) -#define RF_CODE_START 0xAA -#define RF_CODE_ACK 0xA0 -#define RF_CODE_LEARN 0xA1 -#define RF_CODE_LEARN_KO 0xA2 -#define RF_CODE_LEARN_OK 0xA3 -#define RF_CODE_RFIN 0xA4 -#define RF_CODE_RFOUT 0xA5 -#define RF_CODE_SNIFFING_ON 0xA6 -#define RF_CODE_SNIFFING_OFF 0xA7 -#define RF_CODE_RFOUT_NEW 0xA8 -#define RF_CODE_LEARN_NEW 0xA9 -#define RF_CODE_LEARN_KO_NEW 0xAA -#define RF_CODE_LEARN_OK_NEW 0xAB -#define RF_CODE_RFOUT_BUCKET 0xB0 -#define RF_CODE_STOP 0x55 - -// Settings - -#define RF_MAX_KEY_LENGTH (9) +unsigned char _rfb_repeat = RFB_SEND_TIMES; + +#if RFB_PROVIDER == RFB_PROVIDER_RCSWITCH + +#include +RCSwitch * _rfb_modem; +bool _rfb_receive { false }; +bool _rfb_transmit { false }; + +#else + +constexpr bool _rfb_receive { true }; +constexpr bool _rfb_transmit { true }; + +#endif // ----------------------------------------------------------------------------- -// GLOBALS TO THE MODULE +// MATCH RECEIVED CODE WITH THE SPECIFIC RELAY ID // ----------------------------------------------------------------------------- -unsigned char _uartbuf[RF_MESSAGE_SIZE+3] = {0}; -unsigned char _uartpos = 0; -unsigned char _learnId = 0; +#if RELAY_SUPPORT + +struct RfbRelayMatch { + RfbRelayMatch() = default; + RfbRelayMatch(unsigned char id_, PayloadStatus status_) : + id(id_), + status(status_), + _found(true) + {} + + bool ok() { + return _found; + } + + void reset(unsigned char id_, PayloadStatus status_) { + id = id_; + status = status_; + _found = true; + } + + unsigned char id { 0u }; + PayloadStatus status { PayloadStatus::Unknown }; + + private: -enum class RfbLearn { - Disabled, - On, - Off + bool _found { false }; }; -RfbLearn _learnStatus = RfbLearn::Disabled; -bool _rfbin = false; +struct RfbLearn { + unsigned long ts; + unsigned char id; + bool status; +}; + +static std::unique_ptr _rfb_learn; + +#endif // RELAY_SUPPORT + +// ----------------------------------------------------------------------------- +// EFM8BB1 PROTOCOL PARSING +// ----------------------------------------------------------------------------- + +constexpr uint8_t CodeStart { 0xAAu }; +constexpr uint8_t CodeEnd { 0x55u }; + +constexpr uint8_t CodeAck { 0xA0u }; + +// both stock and https://github.com/Portisch/RF-Bridge-EFM8BB1/ +// sending: +constexpr uint8_t CodeLearn { 0xA1u }; + +// receiving: +constexpr uint8_t CodeLearnOk { 0xA2u }; +constexpr uint8_t CodeLearnTimeout { 0xA3u }; +constexpr uint8_t CodeRecvBasic = { 0xA4u }; +constexpr uint8_t CodeSendBasic = { 0xA5u }; + +// only https://github.com/Portisch/RF-Bridge-EFM8BB1/ +constexpr uint8_t CodeRecvProto { 0xA6u }; +constexpr uint8_t CodeRecvBucket { 0xB1u }; + +struct RfbParser { + using callback_type = void(uint8_t, const std::vector&); + using state_type = void(RfbParser::*)(uint8_t); + + // AA XX ... 55 + // ^~~~~ ~~ - protocol head + tail + // ^~ - message code + // ^~~ - actual payload is always 9 bytes + static constexpr size_t PayloadSizeBasic { 9ul }; + static constexpr size_t MessageSizeBasic { PayloadSizeBasic + 3ul }; + + static constexpr size_t MessageSizeMax { 112ul }; + + RfbParser() = delete; + RfbParser(const RfbParser&) = delete; + + RfbParser(callback_type* callback) : + _callback(callback) + {} + + RfbParser(RfbParser&&) = default; + + void stop(uint8_t c) { + } + + void start(uint8_t c) { + switch (c) { + case CodeStart: + _state = &RfbParser::read_code; + break; + default: + _state = &RfbParser::stop; + break; + } + } + + void read_code(uint8_t c) { + _payload_code = c; + switch (c) { + // Generic ACK signal. We *expect* this after our requests + case CodeAck: + // *Expect* any code within a certain window. + // Only matters to us, does not really do anything but help us to signal that the next code needs to be recorded + case CodeLearnTimeout: + _state = &RfbParser::read_end; + break; + // both stock and https://github.com/Portisch/RF-Bridge-EFM8BB1/ + // receive 9 bytes, where first 3 2-byte tuples are timings + // and the last 3 bytes are the actual payload + case CodeLearnOk: + case CodeRecvBasic: + _payload_length = PayloadSizeBasic; + _state = &RfbParser::read_until_length; + break; + // specific to the https://github.com/Portisch/RF-Bridge-EFM8BB1/ + // receive N bytes, where the 1st byte is the protocol ID and the next N-1 bytes are the payload + case CodeRecvProto: + _state = &RfbParser::read_length; + break; + // unlike CodeRecvProto, we don't have any length byte here :/ for some reason, it is there only when sending + // just bail out when we find CodeEnd + // (TODO: is number of buckets somehow convertible to the 'expected' size?) + case CodeRecvBucket: + _state = &RfbParser::read_length; + break; + default: + _state = &RfbParser::stop; + break; + } + } + + void read_until_end(uint8_t c) { + _payload.push_back(c); + if (CodeEnd == c) { + read_end(c); + } + } + + void read_end(uint8_t c) { + _state = (CodeEnd == c) + ? &RfbParser::start + : &RfbParser::stop; + if (_state != &RfbParser::stop) { + _callback(_payload_code, _payload); + _payload.clear(); + return; + } + } + + void read_until_length(uint8_t c) { + _payload.push_back(c); + if ((_payload_offset + _payload_length) == _payload.size()) { + switch (_payload_code) { + case CodeRecvBasic: + case CodeRecvProto: + _state = &RfbParser::read_end; + break; + case CodeRecvBucket: + _state = &RfbParser::read_until_end; + break; + default: + break; + } + + _payload_length = 0u; + } + } + + void read_length(uint8_t c) { + switch (_payload_code) { + case CodeRecvProto: + _payload_length = c; + break; + case CodeRecvBucket: + _payload_length = c * 2; + break; + default: + _state = &RfbParser::stop; + return; + } -struct rfb_message_t { - uint8_t code[RF_MESSAGE_SIZE]; - uint8_t times; + _payload.push_back(c); + _payload_offset = _payload.size(); + _state = &RfbParser::read_until_length; + } + + bool loop(uint8_t c) { + (this->*_state)(c); + return (_state != &RfbParser::stop); + } + + void reset() { + _payload.clear(); + _payload_code = 0u; + _state = &RfbParser::start; + } + + void reserve(size_t size) { + _payload.reserve(size); + } + + private: + + callback_type* _callback { nullptr }; + state_type _state { &RfbParser::start }; + + std::vector _payload; + + size_t _payload_length { 0ul }; + size_t _payload_offset { 0ul }; + + uint8_t _payload_code { 0ul }; }; -static std::queue _rfb_message_queue; -#if RFB_DIRECT - RCSwitch * _rfModem; -#endif +// ----------------------------------------------------------------------------- +// MESSAGE SENDER +// +// Depends on the selected provider. While we do serialize RCSwitch results, +// we don't want to pass around such byte-array everywhere since we already +// know all of the required data members and can prepare a basic POD struct +// ----------------------------------------------------------------------------- + +#if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1 + +struct RfbMessage { + RfbMessage(const RfbMessage&) = default; + RfbMessage(RfbMessage&&) = default; + + explicit RfbMessage(uint8_t* ptr, size_t size, unsigned char repeats_) : + repeats(repeats_) + { + std::copy(ptr, ptr + size, code); + } + + uint8_t code[RfbParser::PayloadSizeBasic] { 0u }; + uint8_t repeats { 1u }; +}; + +#elif RFB_PROVIDER == RFB_PROVIDER_RCSWITCH + +struct RfbMessage { + using code_type = decltype(std::declval().getReceivedValue()); + + static constexpr size_t CodeSize = sizeof(code_type); + static constexpr size_t BufferSize = CodeSize + 4; + + uint8_t protocol; + uint16_t timing; + uint8_t bits; + code_type code; + uint8_t repeats; +}; + +#endif // RFB_PROVIDER == RFB_PROVIDER_EFM8BB1 + +static std::list _rfb_message_queue; -bool _rfb_receive = false; -bool _rfb_transmit = false; -unsigned char _rfb_repeat = RF_SEND_TIMES; +void _rfbLearnImpl(); +void _rfbReceiveImpl(); +void _rfbSendImpl(const RfbMessage& message); // ----------------------------------------------------------------------------- -// PRIVATES +// WEBUI INTEGRATION // ----------------------------------------------------------------------------- #if WEB_SUPPORT @@ -103,7 +334,7 @@ void _rfbWebSocketOnVisible(JsonObject& root) { } void _rfbWebSocketOnConnected(JsonObject& root) { - root["rfbRepeat"] = getSetting("rfbRepeat", RF_SEND_TIMES); + root["rfbRepeat"] = getSetting("rfbRepeat", RFB_SEND_TIMES); root["rfbCount"] = relayCount(); #if RFB_DIRECT root["rfbdirectVisible"] = 1; @@ -128,368 +359,510 @@ void _rfbWebSocketOnData(JsonObject& root) { #endif // WEB_SUPPORT -void _rfbAckImpl(); -void _rfbLearnImpl(); -void _rfbSendImpl(uint8_t * message); -void _rfbReceiveImpl(); +// ----------------------------------------------------------------------------- +// RELAY <-> CODE MATCHING +// ----------------------------------------------------------------------------- -bool _rfbMatch(char* code, unsigned char& relayID, unsigned char& value, char* buffer = NULL) { +#if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1 - if (strlen(code) != 18) return false; +// we only care about last 6 chars (3 bytes in hex), +// since in 'default' mode rfbridge only handles a single protocol +bool _rfbCompare(const char* lhs, const char* rhs, size_t length) { + return (0 == std::memcmp((lhs + length - 6), (rhs + length - 6), 6)); +} - bool found = false; - String compareto = String(&code[12]); - compareto.toUpperCase(); - DEBUG_MSG_P(PSTR("[RF] Trying to match code %s\n"), compareto.c_str()); +#elif RFB_PROVIDER == RFB_PROVIDER_RCSWITCH - for (unsigned char i=0; i std::numeric_limits::max() || id >= relayCount()) { + return; } - } + // when we see the same id twice, we match the opposite statuses + if (matched.ok() && (id == matched.id)) { + matched.status = PayloadStatus::Toggle; + return; + } - if ((action == RF_CODE_LEARN_OK) && (_learnStatus != RfbLearn::Disabled)) { + matched.reset(matched.ok() + ? std::min(static_cast(id), matched.id) + : static_cast(id), + status + ); + }); - DEBUG_MSG_P(PSTR("[RF] Learn success\n")); - rfbStore(_learnId, (_learnStatus == RfbLearn::On), buffer); - // Websocket update - #if WEB_SUPPORT - wsPost([](JsonObject& root) { - _rfbWebSocketSendCodeArray(root, _learnId, 1); - }); - #endif + return matched; - } +} - if (action == RF_CODE_RFIN) { +void _rfbLearnFromString(std::unique_ptr& learn, const char* buffer) { + if (!learn) return; - /* Look for the code, possibly replacing the code with the exact learned one on match - * we want to do this on learn too to be sure that the learned code is the same if it - * is equivalent - */ - unsigned char id; - unsigned char status; - bool matched = _rfbMatch(buffer, id, status, buffer); + DEBUG_MSG_P(PSTR("[RF] Learned %s for relay ID %u\n"), buffer, learn->id); + rfbStore(learn->id, learn->status, buffer); - if (matched) { - DEBUG_MSG_P(PSTR("[RF] Matched message '%s'\n"), buffer); - _rfbin = true; - if (status == 2) { - relayToggle(id); - } else { - relayStatus(id, status == 1); - } - } + // Websocket update needs to happen right here, since the only time + // we send these in bulk is at the very start of the connection +#if WEB_SUPPORT + auto id = learn->id; + wsPost([id](JsonObject& root) { + _rfbWebSocketSendCodeArray(root, id, 1); + }); +#endif - #if MQTT_SUPPORT - mqttSend(MQTT_TOPIC_RFIN, buffer, false, false); - #endif + learn.reset(nullptr); +} +bool _rfbRelayHandler(const char* buffer, bool locked = false) { + _rfb_status_lock = locked; + + bool result { false }; + + auto match = _rfbMatch(buffer); + if (match.ok()) { + DEBUG_MSG_P(PSTR("[RF] Matched with the relay ID %u\n"), match.id); + + switch (match.status) { + case PayloadStatus::On: + case PayloadStatus::Off: + relayStatus(match.id, (PayloadStatus::On == match.status)); + result = true; + break; + case PayloadStatus::Toggle: + relayToggle(match.id); + result = true; + case PayloadStatus::Unknown: + break; + } } + _rfb_status_lock = false; + + return result; } +#endif // RELAY_SUPPORT -// +// ----------------------------------------------------------------------------- // RF handler implementations -// +// ----------------------------------------------------------------------------- -#if !RFB_DIRECT // Default for ITEAD SONOFF RFBRIDGE +#if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1 -void _rfbSendRaw(const uint8_t *message, unsigned char size) { +void _rfbEnqueue(uint8_t* code, size_t size, unsigned char times) { + if (!_rfb_transmit) return; + _rfb_message_queue.push_back(RfbMessage(code, size, times)); +} + +void _rfbEnqueue(const char* code, unsigned char times) { + uint8_t buffer[RfbParser::PayloadSizeBasic] { 0u }; + if (hexDecode(code, strlen(code), buffer, sizeof(buffer))) { + _rfbEnqueue(buffer, sizeof(buffer), times); + } else { + DEBUG_MSG_P(PSTR("[RF] Message size exceeds the available buffer (%u vs. %u)\n"), strlen(code), sizeof(buffer)); + } +} + +void _rfbSendRaw(const uint8_t* message, unsigned char size) { Serial.write(message, size); } void _rfbAckImpl() { + static uint8_t message[3] { + CodeStart, CodeAck, CodeEnd + }; + DEBUG_MSG_P(PSTR("[RF] Sending ACK\n")); - Serial.println(); - Serial.write(RF_CODE_START); - Serial.write(RF_CODE_ACK); - Serial.write(RF_CODE_STOP); + Serial.write(message, sizeof(message)); Serial.flush(); - Serial.println(); } void _rfbLearnImpl() { + static uint8_t message[3] { + CodeStart, CodeLearn, CodeEnd + }; + DEBUG_MSG_P(PSTR("[RF] Sending LEARN\n")); - Serial.println(); - Serial.write(RF_CODE_START); - Serial.write(RF_CODE_LEARN); - Serial.write(RF_CODE_STOP); + Serial.write(message, sizeof(message)); Serial.flush(); - Serial.println(); } -void _rfbSendImpl(uint8_t * message) { - Serial.println(); - Serial.write(RF_CODE_START); - Serial.write(RF_CODE_RFOUT); - _rfbSendRaw(message, RF_MESSAGE_SIZE); - Serial.write(RF_CODE_STOP); +void _rfbSendImpl(const RfbMessage& message) { + Serial.write(CodeStart); + Serial.write(CodeSendBasic); + _rfbSendRaw(message.code, sizeof(message.code)); + Serial.write(CodeEnd); Serial.flush(); - Serial.println(); } -void _rfbReceiveImpl() { - - static bool receiving = false; - - while (Serial.available()) { +void _rfbParse(uint8_t code, const std::vector& payload) { + switch (code) { + case CodeAck: + DEBUG_MSG_P(PSTR("[RF] Received ACK\n")); + break; + case CodeLearnTimeout: + _rfbAckImpl(); +#if RELAY_SUPPORT + _rfb_learn.reset(nullptr); +#endif + DEBUG_MSG_P(PSTR("[RF] Learn timeout\n")); + break; + case CodeLearnOk: + case CodeRecvBasic: { + _rfbAckImpl(); + if (payload.size() != RfbParser::PayloadSizeBasic) { + break; + } - yield(); - uint8_t c = Serial.read(); - //DEBUG_MSG_P(PSTR("[RF] Received 0x%02X\n"), c); + char buffer[(RfbParser::PayloadSizeBasic * 2) + 1] = {0}; + if (!hexEncode(payload.data(), payload.size(), buffer, sizeof(buffer))) { + DEBUG_MSG_P(PSTR("[RF] Received code: %s\n"), buffer); - if (receiving) { - if (c == RF_CODE_STOP && (_uartpos == 1 || _uartpos == RF_MESSAGE_SIZE + 1)) { - _rfbDecode(); - receiving = false; - } else if (_uartpos <= RF_MESSAGE_SIZE) { - _uartbuf[_uartpos++] = c; +#if RELAY_SUPPORT + if (CodeLearnOk == code) { + _rfbLearnFromString(_rfb_learn, buffer); } else { - // wrong message, should have received a RF_CODE_STOP - receiving = false; + _rfbRelayHandler(buffer); } - } else if (c == RF_CODE_START) { - _uartpos = 0; - receiving = true; +#endif + +#if MQTT_SUPPORT + mqttSend(MQTT_TOPIC_RFIN, buffer, false, false); +#endif + +#if BROKER_SUPPORT + RfbridgeBroker::Publish(buffer + 6); +#endif + } + break; + } + case CodeRecvProto: + case CodeRecvBucket: + _rfbAckImpl(); + DEBUG_MSG_P(PSTR("[RF] CANNOT HANDLE 0x%02X, NOT IMPLEMENTED\n"), code); + break; + } +} + +static RfbParser _rfb_parser(_rfbParse); + +void _rfbReceiveImpl() { + + while (Serial.available()) { + auto c = Serial.read(); + if (c < 0) { + continue; } + if (!_rfb_parser.loop(static_cast(c))) { + _rfb_parser.reset(); + } } } -void _rfbParseRaw(char * raw) { - int rawlen = strlen(raw); - if (rawlen > (RF_MAX_MESSAGE_SIZE * 2)) return; +// note that we don't care about queue here, just dump raw message as-is +void _rfbSendRawFromPayload(const char * raw) { + auto rawlen = strlen(raw); + if (rawlen > (RfbParser::MessageSizeMax * 2)) return; if ((rawlen < 2) || (rawlen & 1)) return; - DEBUG_MSG_P(PSTR("[RF] Sending RAW MESSAGE '%s'\n"), raw); + DEBUG_MSG_P(PSTR("[RF] Sending RAW MESSAGE \"%s\"\n"), raw); - uint8_t message[RF_MAX_MESSAGE_SIZE]; - size_t bytes = hexDecode(raw, (size_t)rawlen, message, sizeof(message)); - _rfbSendRaw(message, bytes); + size_t bytes = 0; + uint8_t message[RfbParser::MessageSizeMax] { 0u }; + if ((bytes = hexDecode(raw, rawlen, message, sizeof(message)))) { + if (message[0] != CodeStart) return; + if (message[bytes - 1] != CodeEnd) return; + _rfbSendRaw(message, bytes); + } } -#else // RFB_DIRECT +#elif RFB_PROVIDER == RFB_PROVIDER_RCSWITCH + +namespace { + +size_t _rfb_bits_for_bytes(size_t bits) { + decltype(bits) bytes = 0; + decltype(bits) need = 0; -void _rfbAckImpl() {} + while (need < bits) { + need += 8u; + bytes += 1u; + } + + return bytes; +} + +// TODO: 'Code' long unsigned int != uint32_t, thus the specialization +static_assert(sizeof(uint32_t) == sizeof(long unsigned int), ""); + +template +T _rfb_bswap(T value); + +template <> +[[gnu::unused]] uint32_t _rfb_bswap(uint32_t value) { + return __builtin_bswap32(value); +} + +template <> +[[gnu::unused]] long unsigned int _rfb_bswap(long unsigned int value) { + return __builtin_bswap32(value); +} + +template <> +[[gnu::unused]] uint64_t _rfb_bswap(uint64_t value) { + return __builtin_bswap64(value); +} + +} + +void _rfbEnqueue(uint8_t protocol, uint16_t timing, uint8_t bits, RfbMessage::code_type code, unsigned char repeats) { + if (!_rfb_transmit) return; + _rfb_message_queue.push_back(RfbMessage{protocol, timing, bits, code, repeats}); +} + +void _rfbEnqueue(const char* code, unsigned char times) { + uint8_t buffer[RfbMessage::BufferSize] { 0u }; + if (hexDecode(code, strlen(code), buffer, sizeof(buffer))) { + RfbMessage::code_type code; + std::memcpy(&code, &buffer[5], _rfb_bits_for_bytes(buffer[4])); + code = _rfb_bswap(code); + + _rfbEnqueue(buffer[1], (buffer[3] << 8) | buffer[2], buffer[4], code, times); + } else { + DEBUG_MSG_P(PSTR("[RF] Message size exceeds the available buffer (%u vs. %u)\n"), strlen(code), sizeof(buffer)); + } +} void _rfbLearnImpl() { DEBUG_MSG_P(PSTR("[RF] Entering LEARN mode\n")); } -void _rfbSendImpl(uint8_t * message) { +void _rfbSendImpl(const RfbMessage& message) { if (!_rfb_transmit) return; - unsigned int protocol = message[1]; - unsigned int timing = - (message[2] << 8) | - (message[3] << 0) ; - unsigned int bitlength = message[4]; - unsigned long rf_code = - (message[5] << 24) | - (message[6] << 16) | - (message[7] << 8) | - (message[8] << 0) ; - _rfModem->setProtocol(protocol); - if (timing > 0) { - _rfModem->setPulseLength(timing); + // TODO: note that this seems to be setting global setting + // if code for some reason forgets this, we end up with the previous value + if (message.timing) { + _rfb_modem->setPulseLength(message.timing); } - _rfModem->send(rf_code, bitlength); - _rfModem->resetAvailable(); + + _rfb_modem->send(message.code, message.bits); + _rfb_modem->resetAvailable(); } -void _rfbReceiveImpl() { +// Try to mimic the basic RF message format. although, we might have different size of the code itself +// Skip leading zeroes and only keep the useful data +// +// TODO: 'timing' value shooould be relatively small, +// since it's original intent was to be used with 16bit ints +// TODO: both 'protocol' and 'bitlength' fit in a byte, despite being declared as 'unsigned int' + +template +size_t _rfbModemPack(unsigned int protocol, unsigned int timing, unsigned int bits, RfbMessage::code_type code, uint8_t(&out)[Size]) { + static_assert((sizeof(decltype(code)) == 4) || (sizeof(decltype(code)) == 8), ""); + + size_t index = 0; + out[index++] = 0xC0; + out[index++] = static_cast(protocol); + out[index++] = static_cast(timing >> 8); + out[index++] = static_cast(timing); + out[index++] = static_cast(bits); + + auto bytes = _rfb_bits_for_bytes(bits); + if (bytes > (Size - index)) { + return 0; + } - if (!_rfb_receive) return; + // manually overload each bswap, since we can't use ternary here + // (and if constexpr is only available in Arduino Core 3.0.0) + decltype(code) swapped = _rfb_bswap(code); - static long learn_start = 0; - if ((_learnStatus == RfbLearn::Disabled) && learn_start) { - learn_start = 0; - } - if (_learnStatus != RfbLearn::Disabled) { - if (!learn_start) { - DEBUG_MSG_P(PSTR("[RF] Arming learn timeout\n")); - learn_start = millis(); - } - if (learn_start > 0 && millis() - learn_start > RF_LEARN_TIMEOUT) { - DEBUG_MSG_P(PSTR("[RF] Learn timeout triggered\n")); - memset(_uartbuf, 0, sizeof(_uartbuf)); - _uartbuf[0] = RF_CODE_LEARN_KO; - _rfbDecode(); - _learnStatus = RfbLearn::Disabled; - } - } - - if (_rfModem->available()) { - static unsigned long last = 0; - if (millis() - last > RF_DEBOUNCE) { - last = millis(); - unsigned long rf_code = _rfModem->getReceivedValue(); - if ( rf_code > 0) { - DEBUG_MSG_P(PSTR("[RF] Received code: %08X\n"), rf_code); - unsigned int timing = _rfModem->getReceivedDelay(); - memset(_uartbuf, 0, sizeof(_uartbuf)); - unsigned char *msgbuf = _uartbuf + 1; - _uartbuf[0] = (_learnStatus != RfbLearn::Disabled) ? RF_CODE_LEARN_OK: RF_CODE_RFIN; - msgbuf[0] = 0xC0; - msgbuf[1] = _rfModem->getReceivedProtocol(); - msgbuf[2] = timing >> 8; - msgbuf[3] = timing >> 0; - msgbuf[4] = _rfModem->getReceivedBitlength(); - msgbuf[5] = rf_code >> 24; - msgbuf[6] = rf_code >> 16; - msgbuf[7] = rf_code >> 8; - msgbuf[8] = rf_code >> 0; - _rfbDecode(); - _learnStatus = RfbLearn::Disabled; - } - } - _rfModem->resetAvailable(); + uint8_t raw[sizeof(swapped)]; + std::memcpy(raw, &swapped, sizeof(raw)); + + while (bytes) { + out[index++] = raw[sizeof(raw) - (bytes--)]; } - yield(); + return index; +} + +void _rfbLearnFromReceived(std::unique_ptr& learn, const char* buffer) { + if (millis() - learn->ts > RFB_LEARN_TIMEOUT) { + DEBUG_MSG_P(PSTR("[RF] Learn timeout\n")); + learn.reset(nullptr); + return; + } + _rfbLearnFromString(learn, buffer); } -#endif // RFB_DIRECT +void _rfbReceiveImpl() { -void _rfbEnqueue(uint8_t * code, unsigned char times) { + if (!_rfb_receive) return; + if (!_rfb_modem->available()) return; - if (!_rfb_transmit) return; + static unsigned long last = 0; + if (millis() - last < RFB_RECEIVE_DELAY) return; - // rc-switch will repeat on its own - #if RFB_DIRECT - times = 1; - #endif + last = millis(); - char buffer[RF_MESSAGE_SIZE]; - hexEncode(code, RF_MESSAGE_SIZE, buffer, sizeof(buffer)); - DEBUG_MSG_P(PSTR("[RF] Enqueuing MESSAGE '%s' %d time(s)\n"), buffer, times); + auto rf_code = _rfb_modem->getReceivedValue(); + if (!rf_code) return; + + uint8_t message[RfbMessage::BufferSize]; + auto real_msgsize = _rfbModemPack( + _rfb_modem->getReceivedProtocol(), + _rfb_modem->getReceivedDelay(), + _rfb_modem->getReceivedBitlength(), + rf_code, + message + ); + + char buffer[(sizeof(message) * 2) + 1] = {0}; + if (hexEncode(message, real_msgsize, buffer, sizeof(buffer))) { + DEBUG_MSG_P(PSTR("[RF] Received code: %s\n"), buffer); + +#if RELAY_SUPPORT + if (_rfb_learn) { + _rfbLearnFromReceived(_rfb_learn, buffer); + } else { + _rfbRelayHandler(buffer); + } +#endif - rfb_message_t message; - memcpy(message.code, code, RF_MESSAGE_SIZE); - message.times = times; - _rfb_message_queue.push(message); +#if MQTT_SUPPORT + mqttSend(MQTT_TOPIC_RFIN, buffer, false, false); +#endif + +#if BROKER_SUPPORT + RfbridgeBroker::Publish(message[1], buffer + 10); +#endif + } + + _rfb_modem->resetAvailable(); } +#endif // RFB_PROVIDER == ... + void _rfbSendQueued() { if (!_rfb_transmit) return; - - // Check if there is something in the queue if (_rfb_message_queue.empty()) return; static unsigned long last = 0; - if (millis() - last < RF_SEND_DELAY) return; + if (millis() - last < RFB_SEND_DELAY) return; last = millis(); - // Pop the first message and send it - rfb_message_t message = _rfb_message_queue.front(); - _rfb_message_queue.pop(); - _rfbSendImpl(message.code); + auto message = _rfb_message_queue.front(); + _rfb_message_queue.pop_front(); + + _rfbSendImpl(message); - // Push it to the stack again if we need to send it more than once - if (message.times > 1) { - message.times = message.times - 1; - _rfb_message_queue.push(message); + // Sometimes we really want to repeat the message, not only to rely on built-in transfer repeat + if (message.repeats > 1) { + message.repeats -= 1; + _rfb_message_queue.push_back(std::move(message)); } yield(); } -bool _rfbCompare(const char * code1, const char * code2) { - return strcmp(&code1[12], &code2[12]) == 0; -} - -bool _rfbSameOnOff(unsigned char id) { - return _rfbCompare(rfbRetrieve(id, true).c_str(), rfbRetrieve(id, false).c_str()); -} - -void _rfbParseCode(char * code) { +// Check if the payload looks like a HEX code (plus comma, specifying the 'times' arg for the queue) +void _rfbSendFromPayload(const char * payload) { - // The payload may be a code in HEX format ([0-9A-Z]{18}) or - // the code comma the number of times to transmit it. - char * tok = strtok(code, ","); + size_t times { 1ul }; + size_t len { strlen(payload) }; - // Check if a switch is linked to that message - unsigned char id; - unsigned char status = 0; - if (_rfbMatch(tok, id, status)) { - if (status == 2) { - relayToggle(id); - } else { - relayStatus(id, status == 1); + const char* sep { strchr(payload, ',') }; + if (sep && (*(sep + 1) != '\0')) { + char *endptr = nullptr; + times = strtoul(sep, &endptr, 10); + if (endptr == payload || endptr[0] != '\0') { + return; } - return; + len -= strlen(sep); } - uint8_t message[RF_MESSAGE_SIZE]; - if (hexDecode(tok, strlen(tok), message, sizeof(message))) { - tok = strtok(nullptr, ","); - uint8_t times = (tok != nullptr) ? atoi(tok) : 1; - _rfbEnqueue(message, times); + if (!len || (len & 1)) { + return; } + // We postpone the actual sending until the loop, as we may've been called from MQTT or HTTP API + // RFB_PROVIDER implementation should select the appropriate de-serialization function + _rfbEnqueue(payload, times); + } -void _rfbLearnFromPayload(const char* payload) { +void _rfbLearnStartFromPayload(const char* payload) { // The payload must be the `relayID,mode` (where mode is either 0 or 1) const char* sep = strchr(payload, ','); - if (NULL == sep) { + if (nullptr == sep) { return; } @@ -500,20 +873,21 @@ void _rfbLearnFromPayload(const char* payload) { } std::copy(payload, sep, relay); - if (!isNumber(relay)) { + + char *endptr = nullptr; + const auto id = strtoul(relay, &endptr, 10); + if (endptr == &relay[0] || endptr[0] != '\0') { return; } - _learnId = atoi(relay); - if (_learnId >= relayCount()) { - DEBUG_MSG_P(PSTR("[RF] Wrong learnID (%d)\n"), _learnId); + if (id >= relayCount()) { + DEBUG_MSG_P(PSTR("[RF] Invalid relay ID (%u)\n"), id); return; } ++sep; if ((*sep == '0') || (*sep == '1')) { - _learnStatus = (*sep != '0') ? RfbLearn::On : RfbLearn::Off; - _rfbLearnImpl(); + rfbLearn(id, (*sep != '0')); } } @@ -531,9 +905,9 @@ void _rfbMqttCallback(unsigned int type, const char * topic, char * payload) { mqttSubscribe(MQTT_TOPIC_RFOUT); } - #if !RFB_DIRECT - mqttSubscribe(MQTT_TOPIC_RFRAW); - #endif +#if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1 + mqttSubscribe(MQTT_TOPIC_RFRAW); +#endif } @@ -542,21 +916,32 @@ void _rfbMqttCallback(unsigned int type, const char * topic, char * payload) { String t = mqttMagnitude((char *) topic); if (t.startsWith(MQTT_TOPIC_RFLEARN)) { - _rfbLearnFromPayload(payload); + _rfbLearnStartFromPayload(payload); return; } if (t.equals(MQTT_TOPIC_RFOUT)) { - _rfbParseCode(payload); +#if RELAY_SUPPORT + // we *sometimes* want to check the code against available rfbON / rfbOFF + // e.g. in case we want to control some external device and have an external remote. + // - when remote press happens, relays stay in sync when we receive the code via the processing loop + // - when we send the code here, we never register it as *sent*,, thus relays need to be made in sync manually + if (!_rfbRelayHandler(payload, /* locked = */ true)) { +#endif + _rfbSendFromPayload(payload); +#if RELAY_SUPPORT + } +#endif return; } - #if !RFB_DIRECT - if (t.equals(MQTT_TOPIC_RFRAW)) { - _rfbParseRaw(payload); - return; - } - #endif +#if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1 + if (t.equals(MQTT_TOPIC_RFRAW)) { + // in case this is RAW message, we should not match anything and just send it as-is to the serial + _rfbSendRawFromPayload(payload); + return; + } +#endif } @@ -574,35 +959,35 @@ void _rfbApiSetup() { MQTT_TOPIC_RFOUT, Api::Type::Basic, ApiUnusedArg, apiOk, // just a stub, nothing to return [](const Api&, ApiBuffer& buffer) { - _rfbParseCode(buffer.data); + _rfbSendFromPayload(buffer.data); } }); apiRegister({ MQTT_TOPIC_RFLEARN, Api::Type::Basic, ApiUnusedArg, [](const Api&, ApiBuffer& buffer) { - if (_learnStatus == RfbLearn::Disabled) { - snprintf_P(buffer.data, buffer.size, PSTR("waiting")); - } else { - snprintf_P(buffer.data, buffer.size, PSTR("id:%u,status:%c"), - _learnId, (_learnStatus == RfbLearn::On) ? 'y' : 'n' + if (_rfb_learn) { + snprintf_P(buffer.data, buffer.size, PSTR("learning id:%u,status:%c"), + _rfb_learn->id, _rfb_learn->status ? 't' : 'f' ); + } else { + snprintf_P(buffer.data, buffer.size, PSTR("waiting")); } }, [](const Api&, ApiBuffer& buffer) { - _rfbLearnFromPayload(buffer.data); + _rfbLearnStartFromPayload(buffer.data); } }); - #if !RFB_DIRECT - apiRegister({ - MQTT_TOPIC_RFRAW, Api::Type::Basic, ApiUnusedArg, - apiOk, // just a stub, nothing to return - [](const Api&, ApiBuffer& buffer) { - _rfbParseRaw(buffer.data); - } - }); - #endif +#if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1 + apiRegister({ + MQTT_TOPIC_RFRAW, Api::Type::Basic, ApiUnusedArg, + apiOk, // just a stub, nothing to return + [](const Api&, ApiBuffer& buffer) { + _rfbSendRawFromPayload(buffer.data); + } + }); +#endif } @@ -612,58 +997,58 @@ void _rfbApiSetup() { void _rfbInitCommands() { - terminalRegisterCommand(F("LEARN"), [](const terminal::CommandContext& ctx) { + terminalRegisterCommand(F("RFB.LEARN"), [](const terminal::CommandContext& ctx) { if (ctx.argc != 3) { - terminalError(F("Wrong arguments")); + terminalError(ctx, F("RFB.LEARN ")); return; } - // 1st argument is relayID int id = ctx.argv[1].toInt(); if (id >= relayCount()) { - DEBUG_MSG_P(PSTR("-ERROR: Wrong relayID (%d)\n"), id); + terminalError(ctx, F("Invalid relay ID")); return; } - // 2nd argument is status rfbLearn(id, (ctx.argv[2].toInt()) == 1); - terminalOK(); + terminalOK(ctx); }); - terminalRegisterCommand(F("FORGET"), [](const terminal::CommandContext& ctx) { + terminalRegisterCommand(F("RFB.FORGET"), [](const terminal::CommandContext& ctx) { - if (ctx.argc != 3) { - terminalError(F("Wrong arguments")); + if (ctx.argc < 2) { + terminalError(ctx, F("RFB.FORGET []")); return; } - // 1st argument is relayID int id = ctx.argv[1].toInt(); if (id >= relayCount()) { - DEBUG_MSG_P(PSTR("-ERROR: Wrong relayID (%d)\n"), id); + terminalError(ctx, F("Invalid relay ID")); return; } - // 2nd argument is status - rfbForget(id, (ctx.argv[2].toInt()) == 1); - - terminalOK(); + if (ctx.argc == 3) { + rfbForget(id, (ctx.argv[2].toInt()) == 1); + } else { + rfbForget(id, true); + rfbForget(id, false); + } + terminalOK(ctx); }); - #if !RFB_DIRECT - terminalRegisterCommand(F("RFB.WRITE"), [](const terminal::CommandContext& ctx) { - if (ctx.argc != 2) return; - uint8_t data[RF_MAX_MESSAGE_SIZE]; - size_t bytes = hexDecode(ctx.argv[1].c_str(), ctx.argv[1].length(), data, sizeof(data)); - if (bytes) { - _rfbSendRaw(data, bytes); - } - }); - #endif +#if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1 + terminalRegisterCommand(F("RFB.WRITE"), [](const terminal::CommandContext& ctx) { + if (ctx.argc != 2) return; + uint8_t data[RfbParser::MessageSizeBasic]; + size_t bytes = hexDecode(ctx.argv[1].c_str(), ctx.argv[1].length(), data, sizeof(data)); + if (bytes) { + _rfbSendRaw(data, bytes); + } + }); +#endif } @@ -674,63 +1059,43 @@ void _rfbInitCommands() { // ----------------------------------------------------------------------------- void rfbStore(unsigned char id, bool status, const char * code) { - DEBUG_MSG_P(PSTR("[RF] Storing %d-%s => '%s'\n"), id, status ? "ON" : "OFF", code); - if (status) { - setSetting({"rfbON", id}, code); - } else { - setSetting({"rfbOFF", id}, code); - } + settings_key_t key { status ? F("rfbON") : F("rfbOFF"), id }; + setSetting(key, code); + DEBUG_MSG_P(PSTR("[RF] Saved %s => \"%s\"\n"), key.toString().c_str(), code); } String rfbRetrieve(unsigned char id, bool status) { - if (status) { - return getSetting({"rfbON", id}); - } else { - return getSetting({"rfbOFF", id}); - } + return getSetting({ status ? F("rfbON") : F("rfbOFF"), id }); } void rfbStatus(unsigned char id, bool status) { + // ref. receiver loop, we need to protect ourselves from re-sending the code we received to turn this relay ID on / off + if (_rfb_status_lock) { + return; + } String value = rfbRetrieve(id, status); if (value.length() && !(value.length() & 1)) { - - uint8_t message[RF_MAX_MESSAGE_SIZE]; - size_t bytes = hexDecode(value.c_str(), value.length(), message, sizeof(message)); - if (bytes && !_rfbin) { - if (value.length() == (RF_MESSAGE_SIZE * 2)) { - _rfbEnqueue(message, _rfbSameOnOff(id) ? 1 : _rfb_repeat); - } else { - #if !RFB_DIRECT - _rfbSendRaw(message, bytes); - #endif - } - } - + _rfbSendFromPayload(value.c_str()); } - - _rfbin = false; - } void rfbLearn(unsigned char id, bool status) { - _learnId = id; - _learnStatus = status ? RfbLearn::On : RfbLearn::Off; + _rfb_learn.reset(new RfbLearn{ millis(), id, status }); _rfbLearnImpl(); } void rfbForget(unsigned char id, bool status) { - char key[RF_MAX_KEY_LENGTH] = {0}; - snprintf_P(key, sizeof(key), PSTR("rfb%s%d"), status ? "ON" : "OFF", id); - delSetting(key); + delSetting({status ? F("rfbON") : F("rfbOFF"), id}); - // Websocket update - #if WEB_SUPPORT - wsPost([id](JsonObject& root) { - _rfbWebSocketSendCodeArray(root, id, 1); - }); - #endif + // Websocket update needs to happen right here, since the only time + // we send these in bulk is at the very start of the connection +#if WEB_SUPPORT + wsPost([id](JsonObject& root) { + _rfbWebSocketSendCodeArray(root, id, 1); + }); +#endif } @@ -740,33 +1105,16 @@ void rfbForget(unsigned char id, bool status) { void rfbSetup() { - #if MQTT_SUPPORT - mqttRegister(_rfbMqttCallback); - #endif - - #if API_SUPPORT - _rfbApiSetup(); - #endif +#if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1 - #if WEB_SUPPORT - wsRegister() - .onVisible(_rfbWebSocketOnVisible) - .onConnected(_rfbWebSocketOnConnected) - .onData(_rfbWebSocketOnData) - .onAction(_rfbWebSocketOnAction) - .onKeyCheck(_rfbWebSocketOnKeyCheck); - #endif - - #if TERMINAL_SUPPORT - _rfbInitCommands(); - #endif + _rfb_parser.reserve(RfbParser::MessageSizeBasic); - _rfb_repeat = getSetting("rfbRepeat", RF_SEND_TIMES); - - #if RFB_DIRECT - const auto rx = getSetting("rfbRX", RFB_RX_PIN); - const auto tx = getSetting("rfbTX", RFB_TX_PIN); +#elif RFB_PROVIDER == RFB_PROVIDER_RCSWITCH + { + auto rx = getSetting("rfbRX", RFB_RX_PIN); + auto tx = getSetting("rfbTX", RFB_TX_PIN); + // TODO: tag gpioGetLock with a NAME string, skip log here _rfb_receive = gpioValid(rx); _rfb_transmit = gpioValid(tx); if (!_rfb_transmit && !_rfb_receive) { @@ -774,27 +1122,50 @@ void rfbSetup() { return; } - _rfModem = new RCSwitch(); + _rfb_modem = new RCSwitch(); if (_rfb_receive) { - _rfModem->enableReceive(rx); + _rfb_modem->enableReceive(rx); DEBUG_MSG_P(PSTR("[RF] RF receiver on GPIO %u\n"), rx); } if (_rfb_transmit) { - _rfModem->enableTransmit(tx); - _rfModem->setRepeatTransmit(_rfb_repeat); + auto transmit = getSetting("rfbTransmit", RFB_TRANSMIT_TIMES); + _rfb_modem->enableTransmit(tx); + _rfb_modem->setRepeatTransmit(transmit); DEBUG_MSG_P(PSTR("[RF] RF transmitter on GPIO %u\n"), tx); } - #else - _rfb_receive = true; - _rfb_transmit = true; - #endif + } +#endif - // Register loop only when properly configured - espurnaRegisterLoop([]() -> void { +#if MQTT_SUPPORT + mqttRegister(_rfbMqttCallback); +#endif + +#if API_SUPPORT + _rfbApiSetup(); +#endif + +#if WEB_SUPPORT + wsRegister() + .onVisible(_rfbWebSocketOnVisible) + .onConnected(_rfbWebSocketOnConnected) + .onData(_rfbWebSocketOnData) + .onAction(_rfbWebSocketOnAction) + .onKeyCheck(_rfbWebSocketOnKeyCheck); +#endif + +#if TERMINAL_SUPPORT + _rfbInitCommands(); +#endif + + _rfb_repeat = getSetting("rfbRepeat", RFB_SEND_TIMES); + + // Note: as rfbridge protocol is simplictic enough, we rely on Serial queue to deliver timely updates + // learn / command acks / etc. are not queued, only RF messages are + espurnaRegisterLoop([]() { _rfbReceiveImpl(); _rfbSendQueued(); }); } -#endif +#endif // RFB_SUPPORT diff --git a/code/espurna/rfbridge.h b/code/espurna/rfbridge.h index 23c903b0..f1df96df 100644 --- a/code/espurna/rfbridge.h +++ b/code/espurna/rfbridge.h @@ -6,12 +6,18 @@ Copyright (C) 2016-2019 by Xose Pérez */ +#pragma once + #include "espurna.h" -#if RF_SUPPORT +#if RFB_SUPPORT + +#include "broker.h" -#if RFB_DIRECT -#include +#if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1 +BrokerDeclare(RfbridgeBroker, void(const char* code)); +#elif RFB_PROVIDER == RFB_PROVIDER_RCSWITCH +BrokerDeclare(RfbridgeBroker, void(unsigned char protocol, const char* code)); #endif void rfbStatus(unsigned char id, bool status); @@ -23,4 +29,4 @@ void rfbStore(unsigned char id, bool status, const char * code); void rfbForget(unsigned char id, bool status); void rfbSetup(); -#endif // RF_SUPPORT == 1 +#endif // RFB_SUPPORT == 1 diff --git a/code/espurna/system.cpp b/code/espurna/system.cpp index 1ae72700..77bc8d52 100644 --- a/code/espurna/system.cpp +++ b/code/espurna/system.cpp @@ -251,7 +251,7 @@ void _systemSetupSpecificHardware() { // These devices use the hardware UART // to communicate to secondary microcontrollers - #if (RF_SUPPORT && !RFB_DIRECT) || (RELAY_PROVIDER == RELAY_PROVIDER_DUAL) || (RELAY_PROVIDER == RELAY_PROVIDER_STM) + #if (RFB_SUPPORT && (RFB_PROVIDER == RFB_PROVIDER_EFM8BB1)) || (RELAY_PROVIDER == RELAY_PROVIDER_DUAL) || (RELAY_PROVIDER == RELAY_PROVIDER_STM) Serial.begin(SERIAL_BAUDRATE); #endif diff --git a/code/espurna/utils.cpp b/code/espurna/utils.cpp index c32f24b6..61548d9a 100644 --- a/code/espurna/utils.cpp +++ b/code/espurna/utils.cpp @@ -792,7 +792,7 @@ char* strnstr(const char* buffer, const char* token, size_t n) { } // From a byte array to an hexa char array ("A220EE...", double the size) -size_t hexEncode(uint8_t * in, size_t in_size, char * out, size_t out_size) { +size_t hexEncode(const uint8_t * in, size_t in_size, char * out, size_t out_size) { if ((2 * in_size + 1) > (out_size)) return 0; static const char base16[] = "0123456789ABCDEF"; diff --git a/code/espurna/utils.h b/code/espurna/utils.h index e287b446..2a60e542 100644 --- a/code/espurna/utils.h +++ b/code/espurna/utils.h @@ -69,5 +69,5 @@ void nice_delay(unsigned long ms); double roundTo(double num, unsigned char positions); -size_t hexEncode(uint8_t* in, size_t in_size, char* out, size_t out_size); +size_t hexEncode(const uint8_t* in, size_t in_size, char* out, size_t out_size); size_t hexDecode(const char* in, size_t in_size, uint8_t* out, size_t out_size); diff --git a/code/test/build/nondefault.h b/code/test/build/nondefault.h index af7b0e4c..5b07a003 100644 --- a/code/test/build/nondefault.h +++ b/code/test/build/nondefault.h @@ -5,11 +5,11 @@ #define NETBIOS_SUPPORT 1 #define NOFUSS_SUPPORT 1 #define OTA_MQTT_SUPPORT 1 -#define RFB_DIRECT 1 #define RFM69_SUPPORT 1 -#define RF_SUPPORT 1 #define RPN_RULES_SUPPORT 1 #define SSDP_SUPPORT 1 #define UART_MQTT_SUPPORT 1 #define TERMINAL_WEB_API_SUPPORT 1 #define TERMINAL_MQTT_SUPPORT 1 +#define RFB_SUPPORT 1 +#define RFB_PROVIDER RFB_PROVIDER_RCSWITCH diff --git a/code/test/build/rfbridge.h b/code/test/build/rfbridge.h index b5adfd9e..fe79bdd5 100644 --- a/code/test/build/rfbridge.h +++ b/code/test/build/rfbridge.h @@ -1,4 +1,5 @@ -#define DUMMY_RELAY_COUNT 8 -#define RF_SUPPORT 1 -#define SERIAL_BAUDRATE 19200 -#define DEBUG_SERIAL_SUPPORT 0 +#define DUMMY_RELAY_COUNT 8 +#define SERIAL_BAUDRATE 19200 +#define DEBUG_SERIAL_SUPPORT 0 +#define RFB_SUPPORT 1 +#define RFB_PROVIDER RFB_PROVIDER_EFM8BB1