Browse Source

mqtt: string views in API and internal callbacks

* stateless callbacks for mqttRegister. no module uses the lambda / std
  func with captures or otherwise, so it is kind of pointless to have it
* make sure we know topic and payload length at all invocations and
  not constantly trying to re-parse the same string over and over again
* clean-up api related to string parsing and make sure we allow
  stringview as input
* clean-up id / unsigned number parsing to work with views instead of
  using a generic strto{,u}l; sometimes this works, sometimes doesn't.
  as noticed previously with IR implementation, it *will* parse until
  the '\0' is found in the input and we can't interpret parts of the
  string without doing a copy for the strtoul
* fixing additional bugs caught in ifan, leds, sensors and lights that
  were causing build failures. plus, more range-based parsing code for
  the same reason as described above
pull/2552/head
Maxim Prokhorov 1 year ago
parent
commit
a11942fd7b
40 changed files with 1065 additions and 900 deletions
  1. +59
    -26
      code/espurna/api.cpp
  2. +7
    -10
      code/espurna/api_impl.h
  3. +27
    -10
      code/espurna/api_path.h
  4. +1
    -1
      code/espurna/button.cpp
  5. +14
    -19
      code/espurna/compat.h
  6. +6
    -6
      code/espurna/curtain_kingart.cpp
  7. +4
    -4
      code/espurna/domoticz.cpp
  8. +16
    -18
      code/espurna/garland.cpp
  9. +12
    -12
      code/espurna/homeassistant.cpp
  10. +27
    -30
      code/espurna/ifan.cpp
  11. +14
    -27
      code/espurna/ir.cpp
  12. +11
    -40
      code/espurna/led.cpp
  13. +17
    -18
      code/espurna/led_pattern.re
  14. +75
    -76
      code/espurna/led_pattern.re.ipp
  15. +6
    -7
      code/espurna/libs/URL.h
  16. +91
    -92
      code/espurna/light.cpp
  17. +207
    -180
      code/espurna/mqtt.cpp
  18. +14
    -13
      code/espurna/mqtt.h
  19. +13
    -10
      code/espurna/ota_asynctcp.cpp
  20. +9
    -9
      code/espurna/ota_httpupdate.cpp
  21. +27
    -41
      code/espurna/relay.cpp
  22. +5
    -7
      code/espurna/relay_pulse.ipp
  23. +74
    -64
      code/espurna/rfbridge.cpp
  24. +41
    -6
      code/espurna/rpnrules.cpp
  25. +6
    -2
      code/espurna/scheduler.cpp
  26. +25
    -58
      code/espurna/sensor.cpp
  27. +19
    -12
      code/espurna/settings.cpp
  28. +8
    -0
      code/espurna/settings_helpers.h
  29. +10
    -2
      code/espurna/telnet.cpp
  30. +15
    -11
      code/espurna/terminal.cpp
  31. +2
    -2
      code/espurna/terminal_parsing.h
  32. +9
    -11
      code/espurna/thermostat.cpp
  33. +2
    -2
      code/espurna/thingspeak.cpp
  34. +19
    -5
      code/espurna/types.cpp
  35. +8
    -9
      code/espurna/types.h
  36. +3
    -3
      code/espurna/uartmqtt.cpp
  37. +129
    -53
      code/espurna/utils.cpp
  38. +9
    -4
      code/espurna/utils.h
  39. +2
    -0
      code/espurna/web_utils.h
  40. +22
    -0
      code/test/unit/src/types/types.cpp

+ 59
- 26
code/espurna/api.cpp View File

