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.
 
 
 
 
 
 

2200 lines
59 KiB

/*
IR MODULE
Copyright (C) 2018 by Alexander Kolesnikov (raw and MQTT implementation)
Copyright (C) 2017-2019 by François Déchery
Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2020-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
For the library, see:
https://github.com/crankyoldgit/IRremoteESP8266
To (re)create the string -> Payload decoder .ipp files, add `re2c` to the $PATH and 'run' the environment:
```
$ pio run -e ... \
-t espurna/ir_parse_simple.re.ipp \
-t espurna/ir_parse_state.re.ipp \
-t espurna/ir_parse_raw.re.ipp
```
(see scripts/pio_pre.py and scripts/espurna_utils/build.py for more info)
*/
#include "espurna.h"
#if IR_SUPPORT
#include "ir.h"
#include "mqtt.h"
#include "relay.h"
#include "terminal.h"
#include <IRremoteESP8266.h>
#include <IRrecv.h>
#include <IRsend.h>
#include <IRutils.h>
#include <cstdint>
#include <cstring>
#include <queue>
#include <vector>
// TODO: current library version injects a bunch of stuff into the global scope:
// - `__STDC_LIMIT_MACROS`, forcing some c99 macros related to integer limits
// - `enum decode_type_t` with `UNKNOWN` and `UNUSED` symbols in it
// - various `const T k*` defined in the headers (...that are replacing preprocessor tokens :/)
// - various `ENABLE_...`, `SEND_...`, `DECODE_...`, and etc. preprocessor names
// (like `SEND_RAW` and `DECODE_HASH` for internal settings, or `DPRINT` when `DEBUG` is defined)
// ref. IRremoteESP8266.h, IRrecv.h and IRsend.h
//
// One solution is to patch upstream to have an optional `namespace irremoteesp8266 { ... }` wrapping everything related to the lib through a build flag, possibly versioned as well
// And, getting rid of accidentally exported C stuff in favour of C++ alternatives.
namespace ir {
namespace {
namespace tx {
// Notice that IRremoteEsp8266 includes a *lot* of built-in protocols. the suggested way to build the library
// is to append `-D_IR_ENABLE_DEFAULT_=false` to the build flags and specify the individual `-DSEND_...`
// and `-DDECODE_...` *only* for the required one(s)
//
// `-DIR_TX_SUPPORT=0` disables considerable amount of stuff linked inside of the `IRsend` class (~35KiB), but for
// every category of payloads at the same time; simple, raw and state will all be gone. *It is possible* to introduce
// a more granular control, but idk if it's really worth it (...and likely result in an inextricable web of `#if`s and `#ifdef`s)
#if not IR_TX_SUPPORT
struct NoopSender {
NoopSender(uint16_t, bool, bool) {
}
void begin() {
}
bool send(decode_type_t, const uint8_t*, uint16_t) {
return false;
}
bool send(decode_type_t, uint64_t, uint16_t, uint16_t) {
return false;
}
void sendRaw(const uint16_t*, uint16_t, uint16_t) {
}
};
#define IRsend ::ir::tx::NoopSender
#endif
struct PayloadSenderBase {
PayloadSenderBase() = default;
virtual ~PayloadSenderBase() = default;
PayloadSenderBase(const PayloadSenderBase&) = delete;
PayloadSenderBase& operator=(const PayloadSenderBase&) = delete;
PayloadSenderBase(PayloadSenderBase&&) = delete;
PayloadSenderBase& operator=(PayloadSenderBase&&) = delete;
virtual unsigned long delay() const = 0;
virtual bool send(IRsend& sender) const = 0;
virtual bool reschedule() = 0;
};
using PayloadSenderPtr = std::unique_ptr<PayloadSenderBase>;
namespace build {
// pin that the transmitter is attached to
constexpr unsigned char pin() {
return IR_TX_PIN;
}
// (optional) whether the LED will turn ON when GPIO is LOW and OFF when it's HIGH
// (disabled by default)
constexpr bool inverted() {
return IR_TX_INVERTED == 1;
}
// (optional) enable frequency modulation (ref. IRsend.h, enabled by default and assumes 50% duty cycle)
// (usage is 'hidden' by the protocol handlers, which use `::enableIROut(frequency, duty)`)
constexpr bool modulation() {
return IR_TX_MODULATION == 1;
}
// (optional) number of times that the message will be sent immediately
// (i.e. when the [:<repeats>] is omitted from the MQTT payload)
constexpr uint16_t repeats() {
return IR_TX_REPEATS;
}
// (optional) number of times that the message will be scheduled in the TX queue
// (i.e. when the [:<series>] is omitted from the MQTT payload)
constexpr uint8_t series() {
return IR_TX_SERIES;
}
// (ms)
constexpr unsigned long delay() {
return IR_TX_DELAY;
}
} // namespace build
namespace settings {
unsigned char pin() {
return getSetting("irTx", build::pin());
}
bool inverted() {
return getSetting("irTxInv", build::inverted());
}
bool modulation() {
return getSetting("irTxMod", build::modulation());
}
uint16_t repeats() {
return getSetting("rxTxRepeats", build::repeats());
}
uint8_t series() {
return getSetting("rxTxSeries", build::series());
}
unsigned long delay() {
return getSetting("irTxDelay", build::delay());
}
} // namespace settings
namespace internal {
uint16_t repeats { build::repeats() };
uint8_t series { build::series() };
unsigned long delay { build::delay() };
BasePinPtr pin;
std::unique_ptr<IRsend> instance;
std::queue<PayloadSenderPtr> queue;
} // namespace internal
} // namespace tx
namespace rx {
// `-DIR_RX_SUPPORT=0` disables everything related to the `IRrecv` class, ~20KiB
// (note that receiver objects are still techically there, just not doing anything)
#if not IR_RX_SUPPORT
struct NoopReceiver {
NoopReceiver(uint16_t, uint16_t, uint8_t, bool) {
}
bool decode(decode_results*) const {
return false;
}
void disableIRIn() const {
}
void enableIRIn() const {
}
void enableIRIn(bool) const {
}
};
#define IRrecv ::ir::rx::NoopReceiver
#endif
namespace build {
// pin that the receiver is attached to
constexpr unsigned char pin() {
return IR_RX_PIN;
}
// library handles both the isr timer and the pinMode in the same method,
// can't be set externally and must be passed into the `enableIRIn(bool)`
constexpr bool pullup() {
return IR_RX_PULLUP;
}
// internally, lib uses an u16[] of this size
constexpr uint16_t bufferSize() {
return IR_RX_BUFFER_SIZE;
}
// to be isr-friendly, will allocate second u16[]
// that will be used as a storage when decode()'ing
constexpr bool bufferSave() {
return true;
}
// allow unknown protocols to pass through to the processing
// (notice that this *will* cause RAW output to stop working)
constexpr bool unknown() {
return IR_RX_UNKNOWN;
}
// (ms)
constexpr uint8_t timeout() {
return IR_RX_TIMEOUT;
}
// (ms) minimal time in-between decode() calls
constexpr unsigned long delay() {
return IR_RX_DELAY;
}
} // namespace build
namespace settings {
// specific to the IRrecv
unsigned char pin() {
return getSetting("irRx", build::pin());
}
bool pullup() {
return getSetting("irRxPullup", build::pullup());
}
uint16_t bufferSize() {
return getSetting("irRxBufSize", build::bufferSize());
}
uint8_t timeout() {
return getSetting("irRxTimeout", build::timeout());
}
// local settings
bool unknown() {
return getSetting("irRxUnknown", build::unknown());
}
unsigned long delay() {
return getSetting("irRxDelay", build::delay());
}
} // namespace settings
namespace internal {
bool pullup { build::pullup() };
bool unknown { build::unknown() };
unsigned long delay { build::delay() };
BasePinPtr pin;
std::unique_ptr<IRrecv> instance;
} // namespace internal
} // namespace rx
// TODO: some -std=c++11 things. *almost* direct ports of std::string_view and std::optional
// (may be aliased via `using` and depend on the __cplusplus? string view is not 1-to-1 though)
// obviously, missing most of constexpr init and std::optional optimization related to trivial ctors & dtors
//
// TODO: since the exceptions are disabled, and parsing failure is not really an 'exceptional' result anyway...
// result struct may be in need of an additional struct describing the error, instead of just a boolean true or false
// (something like std::expected - http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0323r8.html)
// current implementation may be adjusted, but not the using DecodeResult = std::optional<T> mentioned above
struct StringView {
StringView() = delete;
~StringView() = default;
constexpr StringView(const StringView&) noexcept = default;
constexpr StringView(StringView&&) noexcept = default;
#if __cplusplus > 201103L
constexpr StringView& operator=(const StringView&) noexcept = default;
constexpr StringView& operator=(StringView&&) noexcept = default;
#else
StringView& operator=(const StringView&) noexcept = default;
StringView& operator=(StringView&&) noexcept = default;
#endif
constexpr StringView(std::nullptr_t) noexcept :
_begin(nullptr),
_length(0)
{}
constexpr StringView(const char* begin, size_t length) noexcept :
_begin(begin),
_length(length)
{}
constexpr StringView(const char* begin, const char* end) noexcept :
StringView(begin, end - begin)
{}
template <size_t Size>
constexpr StringView(const char (&string)[Size]) noexcept :
StringView(&string[0], Size - 1)
{}
StringView& operator=(const String& string) noexcept {
_begin = string.c_str();
_length = string.length();
return *this;
}
explicit StringView(const String& string) noexcept :
StringView(string.c_str(), string.length())
{}
template <size_t Size>
constexpr StringView& operator=(const char (&string)[Size]) noexcept {
_begin = &string[0];
_length = Size - 1;
return *this;
}
const char* begin() const noexcept {
return _begin;
}
const char* end() const noexcept {
return _begin + _length;
}
constexpr size_t length() const noexcept {
return _length;
}
explicit operator bool() const {
return _begin && _length;
}
explicit operator String() const {
String out;
out.concat(_begin, _length);
return out;
}
private:
const char* _begin;
size_t _length;
};
template <typename T>
struct ParseResult {
ParseResult() = default;
ParseResult(ParseResult&& other) noexcept {
if (other._initialized) {
init(std::move(other._result._value));
}
}
explicit ParseResult(T&& value) noexcept {
init(std::move(value));
}
~ParseResult() {
if (_initialized) {
_result._value.~T();
}
}
ParseResult(const ParseResult&) = delete;
ParseResult& operator=(const T& value) = delete;
ParseResult& operator=(T&& value) noexcept {
init(std::move(value));
return *this;
}
explicit operator bool() const noexcept {
return _initialized;
}
T* operator->() {
return &_result._value;
}
const T* operator->() const {
return &_result._value;
}
bool has_value() const noexcept {
return _initialized;
}
const T& value() const & {
return _result._value;
}
T& value() & {
return _result._value;
}
T&& value() && {
return std::move(_result._value);
}
const T&& value() const && {
return std::move(_result._value);
}
private:
struct Empty {
};
union Result {
Result() :
_empty()
{}
~Result() {
}
Result(const Result&) = delete;
Result(Result&&) = delete;
Result& operator=(const Result&) = delete;
Result& operator=(Result&&) = delete;
template <typename... Args>
Result(Args&&... args) :
_value(std::forward<Args>(args)...)
{}
template <typename... Args>
void update(Args&&... args) {
_value = T(std::forward<Args>(args)...);
}
Empty _empty;
T _value;
};
void reset() {
if (_initialized) {
_result._value.~T();
}
}
// TODO: c++ std compliance may enforce weird optimizations if T contains const or reference members, ref.
// - http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0532r0.pdf
// - https://gcc.gnu.org/bugzilla/show_bug.cgi?id=95349
template <typename... Args>
void init(Args&&... args) {
if (!_initialized) {
::new (&_result) Result(std::forward<Args>(args)...);
_initialized = true;
} else {
_result.update(std::forward<Args>(args)...);
}
}
bool _initialized { false };
Result _result;
};
// TODO: std::from_chars works directly with the view. not available with -std=c++11,
// and needs some care in regards to the code size
template <typename T>
T sized(StringView view) {
String value(view);
char* endp { nullptr };
unsigned long result { std::strtoul(value.c_str(), &endp, 10) };
if ((endp != value.c_str()) && (*endp == '\0')) {
constexpr unsigned long Boundary { 1ul << (sizeof(T) * 8) };
if (result < Boundary) {
return result;
}
}
return 0;
}
template <>
unsigned long sized(StringView view) {
String value(view);
char* endp { nullptr };
unsigned long result { std::strtoul(value.c_str(), &endp, 10) };
if ((endp != value.c_str()) && (*endp == '\0')) {
return result;
}
return 0;
}
// Simple messages that transmit the numeric 'value' (up to 8 bytes)
//
// Transmitting:
// Payload: <protocol>:<value>:<bits>[:<repeats>][:<delay>][:<times>]
//
// Required parameters:
// PROTOCOL - decimal ID, will be converted into a named 'decode_type_t'
// (ref. IRremoteESP8266.h and it's protocol descriptions)
// VALUE - hexadecimal representation of the value that will be sent
// (big endian, maximum 8bytes / 64bit. byte is always zero-padded)
// BITS - number of bits associated with the protocol
// (ref. IRremoteESP8266.h and it's protocol descriptions)
//
// Optional payload parameters:
// REPEATS - how many times the message will be sent immediatly
// (defaults to 0 or the value set by the PROTOCOL type)
// SERIES - how many times the message will be scheduled for sending
// (defaults to 1 aka once, [1...120))
// DELAY - minimum amount of time (ms) between queued messages
// (defaults is IR_TX_DELAY, applies to every message in the series)
//
// Receiving:
// Payload: 2:AABBCCDD:32 (<protocol>:<value>:<bits>)
// TODO: type is numeric based on the previous implementation. note that there are
// `::typeToString(decode_type_t)` and `::strToDecodeType(const char*)` (IRutils.h)
// And also see `const char kAllProtocolNames*`, which is a global from the IRtext header with
// \0-terminated chunks of stringivied decode_type_t (counting 'index' will deduce the type)
//
// (but, notice that str->type only works with C strings and *will* do a permissive
// `strToDecodeType(typeToString(static_cast<decode_type_t>(atoi(str))))` when the
// intial attempt fails)
namespace simple {
struct Payload {
decode_type_t type;
uint64_t value;
uint16_t bits;
uint16_t repeats;
uint8_t series;
unsigned long delay;
};
namespace value {
// TODO: endianness of input is always 'big', output is 'little'
// all esp platforms and common build hosts are 'little'
// but, actually make sure bswap is necessary?
// To convert from an existing decimal value, there is a python one-liner:
// >>> bytes(x for x in (123456789).to_bytes(8, 'big', signed=False) if x).hex()
// '075bcd15'
// (and also notice that old version *always* cast `u64` into `u32` which cut off part of the code)
uint64_t decode(StringView view) {
constexpr size_t RawSize { sizeof(uint64_t) };
constexpr size_t BufferSize { (RawSize * 2) + 1 };
if (!(view.length() % 2) && (view.length() < BufferSize)) {
char buffer[BufferSize] {0};
constexpr size_t BufferLen { BufferSize - 1 };
char* ZerolessOffset { std::begin(buffer) + BufferLen - view.length() };
std::fill(std::begin(buffer), ZerolessOffset, '0');
std::copy(view.begin(), view.end(), ZerolessOffset);
uint8_t raw[RawSize] {0};
if (::hexDecode(buffer, BufferLen, raw, sizeof(raw))) {
uint64_t output{0};
std::memcpy(&output, &raw, sizeof(raw));
return __builtin_bswap64(output);
}
}
return 0;
}
String encode(uint64_t input) {
String out;
if (input) {
const uint64_t Value { __builtin_bswap64(input) };
uint8_t raw[sizeof(Value)] {0};
std::memcpy(&raw, &Value, sizeof(raw));
uint8_t* begin { std::begin(raw) };
while (!(*begin)) {
++begin;
}
out = hexEncode(begin, std::end(raw));
} else {
out.concat(F("00"));
}
return out;
}
} // namespace value
namespace payload {
decode_type_t type(StringView view) {
static_assert(std::is_same<int, std::underlying_type<decode_type_t>::type>::value, "");
constexpr int First { -1 };
constexpr int Last { static_cast<int>(decode_type_t::kLastDecodeType) };
String value(view);
int result { value.toInt() };
if ((First < result) && (result < Last)) {
return static_cast<decode_type_t>(result);
}
return decode_type_t::UNKNOWN;
}
uint64_t value(StringView view) {
return ::ir::simple::value::decode(view);
}
uint16_t bits(StringView value) {
return sized<uint16_t>(value);
}
uint16_t repeats(StringView value) {
return sized<uint16_t>(value);
}
uint8_t series(StringView value) {
return sized<uint8_t>(value);
}
unsigned long delay(StringView value) {
return sized<unsigned long>(value);
}
template <typename T>
String encode(T& result) {
String out;
out.reserve(28);
out += static_cast<int>(result.type());
out += ':';
out += value::encode(result.value());
out += ':';
out += static_cast<unsigned int>(result.bits());
return out;
}
} // namespace payload
Payload prepare(StringView type, StringView value, StringView bits, StringView repeats, StringView series, StringView delay) {
Payload result;
result.type = payload::type(type);
result.value = payload::value(value);
result.bits = payload::bits(bits);
if (repeats) {
result.repeats = payload::repeats(repeats);
} else {
result.repeats = tx::internal::repeats;
}
if (series) {
result.series = payload::series(series);
} else {
result.series = tx::internal::series;
}
if (delay) {
result.delay = payload::delay(delay);
} else {
result.delay = tx::internal::delay;
}
return result;
}
#include "ir_parse_simple.re.ipp"
} // namespace simple
// Transmitting:
// Payload: <frequency>:<series>:<delay>:<μs>,<μs>,<μs>,<μs>,...
// | Options | | Message |
//
// FREQUENCY - modulation frequency, either in kHz (<1000) or Hz (>=1000)
// SERIES - how many times the message will be scheduled for sending
// [1...120)
// DELAY - minimum amount of time (ms) between queued messages
//
// Receiving:
// Payload: <μs>,<μs>,<μs>,<μs>,...
//
// The message is encoded as time in microseconds for the IR LED to be in a certain state.
// First one is always ON, and the second one - OFF.
// Also see IRutils.h's `String resultToTimingInfo(decode_results*)` for all of timing info, with a nice table output
// Not really applicable here, though
namespace raw {
static_assert((DECODE_HASH), "");
struct Payload {
uint16_t frequency;
uint8_t series;
unsigned long delay;
std::vector<uint16_t> time;
};
namespace time {
// TODO: compress / decompress with https://tasmota.github.io/docs/IRSend-RAW-Encoding/?
//
// Each rawbuf TIME value is:
// - multiplied by the TICK (old RAWTICK, currently kRawTick in a *global scope*)
// - rounded to the closest multiple of 5 (e.g. 299 becomes 300)
// - assigned an english alphabet letter ID (...or not, when exhausted all of 26 letters)
// Resulting payload contains TIME(μs) alternating between ON and OFF, starting with ON:
// - when first seen, output time directly prefixed with either '+' (ON) or '-' (OFF)
// - on further appearences, replace the time value with a letter that is uppercase for ON and lowercase for OFF
//
// For example, current implementation:
// > 100,200,100,200,200,300,300,300
// |A| |B| |A| |B| |B| |C| |C| |C|
// Becomes:
// > +100-200AbB-300Cc
// |A| |B| |C|
String encode(const uint16_t* begin, const uint16_t* end) {
static_assert((kRawTick == 2), "");
String out;
out.reserve((end - begin) * 5);
for (const uint16_t* it = begin; it != end; ++it) {
if (out.length()) {
out += ',';
}
out += String((*it) * kRawTick, 10);
}
return out;
}
} // namespace time
namespace payload {
uint16_t frequency(StringView value) {
return sized<uint16_t>(value);
}
uint8_t series(StringView value) {
return sized<uint8_t>(value);
}
unsigned long delay(StringView value) {
return sized<unsigned long>(value);
}
uint16_t time(StringView value) {
return sized<uint16_t>(value);
}
template <typename T>
String encode(T& result) {
auto raw = result.raw();
if (raw) {
return time::encode(raw.begin(), raw.end());
}
return F("0");
}
} // namespace payload
Payload prepare(StringView frequency, StringView series, StringView delay, decltype(Payload::time)&& time) {
Payload result;
result.frequency = payload::frequency(frequency);
result.series = payload::series(series);
result.delay = payload::delay(delay);
result.time = std::move(time);
return result;
}
#include "ir_parse_raw.re.ipp"
} // namespace raw
// TODO: current solution works directly with the internal 'u8 state[]', both for receiving and sending
// a more complex protocols for HVAC equipment *could* be handled by the IRacUtils (ref. IRac.h)
// where a generic 'IRac' class will convert certain common properties like temperature, fan speed,
// fan direction and power toggle (and some more, see 'stdAc::state_t'; or, the specific vendor class)
//
// Some problems with state_t, though:
// - not everything is 1-to-1 convertible with specific-protocol-AC-class to state_t
// (or not directly, or with some unexpected limitations)
// - there's no apparent way to know which properties are supported by the protocol.
// protocol-specific classes (e.g. MitsubishiAC) will convert to state_t by omitting certain fields,
// and parse it by ignoring them. but, this is hidden in the implementation
// - some protocols require previous state as a reference for sending, and IRac already has an internal copy
// if the state_t struct. but, notice that it is shared between protocols (as a generic way), so mixing
// protocols becomes are bit of a headache
// - size of the payload is as wide as the largest one, so there's always a static blob of N
// bytes reserved, both inside and with the proposed API of the library
// saving state (to avoid always resetting to defaults on reboot) also becomes a problem,
//
// For a generic solution, supporting state_t would mean to allow to set *every* property declared by the struct
// Common examples and libraries wrapping IRac prefer JSON payload, and both IRac and IRutils contain helpers to convert
// each property to and from strings.
//
// But, preper to split HVAC into a different module, as none of the queueing or generic code facilities are actually useful.
namespace state {
// State messages transmit an arbitrary amount of bytes, by using the assosicated protocol method
// Repeats are intended to be handled via the respective PROTOCOL method automatically
// (and, there's no reliable way besides linking every type with it's method from our side)
//
// Transmitting:
// Payload: <protocol>:<value>[:<series>][:<delay>]
//
// Required parameters:
// PROTOCOL - decimal ID, will be converted into a named 'decode_type_t'
// (ref. IRremoteESP8266.h and it's protocol descriptions)
// VALUE - hexadecimal representation of the value that will be sent
// (big endian, maximum depends on the protocol settings)
//
// Optional payload parameters:
// SERIES - how many times the message will be scheduled for sending
// (defaults to 1 aka once, [1...120))
// DELAY - minimum amount of time (ms) between queued messages
// (defaults is IR_TX_DELAY, applies to every message in the series)
//
// Receiving:
// Payload: 52:112233445566778899AABB (<protocol>:<value>)
static_assert(
sizeof(decltype(decode_results::state)) >= sizeof(decltype(decode_results::value)),
"Unsupported version of IRremoteESP8266");
using Value = std::vector<uint8_t>;
struct Payload {
decode_type_t type;
Value value;
uint8_t series;
unsigned long delay;
};
namespace value {
String encode(const uint8_t* begin, const uint8_t* end) {
return hexEncode(begin, end);
}
template <typename T>
String encode(T&& range) {
return hexEncode(range.begin(), range.end());
}
Value decode(StringView view) {
Value out;
if (!(view.length() % 2)) {
out.resize(view.length() / 2, static_cast<uint8_t>(0));
hexDecode(view.begin(), view.end(),
out.data(), out.data() + out.size());
}
return out;
}
} // namespace value
namespace payload {
template <typename T>
String encode(T& result) {
String out;
out.reserve(4 + (result.bytes() * 2));
out += static_cast<int>(result.type());
out += ':';
auto state = result.state();
out += value::encode(state.begin(), state.end());
return out;
}
} // namespace payload
Payload prepare(StringView type, StringView value, StringView series, StringView delay) {
Payload result;
result.type = ::ir::simple::payload::type(type);
result.value = value::decode(value);
if (series) {
result.series = simple::payload::series(series);
} else {
result.series = tx::internal::series;
}
if (delay) {
result.delay = simple::payload::delay(delay);
} else {
result.delay = tx::internal::delay;
}
return result;
}
#include "ir_parse_state.re.ipp"
} // namespace state
namespace rx {
struct Lock {
Lock(const Lock&) = delete;
Lock(Lock&&) = delete;
Lock& operator=(const Lock&) = delete;
Lock& operator=(Lock&&) = delete;
Lock() {
if (internal::instance) {
internal::instance->disableIRIn();
}
}
~Lock() {
if (internal::instance) {
internal::instance->enableIRIn(internal::pullup);
}
}
};
void configure() {
internal::delay = settings::delay();
internal::pullup = settings::pullup();
internal::unknown = settings::unknown();
}
void setup(BasePinPtr&& pin) {
internal::pin = std::move(pin);
internal::instance = std::make_unique<IRrecv>(
internal::pin->pin(),
settings::bufferSize(),
settings::timeout(),
build::bufferSave());
internal::instance->enableIRIn(internal::pullup);
}
// Current implementation relies on the HEX-encoded 'value' (previously, decimal)
//
// XXX: when protocol is UNKNOWN, `value` is silently replaced with a fnv1 32bit hash.
// can be disabled with `-DDECODE_HASH=0` in the build flags, but it will also
// cause RAW output to stop working, as the `IRrecv::decode()` can never succeed :/
//
// XXX: library utilizes union as a way to store the data, making this an interesting case
// of two-way aliasing inside of the struct. (and sometimes unexpected size requirements)
//
// At the time of writing, it is:
// > union {
// > struct {
// > uint64_t value; // Decoded value
// > uint32_t address; // Decoded device address.
// > uint32_t command; // Decoded command.
// > };
// > uint8_t state[kStateSizeMax]; // Multi-byte results.
// > };
//
// Where `kStateSizeMax` is either:
// - deduced from the largest protocol from the `DECODE_AC` group, *if* any of the protocols is enabled
// - otherwise, it's just `sizeof(uint64_t)`
// (i.e. only extra data is lost, as union members always start at the beginning of the struct)
// Also see IRutils.h's `String resultToHumanReadableBasic(decode_results*);` for type + value as a single line
struct DecodeResult {
template <typename T>
struct Range {
Range() = default;
Range(const T* begin, const T* end) :
_begin(begin),
_end(end)
{}
const T* begin() const {
return _begin;
}
const T* end() const {
return _end;
}
explicit operator bool() const {
return _begin && _end && (_begin < _end);
}
private:
const T* _begin { nullptr };
const T* _end { nullptr };
};
DecodeResult() = delete;
explicit DecodeResult(::decode_results& result) :
_result(result)
{}
decode_type_t type() const {
return _result.decode_type;
}
explicit operator bool() const {
return type() != decode_type_t::UNKNOWN;
}
uint16_t bits() const {
return _result.bits;
}
uint64_t value() const {
return _result.value;
}
// TODO: library examples (and some internal code, too) prefer this to be `bits() / 8`
size_t bytes() const {
const size_t Bits { bits() };
size_t need { 0 };
size_t out { 0 };
while (need < Bits) {
need += 8u;
out += 1u;
}
return out;
}
using Raw = Range<uint16_t>;
Raw raw() const {
if (_result.rawlen > 1) {
return Raw{
const_cast<const uint16_t*>(&_result.rawbuf[1]),
const_cast<const uint16_t*>(&_result.rawbuf[_result.rawlen])};
}
return {};
}
using State = Range<uint8_t>;
State state() const {
const size_t End { std::min(bytes(), sizeof(decltype(_result.state))) };
return State{
&_result.state[0],
&_result.state[End]};
}
private:
const ::decode_results& _result;
};
} // namespace rx
namespace tx {
// TODO: variant instead of virtuals?
struct ReschedulablePayload : public PayloadSenderBase {
static constexpr uint8_t SeriesMax { 120 };
ReschedulablePayload() = delete;
~ReschedulablePayload() = default;
ReschedulablePayload(const ReschedulablePayload&) = delete;
ReschedulablePayload& operator=(const ReschedulablePayload&) = delete;
ReschedulablePayload(ReschedulablePayload&&) = delete;
ReschedulablePayload& operator=(ReschedulablePayload&&) = delete;
ReschedulablePayload(uint8_t series, unsigned long delay) :
_series(std::min(series, SeriesMax)),
_delay(delay)
{}
bool reschedule() override {
return _series && (--_series);
}
unsigned long delay() const override {
return _delay;
}
protected:
size_t series() const {
return _series;
}
private:
uint8_t _series;
unsigned long _delay;
};
struct SimplePayloadSender : public ReschedulablePayload {
SimplePayloadSender() = delete;
explicit SimplePayloadSender(ir::simple::Payload&& payload) :
ReschedulablePayload(payload.series, payload.delay),
_payload(std::move(payload))
{}
bool send(IRsend& sender) const override {
return series() && sender.send(_payload.type, _payload.value, _payload.bits, _payload.repeats);
}
private:
ir::simple::Payload _payload;
};
struct StatePayloadSender : public ReschedulablePayload {
StatePayloadSender() = delete;
explicit StatePayloadSender(ir::state::Payload&& payload) :
ReschedulablePayload(
(payload.value.size() ? payload.series : 0), payload.delay),
_payload(std::move(payload))
{}
bool send(IRsend& sender) const override {
return series() && sender.send(_payload.type, _payload.value.data(), _payload.value.size());
}
private:
ir::state::Payload _payload;
};
struct RawPayloadSender : public ReschedulablePayload {
RawPayloadSender() = delete;
explicit RawPayloadSender(ir::raw::Payload&& payload) :
ReschedulablePayload(
(payload.time.size() ? payload.series : 0), payload.delay),
_payload(std::move(payload))
{}
bool send(IRsend& sender) const override {
if (series()) {
sender.sendRaw(_payload.time.data(), _payload.time.size(), _payload.frequency);
return true;
}
return false;
}
private:
ir::raw::Payload _payload;
};
namespace internal {
PayloadSenderPtr make_sender(ir::simple::Payload&& payload) {
return std::make_unique<SimplePayloadSender>(std::move(payload));
}
PayloadSenderPtr make_sender(ir::state::Payload&& payload) {
return std::make_unique<StatePayloadSender>(std::move(payload));
}
PayloadSenderPtr make_sender(ir::raw::Payload&& payload) {
return std::make_unique<RawPayloadSender>(std::move(payload));
}
void enqueue(PayloadSenderPtr&& sender) {
queue.push(std::move(sender));
}
} // namespace internal
template <typename T>
bool enqueue(typename ir::ParseResult<T>&& result) {
if (result) {
internal::enqueue(internal::make_sender(std::move(result).value()));
return true;
}
return false;
}
void loop() {
if (internal::queue.empty()) {
return;
}
auto& payload = internal::queue.front();
static unsigned long last { millis() - payload->delay() - 1ul };
const unsigned long timestamp { millis() };
if (timestamp - last < payload->delay()) {
return;
}
last = timestamp;
rx::Lock lock;
if (!payload->send(*internal::instance)) {
internal::queue.pop();
return;
}
if (!payload->reschedule()) {
internal::queue.pop();
}
}
void configure() {
internal::delay = settings::delay();
internal::series = settings::series();
internal::repeats = settings::repeats();
}
void setup(BasePinPtr&& pin) {
internal::pin = std::move(pin);
internal::instance = std::make_unique<IRsend>(
internal::pin->pin(),
settings::inverted(),
settings::modulation());
internal::instance->begin();
}
} // namespace tx
#if MQTT_SUPPORT
namespace mqtt {
namespace build {
// (optional) enables simple protocol MQTT rx output
constexpr bool rxSimple() {
return IR_RX_SIMPLE_MQTT == 1;
}
// (optional) enables MQTT RAW rx output (i.e. time values that we received so far)
constexpr bool rxRaw() {
return IR_RX_RAW_MQTT == 1;
}
// (optional) enables MQTT state rx output (commonly, HVAC remotes, or anything that has payload larger than 64bit)
// (*may need* increased timeout setting for the receiver, so it could buffer very large messages consistently and not lose some of the parts)
// (*requires* increase buffer size. but, depends on the protocol, so adjust accordingly)
constexpr bool rxState() {
return IR_RX_STATE_MQTT == 1;
}
// {root}/{topic}
const char* topicRxSimple() {
return IR_RX_SIMPLE_MQTT_TOPIC;
}
const char* topicTxSimple() {
return IR_TX_SIMPLE_MQTT_TOPIC;
}
const char* topicRxRaw() {
return IR_RX_RAW_MQTT_TOPIC;
}
const char* topicTxRaw() {
return IR_TX_RAW_MQTT_TOPIC;
}
const char* topicRxState() {
return IR_RX_STATE_MQTT_TOPIC;
}
const char* topicTxState() {
return IR_TX_STATE_MQTT_TOPIC;
}
} // namespace build
namespace settings {
bool rxSimple() {
return getSetting("irRxMqtt", build::rxSimple());
}
bool rxRaw() {
return getSetting("irRxMqttRaw", build::rxRaw());
}
bool rxState() {
return getSetting("irRxMqttState", build::rxState());
}
} // namespace settings
namespace internal {
bool publish_raw { build::rxRaw() };
bool publish_simple { build::rxSimple() };
bool publish_state { build::rxState() };
void callback(unsigned int type, const char* topic, char* payload) {
switch (type) {
case MQTT_CONNECT_EVENT:
mqttSubscribe(build::topicTxSimple());
mqttSubscribe(build::topicTxState());
mqttSubscribe(build::topicTxRaw());
break;
case MQTT_MESSAGE_EVENT: {
StringView view{payload, payload + strlen(payload)};
String t = mqttMagnitude(topic);
if (t.equals(build::topicTxSimple())) {
ir::tx::enqueue(ir::simple::parse(view));
} else if (t.equals(build::topicTxState())) {
ir::tx::enqueue(ir::state::parse(view));
} else if (t.equals(build::topicTxRaw())) {
ir::tx::enqueue(ir::raw::parse(view));
}
break;
}
}
}
} // namespace internal
void process(rx::DecodeResult& result) {
if (internal::publish_state && result && (result.bytes() > 8)) {
::mqttSend(build::topicRxState(), ::ir::state::payload::encode(result).c_str());
} else if (internal::publish_simple) {
::mqttSend(build::topicRxSimple(), ::ir::simple::payload::encode(result).c_str());
}
if (internal::publish_raw) {
::mqttSend(build::topicRxRaw(), ::ir::raw::payload::encode(result).c_str());
}
}
void configure() {
internal::publish_raw = settings::rxRaw();
internal::publish_simple = settings::rxSimple();
internal::publish_state = settings::rxState();
}
void setup() {
mqttRegister(internal::callback);
}
} // namespace mqtt
#endif
#if TERMINAL_SUPPORT
namespace terminal {
struct ValueCommand {
const __FlashStringHelper* value;
const __FlashStringHelper* command;
};
struct Preset {
const ValueCommand* const begin;
const ValueCommand* const end;
};
namespace build {
// TODO: optimize the array itself via PROGMEM? can't be static though, b/c F(...) will be resolved later and the memory is empty in the flash
// also note of the alignment requirements that don't always get applied to a simple PROGMEM'ed array (unless explicitly set, or the contained value is aligned)
// strings vs. number for values do have a slight overhead (x2 pointers, byte-by-byte cmp instead of a 2byte memcmp), but it seems to be easier to handle here
// but... this also means it *could* seamlessly handle state payloads just as simple values, just by changing the value retrieval function
// TODO: have an actual name for presets (remote, device, etc.)?
// TODO: user-defined presets?
// TODO: pub-sub through terminal?
// Replaced old ir_button.h IR_BUTTON_ACTION_... with an appropriate terminal command
// Unlike the RFbridge implementation, does not depend on the RELAY_SUPPORT and it's indexes
#if IR_RX_PRESET != 0
Preset preset() {
#if IR_RX_PRESET == 1
// For the original Remote shipped with the controller
// +------+------+------+------+
// | UP | Down | OFF | ON |
// +------+------+------+------+
// | R | G | B | W |
// +------+------+------+------+
// | 1 | 2 | 3 |FLASH |
// +------+------+------+------+
// | 4 | 5 | 6 |STROBE|
// +------+------+------+------+
// | 7 | 8 | 9 | FADE |
// +------+------+------+------+
// | 10 | 11 | 12 |SMOOTH|
// +------+------+------+------+
static const std::array<ValueCommand, 20> instance {
{{F("FF906F"), F("brightness +10")},
{F("FFB847"), F("brightness -10")},
{F("FFF807"), F("light off")},
{F("FFB04F"), F("light on")},
{F("FF9867"), F("rgb #FF0000")},
{F("FFD827"), F("rgb #00FF00")},
{F("FF8877"), F("rgb #0000FF")},
{F("FFA857"), F("rgb #FFFFFF")},
{F("FFE817"), F("rgb #D13A01")},
{F("FF48B7"), F("rgb #00E644")},
{F("FF6897"), F("rgb #0040A7")},
//{F("FFB24D"), F("effect flash")},
{F("FF02FD"), F("rgb #E96F2A")},
{F("FF32CD"), F("rgb #00BEBF")},
{F("FF20DF"), F("rgb #56406F")},
//{F("FF00FF"), F("effect strobe")},
{F("FF50AF"), F("rgb #EE9819")},
{F("FF7887"), F("rgb #00799A")},
{F("FF708F"), F("rgb #944E80")},
//{F("FF58A7"), F("effect fade")},
{F("FF38C7"), F("rgb #FFFF00")},
{F("FF28D7"), F("rgb #0060A1")},
{F("FFF00F"), F("rgb #EF45AD")}}
//{F("FF30CF"), F("effect smooth")}
};
#elif IR_RX_PRESET == 2
// Another identical IR Remote shipped with another controller
// +------+------+------+------+
// | UP | Down | OFF | ON |
// +------+------+------+------+
// | R | G | B | W |
// +------+------+------+------+
// | 1 | 2 | 3 |FLASH |
// +------+------+------+------+
// | 4 | 5 | 6 |STROBE|
// +------+------+------+------+
// | 7 | 8 | 9 | FADE |
// +------+------+------+------+
// | 10 | 11 | 12 |SMOOTH|
// +------+------+------+------+
static const std::array<ValueCommand, 20> instance {
{{F("FF00FF"), F("brightness +10")},
{F("FF807F"), F("brightness -10")},
{F("FF40BF"), F("light off")},
{F("FFC03F"), F("light on")},
{F("FF20DF"), F("rgb #FF0000")},
{F("FFA05F"), F("rgb #00FF00")},
{F("FF609F"), F("rgb #0000FF")},
{F("FFE01F"), F("rgb #FFFFFF")},
{F("FF10EF"), F("rgb #D13A01")},
{F("FF906F"), F("rgb #00E644")},
{F("FF50AF"), F("rgb #0040A7")},
//{F("FFD02F"), F("effect flash")},
{F("FF30CF"), F("rgb #E96F2A")},
{F("FFB04F"), F("rgb #00BEBF")},
{F("FF708F"), F("rgb #56406F")},
//{F("FFF00F"), F("effect strobe")},
{F("FF08F7"), F("rgb #EE9819")},
{F("FF8877"), F("rgb #00799A")},
{F("FF48B7"), F("rgb #944E80")},
//{F("FFC837"), F("effect fade")},
{F("FF28D7"), F("rgb #FFFF00")},
{F("FFA857"), F("rgb #0060A1")},
{F("FF6897"), F("rgb #EF45AD")}}
//{F("FFE817"), F("effect smooth")}
};
#elif IR_RX_PRESET == 3
// Samsung AA59-00608A for a generic 8CH module
// +------+------+------+
// | 1 | 2 | 3 |
// +------+------+------+
// | 4 | 5 | 6 |
// +------+------+------+
// | 7 | 8 | 9 |
// +------+------+------+
// | | 0 | |
// +------+------+------+
static const std::array<ValueCommand, 8> instance {
{{F("E0E020DF"), F("relay 0 toggle")},
{F("E0E0A05F"), F("relay 1 toggle")},
{F("E0E0609F"), F("relay 2 toggle")},
{F("E0E010EF"), F("relay 3 toggle")},
{F("E0E0906F"), F("relay 4 toggle")},
{F("E0E050AF"), F("relay 5 toggle")},
{F("E0E030CF"), F("relay 6 toggle")},
{F("E0E0B04F"), F("relay 7 toggle")}}
};
// Plus, 2 extra buttons (TODO: on each side of 0?)
// - E0E0708F
// - E0E08877
#elif IR_RX_PRESET == 4
// +------+------+------+
// | OFF | SRC | MUTE |
// +------+------+------+
// ...
// +------+------+------+
// TODO: ...but what's the rest?
static const std::array<ValueCommand, 1> instance {
{F("FFB24D"), F("relay 0 toggle")}
};
#elif IR_RX_PRESET == 5
// Another identical IR Remote shipped with another controller as SET 1 and 2
// +------+------+------+------+
// | UP | Down | OFF | ON |
// +------+------+------+------+
// | R | G | B | W |
// +------+------+------+------+
// | 1 | 2 | 3 |FLASH |
// +------+------+------+------+
// | 4 | 5 | 6 |STROBE|
// +------+------+------+------+
// | 7 | 8 | 9 | FADE |
// +------+------+------+------+
// | 10 | 11 | 12 |SMOOTH|
// +------+------+------+------+
static const std::array<ValueCommand, 20> instance {
{{F("F700FF"), F("brightness +10")},
{F("F7807F"), F("brightness -10")},
{F("F740BF"), F("light off")},
{F("F7C03F"), F("light on")},
{F("F720DF"), F("rgb #FF0000")},
{F("F7A05F"), F("rgb #00FF00")},
{F("F7609F"), F("rgb #0000FF")},
{F("F7E01F"), F("rgb #FFFFFF")},
{F("F710EF"), F("rgb #D13A01")},
{F("F7906F"), F("rgb #00E644")},
{F("F750AF"), F("rgb #0040A7")},
//{F("F7D02F"), F("effect flash")},
{F("F730CF"), F("rgb #E96F2A")},
{F("F7B04F"), F("rgb #00BEBF")},
{F("F7708F"), F("rgb #56406F")},
//{F("F7F00F"), F("effect strobe")},
{F("F708F7"), F("rgb #EE9819")},
{F("F78877"), F("rgb #00799A")},
{F("F748B7"), F("rgb #944E80")},
//{F("F7C837"), F("effect fade")},
{F("F728D7"), F("rgb #FFFF00")},
{F("F7A857"), F("rgb #0060A1")},
{F("F76897"), F("rgb #EF45AD")}}
//{F("F7E817"), F("effect smooth")}
};
#else
#error "Preset is not handled"
#endif
return {std::begin(instance), std::end(instance)};
}
#endif
} // namespace build
namespace internal {
void inject(String command) {
terminalInject(command.c_str(), command.length());
if (!command.endsWith("\r\n") && !command.endsWith("\n")) {
terminalInject('\n');
}
}
} // namespace internal
void process(rx::DecodeResult& result) {
auto value = ir::simple::value::encode(result.value());
#if IR_RX_PRESET != 0
auto preset = build::preset();
if (preset.begin && preset.end && (preset.begin != preset.end)) {
for (auto* it = preset.begin; it != preset.end; ++it) {
String other((*it).value);
if (other == value) {
internal::inject((*it).command);
return;
}
}
}
#endif
String key;
key += F("irCmd");
key += value;
auto cmd = ::settings::internal::get(key);
if (cmd) {
internal::inject(cmd.ref());
}
}
void setup() {
terminalRegisterCommand(F("IR.SEND"), [](const ::terminal::CommandContext& ctx) {
if (ctx.argv.size() == 2) {
auto view = StringView{ctx.argv[1]};
auto simple = ir::simple::parse(view);
if (ir::tx::enqueue(std::move(simple))) {
terminalOK(ctx);
return;
}
auto state = ir::state::parse(view);
if (ir::tx::enqueue(std::move(state))) {
terminalOK(ctx);
return;
}
auto raw = ir::raw::parse(view);
if (ir::tx::enqueue(std::move(raw))) {
terminalOK(ctx);
return;
}
terminalError(ctx, F("Invalid payload"));
return;
}
terminalError(ctx, F("IR.SEND <PAYLOAD>"));
});
}
} // namespace terminal
#endif
#if DEBUG_SUPPORT
namespace debug {
void log(rx::DecodeResult& result) {
if (!result) {
DEBUG_MSG_P(PSTR("[IR] IN unknown value %s\n"),
ir::simple::value::encode(result.value()).c_str());
} else if (result.bytes() > 8) {
DEBUG_MSG_P(PSTR("[IR] IN protocol %d state %s\n"),
static_cast<int>(result.type()), ir::state::value::encode(result.state()).c_str());
} else {
DEBUG_MSG_P(PSTR("[IR] IN protocol %d value %s bits %hu\n"),
static_cast<int>(result.type()), ir::simple::value::encode(result.value()).c_str(), result.bits());
}
}
} // namespace debug
#endif
namespace rx {
// TODO: rpnlib support like with rfbridge stringified callbacks?
void process(DecodeResult&& result) {
#if DEBUG_SUPPORT
ir::debug::log(result);
#endif
#if TERMINAL_SUPPORT
ir::terminal::process(result);
#endif
#if MQTT_SUPPORT
ir::mqtt::process(result);
#endif
}
// IRrecv uses os timers to schedule things, isr and the system task do the actual processing
// Unless `bufferSave()` is set to `true`, raw value buffers will be shared with the ISR task.
// After `decode()` call, `result` object does not store the actual data though, but references
// the specific buffer that was allocated by the `instance` constructor.
void loop() {
static ::decode_results result;
if (internal::instance->decode(&result)) {
if (result.overflow) {
return;
}
if ((result.decode_type == decode_type_t::UNKNOWN) && !internal::unknown) {
return;
}
static unsigned long last { millis() - internal::delay - 1ul };
unsigned long ts { millis() };
if (ts - last < internal::delay) {
return;
}
last = ts;
process(DecodeResult(result));
}
}
} // namespace rx
#if RELAY_SUPPORT
namespace relay {
namespace settings {
String relayOn(size_t id) {
return getSetting({"irRelayOn", id});
}
String relayOff(size_t id) {
return getSetting({"irRelayOff", id});
}
} // namespace settings
namespace internal {
void callback(size_t id, bool status) {
auto cmd = status
? settings::relayOn(id)
: settings::relayOff(id);
if (!cmd.length()) {
return;
}
StringView view{cmd};
ir::tx::enqueue(ir::simple::parse(view));
}
} // namespace internal
void setup() {
::relayOnStatusNotify(internal::callback);
::relayOnStatusChange(internal::callback);
}
} // namespace relay
#endif
void configure() {
rx::configure();
tx::configure();
#if MQTT_SUPPORT
mqtt::configure();
#endif
}
void setup() {
auto rxPin = gpioRegister(rx::settings::pin());
if (rxPin) {
DEBUG_MSG_P(PSTR("[IR] Receiver on GPIO%hhu\n"), rxPin->pin());
} else {
DEBUG_MSG_P(PSTR("[IR] No receiver configured\n"));
}
auto txPin = gpioRegister(tx::settings::pin());
if (txPin) {
DEBUG_MSG_P(PSTR("[IR] Transmitter on GPIO%hhu\n"), txPin->pin());
} else {
DEBUG_MSG_P(PSTR("[IR] No transmitter configured\n"));
}
if (!rxPin && !txPin) {
return;
}
espurnaRegisterReload(configure);
configure();
if (rxPin && txPin) {
::espurnaRegisterLoop([]() {
ir::rx::loop();
ir::tx::loop();
});
} else if (rxPin) {
::espurnaRegisterLoop([]() {
ir::rx::loop();
});
} else if (txPin) {
::espurnaRegisterLoop([]() {
ir::tx::loop();
});
}
if (txPin) {
#if MQTT_SUPPORT
ir::mqtt::setup();
#endif
#if RELAY_SUPPORT
ir::relay::setup();
#endif
#if TERMINAL_SUPPORT
ir::terminal::setup();
#endif
}
if (txPin) {
tx::setup(std::move(txPin));
}
if (rxPin) {
rx::setup(std::move(rxPin));
}
}
} // namespace
} // namespace ir
#if IR_TEST_SUPPORT
namespace ir {
namespace {
namespace test {
// TODO: may be useful if struct and values comparison error dump happens. but, not really nice looking for structs b/c of the length and no field highlight
#if 0
String serialize(const ::ir::simple::Payload& payload) {
String out;
out.reserve(128);
out += F("{ .type=decode_type_t::");
out += typeToString(payload.type);
out += F(", ");
out += F(".value=");
out += ::ir::simple::value::encode(payload.value);
out += F(", ");
out += F(".bits=");
out += String(payload.bits, 10);
out += F(", ");
out += F(".repeats=");
out += String(payload.repeats, 10);
out += F(", ");
out += F(".series=");
out += String(payload.series, 10);
out += F(", ");
out += F(".delay=");
out += String(payload.delay, 10);
out += F(" }");
return out;
}
String serialize(const ::ir::raw::Payload& payload) {
String out;
out.reserve(128);
out += F("{ .frequency=");
out += String(payload.frequency, 10);
out += F(", ");
out += F(".series=");
out += String(payload.series, 10);
out += F(", ");
out += F(".delay=");
out += String(payload.delay, 10);
out += F(", ");
out += F(".time[");
out += String(payload.time.size(), 10);
out += F("]={");
bool comma { false };
for (auto& value : payload.time) {
if (comma) {
out += F(", ");
}
out += String(value, 10);
comma = true;
}
out += F("} }");
return out;
}
#endif
struct Report {
Report(int line, String&& repr) :
_line(line),
_repr(std::move(repr))
{}
int line() const {
return _line;
}
const String& repr() const {
return _repr;
}
private:
int _line;
String _repr;
};
struct NoopPayloadSender : public ir::tx::ReschedulablePayload {
NoopPayloadSender(uint8_t series, unsigned long delay) :
ir::tx::ReschedulablePayload(series, delay)
{}
bool send(IRsend&) const override {
return series();
}
};
using Reports = std::vector<Report>;
struct Context {
struct View {
explicit View(Context& context) :
_context(context)
{}
template <typename... Args>
void report(Args&&... args) {
_context.report(std::forward<Args>(args)...);
}
private:
Context& _context;
};
using Runner = void(*)(View&);
using Runners = std::initializer_list<Runner>;
Context(Runners runners) :
_begin(std::begin(runners)),
_end(std::end(runners))
{
run();
}
#if DEBUG_SUPPORT
~Context() {
DEBUG_MSG_P(PSTR("[IR TEST] %s\n"),
_reports.size() ? "FAILED" : "SUCCESS");
for (auto& report : _reports) {
DEBUG_MSG_P(PSTR("[IR TEST] " __FILE__ ":%d '%.*s'\n"),
report.line(), report.repr().length(), report.repr().c_str());
}
}
#endif
template <typename... Args>
void report(Args&&... args) {
_reports.emplace_back(std::forward<Args>(args)...);
}
private:
void run() {
View view(*this);
for (auto* it = _begin; it != _end; ++it) {
(*it)(view);
}
}
const Runner* _begin;
const Runner* _end;
Reports _reports;
};
// TODO: unity and pio-test? would need to:
// - use `test_build_project_src = yes` in the .ini
// - disable `DEBUG_SERIAL_SUPPORT` in case it's on `Serial` or anything else allowing output to the `Serial`
// (some code gets automatically generated when `pio test` is called that contains setUp(), tearDown(), etc.)
// - have more preprocessor-wrapped chunks
// - not depend on destructors, since unity uses setjmp and longjmp
// (or use `-DUNITY_EXCLUDE_SETJMP_H`)
// TODO: anything else header-only? may be a problem though with c++ approach, as most popular frameworks depend on std::ostream
// TODO: for parsers specifically, some fuzzing to randomize inputs and test order could be useful
// (also, extending the current set of tests and / or having some helper macro that can fill the boilerplate)
// As a (temporary?) solution for right now, have these 4 macros that setup a Context object and a list of test runners.
// Each runner may call `IR_TEST(<something resolving to bool>)` to immediatly exit current block on failure and save report to the Context object.
// On destruction of the Context object, every report is printed to the debug output.
#define IR_TEST_SETUP_BEGIN() Context runner ## __FILE__ ## __LINE__ {
#define IR_TEST_SETUP_END() }
#define IR_TEST_RUNNER() [](Context::View& __context_view)
#define IR_TEST(EXPRESSION) {\
if (!(EXPRESSION)) {\
__context_view.report(__LINE__, F(#EXPRESSION));\
return;\
}\
}
void setup() {
IR_TEST_SETUP_BEGIN() {
IR_TEST_RUNNER() {
IR_TEST(!ir::simple::parse(""));
},
IR_TEST_RUNNER() {
IR_TEST(!ir::simple::parse(","));
},
IR_TEST_RUNNER() {
IR_TEST(!ir::simple::parse("999::"));
},
IR_TEST_RUNNER() {
IR_TEST(!ir::simple::parse("-5:doesntmatter"));
},
IR_TEST_RUNNER() {
IR_TEST(!ir::simple::parse("2:0:31"));
},
IR_TEST_RUNNER() {
IR_TEST(!ir::simple::parse("2:012:31"));
},
IR_TEST_RUNNER() {
IR_TEST(!ir::simple::parse("2:112233445566778899AA:31"));
},
IR_TEST_RUNNER() {
IR_TEST(::ir::simple::value::encode(0xffaabbccddee) == F("FFAABBCCDDEE"));
},
IR_TEST_RUNNER() {
IR_TEST(::ir::simple::value::encode(0xfaabbccddee) == F("0FAABBCCDDEE"));
},
IR_TEST_RUNNER() {
IR_TEST(::ir::simple::value::encode(0xee) == F("EE"));
},
IR_TEST_RUNNER() {
IR_TEST(::ir::simple::value::encode(0) == F("00"));
},
IR_TEST_RUNNER() {
auto result = ir::simple::parse("2:7FAABBCC:31");
IR_TEST(result.has_value());
auto& payload = result.value();
IR_TEST(payload.type == decode_type_t::RC6);
IR_TEST(payload.value == static_cast<uint64_t>(0x7faabbcc));
IR_TEST(payload.bits == 31);
},
IR_TEST_RUNNER() {
auto result = ir::simple::parse("15:AABBCCDD:25:3");
IR_TEST(result.has_value());
auto& payload = result.value();
IR_TEST(payload.type == decode_type_t::COOLIX);
IR_TEST(payload.value == static_cast<uint64_t>(0xaabbccdd));
IR_TEST(payload.bits == 25);
IR_TEST(payload.repeats == 3);
},
IR_TEST_RUNNER() {
auto result = ir::simple::parse("10:0FEFEFEF:21:2:5:500");
IR_TEST(result.has_value());
auto& payload = result.value();
IR_TEST(payload.type == decode_type_t::LG);
IR_TEST(payload.value == static_cast<uint64_t>(0x0fefefef));
IR_TEST(payload.bits == 21);
IR_TEST(payload.repeats == 2);
IR_TEST(payload.series == 5);
IR_TEST(payload.delay == 500);
},
IR_TEST_RUNNER() {
auto result = ir::simple::parse("20:1122AABBCCDDEEFF:64:2:3:1000");
IR_TEST(result.has_value());
auto ptr = std::make_unique<NoopPayloadSender>(
result->series, result->delay);
IR_TEST(ptr->delay() == 1000);
IRsend sender(GPIO_NONE);
IR_TEST(ptr->send(sender));
IR_TEST(ptr->reschedule());
IR_TEST(ptr->send(sender));
IR_TEST(ptr->reschedule());
IR_TEST(ptr->send(sender));
IR_TEST(!ptr->reschedule());
},
IR_TEST_RUNNER() {
IR_TEST(!ir::state::parse(""));
},
IR_TEST_RUNNER() {
IR_TEST(!ir::state::parse(":"));
},
IR_TEST_RUNNER() {
IR_TEST(!ir::state::parse("-1100,100,150"));
},
IR_TEST_RUNNER() {
IR_TEST(!ir::state::parse("25:"));
},
IR_TEST_RUNNER() {
IR_TEST(!ir::state::parse("30:C"));
},
IR_TEST_RUNNER() {
IR_TEST(ir::state::parse("45:CD"));
},
IR_TEST_RUNNER() {
auto result = ir::state::parse("20:C7B7966A9B29CD3C5F2AC03B91B0B221");
IR_TEST(result.has_value());
auto& payload = result.value();
IR_TEST(payload.type == decode_type_t::MITSUBISHI_AC);
const uint8_t raw[] {
0xc7, 0xb7, 0x96, 0x6a,
0x9b, 0x29, 0xcd, 0x3c,
0x5f, 0x2a, 0xc0, 0x3b,
0x91, 0xb0, 0xb2, 0x21};
IR_TEST(payload.value.size() == sizeof(raw));
IR_TEST(std::equal(std::begin(payload.value), std::end(payload.value),
std::begin(raw)));
},
IR_TEST_RUNNER() {
IR_TEST(!ir::raw::parse("-1:1:500:,200,150,250,50,100,100,150"));
},
IR_TEST_RUNNER() {
auto result = ir::raw::parse("38:1:500:100,200,150,250,50,100,100,150");
IR_TEST(result.has_value());
auto& payload = result.value();
IR_TEST(payload.frequency == 38);
IR_TEST(payload.series == 1);
IR_TEST(payload.delay == 500);
decltype(ir::raw::Payload::time) expected_time {
100, 200, 150, 250, 50, 100, 100, 150};
IR_TEST(expected_time == payload.time);
},
IR_TEST_RUNNER() {
const uint16_t raw[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
IR_TEST(::ir::raw::time::encode(std::begin(raw), std::end(raw)) == F("2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32"));
}
}
IR_TEST_SETUP_END();
}
} // namespace test
} // namespace
} // namespace ir
#endif
void irSetup() {
#if IR_TEST_SUPPORT
ir::test::setup();
#endif
ir::setup();
}
#endif