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.

1307 lines
36 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
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_repeats = RFB_SEND_REPEATS;
  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 _rfbWebSocketOnVisible(JsonObject& root) {
  257. root["rfbVisible"] = 1;
  258. }
  259. #if RELAY_SUPPORT
  260. void _rfbWebSocketSendCodeArray(JsonObject& root, unsigned char start, unsigned char size) {
  261. JsonObject& rfb = root.createNestedObject("rfb");
  262. rfb["size"] = size;
  263. rfb["start"] = start;
  264. JsonArray& on = rfb.createNestedArray("on");
  265. JsonArray& off = rfb.createNestedArray("off");
  266. for (uint8_t id=start; id<start+size; id++) {
  267. on.add(rfbRetrieve(id, true));
  268. off.add(rfbRetrieve(id, false));
  269. }
  270. }
  271. void _rfbWebSocketOnData(JsonObject& root) {
  272. _rfbWebSocketSendCodeArray(root, 0, relayCount());
  273. }
  274. #endif // RELAY_SUPPORT
  275. void _rfbWebSocketOnConnected(JsonObject& root) {
  276. root["rfbRepeat"] = getSetting("rfbRepeat", RFB_SEND_REPEATS);
  277. #if RELAY_SUPPORT
  278. root["rfbCount"] = relayCount();
  279. #endif
  280. #if RFB_PROVIDER == RFB_PROVIDER_RCSWITCH
  281. root["rfbdirectVisible"] = 1;
  282. root["rfbRX"] = getSetting("rfbRX", RFB_RX_PIN);
  283. root["rfbTX"] = getSetting("rfbTX", RFB_TX_PIN);
  284. #endif
  285. }
  286. void _rfbWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) {
  287. #if RELAY_SUPPORT
  288. if (strcmp(action, "rfblearn") == 0) rfbLearn(data["id"], data["status"]);
  289. if (strcmp(action, "rfbforget") == 0) rfbForget(data["id"], data["status"]);
  290. if (strcmp(action, "rfbsend") == 0) rfbStore(data["id"], data["status"], data["data"].as<const char*>());
  291. #endif
  292. }
  293. bool _rfbWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
  294. return (strncmp(key, "rfb", 3) == 0);
  295. }
  296. #endif // WEB_SUPPORT
  297. // -----------------------------------------------------------------------------
  298. // RELAY <-> CODE MATCHING
  299. // -----------------------------------------------------------------------------
  300. #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
  301. // we only care about last 6 chars (3 bytes in hex),
  302. // since in 'default' mode rfbridge only handles a single protocol
  303. bool _rfbCompare(const char* lhs, const char* rhs, size_t length) {
  304. return (0 == std::memcmp((lhs + length - 6), (rhs + length - 6), 6));
  305. }
  306. #elif RFB_PROVIDER == RFB_PROVIDER_RCSWITCH
  307. // protocol is [2:3), actual payload is [10:), as bit length may vary
  308. // although, we don't care if it does, since we expect length of both args to be the same
  309. bool _rfbCompare(const char* lhs, const char* rhs, size_t length) {
  310. return (0 == std::memcmp((lhs + 2), (rhs + 2), 2))
  311. && (0 == std::memcmp((lhs + 10), (rhs + 10), length - 10));
  312. }
  313. #endif // RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
  314. #if RELAY_SUPPORT
  315. // try to find the 'code' saves as either rfbON# or rfbOFF#
  316. //
  317. // **always** expect full length code as input to simplify comparison
  318. // previous implementation tried to help MQTT / API requests to match based on the saved code,
  319. // thus requiring us to 'return' value from settings as the real code, replacing input
  320. RfbRelayMatch _rfbMatch(const char* code) {
  321. if (!relayCount()) {
  322. return {};
  323. }
  324. const auto len = strlen(code);
  325. // we gather all available options, as the kv store might be defined in any order
  326. // scan kvs only once, since we want both ON and OFF options and don't want to depend on the relayCount()
  327. RfbRelayMatch matched;
  328. using namespace settings;
  329. kv_store.foreach([code, len, &matched](kvs_type::KeyValueResult&& kv) {
  330. const auto key = kv.key.read();
  331. PayloadStatus status = key.startsWith(F("rfbON"))
  332. ? PayloadStatus::On : key.startsWith(F("rfbOFF"))
  333. ? PayloadStatus::Off : PayloadStatus::Unknown;
  334. if (PayloadStatus::Unknown == status) {
  335. return;
  336. }
  337. const auto value = kv.value.read();
  338. if (len != value.length()) {
  339. return;
  340. }
  341. if (!_rfbCompare(code, value.c_str(), len)) {
  342. return;
  343. }
  344. // note: strlen is constexpr here
  345. const char* id_ptr = key.c_str() + (
  346. (PayloadStatus::On == status) ? strlen("rfbON") : strlen("rfbOFF"));
  347. if (*id_ptr == '\0') {
  348. return;
  349. }
  350. char *endptr = nullptr;
  351. const auto id = strtoul(id_ptr, &endptr, 10);
  352. if (endptr == id_ptr || endptr[0] != '\0' || id > std::numeric_limits<uint8_t>::max() || id >= relayCount()) {
  353. return;
  354. }
  355. // when we see the same id twice, we match the opposite statuses
  356. if (matched.ok() && (id == matched.id)) {
  357. matched.status = PayloadStatus::Toggle;
  358. return;
  359. }
  360. matched.reset(matched.ok()
  361. ? std::min(static_cast<uint8_t>(id), matched.id)
  362. : static_cast<uint8_t>(id),
  363. status
  364. );
  365. });
  366. return matched;
  367. }
  368. void _rfbLearnFromString(std::unique_ptr<RfbLearn>& learn, const char* buffer) {
  369. if (!learn) return;
  370. DEBUG_MSG_P(PSTR("[RF] Learned relay ID %u after %u ms\n"), learn->id, millis() - learn->ts);
  371. rfbStore(learn->id, learn->status, buffer);
  372. // Websocket update needs to happen right here, since the only time
  373. // we send these in bulk is at the very start of the connection
  374. #if WEB_SUPPORT
  375. auto id = learn->id;
  376. wsPost([id](JsonObject& root) {
  377. _rfbWebSocketSendCodeArray(root, id, 1);
  378. });
  379. #endif
  380. learn.reset(nullptr);
  381. }
  382. bool _rfbRelayHandler(const char* buffer, bool locked = false) {
  383. bool result { false };
  384. auto match = _rfbMatch(buffer);
  385. if (match.ok()) {
  386. DEBUG_MSG_P(PSTR("[RF] Matched with the relay ID %u\n"), match.id);
  387. _rfb_relay_status_lock.set(match.id, locked);
  388. switch (match.status) {
  389. case PayloadStatus::On:
  390. case PayloadStatus::Off:
  391. relayStatus(match.id, (PayloadStatus::On == match.status));
  392. result = true;
  393. break;
  394. case PayloadStatus::Toggle:
  395. relayToggle(match.id);
  396. result = true;
  397. case PayloadStatus::Unknown:
  398. break;
  399. }
  400. }
  401. return result;
  402. }
  403. #endif // RELAY_SUPPORT
  404. // -----------------------------------------------------------------------------
  405. // RF handler implementations
  406. // -----------------------------------------------------------------------------
  407. #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
  408. void _rfbEnqueue(uint8_t (&code)[RfbParser::PayloadSizeBasic], unsigned char repeats = 1u) {
  409. if (!_rfb_transmit) return;
  410. _rfb_message_queue.push_back(RfbMessage(code, repeats));
  411. }
  412. bool _rfbEnqueue(const char* code, size_t length, unsigned char repeats = 1u) {
  413. uint8_t buffer[RfbParser::PayloadSizeBasic] { 0u };
  414. if (hexDecode(code, length, buffer, sizeof(buffer))) {
  415. _rfbEnqueue(buffer, repeats);
  416. return true;
  417. }
  418. DEBUG_MSG_P(PSTR("[RF] Cannot decode the message\n"));
  419. return false;
  420. }
  421. void _rfbSendRaw(const uint8_t* message, unsigned char size) {
  422. Serial.write(message, size);
  423. }
  424. void _rfbAckImpl() {
  425. static uint8_t message[3] {
  426. CodeStart, CodeAck, CodeEnd
  427. };
  428. DEBUG_MSG_P(PSTR("[RF] Sending ACK\n"));
  429. Serial.write(message, sizeof(message));
  430. Serial.flush();
  431. }
  432. void _rfbLearnImpl() {
  433. static uint8_t message[3] {
  434. CodeStart, CodeLearn, CodeEnd
  435. };
  436. DEBUG_MSG_P(PSTR("[RF] Sending LEARN\n"));
  437. Serial.write(message, sizeof(message));
  438. Serial.flush();
  439. }
  440. void _rfbSendImpl(const RfbMessage& message) {
  441. Serial.write(CodeStart);
  442. Serial.write(CodeSendBasic);
  443. _rfbSendRaw(message.code, sizeof(message.code));
  444. Serial.write(CodeEnd);
  445. Serial.flush();
  446. }
  447. void _rfbParse(uint8_t code, const std::vector<uint8_t>& payload) {
  448. switch (code) {
  449. case CodeAck:
  450. DEBUG_MSG_P(PSTR("[RF] Received ACK\n"));
  451. break;
  452. case CodeLearnTimeout:
  453. _rfbAckImpl();
  454. #if RELAY_SUPPORT
  455. if (_rfb_learn) {
  456. DEBUG_MSG_P(PSTR("[RF] Learn timeout after %u ms\n"), millis() - _rfb_learn->ts);
  457. _rfb_learn.reset(nullptr);
  458. }
  459. #endif
  460. break;
  461. case CodeLearnOk:
  462. case CodeRecvBasic: {
  463. _rfbAckImpl();
  464. char buffer[(RfbParser::PayloadSizeBasic * 2) + 1] = {0};
  465. if (hexEncode(payload.data(), payload.size(), buffer, sizeof(buffer))) {
  466. DEBUG_MSG_P(PSTR("[RF] Received code: %s\n"), buffer);
  467. #if RELAY_SUPPORT
  468. if (CodeLearnOk == code) {
  469. _rfbLearnFromString(_rfb_learn, buffer);
  470. } else {
  471. _rfbRelayHandler(buffer, true);
  472. }
  473. #endif
  474. #if MQTT_SUPPORT
  475. mqttSend(MQTT_TOPIC_RFIN, buffer, false, false);
  476. #endif
  477. #if BROKER_SUPPORT
  478. RfbridgeBroker::Publish(RfbDefaultProtocol, buffer + 12);
  479. #endif
  480. }
  481. break;
  482. }
  483. case CodeRecvProto:
  484. case CodeRecvBucket: {
  485. _rfbAckImpl();
  486. char buffer[(RfbParser::MessageSizeMax * 2) + 1] = {0};
  487. if (hexEncode(payload.data(), payload.size(), buffer, sizeof(buffer))) {
  488. DEBUG_MSG_P(PSTR("[RF] Received %s code: %s\n"),
  489. (CodeRecvProto == code) ? "advanced" : "bucket", buffer
  490. );
  491. #if MQTT_SUPPORT
  492. mqttSend(MQTT_TOPIC_RFIN, buffer, false, false);
  493. #endif
  494. #if BROKER_SUPPORT
  495. // ref. https://github.com/Portisch/RF-Bridge-EFM8BB1/wiki/0xA6#example-of-a-received-decoded-protocol
  496. RfbridgeBroker::Publish(payload[0], buffer + 2);
  497. #endif
  498. } else {
  499. DEBUG_MSG_P(PSTR("[RF] Received 0x%02X (%u bytes)\n"), code, payload.size());
  500. }
  501. break;
  502. }
  503. }
  504. }
  505. static RfbParser _rfb_parser(_rfbParse);
  506. void _rfbReceiveImpl() {
  507. while (Serial.available()) {
  508. auto c = Serial.read();
  509. if (c < 0) {
  510. continue;
  511. }
  512. // narrowing is justified, as `c` can only contain byte-sized value
  513. if (!_rfb_parser.loop(static_cast<uint8_t>(c))) {
  514. _rfb_parser.reset();
  515. }
  516. }
  517. }
  518. // note that we don't care about queue here, just dump raw message as-is
  519. void _rfbSendRawFromPayload(const char * raw) {
  520. auto rawlen = strlen(raw);
  521. if (rawlen > (RfbParser::MessageSizeMax * 2)) return;
  522. if ((rawlen < 6) || (rawlen & 1)) return;
  523. DEBUG_MSG_P(PSTR("[RF] Sending RAW MESSAGE \"%s\"\n"), raw);
  524. size_t bytes = 0;
  525. uint8_t message[RfbParser::MessageSizeMax] { 0u };
  526. if ((bytes = hexDecode(raw, rawlen, message, sizeof(message)))) {
  527. if (message[0] != CodeStart) return;
  528. if (message[bytes - 1] != CodeEnd) return;
  529. _rfbSendRaw(message, bytes);
  530. }
  531. }
  532. #elif RFB_PROVIDER == RFB_PROVIDER_RCSWITCH
  533. namespace {
  534. size_t _rfb_bytes_for_bits(size_t bits) {
  535. decltype(bits) bytes = 0;
  536. decltype(bits) need = 0;
  537. while (need < bits) {
  538. need += 8u;
  539. bytes += 1u;
  540. }
  541. return bytes;
  542. }
  543. // TODO: RCSwitch code type: long unsigned int != uint32_t, thus the specialization
  544. static_assert(sizeof(uint32_t) == sizeof(long unsigned int), "");
  545. template <typename T>
  546. T _rfb_bswap(T value);
  547. template <>
  548. [[gnu::unused]] uint32_t _rfb_bswap(uint32_t value) {
  549. return __builtin_bswap32(value);
  550. }
  551. template <>
  552. [[gnu::unused]] long unsigned int _rfb_bswap(long unsigned int value) {
  553. return __builtin_bswap32(value);
  554. }
  555. template <>
  556. [[gnu::unused]] uint64_t _rfb_bswap(uint64_t value) {
  557. return __builtin_bswap64(value);
  558. }
  559. }
  560. void _rfbEnqueue(uint8_t protocol, uint16_t timing, uint8_t bits, RfbMessage::code_type code, unsigned char repeats = 1u) {
  561. if (!_rfb_transmit) return;
  562. _rfb_message_queue.push_back(RfbMessage{protocol, timing, bits, code, repeats});
  563. }
  564. void _rfbEnqueue(const char* message, size_t length, unsigned char repeats = 1u) {
  565. uint8_t buffer[RfbMessage::BufferSize] { 0u };
  566. if (hexDecode(message, length, buffer, sizeof(buffer))) {
  567. const auto bytes = _rfb_bytes_for_bits(buffer[4]);
  568. uint8_t raw_code[sizeof(RfbMessage::code_type)] { 0u };
  569. std::memcpy(&raw_code[sizeof(raw_code) - bytes], &buffer[5], bytes);
  570. RfbMessage::code_type code;
  571. std::memcpy(&code, raw_code, sizeof(code));
  572. _rfbEnqueue(buffer[1], (buffer[2] << 8) | buffer[3], buffer[4], _rfb_bswap(code), repeats);
  573. return;
  574. }
  575. DEBUG_MSG_P(PSTR("[RF] Cannot decode the message\n"));
  576. }
  577. void _rfbLearnImpl() {
  578. DEBUG_MSG_P(PSTR("[RF] Entering LEARN mode\n"));
  579. }
  580. void _rfbSendImpl(const RfbMessage& message) {
  581. if (!_rfb_transmit) return;
  582. // TODO: note that this seems to be setting global setting
  583. // if code for some reason forgets this, we end up with the previous value
  584. _rfb_modem->setProtocol(message.protocol);
  585. if (message.timing) {
  586. _rfb_modem->setPulseLength(message.timing);
  587. }
  588. yield();
  589. _rfb_modem->send(message.code, message.bits);
  590. _rfb_modem->resetAvailable();
  591. }
  592. // Try to mimic the basic RF message format. although, we might have different size of the code itself
  593. // Skip leading zeroes and only keep the useful data
  594. //
  595. // TODO: 'timing' value shooould be relatively small,
  596. // since it's original intent was to be used with 16bit ints
  597. // TODO: both 'protocol' and 'bitlength' fit in a byte, despite being declared as 'unsigned int'
  598. size_t _rfbModemPack(uint8_t (&out)[RfbMessage::BufferSize], RfbMessage::code_type code, unsigned int protocol, unsigned int timing, unsigned int bits) {
  599. static_assert((sizeof(decltype(code)) == 4) || (sizeof(decltype(code)) == 8), "");
  600. size_t index = 0;
  601. out[index++] = 0xC0;
  602. out[index++] = static_cast<uint8_t>(protocol);
  603. out[index++] = static_cast<uint8_t>(timing >> 8);
  604. out[index++] = static_cast<uint8_t>(timing);
  605. out[index++] = static_cast<uint8_t>(bits);
  606. auto bytes = _rfb_bytes_for_bits(bits);
  607. if (bytes > (sizeof(out) - index)) {
  608. return 0;
  609. }
  610. // manually overload each bswap, since we can't use ternary here
  611. // (and `if constexpr (...)` is only available starting from Arduino Core 3.x.x)
  612. decltype(code) swapped = _rfb_bswap(code);
  613. uint8_t raw[sizeof(swapped)];
  614. std::memcpy(raw, &swapped, sizeof(raw));
  615. while (bytes) {
  616. out[index++] = raw[sizeof(raw) - (bytes--)];
  617. }
  618. return index;
  619. }
  620. #if RELAY_SUPPORT
  621. void _rfbLearnStartFromPayload(const char* payload) {
  622. // The payload must be the `relayID,mode` (where mode is either 0 or 1)
  623. const char* sep = strchr(payload, ',');
  624. if (nullptr == sep) {
  625. return;
  626. }
  627. // ref. RelaysMax, we only have up to 2 digits
  628. char relay[3] {0, 0, 0};
  629. if ((sep - payload) > 2) {
  630. return;
  631. }
  632. std::copy(payload, sep, relay);
  633. char *endptr = nullptr;
  634. const auto id = strtoul(relay, &endptr, 10);
  635. if (endptr == &relay[0] || endptr[0] != '\0') {
  636. return;
  637. }
  638. if (id >= relayCount()) {
  639. DEBUG_MSG_P(PSTR("[RF] Invalid relay ID (%u)\n"), id);
  640. return;
  641. }
  642. ++sep;
  643. if ((*sep == '0') || (*sep == '1')) {
  644. rfbLearn(id, (*sep != '0'));
  645. }
  646. }
  647. void _rfbLearnFromReceived(std::unique_ptr<RfbLearn>& learn, const char* buffer) {
  648. if (millis() - learn->ts > RFB_LEARN_TIMEOUT) {
  649. DEBUG_MSG_P(PSTR("[RF] Learn timeout after %u ms\n"), millis() - learn->ts);
  650. learn.reset(nullptr);
  651. return;
  652. }
  653. _rfbLearnFromString(learn, buffer);
  654. }
  655. #endif // RELAY_SUPPORT
  656. void _rfbReceiveImpl() {
  657. if (!_rfb_receive) return;
  658. // TODO: rc-switch isr handler sets 4 variables at the same time and never checks their existence before overwriting them
  659. // thus, we can't *really* trust that all 4 are from the same reading :/
  660. // TODO: in theory, we may also expirience memory tearing while doing 2 separate 32bit reads on the 64bit code value,
  661. // while isr handler *may* write into it at the same time
  662. auto rf_code = _rfb_modem->getReceivedValue();
  663. if (!rf_code) {
  664. return;
  665. }
  666. #if RFB_RECEIVE_DELAY
  667. static unsigned long last = 0;
  668. if (millis() - last < RFB_RECEIVE_DELAY) {
  669. _rfb_modem->resetAvailable();
  670. return;
  671. }
  672. last = millis();
  673. #endif
  674. uint8_t message[RfbMessage::BufferSize];
  675. auto real_msgsize = _rfbModemPack(
  676. message,
  677. rf_code,
  678. _rfb_modem->getReceivedProtocol(),
  679. _rfb_modem->getReceivedDelay(),
  680. _rfb_modem->getReceivedBitlength()
  681. );
  682. char buffer[(sizeof(message) * 2) + 1] = {0};
  683. if (hexEncode(message, real_msgsize, buffer, sizeof(buffer))) {
  684. DEBUG_MSG_P(PSTR("[RF] Received code: %s\n"), buffer);
  685. #if RELAY_SUPPORT
  686. if (_rfb_learn) {
  687. _rfbLearnFromReceived(_rfb_learn, buffer);
  688. } else {
  689. _rfbRelayHandler(buffer, true);
  690. }
  691. #endif
  692. #if MQTT_SUPPORT
  693. mqttSend(MQTT_TOPIC_RFIN, buffer, false, false);
  694. #endif
  695. #if BROKER_SUPPORT
  696. RfbridgeBroker::Publish(message[1], buffer + 10);
  697. #endif
  698. }
  699. _rfb_modem->resetAvailable();
  700. }
  701. #endif // RFB_PROVIDER == ...
  702. void _rfbSendQueued() {
  703. if (!_rfb_transmit) return;
  704. if (_rfb_message_queue.empty()) return;
  705. static unsigned long last = 0;
  706. if (millis() - last < RFB_SEND_DELAY) return;
  707. last = millis();
  708. auto message = _rfb_message_queue.front();
  709. _rfb_message_queue.pop_front();
  710. _rfbSendImpl(message);
  711. // Sometimes we really want to repeat the message, not only to rely on built-in transfer repeat
  712. if (message.repeats > 1) {
  713. message.repeats -= 1;
  714. _rfb_message_queue.push_back(std::move(message));
  715. }
  716. yield();
  717. }
  718. // Check if the payload looks like a HEX code (plus comma, specifying the 'repeats' arg for the queue)
  719. void _rfbSendFromPayload(const char * payload) {
  720. decltype(_rfb_repeats) repeats { _rfb_repeats };
  721. size_t len { strlen(payload) };
  722. const char* sep { strchr(payload, ',') };
  723. if (sep) {
  724. len -= strlen(sep);
  725. sep += 1;
  726. if ('\0' == *sep) return;
  727. if ('-' == *sep) return;
  728. char *endptr = nullptr;
  729. repeats = strtoul(sep, &endptr, 10);
  730. if (endptr == payload || endptr[0] != '\0') {
  731. return;
  732. }
  733. }
  734. if (!len || (len & 1)) {
  735. return;
  736. }
  737. DEBUG_MSG_P(PSTR("[RF] Enqueuing MESSAGE '%s' %u time(s)\n"), payload, repeats);
  738. // We postpone the actual sending until the loop, as we may've been called from MQTT or HTTP API
  739. // RFB_PROVIDER implementation should select the appropriate de-serialization function
  740. _rfbEnqueue(payload, len, repeats);
  741. }
  742. #if MQTT_SUPPORT
  743. void _rfbMqttCallback(unsigned int type, const char * topic, char * payload) {
  744. if (type == MQTT_CONNECT_EVENT) {
  745. #if RELAY_SUPPORT
  746. mqttSubscribe(MQTT_TOPIC_RFLEARN);
  747. #endif
  748. #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
  749. mqttSubscribe(MQTT_TOPIC_RFRAW);
  750. #endif
  751. if (_rfb_transmit) {
  752. mqttSubscribe(MQTT_TOPIC_RFOUT);
  753. }
  754. return;
  755. }
  756. if (type == MQTT_MESSAGE_EVENT) {
  757. String t = mqttMagnitude((char *) topic);
  758. #if RELAY_SUPPORT
  759. if (t.equals(MQTT_TOPIC_RFLEARN)) {
  760. _rfbLearnStartFromPayload(payload);
  761. return;
  762. }
  763. #endif
  764. if (t.equals(MQTT_TOPIC_RFOUT)) {
  765. #if RELAY_SUPPORT
  766. // we *sometimes* want to check the code against available rfbON / rfbOFF
  767. // e.g. in case we want to control some external device and have an external remote.
  768. // - when remote press happens, relays stay in sync when we receive the code via the processing loop
  769. // - when we send the code here, we never register it as *sent*, thus relays need to be made in sync manually
  770. if (!_rfbRelayHandler(payload)) {
  771. #endif
  772. _rfbSendFromPayload(payload);
  773. #if RELAY_SUPPORT
  774. }
  775. #endif
  776. return;
  777. }
  778. #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
  779. if (t.equals(MQTT_TOPIC_RFRAW)) {
  780. // in case this is RAW message, we should not match anything and just send it as-is to the serial
  781. _rfbSendRawFromPayload(payload);
  782. return;
  783. }
  784. #endif
  785. return;
  786. }
  787. }
  788. #endif // MQTT_SUPPORT
  789. #if API_SUPPORT
  790. void _rfbApiSetup() {
  791. apiReserve(3u);
  792. apiRegister({
  793. MQTT_TOPIC_RFOUT, Api::Type::Basic, ApiUnusedArg,
  794. apiOk, // just a stub, nothing to return
  795. [](const Api&, ApiBuffer& buffer) {
  796. _rfbSendFromPayload(buffer.data);
  797. }
  798. });
  799. #if RELAY_SUPPORT
  800. apiRegister({
  801. MQTT_TOPIC_RFLEARN, Api::Type::Basic, ApiUnusedArg,
  802. [](const Api&, ApiBuffer& buffer) {
  803. if (_rfb_learn) {
  804. snprintf_P(buffer.data, buffer.size, PSTR("learning id:%u,status:%c"),
  805. _rfb_learn->id, _rfb_learn->status ? 't' : 'f'
  806. );
  807. } else {
  808. snprintf_P(buffer.data, buffer.size, PSTR("waiting"));
  809. }
  810. },
  811. [](const Api&, ApiBuffer& buffer) {
  812. _rfbLearnStartFromPayload(buffer.data);
  813. }
  814. });
  815. #endif
  816. #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
  817. apiRegister({
  818. MQTT_TOPIC_RFRAW, Api::Type::Basic, ApiUnusedArg,
  819. apiOk, // just a stub, nothing to return
  820. [](const Api&, ApiBuffer& buffer) {
  821. _rfbSendRawFromPayload(buffer.data);
  822. }
  823. });
  824. #endif
  825. }
  826. #endif // API_SUPPORT
  827. #if TERMINAL_SUPPORT
  828. void _rfbInitCommands() {
  829. #if RELAY_SUPPORT
  830. terminalRegisterCommand(F("RFB.LEARN"), [](const terminal::CommandContext& ctx) {
  831. if (ctx.argc != 3) {
  832. terminalError(ctx, F("RFB.LEARN <ID> <STATUS>"));
  833. return;
  834. }
  835. int id = ctx.argv[1].toInt();
  836. if (id >= relayCount()) {
  837. terminalError(ctx, F("Invalid relay ID"));
  838. return;
  839. }
  840. rfbLearn(id, (ctx.argv[2].toInt()) == 1);
  841. terminalOK(ctx);
  842. });
  843. terminalRegisterCommand(F("RFB.FORGET"), [](const terminal::CommandContext& ctx) {
  844. if (ctx.argc < 2) {
  845. terminalError(ctx, F("RFB.FORGET <ID> [<STATUS>]"));
  846. return;
  847. }
  848. int id = ctx.argv[1].toInt();
  849. if (id >= relayCount()) {
  850. terminalError(ctx, F("Invalid relay ID"));
  851. return;
  852. }
  853. if (ctx.argc == 3) {
  854. rfbForget(id, (ctx.argv[2].toInt()) == 1);
  855. } else {
  856. rfbForget(id, true);
  857. rfbForget(id, false);
  858. }
  859. terminalOK(ctx);
  860. });
  861. #endif // if RELAY_SUPPORT
  862. #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
  863. terminalRegisterCommand(F("RFB.WRITE"), [](const terminal::CommandContext& ctx) {
  864. if (ctx.argc != 2) {
  865. terminalError(ctx, F("RFB.WRITE <PAYLOAD>"));
  866. return;
  867. }
  868. _rfbSendRawFromPayload(ctx.argv[1].c_str());
  869. terminalOK(ctx);
  870. });
  871. #endif
  872. }
  873. #endif // TERMINAL_SUPPORT
  874. // -----------------------------------------------------------------------------
  875. // PUBLIC
  876. // -----------------------------------------------------------------------------
  877. void rfbStore(unsigned char id, bool status, const char * code) {
  878. settings_key_t key { status ? F("rfbON") : F("rfbOFF"), id };
  879. setSetting(key, code);
  880. DEBUG_MSG_P(PSTR("[RF] Saved %s => \"%s\"\n"), key.toString().c_str(), code);
  881. }
  882. String rfbRetrieve(unsigned char id, bool status) {
  883. return getSetting({ status ? F("rfbON") : F("rfbOFF"), id });
  884. }
  885. #if RELAY_SUPPORT
  886. void rfbStatus(unsigned char id, bool status) {
  887. // TODO: This is a left-over from the old implementation. Right now we set this lock when relay handler
  888. // is called within the receiver, while this is called from either relayStatus or relay loop calling
  889. // this via provider callback. This prevents us from re-sending the code we just received.
  890. // TODO: Consider having 'origin' of the relay change. Either supply relayStatus with an additional arg,
  891. // or track these statuses directly.
  892. if (!_rfb_relay_status_lock[id]) {
  893. String value = rfbRetrieve(id, status);
  894. if (value.length() && !(value.length() & 1)) {
  895. _rfbSendFromPayload(value.c_str());
  896. }
  897. }
  898. _rfb_relay_status_lock[id] = false;
  899. }
  900. void rfbLearn(unsigned char id, bool status) {
  901. _rfb_learn.reset(new RfbLearn { millis(), id, status });
  902. _rfbLearnImpl();
  903. }
  904. void rfbForget(unsigned char id, bool status) {
  905. delSetting({status ? F("rfbON") : F("rfbOFF"), id});
  906. // Websocket update needs to happen right here, since the only time
  907. // we send these in bulk is at the very start of the connection
  908. #if WEB_SUPPORT
  909. wsPost([id](JsonObject& root) {
  910. _rfbWebSocketSendCodeArray(root, id, 1);
  911. });
  912. #endif
  913. }
  914. #endif // RELAY_SUPPORT
  915. // -----------------------------------------------------------------------------
  916. // SETUP & LOOP
  917. // -----------------------------------------------------------------------------
  918. #if RELAY_SUPPORT && (RFB_PROVIDER == RFB_PROVIDER_RCSWITCH)
  919. // TODO: remove this in 1.16.0
  920. void _rfbSettingsMigrate(int version) {
  921. if (!version || (version > 4)) {
  922. return;
  923. }
  924. auto migrate_code = [](String& out, const String& in) -> bool {
  925. out = "";
  926. if (18 == in.length()) {
  927. uint8_t bits { 0u };
  928. if (!hexDecode(in.c_str() + 8, 2, &bits, 1)) {
  929. return false;
  930. }
  931. auto bytes = _rfb_bytes_for_bits(bits);
  932. out = in.substring(0, 10);
  933. out += (in.c_str() + in.length() - (2 * bytes));
  934. return in != out;
  935. }
  936. return false;
  937. };
  938. String buffer;
  939. for (unsigned char index = 0; index < relayCount(); ++index) {
  940. const settings_key_t on_key {F("rfbON"), index};
  941. if (migrate_code(buffer, getSetting(on_key))) {
  942. setSetting(on_key, buffer);
  943. }
  944. const settings_key_t off_key {F("rfbOFF"), index};
  945. if (migrate_code(buffer, getSetting(off_key))) {
  946. setSetting(off_key, buffer);
  947. }
  948. }
  949. }
  950. #endif
  951. void rfbSetup() {
  952. #if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
  953. _rfb_parser.reserve(RfbParser::MessageSizeBasic);
  954. #elif RFB_PROVIDER == RFB_PROVIDER_RCSWITCH
  955. #if RELAY_SUPPORT
  956. _rfbSettingsMigrate(migrateVersion());
  957. #endif
  958. {
  959. auto rx = getSetting("rfbRX", RFB_RX_PIN);
  960. auto tx = getSetting("rfbTX", RFB_TX_PIN);
  961. // TODO: tag gpioGetLock with a NAME string, skip log here
  962. _rfb_receive = gpioValid(rx);
  963. _rfb_transmit = gpioValid(tx);
  964. if (!_rfb_transmit && !_rfb_receive) {
  965. DEBUG_MSG_P(PSTR("[RF] Neither RX or TX are set\n"));
  966. return;
  967. }
  968. _rfb_modem = new RCSwitch();
  969. if (_rfb_receive) {
  970. _rfb_modem->enableReceive(rx);
  971. DEBUG_MSG_P(PSTR("[RF] RF receiver on GPIO %u\n"), rx);
  972. }
  973. if (_rfb_transmit) {
  974. auto transmit = getSetting("rfbTransmit", RFB_TRANSMIT_REPEATS);
  975. _rfb_modem->enableTransmit(tx);
  976. _rfb_modem->setRepeatTransmit(transmit);
  977. DEBUG_MSG_P(PSTR("[RF] RF transmitter on GPIO %u\n"), tx);
  978. }
  979. }
  980. #endif
  981. #if MQTT_SUPPORT
  982. mqttRegister(_rfbMqttCallback);
  983. #endif
  984. #if API_SUPPORT
  985. _rfbApiSetup();
  986. #endif
  987. #if WEB_SUPPORT
  988. wsRegister()
  989. #if RELAY_SUPPORT
  990. .onData(_rfbWebSocketOnData)
  991. .onAction(_rfbWebSocketOnAction)
  992. #endif
  993. .onConnected(_rfbWebSocketOnConnected)
  994. .onVisible(_rfbWebSocketOnVisible)
  995. .onKeyCheck(_rfbWebSocketOnKeyCheck);
  996. #endif
  997. #if TERMINAL_SUPPORT
  998. _rfbInitCommands();
  999. #endif
  1000. _rfb_repeats = getSetting("rfbRepeat", RFB_SEND_REPEATS);
  1001. // Note: as rfbridge protocol is simplistic enough, we rely on Serial queue to deliver timely updates
  1002. // learn / command acks / etc. are not queued, only RF messages are
  1003. espurnaRegisterLoop([]() {
  1004. _rfbReceiveImpl();
  1005. _rfbSendQueued();
  1006. });
  1007. }
  1008. #endif // RFB_SUPPORT