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.

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