@ -28,7 +28,7 @@ Copyright (C) 2020-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com
// -----------------------------------------------------------------------------
PathParts::PathParts(const String& path) :
PathParts::PathParts(espurna::StringView path) :
_path(path)
{
if (!_path.length()) {
@ -40,7 +40,7 @@ PathParts::PathParts(const String& path) :
size_t length { 0ul };
size_t offset { 0ul };
const char* p { _path.c_str() };
const char* p { _path.begin() };
if (*p == '\0') {
goto error;
}
@ -193,43 +193,76 @@ error:
return false;
}
#if WEB_SUPPORT
String ApiRequest::wildcard(int index) const {
espurna::StringView PathParts::wildcard(const PathParts& pattern, const PathParts& value, int index) {
if (index < 0) {
index = std::abs(index + 1);
}
if (std::abs(index) >= _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);


+ 7
- 10
code/espurna/api_impl.h View File

@ -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;


+ 27
- 10
code/espurna/api_path.h View File

@ -9,9 +9,10 @@ Copyright (C) 2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
#pragma once
#include <Arduino.h>
#include <vector>
#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 };
};

+ 1
- 1
code/espurna/button.cpp View File

@ -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;
}


+ 14
- 19
code/espurna/compat.h View File

@ -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 <Arduino.h>
// -----------------------------------------------------------------------------
// 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 <memory>
#include <type_traits>
namespace std {
#if __cplusplus < 202002L
template <typename T>
using remove_cvref = typename std::remove_cv<std::remove_reference<T>>::type;
#endif
#if __cplusplus < 201304L
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
#endif
#if __cplusplus < 201603L
template <typename T>
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 <typename T, size_t Size>
constexpr size_t size(const T (&)[Size]) {
return Size;
@ -140,16 +141,10 @@ template <typename T>
constexpr auto cend(const T& value) -> decltype(std::end(value)) {
return std::end(value);
}
template <typename T>
constexpr std::reverse_iterator<T> make_reverse_iterator(T iterator) {
return std::reverse_iterator<T>(iterator);
}
#endif
} // namespace std
#endif
// Same as min and max, force same type arguments
#undef constrain


+ 6
- 6
code/espurna/curtain_kingart.cpp View File

@ -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);
}
}


+ 4
- 4
code/espurna/domoticz.cpp View File

@ -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;


+ 16
- 18
code/espurna/garland.cpp View File

@ -329,11 +329,11 @@ bool executeCommand(const String& command) {
one_color_palette.reset(new Palette("Color", {root[MQTT_PAYLOAD_PALETTE].as<uint32_t>()}));
newPalette = one_color_palette.get();
} else {
auto palette = root[MQTT_PAYLOAD_PALETTE].as<const char*>();
auto palette = root[MQTT_PAYLOAD_PALETTE].as<String>();
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<String> 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
}


+ 12
- 12
code/espurna/homeassistant.cpp View File

@ -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::Mask>(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);
}


+ 27
- 30
code/espurna/ifan.cpp View File

@ -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<FanProvider>(std::move(relay_pin), config, onFanSpeedUpdate);
auto provider = std::make_unique<FanProvider>(
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());


+ 14
- 27
code/espurna/ir.cpp View File

@ -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 <typename T>
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;


+ 11
- 40
code/espurna/led.cpp View File

