Browse Source

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
pull/2584/head
Maxim Prokhorov 1 year ago
parent
commit
0e8611588b
8 changed files with 506 additions and 279 deletions
  1. +42
    -16
      code/espurna/relay.cpp
  2. +0
    -232
      code/espurna/relay_pulse.ipp
  3. +368
    -2
      code/espurna/settings.cpp
  4. +42
    -0
      code/espurna/settings.h
  5. +4
    -0
      code/espurna/settings_helpers.h
  6. +0
    -20
      code/espurna/system.cpp
  7. +0
    -9
      code/espurna/system.h
  8. +50
    -0
      code/espurna/terminal.cpp

+ 42
- 16
code/espurna/relay.cpp View File

@ -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<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<Duration>(build::time(index)));
return std::chrono::duration_cast<Duration>(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<pulse::Seconds>(result.duration());
const auto as_seconds =
std::chrono::duration_cast<pulse::Seconds>(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<bool>;
@ -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);
}


+ 0
- 232
code/espurna/relay_pulse.ipp View File

@ -16,238 +16,6 @@ namespace relay {
namespace pulse {
namespace {
struct Result {
Result() = default;
explicit Result(Duration duration) :
_duration(duration)
{}
template <typename T>
Result& operator+=(T duration) {
_result = true;
_duration += std::chrono::duration_cast<Duration>(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());
}


+ 368
- 2
code/espurna/settings.cpp View File

@ -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<duration::Microseconds>(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 <typename T>
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<duration::Seconds>(hours);
return result;
}
template <>
Pair& operator+=(Pair& result, duration::Minutes minutes) {
result.seconds += std::chrono::duration_cast<duration::Seconds>(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<float, std::ratio<1> >;
const auto seconds = Seconds(num * value);
const auto milliseconds =
std::chrono::duration_cast<duration::Milliseconds>(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 <typename T>
T unchecked_parse(StringView view) {
const auto result = duration_convert::parse(view, typename T::period{});
if (result.ok) {
return duration_convert::to_chrono_duration<T>(result.value);
}
return T{}.min();
}
} // namespace duration_convert
template <>
duration::Microseconds convert(const String& value) {
return duration_convert::unchecked_parse<duration::Microseconds>(value);
}
template <>
duration::Milliseconds convert(const String& value) {
return duration_convert::unchecked_parse<duration::Milliseconds>(value);
}
template <>
duration::Seconds convert(const String& value) {
return duration_convert::unchecked_parse<duration::Seconds>(value);
}
template <>
std::chrono::duration<float> convert(const String& value) {
return duration_convert::unchecked_parse<std::chrono::duration<float>>(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 <>


+ 42
- 0
code/espurna/settings.h View File

@ -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 <typename T, typename Rep = typename T::rep, typename Period = typename T::period>
std::chrono::duration<Rep, Period> to_chrono_duration(Pair result) {
using Type = std::chrono::duration<Rep, Period>;
return std::chrono::duration_cast<Type>(result.seconds)
+ std::chrono::duration_cast<Type>(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 <intmax_t Num, intmax_t Den>
Result parse(StringView view, std::ratio<Num, Den> ratio) {
return parse(view, Num, Den);
}
} // namespace duration_convert
template <typename T>
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<float> convert(const String&);
template <>
float convert(const String& value);


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

@ -140,6 +140,10 @@ struct ValueResult {
return moved;
}
espurna::StringView view() const {
return _value;
}
const String& ref() const {
return _value;
}


+ 0
- 20
code/espurna/system.cpp View File

@ -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<duration::Seconds::rep>(value));
}
String serialize(espurna::duration::Seconds value) {
return serialize(value.count());
}
template <>
std::chrono::duration<float> convert(const String& value) {
return std::chrono::duration<float>(convert<float>(value));
}
template <typename T>
T duration_convert(const String& value) {
return T{ convert<typename T::rep>(value) };
}
template <>
duration::Milliseconds convert(const String& value) {
return duration_convert<duration::Milliseconds>(value);
}
String serialize(espurna::duration::Milliseconds value) {
return serialize(value.count());
}


+ 0
- 9
code/espurna/system.h View File

@ -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<float> convert(const String&);
String serialize(heartbeat::Mode);
String serialize(duration::Seconds);
String serialize(duration::Milliseconds);


+ 50
- 0
code/espurna/terminal.cpp View File

@ -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<sleep::Microseconds>(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 <TIME (MICROSECONDS)>"));
return;
}
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<sleep::Microseconds>(result.value);
if (!instantDeepSleep(duration)) {
terminalError(ctx, F("Could not sleep"));
return;
}
}
PROGMEM_STRING(Reset, "RESET");
void reset(CommandContext&& ctx) {
@ -361,6 +408,9 @@ static constexpr ::terminal::Command List[] PROGMEM {
{Adc, commands::adc},
{LightSleep, commands::light_sleep},
{DeepSleep, commands::deep_sleep},
{Reset, commands::reset},
{EraseConfig, commands::erase_config},


Loading…
Cancel
Save