From f3c185cc7367ed6cf3cef0887a7d5dc01acea197 Mon Sep 17 00:00:00 2001 From: Maxim Prokhorov Date: Thu, 13 Aug 2020 03:24:46 +0300 Subject: [PATCH] api: rework storage & json calls, drop std::function - reduce overall size of the structure, store a single required entity inside of Api object itself. - tweak internal functions to expect a certain path size - (kind of a hack) add manual calls to vector::reserve() as we go over the current capacity and vector increases it's size times 2. e.g. for 9 relays, we would allocate space for 32 Api objects when vector size goes from 16 to 32, after we add 18 Api objects with relay + pulse - (breaking) json calls on a separate path, don't waste time encoding a single entity as json object when we can encode more things --- code/espurna/api.cpp | 212 +++++++++++++++++++++++--------------- code/espurna/api.h | 65 +++++++++++- code/espurna/light.cpp | 119 +++++++++++---------- code/espurna/relay.cpp | 96 +++++++++-------- code/espurna/rfbridge.cpp | 77 ++++++++------ code/espurna/sensor.cpp | 53 +++++++--- 6 files changed, 396 insertions(+), 226 deletions(-) diff --git a/code/espurna/api.cpp b/code/espurna/api.cpp index 7bf76a9f..08f60d7e 100644 --- a/code/espurna/api.cpp +++ b/code/espurna/api.cpp @@ -21,19 +21,8 @@ Copyright (C) 2016-2019 by Xose PĂ©rez #include #include -struct web_api_t { - explicit web_api_t(const String& key, api_get_callback_f getFn, api_put_callback_f putFn) : - key(key), - getFn(getFn), - putFn(putFn) - {} - web_api_t() = delete; - - const String key; - api_get_callback_f getFn; - api_put_callback_f putFn; -}; -std::vector _apis; +constexpr size_t ApiPathSizeMax { 64ul }; +std::vector _apis; // ----------------------------------------------------------------------------- // API @@ -50,16 +39,10 @@ bool _asJson(AsyncWebServerRequest *request) { void _onAPIsText(AsyncWebServerRequest *request) { AsyncResponseStream *response = request->beginResponseStream("text/plain"); - String output; - output.reserve(48); + char buffer[ApiPathSizeMax] = {0}; for (auto& api : _apis) { - output = ""; - output += api.key; - output += " -> "; - output += "/api/"; - output += api.key; - output += '\n'; - response->write(output.c_str()); + sprintf_P(buffer, PSTR("/api/%s\n"), api.path.c_str()); + response->write(buffer); } request->send(response); } @@ -69,19 +52,14 @@ constexpr size_t ApiJsonBufferSize = 1024; void _onAPIsJson(AsyncWebServerRequest *request) { DynamicJsonBuffer jsonBuffer(ApiJsonBufferSize); - JsonObject& root = jsonBuffer.createObject(); + JsonArray& root = jsonBuffer.createArray(); - constexpr const int BUFFER_SIZE = 48; - - for (unsigned int i=0; i < _apis.size(); i++) { - char buffer[BUFFER_SIZE] = {0}; - int res = snprintf(buffer, sizeof(buffer), "/api/%s", _apis[i].key.c_str()); - if ((res < 0) || (res > (BUFFER_SIZE - 1))) { - request->send(500); - return; - } - root[_apis[i].key] = buffer; + char buffer[ApiPathSizeMax] = {0}; + for (auto& api : _apis) { + sprintf(buffer, "/api/%s", api.path.c_str()); + root.add(buffer); } + AsyncResponseStream *response = request->beginResponseStream("application/json"); root.printTo(*response); request->send(response); @@ -129,87 +107,159 @@ void _onRPC(AsyncWebServerRequest *request) { } -bool _apiRequestCallback(AsyncWebServerRequest *request) { +struct ApiMatch { + Api* api { nullptr }; + Api::Type type { Api::Type::Basic }; +}; - String url = request->url(); +ApiMatch _apiMatch(const String& url, AsyncWebServerRequest* request) { - // Main API entry point - if (url.equals("/api") || url.equals("/apis")) { - _onAPIs(request); - return true; + ApiMatch result; + char buffer[ApiPathSizeMax] = {0}; + + for (auto& api : _apis) { + sprintf_P(buffer, PSTR("/api/%s"), api.path.c_str()); + if (url != buffer) { + continue; + } + + auto type = _asJson(request) + ? Api::Type::Json + : Api::Type::Basic; + + result.api = &api; + result.type = type; + break; } - // Main RPC entry point - if (url.equals("/rpc")) { - _onRPC(request); + return result; +} + +bool _apiDispatchRequest(const String& url, AsyncWebServerRequest* request) { + + auto match = _apiMatch(url, request); + if (!match.api) { + return false; + } + + if (match.type != match.api->type) { + DEBUG_MSG_P(PSTR("[API] Cannot handle the request type\n")); + request->send(404); return true; } - // Not API request - if (!url.startsWith("/api/")) return false; + const bool is_put = ( + (!apiRestFul() || (request->method() == HTTP_PUT)) + && request->hasParam("value", request->method() == HTTP_PUT) + ); - for (auto& api : _apis) { + ApiBuffer buffer; - // Search API url for the exact match - if (!url.endsWith(api.key)) continue; + switch (match.api->type) { - // Log and check credentials - webLog(request); - if (!apiAuthenticate(request)) return false; + case Api::Type::Basic: { + if (!match.api->get.basic) { + break; + } - // Check if its a PUT - if (api.putFn != NULL) { - if (!apiRestFul() || (request->method() == HTTP_PUT)) { - if (request->hasParam("value", request->method() == HTTP_PUT)) { - AsyncWebParameter* p = request->getParam("value", request->method() == HTTP_PUT); - (api.putFn)((p->value()).c_str()); - } + if (is_put) { + if (!match.api->put.basic) { + break; } + auto value = request->getParam("value", request->method() == HTTP_PUT)->value(); + //memcpy(buffer.data, value.c_str(), value.length()); + std::copy(value.c_str(), value.c_str() + value.length(), buffer.data); + match.api->get.basic(*match.api, buffer); + buffer.erase(); } - // Get response from callback - char value[API_BUFFER_SIZE] = {0}; - (api.getFn)(value, API_BUFFER_SIZE); + match.api->get.basic(*match.api, buffer); + request->send(200, "text/plain", buffer.data); - // The response will be a 404 NOT FOUND if the resource is not available - if (0 == value[0]) { - DEBUG_MSG_P(PSTR("[API] Sending 404 response\n")); - request->send(404); - return false; + return true; + } + + // TODO: pass the body instead of `value` param + // TODO: handle HTTP_PUT + case Api::Type::Json: { + if (!match.api->get.json || is_put) { + break; } - DEBUG_MSG_P(PSTR("[API] Sending response '%s'\n"), value); + DynamicJsonBuffer jsonBuffer(API_BUFFER_SIZE); + JsonObject& root = jsonBuffer.createObject(); - // Format response according to the Accept header - if (_asJson(request)) { - char buffer[64]; - if (isNumber(value)) { - snprintf_P(buffer, sizeof(buffer), PSTR("{ \"%s\": %s }"), api.key.c_str(), value); - } else { - snprintf_P(buffer, sizeof(buffer), PSTR("{ \"%s\": \"%s\" }"), api.key.c_str(), value); - } - request->send(200, "application/json", buffer); - } else { - request->send(200, "text/plain", value); - } + match.api->get.json(*match.api, root); + + AsyncResponseStream *response = request->beginResponseStream("application/json", root.measureLength() + 1); + root.printTo(*response); + request->send(response); return true; + } } - return false; + DEBUG_MSG_P(PSTR("[API] Method not supported\n")); + request->send(405); + + return true; + +} + +bool _apiRequestCallback(AsyncWebServerRequest* request) { + + String url = request->url(); + + if (url.equals("/rpc")) { + _onRPC(request); + return true; + } + + if (url.equals("/api") || url.equals("/apis")) { + _onAPIs(request); + return true; + } + + if (!url.startsWith("/api/")) return false; + if (!apiAuthenticate(request)) return false; + + return _apiDispatchRequest(url, request); } // ----------------------------------------------------------------------------- -void apiRegister(const String& key, api_get_callback_f getFn, api_put_callback_f putFn) { - _apis.emplace_back(key, std::move(getFn), std::move(putFn)); +void apiReserve(size_t size) { + _apis.reserve(_apis.size() + size); +} + +void apiRegister(const Api& api) { + if (api.path.length() >= (ApiPathSizeMax - strlen("/api/") - 1ul)) { + return; + } + _apis.push_back(api); } void apiSetup() { webRequestRegister(_apiRequestCallback); } +void apiOk(const Api&, ApiBuffer& buffer) { + buffer.data[0] = 'O'; + buffer.data[1] = 'K'; + buffer.data[2] = '\0'; +} + +void apiError(const Api&, ApiBuffer& buffer) { + buffer.data[0] = '-'; + buffer.data[1] = 'E'; + buffer.data[2] = 'R'; + buffer.data[3] = 'R'; + buffer.data[4] = 'O'; + buffer.data[5] = 'R'; + buffer.data[6] = '\0'; +} + #endif // API_SUPPORT diff --git a/code/espurna/api.h b/code/espurna/api.h index 7474b55b..4d9651ee 100644 --- a/code/espurna/api.h +++ b/code/espurna/api.h @@ -22,14 +22,71 @@ String apiKey(); #if WEB_SUPPORT && API_SUPPORT -#include +#include -using api_get_callback_f = std::function; -using api_put_callback_f = std::function ; +constexpr unsigned char ApiUnusedArg = 0u; -void apiRegister(const String& key, api_get_callback_f getFn, api_put_callback_f putFn = nullptr); +struct ApiBuffer { + constexpr static size_t size = API_BUFFER_SIZE; + char data[size]; + + void erase() { + std::fill(data, data + size, '\0'); + } +}; + +struct Api { + using BasicHandler = void(*)(const Api& api, ApiBuffer& buffer); + using JsonHandler = void(*)(const Api& api, JsonObject& root); + + enum class Type { + Basic, + Json + }; + + Api() = delete; + + Api(const String& path_, Type type_, unsigned char arg_, BasicHandler get_, BasicHandler put_ = nullptr) : + path(path_), + type(type_), + arg(arg_) + { + get.basic = get_; + put.basic = put_; + } + + Api(const String& path_, Type type_, unsigned char arg_, JsonHandler get_, JsonHandler put_ = nullptr) : + path(path_), + type(type_), + arg(arg_) + { + get.json = get_; + put.json = put_; + } + + String path; + Type type; + unsigned char arg; + + union { + BasicHandler basic; + JsonHandler json; + } get; + + union { + BasicHandler basic; + JsonHandler json; + } put; +}; + +void apiRegister(const Api& api); void apiCommonSetup(); void apiSetup(); +void apiReserve(size_t); + +void apiError(const Api&, ApiBuffer& buffer); +void apiOk(const Api&, ApiBuffer& buffer); + #endif // API_SUPPORT == 1 diff --git a/code/espurna/light.cpp b/code/espurna/light.cpp index 276ca11d..e7acddb9 100644 --- a/code/espurna/light.cpp +++ b/code/espurna/light.cpp @@ -853,86 +853,101 @@ void lightBroker() { #if API_SUPPORT -void _lightAPISetup() { +void _lightApiSetup() { + + // Note that we expect a fixed number of entries. + // Otherwise, underlying vector will reserve more than we need (likely, *2 of the current size) + apiReserve( + (_light_has_color ? 4u : 0u) + 2u + _light_channels.size() + ); if (_light_has_color) { - apiRegister(MQTT_TOPIC_COLOR_RGB, - [](char * buffer, size_t len) { + apiRegister({ + MQTT_TOPIC_COLOR_RGB, Api::Type::Basic, ApiUnusedArg, + [](const Api&, ApiBuffer& buffer) { if (getSetting("useCSS", 1 == LIGHT_USE_CSS)) { - _toRGB(buffer, len, true); + _toRGB(buffer.data, buffer.size, true); } else { - _toLong(buffer, len, true); + _toLong(buffer.data, buffer.size, true); } }, - [](const char * payload) { - lightColor(payload, true); + [](const Api&, ApiBuffer& buffer) { + lightColor(buffer.data, true); lightUpdate(true, true); } - ); + }); - apiRegister(MQTT_TOPIC_COLOR_HSV, - [](char * buffer, size_t len) { - _toHSV(buffer, len); + apiRegister({ + MQTT_TOPIC_COLOR_HSV, Api::Type::Basic, ApiUnusedArg, + [](const Api&, ApiBuffer& buffer) { + _toHSV(buffer.data, buffer.size); }, - [](const char * payload) { - lightColor(payload, false); - lightUpdate(true, true); - } - ); - - apiRegister(MQTT_TOPIC_KELVIN, - [](char * buffer, size_t len) {}, - [](const char * payload) { - _lightAdjustKelvin(payload); + [](const Api&, ApiBuffer& buffer) { + lightColor(buffer.data, false); lightUpdate(true, true); } - ); + }); - apiRegister(MQTT_TOPIC_MIRED, - [](char * buffer, size_t len) {}, - [](const char * payload) { - _lightAdjustMireds(payload); + apiRegister({ + MQTT_TOPIC_MIRED, Api::Type::Basic, ApiUnusedArg, + [](const Api&, ApiBuffer& buffer) { + sprintf(buffer.data, PSTR("%d"), _light_mireds); + }, + [](const Api&, ApiBuffer& buffer) { + _lightAdjustMireds(buffer.data); lightUpdate(true, true); } - ); - - } + }); - for (unsigned int id=0; id<_light_channels.size(); id++) { - - char key[15]; - snprintf_P(key, sizeof(key), PSTR("%s/%d"), MQTT_TOPIC_CHANNEL, id); - apiRegister(key, - [id](char * buffer, size_t len) { - snprintf_P(buffer, len, PSTR("%d"), _light_channels[id].target); + apiRegister({ + MQTT_TOPIC_KELVIN, Api::Type::Basic, ApiUnusedArg, + [](const Api&, ApiBuffer& buffer) { + sprintf(buffer.data, PSTR("%d"), _toKelvin(_light_mireds)); }, - [id](const char * payload) { - _lightAdjustChannel(id, payload); + [](const Api&, ApiBuffer& buffer) { + _lightAdjustKelvin(buffer.data); lightUpdate(true, true); } - ); + }); } - apiRegister(MQTT_TOPIC_TRANSITION, - [](char * buffer, size_t len) { - snprintf_P(buffer, len, PSTR("%d"), lightTransitionTime()); + apiRegister({ + MQTT_TOPIC_TRANSITION, Api::Type::Basic, ApiUnusedArg, + [](const Api&, ApiBuffer& buffer) { + snprintf_P(buffer.data, buffer.size, PSTR("%u"), lightTransitionTime()); }, - [](const char * payload) { - lightTransitionTime(atol(payload)); + [](const Api&, ApiBuffer& buffer) { + lightTransitionTime(atol(buffer.data)); } - ); + }); - apiRegister(MQTT_TOPIC_BRIGHTNESS, - [](char * buffer, size_t len) { - snprintf_P(buffer, len, PSTR("%d"), _light_brightness); + apiRegister({ + MQTT_TOPIC_BRIGHTNESS, Api::Type::Basic, ApiUnusedArg, + [](const Api&, ApiBuffer& buffer) { + snprintf_P(buffer.data, buffer.size, PSTR("%u"), _light_brightness); }, - [](const char * payload) { - _lightAdjustBrightness(payload); + [](const Api&, ApiBuffer& buffer) { + _lightAdjustBrightness(buffer.data); lightUpdate(true, true); } - ); + }); + + char path[32] = {0}; + for (unsigned char id = 0; id < _light_channels.size(); ++id) { + snprintf_P(path, sizeof(path), PSTR(MQTT_TOPIC_CHANNEL "/%u"), id); + apiRegister({ + path, Api::Type::Basic, id, + [](const Api& api, ApiBuffer& buffer) { + snprintf_P(buffer.data, buffer.size, PSTR("%u"), _light_channels[api.arg].target); + }, + [](const Api& api, ApiBuffer& buffer) { + _lightAdjustChannel(api.arg, buffer.data); + lightUpdate(true, true); + } + }); + } } @@ -1421,7 +1436,7 @@ void lightSetup() { #endif #if API_SUPPORT - _lightAPISetup(); + _lightApiSetup(); #endif #if MQTT_SUPPORT diff --git a/code/espurna/relay.cpp b/code/espurna/relay.cpp index 00a4872f..7dfadf16 100644 --- a/code/espurna/relay.cpp +++ b/code/espurna/relay.cpp @@ -1022,60 +1022,72 @@ void relaySetupWS() { void relaySetupAPI() { - char key[20]; - - // API entry points (protected with apikey) - for (unsigned int relayID=0; relayID 2) { + return; + } + + std::copy(buffer.data, sep, relay); + if (!isNumber(relay)) { + return; + } + + _learnId = atoi(relay); if (_learnId >= relayCount()) { DEBUG_MSG_P(PSTR("[RF] Wrong learnID (%d)\n"), _learnId); return; } - tok = strtok(NULL, ","); - if (NULL == tok) return; - _learnStatus = (char) tok[0] != '0'; - _rfbLearnImpl(); + + ++sep; + if ((*sep == '0') || (*sep == '1')) { + _learnStatus = (*sep != '0'); + _rfbLearnImpl(); + } } - ); + }); - #if !RFB_DIRECT - apiRegister(MQTT_TOPIC_RFRAW, - [](char * buffer, size_t len) { - snprintf_P(buffer, len, PSTR("OK")); - }, - [](const char * payload) { - _rfbParseRaw((char *)payload); + #if not RFB_DIRECT + apiRegister({ + MQTT_TOPIC_RFRAW, Api::Type::Basic, ApiUnusedArg, + apiOk, // just a stub, nothing to return + [](const Api&, ApiBuffer& buffer) { + _rfbParseRaw(buffer.data); } - ); + }); #endif } @@ -723,7 +736,7 @@ void rfbSetup() { #endif #if API_SUPPORT - _rfbAPISetup(); + _rfbApiSetup(); #endif #if WEB_SUPPORT diff --git a/code/espurna/sensor.cpp b/code/espurna/sensor.cpp index 7b044769..7da76c18 100644 --- a/code/espurna/sensor.cpp +++ b/code/espurna/sensor.cpp @@ -1403,27 +1403,50 @@ void _sensorWebSocketOnConnected(JsonObject& root) { #if API_SUPPORT -void _sensorAPISetup() { +String _sensorApiMagnitudeName(sensor_magnitude_t& magnitude) { + String name = magnitudeTopic(magnitude.type); + if (SENSOR_USE_INDEX || (sensor_magnitude_t::counts(magnitude.type) > 1)) name = name + "/" + String(magnitude.index_global); + return name; +} + +void _sensorApiJsonCallback(const Api&, JsonObject& root) { + JsonArray& magnitudes = root.createNestedArray("magnitudes"); for (auto& magnitude : _magnitudes) { + JsonArray& data = magnitudes.createNestedArray(); + data.add(_sensorApiMagnitudeName(magnitude)); + data.add(magnitude.last); + data.add(magnitude.reported); + } +} - String topic = magnitudeTopic(magnitude.type); - if (SENSOR_USE_INDEX || (sensor_magnitude_t::counts(magnitude.type) > 1)) topic = topic + "/" + String(magnitude.index_global); +void _sensorApiGetValue(const Api& api, ApiBuffer& buffer) { + auto& magnitude = _magnitudes[api.arg]; + double value = _sensor_realtime ? magnitude.last : magnitude.reported; + dtostrf(value, 1, magnitude.decimals, buffer.data); +} - api_get_callback_f get_cb = [&magnitude](char * buffer, size_t len) { - double value = _sensor_realtime ? magnitude.last : magnitude.reported; - dtostrf(value, 1, magnitude.decimals, buffer); - }; - api_put_callback_f put_cb = nullptr; +void _sensorApiResetEnergyPutCallback(const Api& api, ApiBuffer& buffer) { + _sensorApiResetEnergy(_magnitudes[api.arg], buffer.data); +} - if (magnitude.type == MAGNITUDE_ENERGY) { - put_cb = [&magnitude](const char* payload) { - _sensorApiResetEnergy(magnitude, payload); - }; - } +void _sensorApiSetup() { - apiRegister(topic.c_str(), get_cb, put_cb); + apiReserve( + _magnitudes.size() + sensor_magnitude_t::counts(MAGNITUDE_ENERGY) + 1u + ); + + apiRegister({"magnitudes", Api::Type::Json, ApiUnusedArg, _sensorApiJsonCallback}); + for (unsigned char id = 0; id < _magnitudes.size(); ++id) { + apiRegister({ + _sensorApiMagnitudeName(_magnitudes[id]).c_str(), + Api::Type::Basic, id, + _sensorApiGetValue, + (_magnitudes[id].type == MAGNITUDE_ENERGY) + ? _sensorApiResetEnergyPutCallback + : nullptr + }); } } @@ -2642,7 +2665,7 @@ void sensorSetup() { // API #if API_SUPPORT - _sensorAPISetup(); + _sensorApiSetup(); #endif // Terminal