- /*
-
- RF BRIDGE MODULE
-
- Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
-
- */
-
- #include "rfbridge.h"
-
- #if RFB_SUPPORT
-
- #include "api.h"
- #include "relay.h"
- #include "terminal.h"
- #include "mqtt.h"
- #include "ws.h"
- #include "utils.h"
-
- BrokerBind(RfbridgeBroker);
-
- #include <algorithm>
- #include <bitset>
- #include <cstring>
- #include <list>
- #include <memory>
-
- // -----------------------------------------------------------------------------
- // GLOBALS TO THE MODULE
- // -----------------------------------------------------------------------------
-
- unsigned char _rfb_repeats = RFB_SEND_REPEATS;
-
- #if RFB_PROVIDER == RFB_PROVIDER_RCSWITCH
-
- #include <RCSwitch.h>
- RCSwitch * _rfb_modem;
- bool _rfb_receive { false };
- bool _rfb_transmit { false };
-
- #else
-
- constexpr bool _rfb_receive { true };
- constexpr bool _rfb_transmit { true };
-
- #endif
-
- // -----------------------------------------------------------------------------
- // MATCH RECEIVED CODE WITH THE SPECIFIC RELAY ID
- // -----------------------------------------------------------------------------
-
- #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:
-
- bool _found { false };
- };
-
- struct RfbLearn {
- unsigned long ts;
- unsigned char id;
- bool status;
- };
-
- // Usage depends on the implementation. Will either:
- // - efm8bb1: wait until learn OK / TIMEOUT code
- // - rc-switch: receiver loop will check `ts` vs RFB_LEARN_TIMEOUT
- static std::unique_ptr<RfbLearn> _rfb_learn;
-
- // Individual lock for the relay, prevent rfbStatus from re-sending the code we just received
- static std::bitset<RelaysMax> _rfb_relay_status_lock;
-
- #endif // RELAY_SUPPORT
-
- // -----------------------------------------------------------------------------
- // EFM8BB1 PROTOCOL PARSING
- // -----------------------------------------------------------------------------
-
- constexpr uint8_t RfbDefaultProtocol { 0u };
-
- 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 CodeLearnTimeout { 0xA2u };
- constexpr uint8_t CodeLearnOk { 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<uint8_t>&);
- 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;
-
- explicit 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_end(uint8_t c) {
- if (CodeEnd == c) {
- _callback(_payload_code, _payload);
- }
- _state = &RfbParser::stop;
- }
-
- void read_until_end(uint8_t c) {
- if (CodeEnd == c) {
- read_end(c);
- return;
- }
- _payload.push_back(c);
- }
-
- void read_until_length(uint8_t c) {
- _payload.push_back(c);
-
- if ((_payload_offset + _payload_length) == _payload.size()) {
- switch (_payload_code) {
- case CodeLearnOk:
- case CodeRecvBasic:
- case CodeRecvProto:
- _state = &RfbParser::read_end;
- break;
- case CodeRecvBucket:
- _state = &RfbParser::read_until_end;
- break;
- default:
- _state = &RfbParser::stop;
- 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;
- }
-
- _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_length = 0u;
- _payload_offset = 0u;
- _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<uint8_t> _payload;
-
- size_t _payload_length { 0ul };
- size_t _payload_offset { 0ul };
-
- uint8_t _payload_code { 0ul };
- };
-
- // -----------------------------------------------------------------------------
- // 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 (&data)[RfbParser::PayloadSizeBasic], unsigned char repeats_) :
- repeats(repeats_)
- {
- std::copy(data, data + sizeof(data), 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<RCSwitch>().getReceivedValue());
-
- static constexpr size_t BufferSize = sizeof(code_type) + 5;
-
- uint8_t protocol;
- uint16_t timing;
- uint8_t bits;
- code_type code;
- uint8_t repeats;
- };
-
- #endif // RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
-
- static std::list<RfbMessage> _rfb_message_queue;
-
- void _rfbLearnImpl();
- void _rfbReceiveImpl();
- void _rfbSendImpl(const RfbMessage& message);
-
- // -----------------------------------------------------------------------------
- // WEBUI INTEGRATION
- // -----------------------------------------------------------------------------
-
- #if WEB_SUPPORT
-
- void _rfbWebSocketOnVisible(JsonObject& root) {
- root["rfbVisible"] = 1;
- }
-
- #if RELAY_SUPPORT
-
- void _rfbWebSocketSendCodeArray(JsonObject& root, unsigned char start, unsigned char size) {
- JsonObject& rfb = root.createNestedObject("rfb");
- rfb["size"] = size;
- rfb["start"] = start;
-
- JsonArray& on = rfb.createNestedArray("on");
- JsonArray& off = rfb.createNestedArray("off");
-
- for (uint8_t id=start; id<start+size; id++) {
- on.add(rfbRetrieve(id, true));
- off.add(rfbRetrieve(id, false));
- }
- }
-
- void _rfbWebSocketOnData(JsonObject& root) {
- _rfbWebSocketSendCodeArray(root, 0, relayCount());
- }
-
- #endif // RELAY_SUPPORT
-
- void _rfbWebSocketOnConnected(JsonObject& root) {
- root["rfbRepeat"] = getSetting("rfbRepeat", RFB_SEND_REPEATS);
- #if RELAY_SUPPORT
- root["rfbCount"] = relayCount();
- #endif
- #if RFB_PROVIDER == RFB_PROVIDER_RCSWITCH
- root["rfbdirectVisible"] = 1;
- root["rfbRX"] = getSetting("rfbRX", RFB_RX_PIN);
- root["rfbTX"] = getSetting("rfbTX", RFB_TX_PIN);
- #endif
- }
-
- void _rfbWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) {
- #if RELAY_SUPPORT
- if (strcmp(action, "rfblearn") == 0) rfbLearn(data["id"], data["status"]);
- if (strcmp(action, "rfbforget") == 0) rfbForget(data["id"], data["status"]);
- if (strcmp(action, "rfbsend") == 0) rfbStore(data["id"], data["status"], data["data"].as<const char*>());
- #endif
- }
-
- bool _rfbWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
- return (strncmp(key, "rfb", 3) == 0);
- }
-
- #endif // WEB_SUPPORT
-
- // -----------------------------------------------------------------------------
- // RELAY <-> CODE MATCHING
- // -----------------------------------------------------------------------------
-
- #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
-
- // 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));
- }
-
- #elif RFB_PROVIDER == RFB_PROVIDER_RCSWITCH
-
- // protocol is [2:3), actual payload is [10:), as bit length may vary
- // although, we don't care if it does, since we expect length of both args to be the same
- bool _rfbCompare(const char* lhs, const char* rhs, size_t length) {
- return (0 == std::memcmp((lhs + 2), (rhs + 2), 2))
- && (0 == std::memcmp((lhs + 10), (rhs + 10), length - 10));
- }
-
- #endif // RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
-
- #if RELAY_SUPPORT
-
- // try to find the 'code' saves as either rfbON# or rfbOFF#
- //
- // **always** expect full length code as input to simplify comparison
- // previous implementation tried to help MQTT / API requests to match based on the saved code,
- // thus requiring us to 'return' value from settings as the real code, replacing input
- RfbRelayMatch _rfbMatch(const char* code) {
-
- if (!relayCount()) {
- return {};
- }
-
- const auto len = strlen(code);
-
- // we gather all available options, as the kv store might be defined in any order
- // scan kvs only once, since we want both ON and OFF options and don't want to depend on the relayCount()
- RfbRelayMatch matched;
-
- using namespace settings;
- kv_store.foreach([code, len, &matched](kvs_type::KeyValueResult&& kv) {
- const auto key = kv.key.read();
- PayloadStatus status = key.startsWith(F("rfbON"))
- ? PayloadStatus::On : key.startsWith(F("rfbOFF"))
- ? PayloadStatus::Off : PayloadStatus::Unknown;
-
- if (PayloadStatus::Unknown == status) {
- return;
- }
-
- const auto value = kv.value.read();
- if (len != value.length()) {
- return;
- }
-
- if (!_rfbCompare(code, value.c_str(), len)) {
- return;
- }
-
- // note: strlen is constexpr here
- const char* id_ptr = key.c_str() + (
- (PayloadStatus::On == status) ? strlen("rfbON") : strlen("rfbOFF"));
- if (*id_ptr == '\0') {
- return;
- }
-
- char *endptr = nullptr;
- const auto id = strtoul(id_ptr, &endptr, 10);
- if (endptr == id_ptr || endptr[0] != '\0' || id > std::numeric_limits<uint8_t>::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;
- }
-
- matched.reset(matched.ok()
- ? std::min(static_cast<uint8_t>(id), matched.id)
- : static_cast<uint8_t>(id),
- status
- );
- });
-
-
- return matched;
-
- }
-
- void _rfbLearnFromString(std::unique_ptr<RfbLearn>& learn, const char* buffer) {
- if (!learn) return;
-
- DEBUG_MSG_P(PSTR("[RF] Learned relay ID %u after %u ms\n"), learn->id, millis() - learn->ts);
- rfbStore(learn->id, learn->status, buffer);
-
- // 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
-
- learn.reset(nullptr);
- }
-
- bool _rfbRelayHandler(const char* buffer, bool locked = false) {
- bool result { false };
-
- auto match = _rfbMatch(buffer);
- if (match.ok()) {
- DEBUG_MSG_P(PSTR("[RF] Matched with the relay ID %u\n"), match.id);
- _rfb_relay_status_lock.set(match.id, locked);
-
- 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;
- }
- }
-
- return result;
- }
-
- 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 (nullptr == sep) {
- return;
- }
-
- // ref. RelaysMax, we only have up to 2 digits
- char relay[3] {0, 0, 0};
- if ((sep - payload) > 2) {
- return;
- }
-
- std::copy(payload, sep, relay);
-
- char *endptr = nullptr;
- const auto id = strtoul(relay, &endptr, 10);
- if (endptr == &relay[0] || endptr[0] != '\0') {
- return;
- }
-
- if (id >= relayCount()) {
- DEBUG_MSG_P(PSTR("[RF] Invalid relay ID (%u)\n"), id);
- return;
- }
-
- ++sep;
- if ((*sep == '0') || (*sep == '1')) {
- rfbLearn(id, (*sep != '0'));
- }
- }
-
- void _rfbLearnFromReceived(std::unique_ptr<RfbLearn>& learn, const char* buffer) {
- if (millis() - learn->ts > RFB_LEARN_TIMEOUT) {
- DEBUG_MSG_P(PSTR("[RF] Learn timeout after %u ms\n"), millis() - learn->ts);
- learn.reset(nullptr);
- return;
- }
-
- _rfbLearnFromString(learn, buffer);
- }
-
- #endif // RELAY_SUPPORT
-
- // -----------------------------------------------------------------------------
- // RF handler implementations
- // -----------------------------------------------------------------------------
-
- #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
-
- void _rfbEnqueue(uint8_t (&code)[RfbParser::PayloadSizeBasic], unsigned char repeats = 1u) {
- if (!_rfb_transmit) return;
- _rfb_message_queue.push_back(RfbMessage(code, repeats));
- }
-
- bool _rfbEnqueue(const char* code, size_t length, unsigned char repeats = 1u) {
- uint8_t buffer[RfbParser::PayloadSizeBasic] { 0u };
- if (hexDecode(code, length, buffer, sizeof(buffer))) {
- _rfbEnqueue(buffer, repeats);
- return true;
- }
-
- DEBUG_MSG_P(PSTR("[RF] Cannot decode the message\n"));
- return false;
- }
-
- 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.write(message, sizeof(message));
- Serial.flush();
- }
-
- void _rfbLearnImpl() {
- static uint8_t message[3] {
- CodeStart, CodeLearn, CodeEnd
- };
-
- DEBUG_MSG_P(PSTR("[RF] Sending LEARN\n"));
- Serial.write(message, sizeof(message));
- Serial.flush();
- }
-
- void _rfbSendImpl(const RfbMessage& message) {
- Serial.write(CodeStart);
- Serial.write(CodeSendBasic);
- _rfbSendRaw(message.code, sizeof(message.code));
- Serial.write(CodeEnd);
- Serial.flush();
- }
-
- void _rfbParse(uint8_t code, const std::vector<uint8_t>& payload) {
- switch (code) {
-
- case CodeAck:
- DEBUG_MSG_P(PSTR("[RF] Received ACK\n"));
- break;
-
- case CodeLearnTimeout:
- _rfbAckImpl();
- #if RELAY_SUPPORT
- if (_rfb_learn) {
- DEBUG_MSG_P(PSTR("[RF] Learn timeout after %u ms\n"), millis() - _rfb_learn->ts);
- _rfb_learn.reset(nullptr);
- }
- #endif
- break;
-
- case CodeLearnOk:
- case CodeRecvBasic: {
- _rfbAckImpl();
-
- 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 RELAY_SUPPORT
- if (CodeLearnOk == code) {
- _rfbLearnFromString(_rfb_learn, buffer);
- } else {
- _rfbRelayHandler(buffer, true);
- }
- #endif
-
- #if MQTT_SUPPORT
- mqttSend(MQTT_TOPIC_RFIN, buffer, false, false);
- #endif
-
- #if BROKER_SUPPORT
- RfbridgeBroker::Publish(RfbDefaultProtocol, buffer + 12);
- #endif
- }
- break;
- }
-
- case CodeRecvProto:
- case CodeRecvBucket: {
- _rfbAckImpl();
- char buffer[(RfbParser::MessageSizeMax * 2) + 1] = {0};
- if (hexEncode(payload.data(), payload.size(), buffer, sizeof(buffer))) {
- DEBUG_MSG_P(PSTR("[RF] Received %s code: %s\n"),
- (CodeRecvProto == code) ? "advanced" : "bucket", buffer
- );
-
- #if MQTT_SUPPORT
- mqttSend(MQTT_TOPIC_RFIN, buffer, false, false);
- #endif
-
- #if BROKER_SUPPORT
- // ref. https://github.com/Portisch/RF-Bridge-EFM8BB1/wiki/0xA6#example-of-a-received-decoded-protocol
- RfbridgeBroker::Publish(payload[0], buffer + 2);
- #endif
- } else {
- DEBUG_MSG_P(PSTR("[RF] Received 0x%02X (%u bytes)\n"), code, payload.size());
- }
- break;
- }
-
- }
- }
-
- static RfbParser _rfb_parser(_rfbParse);
-
- void _rfbReceiveImpl() {
-
- while (Serial.available()) {
- auto c = Serial.read();
- if (c < 0) {
- continue;
- }
-
- // narrowing is justified, as `c` can only contain byte-sized value
- if (!_rfb_parser.loop(static_cast<uint8_t>(c))) {
- _rfb_parser.reset();
- }
- }
-
- }
-
- // 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 < 6) || (rawlen & 1)) return;
-
- DEBUG_MSG_P(PSTR("[RF] Sending RAW MESSAGE \"%s\"\n"), raw);
-
- 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);
- }
- }
-
- #elif RFB_PROVIDER == RFB_PROVIDER_RCSWITCH
-
- namespace {
-
- size_t _rfb_bytes_for_bits(size_t bits) {
- decltype(bits) bytes = 0;
- decltype(bits) need = 0;
-
- while (need < bits) {
- need += 8u;
- bytes += 1u;
- }
-
- return bytes;
- }
-
- // TODO: RCSwitch code type: long unsigned int != uint32_t, thus the specialization
- static_assert(sizeof(uint32_t) == sizeof(long unsigned int), "");
-
- template <typename T>
- 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 = 1u) {
- if (!_rfb_transmit) return;
- _rfb_message_queue.push_back(RfbMessage{protocol, timing, bits, code, repeats});
- }
-
- void _rfbEnqueue(const char* message, size_t length, unsigned char repeats = 1u) {
- uint8_t buffer[RfbMessage::BufferSize] { 0u };
- if (hexDecode(message, length, buffer, sizeof(buffer))) {
- const auto bytes = _rfb_bytes_for_bits(buffer[4]);
-
- uint8_t raw_code[sizeof(RfbMessage::code_type)] { 0u };
- std::memcpy(&raw_code[sizeof(raw_code) - bytes], &buffer[5], bytes);
-
- RfbMessage::code_type code;
- std::memcpy(&code, raw_code, sizeof(code));
-
- _rfbEnqueue(buffer[1], (buffer[2] << 8) | buffer[3], buffer[4], _rfb_bswap(code), repeats);
- return;
- }
-
- DEBUG_MSG_P(PSTR("[RF] Cannot decode the message\n"));
- }
-
- void _rfbLearnImpl() {
- DEBUG_MSG_P(PSTR("[RF] Entering LEARN mode\n"));
- }
-
- void _rfbSendImpl(const RfbMessage& message) {
-
- if (!_rfb_transmit) return;
-
- // TODO: note that this seems to be setting global setting
- // if code for some reason forgets this, we end up with the previous value
- _rfb_modem->setProtocol(message.protocol);
- if (message.timing) {
- _rfb_modem->setPulseLength(message.timing);
- }
-
- yield();
-
- _rfb_modem->send(message.code, message.bits);
- _rfb_modem->resetAvailable();
-
- }
-
- // 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'
-
- size_t _rfbModemPack(uint8_t (&out)[RfbMessage::BufferSize], RfbMessage::code_type code, unsigned int protocol, unsigned int timing, unsigned int bits) {
- static_assert((sizeof(decltype(code)) == 4) || (sizeof(decltype(code)) == 8), "");
-
- size_t index = 0;
- out[index++] = 0xC0;
- out[index++] = static_cast<uint8_t>(protocol);
- out[index++] = static_cast<uint8_t>(timing >> 8);
- out[index++] = static_cast<uint8_t>(timing);
- out[index++] = static_cast<uint8_t>(bits);
-
- auto bytes = _rfb_bytes_for_bits(bits);
- if (bytes > (sizeof(out) - index)) {
- return 0;
- }
-
- // manually overload each bswap, since we can't use ternary here
- // (and `if constexpr (...)` is only available starting from Arduino Core 3.x.x)
- decltype(code) swapped = _rfb_bswap(code);
-
- uint8_t raw[sizeof(swapped)];
- std::memcpy(raw, &swapped, sizeof(raw));
-
- while (bytes) {
- out[index++] = raw[sizeof(raw) - (bytes--)];
- }
-
- return index;
- }
-
- void _rfbReceiveImpl() {
-
- if (!_rfb_receive) return;
-
- // TODO: rc-switch isr handler sets 4 variables at the same time and never checks their existence before overwriting them
- // thus, we can't *really* trust that all 4 are from the same reading :/
- // TODO: in theory, we may also expirience memory tearing while doing 2 separate 32bit reads on the 64bit code value,
- // while isr handler *may* write into it at the same time
-
- auto rf_code = _rfb_modem->getReceivedValue();
- if (!rf_code) {
- return;
- }
-
- #if RFB_RECEIVE_DELAY
- static unsigned long last = 0;
- if (millis() - last < RFB_RECEIVE_DELAY) {
- _rfb_modem->resetAvailable();
- return;
- }
-
- last = millis();
- #endif
-
- uint8_t message[RfbMessage::BufferSize];
- auto real_msgsize = _rfbModemPack(
- message,
- rf_code,
- _rfb_modem->getReceivedProtocol(),
- _rfb_modem->getReceivedDelay(),
- _rfb_modem->getReceivedBitlength()
- );
-
- 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, true);
- }
- #endif
-
- #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;
- if (_rfb_message_queue.empty()) return;
-
- static unsigned long last = 0;
- if (millis() - last < RFB_SEND_DELAY) return;
- last = millis();
-
- auto message = _rfb_message_queue.front();
- _rfb_message_queue.pop_front();
-
- _rfbSendImpl(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();
-
- }
-
- // Check if the payload looks like a HEX code (plus comma, specifying the 'repeats' arg for the queue)
- void _rfbSendFromPayload(const char * payload) {
-
- decltype(_rfb_repeats) repeats { _rfb_repeats };
- size_t len { strlen(payload) };
-
- const char* sep { strchr(payload, ',') };
- if (sep) {
- len -= strlen(sep);
-
- sep += 1;
- if ('\0' == *sep) return;
- if ('-' == *sep) return;
-
- char *endptr = nullptr;
- repeats = strtoul(sep, &endptr, 10);
- if (endptr == payload || endptr[0] != '\0') {
- return;
- }
- }
-
- if (!len || (len & 1)) {
- return;
- }
-
- DEBUG_MSG_P(PSTR("[RF] Enqueuing MESSAGE '%s' %u time(s)\n"), payload, repeats);
-
- // 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, len, repeats);
-
- }
-
- #if MQTT_SUPPORT
-
- void _rfbMqttCallback(unsigned int type, const char * topic, char * payload) {
-
- if (type == MQTT_CONNECT_EVENT) {
-
- #if RELAY_SUPPORT
- mqttSubscribe(MQTT_TOPIC_RFLEARN);
- #endif
-
- #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
- mqttSubscribe(MQTT_TOPIC_RFRAW);
- #endif
- if (_rfb_transmit) {
- mqttSubscribe(MQTT_TOPIC_RFOUT);
- }
-
- return;
- }
-
- if (type == MQTT_MESSAGE_EVENT) {
-
- String t = mqttMagnitude((char *) topic);
-
- #if RELAY_SUPPORT
- if (t.equals(MQTT_TOPIC_RFLEARN)) {
- _rfbLearnStartFromPayload(payload);
- return;
- }
- #endif
-
- if (t.equals(MQTT_TOPIC_RFOUT)) {
- #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)) {
- #endif
- _rfbSendFromPayload(payload);
- #if RELAY_SUPPORT
- }
- #endif
- return;
- }
-
- #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
-
- return;
-
- }
-
- }
-
- #endif // MQTT_SUPPORT
-
- #if API_SUPPORT
-
- void _rfbApiSetup() {
-
- apiRegister(F(MQTT_TOPIC_RFOUT),
- apiOk, // just a stub, nothing to return
- [](ApiRequest& request) {
- _rfbSendFromPayload(request.param(F("value")).c_str());
- return true;
- }
- );
-
- #if RELAY_SUPPORT
- apiRegister(F(MQTT_TOPIC_RFLEARN),
- [](ApiRequest& request) {
- char buffer[64] { 0 };
- if (_rfb_learn) {
- snprintf_P(buffer, sizeof(buffer), PSTR("learning id:%u,status:%c"),
- _rfb_learn->id, _rfb_learn->status ? 't' : 'f'
- );
- } else {
- snprintf_P(buffer, sizeof(buffer), PSTR("waiting"));
- }
- request.send(buffer);
- return true;
- },
- [](ApiRequest& request) {
- _rfbLearnStartFromPayload(request.param(F("value")).c_str());
- return true;
- }
- );
- #endif
-
- #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
- apiRegister(F(MQTT_TOPIC_RFRAW),
- apiOk, // just a stub, nothing to return
- [](ApiRequest& request) {
- _rfbSendRawFromPayload(request.param(F("value")).c_str());
- return true;
- }
- );
- #endif
-
- }
-
- #endif // API_SUPPORT
-
- #if TERMINAL_SUPPORT
-
- void _rfbInitCommands() {
-
- #if RELAY_SUPPORT
- terminalRegisterCommand(F("RFB.LEARN"), [](const terminal::CommandContext& ctx) {
-
- if (ctx.argc != 3) {
- terminalError(ctx, F("RFB.LEARN <ID> <STATUS>"));
- return;
- }
-
- int id = ctx.argv[1].toInt();
- if (id >= relayCount()) {
- terminalError(ctx, F("Invalid relay ID"));
- return;
- }
-
- rfbLearn(id, (ctx.argv[2].toInt()) == 1);
-
- terminalOK(ctx);
-
- });
-
- terminalRegisterCommand(F("RFB.FORGET"), [](const terminal::CommandContext& ctx) {
-
- if (ctx.argc < 2) {
- terminalError(ctx, F("RFB.FORGET <ID> [<STATUS>]"));
- return;
- }
-
- int id = ctx.argv[1].toInt();
- if (id >= relayCount()) {
- terminalError(ctx, F("Invalid relay ID"));
- return;
- }
-
- if (ctx.argc == 3) {
- rfbForget(id, (ctx.argv[2].toInt()) == 1);
- } else {
- rfbForget(id, true);
- rfbForget(id, false);
- }
-
- terminalOK(ctx);
- });
- #endif // if RELAY_SUPPORT
-
- #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
- terminalRegisterCommand(F("RFB.WRITE"), [](const terminal::CommandContext& ctx) {
- if (ctx.argc != 2) {
- terminalError(ctx, F("RFB.WRITE <PAYLOAD>"));
- return;
- }
- _rfbSendRawFromPayload(ctx.argv[1].c_str());
- terminalOK(ctx);
- });
- #endif
-
- }
-
- #endif // TERMINAL_SUPPORT
-
- // -----------------------------------------------------------------------------
- // PUBLIC
- // -----------------------------------------------------------------------------
-
- void rfbStore(unsigned char id, bool status, const char * 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) {
- return getSetting({ status ? F("rfbON") : F("rfbOFF"), id });
- }
-
- #if RELAY_SUPPORT
-
- void rfbStatus(unsigned char id, bool status) {
- // TODO: This is a left-over from the old implementation. Right now we set this lock when relay handler
- // is called within the receiver, while this is called from either relayStatus or relay loop calling
- // this via provider callback. This prevents us from re-sending the code we just received.
- // TODO: Consider having 'origin' of the relay change. Either supply relayStatus with an additional arg,
- // or track these statuses directly.
- if (!_rfb_relay_status_lock[id]) {
- String value = rfbRetrieve(id, status);
- if (value.length() && !(value.length() & 1)) {
- _rfbSendFromPayload(value.c_str());
- }
- }
-
- _rfb_relay_status_lock[id] = false;
- }
-
- void rfbLearn(unsigned char id, bool status) {
- _rfb_learn.reset(new RfbLearn { millis(), id, status });
- _rfbLearnImpl();
- }
-
- void rfbForget(unsigned char id, bool status) {
-
- delSetting({status ? F("rfbON") : F("rfbOFF"), id});
-
- // 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
-
- }
-
- #endif // RELAY_SUPPORT
-
- // -----------------------------------------------------------------------------
- // SETUP & LOOP
- // -----------------------------------------------------------------------------
-
- #if RELAY_SUPPORT && (RFB_PROVIDER == RFB_PROVIDER_RCSWITCH)
-
- // TODO: remove this in 1.16.0
-
- void _rfbSettingsMigrate(int version) {
- if (!version || (version > 4)) {
- return;
- }
-
- auto migrate_code = [](String& out, const String& in) -> bool {
- out = "";
-
- if (18 == in.length()) {
- uint8_t bits { 0u };
- if (!hexDecode(in.c_str() + 8, 2, &bits, 1)) {
- return false;
- }
-
- auto bytes = _rfb_bytes_for_bits(bits);
- out = in.substring(0, 10);
- out += (in.c_str() + in.length() - (2 * bytes));
-
- return in != out;
- }
-
- return false;
- };
-
- String buffer;
-
- for (unsigned char index = 0; index < relayCount(); ++index) {
- const settings_key_t on_key {F("rfbON"), index};
- if (migrate_code(buffer, getSetting(on_key))) {
- setSetting(on_key, buffer);
- }
-
- const settings_key_t off_key {F("rfbOFF"), index};
- if (migrate_code(buffer, getSetting(off_key))) {
- setSetting(off_key, buffer);
- }
- }
- }
-
- #endif
-
- void rfbSetup() {
-
- #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
-
- _rfb_parser.reserve(RfbParser::MessageSizeBasic);
-
- #elif RFB_PROVIDER == RFB_PROVIDER_RCSWITCH
-
- #if RELAY_SUPPORT
- _rfbSettingsMigrate(migrateVersion());
- #endif
-
- {
- 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) {
- DEBUG_MSG_P(PSTR("[RF] Neither RX or TX are set\n"));
- return;
- }
-
- _rfb_modem = new RCSwitch();
- if (_rfb_receive) {
- _rfb_modem->enableReceive(rx);
- DEBUG_MSG_P(PSTR("[RF] RF receiver on GPIO %u\n"), rx);
- }
- if (_rfb_transmit) {
- auto transmit = getSetting("rfbTransmit", RFB_TRANSMIT_REPEATS);
- _rfb_modem->enableTransmit(tx);
- _rfb_modem->setRepeatTransmit(transmit);
- DEBUG_MSG_P(PSTR("[RF] RF transmitter on GPIO %u\n"), tx);
- }
- }
-
- #endif
-
- #if MQTT_SUPPORT
- mqttRegister(_rfbMqttCallback);
- #endif
-
- #if API_SUPPORT
- _rfbApiSetup();
- #endif
-
- #if WEB_SUPPORT
- wsRegister()
- #if RELAY_SUPPORT
- .onData(_rfbWebSocketOnData)
- .onAction(_rfbWebSocketOnAction)
- #endif
- .onConnected(_rfbWebSocketOnConnected)
- .onVisible(_rfbWebSocketOnVisible)
- .onKeyCheck(_rfbWebSocketOnKeyCheck);
- #endif
-
- #if TERMINAL_SUPPORT
- _rfbInitCommands();
- #endif
-
- _rfb_repeats = getSetting("rfbRepeat", RFB_SEND_REPEATS);
-
- // Note: as rfbridge protocol is simplistic 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 // RFB_SUPPORT
|