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.

1304 lines
36 KiB

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