Mirror of espurna firmware for wireless switches and more
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1371 lines
34 KiB

/*
RPN RULES MODULE
Use RPNLib library (https://github.com/xoseperez/rpnlib)
Copyright (C) 2019 by Xose Pérez <xose dot perez at gmail dot com>
*/
#include "espurna.h"
#if RPN_RULES_SUPPORT
#include <rpnlib.h>
#include "rpnrules.h"
#include "light.h"
#include "mqtt.h"
#include "ntp.h"
#include "ntp_timelib.h"
#include "relay.h"
#include "rfbridge.h"
#include "rpc.h"
#include "rtcmem.h"
#include "sensor.h"
#include "terminal.h"
#include "wifi.h"
#include "ws.h"
#include <forward_list>
#include <list>
#include <type_traits>
#include <vector>
// -----------------------------------------------------------------------------
namespace espurna {
namespace rpnrules {
namespace {
struct Runner {
enum class Policy {
OneShot,
Periodic
};
Runner() = default;
Runner(Policy policy, unsigned long period) :
_policy(policy),
_period(period),
_last(millis())
{}
Policy policy() const {
return _policy;
}
unsigned long period() const {
return _period;
}
unsigned long last() const {
return _last;
}
explicit operator bool() const {
return _expired;
}
bool match(Policy policy, unsigned long period) const {
return (policy == _policy) && (period == _period);
}
bool expired(unsigned long timestamp) {
if ((timestamp - _last) >= _period) {
_expired = true;
_last = timestamp;
return true;
}
return false;
}
void reset() {
_expired = false;
}
private:
Policy _policy { Policy::Periodic };
uint32_t _period { 0ul };
uint32_t _last { 0ul };
bool _expired { false };
};
// -----------------------------------------------------------------------------
namespace build {
constexpr bool sticky() {
return 1 == RPN_STICKY;
}
constexpr unsigned long delay() {
return RPN_DELAY;
}
} // namespace build
namespace settings {
STRING_VIEW_INLINE(Prefix, "rpn");
namespace keys {
PROGMEM_STRING(Sticky, "rpnSticky");
PROGMEM_STRING(Delay, "rpnDelay");
PROGMEM_STRING(Rule, "rpnRule");
PROGMEM_STRING(Topic, "rpnTopic");
PROGMEM_STRING(Name, "rpnName");
} // namespace keys
bool sticky() {
return getSetting(keys::Sticky, build::sticky());
}
unsigned long delay() {
return getSetting(keys::Delay, build::delay());
}
String rule(size_t index) {
return getSetting({keys::Rule, index});
}
String topic(size_t index) {
return getSetting({keys::Topic, index});
}
String name(size_t index) {
return getSetting({keys::Name, index});
}
} // namespace settings
namespace internal {
rpn_context context;
bool run = false;
unsigned long run_delay = 0;
unsigned long run_last = 0;
using Runners = std::forward_list<Runner>;
Runners runners;
} // namespace internal
void schedule() {
internal::run = true;
}
bool scheduled() {
return internal::run;
}
void reset(bool next) {
internal::run_last = millis();
internal::run = next;
}
void reset() {
reset(false);
}
bool due() {
if (scheduled()) {
auto timestamp = millis();
if (timestamp - internal::run_last > internal::run_delay) {
reset();
return true;
}
}
return false;
}
// enables us to use rules without any events firing, simply by having an internal timer scheduling the loop
// *MUST* run rules loop at least once (at boot, via external event, etc.), so the runners code is executed
struct RunnersHandler {
RunnersHandler() = delete;
RunnersHandler(const RunnersHandler&) = delete;
RunnersHandler& operator=(const RunnersHandler&) = delete;
RunnersHandler(RunnersHandler&&) = default;
RunnersHandler& operator=(RunnersHandler&&) = delete;
explicit RunnersHandler(internal::Runners& runners) :
_runners(runners)
{
auto ts = millis();
for (auto& runner : runners) {
if (runner.expired(ts)) {
schedule();
}
}
}
~RunnersHandler() {
_runners.remove_if([](const Runner& runner) {
return (Runner::Policy::OneShot == runner.policy()) && static_cast<bool>(runner);
});
for (auto& runner : _runners) {
runner.reset();
}
}
private:
internal::Runners& _runners;
};
// -----------------------------------------------------------------------------
#if TERMINAL_SUPPORT
namespace terminal {
String valueToString(const rpn_value& value) {
String out;
if (value.isString()) {
out = value.toString();
} else if (value.isFloat()) {
out = String(value.toFloat(), 10);
} else if (value.isInt()) {
out = String(value.toInt(), 10);
} else if (value.isUint()) {
out = String(value.toUint(), 10);
} else if (value.isBoolean()) {
out = String(value.toBoolean() ? "true" : "false");
} else if (value.isNull()) {
out = F("(null)");
}
return out;
}
char stackTypeTag(rpn_stack_value::Type type) {
switch (type) {
case rpn_stack_value::Type::None:
return 'N';
case rpn_stack_value::Type::Variable:
return '$';
case rpn_stack_value::Type::Array:
return 'A';
case rpn_stack_value::Type::Value:
default:
return ' ';
}
}
void showStack(Print& output) {
output.print(F("Stack:\n"));
auto index = rpn_stack_size(internal::context);
if (index) {
rpn_stack_foreach(internal::context, [&](rpn_stack_value::Type type, const rpn_value& value) {
output.printf_P(PSTR("%c %02u: %s\n"),
stackTypeTag(type), index--,
valueToString(value).c_str());
});
return;
}
output.print(F(" (empty)\n"));
}
PROGMEM_STRING(Runners, "RPN.RUNNERS");
void runners(::terminal::CommandContext&& ctx) {
if (internal::runners.empty()) {
terminalError(ctx, F("No active runners"));
return;
}
for (auto& runner : internal::runners) {
char buffer[128] = {0};
snprintf_P(buffer, sizeof(buffer),
PSTR("%p %s %u ms, last %u ms\n"),
&runner,
(Runner::Policy::Periodic == runner.policy())
? "every"
: "one-shot",
runner.period(), runner.last());
ctx.output.print(buffer);
}
terminalOK(ctx);
}
PROGMEM_STRING(Variables, "RPN.VARS");
void variables(::terminal::CommandContext&& ctx) {
rpn_variables_foreach(internal::context,
[&ctx](const String& name, const rpn_value& value) {
char buffer[256] = {0};
snprintf_P(buffer, sizeof(buffer),
PSTR(" %s: %s\n"),
name.c_str(), valueToString(value).c_str());
ctx.output.print(buffer);
});
terminalOK(ctx);
}
PROGMEM_STRING(Operators, "RPN.OPS");
void operators(::terminal::CommandContext&& ctx) {
rpn_operators_foreach(internal::context,
[&ctx](const String& name, size_t argc, rpn_operator::callback_type) {
char buffer[128] = {0};
snprintf_P(buffer, sizeof(buffer),
PSTR(" %s (%d)\n"),
name.c_str(), argc);
ctx.output.print(buffer);
});
terminalOK(ctx);
}
PROGMEM_STRING(Test, "RPN.TEST");
void test(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 2) {
terminalError(ctx, F("Wrong arguments"));
return;
}
const char* ptr = ctx.argv[1].c_str();
ctx.output.printf_P(PSTR("Expression: \"%s\"\n"), ctx.argv[1].c_str());
if (!rpn_process(internal::context, ptr)) {
rpn_stack_clear(internal::context);
char buffer[64] = {0};
snprintf_P(buffer, sizeof(buffer),
PSTR("at %u (category %d code %d)"),
internal::context.error.position,
static_cast<int>(internal::context.error.category),
internal::context.error.code);
terminalError(ctx, buffer);
return;
}
showStack(ctx.output);
rpn_stack_clear(internal::context);
terminalOK(ctx);
}
static constexpr ::terminal::Command Commands[] PROGMEM {
{Runners, runners},
{Variables, variables},
{Operators, operators},
{Test, test},
};
void setup() {
espurna::terminal::add(Commands);
}
} // namespace terminal
#endif // TERMINAL_SUPPORT
#if WEB_SUPPORT
namespace web {
#if MQTT_SUPPORT
static constexpr std::array<espurna::settings::query::IndexedSetting, 2> Settings PROGMEM {
{{settings::keys::Name, settings::name},
{settings::keys::Topic, settings::topic}}
};
size_t countMqttNames() {
size_t index { 0 };
for (;;) {
auto name = espurna::settings::Key(settings::keys::Name, index);
if (!espurna::settings::has(name.value())) {
break;
}
++index;
}
return index;
}
#endif
bool onKeyCheck(espurna::StringView key, const JsonVariant& value) {
return key.startsWith(settings::Prefix);
}
void onVisible(JsonObject& root) {
wsPayloadModule(root, settings::Prefix);
}
void onConnected(JsonObject& root) {
root[FPSTR(settings::keys::Sticky)] = rpnrules::settings::sticky();
root[FPSTR(settings::keys::Delay)] = rpnrules::settings::delay();
JsonArray& rules = root.createNestedArray(F("rpnRules"));
size_t index { 0 };
String rule;
for (;;) {
rule = rpnrules::settings::rule(index++);
if (!rule.length()) {
break;
}
rules.add(rule);
}
#if MQTT_SUPPORT
espurna::web::ws::EnumerableConfig config{ root, STRING_VIEW("rpnTopics") };
config(STRING_VIEW("topics"), countMqttNames(), Settings);
#endif
}
} // namespace web
#endif // WEB_SUPPORT
#if MQTT_SUPPORT
namespace mqtt {
struct Variable {
String name;
rpn_value value;
};
std::forward_list<Variable> variables;
void subscribe() {
size_t index { 0 };
String topic;
for(;;) {
topic = rpnrules::settings::topic(index++);
if (!topic.length()) {
break;
}
mqttSubscribeRaw(topic.c_str());
}
}
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 count { 0 };
String rpnTopic;
for (;;) {
const auto index = count++;
rpnTopic = rpnrules::settings::topic(index);
if (!rpnTopic.length()) {
break;
}
if (rpnTopic == topic) {
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 = std::move(value);
return;
}
}
variables.emplace_front(
Variable{
.name = std::move(name),
.value = std::move(value),
});
return;
}
}
return;
}
}
void init(rpn_context& context) {
mqttRegister(callback);
rpn_operator_set(context, "mqtt_send", 2, [](rpn_context& ctxt) -> rpn_error {
rpn_value message;
rpn_stack_pop(ctxt, message);
rpn_value topic;
rpn_stack_pop(ctxt, topic);
return ::mqttSendRaw(topic.toString().c_str(), message.toString().c_str())
? rpn_operator_error::Ok
: rpn_operator_error::CannotContinue;
});
}
} // namespace mqtt
#endif // MQTT_SUPPORT
namespace operators {
namespace runners {
rpn_operator_error handle(rpn_context& ctxt, Runner::Policy policy, unsigned long time) {
for (auto& runner : internal::runners) {
if (runner.match(policy, time)) {
return static_cast<bool>(runner)
? rpn_operator_error::Ok
: rpn_operator_error::CannotContinue;
}
}
internal::runners.emplace_front(policy, time);
return rpn_operator_error::CannotContinue;
}
void init(rpn_context& context) {
rpn_operator_set(context, "oneshot_ms", 1, [](rpn_context& ctxt) -> rpn_error {
auto every = rpn_stack_pop(ctxt);
return handle(ctxt, Runner::Policy::OneShot, every.toUint());
});
rpn_operator_set(context, "every_ms", 1, [](rpn_context & ctxt) -> rpn_error {
auto every = rpn_stack_pop(ctxt);
return handle(ctxt, Runner::Policy::Periodic, every.toUint());
});
}
} // namespace runners
#if NTP_SUPPORT
namespace ntp {
template <typename T, size_t Size>
using SplitType = std::integral_constant<bool, sizeof(T) == Size>;
using SplitTimestamp = SplitType<time_t, 8>;
static_assert((sizeof(time_t) == 4) || (sizeof(time_t) == 8), "");
constexpr size_t TimestampSize { SplitTimestamp{} ? 2 : 1 };
using TimestampFunc = rpn_int(*)(time_t);
rpn_error popTimestampPair(rpn_context& ctxt, TimestampFunc func) {
rpn_value rhs = rpn_stack_pop(ctxt);
rpn_value lhs = rpn_stack_pop(ctxt);
auto timestamp = (static_cast<long long>(lhs.toInt()) << 32ll)
| (static_cast<long long>(rhs.toInt()));
rpn_value value(func(timestamp));
rpn_stack_push(ctxt, value);
return 0;
}
rpn_error popTimestampSingle(rpn_context& ctxt, TimestampFunc func) {
rpn_value input = rpn_stack_pop(ctxt);
rpn_value result(func(input.toInt()));
rpn_stack_push(ctxt, result);
return 0;
}
void pushTimestampPair(rpn_context& ctxt, time_t timestamp) {
rpn_value lhs(static_cast<rpn_int>((static_cast<long long>(timestamp) >> 32ll) & 0xffffffffll));
rpn_value rhs(static_cast<rpn_int>(static_cast<long long>(timestamp) & 0xffffffffll));
rpn_stack_push(ctxt, lhs);
rpn_stack_push(ctxt, rhs);
}
void pushTimestampSingle(rpn_context& ctxt, time_t timestamp) {
rpn_value result(static_cast<rpn_int>(timestamp));
rpn_stack_push(ctxt, result);
}
inline rpn_error popTimestamp(const std::true_type&, rpn_context& ctxt, TimestampFunc func) {
return popTimestampPair(ctxt, func);
}
inline rpn_error popTimestamp(const std::false_type&, rpn_context& ctxt, TimestampFunc func) {
return popTimestampSingle(ctxt, func);
}
rpn_error popTimestamp(rpn_context& ctxt, TimestampFunc func) {
return popTimestamp(SplitTimestamp{}, ctxt, func);
}
inline void pushTimestamp(const std::true_type&, rpn_context& ctxt, time_t timestamp) {
pushTimestampPair(ctxt, timestamp);
}
inline void pushTimestamp(const std::false_type&, rpn_context& ctxt, time_t timestamp) {
pushTimestampSingle(ctxt, timestamp);
}
void pushTimestamp(rpn_context& ctxt, time_t timestamp) {
pushTimestamp(SplitTimestamp{}, ctxt, timestamp);
}
rpn_error now(rpn_context & ctxt) {
if (ntpSynced()) {
pushTimestamp(ctxt, ::now());
return 0;
}
return rpn_operator_error::CannotContinue;
}
rpn_error genericTimestampFunc(rpn_context & ctxt, TimestampFunc func) {
return popTimestamp(ctxt, func);
}
namespace internal {
bool tick_minute { false };
bool tick_hour { false };
} // namespace
rpn_error tickMinute(rpn_context& ctxt) {
if (internal::tick_minute) {
internal::tick_minute = false;
return 0;
}
return rpn_operator_error::CannotContinue;
}
rpn_error tickHour(rpn_context& ctxt) {
if (internal::tick_hour) {
internal::tick_hour = false;
return 0;
}
return rpn_operator_error::CannotContinue;
}
#define registerGenericTimestampOperator(context, name, func)\
rpn_operator_set(context, name, TimestampSize, [](rpn_context& ctxt) {\
return genericTimestampFunc(ctxt, func);\
})
void init(rpn_context& context) {
ntpOnTick([](NtpTick tick) {
switch (tick) {
case NtpTick::EveryMinute:
internal::tick_minute = true;
break;
case NtpTick::EveryHour:
internal::tick_hour = true;
break;
}
schedule();
});
rpn_operator_set(context, "tick_1h", 0, tickHour);
rpn_operator_set(context, "tick_1m", 0, tickMinute);
rpn_operator_set(context, "utc", 0, now);
rpn_operator_set(context, "now", 0, now);
registerGenericTimestampOperator(context, "utc_month", ::utc_month);
registerGenericTimestampOperator(context, "month", ::month);
registerGenericTimestampOperator(context, "utc_day", ::utc_day);
registerGenericTimestampOperator(context, "day", ::day);
registerGenericTimestampOperator(context, "utc_dow", ::utc_weekday);
registerGenericTimestampOperator(context, "dow", ::weekday);
registerGenericTimestampOperator(context, "utc_hour", ::utc_hour);
registerGenericTimestampOperator(context, "hour", ::hour);
registerGenericTimestampOperator(context, "utc_minute", ::utc_hour);
registerGenericTimestampOperator(context, "minute", ::hour);
}
#undef registerGenericTimestampOperator
} // namespace ntp
#endif // NTP_SUPPORT
#if RELAY_SUPPORT
namespace relay {
void updateVariables(size_t id, bool status) {
char name[32] = {0};
snprintf(name, sizeof(name), "relay%zu", id);
rpn_variable_set(internal::context, name, rpn_value(status));
schedule();
}
// Accept relay number (unsigned) and numeric API status value (unsigned - 0, 1 and 2)
rpn_error status(rpn_context & ctxt, bool force) {
rpn_value id;
rpn_value status;
rpn_stack_pop(ctxt, id);
rpn_stack_pop(ctxt, status);
rpn_uint value = status.toUint();
if (value == 2) {
::relayToggle(id.toUint());
} else if (::relayStatusTarget(id.toUint()) != (value == 1)) {
::relayStatus(id.toUint(), value == 1);
}
return 0;
}
void init(rpn_context& context) {
relayOnStatusChange(updateVariables);
// always apply status, allow to reset timers when called
rpn_operator_set(context, "relay_reset", 2, [](rpn_context& ctxt) {
return status(ctxt, true);
});
// only update status when target status differs, keep running timers
rpn_operator_set(context, "relay", 2, [](rpn_context& ctxt) {
return status(ctxt, false);
});
}
} // namespace relay
#endif // RELAY_SUPPORT
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
namespace light {
void updateVariables() {
auto channels = lightChannels();
char name[32] = {0};
for (decltype(channels) channel = 0; channel < channels; ++channel) {
auto value = rpn_value(static_cast<rpn_int>(lightChannel(channel)));
snprintf(name, sizeof(name), "channel%u", channel);
rpn_variable_set(internal::context, name, std::move(value));
}
schedule();
}
void init(rpn_context& context) {
lightOnReport(updateVariables);
rpn_operator_set(context, "update", 0, [](rpn_context& ctxt) -> rpn_error {
::lightUpdate();
return 0;
});
rpn_operator_set(context, "brightness", 0, [](rpn_context& ctxt) -> rpn_error {
rpn_value value { static_cast<rpn_int>(::lightBrightness()) };
rpn_stack_push(ctxt, value);
return 0;
});
rpn_operator_set(context, "set_brightness", 1, [](rpn_context& ctxt) -> rpn_error {
rpn_value value;
rpn_stack_pop(ctxt, value);
::lightBrightness(value.toInt());
return 0;
});
rpn_operator_set(context, "channel", 2, [](rpn_context& ctxt) -> rpn_error {
rpn_value id;
rpn_stack_pop(ctxt, id);
rpn_value value;
rpn_stack_pop(ctxt, value);
::lightChannel(id.toUint(), id.toInt());
return 0;
});
}
} // namespace light
#endif // LIGHT_PROVIDER
#if RFB_SUPPORT
namespace rfbridge {
struct Code {
unsigned char protocol;
String raw;
size_t count;
decltype(millis()) last;
};
struct Match {
unsigned char protocol;
String raw;
};
namespace build {
constexpr uint32_t RepeatWindow { 2000ul };
constexpr uint32_t MatchWindow { 2000ul };
constexpr uint32_t StaleDelay { 10000ul };
} // namespace build
namespace settings {
namespace keys {
PROGMEM_STRING(RepeatWindow, "rfbRepeatWindow");
PROGMEM_STRING(MatchWindow, "rfbWatchWindow");
PROGMEM_STRING(StaleDelay, "rfbStaleDelay");
} // namespace keys
uint32_t repeatWindow() {
return getSetting(keys::RepeatWindow, build::RepeatWindow);
}
uint32_t matchWindow() {
return getSetting(keys::MatchWindow, build::MatchWindow);
}
uint32_t staleDelay() {
return getSetting(keys::StaleDelay, build::StaleDelay);
}
} // namespace settings
namespace internal {
// TODO: in theory, we could do with forward_list. however, this would require a more complicated removal process,
// as we would no longer know the previous element and would need to track 2 elements at a time
using Codes = std::list<Code>;
Codes codes;
Codes::iterator find(Codes& container, unsigned char protocol, StringView match) {
return std::find_if(container.begin(), container.end(),
[protocol, &match](const Code& code) {
return (code.protocol == protocol) && (code.raw == match);
});
}
uint32_t repeat_window { build::RepeatWindow };
uint32_t match_window { build::MatchWindow };
uint32_t stale_delay { build::StaleDelay };
} // namespace internal
rpn_error sequence(rpn_context& ctxt) {
auto raw_second = rpn_stack_pop(ctxt);
auto proto_second = rpn_stack_pop(ctxt);
auto raw_first = rpn_stack_pop(ctxt);
auto proto_first = rpn_stack_pop(ctxt);
// find 2 codes in the same order and save pointers
Match match[2] {
{static_cast<unsigned char>(proto_first.toUint()), raw_first.toString()},
{static_cast<unsigned char>(proto_second.toUint()), raw_second.toString()}
};
Code* refs[2] {nullptr, nullptr};
for (auto& recent : internal::codes) {
if ((refs[0] != nullptr) && (refs[1] != nullptr)) {
break;
}
for (int index = 0; index < 2; ++index) {
if ((refs[index] == nullptr)
&& (match[index].protocol == recent.protocol)
&& (match[index].raw == recent.raw)) {
refs[index] = &recent;
}
}
}
if ((refs[0] == nullptr) || (refs[1] == nullptr)) {
return rpn_operator_error::CannotContinue;
}
// purge codes to avoid matching again on the next rules run
if ((millis() - refs[0]->last) > (millis() - refs[1]->last)) {
internal::codes.remove_if([&refs](Code& code) {
return (refs[0] == &code) || (refs[1] == &code);
});
return rpn_operator_error::Ok;
}
return rpn_operator_error::CannotContinue;
}
rpn_error sendCode(rpn_context& ctxt) {
auto code = rpn_stack_pop(ctxt);
if (!code.isString()) {
return rpn_operator_error::InvalidArgument;
}
::rfbSend(code.toString());
return rpn_operator_error::Ok;
}
rpn_error popCode(rpn_context& ctxt) {
auto code = rpn_stack_pop(ctxt);
auto proto = rpn_stack_pop(ctxt);
auto result = internal::find(internal::codes, proto.toUint(), code.toString());
if (result == internal::codes.end()) {
return rpn_operator_error::CannotContinue;
}
internal::codes.erase(result);
return rpn_operator_error::Ok;
}
rpn_error codeInfo(rpn_context& ctxt) {
auto code = rpn_stack_pop(ctxt);
auto proto = rpn_stack_pop(ctxt);
auto result = internal::find(internal::codes, proto.toUint(), code.toString());
if (result == internal::codes.end()) {
return rpn_operator_error::CannotContinue;
}
rpn_stack_push(ctxt, rpn_value(
static_cast<rpn_uint>((*result).count)));
rpn_stack_push(ctxt, rpn_value(
static_cast<rpn_uint>((*result).last)));
return rpn_operator_error::Ok;
}
rpn_error matchAndWait(rpn_context& ctxt) {
auto code = rpn_stack_pop(ctxt);
auto proto = rpn_stack_pop(ctxt);
auto count = rpn_stack_pop(ctxt);
auto time = rpn_stack_pop(ctxt);
auto result = internal::find(internal::codes, proto.toUint(), code.toString());
if (result == internal::codes.end()) {
return rpn_operator_error::CannotContinue;
}
if ((*result).count < count.toUint()) {
return rpn_operator_error::CannotContinue;
}
// purge code to avoid matching again on the next rules run
if (rpn_operator_error::Ok == operators::runners::handle(ctxt, Runner::Policy::OneShot, time.toUint())) {
internal::codes.erase(result);
return rpn_operator_error::Ok;
}
return rpn_operator_error::CannotContinue;
}
rpn_error match(rpn_context& ctxt) {
auto code = rpn_stack_pop(ctxt);
auto proto = rpn_stack_pop(ctxt);
auto count = rpn_stack_pop(ctxt);
auto result = internal::find(internal::codes, proto.toUint(), code.toString());
if (result == internal::codes.end()) {
return rpn_operator_error::CannotContinue;
}
// only process recent codes, ignore when rule is processing outside of this small window
if (millis() - (*result).last >= internal::match_window) {
return rpn_operator_error::CannotContinue;
}
// purge code to avoid matching again on the next rules run
if ((*result).count == count.toUint()) {
internal::codes.erase(result);
return rpn_operator_error::Ok;
}
return rpn_operator_error::CannotContinue;
}
void codeHandler(unsigned char protocol, StringView raw_code) {
// remove really old codes that we have not seen in a while to avoid memory exhaustion
auto ts = millis();
auto old = std::remove_if(internal::codes.begin(), internal::codes.end(), [ts](Code& code) {
return (ts - code.last) >= internal::stale_delay;
});
if (old != internal::codes.end()) {
internal::codes.erase(old, internal::codes.end());
}
auto result = internal::find(internal::codes, protocol, raw_code);
if (result != internal::codes.end()) {
// we also need to reset the counter at a certain point to allow next batch of repeats to go through
if (millis() - (*result).last >= internal::repeat_window) {
(*result).count = 0;
}
(*result).last = millis();
(*result).count += 1u;
} else {
internal::codes.push_back({protocol, raw_code.toString(), 1u, millis()});
}
schedule();
}
PROGMEM_STRING(RfbCodes, "RFB.CODES");
void rfb_codes(::terminal::CommandContext&& ctx) {
for (auto& code : internal::codes) {
char buffer[128] = {0};
snprintf_P(buffer, sizeof(buffer),
PSTR("proto=%u raw=\"%s\" count=%u last=%u\n"),
code.protocol, code.raw.c_str(), code.count, code.last);
ctx.output.print(buffer);
}
terminalOK(ctx);
}
static ::terminal::Command RfbCommands[] PROGMEM {
{RfbCodes, rfb_codes},
};
void init(rpn_context& context) {
// - Repeat window is an arbitrary time, just about 3-4 more times it takes for
// a code to be sent again when holding a generic remote button
// Code counter is reset to 0 when outside of the window.
// - Stale delay allows the handler to remove really old codes.
// (TODO: can this happen in loop() cb instead?)
internal::repeat_window = settings::repeatWindow();
internal::match_window = settings::matchWindow();
internal::stale_delay = settings::staleDelay();
#if TERMINAL_SUPPORT
espurna::terminal::add(RfbCommands);
#endif
// Main bulk of the processing goes on in here
::rfbOnCode(codeHandler);
// And codes can later be accessed by operators
rpn_operator_set(context, "rfb_send", 1, sendCode);
rpn_operator_set(context, "rfb_pop", 2, popCode);
rpn_operator_set(context, "rfb_info", 2, codeInfo);
rpn_operator_set(context, "rfb_sequence", 4, sequence);
rpn_operator_set(context, "rfb_match", 3, match);
rpn_operator_set(context, "rfb_match_wait", 4, matchAndWait);
}
} // namespace rfbridge
#endif // RFB_SUPPORT
#if SENSOR_SUPPORT
namespace sensor {
void updateVariables(const espurna::sensor::Value& value) {
static_assert(std::is_same<decltype(value.value), rpn_float>::value, "");
auto topic = value.topic;
topic.replace("/", "");
rpn_variable_set(internal::context,
topic, rpn_value(static_cast<rpn_float>(value.value)));
}
void init(rpn_context&) {
sensorOnMagnitudeRead(updateVariables);
}
} // namespace sensor
#endif // SENSOR_SUPPORT
#if DEBUG_SUPPORT
namespace debug {
void init(rpn_context& context) {
rpn_operator_set(context, "dbgmsg", 1, [](rpn_context & ctxt) -> rpn_error {
rpn_value message;
rpn_stack_pop(ctxt, message);
DEBUG_MSG_P(PSTR("[RPN] %s\n"), message.toString().c_str());
return 0;
});
}
} // namespace debug
#endif
namespace system {
using SystemSleepAction = bool(*)(sleep::Microseconds);
rpn_error with_sleep_duration(rpn_context& ctxt, SystemSleepAction action) {
auto value = rpn_stack_pop(ctxt).checkedToUint();
if (!value.ok()) {
return value.error();
}
if (!action(sleep::Microseconds{ value.value() })) {
return rpn_operator_error::CannotContinue;
}
return 0;
}
void init(rpn_context& context) {
rpn_operator_set(context, "delay", 1, [](rpn_context& ctxt) -> rpn_error {
auto ms = rpn_stack_pop(ctxt);
delay(ms.toUint());
return 0;
});
rpn_operator_set(context, "yield", 0, [](rpn_context& ctxt) -> rpn_error {
yield();
return 0;
});
rpn_operator_set(context, "reset", 0, [](rpn_context& ctxt) -> rpn_error {
static bool once = ([]() {
prepareReset(CustomResetReason::Rule);
return true;
})();
return once
? rpn_operator_error::CannotContinue
: rpn_operator_error::Ok;
});
rpn_operator_set(internal::context, "millis", 0, [](rpn_context & ctxt) -> rpn_error {
rpn_stack_push(ctxt, rpn_value(static_cast<uint32_t>(millis())));
return 0;
});
rpn_operator_set(context, "light_sleep", 1, [](rpn_context& ctxt) -> rpn_error {
return with_sleep_duration(ctxt, [](sleep::Microseconds time) -> bool {
return instantLightSleep(time);
});
});
rpn_operator_set(context, "deep_sleep", 1, [](rpn_context& ctxt) -> rpn_error {
return with_sleep_duration(ctxt, instantDeepSleep);
});
rpn_operator_set(context, "mem?", 0, [](rpn_context& ctxt) -> rpn_error {
rpn_stack_push(ctxt, rpn_value(::rtcmemStatus()));
return 0;
});
rpn_operator_set(context, "mem_write", 2, [](rpn_context& ctxt) -> rpn_error {
auto addr = rpn_stack_pop(ctxt).toUint();
auto value = rpn_stack_pop(ctxt).toUint();
if (addr < RTCMEM_BLOCKS) {
auto* rtcmem = reinterpret_cast<volatile uint32_t*>(RTCMEM_ADDR);
*(rtcmem + addr) = value;
return 0;
}
return rpn_operator_error::InvalidArgument;
});
rpn_operator_set(context, "mem_read", 1, [](rpn_context& ctxt) -> rpn_error {
auto addr = rpn_stack_pop(ctxt).toUint();
if (addr < RTCMEM_BLOCKS) {
auto* rtcmem = reinterpret_cast<volatile uint32_t*>(RTCMEM_ADDR);
rpn_uint result = *(rtcmem + addr);
rpn_stack_push(ctxt, rpn_value(result));
return 0;
}
return rpn_operator_error::InvalidArgument;
});
}
} // namespace system
namespace wifi {
void init(rpn_context& context) {
rpn_operator_set(context, "stations", 0, [](rpn_context& ctxt) -> rpn_error {
rpn_stack_push(ctxt, rpn_value {
static_cast<rpn_uint>(wifiApStations()) });
return 0;
});
rpn_operator_set(context, "disconnect", 0, [](rpn_context& ctxt) -> rpn_error {
wifiDisconnect();
yield();
return 0;
});
rpn_operator_set(context, "rssi", 0, [](rpn_context& ctxt) -> rpn_error {
const rpn_int rssi = wifiConnected()
? wifi_station_get_rssi()
: -127;
rpn_stack_push(ctxt, rpn_value { rssi });
return 0;
});
}
} // namespace wifi
void init(rpn_context& context) {
rpn_init(context);
runners::init(context);
system::init(context);
wifi::init(context);
#if DEBUG_SUPPORT
debug::init(context);
#endif
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
light::init(context);
#endif
#if MQTT_SUPPORT
mqtt::init(context);
#endif
#if NTP_SUPPORT
ntp::init(context);
#endif
#if RFB_SUPPORT
rfbridge::init(context);
#endif
#if RELAY_SUPPORT
relay::init(context);
#endif
#if SENSOR_SUPPORT
sensor::init(context);
#endif
}
} // namespace operators
void init() {
operators::init(internal::context);
// XXX: workaround for the vector 2x growth on push. will need to fix this in the rpnlib
internal::context.operators.shrink_to_fit();
DEBUG_MSG_P(PSTR("[RPN] Registered %u operators\n"), internal::context.operators.size());
}
void run() {
#if MQTT_SUPPORT
if (!mqtt::variables.empty()) {
schedule();
}
for (auto& variable : mqtt::variables) {
rpn_variable_set(internal::context, variable.name, variable.value);
}
mqtt::variables.clear();
#endif
if (!due()) {
return;
}
size_t index { 0 };
String rule;
for (;;) {
rule = settings::rule(index++);
if (!rule.length()) {
break;
}
rpn_process(internal::context, rule.c_str());
rpn_stack_clear(internal::context);
}
if (!settings::sticky()) {
rpn_variables_clear(internal::context);
}
}
void loop() {
RunnersHandler handler(internal::runners);
run();
}
void configure() {
#if MQTT_SUPPORT
if (mqttConnected()) {
mqtt::subscribe();
}
#endif
internal::run_delay = rpnrules::settings::delay();
}
void setup() {
init();
configure();
#if TERMINAL_SUPPORT
terminal::setup();
#endif
#if WEB_SUPPORT
wsRegister()
.onVisible(web::onVisible)
.onConnected(web::onConnected)
.onKeyCheck(web::onKeyCheck);
#endif
espurnaRegisterReload(configure);
espurnaRegisterLoop(loop);
reset(true);
}
} // namespace
} // namespace rpnrules
} // namespace espurna
void rpnSetup() {
espurna::rpnrules::setup();
}
#endif // RPN_RULES_SUPPORT