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.

267 lines
6.0 KiB

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
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. API MODULE
  3. Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
  4. */
  5. #include "api.h"
  6. // -----------------------------------------------------------------------------
  7. #if API_SUPPORT
  8. #include <vector>
  9. #include "system.h"
  10. #include "web.h"
  11. #include "rpc.h"
  12. #include <ESPAsyncTCP.h>
  13. #include <ArduinoJson.h>
  14. constexpr size_t ApiPathSizeMax { 64ul };
  15. std::vector<Api> _apis;
  16. // -----------------------------------------------------------------------------
  17. // API
  18. // -----------------------------------------------------------------------------
  19. bool _asJson(AsyncWebServerRequest *request) {
  20. bool asJson = false;
  21. if (request->hasHeader("Accept")) {
  22. AsyncWebHeader* h = request->getHeader("Accept");
  23. asJson = h->value().equals("application/json");
  24. }
  25. return asJson;
  26. }
  27. void _onAPIsText(AsyncWebServerRequest *request) {
  28. AsyncResponseStream *response = request->beginResponseStream("text/plain");
  29. char buffer[ApiPathSizeMax] = {0};
  30. for (auto& api : _apis) {
  31. sprintf_P(buffer, PSTR("/api/%s\n"), api.path.c_str());
  32. response->write(buffer);
  33. }
  34. request->send(response);
  35. }
  36. constexpr size_t ApiJsonBufferSize = 1024;
  37. void _onAPIsJson(AsyncWebServerRequest *request) {
  38. DynamicJsonBuffer jsonBuffer(ApiJsonBufferSize);
  39. JsonArray& root = jsonBuffer.createArray();
  40. char buffer[ApiPathSizeMax] = {0};
  41. for (auto& api : _apis) {
  42. sprintf(buffer, "/api/%s", api.path.c_str());
  43. root.add(buffer);
  44. }
  45. AsyncResponseStream *response = request->beginResponseStream("application/json");
  46. root.printTo(*response);
  47. request->send(response);
  48. }
  49. void _onAPIs(AsyncWebServerRequest *request) {
  50. webLog(request);
  51. if (!apiAuthenticate(request)) return;
  52. bool asJson = _asJson(request);
  53. String output;
  54. if (asJson) {
  55. _onAPIsJson(request);
  56. } else {
  57. _onAPIsText(request);
  58. }
  59. }
  60. void _onRPC(AsyncWebServerRequest *request) {
  61. webLog(request);
  62. if (!apiAuthenticate(request)) return;
  63. //bool asJson = _asJson(request);
  64. int response = 404;
  65. if (request->hasParam("action")) {
  66. AsyncWebParameter* p = request->getParam("action");
  67. const auto action = p->value();
  68. DEBUG_MSG_P(PSTR("[RPC] Action: %s\n"), action.c_str());
  69. if (rpcHandleAction(action)) {
  70. response = 204;
  71. }
  72. }
  73. request->send(response);
  74. }
  75. struct ApiMatch {
  76. Api* api { nullptr };
  77. Api::Type type { Api::Type::Basic };
  78. };
  79. ApiMatch _apiMatch(const String& url, AsyncWebServerRequest* request) {
  80. ApiMatch result;
  81. char buffer[ApiPathSizeMax] = {0};
  82. for (auto& api : _apis) {
  83. sprintf_P(buffer, PSTR("/api/%s"), api.path.c_str());
  84. if (url != buffer) {
  85. continue;
  86. }
  87. auto type = _asJson(request)
  88. ? Api::Type::Json
  89. : Api::Type::Basic;
  90. result.api = &api;
  91. result.type = type;
  92. break;
  93. }
  94. return result;
  95. }
  96. bool _apiDispatchRequest(const String& url, AsyncWebServerRequest* request) {
  97. auto match = _apiMatch(url, request);
  98. if (!match.api) {
  99. return false;
  100. }
  101. if (match.type != match.api->type) {
  102. DEBUG_MSG_P(PSTR("[API] Cannot handle the request type\n"));
  103. request->send(404);
  104. return true;
  105. }
  106. const bool is_put = (
  107. (!apiRestFul() || (request->method() == HTTP_PUT))
  108. && request->hasParam("value", request->method() == HTTP_PUT)
  109. );
  110. ApiBuffer buffer;
  111. switch (match.api->type) {
  112. case Api::Type::Basic: {
  113. if (!match.api->get.basic) {
  114. break;
  115. }
  116. if (is_put) {
  117. if (!match.api->put.basic) {
  118. break;
  119. }
  120. auto value = request->getParam("value", request->method() == HTTP_PUT)->value();
  121. if (buffer.size < (value.length() + 1ul)) {
  122. break;
  123. }
  124. std::copy(value.c_str(), value.c_str() + value.length() + 1, buffer.data);
  125. match.api->put.basic(*match.api, buffer);
  126. buffer.erase();
  127. }
  128. match.api->get.basic(*match.api, buffer);
  129. request->send(200, "text/plain", buffer.data);
  130. return true;
  131. }
  132. // TODO: pass the body instead of `value` param
  133. // TODO: handle HTTP_PUT
  134. case Api::Type::Json: {
  135. if (!match.api->get.json || is_put) {
  136. break;
  137. }
  138. DynamicJsonBuffer jsonBuffer(API_BUFFER_SIZE);
  139. JsonObject& root = jsonBuffer.createObject();
  140. match.api->get.json(*match.api, root);
  141. AsyncResponseStream *response = request->beginResponseStream("application/json", root.measureLength() + 1);
  142. root.printTo(*response);
  143. request->send(response);
  144. return true;
  145. }
  146. }
  147. DEBUG_MSG_P(PSTR("[API] Method not supported\n"));
  148. request->send(405);
  149. return true;
  150. }
  151. bool _apiRequestCallback(AsyncWebServerRequest* request) {
  152. String url = request->url();
  153. if (url.equals("/rpc")) {
  154. _onRPC(request);
  155. return true;
  156. }
  157. if (url.equals("/api") || url.equals("/apis")) {
  158. _onAPIs(request);
  159. return true;
  160. }
  161. if (!url.startsWith("/api/")) return false;
  162. if (!apiAuthenticate(request)) return false;
  163. return _apiDispatchRequest(url, request);
  164. }
  165. // -----------------------------------------------------------------------------
  166. void apiReserve(size_t size) {
  167. _apis.reserve(_apis.size() + size);
  168. }
  169. void apiRegister(const Api& api) {
  170. if (api.path.length() >= (ApiPathSizeMax - strlen("/api/") - 1ul)) {
  171. return;
  172. }
  173. _apis.push_back(api);
  174. }
  175. void apiSetup() {
  176. webRequestRegister(_apiRequestCallback);
  177. }
  178. void apiOk(const Api&, ApiBuffer& buffer) {
  179. buffer.data[0] = 'O';
  180. buffer.data[1] = 'K';
  181. buffer.data[2] = '\0';
  182. }
  183. void apiError(const Api&, ApiBuffer& buffer) {
  184. buffer.data[0] = '-';
  185. buffer.data[1] = 'E';
  186. buffer.data[2] = 'R';
  187. buffer.data[3] = 'R';
  188. buffer.data[4] = 'O';
  189. buffer.data[5] = 'R';
  190. buffer.data[6] = '\0';
  191. }
  192. #endif // API_SUPPORT