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.

1182 lines
32 KiB

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