/*
|
|
|
|
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 "rpnrules.h"
|
|
|
|
#if RPN_RULES_SUPPORT
|
|
|
|
#include "broker.h"
|
|
#include "light.h"
|
|
#include "mqtt.h"
|
|
#include "ntp.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 <list>
|
|
#include <type_traits>
|
|
#include <vector>
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Custom commands
|
|
// -----------------------------------------------------------------------------
|
|
|
|
rpn_context _rpn_ctxt;
|
|
bool _rpn_run = false;
|
|
unsigned long _rpn_delay = RPN_DELAY;
|
|
unsigned long _rpn_last = 0;
|
|
|
|
struct RpnRunner {
|
|
enum class Policy {
|
|
OneShot,
|
|
Periodic
|
|
};
|
|
|
|
RpnRunner(Policy policy_, uint32_t period_) :
|
|
policy(policy_),
|
|
period(period_),
|
|
last(millis())
|
|
{}
|
|
|
|
Policy policy { Policy::Periodic };
|
|
|
|
uint32_t period { 0ul };
|
|
uint32_t last { 0ul };
|
|
|
|
bool expired { false };
|
|
};
|
|
|
|
std::vector<RpnRunner> _rpn_runners;
|
|
|
|
rpn_operator_error _rpnRunnerHandler(rpn_context & ctxt, RpnRunner::Policy policy, uint32_t time) {
|
|
for (auto& runner : _rpn_runners) {
|
|
if ((policy == runner.policy) && (time == runner.period)) {
|
|
return runner.expired
|
|
? rpn_operator_error::Ok
|
|
: rpn_operator_error::CannotContinue;
|
|
}
|
|
}
|
|
|
|
_rpn_runners.emplace_back(policy, time);
|
|
|
|
return rpn_operator_error::CannotContinue;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
bool _rpnWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
|
|
return (strncmp(key, "rpn", 3) == 0);
|
|
}
|
|
|
|
void _rpnWebSocketOnConnected(JsonObject& root) {
|
|
|
|
root["rpnSticky"] = getSetting("rpnSticky", 1 == RPN_STICKY);
|
|
root["rpnDelay"] = getSetting("rpnDelay", RPN_DELAY);
|
|
JsonArray& rules = root.createNestedArray("rpnRules");
|
|
|
|
unsigned char i = 0;
|
|
String rule = getSetting({"rpnRule", i});
|
|
while (rule.length()) {
|
|
rules.add(rule);
|
|
rule = getSetting({"rpnRule", ++i});
|
|
}
|
|
|
|
#if MQTT_SUPPORT
|
|
i=0;
|
|
JsonArray& topics = root.createNestedArray("rpnTopics");
|
|
JsonArray& names = root.createNestedArray("rpnNames");
|
|
String rpn_topic = getSetting({"rpnTopic", i});
|
|
while (rpn_topic.length() > 0) {
|
|
String rpn_name = getSetting({"rpnName", i});
|
|
topics.add(rpn_topic);
|
|
names.add(rpn_name);
|
|
rpn_topic = getSetting({"rpnTopic", ++i});
|
|
}
|
|
#endif
|
|
|
|
}
|
|
|
|
#if MQTT_SUPPORT
|
|
|
|
void _rpnMQTTSubscribe() {
|
|
unsigned char i = 0;
|
|
String rpn_topic = getSetting({"rpnTopic", i});
|
|
while (rpn_topic.length()) {
|
|
mqttSubscribeRaw(rpn_topic.c_str());
|
|
rpn_topic = getSetting({"rpnTopic", ++i});
|
|
}
|
|
}
|
|
|
|
void _rpnMQTTCallback(unsigned int type, const char * topic, const char * payload) {
|
|
|
|
if (type == MQTT_CONNECT_EVENT) {
|
|
_rpnMQTTSubscribe();
|
|
}
|
|
|
|
if (type == MQTT_MESSAGE_EVENT) {
|
|
unsigned char i = 0;
|
|
String rpn_topic = getSetting({"rpnTopic", i});
|
|
while (rpn_topic.length()) {
|
|
if (rpn_topic.equals(topic)) {
|
|
String rpn_name = getSetting({"rpnName", i});
|
|
if (rpn_name.length()) {
|
|
rpn_value value { atof(payload) };
|
|
rpn_variable_set(_rpn_ctxt, rpn_name, value);
|
|
_rpn_run = true;
|
|
break;
|
|
}
|
|
}
|
|
rpn_topic = getSetting({"rpnTopic", ++i});
|
|
}
|
|
}
|
|
|
|
}
|
|
#endif // MQTT_SUPPORT
|
|
|
|
void _rpnConfigure() {
|
|
#if MQTT_SUPPORT
|
|
if (mqttConnected()) _rpnMQTTSubscribe();
|
|
#endif
|
|
_rpn_delay = getSetting("rpnDelay", RPN_DELAY);
|
|
}
|
|
|
|
void _rpnBrokerCallback(const String& topic, unsigned char id, double value, const char*) {
|
|
|
|
char name[32] = {0};
|
|
snprintf(name, sizeof(name), "%s%u", topic.c_str(), id);
|
|
|
|
if (topic == MQTT_TOPIC_RELAY) {
|
|
rpn_variable_set(_rpn_ctxt, name, rpn_value(static_cast<bool>(value)));
|
|
} else {
|
|
rpn_variable_set(_rpn_ctxt, name, rpn_value(value));
|
|
}
|
|
|
|
_rpn_run = true;
|
|
|
|
}
|
|
|
|
void _rpnBrokerStatus(const String& topic, unsigned char id, unsigned int value) {
|
|
_rpnBrokerCallback(topic, id, double(value), nullptr);
|
|
}
|
|
|
|
#if NTP_SUPPORT
|
|
|
|
namespace {
|
|
|
|
constexpr bool time_t_is_32bit { sizeof(time_t) == 4 };
|
|
constexpr bool time_t_is_64bit { sizeof(time_t) == 8 };
|
|
static_assert(time_t_is_32bit || time_t_is_64bit, "");
|
|
|
|
template <typename T>
|
|
using split_t = std::integral_constant<bool, sizeof(T) == 8>;
|
|
|
|
using RpnNtpFunc = rpn_int(*)(time_t);
|
|
|
|
rpn_error _rpnNtpPopTimestampPair(rpn_context& ctxt, RpnNtpFunc 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 _rpnNtpPopTimestampSingle(rpn_context& ctxt, RpnNtpFunc func) {
|
|
rpn_value input = rpn_stack_pop(ctxt);
|
|
rpn_value result(func(input.toInt()));
|
|
rpn_stack_push(ctxt, result);
|
|
return 0;
|
|
}
|
|
|
|
void _rpnNtpPushTimestampPair(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 _rpnNtpPushTimestampSingle(rpn_context& ctxt, time_t timestamp) {
|
|
rpn_value result(static_cast<rpn_int>(timestamp));
|
|
rpn_stack_push(ctxt, result);
|
|
}
|
|
|
|
inline rpn_error _rpnNtpPopTimestamp(const std::true_type&, rpn_context& ctxt, RpnNtpFunc func) {
|
|
return _rpnNtpPopTimestampPair(ctxt, func);
|
|
}
|
|
|
|
inline rpn_error _rpnNtpPopTimestamp(const std::false_type&, rpn_context& ctxt, RpnNtpFunc func) {
|
|
return _rpnNtpPopTimestampSingle(ctxt, func);
|
|
}
|
|
|
|
rpn_error _rpnNtpPopTimestamp(rpn_context& ctxt, RpnNtpFunc func) {
|
|
return _rpnNtpPopTimestamp(split_t<time_t>{}, ctxt, func);
|
|
}
|
|
|
|
inline void _rpnNtpPushTimestamp(const std::true_type&, rpn_context& ctxt, time_t timestamp) {
|
|
_rpnNtpPushTimestampPair(ctxt, timestamp);
|
|
}
|
|
|
|
inline void _rpnNtpPushTimestamp(const std::false_type&, rpn_context& ctxt, time_t timestamp) {
|
|
_rpnNtpPushTimestampSingle(ctxt, timestamp);
|
|
}
|
|
|
|
void _rpnNtpPushTimestamp(rpn_context& ctxt, time_t timestamp) {
|
|
_rpnNtpPushTimestamp(split_t<time_t>{}, ctxt, timestamp);
|
|
}
|
|
|
|
rpn_error _rpnNtpNow(rpn_context & ctxt) {
|
|
if (ntpSynced()) {
|
|
_rpnNtpPushTimestamp(ctxt, now());
|
|
return 0;
|
|
}
|
|
|
|
return rpn_operator_error::CannotContinue;
|
|
}
|
|
|
|
rpn_error _rpnNtpFunc(rpn_context & ctxt, RpnNtpFunc func) {
|
|
return _rpnNtpPopTimestamp(ctxt, func);
|
|
}
|
|
|
|
bool _rpn_ntp_tick_minute { false };
|
|
bool _rpn_ntp_tick_hour { false };
|
|
|
|
rpn_error _rpnNtpTickMinute(rpn_context& ctxt) {
|
|
if (_rpn_ntp_tick_minute) {
|
|
_rpn_ntp_tick_minute = false;
|
|
return 0;
|
|
}
|
|
|
|
return rpn_operator_error::CannotContinue;
|
|
}
|
|
|
|
rpn_error _rpnNtpTickHour(rpn_context& ctxt) {
|
|
if (_rpn_ntp_tick_hour) {
|
|
_rpn_ntp_tick_hour = false;
|
|
return 0;
|
|
}
|
|
|
|
return rpn_operator_error::CannotContinue;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
#endif // NTP_SUPPORT
|
|
|
|
String _rpnValueToString(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 _rpnStackTypeTag(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 ' ';
|
|
}
|
|
}
|
|
|
|
#if RELAY_SUPPORT
|
|
|
|
rpn_error _rpnRelayStatus(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;
|
|
}
|
|
|
|
#endif // RELAY_SUPPORT
|
|
|
|
#if RFB_SUPPORT
|
|
|
|
struct rpn_rfbridge_code {
|
|
unsigned char protocol;
|
|
String raw;
|
|
size_t count;
|
|
decltype(millis()) last;
|
|
};
|
|
|
|
// 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
|
|
static std::list<rpn_rfbridge_code> _rfb_codes;
|
|
|
|
static uint32_t _rfb_code_repeat_window;
|
|
static uint32_t _rfb_code_stale_delay;
|
|
|
|
static uint32_t _rfb_code_match_window;
|
|
|
|
struct rpn_rfbridge_match {
|
|
unsigned char protocol;
|
|
String raw;
|
|
};
|
|
|
|
rpn_error _rpnRfbSequence(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
|
|
rpn_rfbridge_match match[2] {
|
|
{static_cast<unsigned char>(proto_first.toUint()), raw_first.toString()},
|
|
{static_cast<unsigned char>(proto_second.toUint()), raw_second.toString()}
|
|
};
|
|
rpn_rfbridge_code* refs[2] {nullptr, nullptr};
|
|
|
|
for (auto& recent : _rfb_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)) {
|
|
_rfb_codes.remove_if([&refs](rpn_rfbridge_code& code) {
|
|
return (refs[0] == &code) || (refs[1] == &code);
|
|
});
|
|
return rpn_operator_error::Ok;
|
|
}
|
|
|
|
return rpn_operator_error::CannotContinue;
|
|
}
|
|
|
|
decltype(_rfb_codes)::iterator _rpnRfbFindCode(unsigned char protocol, const String& match) {
|
|
return std::find_if(_rfb_codes.begin(), _rfb_codes.end(), [protocol, &match](const rpn_rfbridge_code& code) {
|
|
return (code.protocol == protocol) && (code.raw == match);
|
|
});
|
|
}
|
|
|
|
rpn_error _rpnRfbPop(rpn_context& ctxt) {
|
|
auto code = rpn_stack_pop(ctxt);
|
|
auto proto = rpn_stack_pop(ctxt);
|
|
|
|
auto result = _rpnRfbFindCode(proto.toUint(), code.toString());
|
|
if (result == _rfb_codes.end()) {
|
|
return rpn_operator_error::CannotContinue;
|
|
}
|
|
|
|
_rfb_codes.erase(result);
|
|
return rpn_operator_error::Ok;
|
|
}
|
|
|
|
rpn_error _rpnRfbInfo(rpn_context& ctxt) {
|
|
auto code = rpn_stack_pop(ctxt);
|
|
auto proto = rpn_stack_pop(ctxt);
|
|
|
|
auto result = _rpnRfbFindCode(proto.toUint(), code.toString());
|
|
if (result == _rfb_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 _rpnRfbWaitMatch(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 = _rpnRfbFindCode(proto.toUint(), code.toString());
|
|
if (result == _rfb_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 == _rpnRunnerHandler(ctxt, RpnRunner::Policy::OneShot, time.toUint())) {
|
|
_rfb_codes.erase(result);
|
|
return rpn_operator_error::Ok;
|
|
}
|
|
|
|
return rpn_operator_error::CannotContinue;
|
|
}
|
|
|
|
rpn_error _rpnRfbMatcher(rpn_context& ctxt) {
|
|
auto code = rpn_stack_pop(ctxt);
|
|
auto proto = rpn_stack_pop(ctxt);
|
|
auto count = rpn_stack_pop(ctxt);
|
|
|
|
auto result = _rpnRfbFindCode(proto.toUint(), code.toString());
|
|
if (result == _rfb_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 >= _rfb_code_match_window) {
|
|
return rpn_operator_error::CannotContinue;
|
|
}
|
|
|
|
// purge code to avoid matching again on the next rules run
|
|
if ((*result).count == count.toUint()) {
|
|
_rfb_codes.erase(result);
|
|
return rpn_operator_error::Ok;
|
|
}
|
|
|
|
return rpn_operator_error::CannotContinue;
|
|
}
|
|
|
|
void _rpnBrokerRfbridgeCallback(unsigned char protocol, const char* 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(_rfb_codes.begin(), _rfb_codes.end(), [ts](rpn_rfbridge_code& code) {
|
|
return (ts - code.last) >= _rfb_code_stale_delay;
|
|
});
|
|
|
|
if (old != _rfb_codes.end()) {
|
|
_rfb_codes.erase(old, _rfb_codes.end());
|
|
}
|
|
|
|
auto result = _rpnRfbFindCode(protocol, raw_code);
|
|
if (result != _rfb_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 >= _rfb_code_repeat_window) {
|
|
(*result).count = 0;
|
|
}
|
|
(*result).last = millis();
|
|
(*result).count += 1u;
|
|
} else {
|
|
_rfb_codes.push_back({protocol, raw_code, 1u, millis()});
|
|
}
|
|
|
|
_rpn_run = true;
|
|
}
|
|
|
|
void _rpnRfbSetup() {
|
|
// - 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 broker callback to remove really old codes.
|
|
// (TODO: can this happen in loop() cb instead?)
|
|
_rfb_code_repeat_window = getSetting("rfbRepeatWindow", 2000ul);
|
|
_rfb_code_match_window = getSetting("rfbMatchWindow", 2000ul);
|
|
_rfb_code_stale_delay = getSetting("rfbStaleDelay", 10000ul);
|
|
|
|
#if TERMINAL_SUPPORT
|
|
terminalRegisterCommand(F("RFB.CODES"), [](const terminal::CommandContext& ctx) {
|
|
for (auto& code : _rfb_codes) {
|
|
char buffer[128] = {0};
|
|
snprintf_P(buffer, sizeof(buffer),
|
|
PSTR("proto=%u raw=\"%s\" count=%u last=%u"),
|
|
code.protocol,
|
|
code.raw.c_str(),
|
|
code.count,
|
|
code.last
|
|
);
|
|
ctx.output.println(buffer);
|
|
}
|
|
});
|
|
#endif
|
|
|
|
// Main bulk of the processing goes on in here
|
|
RfbridgeBroker::Register(_rpnBrokerRfbridgeCallback);
|
|
}
|
|
|
|
#endif // RFB_SUPPORT
|
|
|
|
void _rpnShowStack(Print& print) {
|
|
print.println(F("Stack:"));
|
|
|
|
auto index = rpn_stack_size(_rpn_ctxt);
|
|
if (!index) {
|
|
print.println(F(" (empty)"));
|
|
return;
|
|
}
|
|
|
|
rpn_stack_foreach(_rpn_ctxt, [&index, &print](rpn_stack_value::Type type, const rpn_value& value) {
|
|
print.printf("%c %02u: %s\n",
|
|
_rpnStackTypeTag(type), index--,
|
|
_rpnValueToString(value).c_str()
|
|
);
|
|
});
|
|
}
|
|
|
|
void _rpnInit() {
|
|
|
|
// Init context
|
|
rpn_init(_rpn_ctxt);
|
|
|
|
// Time functions need NTP support
|
|
// TODO: since 1.15.0, timelib+ntpclientlib are no longer used with latest Cores
|
|
// `now` is always in UTC, `utc_...` functions to be used instead to convert time
|
|
#if NTP_SUPPORT && !NTP_LEGACY_SUPPORT
|
|
{
|
|
constexpr size_t time_t_argc { split_t<time_t>{} ? 2 : 1 };
|
|
|
|
rpn_operator_set(_rpn_ctxt, "tick_1h", 0, _rpnNtpTickHour);
|
|
rpn_operator_set(_rpn_ctxt, "tick_1m", 0, _rpnNtpTickMinute);
|
|
|
|
rpn_operator_set(_rpn_ctxt, "utc", 0, _rpnNtpNow);
|
|
rpn_operator_set(_rpn_ctxt, "now", 0, _rpnNtpNow);
|
|
|
|
rpn_operator_set(_rpn_ctxt, "utc_month", time_t_argc, [](rpn_context & ctxt) {
|
|
return _rpnNtpFunc(ctxt, utc_month);
|
|
});
|
|
rpn_operator_set(_rpn_ctxt, "month", time_t_argc, [](rpn_context & ctxt) {
|
|
return _rpnNtpFunc(ctxt, month);
|
|
});
|
|
|
|
rpn_operator_set(_rpn_ctxt, "utc_day", time_t_argc, [](rpn_context & ctxt) {
|
|
return _rpnNtpFunc(ctxt, utc_day);
|
|
});
|
|
rpn_operator_set(_rpn_ctxt, "day", time_t_argc, [](rpn_context & ctxt) {
|
|
return _rpnNtpFunc(ctxt, day);
|
|
});
|
|
|
|
rpn_operator_set(_rpn_ctxt, "utc_dow", time_t_argc, [](rpn_context & ctxt) {
|
|
return _rpnNtpFunc(ctxt, utc_weekday);
|
|
});
|
|
rpn_operator_set(_rpn_ctxt, "dow", time_t_argc, [](rpn_context & ctxt) {
|
|
return _rpnNtpFunc(ctxt, weekday);
|
|
});
|
|
|
|
rpn_operator_set(_rpn_ctxt, "utc_hour", time_t_argc, [](rpn_context & ctxt) {
|
|
return _rpnNtpFunc(ctxt, utc_hour);
|
|
});
|
|
rpn_operator_set(_rpn_ctxt, "hour", time_t_argc, [](rpn_context & ctxt) {
|
|
return _rpnNtpFunc(ctxt, hour);
|
|
});
|
|
|
|
rpn_operator_set(_rpn_ctxt, "utc_minute", time_t_argc, [](rpn_context & ctxt) {
|
|
return _rpnNtpFunc(ctxt, utc_minute);
|
|
});
|
|
rpn_operator_set(_rpn_ctxt, "minute", time_t_argc, [](rpn_context & ctxt) {
|
|
return _rpnNtpFunc(ctxt, minute);
|
|
});
|
|
}
|
|
#endif
|
|
|
|
// TODO: 1.14.0 weekday(...) conversion seemed to have 0..6 range with Monday as 0
|
|
// using classic Sunday as first, but instead of 0 it is 1
|
|
// Implementation above also uses 1 for Sunday, staying compatible with TimeLib
|
|
#if NTP_SUPPORT && NTP_LEGACY_SUPPORT
|
|
rpn_operator_set(_rpn_ctxt, "utc", 0, [](rpn_context & ctxt) -> rpn_error {
|
|
if (!ntpSynced()) return rpn_operator_error::CannotContinue;
|
|
rpn_value ts { static_cast<rpn_int>(ntpLocal2UTC(now())) };
|
|
rpn_stack_push(ctxt, ts);
|
|
return 0;
|
|
});
|
|
rpn_operator_set(_rpn_ctxt, "now", 0, _rpnNtpNow);
|
|
|
|
rpn_operator_set(_rpn_ctxt, "month", 1, [](rpn_context & ctxt) {
|
|
return _rpnNtpFunc(ctxt, month);
|
|
});
|
|
rpn_operator_set(_rpn_ctxt, "day", 1, [](rpn_context & ctxt) {
|
|
return _rpnNtpFunc(ctxt, day);
|
|
});
|
|
rpn_operator_set(_rpn_ctxt, "dow", 1, [](rpn_context & ctxt) {
|
|
return _rpnNtpFunc(ctxt, weekday);
|
|
});
|
|
rpn_operator_set(_rpn_ctxt, "hour", 1, [](rpn_context & ctxt) {
|
|
return _rpnNtpFunc(ctxt, hour);
|
|
});
|
|
rpn_operator_set(_rpn_ctxt, "minute", 1, [](rpn_context & ctxt) {
|
|
return _rpnNtpFunc(ctxt, minute);
|
|
});
|
|
#endif
|
|
|
|
// Accept relay number and numeric API status value (0, 1 and 2)
|
|
#if RELAY_SUPPORT
|
|
|
|
// apply status and reset timers when called
|
|
rpn_operator_set(_rpn_ctxt, "relay_reset", 2, [](rpn_context & ctxt) {
|
|
return _rpnRelayStatus(ctxt, true);
|
|
});
|
|
|
|
// only update status when target status differs, keep running timers
|
|
rpn_operator_set(_rpn_ctxt, "relay", 2, [](rpn_context & ctxt) {
|
|
return _rpnRelayStatus(ctxt, false);
|
|
});
|
|
|
|
#endif // RELAY_SUPPORT == 1
|
|
|
|
// Channel operators
|
|
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
|
|
|
|
rpn_operator_set(_rpn_ctxt, "update", 0, [](rpn_context & ctxt) -> rpn_error {
|
|
lightUpdate();
|
|
return 0;
|
|
});
|
|
|
|
rpn_operator_set(_rpn_ctxt, "black", 0, [](rpn_context & ctxt) -> rpn_error {
|
|
lightColor(0ul);
|
|
return 0;
|
|
});
|
|
|
|
rpn_operator_set(_rpn_ctxt, "channel", 2, [](rpn_context & ctxt) -> rpn_error {
|
|
rpn_value value;
|
|
rpn_value id;
|
|
rpn_stack_pop(ctxt, id);
|
|
rpn_stack_pop(ctxt, value);
|
|
lightChannel(id.toUint(), id.toInt());
|
|
return 0;
|
|
});
|
|
|
|
#endif
|
|
|
|
#if RFB_SUPPORT
|
|
rpn_operator_set(_rpn_ctxt, "rfb_pop", 2, _rpnRfbPop);
|
|
rpn_operator_set(_rpn_ctxt, "rfb_info", 2, _rpnRfbInfo);
|
|
rpn_operator_set(_rpn_ctxt, "rfb_sequence", 4, _rpnRfbSequence);
|
|
rpn_operator_set(_rpn_ctxt, "rfb_match", 3, _rpnRfbMatcher);
|
|
rpn_operator_set(_rpn_ctxt, "rfb_match_wait", 4, _rpnRfbWaitMatch);
|
|
#endif
|
|
|
|
#if MQTT_SUPPORT
|
|
rpn_operator_set(_rpn_ctxt, "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;
|
|
});
|
|
#endif
|
|
|
|
// Some debugging. Dump stack contents
|
|
#if TERMINAL_SUPPORT
|
|
rpn_operator_set(_rpn_ctxt, "showstack", 0, [](rpn_context & ctxt) -> rpn_error {
|
|
_rpnShowStack(terminalDefaultStream());
|
|
return 0;
|
|
});
|
|
#endif
|
|
|
|
// And, simple string logging
|
|
#if DEBUG_SUPPORT
|
|
rpn_operator_set(_rpn_ctxt, "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;
|
|
});
|
|
#endif
|
|
|
|
rpn_operator_set(_rpn_ctxt, "mem?", 0, [](rpn_context & ctxt) -> rpn_error {
|
|
rpn_stack_push(ctxt, rpn_value(rtcmemStatus()));
|
|
return 0;
|
|
});
|
|
|
|
rpn_operator_set(_rpn_ctxt, "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(_rpn_ctxt, "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;
|
|
});
|
|
|
|
rpn_operator_set(_rpn_ctxt, "sleep", 2, [](rpn_context & ctxt) -> rpn_error {
|
|
static bool once { false };
|
|
if (once) return rpn_operator_error::CannotContinue;
|
|
|
|
auto value = rpn_stack_pop(ctxt).checkedToUint();
|
|
if (!value.ok()) {
|
|
return value.error();
|
|
}
|
|
|
|
uint64_t duration = value.value();
|
|
if (!duration) {
|
|
return rpn_operator_error::CannotContinue;
|
|
}
|
|
|
|
auto mode = rpn_stack_pop(ctxt).toUint();
|
|
|
|
once = true;
|
|
schedule_function([duration, mode]() {
|
|
wifiTurnOff();
|
|
ESP.deepSleep(duration * 1000000ull, static_cast<RFMode>(mode));
|
|
});
|
|
|
|
return 0;
|
|
});
|
|
|
|
rpn_operator_set(_rpn_ctxt, "stations", 0, [](rpn_context & ctxt) -> rpn_error {
|
|
if (!(WiFi.getMode() & WIFI_AP)) return rpn_operator_error::CannotContinue;
|
|
rpn_stack_push(ctxt, rpn_value(static_cast<rpn_uint>(WiFi.softAPgetStationNum())));
|
|
return 0;
|
|
});
|
|
|
|
rpn_operator_set(_rpn_ctxt, "disconnect", 0, [](rpn_context & ctxt) -> rpn_error {
|
|
wifiDisconnect();
|
|
yield();
|
|
return 0;
|
|
});
|
|
|
|
rpn_operator_set(_rpn_ctxt, "rssi", 0, [](rpn_context & ctxt) -> rpn_error {
|
|
if (!wifiConnected()) return rpn_operator_error::CannotContinue;
|
|
rpn_stack_push(ctxt, rpn_value(static_cast<rpn_int>(WiFi.RSSI())));
|
|
return 0;
|
|
});
|
|
|
|
rpn_operator_set(_rpn_ctxt, "delay", 1, [](rpn_context & ctxt) -> rpn_error {
|
|
auto ms = rpn_stack_pop(ctxt);
|
|
delay(ms.toUint());
|
|
return 0;
|
|
});
|
|
|
|
rpn_operator_set(_rpn_ctxt, "yield", 0, [](rpn_context & ctxt) -> rpn_error {
|
|
yield();
|
|
return 0;
|
|
});
|
|
|
|
rpn_operator_set(_rpn_ctxt, "reset", 0, [](rpn_context & ctxt) -> rpn_error {
|
|
static bool once = ([]() {
|
|
deferredReset(100, CUSTOM_RESET_TERMINAL);
|
|
return true;
|
|
})();
|
|
return once
|
|
? rpn_operator_error::CannotContinue
|
|
: rpn_operator_error::Ok;
|
|
});
|
|
|
|
rpn_operator_set(_rpn_ctxt, "millis", 0, [](rpn_context & ctxt) -> rpn_error {
|
|
rpn_stack_push(ctxt, rpn_value(static_cast<uint32_t>(millis())));
|
|
return 0;
|
|
});
|
|
|
|
rpn_operator_set(_rpn_ctxt, "oneshot_ms", 1, [](rpn_context & ctxt) -> rpn_error {
|
|
auto every = rpn_stack_pop(ctxt);
|
|
return _rpnRunnerHandler(ctxt, RpnRunner::Policy::OneShot, every.toUint());
|
|
});
|
|
|
|
rpn_operator_set(_rpn_ctxt, "every_ms", 1, [](rpn_context & ctxt) -> rpn_error {
|
|
auto every = rpn_stack_pop(ctxt);
|
|
return _rpnRunnerHandler(ctxt, RpnRunner::Policy::Periodic, every.toUint());
|
|
});
|
|
|
|
// XXX: workaround for the vector 2x growth on push. will need to fix this in the rpnlib
|
|
_rpn_ctxt.operators.shrink_to_fit();
|
|
|
|
DEBUG_MSG_P(PSTR("[RPN] Registered %u operators\n"), _rpn_ctxt.operators.size());
|
|
|
|
}
|
|
|
|
#if TERMINAL_SUPPORT
|
|
|
|
void _rpnInitCommands() {
|
|
|
|
terminalRegisterCommand(F("RPN.RUNNERS"), [](const terminal::CommandContext& ctx) {
|
|
if (!_rpn_runners.size()) {
|
|
terminalError(ctx, F("No active runners"));
|
|
return;
|
|
}
|
|
|
|
for (auto& runner : _rpn_runners) {
|
|
char buffer[128] = {0};
|
|
snprintf_P(buffer, sizeof(buffer), PSTR("%p %s %u ms, last %u ms"),
|
|
&runner, (RpnRunner::Policy::Periodic == runner.policy) ? "every" : "one-shot",
|
|
runner.period, runner.last
|
|
);
|
|
ctx.output.println(buffer);
|
|
}
|
|
|
|
terminalOK(ctx);
|
|
});
|
|
|
|
terminalRegisterCommand(F("RPN.VARS"), [](const terminal::CommandContext& ctx) {
|
|
rpn_variables_foreach(_rpn_ctxt, [&ctx](const String& name, const rpn_value& value) {
|
|
char buffer[256] = {0};
|
|
snprintf_P(buffer, sizeof(buffer), PSTR(" %s: %s"), name.c_str(), _rpnValueToString(value).c_str());
|
|
ctx.output.println(buffer);
|
|
});
|
|
terminalOK(ctx);
|
|
});
|
|
|
|
terminalRegisterCommand(F("RPN.OPS"), [](const terminal::CommandContext& ctx) {
|
|
rpn_operators_foreach(_rpn_ctxt, [&ctx](const String& name, size_t argc, rpn_operator::callback_type) {
|
|
char buffer[128] = {0};
|
|
snprintf_P(buffer, sizeof(buffer), PSTR(" %s (%d)"), name.c_str(), argc);
|
|
ctx.output.println(buffer);
|
|
});
|
|
terminalOK(ctx);
|
|
});
|
|
|
|
terminalRegisterCommand(F("RPN.TEST"), [](const terminal::CommandContext& ctx) {
|
|
if (ctx.argc != 2) {
|
|
terminalError(F("Wrong arguments"));
|
|
return;
|
|
}
|
|
|
|
ctx.output.print(F("Running RPN expression: "));
|
|
ctx.output.println(ctx.argv[1].c_str());
|
|
|
|
if (!rpn_process(_rpn_ctxt, ctx.argv[1].c_str())) {
|
|
rpn_stack_clear(_rpn_ctxt);
|
|
char buffer[64] = {0};
|
|
snprintf_P(buffer, sizeof(buffer), PSTR("position=%u category=%d code=%d"),
|
|
_rpn_ctxt.error.position, static_cast<int>(_rpn_ctxt.error.category), _rpn_ctxt.error.code);
|
|
terminalError(ctx, buffer);
|
|
return;
|
|
}
|
|
|
|
_rpnShowStack(ctx.output);
|
|
rpn_stack_clear(_rpn_ctxt);
|
|
|
|
terminalOK(ctx);
|
|
});
|
|
|
|
}
|
|
#endif
|
|
|
|
// enables us to use rules without any events firing
|
|
// notice: requires rpnRun to trigger at least once so that we can install runners
|
|
void _rpnRunnersCheck() {
|
|
auto ts = millis();
|
|
for (auto& runner : _rpn_runners) {
|
|
if (ts - runner.last >= runner.period) {
|
|
runner.expired = true;
|
|
runner.last = ts;
|
|
_rpn_run = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void _rpnRunnersReset() {
|
|
auto old = std::remove_if(_rpn_runners.begin(), _rpn_runners.end(), [](RpnRunner& runner) {
|
|
return (RpnRunner::Policy::OneShot == runner.policy) && runner.expired;
|
|
});
|
|
|
|
if (old != _rpn_runners.end()) {
|
|
_rpn_runners.erase(old, _rpn_runners.end());
|
|
}
|
|
|
|
for (auto& runner : _rpn_runners) {
|
|
runner.expired = false;
|
|
}
|
|
}
|
|
|
|
void _rpnRun() {
|
|
|
|
if (!_rpn_run) {
|
|
return;
|
|
}
|
|
|
|
if (millis() - _rpn_last <= _rpn_delay) {
|
|
return;
|
|
}
|
|
|
|
_rpn_last = millis();
|
|
_rpn_run = false;
|
|
|
|
String rule;
|
|
unsigned char i = 0;
|
|
while ((rule = getSetting({"rpnRule", i++})).length()) {
|
|
rpn_process(_rpn_ctxt, rule.c_str());
|
|
rpn_stack_clear(_rpn_ctxt);
|
|
}
|
|
|
|
if (!getSetting("rpnSticky", 1 == RPN_STICKY)) {
|
|
rpn_variables_clear(_rpn_ctxt);
|
|
}
|
|
|
|
}
|
|
|
|
void _rpnLoop() {
|
|
|
|
_rpnRunnersCheck();
|
|
_rpnRun();
|
|
_rpnRunnersReset();
|
|
|
|
}
|
|
|
|
void rpnSetup() {
|
|
|
|
// Init context
|
|
_rpnInit();
|
|
|
|
// Load & cache settings
|
|
_rpnConfigure();
|
|
|
|
// Terminal commands
|
|
#if TERMINAL_SUPPORT
|
|
_rpnInitCommands();
|
|
#endif
|
|
|
|
// Websockets
|
|
#if WEB_SUPPORT
|
|
wsRegister()
|
|
.onVisible([](JsonObject& root) { root["rpnVisible"] = 1; })
|
|
.onConnected(_rpnWebSocketOnConnected)
|
|
.onKeyCheck(_rpnWebSocketOnKeyCheck);
|
|
#endif
|
|
|
|
// MQTT
|
|
#if MQTT_SUPPORT
|
|
mqttRegister(_rpnMQTTCallback);
|
|
#endif
|
|
|
|
#if NTP_SUPPORT
|
|
NtpBroker::Register([](NtpTick tick, time_t, const String&) {
|
|
switch (tick) {
|
|
case NtpTick::EveryMinute:
|
|
_rpn_ntp_tick_minute = true;
|
|
break;
|
|
case NtpTick::EveryHour:
|
|
_rpn_ntp_tick_hour = true;
|
|
break;
|
|
}
|
|
_rpn_run = true;
|
|
});
|
|
#endif
|
|
|
|
StatusBroker::Register(_rpnBrokerStatus);
|
|
|
|
#if RFB_SUPPORT
|
|
_rpnRfbSetup();
|
|
#endif
|
|
|
|
#if SENSOR_SUPPORT
|
|
SensorReadBroker::Register(_rpnBrokerCallback);
|
|
#endif
|
|
|
|
espurnaRegisterReload(_rpnConfigure);
|
|
espurnaRegisterLoop(_rpnLoop);
|
|
|
|
_rpn_last = millis();
|
|
_rpn_run = true;
|
|
|
|
}
|
|
|
|
#endif // RPN_RULES_SUPPORT
|