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.

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