/* RF BRIDGE MODULE Copyright (C) 2016-2019 by Xose PĂ©rez */ #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 #include #include #include // ----------------------------------------------------------------------------- // GLOBALS TO THE MODULE // ----------------------------------------------------------------------------- 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 // ----------------------------------------------------------------------------- // 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; }; 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; } _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 }; }; // ----------------------------------------------------------------------------- // 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; void _rfbLearnImpl(); void _rfbReceiveImpl(); void _rfbSendImpl(const RfbMessage& message); // ----------------------------------------------------------------------------- // WEBUI INTEGRATION // ----------------------------------------------------------------------------- #if WEB_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()); } bool _rfbWebSocketOnKeyCheck(const char * key, JsonVariant& value) { return (strncmp(key, "rfb", 3) == 0); } void _rfbWebSocketOnData(JsonObject& root) { _rfbWebSocketSendCodeArray(root, 0, relayCount()); } #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 static bool _rfb_status_lock = false; // 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::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(id), matched.id) : static_cast(id), status ); }); return matched; } void _rfbLearnFromString(std::unique_ptr& learn, const char* buffer) { if (!learn) return; DEBUG_MSG_P(PSTR("[RF] Learned %s for relay ID %u\n"), buffer, learn->id); 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) { _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_PROVIDER == RFB_PROVIDER_EFM8BB1 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.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& 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; } 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); } #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(); } } } // 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); 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_bits_for_bytes(size_t bits) { decltype(bits) bytes = 0; decltype(bits) need = 0; 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(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 if (message.timing) { _rfb_modem->setPulseLength(message.timing); } _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' 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; } // 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); uint8_t raw[sizeof(swapped)]; std::memcpy(raw, &swapped, sizeof(raw)); while (bytes) { out[index++] = raw[sizeof(raw) - (bytes--)]; } 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); } void _rfbReceiveImpl() { if (!_rfb_receive) return; if (!_rfb_modem->available()) return; static unsigned long last = 0; if (millis() - last < RFB_RECEIVE_DELAY) return; last = millis(); 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 #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 'times' arg for the queue) void _rfbSendFromPayload(const char * payload) { size_t times { 1ul }; size_t len { strlen(payload) }; 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; } len -= strlen(sep); } 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 _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')); } } #if MQTT_SUPPORT void _rfbMqttCallback(unsigned int type, const char * topic, char * payload) { if (type == MQTT_CONNECT_EVENT) { char buffer[strlen(MQTT_TOPIC_RFLEARN) + 3]; snprintf_P(buffer, sizeof(buffer), PSTR("%s/+"), MQTT_TOPIC_RFLEARN); mqttSubscribe(buffer); if (_rfb_transmit) { mqttSubscribe(MQTT_TOPIC_RFOUT); } #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1 mqttSubscribe(MQTT_TOPIC_RFRAW); #endif } if (type == MQTT_MESSAGE_EVENT) { String t = mqttMagnitude((char *) topic); if (t.startsWith(MQTT_TOPIC_RFLEARN)) { _rfbLearnStartFromPayload(payload); return; } 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, /* locked = */ true)) { #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 } } #endif // MQTT_SUPPORT #if API_SUPPORT void _rfbApiSetup() { apiReserve(3u); apiRegister({ MQTT_TOPIC_RFOUT, Api::Type::Basic, ApiUnusedArg, apiOk, // just a stub, nothing to return [](const Api&, ApiBuffer& buffer) { _rfbSendFromPayload(buffer.data); } }); apiRegister({ MQTT_TOPIC_RFLEARN, Api::Type::Basic, ApiUnusedArg, [](const Api&, ApiBuffer& buffer) { 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) { _rfbLearnStartFromPayload(buffer.data); } }); #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 } #endif // API_SUPPORT #if TERMINAL_SUPPORT void _rfbInitCommands() { terminalRegisterCommand(F("RFB.LEARN"), [](const terminal::CommandContext& ctx) { if (ctx.argc != 3) { terminalError(ctx, F("RFB.LEARN ")); 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 []")); 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); }); #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 } #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 }); } 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)) { _rfbSendFromPayload(value.c_str()); } } 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 } // ----------------------------------------------------------------------------- // SETUP & LOOP // ----------------------------------------------------------------------------- void rfbSetup() { #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1 _rfb_parser.reserve(RfbParser::MessageSizeBasic); #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) { 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_TIMES); _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() .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 // RFB_SUPPORT