Fork of the espurna firmware for `mhsw` switches
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1276 lines
35 KiB

4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
Terminal: change command-line parser (#2247) Change the underlying command line handling: - switch to a custom parser, inspired by redis / sds - update terminalRegisterCommand signature, pass only bare minimum - clean-up `help` & `commands`. update settings `set`, `get` and `del` - allow our custom test suite to run command-line tests - clean-up Stream IO to allow us to print large things into debug stream (for example, `eeprom.dump`) - send parsing errors to the debug log As a proof of concept, introduce `TERMINAL_MQTT_SUPPORT` and `TERMINAL_WEB_API_SUPPORT` - MQTT subscribes to the `<root>/cmd/set` and sends response to the `<root>/cmd`. We can't output too much, as we don't have any large-send API. - Web API listens to the `/api/cmd?apikey=...&line=...` (or PUT, params inside the body). This one is intended as a possible replacement of the `API_SUPPORT`. Internals introduce a 'task' around the AsyncWebServerRequest object that will simulate what WiFiClient does and push data into it continuously, switching between CONT and SYS. Both are experimental. We only accept a single command and not every command is updated to use Print `ctx.output` object. We are also somewhat limited by the Print / Stream overall, perhaps I am overestimating the usefulness of Arduino compatibility to such an extent :) Web API handler can also sometimes show only part of the result, whenever the command tries to yield() by itself waiting for something. Perhaps we would need to create a custom request handler for that specific use-case.
4 years ago
Terminal: change command-line parser (#2247) Change the underlying command line handling: - switch to a custom parser, inspired by redis / sds - update terminalRegisterCommand signature, pass only bare minimum - clean-up `help` & `commands`. update settings `set`, `get` and `del` - allow our custom test suite to run command-line tests - clean-up Stream IO to allow us to print large things into debug stream (for example, `eeprom.dump`) - send parsing errors to the debug log As a proof of concept, introduce `TERMINAL_MQTT_SUPPORT` and `TERMINAL_WEB_API_SUPPORT` - MQTT subscribes to the `<root>/cmd/set` and sends response to the `<root>/cmd`. We can't output too much, as we don't have any large-send API. - Web API listens to the `/api/cmd?apikey=...&line=...` (or PUT, params inside the body). This one is intended as a possible replacement of the `API_SUPPORT`. Internals introduce a 'task' around the AsyncWebServerRequest object that will simulate what WiFiClient does and push data into it continuously, switching between CONT and SYS. Both are experimental. We only accept a single command and not every command is updated to use Print `ctx.output` object. We are also somewhat limited by the Print / Stream overall, perhaps I am overestimating the usefulness of Arduino compatibility to such an extent :) Web API handler can also sometimes show only part of the result, whenever the command tries to yield() by itself waiting for something. Perhaps we would need to create a custom request handler for that specific use-case.
4 years ago
Terminal: change command-line parser (#2247) Change the underlying command line handling: - switch to a custom parser, inspired by redis / sds - update terminalRegisterCommand signature, pass only bare minimum - clean-up `help` & `commands`. update settings `set`, `get` and `del` - allow our custom test suite to run command-line tests - clean-up Stream IO to allow us to print large things into debug stream (for example, `eeprom.dump`) - send parsing errors to the debug log As a proof of concept, introduce `TERMINAL_MQTT_SUPPORT` and `TERMINAL_WEB_API_SUPPORT` - MQTT subscribes to the `<root>/cmd/set` and sends response to the `<root>/cmd`. We can't output too much, as we don't have any large-send API. - Web API listens to the `/api/cmd?apikey=...&line=...` (or PUT, params inside the body). This one is intended as a possible replacement of the `API_SUPPORT`. Internals introduce a 'task' around the AsyncWebServerRequest object that will simulate what WiFiClient does and push data into it continuously, switching between CONT and SYS. Both are experimental. We only accept a single command and not every command is updated to use Print `ctx.output` object. We are also somewhat limited by the Print / Stream overall, perhaps I am overestimating the usefulness of Arduino compatibility to such an extent :) Web API handler can also sometimes show only part of the result, whenever the command tries to yield() by itself waiting for something. Perhaps we would need to create a custom request handler for that specific use-case.
4 years ago
Terminal: change command-line parser (#2247) Change the underlying command line handling: - switch to a custom parser, inspired by redis / sds - update terminalRegisterCommand signature, pass only bare minimum - clean-up `help` & `commands`. update settings `set`, `get` and `del` - allow our custom test suite to run command-line tests - clean-up Stream IO to allow us to print large things into debug stream (for example, `eeprom.dump`) - send parsing errors to the debug log As a proof of concept, introduce `TERMINAL_MQTT_SUPPORT` and `TERMINAL_WEB_API_SUPPORT` - MQTT subscribes to the `<root>/cmd/set` and sends response to the `<root>/cmd`. We can't output too much, as we don't have any large-send API. - Web API listens to the `/api/cmd?apikey=...&line=...` (or PUT, params inside the body). This one is intended as a possible replacement of the `API_SUPPORT`. Internals introduce a 'task' around the AsyncWebServerRequest object that will simulate what WiFiClient does and push data into it continuously, switching between CONT and SYS. Both are experimental. We only accept a single command and not every command is updated to use Print `ctx.output` object. We are also somewhat limited by the Print / Stream overall, perhaps I am overestimating the usefulness of Arduino compatibility to such an extent :) Web API handler can also sometimes show only part of the result, whenever the command tries to yield() by itself waiting for something. Perhaps we would need to create a custom request handler for that specific use-case.
4 years ago
  1. /*
  2. RF BRIDGE MODULE
  3. Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
  4. */
  5. #include "rfbridge.h"
  6. #if RFB_SUPPORT
  7. #include "api.h"
  8. #include "relay.h"
  9. #include "terminal.h"
  10. #include "mqtt.h"
  11. #include "ws.h"
  12. #include "utils.h"
  13. BrokerBind(RfbridgeBroker);
  14. #include <algorithm>
  15. #include <bitset>
  16. #include <cstring>
  17. #include <list>
  18. #include <memory>
  19. // -----------------------------------------------------------------------------
  20. // GLOBALS TO THE MODULE
  21. // -----------------------------------------------------------------------------
  22. unsigned char _rfb_repeat = RFB_SEND_TIMES;
  23. #if RFB_PROVIDER == RFB_PROVIDER_RCSWITCH
  24. #include <RCSwitch.h>
  25. RCSwitch * _rfb_modem;
  26. bool _rfb_receive { false };
  27. bool _rfb_transmit { false };
  28. #else
  29. constexpr bool _rfb_receive { true };
  30. constexpr bool _rfb_transmit { true };
  31. #endif
  32. // -----------------------------------------------------------------------------
  33. // MATCH RECEIVED CODE WITH THE SPECIFIC RELAY ID
  34. // -----------------------------------------------------------------------------
  35. #if RELAY_SUPPORT
  36. struct RfbRelayMatch {
  37. RfbRelayMatch() = default;
  38. RfbRelayMatch(unsigned char id_, PayloadStatus status_) :
  39. id(id_),
  40. status(status_),
  41. _found(true)
  42. {}
  43. bool ok() {
  44. return _found;
  45. }
  46. void reset(unsigned char id_, PayloadStatus status_) {
  47. id = id_;
  48. status = status_;
  49. _found = true;
  50. }
  51. unsigned char id { 0u };
  52. PayloadStatus status { PayloadStatus::Unknown };
  53. private:
  54. bool _found { false };
  55. };
  56. struct RfbLearn {
  57. unsigned long ts;
  58. unsigned char id;
  59. bool status;
  60. };
  61. // Usage depends on the implementation. Will either:
  62. // - efm8bb1: wait until learn OK / TIMEOUT code
  63. // - rc-switch: receiver loop will check `ts` vs RFB_LEARN_TIMEOUT
  64. static std::unique_ptr<RfbLearn> _rfb_learn;
  65. // Individual lock for the relay, prevent rfbStatus from re-sending the code we just received
  66. static std::bitset<RelaysMax> _rfb_relay_status_lock;
  67. #endif // RELAY_SUPPORT
  68. // -----------------------------------------------------------------------------
  69. // EFM8BB1 PROTOCOL PARSING
  70. // -----------------------------------------------------------------------------
  71. constexpr uint8_t RfbDefaultProtocol { 0u };
  72. constexpr uint8_t CodeStart { 0xAAu };
  73. constexpr uint8_t CodeEnd { 0x55u };
  74. constexpr uint8_t CodeAck { 0xA0u };
  75. // both stock and https://github.com/Portisch/RF-Bridge-EFM8BB1/
  76. // sending:
  77. constexpr uint8_t CodeLearn { 0xA1u };
  78. // receiving:
  79. constexpr uint8_t CodeLearnTimeout { 0xA2u };
  80. constexpr uint8_t CodeLearnOk { 0xA3u };
  81. constexpr uint8_t CodeRecvBasic = { 0xA4u };
  82. constexpr uint8_t CodeSendBasic = { 0xA5u };
  83. // only https://github.com/Portisch/RF-Bridge-EFM8BB1/
  84. constexpr uint8_t CodeRecvProto { 0xA6u };
  85. constexpr uint8_t CodeRecvBucket { 0xB1u };
  86. struct RfbParser {
  87. using callback_type = void(uint8_t, const std::vector<uint8_t>&);
  88. using state_type = void(RfbParser::*)(uint8_t);
  89. // AA XX ... 55
  90. // ^~~~~ ~~ - protocol head + tail
  91. // ^~ - message code
  92. // ^~~ - actual payload is always 9 bytes
  93. static constexpr size_t PayloadSizeBasic { 9ul };
  94. static constexpr size_t MessageSizeBasic { PayloadSizeBasic + 3ul };
  95. static constexpr size_t MessageSizeMax { 112ul };
  96. RfbParser() = delete;
  97. RfbParser(const RfbParser&) = delete;
  98. explicit RfbParser(callback_type* callback) :
  99. _callback(callback)
  100. {}
  101. RfbParser(RfbParser&&) = default;
  102. void stop(uint8_t c) {
  103. }
  104. void start(uint8_t c) {
  105. switch (c) {
  106. case CodeStart:
  107. _state = &RfbParser::read_code;
  108. break;
  109. default:
  110. _state = &RfbParser::stop;
  111. break;
  112. }
  113. }
  114. void read_code(uint8_t c) {
  115. _payload_code = c;
  116. switch (c) {
  117. // Generic ACK signal. We *expect* this after our requests
  118. case CodeAck:
  119. // *Expect* any code within a certain window.
  120. // Only matters to us, does not really do anything but help us to signal that the next code needs to be recorded
  121. case CodeLearnTimeout:
  122. _state = &RfbParser::read_end;
  123. break;
  124. // both stock and https://github.com/Portisch/RF-Bridge-EFM8BB1/
  125. // receive 9 bytes, where first 3 2-byte tuples are timings
  126. // and the last 3 bytes are the actual payload
  127. case CodeLearnOk:
  128. case CodeRecvBasic:
  129. _payload_length = PayloadSizeBasic;
  130. _state = &RfbParser::read_until_length;
  131. break;
  132. // specific to the https://github.com/Portisch/RF-Bridge-EFM8BB1/
  133. // receive N bytes, where the 1st byte is the protocol ID and the next N-1 bytes are the payload
  134. case CodeRecvProto:
  135. _state = &RfbParser::read_length;
  136. break;
  137. // unlike CodeRecvProto, we don't have any length byte here :/ for some reason, it is there only when sending
  138. // just bail out when we find CodeEnd
  139. // (TODO: is number of buckets somehow convertible to the 'expected' size?)
  140. case CodeRecvBucket:
  141. _state = &RfbParser::read_length;
  142. break;
  143. default:
  144. _state = &RfbParser::stop;
  145. break;
  146. }
  147. }
  148. void read_end(uint8_t c) {
  149. if (CodeEnd == c) {
  150. _callback(_payload_code, _payload);
  151. }
  152. _state = &RfbParser::stop;
  153. }
  154. void read_until_end(uint8_t c) {
  155. if (CodeEnd == c) {
  156. read_end(c);
  157. return;
  158. }
  159. _payload.push_back(c);
  160. }
  161. void read_until_length(uint8_t c) {
  162. _payload.push_back(c);
  163. if ((_payload_offset + _payload_length) == _payload.size()) {
  164. switch (_payload_code) {
  165. case CodeLearnOk:
  166. case CodeRecvBasic:
  167. case CodeRecvProto:
  168. _state = &RfbParser::read_end;
  169. break;
  170. case CodeRecvBucket:
  171. _state = &RfbParser::read_until_end;
  172. break;
  173. default:
  174. _state = &RfbParser::stop;
  175. break;
  176. }
  177. _payload_length = 0u;
  178. }
  179. }
  180. void read_length(uint8_t c) {
  181. switch (_payload_code) {
  182. case CodeRecvProto:
  183. _payload_length = c;
  184. break;
  185. case CodeRecvBucket:
  186. _payload_length = c * 2;
  187. break;
  188. default:
  189. _state = &RfbParser::stop;
  190. return;
  191. }
  192. _payload.push_back(c);
  193. _payload_offset = _payload.size();
  194. _state = &RfbParser::read_until_length;
  195. }
  196. bool loop(uint8_t c) {
  197. (this->*_state)(c);
  198. return (_state != &RfbParser::stop);
  199. }
  200. void reset() {
  201. _payload.clear();
  202. _payload_length = 0u;
  203. _payload_offset = 0u;
  204. _payload_code = 0u;
  205. _state = &RfbParser::start;
  206. }
  207. void reserve(size_t size) {
  208. _payload.reserve(size);
  209. }
  210. private:
  211. callback_type* _callback { nullptr };
  212. state_type _state { &RfbParser::start };
  213. std::vector<uint8_t> _payload;
  214. size_t _payload_length { 0ul };
  215. size_t _payload_offset { 0ul };
  216. uint8_t _payload_code { 0ul };
  217. };
  218. // -----------------------------------------------------------------------------
  219. // MESSAGE SENDER
  220. //
  221. // Depends on the selected provider. While we do serialize RCSwitch results,
  222. // we don't want to pass around such byte-array everywhere since we already
  223. // know all of the required data members and can prepare a basic POD struct
  224. // -----------------------------------------------------------------------------
  225. #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
  226. struct RfbMessage {
  227. RfbMessage(const RfbMessage&) = default;
  228. RfbMessage(RfbMessage&&) = default;
  229. explicit RfbMessage(uint8_t (&data)[RfbParser::PayloadSizeBasic], unsigned char repeats_) :
  230. repeats(repeats_)
  231. {
  232. std::copy(data, data + sizeof(data), code);
  233. }
  234. uint8_t code[RfbParser::PayloadSizeBasic] { 0u };
  235. uint8_t repeats { 1u };
  236. };
  237. #elif RFB_PROVIDER == RFB_PROVIDER_RCSWITCH
  238. struct RfbMessage {
  239. using code_type = decltype(std::declval<RCSwitch>().getReceivedValue());
  240. static constexpr size_t BufferSize = sizeof(code_type) + 5;
  241. uint8_t protocol;
  242. uint16_t timing;
  243. uint8_t bits;
  244. code_type code;
  245. uint8_t repeats;
  246. };
  247. #endif // RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
  248. static std::list<RfbMessage> _rfb_message_queue;
  249. void _rfbLearnImpl();
  250. void _rfbReceiveImpl();
  251. void _rfbSendImpl(const RfbMessage& message);
  252. // -----------------------------------------------------------------------------
  253. // WEBUI INTEGRATION
  254. // -----------------------------------------------------------------------------
  255. #if WEB_SUPPORT
  256. void _rfbWebSocketSendCodeArray(JsonObject& root, unsigned char start, unsigned char size) {
  257. JsonObject& rfb = root.createNestedObject("rfb");
  258. rfb["size"] = size;
  259. rfb["start"] = start;
  260. JsonArray& on = rfb.createNestedArray("on");
  261. JsonArray& off = rfb.createNestedArray("off");
  262. for (uint8_t id=start; id<start+size; id++) {
  263. on.add(rfbRetrieve(id, true));
  264. off.add(rfbRetrieve(id, false));
  265. }
  266. }
  267. void _rfbWebSocketOnVisible(JsonObject& root) {
  268. root["rfbVisible"] = 1;
  269. }
  270. void _rfbWebSocketOnConnected(JsonObject& root) {
  271. root["rfbRepeat"] = getSetting("rfbRepeat", RFB_SEND_TIMES);
  272. root["rfbCount"] = relayCount();
  273. #if RFB_PROVIDER == RFB_PROVIDER_RCSWITCH
  274. root["rfbdirectVisible"] = 1;
  275. root["rfbRX"] = getSetting("rfbRX", RFB_RX_PIN);
  276. root["rfbTX"] = getSetting("rfbTX", RFB_TX_PIN);
  277. #endif
  278. }
  279. void _rfbWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) {
  280. if (strcmp(action, "rfblearn") == 0) rfbLearn(data["id"], data["status"]);
  281. if (strcmp(action, "rfbforget") == 0) rfbForget(data["id"], data["status"]);
  282. if (strcmp(action, "rfbsend") == 0) rfbStore(data["id"], data["status"], data["data"].as<const char*>());
  283. }
  284. bool _rfbWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
  285. return (strncmp(key, "rfb", 3) == 0);
  286. }
  287. void _rfbWebSocketOnData(JsonObject& root) {
  288. _rfbWebSocketSendCodeArray(root, 0, relayCount());
  289. }
  290. #endif // WEB_SUPPORT
  291. // -----------------------------------------------------------------------------
  292. // RELAY <-> CODE MATCHING
  293. // -----------------------------------------------------------------------------
  294. #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
  295. // we only care about last 6 chars (3 bytes in hex),
  296. // since in 'default' mode rfbridge only handles a single protocol
  297. bool _rfbCompare(const char* lhs, const char* rhs, size_t length) {
  298. return (0 == std::memcmp((lhs + length - 6), (rhs + length - 6), 6));
  299. }
  300. #elif RFB_PROVIDER == RFB_PROVIDER_RCSWITCH
  301. // protocol is [2:3), actual payload is [10:), as bit length may vary
  302. // although, we don't care if it does, since we expect length of both args to be the same
  303. bool _rfbCompare(const char* lhs, const char* rhs, size_t length) {
  304. return (0 == std::memcmp((lhs + 2), (rhs + 2), 2))
  305. && (0 == std::memcmp((lhs + 10), (rhs + 10), length - 10));
  306. }
  307. #endif // RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
  308. #if RELAY_SUPPORT
  309. // try to find the 'code' saves as either rfbON# or rfbOFF#
  310. //
  311. // **always** expect full length code as input to simplify comparison
  312. // previous implementation tried to help MQTT / API requests to match based on the saved code,
  313. // thus requiring us to 'return' value from settings as the real code, replacing input
  314. RfbRelayMatch _rfbMatch(const char* code) {
  315. if (!relayCount()) {
  316. return {};
  317. }
  318. const auto len = strlen(code);
  319. // we gather all available options, as the kv store might be defined in any order
  320. // scan kvs only once, since we want both ON and OFF options and don't want to depend on the relayCount()
  321. RfbRelayMatch matched;
  322. using namespace settings;
  323. kv_store.foreach([code, len, &matched](kvs_type::KeyValueResult&& kv) {
  324. const auto key = kv.key.read();
  325. PayloadStatus status = key.startsWith(F("rfbON"))
  326. ? PayloadStatus::On : key.startsWith(F("rfbOFF"))
  327. ? PayloadStatus::Off : PayloadStatus::Unknown;
  328. if (PayloadStatus::Unknown == status) {
  329. return;
  330. }
  331. const auto value = kv.value.read();
  332. if (len != value.length()) {
  333. return;
  334. }
  335. if (!_rfbCompare(code, value.c_str(), len)) {
  336. return;
  337. }
  338. // note: strlen is constexpr here
  339. const char* id_ptr = key.c_str() + (
  340. (PayloadStatus::On == status) ? strlen("rfbON") : strlen("rfbOFF"));
  341. if (*id_ptr == '\0') {
  342. return;
  343. }
  344. char *endptr = nullptr;
  345. const auto id = strtoul(id_ptr, &endptr, 10);
  346. if (endptr == id_ptr || endptr[0] != '\0' || id > std::numeric_limits<uint8_t>::max() || id >= relayCount()) {
  347. return;
  348. }
  349. // when we see the same id twice, we match the opposite statuses
  350. if (matched.ok() && (id == matched.id)) {
  351. matched.status = PayloadStatus::Toggle;
  352. return;
  353. }
  354. matched.reset(matched.ok()
  355. ? std::min(static_cast<uint8_t>(id), matched.id)
  356. : static_cast<uint8_t>(id),
  357. status
  358. );
  359. });
  360. return matched;
  361. }
  362. void _rfbLearnFromString(std::unique_ptr<RfbLearn>& learn, const char* buffer) {
  363. if (!learn) return;
  364. DEBUG_MSG_P(PSTR("[RF] Learned relay ID %u after %u ms\n"), learn->id, millis() - learn->ts);
  365. rfbStore(learn->id, learn->status, buffer);
  366. // Websocket update needs to happen right here, since the only time
  367. // we send these in bulk is at the very start of the connection
  368. #if WEB_SUPPORT
  369. auto id = learn->id;
  370. wsPost([id](JsonObject& root) {
  371. _rfbWebSocketSendCodeArray(root, id, 1);
  372. });
  373. #endif
  374. learn.reset(nullptr);
  375. }
  376. bool _rfbRelayHandler(const char* buffer, bool locked = false) {
  377. bool result { false };
  378. auto match = _rfbMatch(buffer);
  379. if (match.ok()) {
  380. DEBUG_MSG_P(PSTR("[RF] Matched with the relay ID %u\n"), match.id);
  381. _rfb_relay_status_lock.set(match.id, locked);
  382. switch (match.status) {
  383. case PayloadStatus::On:
  384. case PayloadStatus::Off:
  385. relayStatus(match.id, (PayloadStatus::On == match.status));
  386. result = true;
  387. break;
  388. case PayloadStatus::Toggle:
  389. relayToggle(match.id);
  390. result = true;
  391. case PayloadStatus::Unknown:
  392. break;
  393. }
  394. }
  395. return result;
  396. }
  397. #endif // RELAY_SUPPORT
  398. // -----------------------------------------------------------------------------
  399. // RF handler implementations
  400. // -----------------------------------------------------------------------------
  401. #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
  402. void _rfbEnqueue(uint8_t (&code)[RfbParser::PayloadSizeBasic], unsigned char repeats = 1u) {
  403. if (!_rfb_transmit) return;
  404. _rfb_message_queue.push_back(RfbMessage(code, repeats));
  405. }
  406. bool _rfbEnqueue(const char* code, unsigned char repeats = 1u) {
  407. uint8_t buffer[RfbParser::PayloadSizeBasic] { 0u };
  408. if (hexDecode(code, strlen(code), buffer, sizeof(buffer))) {
  409. _rfbEnqueue(buffer, repeats);
  410. return true;
  411. }
  412. DEBUG_MSG_P(PSTR("[RF] Cannot decode the message\n"));
  413. return false;
  414. }
  415. void _rfbSendRaw(const uint8_t* message, unsigned char size) {
  416. Serial.write(message, size);
  417. }
  418. void _rfbAckImpl() {
  419. static uint8_t message[3] {
  420. CodeStart, CodeAck, CodeEnd
  421. };
  422. DEBUG_MSG_P(PSTR("[RF] Sending ACK\n"));
  423. Serial.write(message, sizeof(message));
  424. Serial.flush();
  425. }
  426. void _rfbLearnImpl() {
  427. static uint8_t message[3] {
  428. CodeStart, CodeLearn, CodeEnd
  429. };
  430. DEBUG_MSG_P(PSTR("[RF] Sending LEARN\n"));
  431. Serial.write(message, sizeof(message));
  432. Serial.flush();
  433. }
  434. void _rfbSendImpl(const RfbMessage& message) {
  435. Serial.write(CodeStart);
  436. Serial.write(CodeSendBasic);
  437. _rfbSendRaw(message.code, sizeof(message.code));
  438. Serial.write(CodeEnd);
  439. Serial.flush();
  440. }
  441. void _rfbParse(uint8_t code, const std::vector<uint8_t>& payload) {
  442. switch (code) {
  443. case CodeAck:
  444. DEBUG_MSG_P(PSTR("[RF] Received ACK\n"));
  445. break;
  446. case CodeLearnTimeout:
  447. _rfbAckImpl();
  448. #if RELAY_SUPPORT
  449. if (_rfb_learn) {
  450. DEBUG_MSG_P(PSTR("[RF] Learn timeout after %u ms\n"), millis() - _rfb_learn->ts);
  451. _rfb_learn.reset(nullptr);
  452. }
  453. #endif
  454. break;
  455. case CodeLearnOk:
  456. case CodeRecvBasic: {
  457. _rfbAckImpl();
  458. char buffer[(RfbParser::PayloadSizeBasic * 2) + 1] = {0};
  459. if (hexEncode(payload.data(), payload.size(), buffer, sizeof(buffer))) {
  460. DEBUG_MSG_P(PSTR("[RF] Received code: %s\n"), buffer);
  461. #if RELAY_SUPPORT
  462. if (CodeLearnOk == code) {
  463. _rfbLearnFromString(_rfb_learn, buffer);
  464. } else {
  465. _rfbRelayHandler(buffer, true);
  466. }
  467. #endif
  468. #if MQTT_SUPPORT
  469. mqttSend(MQTT_TOPIC_RFIN, buffer, false, false);
  470. #endif
  471. #if BROKER_SUPPORT
  472. RfbridgeBroker::Publish(RfbDefaultProtocol, buffer + 6);
  473. #endif
  474. }
  475. break;
  476. }
  477. case CodeRecvProto:
  478. case CodeRecvBucket: {
  479. _rfbAckImpl();
  480. char buffer[(RfbParser::MessageSizeMax * 2) + 1] = {0};
  481. if (hexEncode(payload.data(), payload.size(), buffer, sizeof(buffer))) {
  482. DEBUG_MSG_P(PSTR("[RF] Received %s code: %s\n"),
  483. (CodeRecvProto == code) ? "advanced" : "bucket", buffer
  484. );
  485. #if MQTT_SUPPORT
  486. mqttSend(MQTT_TOPIC_RFIN, buffer, false, false);
  487. #endif
  488. #if BROKER_SUPPORT
  489. // ref. https://github.com/Portisch/RF-Bridge-EFM8BB1/wiki/0xA6#example-of-a-received-decoded-protocol
  490. RfbridgeBroker::Publish(payload[0], buffer + 2);
  491. #endif
  492. } else {
  493. DEBUG_MSG_P(PSTR("[RF] Received 0x%02X (%u bytes)\n"), code, payload.size());
  494. }
  495. break;
  496. }
  497. }
  498. }
  499. static RfbParser _rfb_parser(_rfbParse);
  500. void _rfbReceiveImpl() {
  501. while (Serial.available()) {
  502. auto c = Serial.read();
  503. if (c < 0) {
  504. continue;
  505. }
  506. // narrowing is justified, as `c` can only contain byte-sized value
  507. if (!_rfb_parser.loop(static_cast<uint8_t>(c))) {
  508. _rfb_parser.reset();
  509. }
  510. }
  511. }
  512. // note that we don't care about queue here, just dump raw message as-is
  513. void _rfbSendRawFromPayload(const char * raw) {
  514. auto rawlen = strlen(raw);
  515. if (rawlen > (RfbParser::MessageSizeMax * 2)) return;
  516. if ((rawlen < 6) || (rawlen & 1)) return;
  517. DEBUG_MSG_P(PSTR("[RF] Sending RAW MESSAGE \"%s\"\n"), raw);
  518. size_t bytes = 0;
  519. uint8_t message[RfbParser::MessageSizeMax] { 0u };
  520. if ((bytes = hexDecode(raw, rawlen, message, sizeof(message)))) {
  521. if (message[0] != CodeStart) return;
  522. if (message[bytes - 1] != CodeEnd) return;
  523. _rfbSendRaw(message, bytes);
  524. }
  525. }
  526. #elif RFB_PROVIDER == RFB_PROVIDER_RCSWITCH
  527. namespace {
  528. size_t _rfb_bytes_for_bits(size_t bits) {
  529. decltype(bits) bytes = 0;
  530. decltype(bits) need = 0;
  531. while (need < bits) {
  532. need += 8u;
  533. bytes += 1u;
  534. }
  535. return bytes;
  536. }
  537. // TODO: RCSwitch code type: long unsigned int != uint32_t, thus the specialization
  538. static_assert(sizeof(uint32_t) == sizeof(long unsigned int), "");
  539. template <typename T>
  540. T _rfb_bswap(T value);
  541. template <>
  542. [[gnu::unused]] uint32_t _rfb_bswap(uint32_t value) {
  543. return __builtin_bswap32(value);
  544. }
  545. template <>
  546. [[gnu::unused]] long unsigned int _rfb_bswap(long unsigned int value) {
  547. return __builtin_bswap32(value);
  548. }
  549. template <>
  550. [[gnu::unused]] uint64_t _rfb_bswap(uint64_t value) {
  551. return __builtin_bswap64(value);
  552. }
  553. }
  554. void _rfbEnqueue(uint8_t protocol, uint16_t timing, uint8_t bits, RfbMessage::code_type code, unsigned char repeats = 1u) {
  555. if (!_rfb_transmit) return;
  556. _rfb_message_queue.push_back(RfbMessage{protocol, timing, bits, code, repeats});
  557. }
  558. void _rfbEnqueue(const char* message, unsigned char repeats = 1u) {
  559. uint8_t buffer[RfbMessage::BufferSize] { 0u };
  560. if (hexDecode(message, strlen(message), buffer, sizeof(buffer))) {
  561. const auto bytes = _rfb_bytes_for_bits(buffer[4]);
  562. uint8_t raw_code[sizeof(RfbMessage::code_type)] { 0u };
  563. std::memcpy(&raw_code[sizeof(raw_code) - bytes], &buffer[5], bytes);
  564. RfbMessage::code_type code;
  565. std::memcpy(&code, raw_code, sizeof(code));
  566. _rfbEnqueue(buffer[1], (buffer[2] << 8) | buffer[3], buffer[4], _rfb_bswap(code), repeats);
  567. return;
  568. }
  569. DEBUG_MSG_P(PSTR("[RF] Cannot decode the message\n"));
  570. }
  571. void _rfbLearnImpl() {
  572. DEBUG_MSG_P(PSTR("[RF] Entering LEARN mode\n"));
  573. }
  574. void _rfbSendImpl(const RfbMessage& message) {
  575. if (!_rfb_transmit) return;
  576. // TODO: note that this seems to be setting global setting
  577. // if code for some reason forgets this, we end up with the previous value
  578. _rfb_modem->setProtocol(message.protocol);
  579. if (message.timing) {
  580. _rfb_modem->setPulseLength(message.timing);
  581. }
  582. yield();
  583. _rfb_modem->send(message.code, message.bits);
  584. _rfb_modem->resetAvailable();
  585. }
  586. // Try to mimic the basic RF message format. although, we might have different size of the code itself
  587. // Skip leading zeroes and only keep the useful data
  588. //
  589. // TODO: 'timing' value shooould be relatively small,
  590. // since it's original intent was to be used with 16bit ints
  591. // TODO: both 'protocol' and 'bitlength' fit in a byte, despite being declared as 'unsigned int'
  592. size_t _rfbModemPack(uint8_t (&out)[RfbMessage::BufferSize], RfbMessage::code_type code, unsigned int protocol, unsigned int timing, unsigned int bits) {
  593. static_assert((sizeof(decltype(code)) == 4) || (sizeof(decltype(code)) == 8), "");
  594. size_t index = 0;
  595. out[index++] = 0xC0;
  596. out[index++] = static_cast<uint8_t>(protocol);
  597. out[index++] = static_cast<uint8_t>(timing >> 8);
  598. out[index++] = static_cast<uint8_t>(timing);
  599. out[index++] = static_cast<uint8_t>(bits);
  600. auto bytes = _rfb_bytes_for_bits(bits);
  601. if (bytes > (sizeof(out) - index)) {
  602. return 0;
  603. }
  604. // manually overload each bswap, since we can't use ternary here
  605. // (and `if constexpr (...)` is only available starting from Arduino Core 3.x.x)
  606. decltype(code) swapped = _rfb_bswap(code);
  607. uint8_t raw[sizeof(swapped)];
  608. std::memcpy(raw, &swapped, sizeof(raw));
  609. while (bytes) {
  610. out[index++] = raw[sizeof(raw) - (bytes--)];
  611. }
  612. return index;
  613. }
  614. void _rfbLearnFromReceived(std::unique_ptr<RfbLearn>& learn, const char* buffer) {
  615. if (millis() - learn->ts > RFB_LEARN_TIMEOUT) {
  616. DEBUG_MSG_P(PSTR("[RF] Learn timeout after %u ms\n"), millis() - learn->ts);
  617. learn.reset(nullptr);
  618. return;
  619. }
  620. _rfbLearnFromString(learn, buffer);
  621. }
  622. void _rfbReceiveImpl() {
  623. if (!_rfb_receive) return;
  624. // TODO: rc-switch isr handler sets 4 variables at the same time and never checks their existence before overwriting them
  625. // thus, we can't *really* trust that all 4 are from the same reading :/
  626. // TODO: in theory, we may also expirience memory tearing while doing 2 separate 32bit reads on the 64bit code value,
  627. // while isr handler *may* write into it at the same time
  628. auto rf_code = _rfb_modem->getReceivedValue();
  629. if (!rf_code) {
  630. return;
  631. }
  632. #if RFB_RECEIVE_DELAY
  633. static unsigned long last = 0;
  634. if (millis() - last < RFB_RECEIVE_DELAY) {
  635. _rfb_modem->resetAvailable();
  636. return;
  637. }
  638. last = millis();
  639. #endif
  640. uint8_t message[RfbMessage::BufferSize];
  641. auto real_msgsize = _rfbModemPack(
  642. message,
  643. rf_code,
  644. _rfb_modem->getReceivedProtocol(),
  645. _rfb_modem->getReceivedDelay(),
  646. _rfb_modem->getReceivedBitlength()
  647. );
  648. char buffer[(sizeof(message) * 2) + 1] = {0};
  649. if (hexEncode(message, real_msgsize, buffer, sizeof(buffer))) {
  650. DEBUG_MSG_P(PSTR("[RF] Received code: %s\n"), buffer);
  651. #if RELAY_SUPPORT
  652. if (_rfb_learn) {
  653. _rfbLearnFromReceived(_rfb_learn, buffer);
  654. } else {
  655. _rfbRelayHandler(buffer, true);
  656. }
  657. #endif
  658. #if MQTT_SUPPORT
  659. mqttSend(MQTT_TOPIC_RFIN, buffer, false, false);
  660. #endif
  661. #if BROKER_SUPPORT
  662. RfbridgeBroker::Publish(message[1], buffer + 10);
  663. #endif
  664. }
  665. _rfb_modem->resetAvailable();
  666. }
  667. #endif // RFB_PROVIDER == ...
  668. void _rfbSendQueued() {
  669. if (!_rfb_transmit) return;
  670. if (_rfb_message_queue.empty()) return;
  671. static unsigned long last = 0;
  672. if (millis() - last < RFB_SEND_DELAY) return;
  673. last = millis();
  674. auto message = _rfb_message_queue.front();
  675. _rfb_message_queue.pop_front();
  676. _rfbSendImpl(message);
  677. // Sometimes we really want to repeat the message, not only to rely on built-in transfer repeat
  678. if (message.repeats > 1) {
  679. message.repeats -= 1;
  680. _rfb_message_queue.push_back(std::move(message));
  681. }
  682. yield();
  683. }
  684. // Check if the payload looks like a HEX code (plus comma, specifying the 'repeats' arg for the queue)
  685. void _rfbSendFromPayload(const char * payload) {
  686. size_t repeats { 1ul };
  687. size_t len { strlen(payload) };
  688. const char* sep { strchr(payload, ',') };
  689. if (sep && (*(sep + 1) != '\0')) {
  690. char *endptr = nullptr;
  691. repeats = strtoul(sep, &endptr, 10);
  692. if (endptr == payload || endptr[0] != '\0') {
  693. return;
  694. }
  695. len -= strlen(sep);
  696. }
  697. if (!len || (len & 1)) {
  698. return;
  699. }
  700. DEBUG_MSG_P(PSTR("[RF] Enqueuing MESSAGE '%s' %u time(s)\n"), payload, repeats);
  701. // We postpone the actual sending until the loop, as we may've been called from MQTT or HTTP API
  702. // RFB_PROVIDER implementation should select the appropriate de-serialization function
  703. _rfbEnqueue(payload, repeats);
  704. }
  705. void _rfbLearnStartFromPayload(const char* payload) {
  706. // The payload must be the `relayID,mode` (where mode is either 0 or 1)
  707. const char* sep = strchr(payload, ',');
  708. if (nullptr == sep) {
  709. return;
  710. }
  711. // ref. RelaysMax, we only have up to 2 digits
  712. char relay[3] {0, 0, 0};
  713. if ((sep - payload) > 2) {
  714. return;
  715. }
  716. std::copy(payload, sep, relay);
  717. char *endptr = nullptr;
  718. const auto id = strtoul(relay, &endptr, 10);
  719. if (endptr == &relay[0] || endptr[0] != '\0') {
  720. return;
  721. }
  722. if (id >= relayCount()) {
  723. DEBUG_MSG_P(PSTR("[RF] Invalid relay ID (%u)\n"), id);
  724. return;
  725. }
  726. ++sep;
  727. if ((*sep == '0') || (*sep == '1')) {
  728. rfbLearn(id, (*sep != '0'));
  729. }
  730. }
  731. #if MQTT_SUPPORT
  732. void _rfbMqttCallback(unsigned int type, const char * topic, char * payload) {
  733. if (type == MQTT_CONNECT_EVENT) {
  734. char buffer[strlen(MQTT_TOPIC_RFLEARN) + 3];
  735. snprintf_P(buffer, sizeof(buffer), PSTR("%s/+"), MQTT_TOPIC_RFLEARN);
  736. mqttSubscribe(buffer);
  737. if (_rfb_transmit) {
  738. mqttSubscribe(MQTT_TOPIC_RFOUT);
  739. }
  740. #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
  741. mqttSubscribe(MQTT_TOPIC_RFRAW);
  742. #endif
  743. }
  744. if (type == MQTT_MESSAGE_EVENT) {
  745. String t = mqttMagnitude((char *) topic);
  746. if (t.startsWith(MQTT_TOPIC_RFLEARN)) {
  747. _rfbLearnStartFromPayload(payload);
  748. return;
  749. }
  750. if (t.equals(MQTT_TOPIC_RFOUT)) {
  751. #if RELAY_SUPPORT
  752. // we *sometimes* want to check the code against available rfbON / rfbOFF
  753. // e.g. in case we want to control some external device and have an external remote.
  754. // - when remote press happens, relays stay in sync when we receive the code via the processing loop
  755. // - when we send the code here, we never register it as *sent*, thus relays need to be made in sync manually
  756. if (!_rfbRelayHandler(payload)) {
  757. #endif
  758. _rfbSendFromPayload(payload);
  759. #if RELAY_SUPPORT
  760. }
  761. #endif
  762. return;
  763. }
  764. #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
  765. if (t.equals(MQTT_TOPIC_RFRAW)) {
  766. // in case this is RAW message, we should not match anything and just send it as-is to the serial
  767. _rfbSendRawFromPayload(payload);
  768. return;
  769. }
  770. #endif
  771. }
  772. }
  773. #endif // MQTT_SUPPORT
  774. #if API_SUPPORT
  775. void _rfbApiSetup() {
  776. apiReserve(3u);
  777. apiRegister({
  778. MQTT_TOPIC_RFOUT, Api::Type::Basic, ApiUnusedArg,
  779. apiOk, // just a stub, nothing to return
  780. [](const Api&, ApiBuffer& buffer) {
  781. _rfbSendFromPayload(buffer.data);
  782. }
  783. });
  784. apiRegister({
  785. MQTT_TOPIC_RFLEARN, Api::Type::Basic, ApiUnusedArg,
  786. [](const Api&, ApiBuffer& buffer) {
  787. if (_rfb_learn) {
  788. snprintf_P(buffer.data, buffer.size, PSTR("learning id:%u,status:%c"),
  789. _rfb_learn->id, _rfb_learn->status ? 't' : 'f'
  790. );
  791. } else {
  792. snprintf_P(buffer.data, buffer.size, PSTR("waiting"));
  793. }
  794. },
  795. [](const Api&, ApiBuffer& buffer) {
  796. _rfbLearnStartFromPayload(buffer.data);
  797. }
  798. });
  799. #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
  800. apiRegister({
  801. MQTT_TOPIC_RFRAW, Api::Type::Basic, ApiUnusedArg,
  802. apiOk, // just a stub, nothing to return
  803. [](const Api&, ApiBuffer& buffer) {
  804. _rfbSendRawFromPayload(buffer.data);
  805. }
  806. });
  807. #endif
  808. }
  809. #endif // API_SUPPORT
  810. #if TERMINAL_SUPPORT
  811. void _rfbInitCommands() {
  812. #if RELAY_SUPPORT
  813. terminalRegisterCommand(F("RFB.LEARN"), [](const terminal::CommandContext& ctx) {
  814. if (ctx.argc != 3) {
  815. terminalError(ctx, F("RFB.LEARN <ID> <STATUS>"));
  816. return;
  817. }
  818. int id = ctx.argv[1].toInt();
  819. if (id >= relayCount()) {
  820. terminalError(ctx, F("Invalid relay ID"));
  821. return;
  822. }
  823. rfbLearn(id, (ctx.argv[2].toInt()) == 1);
  824. terminalOK(ctx);
  825. });
  826. terminalRegisterCommand(F("RFB.FORGET"), [](const terminal::CommandContext& ctx) {
  827. if (ctx.argc < 2) {
  828. terminalError(ctx, F("RFB.FORGET <ID> [<STATUS>]"));
  829. return;
  830. }
  831. int id = ctx.argv[1].toInt();
  832. if (id >= relayCount()) {
  833. terminalError(ctx, F("Invalid relay ID"));
  834. return;
  835. }
  836. if (ctx.argc == 3) {
  837. rfbForget(id, (ctx.argv[2].toInt()) == 1);
  838. } else {
  839. rfbForget(id, true);
  840. rfbForget(id, false);
  841. }
  842. terminalOK(ctx);
  843. });
  844. #endif // if RELAY_SUPPORT
  845. #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
  846. terminalRegisterCommand(F("RFB.WRITE"), [](const terminal::CommandContext& ctx) {
  847. if (ctx.argc != 2) {
  848. terminalError(ctx, F("RFB.WRITE <PAYLOAD>"));
  849. return;
  850. }
  851. _rfbSendRawFromPayload(ctx.argv[1].c_str());
  852. terminalOK(ctx);
  853. });
  854. #endif
  855. }
  856. #endif // TERMINAL_SUPPORT
  857. // -----------------------------------------------------------------------------
  858. // PUBLIC
  859. // -----------------------------------------------------------------------------
  860. void rfbStore(unsigned char id, bool status, const char * code) {
  861. settings_key_t key { status ? F("rfbON") : F("rfbOFF"), id };
  862. setSetting(key, code);
  863. DEBUG_MSG_P(PSTR("[RF] Saved %s => \"%s\"\n"), key.toString().c_str(), code);
  864. }
  865. String rfbRetrieve(unsigned char id, bool status) {
  866. return getSetting({ status ? F("rfbON") : F("rfbOFF"), id });
  867. }
  868. void rfbStatus(unsigned char id, bool status) {
  869. // TODO: This is a left-over from the old implementation. Right now we set this lock when relay handler
  870. // is called within the receiver, while this is called from either relayStatus or relay loop calling
  871. // this via provider callback. This prevents us from re-sending the code we just received.
  872. // TODO: Consider having 'origin' of the relay change. Either supply relayStatus with an additional arg,
  873. // or track these statuses directly.
  874. if (!_rfb_relay_status_lock[id]) {
  875. String value = rfbRetrieve(id, status);
  876. if (value.length() && !(value.length() & 1)) {
  877. _rfbSendFromPayload(value.c_str());
  878. }
  879. }
  880. _rfb_relay_status_lock[id] = false;
  881. }
  882. void rfbLearn(unsigned char id, bool status) {
  883. _rfb_learn.reset(new RfbLearn { millis(), id, status });
  884. _rfbLearnImpl();
  885. }
  886. void rfbForget(unsigned char id, bool status) {
  887. delSetting({status ? F("rfbON") : F("rfbOFF"), id});
  888. // Websocket update needs to happen right here, since the only time
  889. // we send these in bulk is at the very start of the connection
  890. #if WEB_SUPPORT
  891. wsPost([id](JsonObject& root) {
  892. _rfbWebSocketSendCodeArray(root, id, 1);
  893. });
  894. #endif
  895. }
  896. // -----------------------------------------------------------------------------
  897. // SETUP & LOOP
  898. // -----------------------------------------------------------------------------
  899. #if RFB_PROVIDER == RFB_PROVIDER_RCSWITCH
  900. // TODO: remove this in 1.16.0
  901. void _rfbSettingsMigrate(int version) {
  902. if (!version || (version > 4)) {
  903. return;
  904. }
  905. auto migrate_code = [](const String& in, String& out) -> bool {
  906. out = "";
  907. if (18 == in.length()) {
  908. uint8_t bits { 0u };
  909. if (!hexDecode(in.c_str() + 8, 2, &bits, 1)) {
  910. return false;
  911. }
  912. auto bytes = _rfb_bytes_for_bits(bits);
  913. out = in.substring(0, 10);
  914. out += (in.c_str() + in.length() - (2 * bytes));
  915. return in != out;
  916. }
  917. return false;
  918. };
  919. String buffer;
  920. for (unsigned char index = 0; index < relayCount(); ++index) {
  921. const settings_key_t on_key {F("rfbON"), index};
  922. if (migrate_code(getSetting(on_key), buffer)) {
  923. setSetting(on_key, buffer);
  924. }
  925. const settings_key_t off_key {F("rfbOFF"), index};
  926. if (migrate_code(getSetting(off_key), buffer)) {
  927. setSetting(off_key, buffer);
  928. }
  929. }
  930. }
  931. #endif
  932. void rfbSetup() {
  933. #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
  934. _rfb_parser.reserve(RfbParser::MessageSizeBasic);
  935. #elif RFB_PROVIDER == RFB_PROVIDER_RCSWITCH
  936. _rfbSettingsMigrate(migrateVersion());
  937. {
  938. auto rx = getSetting("rfbRX", RFB_RX_PIN);
  939. auto tx = getSetting("rfbTX", RFB_TX_PIN);
  940. // TODO: tag gpioGetLock with a NAME string, skip log here
  941. _rfb_receive = gpioValid(rx);
  942. _rfb_transmit = gpioValid(tx);
  943. if (!_rfb_transmit && !_rfb_receive) {
  944. DEBUG_MSG_P(PSTR("[RF] Neither RX or TX are set\n"));
  945. return;
  946. }
  947. _rfb_modem = new RCSwitch();
  948. if (_rfb_receive) {
  949. _rfb_modem->enableReceive(rx);
  950. DEBUG_MSG_P(PSTR("[RF] RF receiver on GPIO %u\n"), rx);
  951. }
  952. if (_rfb_transmit) {
  953. auto transmit = getSetting("rfbTransmit", RFB_TRANSMIT_TIMES);
  954. _rfb_modem->enableTransmit(tx);
  955. _rfb_modem->setRepeatTransmit(transmit);
  956. DEBUG_MSG_P(PSTR("[RF] RF transmitter on GPIO %u\n"), tx);
  957. }
  958. }
  959. #endif
  960. #if MQTT_SUPPORT
  961. mqttRegister(_rfbMqttCallback);
  962. #endif
  963. #if API_SUPPORT
  964. _rfbApiSetup();
  965. #endif
  966. #if WEB_SUPPORT
  967. wsRegister()
  968. .onVisible(_rfbWebSocketOnVisible)
  969. .onConnected(_rfbWebSocketOnConnected)
  970. .onData(_rfbWebSocketOnData)
  971. .onAction(_rfbWebSocketOnAction)
  972. .onKeyCheck(_rfbWebSocketOnKeyCheck);
  973. #endif
  974. #if TERMINAL_SUPPORT
  975. _rfbInitCommands();
  976. #endif
  977. _rfb_repeat = getSetting("rfbRepeat", RFB_SEND_TIMES);
  978. // Note: as rfbridge protocol is simplistic enough, we rely on Serial queue to deliver timely updates
  979. // learn / command acks / etc. are not queued, only RF messages are
  980. espurnaRegisterLoop([]() {
  981. _rfbReceiveImpl();
  982. _rfbSendQueued();
  983. });
  984. }
  985. #endif // RFB_SUPPORT