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.

1172 lines
32 KiB

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