diff --git a/code/espurna/api.cpp b/code/espurna/api.cpp index 244ebe00..5dd09b47 100644 --- a/code/espurna/api.cpp +++ b/code/espurna/api.cpp @@ -28,7 +28,7 @@ Copyright (C) 2020-2021 by Maxim Prokhorov = _pattern.parts().size()) { - return _empty_string(); - } + espurna::StringView out; + + if (std::abs(index) < pattern.parts().size()) { + const auto& pattern_parts = pattern.parts(); + int counter { 0 }; + + for (size_t part = 0; part < pattern.size(); ++part) { + const auto& lhs = pattern_parts[part]; + const auto& rhs = value.parts()[part]; - int counter { 0 }; - auto& pattern = _pattern.parts(); + const auto path = value.path(); - for (unsigned int part = 0; part < pattern.size(); ++part) { - auto& lhs = pattern[part]; - if (PathPart::Type::SingleWildcard == lhs.type) { - if (counter == index) { - auto& rhs = _parts.parts()[part]; - return _parts.path().substring(rhs.offset, rhs.offset + rhs.length); + switch (lhs.type) { + case PathPart::Type::Value: + case PathPart::Type::Unknown: + break; + + case PathPart::Type::SingleWildcard: + if (counter == index) { + out = espurna::StringView( + path.begin() + rhs.offset, path.begin() + rhs.offset + rhs.length); + return out; + } + ++counter; + break; + + case PathPart::Type::MultiWildcard: + if (counter == index) { + out = espurna::StringView( + path.begin() + rhs.offset, path.end()); + } + return out; } - ++counter; } } - return _empty_string(); + return out; } -size_t ApiRequest::wildcards() const { - size_t result { 0ul }; - for (auto& part : _pattern) { - if (PathPart::Type::SingleWildcard == part.type) { - ++result; +size_t PathParts::wildcards(const PathParts& pattern) { + size_t out { 0 }; + + for (const auto& part : pattern) { + switch (part.type) { + case PathPart::Type::Unknown: + case PathPart::Type::Value: + case PathPart::Type::MultiWildcard: + break; + case PathPart::Type::SingleWildcard: + ++out; + break; } } - return result; + return out; +} + +#if WEB_SUPPORT + +String ApiRequest::wildcard(int index) const { + return PathParts::wildcard(_pattern, _parts, index).toString(); +} + +size_t ApiRequest::wildcards() const { + return PathParts::wildcards(_pattern); } #endif @@ -447,7 +480,7 @@ public: } void _handleGet(AsyncWebServerRequest* request, ApiRequest& apireq) { - DynamicJsonBuffer jsonBuffer(API_JSON_BUFFER_SIZE); + DynamicJsonBuffer jsonBuffer(BufferSize); JsonObject& root = jsonBuffer.createObject(); if (!_get(apireq, root)) { request->send(500); @@ -467,7 +500,7 @@ public: void _handlePut(AsyncWebServerRequest* request, uint8_t* data, size_t size) { // XXX: arduinojson v5 de-serializer will happily read garbage from raw ptr, since there's no length limit // this is fixed in v6 though. for now, use a wrapper, but be aware that this actually uses more mem for the jsonbuffer - DynamicJsonBuffer jsonBuffer(API_JSON_BUFFER_SIZE); + DynamicJsonBuffer jsonBuffer(BufferSize); ReadOnlyStream stream(data, size); JsonObject& root = jsonBuffer.parseObject(stream); diff --git a/code/espurna/api_impl.h b/code/espurna/api_impl.h index 31618017..156d2aae 100644 --- a/code/espurna/api_impl.h +++ b/code/espurna/api_impl.h @@ -57,13 +57,15 @@ struct ApiRequest { }); } - const String& param(const String& name) { - auto* result = _request.getParam(name, HTTP_PUT == _request.method()); + espurna::StringView param(const String& name) { + const auto* result = _request.getParam(name, HTTP_PUT == _request.method()); + + espurna::StringView out; if (result) { - return result->value(); + out = result->value(); } - return _empty_string(); + return out; } void send(const String& payload) { @@ -86,7 +88,7 @@ struct ApiRequest { } String part(size_t index) const { - return _parts[index]; + return _parts[index].toString(); } // Only works when pattern cointains '+', retrieving the part at the same index from the real path @@ -95,11 +97,6 @@ struct ApiRequest { size_t wildcards() const; private: - const String& _empty_string() const { - static const String string; - return string; - } - bool _done { false }; AsyncWebServerRequest& _request; diff --git a/code/espurna/api_path.h b/code/espurna/api_path.h index 1f3fc811..88936833 100644 --- a/code/espurna/api_path.h +++ b/code/espurna/api_path.h @@ -9,9 +9,10 @@ Copyright (C) 2021 by Maxim Prokhorov #pragma once #include - #include +#include "types.h" + // ----------------------------------------------------------------------------- struct PathPart { @@ -33,8 +34,8 @@ struct PathParts { PathParts() = delete; PathParts(const PathParts&) = delete; - explicit PathParts(const String& path); - PathParts(const String& path, Parts&& parts) : + explicit PathParts(espurna::StringView path); + PathParts(espurna::StringView path, Parts&& parts) : _path(path), _parts(std::move(parts)), _ok(_parts.size()) @@ -44,7 +45,7 @@ struct PathParts { PathParts(other._path, std::move(other._parts)) {} - PathParts(const String& path, PathParts&& other) noexcept : + PathParts(espurna::StringView path, PathParts&& other) noexcept : _path(path), _parts(std::move(other._parts)), _ok(other._ok) @@ -62,12 +63,19 @@ struct PathParts { _parts.reserve(size); } - String operator[](size_t index) const { - auto& part = _parts[index]; - return _path.substring(part.offset, part.offset + part.length); + espurna::StringView operator[](size_t index) const { + return get(_parts[index]); } - const String& path() const { + espurna::StringView back() const { + return get(_parts.back()); + } + + espurna::StringView front() const { + return get(_parts.front()); + } + + espurna::StringView path() const { return _path; } @@ -88,11 +96,20 @@ struct PathParts { } bool match(const PathParts& path) const; - bool match(const String& path) const { + bool match(espurna::StringView path) const { return match(PathParts(path)); } + static espurna::StringView wildcard(const PathParts& pattern, const PathParts& value, int index); + static size_t wildcards(const PathParts& pattern); + private: + espurna::StringView get(const PathPart& part) const { + return espurna::StringView( + _path.begin() + part.offset, + _path.begin() + part.offset + part.length); + } + PathPart& emplace_back(PathPart part) { _parts.push_back(part); return _parts.back(); @@ -106,7 +123,7 @@ private: }); } - const String& _path; + espurna::StringView _path; Parts _parts; bool _ok { false }; }; diff --git a/code/espurna/button.cpp b/code/espurna/button.cpp index bc716d65..80b43240 100644 --- a/code/espurna/button.cpp +++ b/code/espurna/button.cpp @@ -773,7 +773,7 @@ namespace terminal { void button(::terminal::CommandContext&& ctx) { if (ctx.argv.size() == 2) { size_t id; - if (!tryParseId(ctx.argv[1], buttonCount, id)) { + if (!tryParseId(ctx.argv[1], buttonCount(), id)) { terminalError(ctx, F("Invalid button ID")); return; } diff --git a/code/espurna/compat.h b/code/espurna/compat.h index 6a7ad58c..01e1fd4c 100644 --- a/code/espurna/compat.h +++ b/code/espurna/compat.h @@ -6,17 +6,7 @@ COMPATIBILITY BETWEEN 2.3.0 and latest versions #pragma once -#include "espurna.h" - -// ----------------------------------------------------------------------------- - -inline constexpr bool isEspurnaMinimal() { -#if defined(ESPURNA_MINIMAL_ARDUINO_OTA) || defined(ESPURNA_MINIMAL_WEBUI) - return true; -#else - return false; -#endif -} +#include // ----------------------------------------------------------------------------- // Core version 2.4.2 and higher changed the cont_t structure to a pointer: @@ -106,21 +96,32 @@ using std::isnan; // ----------------------------------------------------------------------------- // various backports for C++11, since we still use it with gcc v4.8 // ----------------------------------------------------------------------------- -#if __cplusplus <= 201103L #include +#include + namespace std { +#if __cplusplus < 202002L +template +using remove_cvref = typename std::remove_cv>::type; +#endif + +#if __cplusplus < 201304L template std::unique_ptr make_unique(Args&&... args) { return std::unique_ptr(new T(std::forward(args)...)); } +#endif +#if __cplusplus < 201603L template constexpr const T& clamp(const T& value, const T& low, const T& high) { return (value < low) ? low : (high < value) ? high : value; } +#endif +#if __cplusplus < 201411L template constexpr size_t size(const T (&)[Size]) { return Size; @@ -140,16 +141,10 @@ template constexpr auto cend(const T& value) -> decltype(std::end(value)) { return std::end(value); } - -template -constexpr std::reverse_iterator make_reverse_iterator(T iterator) { - return std::reverse_iterator(iterator); -} +#endif } // namespace std -#endif - // Same as min and max, force same type arguments #undef constrain diff --git a/code/espurna/curtain_kingart.cpp b/code/espurna/curtain_kingart.cpp index 4097b4da..705b0db6 100644 --- a/code/espurna/curtain_kingart.cpp +++ b/code/espurna/curtain_kingart.cpp @@ -349,21 +349,21 @@ void _KACurtainResult() { #if MQTT_SUPPORT //------------------------------------------------------------------------------ -void _curtainMQTTCallback(unsigned int type, const char* topic, char* payload) { +void _curtainMQTTCallback(unsigned int type, espurna::StringView topic, espurna::StringView payload) { if (type == MQTT_CONNECT_EVENT) { mqttSubscribe(MQTT_TOPIC_CURTAIN); } else if (type == MQTT_MESSAGE_EVENT) { // Match topic - const String t = mqttMagnitude(topic); + const auto t = mqttMagnitude(topic); if (t.equals(MQTT_TOPIC_CURTAIN)) { - if (strcmp(payload, "pause") == 0) { + if (payload == "pause") { _KACurtainSet(CURTAIN_BUTTON_PAUSE); - } else if (strcmp(payload, "on") == 0) { + } else if (payload == "on") { _KACurtainSet(CURTAIN_BUTTON_OPEN); - } else if (strcmp(payload, "off") == 0) { + } else if (payload == "off") { _KACurtainSet(CURTAIN_BUTTON_CLOSE); } else { - _curtain_position_set = String(payload).toInt(); + _curtain_position_set = payload.toString().toInt(); _KACurtainSet(CURTAIN_BUTTON_UNKNOWN, _curtain_position_set); } } diff --git a/code/espurna/domoticz.cpp b/code/espurna/domoticz.cpp index bce1984e..dcae69dc 100644 --- a/code/espurna/domoticz.cpp +++ b/code/espurna/domoticz.cpp @@ -282,7 +282,7 @@ void unsubscribe() { mqttUnsubscribeRaw(settings::topicOut().c_str()); } -void callback(unsigned int type, const char* topic, char* payload) { +void callback(unsigned int type, espurna::StringView topic, espurna::StringView payload) { if (!enabled()) { return; } @@ -296,10 +296,10 @@ void callback(unsigned int type, const char* topic, char* payload) { } if (type == MQTT_MESSAGE_EVENT) { - auto out = settings::topicOut(); - if (out.equals(topic)) { + const auto out = settings::topicOut(); + if (topic == out) { DynamicJsonBuffer jsonBuffer(1024); - JsonObject& root = jsonBuffer.parseObject(payload); + JsonObject& root = jsonBuffer.parseObject(payload.begin()); if (!root.success()) { DEBUG_MSG_P(PSTR("[DOMOTICZ] Error parsing data\n")); return; diff --git a/code/espurna/garland.cpp b/code/espurna/garland.cpp index a31ae0f5..8ba0ce5f 100644 --- a/code/espurna/garland.cpp +++ b/code/espurna/garland.cpp @@ -329,11 +329,11 @@ bool executeCommand(const String& command) { one_color_palette.reset(new Palette("Color", {root[MQTT_PAYLOAD_PALETTE].as()})); newPalette = one_color_palette.get(); } else { - auto palette = root[MQTT_PAYLOAD_PALETTE].as(); + auto palette = root[MQTT_PAYLOAD_PALETTE].as(); bool palette_found = false; for (size_t i = 0; i < pals.size(); ++i) { auto pal_name = pals[i].name(); - if (strcmp(palette, pal_name) == 0) { + if (palette = pal_name) { newPalette = &pals[i]; palette_found = true; scene_setup_required = true; @@ -341,9 +341,9 @@ bool executeCommand(const String& command) { } } if (!palette_found) { - uint32_t color = (uint32_t)strtoul(palette, NULL, 0); - if (color != 0) { - one_color_palette.reset(new Palette("Color", {color})); + const auto result = parseUnsigned(palette); + if (result.ok) { + one_color_palette.reset(new Palette("Color", {result.value})); newPalette = one_color_palette.get(); } } @@ -409,17 +409,14 @@ void garlandLoop(void) { } //------------------------------------------------------------------------------ -void garlandMqttCallback(unsigned int type, const char* topic, char* payload) { +void garlandMqttCallback(unsigned int type, espurna::StringView topic, espurna::StringView payload) { if (type == MQTT_CONNECT_EVENT) { mqttSubscribe(MQTT_TOPIC_GARLAND); } if (type == MQTT_MESSAGE_EVENT) { - // Match topic - String t = mqttMagnitude(topic); - + auto t = mqttMagnitude(topic); if (t.equals(MQTT_TOPIC_GARLAND)) { - // Parse JSON input DynamicJsonBuffer jsonBuffer; JsonObject& root = jsonBuffer.parseObject(payload); if (!root.success()) { @@ -433,7 +430,7 @@ void garlandMqttCallback(unsigned int type, const char* topic, char* payload) { } if (command == MQTT_COMMAND_IMMEDIATE) { - _immediate_command = payload; + _immediate_command = payload.toString(); } else if (command == MQTT_COMMAND_RESET) { std::queue empty_queue; std::swap(_command_queue, empty_queue); @@ -444,9 +441,9 @@ void garlandMqttCallback(unsigned int type, const char* topic, char* payload) { setDefault(); garlandEnabled(true); } else if (command == MQTT_COMMAND_QUEUE) { - _command_queue.push(payload); + _command_queue.push(payload.toString()); } else if (command == MQTT_COMMAND_SEQUENCE) { - _command_sequence.push_back(payload); + _command_sequence.push_back(payload.toString()); } } } @@ -674,18 +671,19 @@ byte Anim::rngb() { //------------------------------------------------------------------------------ void garlandEnabled(bool enabled) { - _garland_enabled = enabled; setSetting(NAME_GARLAND_ENABLED, _garland_enabled); - if (!_garland_enabled) { - schedule_function([]() { + if (_garland_enabled != enabled) { + espurnaRegisterOnceUnique([]() { pixels.clear(); pixels.show(); }); } + _garland_enabled = enabled; + #if WEB_SUPPORT - wsPost([](JsonObject& root) { - root["garlandEnabled"] = _garland_enabled; + wsPost([enabled](JsonObject& root) { + root["garlandEnabled"] = enabled; }); #endif } diff --git a/code/espurna/homeassistant.cpp b/code/espurna/homeassistant.cpp index 84ce8de7..acae5055 100644 --- a/code/espurna/homeassistant.cpp +++ b/code/espurna/homeassistant.cpp @@ -316,7 +316,7 @@ struct RelayContext { RelayContext makeRelayContext() { return { - mqttTopic(MQTT_TOPIC_STATUS, false), + mqttTopic(MQTT_TOPIC_STATUS), quote(mqttPayloadStatus(true)), quote(mqttPayloadStatus(false)), quote(relayPayload(PayloadStatus::On).toString()), @@ -372,8 +372,8 @@ public: json[F("pl_off")] = _relay.payload_off.c_str(); json[F("uniq_id")] = uniqueId(); json[F("name")] = _ctx.name() + ' ' + _index; - json[F("stat_t")] = mqttTopic(MQTT_TOPIC_RELAY, _index, false); - json[F("cmd_t")] = mqttTopic(MQTT_TOPIC_RELAY, _index, true); + json[F("stat_t")] = mqttTopic(MQTT_TOPIC_RELAY, _index); + json[F("cmd_t")] = mqttTopicSetter(MQTT_TOPIC_RELAY, _index); json.printTo(_message); } return _message; @@ -477,10 +477,10 @@ public: json[F("name")] = _ctx.name() + ' ' + F("Light"); - json[F("stat_t")] = mqttTopic(MQTT_TOPIC_LIGHT_JSON, false); - json[F("cmd_t")] = mqttTopic(MQTT_TOPIC_LIGHT_JSON, true); + json[F("stat_t")] = mqttTopic(MQTT_TOPIC_LIGHT_JSON); + json[F("cmd_t")] = mqttTopicSetter(MQTT_TOPIC_LIGHT_JSON); - json[F("avty_t")] = mqttTopic(MQTT_TOPIC_STATUS, false); + json[F("avty_t")] = mqttTopic(MQTT_TOPIC_STATUS); json[F("pl_avail")] = quote(mqttPayloadStatus(true)); json[F("pl_not_avail")] = quote(mqttPayloadStatus(false)); @@ -597,7 +597,7 @@ bool heartbeat(espurna::heartbeat::Mask mask) { String message; root.printTo(message); - String topic = mqttTopic(MQTT_TOPIC_LIGHT_JSON, false); + String topic = mqttTopic(MQTT_TOPIC_LIGHT_JSON); mqttSendRaw(topic.c_str(), message.c_str(), false); } @@ -608,9 +608,9 @@ void publishLightJson() { heartbeat(static_cast(heartbeat::Report::Light)); } -void receiveLightJson(char* payload) { +void receiveLightJson(espurna::StringView payload) { DynamicJsonBuffer buffer(1024); - JsonObject& root = buffer.parseObject(payload); + JsonObject& root = buffer.parseObject(payload.begin()); if (!root.success()) { return; } @@ -719,7 +719,7 @@ public: json[F("uniq_id")] = uniqueId(); json[F("name")] = _ctx.name() + ' ' + name() + ' ' + localId(); - json[F("stat_t")] = mqttTopic(_info.topic, false); + json[F("stat_t")] = mqttTopic(_info.topic); json[F("unit_of_meas")] = magnitudeUnitsName(_info.units); json.printTo(_message); @@ -1026,7 +1026,7 @@ void configure() { homeassistant::publishDiscovery(); } -void mqttCallback(unsigned int type, const char* topic, char* payload) { +void mqttCallback(unsigned int type, StringView topic, StringView payload) { if (MQTT_DISCONNECT_EVENT == type) { if (internal::state == internal::State::Sent) { internal::state = internal::State::Pending; @@ -1045,7 +1045,7 @@ void mqttCallback(unsigned int type, const char* topic, char* payload) { #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE if (type == MQTT_MESSAGE_EVENT) { - String t = ::mqttMagnitude(topic); + auto t = ::mqttMagnitude(topic); if (t.equals(MQTT_TOPIC_LIGHT_JSON)) { receiveLightJson(payload); } diff --git a/code/espurna/ifan.cpp b/code/espurna/ifan.cpp index 38d259b8..af09c7a6 100644 --- a/code/espurna/ifan.cpp +++ b/code/espurna/ifan.cpp @@ -69,7 +69,7 @@ String speedToPayload(FanSpeed speed) { return espurna::settings::internal::serialize(speed); } -constexpr unsigned long DefaultSaveDelay { 1000ul }; +static constexpr auto DefaultSaveDelay = duration::Seconds{ 10 }; // We expect to write a specific 'mask' via GPIO LOW & HIGH to set the speed // Sync up with the relay and write it on ON / OFF status events @@ -96,25 +96,21 @@ constexpr int controlPin() { } struct Config { - Config() = default; - explicit Config(unsigned long save_, FanSpeed speed_) : - save(save_), - speed(speed_) - {} - - unsigned long save { DefaultSaveDelay }; - FanSpeed speed { FanSpeed::Off }; - StatePins state_pins; + duration::Seconds save; + FanSpeed speed; }; Config readSettings() { - return Config( - getSetting("fanSave", DefaultSaveDelay), - getSetting("fanSpeed", FanSpeed::Medium) - ); + return Config{ + .save = getSetting("fanSave", DefaultSaveDelay), + .speed = getSetting("fanSpeed", FanSpeed::Medium)}; } -Config config; +StatePins state_pins; +Config config { + .save = DefaultSaveDelay, + .speed = FanSpeed::Medium, +}; void configure() { config = readSettings(); @@ -127,10 +123,10 @@ void report(FanSpeed speed [[gnu::unused]]) { } void save(FanSpeed speed) { - static Ticker ticker; + static timer::SystemTimer ticker; config.speed = speed; - ticker.once_ms(config.save, []() { - auto value = speedToPayload(config.speed); + ticker.once(config.save, []() { + const auto value = speedToPayload(config.speed); setSetting("fanSpeed", value); DEBUG_MSG_P(PSTR("[IFAN] Saved speed setting \"%s\"\n"), value.c_str()); }); @@ -217,13 +213,13 @@ void updateSpeed(FanSpeed speed) { updateSpeed(config, speed); } -void updateSpeedFromPayload(const String& payload) { - updateSpeed(payloadToSpeed(payload)); +void updateSpeedFromPayload(StringView payload) { + updateSpeed(payloadToSpeed(payload.toString())); } #if MQTT_SUPPORT -void onMqttEvent(unsigned int type, const char* topic, char* payload) { +void onMqttEvent(unsigned int type, StringView topic, StringView payload) { switch (type) { case MQTT_CONNECT_EVENT: @@ -245,9 +241,10 @@ void onMqttEvent(unsigned int type, const char* topic, char* payload) { class FanProvider : public RelayProviderBase { public: - explicit FanProvider(BasePinPtr&& pin, const Config& config, FanSpeedUpdate& callback) : + FanProvider(BasePinPtr&& pin, const Config& config, const StatePins& pins, FanSpeedUpdate& callback) : _pin(std::move(pin)), - _config(config) + _config(config), + _pins(pins) { callback = [this](FanSpeed speed) { change(speed); @@ -265,8 +262,8 @@ public: auto state = stateFromSpeed(speed); DEBUG_MSG_P(PSTR("[IFAN] State mask: %s\n"), maskFromSpeed(speed)); - for (size_t index = 0; index < _config.state_pins.size(); ++index) { - auto& pin = _config.state_pins[index].second; + for (size_t index = 0; index < _pins.size(); ++index) { + auto& pin = _pins[index].second; if (!pin) { continue; } @@ -282,6 +279,7 @@ public: private: BasePinPtr _pin; const Config& _config; + const StatePins& _pins; }; #if TERMINAL_SUPPORT @@ -314,19 +312,18 @@ void setup() { #endif void setup() { - - config.state_pins = setupStatePins(); - if (!config.state_pins.size()) { + state_pins = setupStatePins(); + if (!state_pins.size()) { return; } configure(); - espurnaRegisterReload(configure); auto relay_pin = gpioRegister(controlPin()); if (relay_pin) { - auto provider = std::make_unique(std::move(relay_pin), config, onFanSpeedUpdate); + auto provider = std::make_unique( + std::move(relay_pin), config, state_pins, onFanSpeedUpdate); if (!relayAdd(std::move(provider))) { DEBUG_MSG_P(PSTR("[IFAN] Could not add relay provider for GPIO%d\n"), controlPin()); gpioUnlock(controlPin()); diff --git a/code/espurna/ir.cpp b/code/espurna/ir.cpp index 20c4636f..5a693381 100644 --- a/code/espurna/ir.cpp +++ b/code/espurna/ir.cpp @@ -418,33 +418,22 @@ private: Result _result; }; -// TODO: std::from_chars works directly with the view. not available with -std=c++11, -// and needs some care in regards to the code size - template -T sized(StringView view) { - String value(view); - - char* endp { nullptr }; - unsigned long result { std::strtoul(value.c_str(), &endp, 10) }; - if ((endp != value.c_str()) && (*endp == '\0')) { - constexpr unsigned long Boundary { 1ul << (sizeof(T) * 8) }; - if (result < Boundary) { - return result; - } +T sized(StringView value) { + const auto result = parseUnsigned(value, 10); + constexpr decltype(result.value) Boundary { 1ul << (sizeof(T) * 8) }; + if (result.ok && (result.value < Boundary)) { + return result.value; } return 0; } template <> -unsigned long sized(StringView view) { - String value(view); - - char* endp { nullptr }; - unsigned long result { std::strtoul(value.c_str(), &endp, 10) }; - if ((endp != value.c_str()) && (*endp == '\0')) { - return result; +unsigned long sized(StringView value) { + const auto result = parseUnsigned(value, 10); + if (result.ok) { + return result.value; } return 0; @@ -1263,7 +1252,7 @@ bool publish_raw { build::rxRaw() }; bool publish_simple { build::rxSimple() }; bool publish_state { build::rxState() }; -void callback(unsigned int type, const char* topic, char* payload) { +void callback(unsigned int type, StringView topic, StringView payload) { switch (type) { case MQTT_CONNECT_EVENT: @@ -1273,15 +1262,13 @@ void callback(unsigned int type, const char* topic, char* payload) { break; case MQTT_MESSAGE_EVENT: { - StringView view{payload, payload + strlen(payload)}; - - String t = mqttMagnitude(topic); + auto t = mqttMagnitude(topic); if (t.equals(build::topicTxSimple())) { - ir::tx::enqueue(ir::simple::parse(view)); + ir::tx::enqueue(ir::simple::parse(payload)); } else if (t.equals(build::topicTxState())) { - ir::tx::enqueue(ir::state::parse(view)); + ir::tx::enqueue(ir::state::parse(payload)); } else if (t.equals(build::topicTxRaw())) { - ir::tx::enqueue(ir::raw::parse(view)); + ir::tx::enqueue(ir::raw::parse(payload)); } break; diff --git a/code/espurna/led.cpp b/code/espurna/led.cpp index 48f5a5fb..4e475732 100644 --- a/code/espurna/led.cpp +++ b/code/espurna/led.cpp @@ -362,11 +362,8 @@ bool Led::toggle() { #include "led_pattern.re.ipp" -} // namespace - namespace settings { namespace keys { -namespace { alignas(4) static constexpr char Gpio[] PROGMEM = "ledGpio"; alignas(4) static constexpr char Inverse[] PROGMEM = "ledInv"; @@ -374,11 +371,9 @@ alignas(4) static constexpr char Mode[] PROGMEM = "ledMode"; alignas(4) static constexpr char Relay[] PROGMEM = "ledRelay"; alignas(4) static constexpr char Pattern[] PROGMEM = "ledPattern"; -} // namespace } // namespace keys namespace options { -namespace { using espurna::settings::options::Enumeration; @@ -415,9 +410,9 @@ static constexpr Enumeration LedModeOptions[] PROGMEM { #endif }; -} // namespace } // namespace options } // namespace settings +} // namespace } // namespace led // ----------------------------------------------------------------------------- @@ -445,9 +440,10 @@ String serialize(LedMode mode) { // ----------------------------------------------------------------------------- namespace led { -namespace build { namespace { +namespace build { + constexpr size_t LedsMax { 8ul }; constexpr size_t preconfiguredLeds() { @@ -531,11 +527,9 @@ constexpr bool inverse(size_t index) { ); } -} // namespace } // namespace build namespace settings { -namespace { unsigned char pin(size_t id) { return getSetting({keys::Gpio, id}, build::pin(id)); @@ -567,7 +561,6 @@ void migrate(int version) { } } -} // namespace } // namespace settings // For network-based modes, indefinitely cycle ON <-> OFF @@ -590,18 +583,15 @@ LED_STATIC_DELAY(NetworkConfigInverse, 900, 100); LED_STATIC_DELAY(NetworkIdle, 500, 500); namespace internal { -namespace { std::vector leds; bool update { false }; -} // namespace } // namespace internal namespace settings { namespace query { namespace internal { -namespace { #define ID_VALUE(NAME)\ String NAME (size_t id) {\ @@ -616,11 +606,8 @@ ID_VALUE(relay) #undef ID_VALUE -} // namespace } // namespace internal -namespace { - static constexpr espurna::settings::query::IndexedSetting IndexedSettings[] PROGMEM { {keys::Gpio, internal::pin}, {keys::Inverse, internal::inverse}, @@ -646,14 +633,12 @@ void setup() { }); } -} // namespace } // namespace query } // namespace settings #if RELAY_SUPPORT namespace relay { namespace internal { -namespace { struct Link { Led& led; @@ -697,11 +682,8 @@ size_t find(Led& led) { return RelaysMax; } -} // namespace } // namespace internal -namespace { - void unlink(Led& led) { internal::unlink(led); } @@ -730,12 +712,9 @@ bool areAnyOn() { return result; } -} // namespace } // namespace relay #endif -namespace { - size_t count() { return internal::leds.size(); } @@ -794,8 +773,10 @@ void pattern(Led& led, Pattern&& other) { } void payload_status(Led& led, StringView payload) { - led.mode(LedMode::Manual); led.stop(); + led.status(false); + + led.mode(LedMode::Manual); const auto value = rpcParsePayload(payload); switch (value) { @@ -957,13 +938,10 @@ void loop() { cancel(); } -} // namespace - #if MQTT_SUPPORT namespace mqtt { -namespace { -void callback(unsigned int type, const char* topic, char* payload) { +void callback(unsigned int type, StringView topic, StringView payload) { if (type == MQTT_CONNECT_EVENT) { mqttSubscribe(MQTT_TOPIC_LED "/+"); return; @@ -972,13 +950,13 @@ void callback(unsigned int type, const char* topic, char* payload) { // Only want `led/+/` // We get the led ID from the `+` if (type == MQTT_MESSAGE_EVENT) { - const String magnitude = mqttMagnitude(topic); + const auto magnitude = mqttMagnitude(topic); if (!magnitude.startsWith(MQTT_TOPIC_LED)) { return; } size_t ledID; - if (tryParseId(mqttMagnitudeTail(magnitude, MQTT_TOPIC_LED), ledCount, ledID)) { + if (tryParseIdPath(magnitude, ledCount(), ledID)) { payload_status(internal::leds[ledID], payload); } @@ -986,13 +964,11 @@ void callback(unsigned int type, const char* topic, char* payload) { } } -} // namespace } // namespace mqtt #endif // MQTT_SUPPORT #if WEB_SUPPORT namespace web { -namespace { bool onKeyCheck(StringView key, const JsonVariant&) { return settings::query::checkSamePrefix(key); @@ -1009,20 +985,18 @@ void onConnected(JsonObject& root) { } } -} // namespace } // namespace web #endif // WEB_SUPPORT #if TERMINAL_SUPPORT namespace terminal { -namespace { alignas(4) static constexpr char Led[] PROGMEM = "LED"; void led(::terminal::CommandContext&& ctx) { if (ctx.argv.size() > 1) { size_t id; - if (!tryParseId(ctx.argv[1], ledCount, id)) { + if (!tryParseId(ctx.argv[1], ledCount(), id)) { terminalError(ctx, F("Invalid ledID")); return; } @@ -1056,12 +1030,9 @@ void setup() { espurna::terminal::add(Commands); } -} // namespace } // namespace terminal #endif -namespace { - void setup() { migrateVersion(settings::migrate); internal::leds.reserve(build::preconfiguredLeds()); @@ -1107,7 +1078,7 @@ void setup() { } // namespace } // namespace led -} // namespace led +} // namespace espurna bool ledStatus(size_t id, bool status) { if (id < espurna::led::count()) { diff --git a/code/espurna/led_pattern.re b/code/espurna/led_pattern.re index 1f64cd69..dab2007e 100644 --- a/code/espurna/led_pattern.re +++ b/code/espurna/led_pattern.re @@ -13,8 +13,6 @@ Copyright (C) 2020-2021 by Maxim Prokhorov #include +#include "../types.h" + class URL { public: URL() = default; @@ -22,22 +24,19 @@ public: URL& operator=(const URL&) = default; URL& operator=(URL&&) = default; - URL(const String& string) { + explicit URL(espurna::StringView string) { _parse(string); } - URL(String&& string) { - _parse(std::move(string)); - } - String protocol; String host; String path; uint16_t port { 0 }; private: - void _parse(String buffer) { - // cut the protocol part + void _parse(espurna::StringView string) { + auto buffer = string.toString(); + int index = buffer.indexOf("://"); if (index > 0) { this->protocol = buffer.substring(0, index); diff --git a/code/espurna/light.cpp b/code/espurna/light.cpp index 76ca0082..88e0cda6 100644 --- a/code/espurna/light.cpp +++ b/code/espurna/light.cpp @@ -1019,33 +1019,59 @@ void _lightFromHexPayload(espurna::StringView payload) { } } -void _lightFromCommaSeparatedPayload(espurna::StringView payload) { - constexpr size_t BufferSize { 16 }; - if (payload.length() < BufferSize) { - char buffer[BufferSize] = {0}; - std::copy(payload.begin(), payload.end(), buffer); +template +const char* _lightForEachToken(espurna::StringView payload, char sep, T&& callback) { + const auto begin = payload.begin(); + const auto end = payload.end(); + + auto it = begin; + for (auto last = it; it != end; ++it) { + last = it; + it = std::find(it, payload.end(), ','); + if (!callback(espurna::StringView(last, it))) { + break; + } + if (it == end) { + break; + } + } - auto it = _light_channels.begin(); - char* tok = std::strtok(buffer, ","); + return it; +} - while ((it != _light_channels.end()) && (tok != nullptr)) { - char* endp { nullptr }; - auto value = std::strtol(tok, &endp, 10); - if ((endp == tok) || (*endp != '\0')) { - break; +template +const char* _lightApplyForEachToken(espurna::StringView payload, char sep, Begin& it, End end) { + return _lightForEachToken(payload, sep, + [&](espurna::StringView token) { + if (it != end) { + const auto result = parseUnsigned(token, 10); + if (result.ok) { + (*it) = result.value; + ++it; + return true; + } } - (*it) = value; - ++it; + return false; + }); +} - tok = std::strtok(nullptr, ","); - } +void _lightFromCommaSeparatedPayload(espurna::StringView payload) { + const auto end = _light_channels.end(); - // same as previous versions, set the rest to zeroes - while (it != _light_channels.end()) { - (*it) = 0; - ++it; - } + auto it = _light_channels.begin(); + if (it == end) { + return; + } + + // every channel value is separated by a comma + _lightApplyForEachToken(payload, ',', it, end); + + // and fill the rest with zeroes + while (it != end) { + DEBUG_MSG_P(PSTR(":set %p with zero\n"), it); + (*it) = 0; + ++it; } } @@ -1072,45 +1098,29 @@ void _lightFromRgbPayload(espurna::StringView payload) { _lightFromCommaSeparatedPayload(payload); } -// HSV string is expected to be "H,S,V", where: -// - H [0...360] -// - S [0...100] -// - V [0...100] - void _lightFromHsvPayload(espurna::StringView payload) { - if (!_light_has_color || !payload.length() || (payload[0] == '\0')) { + if (!_light_has_color || !payload.length()) { return; } - constexpr size_t BufferSize { 16 }; - - if (payload.length() < BufferSize) { - char buffer[BufferSize] = {0}; - std::copy(payload.begin(), payload.end(), buffer); - - long values[3] {0, 0, 0}; - char* tok = std::strtok(buffer, ","); - - auto it = std::begin(values); - while ((it != std::end(values)) && (tok != nullptr)) { - char* endp { nullptr }; - auto value = std::strtol(tok, &endp, 10); - if ((endp == tok) || (*endp != '\0')) { - break; - } + long hsv[3] {0, 0, 0}; + auto it = std::begin(hsv); - (*it) = value; - ++it; - - tok = std::strtok(nullptr, ","); - } + // HSV string is expected to be "H,S,V", where: + // - H [0...360] + // - S [0...100] + // - V [0...100] + const auto parsed = _lightApplyForEachToken( + payload, ',', it, std::end(hsv)); - if (it != std::end(values)) { - return; - } - - lightHsv({values[0], values[1], values[2]}); + // discard partial or uneven payloads + if ((parsed != payload.end()) || (it != std::end(hsv))) { + return; } + + // values are expected to be 'clamped' either + // in the call or in ctor of the helper object + lightHsv({hsv[0], hsv[1], hsv[2]}); } // Thanks to Sacha Telgenhof for sharing this code in his AiLight library @@ -1261,17 +1271,15 @@ String _lightRgbPayload() { return _lightRgbPayload(_lightToInputRgb()); } -void _lightFromGroupPayload(const char* payload) { - if (!payload || *payload == '\0') { +void _lightFromGroupPayload(espurna::StringView payload) { + if (!payload.length()) { return; } constexpr size_t BufferSize { 32 }; - const size_t PayloadLen { strlen(payload) }; - - if (PayloadLen < BufferSize) { + if (payload.length() < BufferSize) { char buffer[BufferSize] = {0}; - std::copy(payload, payload + PayloadLen, buffer); + std::copy(payload.begin(), payload.end(), buffer); char* tok = std::strtok(buffer, ","); auto it = _light_channels.begin(); @@ -2001,7 +2009,8 @@ void _lightSaveSettings() { } for (size_t channel = 0; channel < _light_channels.size(); ++channel) { - espurna::light::settings::value(channel, _light_channels[channel].inputValue); + espurna::light::settings::value( + channel, _light_channels[channel].inputValue); } espurna::light::settings::brightness(_light_brightness); @@ -2038,7 +2047,7 @@ bool _lightParsePayload(espurna::StringView payload) { } bool _lightTryParseChannel(espurna::StringView value, size_t& id) { - return tryParseId(value, lightChannels, id); + return tryParseIdPath(value, lightChannels(), id); } } // namespace @@ -2049,6 +2058,18 @@ bool _lightTryParseChannel(espurna::StringView value, size_t& id) { namespace { +bool _lightApiTransition(espurna::StringView payload) { + const auto result = parseUnsigned(payload, 10); + if (result.ok) { + lightTransition( + espurna::duration::Milliseconds(result.value), + _light_transition_step); + return true; + } + + return false; +} + int _lightMqttReportMask() { return espurna::light::DefaultReport & ~(static_cast(mqttForward() ? espurna::light::Report::None : espurna::light::Report::Mqtt)); } @@ -2081,7 +2102,7 @@ bool _lightMqttHeartbeat(espurna::heartbeat::Mask mask) { return mqttConnected(); } -void _lightMqttCallback(unsigned int type, const char* topic, char* payload) { +void _lightMqttCallback(unsigned int type, espurna::StringView topic, espurna::StringView payload) { String mqtt_group_color = espurna::light::settings::mqttGroup(); if (type == MQTT_CONNECT_EVENT) { @@ -2113,14 +2134,14 @@ void _lightMqttCallback(unsigned int type, const char* topic, char* payload) { if (type == MQTT_MESSAGE_EVENT) { // Group color - if ((mqtt_group_color.length() > 0) && (mqtt_group_color.equals(topic))) { + if ((mqtt_group_color.length() > 0) && (topic == mqtt_group_color)) { _lightFromGroupPayload(payload); _lightUpdateFromMqttGroup(); return; } // Match topic - String t = mqttMagnitude(topic); + auto t = mqttMagnitude(topic); // Color temperature in mireds if (t.equals(MQTT_TOPIC_MIRED)) { @@ -2149,17 +2170,9 @@ void _lightMqttCallback(unsigned int type, const char* topic, char* payload) { return; } - // Transition setting + // Transition setting (persist) if (t.equals(MQTT_TOPIC_TRANSITION)) { - char* endp { nullptr }; - auto result = strtoul(payload, &endp, 10); - if (!endp || (endp == payload)) { - return; - } - - lightTransition( - espurna::duration::Milliseconds(result), - _light_transition_step); + _lightApiTransition(payload); return; } @@ -2173,7 +2186,7 @@ void _lightMqttCallback(unsigned int type, const char* topic, char* payload) { // Channel if (t.startsWith(MQTT_TOPIC_CHANNEL)) { size_t id; - if (_lightTryParseChannel(mqttMagnitudeTail(t, MQTT_TOPIC_CHANNEL), id)) { + if (_lightTryParseChannel(t, id)) { _lightAdjustChannel(id, payload); _lightUpdateFromMqtt(); } @@ -2318,21 +2331,7 @@ void _lightApiSetup() { return true; }, [](ApiRequest& request) { - auto value = request.param(F("value")); - - const char* p { value.c_str() }; - char* endp { nullptr }; - - auto result = strtoul(p, &endp, 10); - if (!endp || (endp == p)) { - return false; - } - - lightTransition( - espurna::duration::Milliseconds(result), - _light_transition_step); - - return true; + return _lightApiTransition(request.param(F("value"))); } ); @@ -2448,10 +2447,10 @@ void _lightWebSocketOnAction(uint32_t client_id, const char* action, JsonObject& STRING_VIEW_INLINE(Hsv, "hsv"); if (data.containsKey(Rgb)) { - _lightFromRgbPayload(data[Rgb].as()); + _lightFromRgbPayload(data[Rgb].as()); lightUpdate(); } else if (data.containsKey(Hsv)) { - _lightFromHsvPayload(data[Hsv].as()); + _lightFromHsvPayload(data[Hsv].as()); lightUpdate(); } } diff --git a/code/espurna/mqtt.cpp b/code/espurna/mqtt.cpp index edc91ba6..14e3290e 100644 --- a/code/espurna/mqtt.cpp +++ b/code/espurna/mqtt.cpp @@ -72,12 +72,12 @@ namespace { #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT -struct MqttPidCallback { +struct MqttPidCallbackHandler { uint16_t pid; - mqtt_pid_callback_f run; + MqttPidCallback callback; }; -using MqttPidCallbacks = std::forward_list; +using MqttPidCallbacks = std::forward_list; MqttPidCallbacks _mqtt_publish_callbacks; MqttPidCallbacks _mqtt_subscribe_callbacks; @@ -91,7 +91,7 @@ espurna::duration::Seconds _mqtt_heartbeat_interval; String _mqtt_payload_online; String _mqtt_payload_offline; -std::forward_list _mqtt_callbacks; +std::forward_list _mqtt_callbacks; } // namespace @@ -124,17 +124,15 @@ namespace mqtt { namespace build { namespace { -constexpr espurna::duration::Milliseconds SkipTime { MQTT_SKIP_TIME }; +static constexpr espurna::duration::Milliseconds SkipTime { MQTT_SKIP_TIME }; -constexpr espurna::duration::Milliseconds ReconnectDelayMin { MQTT_RECONNECT_DELAY_MIN }; -constexpr espurna::duration::Milliseconds ReconnectDelayMax { MQTT_RECONNECT_DELAY_MAX }; -constexpr espurna::duration::Milliseconds ReconnectStep { MQTT_RECONNECT_DELAY_STEP }; +static constexpr espurna::duration::Milliseconds ReconnectDelayMin { MQTT_RECONNECT_DELAY_MIN }; +static constexpr espurna::duration::Milliseconds ReconnectDelayMax { MQTT_RECONNECT_DELAY_MAX }; +static constexpr espurna::duration::Milliseconds ReconnectStep { MQTT_RECONNECT_DELAY_STEP }; -constexpr size_t MessageLogMax { 128ul }; +static constexpr size_t MessageLogMax { 128ul }; -const __FlashStringHelper* server() { - return F(MQTT_SERVER); -} +alignas(4) static constexpr char Server[] PROGMEM = MQTT_SERVER; constexpr uint16_t port() { return MQTT_PORT; @@ -148,25 +146,12 @@ constexpr bool autoconnect() { return 1 == MQTT_AUTOCONNECT; } -const __FlashStringHelper* topic() { - return F(MQTT_TOPIC); -} - -const __FlashStringHelper* getter() { - return F(MQTT_GETTER); -} - -const __FlashStringHelper* setter() { - return F(MQTT_SETTER); -} +alignas(4) static constexpr char Topic[] PROGMEM = MQTT_TOPIC; +alignas(4) static constexpr char Getter[] PROGMEM = MQTT_GETTER; +alignas(4) static constexpr char Setter[] PROGMEM = MQTT_SETTER; -const __FlashStringHelper* user() { - return F(MQTT_USER); -} - -const __FlashStringHelper* password() { - return F(MQTT_PASS); -} +alignas(4) static constexpr char User[] PROGMEM = MQTT_USER; +alignas(4) static constexpr char Password[] PROGMEM = MQTT_PASS; constexpr int qos() { return MQTT_QOS; @@ -176,8 +161,8 @@ constexpr bool retain() { return 1 == MQTT_RETAIN; } -constexpr KeepAlive KeepaliveMin { 15 }; -constexpr KeepAlive KeepaliveMax{ KeepAlive::max() }; +static constexpr KeepAlive KeepaliveMin { 15 }; +static constexpr KeepAlive KeepaliveMax{ KeepAlive::max() }; constexpr KeepAlive keepalive() { return KeepAlive { MQTT_KEEPALIVE }; @@ -186,29 +171,21 @@ constexpr KeepAlive keepalive() { static_assert(keepalive() >= KeepaliveMin, ""); static_assert(keepalive() <= KeepaliveMax, ""); -const __FlashStringHelper* topicWill() { - return F(MQTT_TOPIC_STATUS); -} +alignas(4) static constexpr char TopicWill[] PROGMEM = MQTT_TOPIC_STATUS; constexpr bool json() { return 1 == MQTT_USE_JSON; } -const __FlashStringHelper* topicJson() { - return F(MQTT_TOPIC_JSON); -} +static constexpr auto JsonDelay = espurna::duration::Milliseconds(MQTT_USE_JSON_DELAY); +alignas(4) static constexpr char TopicJson[] PROGMEM = MQTT_TOPIC_JSON; constexpr espurna::duration::Milliseconds skipTime() { return espurna::duration::Milliseconds(MQTT_SKIP_TIME); } -const __FlashStringHelper* payloadOnline() { - return F(MQTT_STATUS_ONLINE); -} - -const __FlashStringHelper* payloadOffline() { - return F(MQTT_STATUS_OFFLINE); -} +alignas(4) static constexpr char PayloadOnline[] PROGMEM = MQTT_STATUS_ONLINE; +alignas(4) static constexpr char PayloadOffline[] PROGMEM = MQTT_STATUS_OFFLINE; constexpr bool secure() { return 1 == MQTT_SSL_ENABLED; @@ -218,9 +195,7 @@ int secureClientCheck() { return MQTT_SECURE_CLIENT_CHECK; } -const __FlashStringHelper* fingerprint() { - return F(MQTT_SSL_FINGERPRINT); -} +alignas(4) static constexpr char Fingerprint[] PROGMEM = MQTT_SSL_FINGERPRINT; constexpr uint16_t mfln() { return MQTT_SECURE_CLIENT_MFLN; @@ -272,7 +247,7 @@ alignas(4) static constexpr char SecureClientMfln[] PROGMEM = "mqttScMFLN"; namespace { String server() { - return getSetting(keys::Server, build::server()); + return getSetting(keys::Server, espurna::StringView(build::Server)); } uint16_t port() { @@ -288,23 +263,23 @@ bool autoconnect() { } String topic() { - return getSetting(keys::Topic, build::topic()); + return getSetting(keys::Topic, espurna::StringView(build::Topic)); } String getter() { - return getSetting(keys::Getter, build::getter()); + return getSetting(keys::Getter, espurna::StringView(build::Getter)); } String setter() { - return getSetting(keys::Setter, build::setter()); + return getSetting(keys::Setter, espurna::StringView(build::Setter)); } String user() { - return getSetting(keys::User, build::user()); + return getSetting(keys::User, espurna::StringView(build::User)); } String password() { - return getSetting(keys::Password, build::password()); + return getSetting(keys::Password, espurna::StringView(build::Password)); } int qos() { @@ -326,7 +301,7 @@ String clientId() { } String topicWill() { - return getSetting(keys::TopicWill, build::topicWill()); + return getSetting(keys::TopicWill, espurna::StringView(build::TopicWill)); } bool json() { @@ -334,7 +309,7 @@ bool json() { } String topicJson() { - return getSetting(keys::TopicJson, build::topicJson()); + return getSetting(keys::TopicJson, espurna::StringView(build::TopicJson)); } espurna::heartbeat::Mode heartbeatMode() { @@ -350,11 +325,11 @@ espurna::duration::Milliseconds skipTime() { } String payloadOnline() { - return getSetting(keys::PayloadOnline, build::payloadOnline()); + return getSetting(keys::PayloadOnline, espurna::StringView(build::PayloadOnline)); } String payloadOffline() { - return getSetting(keys::PayloadOffline, build::payloadOffline()); + return getSetting(keys::PayloadOffline, espurna::StringView(build::PayloadOffline)); } [[gnu::unused]] @@ -369,7 +344,7 @@ int secureClientCheck() { [[gnu::unused]] String fingerprint() { - return getSetting(keys::Fingerprint, build::fingerprint()); + return getSetting(keys::Fingerprint, espurna::StringView(build::Fingerprint)); } [[gnu::unused]] @@ -487,13 +462,32 @@ static void _mqttApplySetting(Lhs& lhs, Rhs&& rhs) { } } -template -static void _mqttApplyTopic(String& lhs, Rhs&& rhs) { - auto topic = mqttTopic(rhs, false); - if (lhs != topic) { - mqttFlush(); - lhs = std::move(topic); +// Can't have **any** MQTT placeholders but our own `{magnitude}` +bool _mqttValidTopicString(espurna::StringView value) { + size_t hash = 0; + size_t plus = 0; + for (auto it = value.begin(); it != value.end(); ++it) { + switch (*it) { + case '#': + ++hash; + break; + case '+': + ++plus; + break; + } } + + return (hash <= 1) && (plus == 0); +} + +bool _mqttApplyValidTopicString(String& lhs, String&& rhs) { + if (_mqttValidTopicString(rhs)) { + _mqttApplySetting(lhs, std::move(rhs)); + return true; + } + + mqttDisconnect(); + return false; } } // namespace @@ -715,6 +709,8 @@ void _mqttMdnsStop(); void _mqttConfigure() { + _mqtt_enabled = false; + // Make sure we have both the server to connect to things are enabled { _mqttApplySetting(_mqtt_settings.server, mqtt::settings::server()); @@ -734,7 +730,6 @@ void _mqttConfigure() { _mqttMdnsSchedule(); } #endif - _mqtt_enabled = false; return; } } @@ -743,22 +738,34 @@ void _mqttConfigure() { { // Replace things inside curly braces (like {hostname}, {mac} etc.) auto topic = _mqttPlaceholders(mqtt::settings::topic()); + if (!_mqttValidTopicString(topic)) { + mqttDisconnect(); + return; + } + + // Topic **must** end with some kind of word if (topic.endsWith("/")) { topic.remove(topic.length() - 1); } + // For simple topics, sssume right-hand side contains magnitude if (topic.indexOf("#") == -1) { topic.concat("/#"); } - _mqttApplySetting(_mqtt_settings.topic, topic); + _mqttApplySetting(_mqtt_settings.topic, std::move(topic)); } // Getter and setter - _mqttApplySetting(_mqtt_settings.getter, mqtt::settings::getter()); - _mqttApplySetting(_mqtt_settings.setter, mqtt::settings::setter()); + _mqttApplyValidTopicString(_mqtt_settings.getter, mqtt::settings::getter()); + _mqttApplyValidTopicString(_mqtt_settings.setter, mqtt::settings::setter()); _mqttApplySetting(_mqtt_forward, - !_mqtt_settings.setter.equals(_mqtt_settings.getter)); + !_mqtt_settings.setter.equals(_mqtt_settings.getter)); + + // Last will aka status topic + // (note that *must* be after topic updates) + _mqttApplyValidTopicString(_mqtt_settings.will, + mqttTopic(mqtt::settings::topicWill())); // MQTT options _mqttApplySetting(_mqtt_settings.user, _mqttPlaceholders(mqtt::settings::user())); @@ -770,12 +777,10 @@ void _mqttConfigure() { _mqttApplySetting(_mqtt_settings.retain, mqtt::settings::retain()); _mqttApplySetting(_mqtt_settings.keepalive, mqtt::settings::keepalive()); - _mqttApplyTopic(_mqtt_settings.will, mqtt::settings::topicWill()); - // MQTT JSON _mqttApplySetting(_mqtt_use_json, mqtt::settings::json()); if (_mqtt_use_json) { - _mqttApplyTopic(_mqtt_settings.topic_json, mqtt::settings::topicJson()); + _mqttApplyValidTopicString(_mqtt_settings.topic_json, mqtt::settings::topicJson()); } // Heartbeat messages @@ -1006,13 +1011,13 @@ void _mqttCommandsSetup() { namespace { -void _mqttCallback(unsigned int type, const char* topic, char* payload) { +void _mqttCallback(unsigned int type, espurna::StringView topic, espurna::StringView payload) { if (type == MQTT_CONNECT_EVENT) { mqttSubscribe(MQTT_TOPIC_ACTION); } if (type == MQTT_MESSAGE_EVENT) { - String t = mqttMagnitude(topic); + auto t = mqttMagnitude(topic); if (t.equals(MQTT_TOPIC_ACTION)) { rpcHandleAction(payload); } @@ -1117,8 +1122,10 @@ void _mqttOnConnect() { systemHeartbeat(_mqttHeartbeat, _mqtt_heartbeat_mode, _mqtt_heartbeat_interval); // Notify all subscribers about the connection - for (auto& callback : _mqtt_callbacks) { - callback(MQTT_CONNECT_EVENT, nullptr, nullptr); + for (const auto callback : _mqtt_callbacks) { + callback(MQTT_CONNECT_EVENT, + espurna::StringView(), + espurna::StringView()); } DEBUG_MSG_P(PSTR("[MQTT] Connected!\n")); @@ -1136,8 +1143,10 @@ void _mqttOnDisconnect() { systemStopHeartbeat(_mqttHeartbeat); // Notify all subscribers about the disconnect - for (auto& callback : _mqtt_callbacks) { - callback(MQTT_DISCONNECT_EVENT, nullptr, nullptr); + for (const auto callback : _mqtt_callbacks) { + callback(MQTT_DISCONNECT_EVENT, + espurna::StringView(), + espurna::StringView()); } DEBUG_MSG_P(PSTR("[MQTT] Disconnected!\n")); @@ -1158,7 +1167,7 @@ void _mqttPidCallback(MqttPidCallbacks& callbacks, uint16_t pid) { while (it != end) { if ((*it).pid == pid) { - (*it).run(); + (*it).callback(); it = callbacks.erase_after(prev); } else { prev = it; @@ -1191,29 +1200,38 @@ bool _mqttMaybeSkipRetained(char* topic) { // TODO: Current callback model does not allow to pass message length. Instead, implement a topic filter and record all subscriptions. That way we don't need to filter out events and could implement per-event callbacks. void _mqttOnMessageAsync(char* topic, char* payload, AsyncMqttClientMessageProperties, size_t len, size_t index, size_t total) { - if (!len || (len > MQTT_BUFFER_MAX_SIZE) || (total > MQTT_BUFFER_MAX_SIZE)) return; - if (_mqttMaybeSkipRetained(topic)) return; + static constexpr size_t BufferSize { MQTT_BUFFER_MAX_SIZE }; + static_assert(BufferSize > 0, ""); - static char message[((MQTT_BUFFER_MAX_SIZE + 1) + 31) & -32] = {0}; - memmove(message + index, (char *) payload, len); + if (!len || (len > BufferSize) || (total > BufferSize)) { + return; + } + + if (_mqttMaybeSkipRetained(topic)) { + return; + } + + alignas(4) static char buffer[((BufferSize + 3) & ~3) + 4] = {0}; + std::copy(payload, payload + len, buffer); // Not done yet if (total != (len + index)) { DEBUG_MSG_P(PSTR("[MQTT] Buffered %s => %u / %u bytes\n"), topic, len, total); return; } - message[len + index] = '\0'; + + buffer[len + index] = '\0'; if (len < mqtt::build::MessageLogMax) { - DEBUG_MSG_P(PSTR("[MQTT] Received %s => %s\n"), topic, message); + DEBUG_MSG_P(PSTR("[MQTT] Received %s => %s\n"), topic, buffer); } else { DEBUG_MSG_P(PSTR("[MQTT] Received %s => (%u bytes)\n"), topic, len); } - // Call subscribers with the message buffer - for (auto& callback : _mqtt_callbacks) { - callback(MQTT_MESSAGE_EVENT, topic, message); + auto topic_view = espurna::StringView{ topic }; + auto message_view = espurna::StringView{ &buffer[0], &buffer[total] }; + for (const auto callback : _mqtt_callbacks) { + callback(MQTT_MESSAGE_EVENT, topic_view, message_view); } - } #else @@ -1247,89 +1265,91 @@ void _mqttOnMessage(char* topic, char* payload, unsigned int len) { // Public API // ----------------------------------------------------------------------------- -/** - Returns the magnitude part of a topic - - @param topic the full MQTT topic - @return String object with the magnitude part. -*/ -String mqttMagnitude(const char* topic) { - String output; - - String pattern = _mqtt_settings.topic + _mqtt_settings.setter; - int position = pattern.indexOf("#"); - - if (position >= 0) { - String start = pattern.substring(0, position); - String end = pattern.substring(position + 1); +// Return {magnitude} (aka #) part of the topic string +// e.g. +// * /#/set - generic topic placement +// ^ +// * /#//set - when {magnitude} is used +// ^ +// * #//set - when magnitude is at the start +// ^ +// * #/set - when *only* {magnitude} is used (or, empty topic string) +// ^ +// Depends on the topic and setter settings values. +// Note that function is ignoring the fact that these strings may not contain the +// root topic b/c MQTT handles that instead of us (and it's good idea to trust it). +espurna::StringView mqttMagnitude(espurna::StringView topic) { + using espurna::StringView; + StringView out; + + const auto pattern = _mqtt_settings.topic + _mqtt_settings.setter; + auto it = std::find(pattern.begin(), pattern.end(), '#'); + if (it == pattern.end()) { + return out; + } - String magnitude(topic); - if (magnitude.startsWith(start) && magnitude.endsWith(end)) { - output = std::move(magnitude); - output.replace(start, ""); - output.replace(end, ""); - } + const auto start = StringView(pattern.begin(), it); + if (start.length()) { + topic = StringView(topic.begin() + start.length(), topic.end()); } - return output; -} + const auto end = StringView(it + 1, pattern.end()); + if (end.length()) { + topic = StringView(topic.begin(), topic.end() - end.length()); + } -// Retrieve lefthand side of the extracted magnitude value -espurna::StringView mqttMagnitudeTail(espurna::StringView magnitude, espurna::StringView topic) { - return espurna::StringView(magnitude.begin() + topic.length(), magnitude.end()); + out = StringView(topic.begin(), topic.end()); + return out; } -/** - Returns a full MQTT topic from the magnitude - - @param magnitude the magnitude part of the topic. - @param is_set whether to build a command topic (true) - or a state topic (false). - @return String full MQTT topic. -*/ -String mqttTopic(const String& magnitude, bool is_set) { - String output; - output.reserve(magnitude.length() +// Creates a proper MQTT topic for on the given 'magnitude' +static String _mqttTopicWith(String magnitude) { + String out; + out.reserve(magnitude.length() + _mqtt_settings.topic.length() + _mqtt_settings.setter.length() + _mqtt_settings.getter.length()); - output += _mqtt_settings.topic; - output.replace("#", magnitude); - output += is_set ? _mqtt_settings.setter : _mqtt_settings.getter; + out += _mqtt_settings.topic; + out.replace("#", magnitude); - return output; + return out; } -String mqttTopic(const char* magnitude, bool is_set) { - return mqttTopic(String(magnitude), is_set); +// When magnitude is a status topic aka getter +static String _mqttTopicGetter(String magnitude) { + return _mqttTopicWith(magnitude) + _mqtt_settings.getter; } -/** - Returns a full MQTT topic from the magnitude +// When magnitude is an input topic aka setter +String _mqttTopicSetter(String magnitude) { + return _mqttTopicWith(magnitude) + _mqtt_settings.setter; +} - @param magnitude the magnitude part of the topic. - @param index index of the magnitude when more than one such magnitudes. - @param is_set whether to build a command topic (true) - or a state topic (false). - @return String full MQTT topic. -*/ -String mqttTopic(const String& magnitude, unsigned int index, bool is_set) { - String output; - output.reserve(magnitude.length() + (sizeof(decltype(index)) * 4)); - output += magnitude; - output += '/'; - output += index; - return mqttTopic(output, is_set); +// When magnitude is indexed, append its index to the topic +static String _mqttTopicIndexed(String topic, size_t index) { + return topic + '/' + String(index, 10); } -String mqttTopic(const char* magnitude, unsigned int index, bool is_set) { - return mqttTopic(String(magnitude), index, is_set); +String mqttTopic(const String& magnitude) { + return _mqttTopicGetter(magnitude); +} + +String mqttTopic(const String& magnitude, size_t index) { + return _mqttTopicGetter(_mqttTopicIndexed(magnitude, index)); +} + +String mqttTopicSetter(const String& magnitude) { + return _mqttTopicSetter(magnitude); +} + +String mqttTopicSetter(const String& magnitude, size_t index) { + return _mqttTopicSetter(_mqttTopicIndexed(magnitude, index)); } // ----------------------------------------------------------------------------- -uint16_t mqttSendRaw(const char * topic, const char * message, bool retain, int qos) { +uint16_t mqttSendRaw(const char* topic, const char* message, bool retain, int qos) { if (_mqtt.connected()) { const unsigned int packetId { #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT @@ -1362,34 +1382,33 @@ uint16_t mqttSendRaw(const char * topic, const char * message, bool retain, int return false; } -uint16_t mqttSendRaw(const char * topic, const char * message, bool retain) { +uint16_t mqttSendRaw(const char* topic, const char* message, bool retain) { return mqttSendRaw(topic, message, retain, _mqtt_settings.qos); } -uint16_t mqttSendRaw(const char * topic, const char * message) { +uint16_t mqttSendRaw(const char* topic, const char* message) { return mqttSendRaw(topic, message, _mqtt_settings.retain); } -bool mqttSend(const char * topic, const char * message, bool force, bool retain) { +bool mqttSend(const char* topic, const char* message, bool force, bool retain) { if (!force && _mqtt_use_json) { mqttEnqueue(topic, message); - _mqtt_json_payload_flush.once( - espurna::duration::Milliseconds(MQTT_USE_JSON_DELAY), mqttFlush); + _mqtt_json_payload_flush.once(mqtt::build::JsonDelay, mqttFlush); return true; } - return mqttSendRaw(mqttTopic(topic, false).c_str(), message, retain) > 0; + return mqttSendRaw(mqttTopic(topic).c_str(), message, retain) > 0; } -bool mqttSend(const char * topic, const char * message, bool force) { +bool mqttSend(const char* topic, const char* message, bool force) { return mqttSend(topic, message, force, _mqtt_settings.retain); } -bool mqttSend(const char * topic, const char * message) { +bool mqttSend(const char* topic, const char* message) { return mqttSend(topic, message, false); } -bool mqttSend(const char * topic, unsigned int index, const char * message, bool force, bool retain) { +bool mqttSend(const char* topic, unsigned int index, const char* message, bool force, bool retain) { const size_t TopicLen { strlen(topic) }; String out; out.reserve(TopicLen + 5); @@ -1401,11 +1420,11 @@ bool mqttSend(const char * topic, unsigned int index, const char * message, bool return mqttSend(out.c_str(), message, force, retain); } -bool mqttSend(const char * topic, unsigned int index, const char * message, bool force) { +bool mqttSend(const char* topic, unsigned int index, const char* message, bool force) { return mqttSend(topic, index, message, force, _mqtt_settings.retain); } -bool mqttSend(const char * topic, unsigned int index, const char * message) { +bool mqttSend(const char* topic, unsigned int index, const char* message) { return mqttSend(topic, index, message, false); } @@ -1467,7 +1486,7 @@ void mqttFlush() { mqttSendRaw(_mqtt_settings.topic_json.c_str(), output.c_str(), false); } -void mqttEnqueue(const char* topic, const char* message) { +void mqttEnqueue(espurna::StringView topic, espurna::StringView payload) { // Queue is not meant to send message "offline" // We must prevent the queue does not get full while offline if (_mqtt.connected()) { @@ -1475,11 +1494,13 @@ void mqttEnqueue(const char* topic, const char* message) { mqttFlush(); } - _mqtt_json_payload.remove_if([topic](const MqttPayload& payload) { - return payload.topic() == topic; - }); + _mqtt_json_payload.remove_if( + [topic](const MqttPayload& payload) { + return topic == payload.topic(); + }); - _mqtt_json_payload.emplace_front(topic, message); + _mqtt_json_payload.emplace_front( + topic.toString(), payload.toString()); ++_mqtt_json_payload_count; } } @@ -1502,11 +1523,11 @@ uint16_t mqttSubscribeRaw(const char* topic) { return mqttSubscribeRaw(topic, _mqtt_settings.qos); } -bool mqttSubscribe(const char * topic) { - return mqttSubscribeRaw(mqttTopic(topic, true).c_str(), _mqtt_settings.qos); +bool mqttSubscribe(const char* topic) { + return mqttSubscribeRaw(mqttTopicSetter(topic).c_str(), _mqtt_settings.qos); } -uint16_t mqttUnsubscribeRaw(const char * topic) { +uint16_t mqttUnsubscribeRaw(const char* topic) { uint16_t pid { 0u }; if (_mqtt.connected() && (strlen(topic) > 0)) { pid = _mqtt.unsubscribe(topic); @@ -1516,8 +1537,8 @@ uint16_t mqttUnsubscribeRaw(const char * topic) { return pid; } -bool mqttUnsubscribe(const char * topic) { - return mqttUnsubscribeRaw(mqttTopic(topic, true).c_str()); +bool mqttUnsubscribe(const char* topic) { + return mqttUnsubscribeRaw(mqttTopicSetter(topic).c_str()); } // ----------------------------------------------------------------------------- @@ -1550,7 +1571,7 @@ bool mqttForward() { @param standalone function pointer */ -void mqttRegister(mqtt_callback_f callback) { +void mqttRegister(MqttCallback callback) { _mqtt_callbacks.push_front(callback); } @@ -1561,9 +1582,12 @@ void mqttRegister(mqtt_callback_f callback) { @param callable object */ -void mqttOnPublish(uint16_t pid, mqtt_pid_callback_f callback) { - auto callable = MqttPidCallback { pid, callback }; - _mqtt_publish_callbacks.push_front(std::move(callable)); +void mqttOnPublish(uint16_t pid, MqttPidCallback callback) { + _mqtt_publish_callbacks.push_front( + MqttPidCallbackHandler{ + .pid = pid, + .callback = std::move(callback), + }); } /** @@ -1571,9 +1595,12 @@ void mqttOnPublish(uint16_t pid, mqtt_pid_callback_f callback) { @param callable object */ -void mqttOnSubscribe(uint16_t pid, mqtt_pid_callback_f callback) { - auto callable = MqttPidCallback { pid, callback }; - _mqtt_subscribe_callbacks.push_front(std::move(callable)); +void mqttOnSubscribe(uint16_t pid, MqttPidCallback callback) { + _mqtt_subscribe_callbacks.push_front( + MqttPidCallbackHandler{ + .pid = pid, + .callback = std::move(callback), + }); } #endif @@ -1772,7 +1799,7 @@ void mqttSetup() { .onConnected(_mqttWebSocketOnConnected) .onKeyCheck(_mqttWebSocketOnKeyCheck); - mqttRegister([](unsigned int type, const char*, char*) { + mqttRegister([](unsigned int type, espurna::StringView, espurna::StringView) { if ((type == MQTT_CONNECT_EVENT) || (type == MQTT_DISCONNECT_EVENT)) { wsPost(_mqttWebSocketOnData); } diff --git a/code/espurna/mqtt.h b/code/espurna/mqtt.h index 06fc5f0f..ab9efa7c 100644 --- a/code/espurna/mqtt.h +++ b/code/espurna/mqtt.h @@ -54,23 +54,24 @@ Updated secure client support by Niek van der Maas < mail at niekvandermaas dot #define MQTT_TOPIC_CMD "cmd" #define MQTT_TOPIC_SCHEDULE "schedule" -using mqtt_callback_f = std::function; -using mqtt_pid_callback_f = std::function; - void mqttHeartbeat(espurna::heartbeat::Callback); -void mqttRegister(mqtt_callback_f callback); -void mqttOnPublish(uint16_t pid, mqtt_pid_callback_f); -void mqttOnSubscribe(uint16_t pid, mqtt_pid_callback_f); +// stateless callback; generally, registered once per module when calling setup() +using MqttCallback = void(*)(unsigned int type, espurna::StringView topic, espurna::StringView payload); +void mqttRegister(MqttCallback); + +// stateful callback for ACK'ed messages; should be used when waiting for certain messsage to be PUBlished +using MqttPidCallback = std::function; +void mqttOnPublish(uint16_t pid, MqttPidCallback); +void mqttOnSubscribe(uint16_t pid, MqttPidCallback); -String mqttTopic(const String& magnitude, bool is_set); -String mqttTopic(const char* magnitude, bool is_set); +String mqttTopic(const String& magnitude); +String mqttTopic(const String& magnitude, size_t index); -String mqttTopic(const String& magnitude, unsigned int index, bool is_set); -String mqttTopic(const char* magnitude, unsigned int index, bool is_set); +String mqttTopicSetter(const String& magnitude); +String mqttTopicSetter(const String& magnitude, size_t index); -String mqttMagnitude(const char* topic); -espurna::StringView mqttMagnitudeTail(espurna::StringView magnitude, espurna::StringView topic); +espurna::StringView mqttMagnitude(espurna::StringView topic); uint16_t mqttSendRaw(const char * topic, const char * message, bool retain, int qos); uint16_t mqttSendRaw(const char * topic, const char * message, bool retain); @@ -94,7 +95,7 @@ bool mqttSend(const char * topic, unsigned int index, const char * message); void mqttSendStatus(); void mqttFlush(); -void mqttEnqueue(const char* topic, const char* message); +void mqttEnqueue(espurna::StringView topic, espurna::StringView payload); const String& mqttPayloadOnline(); const String& mqttPayloadOffline(); diff --git a/code/espurna/ota_asynctcp.cpp b/code/espurna/ota_asynctcp.cpp index 3276a067..9f594ae6 100644 --- a/code/espurna/ota_asynctcp.cpp +++ b/code/espurna/ota_asynctcp.cpp @@ -31,6 +31,7 @@ Copyright (C) 2016-2019 by Xose Pérez #include +namespace espurna { namespace ota { namespace asynctcp { namespace { @@ -217,9 +218,9 @@ bool BasicHttpClient::connect() { // ----------------------------------------------------------------------------- -void clientFromUrl(URL&& url) { +void clientFromUrl(URL url) { if (!url.protocol.equals("http") && !url.protocol.equals("https")) { - DEBUG_MSG_P(PSTR("[OTA] Incorrect URL specified\n")); + DEBUG_MSG_P(PSTR("[OTA] Unsupported protocol\n")); return; } @@ -229,14 +230,16 @@ void clientFromUrl(URL&& url) { return; } + DEBUG_MSG_P(PSTR("[OTA] Connecting to %s:%hu\n"), url.host.c_str(), url.port); + internal::client = std::make_unique(std::move(url)); if (!internal::client->connect()) { DEBUG_MSG_P(PSTR("[OTA] Connection failed\n")); } } -void clientFromUrl(const String& string) { - clientFromUrl(URL(string)); +void clientFromUrl(StringView payload) { + clientFromUrl(URL(payload)); } #if TERMINAL_SUPPORT @@ -257,22 +260,21 @@ static constexpr ::terminal::Command OtaCommands[] PROGMEM { }; void terminalSetup() { - espurna::terminal::add(OtaCommands); + terminal::add(OtaCommands); } #endif // TERMINAL_SUPPORT #if OTA_MQTT_SUPPORT -void mqttCallback(unsigned int type, const char* topic, char* payload) { +void mqttCallback(unsigned int type, StringView topic, StringView payload) { if (type == MQTT_CONNECT_EVENT) { mqttSubscribe(MQTT_TOPIC_OTA); return; } if (type == MQTT_MESSAGE_EVENT) { - String t = mqttMagnitude(topic); + auto t = mqttMagnitude(topic); if (t.equals(MQTT_TOPIC_OTA)) { - DEBUG_MSG_P(PSTR("[OTA] Initiating from URL: %s\n"), payload); clientFromUrl(payload); } return; @@ -284,6 +286,7 @@ void mqttCallback(unsigned int type, const char* topic, char* payload) { } // namespace } // namespace asynctcp } // namespace ota +} // namespace espurna #endif @@ -293,11 +296,11 @@ void otaClientSetup() { moveSetting("otafp", "otaFP"); #if TERMINAL_SUPPORT - ota::asynctcp::terminalSetup(); + espurna::ota::asynctcp::terminalSetup(); #endif #if (MQTT_SUPPORT && OTA_MQTT_SUPPORT) - mqttRegister(ota::asynctcp::mqttCallback); + mqttRegister(espurna::ota::asynctcp::mqttCallback); #endif } diff --git a/code/espurna/ota_httpupdate.cpp b/code/espurna/ota_httpupdate.cpp index 6af3fba1..4e551b1e 100644 --- a/code/espurna/ota_httpupdate.cpp +++ b/code/espurna/ota_httpupdate.cpp @@ -22,7 +22,6 @@ Copyright (C) 2019 by Maxim Prokhorov #include #include -#include "libs/URL.h" #include "libs/TypeChecks.h" #include "libs/SecureClientHelpers.h" @@ -44,6 +43,7 @@ namespace { // ----------------------------------------------------------------------------- +namespace espurna { namespace ota { namespace httpupdate { namespace { @@ -144,7 +144,7 @@ void clientFromUrl(const String& url) { } #endif - DEBUG_MSG_P(PSTR("[OTA] Incorrect URL specified\n")); + DEBUG_MSG_P(PSTR("[OTA] Unsupported protocol\n")); } void clientFromInternalUrl() { @@ -153,8 +153,8 @@ void clientFromInternalUrl() { } [[gnu::unused]] -void clientQueueUrl(String url) { - internal::url = std::move(url); +void clientQueueUrl(espurna::StringView url) { + internal::url = url.toString(); espurnaRegisterOnceUnique(clientFromInternalUrl); } @@ -182,16 +182,15 @@ void terminalSetup() { #if (MQTT_SUPPORT && OTA_MQTT_SUPPORT) -void mqttCallback(unsigned int type, const char* topic, char* payload) { +void mqttCallback(unsigned int type, StringView topic, StringView payload) { if (type == MQTT_CONNECT_EVENT) { mqttSubscribe(MQTT_TOPIC_OTA); return; } if (type == MQTT_MESSAGE_EVENT) { - const String t = mqttMagnitude(topic); + const auto t = mqttMagnitude(topic); if (!internal::url.length() && t.equals(MQTT_TOPIC_OTA)) { - DEBUG_MSG_P(PSTR("[OTA] Queuing from URL: %s\n"), payload); clientQueueUrl(payload); } @@ -204,6 +203,7 @@ void mqttCallback(unsigned int type, const char* topic, char* payload) { } // namespace } // namespace httpupdate } // namespace ota +} // namespace espurna // ----------------------------------------------------------------------------- @@ -211,11 +211,11 @@ void otaClientSetup() { moveSetting("otafp", "otaFP"); #if TERMINAL_SUPPORT - ota::httpupdate::terminalSetup(); + espurna::ota::httpupdate::terminalSetup(); #endif #if (MQTT_SUPPORT && OTA_MQTT_SUPPORT) - mqttRegister(ota::httpupdate::mqttCallback); + mqttRegister(espurna::ota::httpupdate::mqttCallback); #endif } diff --git a/code/espurna/relay.cpp b/code/espurna/relay.cpp index 5eb82684..5da3171f 100644 --- a/code/espurna/relay.cpp +++ b/code/espurna/relay.cpp @@ -221,29 +221,29 @@ alignas(4) static constexpr char PayloadOn[] PROGMEM = RELAY_MQTT_ON; alignas(4) static constexpr char PayloadOff[] PROGMEM = RELAY_MQTT_OFF; alignas(4) static constexpr char PayloadToggle[] PROGMEM = RELAY_MQTT_TOGGLE; -constexpr espurna::StringView mqttTopicSub(size_t index) { +const StringView mqttTopicSub(size_t index) { return ( - (index == 0) ? STRING_VIEW(RELAY1_MQTT_TOPIC_SUB) : - (index == 1) ? STRING_VIEW(RELAY2_MQTT_TOPIC_SUB) : - (index == 2) ? STRING_VIEW(RELAY3_MQTT_TOPIC_SUB) : - (index == 3) ? STRING_VIEW(RELAY4_MQTT_TOPIC_SUB) : - (index == 4) ? STRING_VIEW(RELAY5_MQTT_TOPIC_SUB) : - (index == 5) ? STRING_VIEW(RELAY6_MQTT_TOPIC_SUB) : - (index == 6) ? STRING_VIEW(RELAY7_MQTT_TOPIC_SUB) : - (index == 7) ? STRING_VIEW(RELAY8_MQTT_TOPIC_SUB) : "" + (index == 0) ? StringView(PSTR(RELAY1_MQTT_TOPIC_SUB)) : + (index == 1) ? StringView(PSTR(RELAY2_MQTT_TOPIC_SUB)) : + (index == 2) ? StringView(PSTR(RELAY3_MQTT_TOPIC_SUB)) : + (index == 3) ? StringView(PSTR(RELAY4_MQTT_TOPIC_SUB)) : + (index == 4) ? StringView(PSTR(RELAY5_MQTT_TOPIC_SUB)) : + (index == 5) ? StringView(PSTR(RELAY6_MQTT_TOPIC_SUB)) : + (index == 6) ? StringView(PSTR(RELAY7_MQTT_TOPIC_SUB)) : + (index == 7) ? StringView(PSTR(RELAY8_MQTT_TOPIC_SUB)) : "" ); } -constexpr espurna::StringView mqttTopicPub(size_t index) { +const StringView mqttTopicPub(size_t index) { return ( - (index == 0) ? STRING_VIEW(RELAY1_MQTT_TOPIC_PUB) : - (index == 1) ? STRING_VIEW(RELAY2_MQTT_TOPIC_PUB) : - (index == 2) ? STRING_VIEW(RELAY3_MQTT_TOPIC_PUB) : - (index == 3) ? STRING_VIEW(RELAY4_MQTT_TOPIC_PUB) : - (index == 4) ? STRING_VIEW(RELAY5_MQTT_TOPIC_PUB) : - (index == 5) ? STRING_VIEW(RELAY6_MQTT_TOPIC_PUB) : - (index == 6) ? STRING_VIEW(RELAY7_MQTT_TOPIC_PUB) : - (index == 7) ? STRING_VIEW(RELAY8_MQTT_TOPIC_PUB) : "" + (index == 0) ? StringView(PSTR(RELAY1_MQTT_TOPIC_PUB)) : + (index == 1) ? StringView(PSTR(RELAY2_MQTT_TOPIC_PUB)) : + (index == 2) ? StringView(PSTR(RELAY3_MQTT_TOPIC_PUB)) : + (index == 3) ? StringView(PSTR(RELAY4_MQTT_TOPIC_PUB)) : + (index == 4) ? StringView(PSTR(RELAY5_MQTT_TOPIC_PUB)) : + (index == 5) ? StringView(PSTR(RELAY6_MQTT_TOPIC_PUB)) : + (index == 6) ? StringView(PSTR(RELAY7_MQTT_TOPIC_PUB)) : + (index == 7) ? StringView(PSTR(RELAY8_MQTT_TOPIC_PUB)) : "" ); } @@ -1459,26 +1459,12 @@ Stream* StmProvider::_port = nullptr; // ----------------------------------------------------------------------------- bool _relayTryParseId(espurna::StringView value, size_t& id) { - return tryParseId(value, relayCount, id); + return tryParseId(value, relayCount(), id); } [[gnu::unused]] -bool _relayTryParseIdFromPath(espurna::StringView endpoint, size_t& id) { - const auto begin = std::make_reverse_iterator(endpoint.end()); - const auto end = std::make_reverse_iterator(endpoint.begin()); - - auto next_slash = std::find(begin, end, '/'); - if (next_slash == end) { - return false; - } - - espurna::StringView tail { next_slash.base() + 1, endpoint.end() }; - if ((*tail.begin()) == '\0') { - DEBUG_MSG_P(PSTR("[RELAY] relayID was not specified\n")); - return false; - } - - return _relayTryParseId(tail, id); +bool _relayTryParseIdFromPath(espurna::StringView value, size_t& id) { + return tryParseIdPath(value, relayCount(), id); } void _relayHandleStatus(size_t id, PayloadStatus status) { @@ -2462,7 +2448,7 @@ bool _relayMqttHeartbeat(espurna::heartbeat::Mask mask) { return mqttConnected(); } -void _relayMqttHandleCustomTopic(const String& topic, espurna::StringView payload) { +void _relayMqttHandleCustomTopic(espurna::StringView topic, espurna::StringView payload) { PathParts received(topic); for (auto& topic : _relay_custom_topics) { if (topic.match(received)) { @@ -2487,10 +2473,8 @@ void _relayMqttHandleDisconnect() { } // namespace -void relayMQTTCallback(unsigned int type, const char* topic, char* payload) { - +void relayMQTTCallback(unsigned int type, espurna::StringView topic, espurna::StringView payload) { static bool connected { false }; - if (!_relays.size()) { return; } @@ -2504,7 +2488,7 @@ void relayMQTTCallback(unsigned int type, const char* topic, char* payload) { } if (type == MQTT_MESSAGE_EVENT) { - String t = mqttMagnitude(topic); + const auto t = mqttMagnitude(topic); auto is_relay = t.startsWith(MQTT_TOPIC_RELAY); auto is_pulse = t.startsWith(MQTT_TOPIC_PULSE); @@ -2592,8 +2576,10 @@ static void _relayCommand(::terminal::CommandContext&& ctx) { return; } + ctx.output.println(id); + if (ctx.argv.size() > 2) { - auto status = relayParsePayload(ctx.argv[2].c_str()); + auto status = relayParsePayload(ctx.argv[2]); if (PayloadStatus::Unknown == status) { terminalError(ctx, F("Invalid status")); return; diff --git a/code/espurna/relay_pulse.ipp b/code/espurna/relay_pulse.ipp index 3e36e7f6..6f02830f 100644 --- a/code/espurna/relay_pulse.ipp +++ b/code/espurna/relay_pulse.ipp @@ -149,21 +149,19 @@ update_decimal: ++ptr; if (type != Type::Unknown) { - char* endp { nullptr }; - uint32_t value = strtoul(token.c_str(), &endp, 10); - - if (endp && (endp != token.c_str()) && endp[0] == '\0') { + const auto result = parseUnsigned(token, 10); + if (result.ok) { switch (type) { case Type::Hours: { - out += ::espurna::duration::Hours { value }; + out += ::espurna::duration::Hours { result.value }; break; } case Type::Minutes: { - out += ::espurna::duration::Minutes { value }; + out += ::espurna::duration::Minutes { result.value }; break; } case Type::Seconds: { - out += ::espurna::duration::Seconds { value }; + out += ::espurna::duration::Seconds { result.value }; break; } case Type::Unknown: diff --git a/code/espurna/rfbridge.cpp b/code/espurna/rfbridge.cpp index 8649bc9d..c378d4de 100644 --- a/code/espurna/rfbridge.cpp +++ b/code/espurna/rfbridge.cpp @@ -351,24 +351,24 @@ String on(size_t id) { return getSetting({FPSTR(keys::On), id}); } -void store(const __FlashStringHelper* prefix, size_t id, const String& value) { +void store(espurna::StringView prefix, size_t id, const String& value) { const espurna::settings::Key key(prefix, id); setSetting(key, value); DEBUG_MSG_P(PSTR("[RF] Saved %s => \"%s\"\n"), key.c_str(), value.c_str()); } void off(size_t id, const String& value) { - store(FPSTR(keys::Off), id, value); + store(keys::Off, id, value); } void on(size_t id, const String& value) { - store(FPSTR(keys::On), id, value); + store(keys::On, id, value); } } // namespace settings } // namespace rfbridge -void _rfbStore(size_t id, bool status, const String& code) { +void _rfbStore(size_t id, bool status, String code) { if (status) { rfbridge::settings::on(id, code); } else { @@ -494,20 +494,17 @@ bool _rfbCompare(const char* lhs, const char* rhs, size_t length) { // **always** expect full length code as input to simplify comparison // previous implementation tried to help MQTT / API requests to match based on the saved code, // thus requiring us to 'return' value from settings as the real code, replacing input -RfbRelayMatch _rfbMatch(const char* code) { +RfbRelayMatch _rfbMatch(espurna::StringView code) { RfbRelayMatch matched; - if (!relayCount()) { return matched; } - const espurna::StringView codeView(code); - // we gather all available options, as the kv store might be defined in any order // scan kvs only once, since we want both ON and OFF options and don't want to depend on the relayCount() espurna::settings::foreach_prefix( - [codeView, &matched](espurna::StringView prefix, String key, const espurna::settings::kvs_type::ReadResult& value) { - if (codeView.length() != value.length()) { + [code, &matched](espurna::StringView prefix, String key, const espurna::settings::kvs_type::ReadResult& value) { + if (code.length() != value.length()) { return; } @@ -520,7 +517,7 @@ RfbRelayMatch _rfbMatch(const char* code) { return; } - if (!_rfbCompare(codeView.c_str(), value.read().c_str(), codeView.length())) { + if (!_rfbCompare(code.begin(), value.read().begin(), code.length())) { return; } @@ -550,11 +547,13 @@ RfbRelayMatch _rfbMatch(const char* code) { return matched; } -void _rfbLearnFromString(std::unique_ptr& learn, const char* buffer) { - if (!learn) return; +void _rfbLearnFromString(std::unique_ptr& learn, espurna::StringView buffer) { + if (!learn) { + return; + } DEBUG_MSG_P(PSTR("[RF] Learned relay ID %u after %u ms\n"), learn->id, millis() - learn->ts); - _rfbStore(learn->id, learn->status, buffer); + _rfbStore(learn->id, learn->status, buffer.toString()); // Websocket update needs to happen right here, since the only time // we send these in bulk is at the very start of the connection @@ -568,10 +567,10 @@ void _rfbLearnFromString(std::unique_ptr& learn, const char* buffer) { learn.reset(nullptr); } -bool _rfbRelayHandler(const char* buffer, bool locked = false) { +bool _rfbRelayHandler(espurna::StringView payload, bool locked = false) { bool result { false }; - auto match = _rfbMatch(buffer); + const auto match = _rfbMatch(payload); if (match) { DEBUG_MSG_P(PSTR("[RF] Matched with the relay ID %u\n"), match.id()); _rfb_relay_status_lock.set(match.id(), locked); @@ -593,20 +592,24 @@ bool _rfbRelayHandler(const char* buffer, bool locked = false) { return result; } -void _rfbLearnStartFromPayload(const char* payload) { +void _rfbLearnStartFromPayload(espurna::StringView payload) { // The payload must be the `relayID,mode` (where mode is either 0 or 1) - const char* sep = strchr(payload, ','); - if (nullptr == sep) { + auto it = std::find(payload.begin(), payload.end(), ','); + if (it == payload.end()) { return; } // ref. RelaysMax, we only have up to 2 digits + if ((it + 1) == payload.end()) { + return; + } + char relay[3] {0, 0, 0}; - if ((sep - payload) > 2) { + if (std::distance(payload.begin(), it) > 2) { return; } - std::copy(payload, sep, relay); + std::copy(payload.begin(), it, relay); size_t id; if (!tryParseId(relay, relayCount, id)) { @@ -614,13 +617,13 @@ void _rfbLearnStartFromPayload(const char* payload) { return; } - ++sep; - if ((*sep == '0') || (*sep == '1')) { - rfbLearn(id, (*sep != '0')); + ++it; + if ((*it == '0') || (*it == '1')) { + rfbLearn(id, (*it != '0')); } } -void _rfbLearnFromReceived(std::unique_ptr& learn, const char* buffer) { +void _rfbLearnFromReceived(std::unique_ptr& learn, espurna::StringView buffer) { if (millis() - learn->ts > RFB_LEARN_TIMEOUT) { DEBUG_MSG_P(PSTR("[RF] Learn timeout after %u ms\n"), millis() - learn->ts); learn.reset(nullptr); @@ -643,9 +646,9 @@ void _rfbEnqueue(uint8_t (&code)[RfbParser::PayloadSizeBasic], unsigned char rep _rfb_message_queue.push_back(RfbMessage(code, repeats)); } -bool _rfbEnqueue(const char* code, size_t length, unsigned char repeats = 1u) { +bool _rfbEnqueue(espurna::StringView code, unsigned char repeats = 1u) { uint8_t buffer[RfbParser::PayloadSizeBasic] { 0u }; - if (hexDecode(code, length, buffer, sizeof(buffer))) { + if (hexDecode(code.begin(), code.length(), buffer, sizeof(buffer))) { _rfbEnqueue(buffer, repeats); return true; } @@ -775,18 +778,29 @@ void _rfbReceiveImpl() { } // note that we don't care about queue here, just dump raw message as-is -void _rfbSendRawFromPayload(const char * raw) { - auto rawlen = strlen(raw); - if (rawlen > (RfbParser::MessageSizeMax * 2)) return; - if ((rawlen < 6) || (rawlen & 1)) return; +void _rfbSendRawFromPayload(espurna::StringView raw) { + if (raw.length() > (RfbParser::MessageSizeMax * 2)) { + return; + } + + if ((raw.length() < 6) || (raw.length() & 1)) { + return; + } - DEBUG_MSG_P(PSTR("[RF] Sending RAW MESSAGE \"%s\"\n"), raw); + DEBUG_MSG_P(PSTR("[RF] Sending RAW MESSAGE \"%.*s\"\n"), + raw.length(), raw.begin()); size_t bytes = 0; uint8_t message[RfbParser::MessageSizeMax] { 0u }; - if ((bytes = hexDecode(raw, rawlen, message, sizeof(message)))) { - if (message[0] != CodeStart) return; - if (message[bytes - 1] != CodeEnd) return; + if ((bytes = hexDecode(raw.begin(), raw.length(), message, sizeof(message)))) { + if (message[0] != CodeStart) { + return; + } + + if (message[bytes - 1] != CodeEnd) { + return; + } + _rfbSendRaw(message, bytes); } } @@ -836,9 +850,9 @@ void _rfbEnqueue(uint8_t protocol, uint16_t timing, uint8_t bits, RfbMessage::co _rfb_message_queue.push_back(RfbMessage{protocol, timing, bits, code, repeats}); } -void _rfbEnqueue(const char* message, size_t length, unsigned char repeats = 1u) { +void _rfbEnqueue(espurna::StringView message, unsigned char repeats = 1u) { uint8_t buffer[RfbMessage::BufferSize] { 0u }; - if (hexDecode(message, length, buffer, sizeof(buffer))) { + if (hexDecode(message.begin(), message.length(), buffer, sizeof(buffer))) { const auto bytes = _rfb_bytes_for_bits(buffer[4]); uint8_t raw_code[sizeof(RfbMessage::code_type)] { 0u }; @@ -997,52 +1011,49 @@ void _rfbSendQueued() { } // Check if the payload looks like a HEX code (plus comma, specifying the 'repeats' arg for the queue) -void _rfbSendFromPayload(const char * payload) { - size_t len { strlen(payload) }; - if (!len) { +void _rfbSendFromPayload(espurna::StringView payload) { + if (!payload.length()) { return; } decltype(_rfb_repeats) repeats { _rfb_repeats }; - const char* sep { strchr(payload, ',') }; - if (sep) { - len -= strlen(sep); - - sep += 1; - if ('\0' == *sep) return; - if ('-' == *sep) return; + auto it = std::find(payload.begin(), payload.end(), ','); + if (it != payload.end()) { + it += 1; + if ((it == payload.end()) || (*it == '\0') || (*it == '-')) { + return; + } - char *endptr = nullptr; - repeats = strtoul(sep, &endptr, 10); - if (endptr == payload || endptr[0] != '\0') { + const auto result = parseUnsigned( + espurna::StringView(it, payload.end()), 10); + if (!result.ok) { return; } + + repeats = result.value; } - if (!len || (len & 1)) { + payload = espurna::StringView(it, payload.end()); + if (!payload.length()) { return; } - DEBUG_MSG_P(PSTR("[RF] Enqueuing MESSAGE '%s' %u time(s)\n"), payload, repeats); + DEBUG_MSG_P(PSTR("[RF] Enqueuing MESSAGE '%.*s' %u time(s)\n"), + payload.length(), payload.begin(), repeats); // We postpone the actual sending until the loop, as we may've been called from MQTT or HTTP API // RFB_PROVIDER implementation should select the appropriate de-serialization function - _rfbEnqueue(payload, len, repeats); + _rfbEnqueue(payload, repeats); } -void rfbSend(const char* code) { +void rfbSend(espurna::StringView code) { _rfbSendFromPayload(code); } -void rfbSend(const String& code) { - _rfbSendFromPayload(code.c_str()); -} - #if MQTT_SUPPORT -void _rfbMqttCallback(unsigned int type, const char* topic, char* payload) { - +void _rfbMqttCallback(unsigned int type, espurna::StringView topic, espurna::StringView payload) { if (type == MQTT_CONNECT_EVENT) { #if RELAY_SUPPORT @@ -1060,8 +1071,7 @@ void _rfbMqttCallback(unsigned int type, const char* topic, char* payload) { } if (type == MQTT_MESSAGE_EVENT) { - - String t = mqttMagnitude(topic); + auto t = mqttMagnitude(topic); #if RELAY_SUPPORT if (t.equals(MQTT_TOPIC_RFLEARN)) { @@ -1138,7 +1148,7 @@ void _rfbApiSetup() { apiRegister(F(MQTT_TOPIC_RFRAW), apiOk, // just a stub, nothing to return [](ApiRequest& request) { - _rfbSendRawFromPayload(request.param(F("value")).c_str()); + _rfbSendRawFromPayload(request.param(F("value"))); return true; } ); @@ -1227,7 +1237,7 @@ static void _rfbCommandWrite(::terminal::CommandContext&& ctx) { terminalError(ctx, F("RFB.WRITE ")); return; } - _rfbSendRawFromPayload(ctx.argv[1].c_str()); + _rfbSendRawFromPayload(ctx.argv[1]); terminalOK(ctx); } #endif diff --git a/code/espurna/rpnrules.cpp b/code/espurna/rpnrules.cpp index 9bba5bb4..26f394a7 100644 --- a/code/espurna/rpnrules.cpp +++ b/code/espurna/rpnrules.cpp @@ -34,6 +34,7 @@ Copyright (C) 2019 by Xose Pérez // ----------------------------------------------------------------------------- +namespace espurna { namespace rpnrules { namespace { @@ -447,13 +448,37 @@ void subscribe() { } } -void callback(unsigned int type, const char * topic, const char * payload) { +rpn_value process_variable(espurna::StringView payload) { + auto tmp = std::make_unique(); + + rpn_value out; + if (!rpn_process(*tmp, payload.begin())) { + return out; + } + + if (rpn_stack_size(*tmp) != 1) { + return out; + } + + out = rpn_stack_pop(*tmp); + return out; +} + +void callback(unsigned int type, StringView topic, StringView payload) { if (type == MQTT_CONNECT_EVENT) { subscribe(); return; } if (type == MQTT_MESSAGE_EVENT) { + if (!payload.length()) { + return; + } + + if ((payload[0] == '&') || (payload[0] == '$')) { + return; + } + size_t index { 0 }; String rpnTopic; @@ -464,20 +489,29 @@ void callback(unsigned int type, const char * topic, const char * payload) { } if (rpnTopic == topic) { - auto name = rpnrules::settings::name(index); + const auto name = rpnrules::settings::name(index); if (!name.length()) { break; } + auto value = process_variable(payload); + if (value.isNull() || value.isError()) { + return; + } + for (auto& variable : variables) { if (variable.name == name) { - variable.value = rpn_value{atof(payload)}; + variable.value = std::move(value); return; } } - variables.emplace_front(Variable{ - std::move(name), rpn_value{atof(payload)}}); + variables.emplace_front( + Variable{ + .name = std::move(name), + .value = std::move(value), + }); + return; } } @@ -1340,9 +1374,10 @@ void setup() { } // namespace } // namespace rpnrules +} // namespace espurna void rpnSetup() { - rpnrules::setup(); + espurna::rpnrules::setup(); } #endif // RPN_RULES_SUPPORT diff --git a/code/espurna/scheduler.cpp b/code/espurna/scheduler.cpp index d52ff9e6..ea829af5 100644 --- a/code/espurna/scheduler.cpp +++ b/code/espurna/scheduler.cpp @@ -531,11 +531,15 @@ bool set(ApiRequest&, JsonObject& root) { namespace schedule { +bool tryParseId(StringView value, size_t& out) { + return ::tryParseId(value, build::max(), out); +} + bool get(ApiRequest& req, JsonObject& root) { const auto param = req.wildcard(0); size_t id; - if (tryParseId(param, build::max, id)) { + if (tryParseId(param, id)) { print(root, settings::schedule(id)); return true; } @@ -547,7 +551,7 @@ bool set(ApiRequest& req, JsonObject& root) { const auto param = req.wildcard(0); size_t id; - if (tryParseId(param, build::max, id)) { + if (tryParseId(param, id)) { return api::set(root, id); } diff --git a/code/espurna/sensor.cpp b/code/espurna/sensor.cpp index 2e6b4ff9..12bb4e08 100644 --- a/code/espurna/sensor.cpp +++ b/code/espurna/sensor.cpp @@ -2954,8 +2954,8 @@ ParseResult convert(StringView value) { return out; } - const auto begin = value.c_str(); - const auto end = value.c_str() + value.length(); + const auto begin = value.begin(); + const auto end = value.end(); String kwh_number; @@ -2972,25 +2972,26 @@ ParseResult convert(StringView value) { KilowattHours::Type kwh { 0 }; WattSeconds::Type ws { 0 }; - char* endp { nullptr }; - kwh = strtoul(kwh_number.c_str(), &endp, 10); - if (!endp || (endp == kwh_number.c_str())) { + const auto result = parseUnsigned(kwh_number, 10); + if (!result.ok) { return out; } + kwh = result.value; + if ((it != end) && (*it == '+')) { ++it; if (it == end) { return out; } - String ws_number; - ws_number.concat(it, (end - it)); - - ws = strtoul(ws_number.c_str(), &endp, 10); - if (!endp || (endp == ws_number.c_str())) { + const auto result = parseUnsigned( + StringView(it, end), 10); + if (!result.ok) { return out; } + + ws = result.value; } out = Energy { @@ -3016,7 +3017,7 @@ void set(const Magnitude& magnitude, const Energy& energy) { } } -void set(const Magnitude& magnitude, const String& payload) { +void set(const Magnitude& magnitude, StringView payload) { if (!payload.length()) { return; } @@ -3029,14 +3030,6 @@ void set(const Magnitude& magnitude, const String& payload) { set(magnitude, energy.value()); } -void set(const Magnitude& magnitude, const char* payload) { - if (!payload) { - return; - } - - set(magnitude, String(payload)); -} - Energy get(unsigned char index) { Energy result; @@ -3547,28 +3540,16 @@ void setup() { } // namespace web #endif -bool tryParseIndex(const char* p, unsigned char type, unsigned char& output) { - char* endp { nullptr }; - const unsigned long result { strtoul(p, &endp, 10) }; - if ((endp == p) || (*endp != '\0') || (result >= magnitude::count(type))) { - DEBUG_MSG_P(PSTR("[SENSOR] Invalid magnitude ID (%s)\n"), p); - return false; - } - - output = result; - return true; -} - #if API_SUPPORT namespace api { namespace { template bool tryHandle(ApiRequest& request, unsigned char type, T&& callback) { - unsigned char index { 0u }; + size_t index = 0; if (request.wildcards()) { - auto index_param = request.wildcard(0); - if (!tryParseIndex(index_param.c_str(), type, index)) { + const auto param = request.wildcard(0); + if (!::tryParseId(param, magnitude::count(type), index)) { return false; } } @@ -3634,7 +3615,7 @@ void setup() { namespace mqtt { namespace { -void callback(unsigned int type, const char* topic, char* payload) { +void callback(unsigned int type, StringView topic, StringView payload) { if (!magnitude::count(MAGNITUDE_ENERGY)) { return; } @@ -3642,43 +3623,29 @@ void callback(unsigned int type, const char* topic, char* payload) { static const auto base = magnitude::topic(MAGNITUDE_ENERGY); switch (type) { - case MQTT_MESSAGE_EVENT: { - String tail = mqttMagnitude(topic); - if (!tail.startsWith(base)) { + auto t = mqttMagnitude(topic); + if (!t.startsWith(base)) { break; } - for (auto ptr = tail.c_str(); ptr != tail.end(); ++ptr) { - if (*ptr != '/') { - continue; - } - - ++ptr; - if (ptr == tail.end()) { - break; - } - - unsigned char index; - if (!tryParseIndex(ptr, MAGNITUDE_ENERGY, index)) { - break; - } + size_t index; + if (!tryParseIdPath(t, magnitude::count(MAGNITUDE_ENERGY), index)) { + break; + } - const auto* magnitude = magnitude::find(MAGNITUDE_ENERGY, index); - if (magnitude) { - energy::set(*magnitude, static_cast(payload)); - } + const auto* magnitude = magnitude::find(MAGNITUDE_ENERGY, index); + if (magnitude) { + energy::set(*magnitude, payload.toString()); } break; } case MQTT_CONNECT_EVENT: - { mqttSubscribe((base + F("/+")).c_str()); break; - } } } diff --git a/code/espurna/settings.cpp b/code/espurna/settings.cpp index 17aae351..27858e96 100644 --- a/code/espurna/settings.cpp +++ b/code/espurna/settings.cpp @@ -244,7 +244,7 @@ bool convert(const String& value) { template <> uint32_t convert(const String& value) { - return parseUnsigned(value); + return parseUnsigned(value).value; } String serialize(uint32_t value, int base) { @@ -526,7 +526,7 @@ void moveSettings(const String& from, const String& to) { for (size_t index = 0; index < 100; ++index) { const auto keys = SettingsKeyPair{ .from = {from, index}, - .to = {to, index} + .to = {to, index}, }; const auto result = espurna::settings::get(keys.from.value()); @@ -571,24 +571,23 @@ String getSetting(const String& key) { } String getSetting(const __FlashStringHelper* key) { - return getSetting(String(key)); + return getSetting(espurna::settings::Key(key)); } String getSetting(const char* key) { - return getSetting(String(key)); + return getSetting(espurna::settings::Key(key)); } String getSetting(const espurna::settings::Key& key) { - static const String defaultValue(""); - return getSetting(key, defaultValue); + return getSetting(key, espurna::StringView("")); } String getSetting(const espurna::settings::Key& key, const char* defaultValue) { - return getSetting(key, String(defaultValue)); + return getSetting(key, espurna::StringView(defaultValue)); } String getSetting(const espurna::settings::Key& key, const __FlashStringHelper* defaultValue) { - return getSetting(key, String(defaultValue)); + return getSetting(key, espurna::StringView(defaultValue)); } String getSetting(const espurna::settings::Key& key, const String& defaultValue) { @@ -601,21 +600,29 @@ String getSetting(const espurna::settings::Key& key, const String& defaultValue) } String getSetting(const espurna::settings::Key& key, String&& defaultValue) { + String out; + auto result = espurna::settings::get(key.value()); if (result) { - return std::move(result).get(); + out = std::move(result).get(); + } else { + out = std::move(defaultValue); } - return std::move(defaultValue); + return out; } String getSetting(const espurna::settings::Key& key, espurna::StringView defaultValue) { + String out; + auto result = espurna::settings::get(key.value()); if (result) { - return std::move(result).get(); + out = std::move(result).get(); + } else { + out = defaultValue.toString(); } - return String(defaultValue); + return out; } bool delSetting(const String& key) { diff --git a/code/espurna/settings_helpers.h b/code/espurna/settings_helpers.h index 741313e1..96bee9ac 100644 --- a/code/espurna/settings_helpers.h +++ b/code/espurna/settings_helpers.h @@ -41,6 +41,10 @@ public: _key(std::move(key)) {} + Key(StringView key) : + Key(key.toString()) + {} + Key(const String& prefix, size_t index) : _key(prefix) { @@ -53,6 +57,10 @@ public: _key += index; } + Key(StringView key, size_t index) : + Key(key.toString(), index) + {} + Key(const char* prefix, size_t index) : _key(prefix) { diff --git a/code/espurna/telnet.cpp b/code/espurna/telnet.cpp index 63718ba0..ce81c97e 100644 --- a/code/espurna/telnet.cpp +++ b/code/espurna/telnet.cpp @@ -45,6 +45,14 @@ namespace espurna { namespace telnet { namespace { +constexpr bool isEspurnaMinimal() { +#if defined(ESPURNA_MINIMAL_ARDUINO_OTA) || defined(ESPURNA_MINIMAL_WEBUI) + return true; +#else + return false; +#endif +} + namespace build { constexpr size_t ClientsMax { TELNET_MAX_CLIENTS }; @@ -929,7 +937,7 @@ void connect_url(String url) { } void setup() { - mqttRegister([](unsigned int type, const char* topic, const char* payload) { + mqttRegister([](unsigned int type, StringView topic, StringView payload) { switch (type) { case MQTT_CONNECT_EVENT: mqttSubscribe(MQTT_TOPIC_TELNET_REVERSE); @@ -938,7 +946,7 @@ void setup() { case MQTT_MESSAGE_EVENT: { auto t = mqttMagnitude(topic); if (t.equals(MQTT_TOPIC_TELNET_REVERSE)) { - connect_url(payload); + connect_url(payload.toString()); } break; } diff --git a/code/espurna/terminal.cpp b/code/espurna/terminal.cpp index e95e7b69..48fd0fa4 100644 --- a/code/espurna/terminal.cpp +++ b/code/espurna/terminal.cpp @@ -459,19 +459,24 @@ void setup() { namespace mqtt { void setup() { - mqttRegister([](unsigned int type, const char* topic, char* payload) { + mqttRegister([](unsigned int type, StringView topic, StringView payload) { if (type == MQTT_CONNECT_EVENT) { mqttSubscribe(MQTT_TOPIC_CMD); return; } if (type == MQTT_MESSAGE_EVENT) { - String t = mqttMagnitude(topic); - if (!t.startsWith(MQTT_TOPIC_CMD)) return; - if (!strlen(payload)) return; + auto t = mqttMagnitude(topic); + if (!t.startsWith(MQTT_TOPIC_CMD)) { + return; + } - auto line = String(payload); - if (!line.endsWith("\r\n") && !line.endsWith("\n")) { + if (!payload.length()) { + return; + } + + auto line = payload.toString(); + if (!payload.endsWith("\r\n") && !payload.endsWith("\n")) { line += '\n'; } @@ -481,14 +486,13 @@ void setup() { // TODO: or, at least, make it growable on-demand and cap at MSS? // TODO: PrintLine<...> instead of one giant blob? - auto cmd = std::make_shared(std::move(line)); - - espurnaRegisterOnce([cmd]() { + auto ptr = std::make_shared(std::move(line)); + espurnaRegisterOnce([ptr]() { PrintString out(TCP_MSS); - api_find_and_call(*cmd, out); + api_find_and_call(*ptr, out); if (out.length()) { - static const auto topic = mqttTopic(MQTT_TOPIC_CMD, false); + static const auto topic = mqttTopic(MQTT_TOPIC_CMD); mqttSendRaw(topic.c_str(), out.c_str(), false); } }); diff --git a/code/espurna/terminal_parsing.h b/code/espurna/terminal_parsing.h index 625916da..73d3731e 100644 --- a/code/espurna/terminal_parsing.h +++ b/code/espurna/terminal_parsing.h @@ -80,7 +80,7 @@ struct LineBuffer { } return Result{ - .line = StringView(nullptr), + .line = StringView(), .overflow = _overflow }; } @@ -164,7 +164,7 @@ struct LineView { } } - return StringView{nullptr}; + return StringView(); } explicit operator bool() const { diff --git a/code/espurna/thermostat.cpp b/code/espurna/thermostat.cpp index 803e665f..679d1d52 100644 --- a/code/espurna/thermostat.cpp +++ b/code/espurna/thermostat.cpp @@ -176,7 +176,7 @@ bool _thermostatMqttHeartbeat(espurna::heartbeat::Mask mask) { return mqttConnected(); } -void thermostatMqttCallback(unsigned int type, const char* topic, char* payload) { +void thermostatMqttCallback(unsigned int type, espurna::StringView topic, espurna::StringView payload) { if (type == MQTT_CONNECT_EVENT) { mqttSubscribeRaw(thermostat_remote_sensor_topic.c_str()); @@ -184,24 +184,22 @@ void thermostatMqttCallback(unsigned int type, const char* topic, char* payload) } if (type == MQTT_MESSAGE_EVENT) { - - // Match topic - String t = mqttMagnitude(topic); - - if (strcmp(topic, thermostat_remote_sensor_topic.c_str()) != 0 - && !t.equals(MQTT_TOPIC_HOLD_TEMP)) + auto t = mqttMagnitude(topic); + if ((topic != thermostat_remote_sensor_topic) + && !t.equals(MQTT_TOPIC_HOLD_TEMP)) + { return; + } - // Parse JSON input DynamicJsonBuffer jsonBuffer; - JsonObject& root = jsonBuffer.parseObject(payload); + JsonObject& root = jsonBuffer.parseObject(payload.begin()); if (!root.success()) { DEBUG_MSG_P(PSTR("[THERMOSTAT] Error parsing data\n")); return; } - // Check rempte sensor temperature - if (strcmp(topic, thermostat_remote_sensor_topic.c_str()) == 0) { + // Check remote sensor temperature + if (topic == thermostat_remote_sensor_topic) { if (root.containsKey(magnitudeTopic(MAGNITUDE_TEMPERATURE))) { String remote_temp = root[magnitudeTopic(MAGNITUDE_TEMPERATURE)]; _remote_temp.temp = remote_temp.toFloat(); diff --git a/code/espurna/thingspeak.cpp b/code/espurna/thingspeak.cpp index bf640b69..b1809a55 100644 --- a/code/espurna/thingspeak.cpp +++ b/code/espurna/thingspeak.cpp @@ -94,7 +94,7 @@ String apiKey() { } String address() { - return getSetting(FPSTR(keys::Address), FPSTR(build::ApiKey)); + return getSetting(FPSTR(keys::Address), FPSTR(build::Address)); } #if RELAY_SUPPORT @@ -362,7 +362,7 @@ public: } bool send(const String& address, const String& data, Completion completion) { - _address = address; + _address = URL(address); return send(data, completion); } diff --git a/code/espurna/types.cpp b/code/espurna/types.cpp index 54cce360..ef4ea8fd 100644 --- a/code/espurna/types.cpp +++ b/code/espurna/types.cpp @@ -116,11 +116,9 @@ bool StringView::equals(StringView other) const { bool StringView::equalsIgnoreCase(StringView other) const { if (other._len == _len) { - if (inFlash(_ptr) || inFlash(other._ptr)) { - if (_ptr == other._ptr) { - return true; - } - + if (inFlash(_ptr) && inFlash(other._ptr) && (_ptr == other._ptr)) { + return true; + } else if (inFlash(_ptr) || inFlash(other._ptr)) { String copy; const char* ptr = _ptr; if (inFlash(_ptr)) { @@ -137,4 +135,20 @@ bool StringView::equalsIgnoreCase(StringView other) const { return false; } +bool StringView::startsWith(StringView other) const { + if (other._len <= _len) { + return StringView(begin(), begin() + other._len).equals(other); + } + + return false; +} + +bool StringView::endsWith(StringView other) const { + if (other._len <= _len) { + return StringView(end() - other._len, end()).equals(other); + } + + return false; +} + } // namespace espurna diff --git a/code/espurna/types.h b/code/espurna/types.h index e59760dd..26e10432 100644 --- a/code/espurna/types.h +++ b/code/espurna/types.h @@ -12,6 +12,8 @@ Copyright (C) 2019-2021 by Maxim Prokhorov +#include "compat.h" + // missing in our original header extern "C" int memcmp_P(const void*, const void*, size_t); @@ -52,10 +54,7 @@ struct Callback { Callback& operator=(Callback&& other) noexcept; template - using remove_cvref = typename std::remove_cv>::type; - - template - using is_callback = std::is_same, Callback>; + using is_callback = std::is_same, Callback>; template using is_type = std::is_same; @@ -212,6 +211,8 @@ struct StringView { StringView() noexcept = default; ~StringView() = default; + StringView(std::nullptr_t) = delete; + constexpr StringView(const StringView&) noexcept = default; constexpr StringView(StringView&&) noexcept = default; @@ -246,11 +247,6 @@ struct StringView { _len(strlen_P(_ptr)) {} - explicit constexpr StringView(std::nullptr_t) noexcept : - _ptr(nullptr), - _len(0) - {} - StringView(const String& string) noexcept : StringView(string.c_str(), string.length()) {} @@ -301,6 +297,9 @@ struct StringView { bool equals(StringView) const; bool equalsIgnoreCase(StringView) const; + bool startsWith(StringView) const; + bool endsWith(StringView) const; + private: #if defined(HOST_MOCK) constexpr static bool inFlash(const char*) { diff --git a/code/espurna/uartmqtt.cpp b/code/espurna/uartmqtt.cpp index 3eff77ce..59589aa1 100644 --- a/code/espurna/uartmqtt.cpp +++ b/code/espurna/uartmqtt.cpp @@ -125,7 +125,7 @@ String serialize(Span bytes, bool encode) { // have time to send the previously buffered data. void send(String data) { mqttSendRaw( - mqttTopic(MQTT_TOPIC_UARTIN, false).c_str(), + mqttTopic(MQTT_TOPIC_UARTIN).c_str(), data.c_str(), false, 0); } @@ -238,7 +238,7 @@ void write(Print& print, uint8_t termination, bool decode) { } -void mqtt_callback(unsigned int type, const char* topic, const char* payload) { +void mqtt_callback(unsigned int type, StringView topic, StringView payload) { static constexpr char Subscription[] = MQTT_TOPIC_UARTOUT; switch (type) { @@ -248,7 +248,7 @@ void mqtt_callback(unsigned int type, const char* topic, const char* payload) { case MQTT_MESSAGE_EVENT: const auto t = mqttMagnitude(topic); if (t.equals(Subscription)) { - enqueue(payload); + enqueue(payload.toString()); } break; } diff --git a/code/espurna/utils.cpp b/code/espurna/utils.cpp index bae8c66e..5cc294a6 100644 --- a/code/espurna/utils.cpp +++ b/code/espurna/utils.cpp @@ -13,16 +13,127 @@ Copyright (C) 2017-2019 by Xose Pérez #include #include -bool tryParseId(espurna::StringView value, TryParseIdFunc limit, size_t& out) { - static_assert(std::numeric_limits::max() >= std::numeric_limits::max(), ""); +// We can only return small values (max 'z' aka 122) +static constexpr uint8_t InvalidByte { 255u }; - char* endp { nullptr }; - out = strtoul(value.begin(), &endp, 10); // TODO from_chars - if ((endp == value.begin()) || (*endp != '\0') || (out >= limit())) { - return false; +static uint8_t bin_char2byte(char c) { + switch (c) { + case '0'...'1': + return (c - '0'); } - return true; + return InvalidByte; +} + +static uint8_t oct_char2byte(char c) { + switch (c) { + case '0'...'7': + return (c - '0'); + } + + return InvalidByte; +} + +static uint8_t dec_char2byte(char c) { + switch (c) { + case '0'...'9': + return (c - '0'); + } + + return InvalidByte; +} + +static uint8_t hex_char2byte(char c) { + switch (c) { + case '0'...'9': + return (c - '0'); + case 'a'...'f': + return 10 + (c - 'a'); + case 'A'...'F': + return 10 + (c - 'A'); + } + + return InvalidByte; +} + +static ParseUnsignedResult parseUnsignedImpl(espurna::StringView value, int base) { + auto out = ParseUnsignedResult{ + .ok = false, + .value = 0, + }; + + using Char2Byte = uint8_t(*)(char); + Char2Byte char2byte = nullptr; + + switch (base) { + case 2: + char2byte = bin_char2byte; + break; + case 8: + char2byte = oct_char2byte; + break; + case 10: + char2byte = dec_char2byte; + break; + case 16: + char2byte = hex_char2byte; + break; + } + + if (!char2byte) { + return out; + } + + for (auto it = value.begin(); it != value.end(); ++it) { + const auto digit = char2byte(*it); + if (digit == InvalidByte) { + out.ok = false; + goto err; + } + + const auto value = out.value; + out.value = (out.value * uint32_t(base)) + digit; + // TODO explicitly set the output bit width? + if (value > out.value) { + out.ok = false; + goto err; + } + + out.ok = true; + } + +err: + return out; +} + +bool tryParseId(espurna::StringView value, size_t limit, size_t& out) { + using T = std::remove_cvref::type; + static_assert(std::is_same::value, ""); + + if (value.length()) { + const auto result = parseUnsignedImpl(value, 10); + if (result.ok && (result.value < limit)) { + out = result.value; + return true; + } + } + + return false; +} + +bool tryParseIdPath(espurna::StringView value, size_t limit, size_t& out) { + if (value.length()) { + const auto before_begin = value.begin() - 1; + for (auto it = value.end() - 1; it != before_begin; --it) { + if ((*it) == '/') { + return tryParseId( + espurna::StringView(it + 1, value.end()), + limit, out); + } + } + } + + return false; } String prettyDuration(espurna::duration::Seconds seconds) { @@ -187,37 +298,15 @@ char* strnstr(const char* buffer, const char* token, size_t n) { return nullptr; } -namespace { - -uint32_t parseUnsignedImpl(const String& value, int base) { - const char* ptr { value.c_str() }; - char* endp { nullptr }; - - // invalidate the whole string when invalid chars are detected - // while this does not return a 'result' type, we can't know - // whether 0 was the actual decoded number or not - const auto result = strtoul(ptr, &endp, base); - if (endp == ptr || endp[0] != '\0') { - return 0; - } - - return result; -} - -} // namespace - -uint32_t parseUnsigned(const String& value, int base) { +ParseUnsignedResult parseUnsigned(espurna::StringView value, int base) { return parseUnsignedImpl(value, base); } -uint32_t parseUnsigned(const String& value) { - if (!value.length()) { - return 0; - } - +ParseUnsignedResult parseUnsigned(espurna::StringView value) { int base = 10; - if (value.length() > 2) { - auto* ptr = value.c_str(); + + if (value.length() && (value.length() > 2)) { + const auto* ptr = value.begin(); if (*ptr == '0') { switch (*(ptr + 1)) { case 'b': @@ -231,9 +320,12 @@ uint32_t parseUnsigned(const String& value) { break; } } + + value = espurna::StringView( + value.begin() + 2, value.end()); } - return parseUnsignedImpl((base == 10) ? value : value.substring(2), base); + return parseUnsignedImpl(value, base); } String formatUnsigned(uint32_t value, int base) { @@ -322,34 +414,18 @@ size_t hexEncode(const uint8_t* in, size_t in_size, char* out, size_t out_size) // From an hexa char array ("A220EE...") to a byte array (half the size) uint8_t* hexDecode(const char* in_begin, const char* in_end, uint8_t* out_begin, uint8_t* out_end) { - // We can only return small values (max 'z' aka 122) - constexpr uint8_t InvalidByte { 255u }; - - auto char2byte = [](char ch) -> uint8_t { - switch (ch) { - case '0'...'9': - return (ch - '0'); - case 'a'...'f': - return 10 + (ch - 'a'); - case 'A'...'F': - return 10 + (ch - 'A'); - } - - return InvalidByte; - }; - constexpr uint8_t Shift { 4 }; const char* in_ptr { in_begin }; uint8_t* out_ptr { out_begin }; while ((in_ptr != in_end) && (out_ptr != out_end)) { - uint8_t lhs = char2byte(*in_ptr); + uint8_t lhs = hex_char2byte(*in_ptr); if (lhs == InvalidByte) { break; } ++in_ptr; - uint8_t rhs = char2byte(*in_ptr); + uint8_t rhs = hex_char2byte(*in_ptr); if (rhs == InvalidByte) { break; } diff --git a/code/espurna/utils.h b/code/espurna/utils.h index 1be39683..b57f410c 100644 --- a/code/espurna/utils.h +++ b/code/espurna/utils.h @@ -29,8 +29,13 @@ double roundTo(double num, unsigned char positions); bool almostEqual(double lhs, double rhs, int ulp); bool almostEqual(double lhs, double rhs); -uint32_t parseUnsigned(const String&, int base); -uint32_t parseUnsigned(const String&); +struct ParseUnsignedResult { + bool ok; + uint32_t value; +}; + +ParseUnsignedResult parseUnsigned(espurna::StringView, int base); +ParseUnsignedResult parseUnsigned(espurna::StringView); String formatUnsigned(uint32_t value, int base); char* hexEncode(const uint8_t* in_begin, const uint8_t* in_end, char* out_begin, char* out_end); @@ -50,7 +55,7 @@ inline String hexEncode(uint8_t value) { uint8_t* hexDecode(const char* in_begin, const char* in_end, uint8_t* out_begin, uint8_t* out_end); size_t hexDecode(const char* in, size_t in_size, uint8_t* out, size_t out_size); -using TryParseIdFunc = size_t(*)(); -bool tryParseId(espurna::StringView, TryParseIdFunc limit, size_t& out); +bool tryParseId(espurna::StringView, size_t limit, size_t& out); +bool tryParseIdPath(espurna::StringView, size_t limit, size_t& out); espurna::StringView stripNewline(espurna::StringView); diff --git a/code/espurna/web_utils.h b/code/espurna/web_utils.h index cf17b2fe..49806d27 100644 --- a/code/espurna/web_utils.h +++ b/code/espurna/web_utils.h @@ -48,6 +48,8 @@ struct StringTraits<::espurna::StringView, void> { return nullptr; } + // technically, we could've had append w/ strings only + // but, this also means append of char, which we could not do static const bool has_append = false; static const bool has_equals = true; static const bool should_duplicate = true; diff --git a/code/test/unit/src/types/types.cpp b/code/test/unit/src/types/types.cpp index 7bdbb03b..e24eeb5a 100644 --- a/code/test/unit/src/types/types.cpp +++ b/code/test/unit/src/types/types.cpp @@ -18,6 +18,17 @@ void test_view() { expected, view.begin(), view.length()); } +void test_view_nullptr() { + auto func = [](espurna::StringView view) { + TEST_ASSERT_EQUAL(0, view.length()); + TEST_ASSERT_EQUAL(nullptr, view.begin()); + TEST_ASSERT_EQUAL(nullptr, view.end()); + }; + + static_assert(!std::is_convertible::value, ""); + func(espurna::StringView()); +} + void test_view_convert() { const String origin("12345"); StringView view(origin); @@ -40,6 +51,15 @@ void test_view_convert() { TEST_ASSERT(view.equals(copy_view)); } +void test_view_compare() { + StringView base("aaaa bbbb cccc dddd"); + + TEST_ASSERT(base.startsWith("aaaa")); + TEST_ASSERT(base.endsWith("dddd")); + TEST_ASSERT(base.equals("aaaa bbbb cccc dddd")); + TEST_ASSERT(base.equalsIgnoreCase("aaaa BBBB cccc DDDD")); +} + void test_callback_empty() { Callback callback; TEST_ASSERT(callback.isEmpty()); @@ -204,7 +224,9 @@ int main(int, char**) { using namespace espurna::test; RUN_TEST(test_view); + RUN_TEST(test_view_nullptr); RUN_TEST(test_view_convert); + RUN_TEST(test_view_compare); RUN_TEST(test_callback_empty); RUN_TEST(test_callback_simple); RUN_TEST(test_callback_lambda);