From 0e8611588b34eea15015c3bcc3473c464920376a Mon Sep 17 00:00:00 2001 From: Maxim Prokhorov Date: Sat, 11 Mar 2023 17:41:21 +0300 Subject: [PATCH] system: generic duration parser and sleep commands updated duration parser with a fixed type instead of just millis separate decimal and floating point converter as well, try to avoid possible precision loss with microseconds use --- code/espurna/relay.cpp | 58 +++-- code/espurna/relay_pulse.ipp | 232 -------------------- code/espurna/settings.cpp | 370 +++++++++++++++++++++++++++++++- code/espurna/settings.h | 42 ++++ code/espurna/settings_helpers.h | 4 + code/espurna/system.cpp | 20 -- code/espurna/system.h | 9 - code/espurna/terminal.cpp | 50 +++++ 8 files changed, 506 insertions(+), 279 deletions(-) diff --git a/code/espurna/relay.cpp b/code/espurna/relay.cpp index eb44eeb8..e0737aa0 100644 --- a/code/espurna/relay.cpp +++ b/code/espurna/relay.cpp @@ -327,8 +327,6 @@ enum class Mode { } // namespace relay } // namespace espurna -#include "relay_pulse.ipp" - namespace espurna { namespace relay { namespace pulse { @@ -385,13 +383,36 @@ PROGMEM_STRING(Mode, "relayPulse"); namespace { -Result time(size_t index) { - const auto time = espurna::settings::get(espurna::settings::Key{keys::Time, index}.value()); +using ParseResult = espurna::settings::internal::duration_convert::Result; + +Duration native_duration(ParseResult result) { + using namespace espurna::settings::internal; + + if (result.ok) { + return duration_convert::to_chrono_duration(result.value); + } + + return Duration::min(); +} + +ParseResult parse_time(StringView view) { + using namespace espurna::settings::internal; + return duration_convert::parse(view, Seconds::period{}); +} + +Duration native_duration(StringView view) { + return native_duration(parse_time(view)); +} + +Duration time(size_t index) { + const auto time = espurna::settings::get( + espurna::settings::Key{keys::Time, index}.value()); + if (!time) { - return Result(std::chrono::duration_cast(build::time(index))); + return std::chrono::duration_cast(build::time(index)); } - return parse(time.ref()); + return native_duration(time.view()); } Mode mode(size_t index) { @@ -1001,7 +1022,9 @@ ID_VALUE(delayOff, settings::delayOff) ID_VALUE(pulseMode, pulse::settings::mode) String pulseTime(size_t index) { const auto result = pulse::settings::time(index); - const auto as_seconds = std::chrono::duration_cast(result.duration()); + const auto as_seconds = + std::chrono::duration_cast(result); + return espurna::settings::internal::serialize(as_seconds.count()); } @@ -1637,10 +1660,11 @@ bool _relayHandlePulsePayload(size_t id, espurna::StringView payload) { return false; } - using namespace espurna::relay::pulse; - const auto pulse = parse(payload); - if (pulse) { - trigger(pulse.duration(), id, status); + using namespace espurna::relay::pulse::settings; + const auto pulse = parse_time(payload); + + if (pulse.ok) { + espurna::relay::pulse::trigger(native_duration(pulse), id, status); relayToggle(id, true, false); return true; @@ -2213,8 +2237,8 @@ void _relayConfigure() { relay.pulse = espurna::relay::pulse::settings::mode(id); relay.pulse_time = (relay.pulse != espurna::relay::pulse::Mode::None) - ? espurna::relay::pulse::settings::time(id).duration() - : espurna::duration::Milliseconds { 0 }; + ? espurna::relay::pulse::settings::time(id) + : espurna::relay::pulse::Duration::min(); relay.delay_on = espurna::relay::settings::delayOn(id); relay.delay_off = espurna::relay::settings::delayOff(id); @@ -2808,12 +2832,14 @@ static void _relayCommandPulse(::terminal::CommandContext&& ctx) { return; } - const auto pulse = espurna::relay::pulse::parse(ctx.argv[2]); - if (!pulse) { + const auto time = espurna::relay::pulse::settings::parse_time(ctx.argv[2]); + if (!time.ok) { terminalError(ctx, F("Invalid pulse time")); return; } + const auto duration = espurna::relay::pulse::settings::native_duration(time); + bool toggle = true; if (ctx.argv.size() == 4) { auto* convert= espurna::settings::internal::convert; @@ -2826,9 +2852,9 @@ static void _relayCommandPulse(::terminal::CommandContext&& ctx) { return; } - const auto duration = pulse.duration(); const auto target = toggle ? status : !status; espurna::relay::pulse::trigger(duration, id, target); + if ((duration.count() > 0) && toggle) { relayToggle(id, true, false); } diff --git a/code/espurna/relay_pulse.ipp b/code/espurna/relay_pulse.ipp index 6f02830f..e2b0f4b6 100644 --- a/code/espurna/relay_pulse.ipp +++ b/code/espurna/relay_pulse.ipp @@ -16,238 +16,6 @@ namespace relay { namespace pulse { namespace { -struct Result { - Result() = default; - explicit Result(Duration duration) : - _duration(duration) - {} - - template - Result& operator+=(T duration) { - _result = true; - _duration += std::chrono::duration_cast(duration); - return *this; - } - - explicit operator bool() const { - return _result; - } - - void reset() { - _result = false; - _duration = Duration::min(); - } - - Duration duration() const { - return _duration; - } - - Duration::rep count() const { - return _duration.count(); - } - -private: - bool _result { false }; - Duration _duration { Duration::min() }; -}; - -namespace internal { - -enum class Type { - Unknown, - Seconds, - Minutes, - Hours -}; - -bool validNextType(Type lhs, Type rhs) { - switch (lhs) { - case Type::Unknown: - return true; - case Type::Hours: - return (rhs == Type::Minutes) || (rhs == Type::Seconds); - case Type::Minutes: - return (rhs == Type::Seconds); - case Type::Seconds: - break; - } - - return false; -} - -Result parse(const char* begin, const char* end) { - Result out; - - String token; - Type last { Type::Unknown }; - Type type { Type::Unknown }; - - const char* ptr { begin }; - if (!begin || !end || (begin == end)) { - goto output; - } - -loop: - while (ptr != end) { - switch (*ptr) { - case '\0': - if (last == Type::Unknown) { - goto update_floating; - } - goto output; - case '0'...'9': - token += (*ptr); - ++ptr; - break; - case 'h': - if (validNextType(last, Type::Hours)) { - type = Type::Hours; - goto update_decimal; - } - goto reset; - case 'm': - if (validNextType(last, Type::Minutes)) { - type = Type::Minutes; - goto update_decimal; - } - goto reset; - case 's': - if (validNextType(last, Type::Seconds)) { - type = Type::Seconds; - goto update_decimal; - } - goto reset; - case ',': - case '.': - if (out) { - goto reset; - } - goto read_floating; - } - } - - if (token.length()) { - goto update_floating; - } - - goto output; - -update_floating: - { - char* endp { nullptr }; - auto value = strtod(token.c_str(), &endp); - if (endp && (endp != token.c_str()) && endp[0] == '\0') { - out += Seconds(value); - goto output; - } - - goto reset; - } - -update_decimal: - last = type; - ++ptr; - - if (type != Type::Unknown) { - const auto result = parseUnsigned(token, 10); - if (result.ok) { - switch (type) { - case Type::Hours: { - out += ::espurna::duration::Hours { result.value }; - break; - } - case Type::Minutes: { - out += ::espurna::duration::Minutes { result.value }; - break; - } - case Type::Seconds: { - out += ::espurna::duration::Seconds { result.value }; - break; - } - case Type::Unknown: - goto reset; - } - - type = Type::Unknown; - token = ""; - - goto loop; - } - } - - goto reset; - -read_floating: - switch (*ptr) { - case ',': - case '.': - token += '.'; - ++ptr; - break; - default: - goto reset; - } - - while (ptr != end) { - switch (*ptr) { - case '\0': - goto update_floating; - case '0'...'9': - token += (*ptr); - break; - case 'e': - case 'E': - token += (*ptr); - ++ptr; - - while (ptr != end) { - switch (*ptr) { - case '\0': - goto reset; - case '-': - case '+': - token += (*ptr); - ++ptr; - goto read_floating_exponent; - case '0'...'9': - goto read_floating_exponent; - } - } - - goto reset; - case ',': - case '.': - goto reset; - } - - ++ptr; - } - - goto update_floating; - -read_floating_exponent: - while (ptr != end) { - switch (*ptr) { - case '0'...'9': - token += *(ptr); - ++ptr; - break; - } - - goto reset; - } - - goto update_floating; - -reset: - out.reset(); - -output: - return out; -} - -} // namespace internal - Result parse(StringView value) { return internal::parse(value.begin(), value.end()); } diff --git a/code/espurna/settings.cpp b/code/espurna/settings.cpp index e8efe507..f85d7eee 100644 --- a/code/espurna/settings.cpp +++ b/code/espurna/settings.cpp @@ -190,15 +190,381 @@ void foreach_prefix(PrefixResultCallback&& callback, query::StringViewIterator p // -------------------------------------------------------------------------- namespace internal { +namespace duration_convert { +namespace { + +// Input is always normalized to Pair, specific units are converted on demand + +constexpr auto MicrosecondsPerSecond = + duration::Microseconds{ duration::Microseconds::period::den }; + +void adjust_microseconds(Pair& pair) { + if (pair.microseconds >= MicrosecondsPerSecond) { + pair.seconds += duration::Seconds{ 1 }; + pair.microseconds -= MicrosecondsPerSecond; + } +} + +Pair from_chrono_duration(duration::Microseconds microseconds) { + Pair out{}; + + while (microseconds > MicrosecondsPerSecond) { + out.seconds += duration::Seconds{ 1 }; + microseconds -= MicrosecondsPerSecond; + } + + out.microseconds += microseconds; + adjust_microseconds(out); + + return out; +} + +constexpr auto MillisecondsPerSecond = + duration::Milliseconds{ duration::Milliseconds::period::den }; + +Pair from_chrono_duration(duration::Milliseconds milliseconds) { + Pair out{}; + + while (milliseconds >= MillisecondsPerSecond) { + out.seconds += duration::Seconds{ 1 }; + milliseconds -= MillisecondsPerSecond; + } + + const auto microseconds = + std::chrono::duration_cast(milliseconds); + out.microseconds += microseconds; + adjust_microseconds(out); + + return out; +} + +Pair& operator+=(Pair& lhs, const Pair& rhs) { + lhs.seconds += rhs.seconds; + lhs.microseconds += rhs.microseconds; + + adjust_microseconds(lhs); + + return lhs; +} + +template +Pair& operator+=(Pair&, T); + +template <> +Pair& operator+=(Pair& result, duration::Microseconds microseconds) { + result += from_chrono_duration(microseconds); + return result; +} + +template <> +Pair& operator+=(Pair& result, duration::Milliseconds milliseconds) { + result += from_chrono_duration(milliseconds); + return result; +} + +template <> +Pair& operator+=(Pair& result, duration::Hours hours) { + result.seconds += std::chrono::duration_cast(hours); + return result; +} + +template <> +Pair& operator+=(Pair& result, duration::Minutes minutes) { + result.seconds += std::chrono::duration_cast(minutes); + return result; +} + +template <> +Pair& operator+=(Pair& result, duration::Seconds seconds) { + result.seconds += seconds; + return result; +} + +// Besides decimal or raw input with the specified ratio, +// string parser also supports type specifiers at the end of decimal number + +enum class Type { + Unknown, + Seconds, + Minutes, + Hours, +}; + +bool validNextType(Type lhs, Type rhs) { + switch (lhs) { + case Type::Unknown: + return true; + case Type::Hours: + return (rhs == Type::Minutes) || (rhs == Type::Seconds); + case Type::Minutes: + return (rhs == Type::Seconds); + case Type::Seconds: + break; + } + + return false; +} + +} // namespace + +Result parse(StringView view, int num, int den) { + Result out; + out.ok = false; + + String token; + Type last { Type::Unknown }; + Type type { Type::Unknown }; + + const char* ptr { view.begin() }; + if (!view.begin() || !view.length()) { + goto output; + } + +loop: + while (ptr != view.end()) { + switch (*ptr) { + case '0'...'9': + token += (*ptr); + ++ptr; + break; + + case 'h': + if (validNextType(last, Type::Hours)) { + type = Type::Hours; + goto update_spec; + } + goto reset; + + case 'm': + if (validNextType(last, Type::Minutes)) { + type = Type::Minutes; + goto update_spec; + } + goto reset; + + case 's': + if (validNextType(last, Type::Seconds)) { + type = Type::Seconds; + goto update_spec; + } + goto reset; + + case 'e': + case 'E': + goto read_floating_exponent; + + case ',': + case '.': + if (out.ok) { + goto reset; + } + + goto read_floating; + } + } + + if (token.length()) { + goto update_decimal; + } + + goto output; + +update_floating: + { + // only seconds and up, anything down of milli does not make sense here + if (den > 1) { + goto reset; + } + + char* endp { nullptr }; + auto value = strtod(token.c_str(), &endp); + if (endp && (endp != token.c_str()) && endp[0] == '\0') { + using Seconds = std::chrono::duration >; + + const auto seconds = Seconds(num * value); + const auto milliseconds = + std::chrono::duration_cast(seconds); + + out.value += milliseconds; + out.ok = true; + + goto output; + } + + goto reset; + } + +update_decimal: + { + const auto result = parseUnsigned(token, 10); + if (result.ok) { + // num and den are constexpr and bound to ratio types, so duration cast has to happen manually + if ((num == 1) && (den == 1)) { + out.value += duration::Seconds{ result.value }; + } else if ((num == 1) && (den > 1)) { + out.value += duration::Seconds{ result.value / den }; + out.value += duration::Microseconds{ result.value % den * duration::Microseconds::period::den / den }; + } else if ((num > 1) && (den == 1)) { + out.value += duration::Seconds{ result.value * num }; + } else { + goto reset; + } + + out.ok = true; + goto output; + } + + goto reset; + } + +update_spec: + last = type; + ++ptr; + + if (type != Type::Unknown) { + const auto result = parseUnsigned(token, 10); + if (result.ok) { + switch (type) { + case Type::Hours: + out.value += duration::Hours { result.value }; + break; + + case Type::Minutes: + out.value += duration::Minutes { result.value }; + break; + + case Type::Seconds: + out.value += duration::Seconds { result.value }; + break; + + case Type::Unknown: + goto reset; + } + + out.ok = true; + type = Type::Unknown; + token = ""; + + goto loop; + } + } + + goto reset; + +read_floating: + switch (*ptr) { + case ',': + case '.': + token += '.'; + ++ptr; + break; + + default: + goto reset; + } + + while (ptr != view.end()) { + switch (*ptr) { + case '0'...'9': + token += (*ptr); + break; + + case 'e': + case 'E': + goto read_floating_exponent; + + case ',': + case '.': + goto reset; + } + + ++ptr; + } + + goto update_floating; + +read_floating_exponent: + { + token += (*ptr); + ++ptr; + + bool sign { false }; + + while (ptr != view.end()) { + switch (*ptr) { + case '-': + case '+': + if (sign) { + goto reset; + } + + sign = true; + + token += (*ptr); + ++ptr; + break; + + case '0'...'9': + token += (*ptr); + ++ptr; + break; + + default: + goto reset; + } + } + + goto update_floating; + } + +reset: + out.ok = false; + +output: + return out; +} + +template +T unchecked_parse(StringView view) { + const auto result = duration_convert::parse(view, typename T::period{}); + + if (result.ok) { + return duration_convert::to_chrono_duration(result.value); + } + + return T{}.min(); +} + +} // namespace duration_convert + +template <> +duration::Microseconds convert(const String& value) { + return duration_convert::unchecked_parse(value); +} + +template <> +duration::Milliseconds convert(const String& value) { + return duration_convert::unchecked_parse(value); +} + +template <> +duration::Seconds convert(const String& value) { + return duration_convert::unchecked_parse(value); +} + +template <> +std::chrono::duration convert(const String& value) { + return duration_convert::unchecked_parse>(value); +} template <> float convert(const String& value) { - return atof(value.c_str()); + return strtod(value.c_str(), nullptr); } template <> double convert(const String& value) { - return atof(value.c_str()); + return strtod(value.c_str(), nullptr); } template <> diff --git a/code/espurna/settings.h b/code/espurna/settings.h index d96e4400..a9728767 100644 --- a/code/espurna/settings.h +++ b/code/espurna/settings.h @@ -91,10 +91,52 @@ void foreach_prefix(PrefixResultCallback&&, settings::query::StringViewIterator) // -------------------------------------------------------------------------- namespace internal { +namespace duration_convert { + +// A more losely typed duration, so we could have a single type +struct Pair { + duration::Seconds seconds{}; + duration::Microseconds microseconds{}; +}; + +struct Result { + Pair value; + bool ok { false }; +}; + +template +std::chrono::duration to_chrono_duration(Pair result) { + using Type = std::chrono::duration; + return std::chrono::duration_cast(result.seconds) + + std::chrono::duration_cast(result.microseconds); +} + +// Attempt to parse the given string with the specific ratio +// Same as chrono, std::ratio<1> is a second +Result parse(StringView, int num, int den); + +template +Result parse(StringView view, std::ratio ratio) { + return parse(view, Num, Den); +} + +} // namespace duration_convert template T convert(const String& value); +template <> +duration::Microseconds convert(const String&); + +template <> +duration::Milliseconds convert(const String&); + +template <> +duration::Seconds convert(const String&); + +template <> +std::chrono::duration convert(const String&); + template <> float convert(const String& value); diff --git a/code/espurna/settings_helpers.h b/code/espurna/settings_helpers.h index c3092a14..9c52ac98 100644 --- a/code/espurna/settings_helpers.h +++ b/code/espurna/settings_helpers.h @@ -140,6 +140,10 @@ struct ValueResult { return moved; } + espurna::StringView view() const { + return _value; + } + const String& ref() const { return _value; } diff --git a/code/espurna/system.cpp b/code/espurna/system.cpp index a341fd04..a1ad4dbd 100644 --- a/code/espurna/system.cpp +++ b/code/espurna/system.cpp @@ -102,30 +102,10 @@ String serialize(espurna::heartbeat::Mode mode) { return serialize(system::settings::options::HeartbeatModeOptions, mode); } -template <> -duration::Seconds convert(const String& value) { - return duration::Seconds(convert(value)); -} - String serialize(espurna::duration::Seconds value) { return serialize(value.count()); } -template <> -std::chrono::duration convert(const String& value) { - return std::chrono::duration(convert(value)); -} - -template -T duration_convert(const String& value) { - return T{ convert(value) }; -} - -template <> -duration::Milliseconds convert(const String& value) { - return duration_convert(value); -} - String serialize(espurna::duration::Milliseconds value) { return serialize(value.count()); } diff --git a/code/espurna/system.h b/code/espurna/system.h index 9ffc4f87..0fb09538 100644 --- a/code/espurna/system.h +++ b/code/espurna/system.h @@ -361,15 +361,6 @@ namespace internal { template <> heartbeat::Mode convert(const String&); -template <> -duration::Milliseconds convert(const String&); - -template <> -duration::Microseconds convert(const String&); - -template <> -std::chrono::duration convert(const String&); - String serialize(heartbeat::Mode); String serialize(duration::Seconds); String serialize(duration::Milliseconds); diff --git a/code/espurna/terminal.cpp b/code/espurna/terminal.cpp index 403371cf..9335c65b 100644 --- a/code/espurna/terminal.cpp +++ b/code/espurna/terminal.cpp @@ -83,6 +83,53 @@ void help(CommandContext&& ctx) { terminalOK(ctx); } +PROGMEM_STRING(LightSleep, "SLEEP.LIGHT"); + +void light_sleep(CommandContext&& ctx) { + if (ctx.argv.size() == 2) { + using namespace espurna::settings::internal::duration_convert; + + const auto result = parse(ctx.argv[1], std::micro{}); + if (!result.ok) { + terminalError(ctx, F("Invalid time")); + return; + } + + const auto duration = to_chrono_duration(result.value); + if (!instantLightSleep(duration)) { + terminalError(ctx, F("Could not sleep")); + return; + } + + return; + } + + instantLightSleep(); +} + +PROGMEM_STRING(DeepSleep, "SLEEP.DEEP"); + +void deep_sleep(CommandContext&& ctx) { + if (ctx.argv.size() != 2) { + terminalError(ctx, F("SLEEP.DEEP