@ -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<LedMode> 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<Led> 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/+/<MQTT_SETTER>`
// 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()) {


+ 17
- 18
code/espurna/led_pattern.re View File

@ -13,8 +13,6 @@ Copyright (C) 2020-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com
// And returns a list of Delay objects for the pattern
Pattern::Pattern(espurna::StringView value) {
char buffer[16];
const char* on1;
const char* on2;
@ -48,27 +46,28 @@ loop:
wsp { goto loop; }
@on1 num @on2 [,] @off1 num @off2 [,] @repeat1 num @repeat2 {
memcpy(buffer, on1, on2 - on1);
buffer[on2 - on1] = '\0';
espurna::duration::Milliseconds::rep on { strtoul(buffer, nullptr, 10) };
memcpy(buffer, off1, off2 - off1);
buffer[off2 - off1] = '\0';
espurna::duration::Milliseconds::rep off { strtoul(buffer, nullptr, 10) };
const auto on = parseUnsigned(StringView(on1, on2), 10);
if (!on.ok) {
return;
}
memcpy(buffer, repeat1, repeat2 - repeat1);
buffer[repeat2 - repeat1] = '\0';
const auto off = parseUnsigned(StringView(off1, off2), 10);
if (!off.ok) {
return;
}
using Repeats = Delay::Repeats;
Repeats repeats { strtoul(buffer, nullptr, 10) };
constexpr Repeats RepeatsMax { Delay::RepeatsMax };
_delays.emplace_back(
std::min(espurna::duration::Milliseconds(on), Delay::MillisecondsMax),
std::min(espurna::duration::Milliseconds(off), Delay::MillisecondsMax),
std::min(repeats, RepeatsMax));
const auto repeats = parseUnsigned(StringView(repeat1, repeat2), 10);
if (!repeats.ok) {
return;
}
if (repeats) {
_delays.emplace_back(
std::min(duration::Milliseconds(on.value), Delay::MillisecondsMax),
std::min(duration::Milliseconds(off.value), Delay::MillisecondsMax),
std::min(repeats.value, RepeatsMax));
if (repeats.value) {
goto loop;
}
}


+ 75
- 76
code/espurna/led_pattern.re.ipp View File

@ -1,5 +1,5 @@
/* Generated by re2c 2.2 */
#line 1 "espurna\\led_pattern.re"
/* Generated by re2c 3.0 */
#line 1 "espurna/led_pattern.re"
/*
LED MODULE
@ -15,8 +15,6 @@ Copyright (C) 2020-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com
// And returns a list of Delay objects for the pattern
Pattern::Pattern(espurna::StringView value) {
char buffer[16];
const char* on1;
const char* on2;
@ -31,127 +29,128 @@ Pattern::Pattern(espurna::StringView value) {
const char* YYMARKER;
loop:
#line 35 "espurna\\led_pattern.re.ipp"
#line 33 "espurna/led_pattern.re.ipp"
const char *yyt1;const char *yyt2;const char *yyt3;
#line 32 "espurna\\led_pattern.re"
#line 30 "espurna/led_pattern.re"
#line 40 "espurna\\led_pattern.re.ipp"
#line 38 "espurna/led_pattern.re.ipp"
{
char yych;
yych = (char)*YYCURSOR;
switch (yych) {
case '\t':
case ' ': goto yy4;
case '0' ... '9':
yyt1 = YYCURSOR;
goto yy7;
default:
if (YYLIMIT <= YYCURSOR) goto yy18;
goto yy2;
case '\t':
case ' ': goto yy3;
case '0' ... '9':
yyt1 = YYCURSOR;
goto yy5;
default:
if (YYLIMIT <= YYCURSOR) goto yy13;
goto yy1;
}
yy2:
yy1:
++YYCURSOR;
yy3:
#line 46 "espurna\\led_pattern.re"
yy2:
#line 44 "espurna/led_pattern.re"
{ return; }
#line 59 "espurna\\led_pattern.re.ipp"
yy4:
#line 57 "espurna/led_pattern.re.ipp"
yy3:
yych = (char)*++YYCURSOR;
switch (yych) {
case '\t':
case ' ': goto yy4;
default: goto yy6;
case '\t':
case ' ': goto yy3;
default: goto yy4;
}
yy6:
#line 48 "espurna\\led_pattern.re"
yy4:
#line 46 "espurna/led_pattern.re"
{ goto loop; }
#line 70 "espurna\\led_pattern.re.ipp"
yy7:
#line 68 "espurna/led_pattern.re.ipp"
yy5:
yych = (char)*(YYMARKER = ++YYCURSOR);
switch (yych) {
case ',': goto yy8;
case '0' ... '9': goto yy10;
default: goto yy3;
case ',': goto yy6;
case '0' ... '9': goto yy8;
default: goto yy2;
}
yy8:
yy6:
yych = (char)*++YYCURSOR;
switch (yych) {
case '0' ... '9':
yyt2 = YYCURSOR;
goto yy12;
default: goto yy9;
case '0' ... '9':
yyt2 = YYCURSOR;
goto yy9;
default: goto yy7;
}
yy9:
yy7:
YYCURSOR = YYMARKER;
goto yy3;
yy10:
goto yy2;
yy8:
yych = (char)*++YYCURSOR;
switch (yych) {
case ',': goto yy8;
case '0' ... '9': goto yy10;
default: goto yy9;
case ',': goto yy6;
case '0' ... '9': goto yy8;
default: goto yy7;
}
yy12:
yy9:
yych = (char)*++YYCURSOR;
switch (yych) {
case ',': goto yy14;
case '0' ... '9': goto yy12;
default: goto yy9;
case ',': goto yy10;
case '0' ... '9': goto yy9;
default: goto yy7;
}
yy14:
yy10:
yych = (char)*++YYCURSOR;
switch (yych) {
case '0' ... '9':
yyt3 = YYCURSOR;
goto yy15;
default: goto yy9;
case '0' ... '9':
yyt3 = YYCURSOR;
goto yy11;
default: goto yy7;
}
yy15:
yy11:
yych = (char)*++YYCURSOR;
switch (yych) {
case '0' ... '9': goto yy15;
default: goto yy17;
case '0' ... '9': goto yy11;
default: goto yy12;
}
yy17:
yy12:
on1 = yyt1;
off1 = yyt2;
repeat1 = yyt3;
on2 = yyt2 - 1;
off2 = yyt3 - 1;
repeat2 = YYCURSOR;
#line 50 "espurna\\led_pattern.re"
#line 48 "espurna/led_pattern.re"
{
memcpy(buffer, on1, on2 - on1);
buffer[on2 - on1] = '\0';
espurna::duration::Milliseconds::rep on { strtoul(buffer, nullptr, 10) };
memcpy(buffer, off1, off2 - off1);
buffer[off2 - off1] = '\0';
espurna::duration::Milliseconds::rep off { strtoul(buffer, nullptr, 10) };
const auto on = parseUnsigned(StringView(on1, on2), 10);
if (!on.ok) {
return;
}
memcpy(buffer, repeat1, repeat2 - repeat1);
buffer[repeat2 - repeat1] = '\0';
const auto off = parseUnsigned(StringView(off1, off2), 10);
if (!off.ok) {
return;
}
using Repeats = Delay::Repeats;
Repeats repeats { strtoul(buffer, nullptr, 10) };
constexpr Repeats RepeatsMax { Delay::RepeatsMax };
_delays.emplace_back(
std::min(espurna::duration::Milliseconds(on), Delay::MillisecondsMax),
std::min(espurna::duration::Milliseconds(off), Delay::MillisecondsMax),
std::min(repeats, RepeatsMax));
const auto repeats = parseUnsigned(StringView(repeat1, repeat2), 10);
if (!repeats.ok) {
return;
}
if (repeats) {
_delays.emplace_back(
std::min(duration::Milliseconds(on.value), Delay::MillisecondsMax),
std::min(duration::Milliseconds(off.value), Delay::MillisecondsMax),
std::min(repeats.value, RepeatsMax));
if (repeats.value) {
goto loop;
}
}
#line 150 "espurna\\led_pattern.re.ipp"
yy18:
#line 45 "espurna\\led_pattern.re"
#line 149 "espurna/led_pattern.re.ipp"
yy13:
#line 43 "espurna/led_pattern.re"
{ return; }
#line 154 "espurna\\led_pattern.re.ipp"
#line 153 "espurna/led_pattern.re.ipp"
}
#line 75 "espurna\\led_pattern.re"
#line 74 "espurna/led_pattern.re"
}

+ 6
- 7
code/espurna/libs/URL.h View File

@ -13,6 +13,8 @@
#include <cstdint>
#include <utility>
#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);


+ 91
- 92
code/espurna/light.cpp View File

@ -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 <typename T>
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 <typename Begin, typename End>
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<int>(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<espurna::StringView>());
_lightFromRgbPayload(data[Rgb].as<String>());
lightUpdate();
} else if (data.containsKey(Hsv)) {
_lightFromHsvPayload(data[Hsv].as<espurna::StringView>());
_lightFromHsvPayload(data[Hsv].as<String>());
lightUpdate();
}
}


+ 207
- 180
code/espurna/mqtt.cpp View File

@ -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<MqttPidCallback>;
using MqttPidCallbacks = std::forward_list<MqttPidCallbackHandler>;
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_callback_f> _mqtt_callbacks;
std::forward_list<MqttCallback> _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 <typename Rhs>
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.
// * <TOPIC>/#/set - generic topic placement
// ^
// * <LHS>/#/<RHS>/set - when {magnitude} is used
// ^
// * #/<RHS>/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);
}


+ 14
- 13
code/espurna/mqtt.h View File

@ -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<void(unsigned int type, const char* topic, char* payload)>;
using mqtt_pid_callback_f = std::function<void()>;
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()>;
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();


+ 13
- 10
code/espurna/ota_asynctcp.cpp View File

@ -31,6 +31,7 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
#include <ESPAsyncTCP.h>
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<BasicHttpClient>(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
}


+ 9
- 9
code/espurna/ota_httpupdate.cpp View File

@ -22,7 +22,6 @@ Copyright (C) 2019 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
#include <ESP8266HTTPClient.h>
#include <ESP8266httpUpdate.h>
#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
}


+ 27
- 41
code/espurna/relay.cpp View File

@ -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;


+ 5
- 7
code/espurna/relay_pulse.ipp View File

@ -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:


+ 74
- 64
code/espurna/rfbridge.cpp View File

@ -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<RfbLearn>& learn, const char* buffer) {
if (!learn) return;
void _rfbLearnFromString(std::unique_ptr<RfbLearn>& 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<RfbLearn>& 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<RfbLearn>& learn, const char* buffer) {
void _rfbLearnFromReceived(std::unique_ptr<RfbLearn>& 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 <PAYLOAD>"));
return;
}
_rfbSendRawFromPayload(ctx.argv[1].c_str());
_rfbSendRawFromPayload(ctx.argv[1]);
terminalOK(ctx);
}
#endif


+ 41
- 6
code/espurna/rpnrules.cpp View File

@ -34,6 +34,7 @@ Copyright (C) 2019 by Xose Pérez <xose dot perez at gmail dot com>
// -----------------------------------------------------------------------------
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_context>();
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

+ 6
- 2
code/espurna/scheduler.cpp View File

@ -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);
}


+ 25
- 58
code/espurna/sensor.cpp View File

@ -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 <typename T>
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<const char*>(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;
}
}
}


+ 19
- 12
code/espurna/settings.cpp View File

@ -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) {


+ 8
- 0
code/espurna/settings_helpers.h View File

@ -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)
{


+ 10
- 2
code/espurna/telnet.cpp View File

@ -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;
}


+ 15
- 11
code/espurna/terminal.cpp View File

@ -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<String>(std::move(line));
espurnaRegisterOnce([cmd]() {
auto ptr = std::make_shared<String>(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);
}
});


+ 2
- 2
code/espurna/terminal_parsing.h View File

@ -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 {


+ 9
- 11
code/espurna/thermostat.cpp View File

@ -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();


+ 2
- 2
code/espurna/thingspeak.cpp View File

@ -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);
}


+ 19
- 5
code/espurna/types.cpp View File

@ -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

+ 8
- 9
code/espurna/types.h View File

@ -12,6 +12,8 @@ Copyright (C) 2019-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com
#include <memory>
#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 <typename T>
using remove_cvref = typename std::remove_cv<std::remove_reference<T>>::type;
template <typename T>
using is_callback = std::is_same<remove_cvref<T>, Callback>;
using is_callback = std::is_same<std::remove_cvref<T>, Callback>;
template <typename T>
using is_type = std::is_same<T, Type>;
@ -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*) {


+ 3
- 3
code/espurna/uartmqtt.cpp View File

@ -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;
}


+ 129
- 53
code/espurna/utils.cpp View File

@ -13,16 +13,127 @@ Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
#include <limits>
#include <random>
bool tryParseId(espurna::StringView value, TryParseIdFunc limit, size_t& out) {
static_assert(std::numeric_limits<size_t>::max() >= std::numeric_limits<unsigned long>::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<decltype(out)>::type;
static_assert(std::is_same<T, size_t>::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;
}


+ 9
- 4
code/espurna/utils.h View File

@ -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);

+ 2
- 0
code/espurna/web_utils.h View File

@ -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;


+ 22
- 0
code/test/unit/src/types/types.cpp View File

@ -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<std::nullptr_t, espurna::StringView>::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);


Loading…
Cancel
Save