// The sketch is auto-generated with XOD (https://xod.io). // // You can compile and upload it to an Arduino-compatible board with // Arduino IDE. // // Rough code overview: // // - Configuration section // - STL shim // - Immutable list classes and functions // - XOD runtime environment // - Native node implementation // - Program graph definition // // Search for comments fenced with '====' and '----' to navigate through // the major code blocks. #include #include /*============================================================================= * * * Configuration * * =============================================================================*/ // Uncomment to turn on debug of the program //#define XOD_DEBUG // Uncomment to trace the program runtime in the Serial Monitor //#define XOD_DEBUG_ENABLE_TRACE /*============================================================================= * * * STL shim. Provides implementation for vital std::* constructs * * =============================================================================*/ namespace xod { namespace std { template< class T > struct remove_reference {typedef T type;}; template< class T > struct remove_reference {typedef T type;}; template< class T > struct remove_reference {typedef T type;}; template typename remove_reference::type&& move(T&& a) { return static_cast::type&&>(a); } } // namespace std } // namespace xod /*============================================================================= * * * XOD-specific list/array implementations * * =============================================================================*/ #ifndef XOD_LIST_H #define XOD_LIST_H namespace xod { namespace detail { /* * Cursors are used internaly by iterators and list views. They are not exposed * directly to a list consumer. * * The base `Cursor` is an interface which provides the bare minimum of methods * to facilitate a single iteration pass. */ template class Cursor { public: virtual ~Cursor() { } virtual bool isValid() const = 0; virtual bool value(T* out) const = 0; virtual void next() = 0; }; template class NilCursor : public Cursor { public: virtual bool isValid() const { return false; } virtual bool value(T*) const { return false; } virtual void next() { } }; } // namespace detail /* * Iterator is an object used to iterate a list once. * * Users create new iterators by calling `someList.iterate()`. * Iterators are created on stack and are supposed to have a * short live, e.g. for a duration of `for` loop or node’s * `evaluate` function. Iterators can’t be copied. * * Implemented as a pimpl pattern wrapper over the cursor. * Once created for a cursor, an iterator owns that cursor * and will delete the cursor object once destroyed itself. */ template class Iterator { public: static Iterator nil() { return Iterator(new detail::NilCursor()); } Iterator(detail::Cursor* cursor) : _cursor(cursor) { } ~Iterator() { if (_cursor) delete _cursor; } Iterator(const Iterator& that) = delete; Iterator& operator=(const Iterator& that) = delete; Iterator(Iterator&& it) : _cursor(it._cursor) { it._cursor = nullptr; } Iterator& operator=(Iterator&& it) { auto tmp = it._cursor; it._cursor = _cursor; _cursor = tmp; return *this; } operator bool() const { return _cursor->isValid(); } bool value(T* out) const { return _cursor->value(out); } T operator*() const { T out; _cursor->value(&out); return out; } Iterator& operator++() { _cursor->next(); return *this; } private: detail::Cursor* _cursor; }; /* * An interface for a list view. A particular list view provides a new * kind of iteration over existing data. This way we can use list slices, * list concatenations, list rotations, etc without introducing new data * buffers. We just change the way already existing data is iterated. * * ListView is not exposed to a list user directly, it is used internally * by the List class. However, deriving a new ListView is necessary if you * make a new list/string processing node. */ template class ListView { public: virtual Iterator iterate() const = 0; }; /* * The list as it seen by data consumers. Have a single method `iterate` * to create a new iterator. * * Implemented as pimpl pattern wrapper over a list view. Takes pointer * to a list view in constructor and expects the view will be alive for * the whole life time of the list. */ template class List { public: constexpr List() : _view(nullptr) { } List(const ListView* view) : _view(view) { } Iterator iterate() const { return _view ? _view->iterate() : Iterator::nil(); } // pre 0.15.0 backward compatibility List* operator->() __attribute__ ((deprecated)) { return this; } const List* operator->() const __attribute__ ((deprecated)) { return this; } private: const ListView* _view; }; /* * A list view over an old good plain C array. * * Expects the array will be alive for the whole life time of the * view. */ template class PlainListView : public ListView { public: class Cursor : public detail::Cursor { public: Cursor(const PlainListView* owner) : _owner(owner) , _idx(0) { } bool isValid() const override { return _idx < _owner->_len; } bool value(T* out) const override { if (!isValid()) return false; *out = _owner->_data[_idx]; return true; } void next() override { ++_idx; } private: const PlainListView* _owner; size_t _idx; }; public: PlainListView(const T* data, size_t len) : _data(data) , _len(len) { } virtual Iterator iterate() const override { return Iterator(new Cursor(this)); } private: friend class Cursor; const T* _data; size_t _len; }; /* * A list view over a null-terminated C-String. * * Expects the char buffer will be alive for the whole life time of the view. * You can use string literals as a buffer, since they are persistent for * the program execution time. */ class CStringView : public ListView { public: class Cursor : public detail::Cursor { public: Cursor(const char* str) : _ptr(str) { } bool isValid() const override { return (bool)*_ptr; } bool value(char* out) const override { *out = *_ptr; return (bool)*_ptr; } void next() override { ++_ptr; } private: const char* _ptr; }; public: CStringView(const char* str = nullptr) : _str(str) { } CStringView& operator=(const CStringView& rhs) { _str = rhs._str; return *this; } virtual Iterator iterate() const override { return _str ? Iterator(new Cursor(_str)) : Iterator::nil(); } private: friend class Cursor; const char* _str; }; /* * A list view over two other lists (Left and Right) which first iterates the * left one, and when exhausted, iterates the right one. * * Expects both Left and Right to be alive for the whole view life time. */ template class ConcatListView : public ListView { public: class Cursor : public detail::Cursor { public: Cursor(Iterator&& left, Iterator&& right) : _left(std::move(left)) , _right(std::move(right)) { } bool isValid() const override { return _left || _right; } bool value(T* out) const override { return _left.value(out) || _right.value(out); } void next() override { _left ? ++_left : ++_right; } private: Iterator _left; Iterator _right; }; public: ConcatListView() { } ConcatListView(List left, List right) : _left(left) , _right(right) { } ConcatListView& operator=(const ConcatListView& rhs) { _left = rhs._left; _right = rhs._right; return *this; } virtual Iterator iterate() const override { return Iterator(new Cursor(_left.iterate(), _right.iterate())); } private: friend class Cursor; List _left; List _right; }; //---------------------------------------------------------------------------- // Text string helpers //---------------------------------------------------------------------------- using XString = List; /* * List and list view in a single pack. An utility used to define constant * string literals in XOD. */ class XStringCString : public XString { public: XStringCString(const char* str) : XString(&_view) , _view(str) { } private: CStringView _view; }; } // namespace xod #endif /*============================================================================= * * * Functions to work with memory * * =============================================================================*/ #ifndef XOD_NO_PLACEMENT_NEW // Placement `new` for Arduino void* operator new(size_t, void* ptr) { return ptr; } #endif /*============================================================================= * * * UART Classes, that wraps Serials * * =============================================================================*/ class HardwareSerial; class SoftwareSerial; namespace xod { class Uart { private: long _baud; protected: bool _started = false; public: Uart(long baud) { _baud = baud; } virtual void begin() = 0; virtual void end() = 0; virtual void flush() = 0; virtual bool available() = 0; virtual bool writeByte(uint8_t) = 0; virtual bool readByte(uint8_t*) = 0; virtual SoftwareSerial* toSoftwareSerial() { return nullptr; } virtual HardwareSerial* toHardwareSerial() { return nullptr; } void changeBaudRate(long baud) { _baud = baud; if (_started) { end(); begin(); } } long getBaudRate() const { return _baud; } Stream* toStream() { Stream* stream = (Stream*) toHardwareSerial(); if (stream) return stream; return (Stream*) toSoftwareSerial(); } }; class HardwareUart : public Uart { private: HardwareSerial* _serial; public: HardwareUart(HardwareSerial& hserial, uint32_t baud = 115200) : Uart(baud) { _serial = &hserial; } void begin(); void end(); void flush(); bool available() { return (bool) _serial->available(); } bool writeByte(uint8_t byte) { return (bool) _serial->write(byte); } bool readByte(uint8_t* out) { int data = _serial->read(); if (data == -1) return false; *out = data; return true; } HardwareSerial* toHardwareSerial() { return _serial; } }; void HardwareUart::begin() { _started = true; _serial->begin(getBaudRate()); }; void HardwareUart::end() { _started = false; _serial->end(); }; void HardwareUart::flush() { _serial->flush(); }; } // namespace xod /*============================================================================= * * * Basic algorithms for XOD lists * * =============================================================================*/ #ifndef XOD_LIST_FUNCS_H #define XOD_LIST_FUNCS_H namespace xod { /* * Folds a list from left. Also known as "reduce". */ template TR foldl(List xs, TR (*func)(TR, T), TR acc) { for (auto it = xs.iterate(); it; ++it) acc = func(acc, *it); return acc; } template size_t lengthReducer(size_t len, T) { return len + 1; } /* * Computes length of a list. */ template size_t length(List xs) { return foldl(xs, lengthReducer, (size_t)0); } template T* dumpReducer(T* buff, T x) { *buff = x; return buff + 1; } /* * Copies a list content into a memory buffer. * * It is expected that `outBuff` has enough size to fit all the data. */ template size_t dump(List xs, T* outBuff) { T* buffEnd = foldl(xs, dumpReducer, outBuff); return buffEnd - outBuff; } /* * Compares two lists. */ template bool equal(List lhs, List rhs) { auto lhsIt = lhs.iterate(); auto rhsIt = rhs.iterate(); for (; lhsIt && rhsIt; ++lhsIt, ++rhsIt) { if (*lhsIt != *rhsIt) return false; } return !lhsIt && !rhsIt; } } // namespace xod #endif /*============================================================================= * * * Runtime * * =============================================================================*/ //---------------------------------------------------------------------------- // Debug routines //---------------------------------------------------------------------------- #ifndef DEBUG_SERIAL # define DEBUG_SERIAL Serial #endif #if defined(XOD_DEBUG) && defined(XOD_DEBUG_ENABLE_TRACE) # define XOD_TRACE(x) { DEBUG_SERIAL.print(x); DEBUG_SERIAL.flush(); } # define XOD_TRACE_LN(x) { DEBUG_SERIAL.println(x); DEBUG_SERIAL.flush(); } # define XOD_TRACE_F(x) XOD_TRACE(F(x)) # define XOD_TRACE_FLN(x) XOD_TRACE_LN(F(x)) #else # define XOD_TRACE(x) # define XOD_TRACE_LN(x) # define XOD_TRACE_F(x) # define XOD_TRACE_FLN(x) #endif //---------------------------------------------------------------------------- // PGM space utilities //---------------------------------------------------------------------------- #define pgm_read_nodeid(address) (pgm_read_word(address)) /* * Workaround for bugs: * https://github.com/arduino/ArduinoCore-sam/pull/43 * https://github.com/arduino/ArduinoCore-samd/pull/253 * Remove after the PRs merge */ #if !defined(ARDUINO_ARCH_AVR) && defined(pgm_read_ptr) # undef pgm_read_ptr # define pgm_read_ptr(addr) (*(const void **)(addr)) #endif //---------------------------------------------------------------------------- // Compatibilities //---------------------------------------------------------------------------- #if !defined(ARDUINO_ARCH_AVR) /* * Provide dtostrf function for non-AVR platforms. Although many platforms * provide a stub many others do not. And the stub is based on `sprintf` * which doesn’t work with floating point formatters on some platforms * (e.g. Arduino M0). * * This is an implementation based on `fcvt` standard function. Taken here: * https://forum.arduino.cc/index.php?topic=368720.msg2542614#msg2542614 */ char *dtostrf(double val, int width, unsigned int prec, char *sout) { int decpt, sign, reqd, pad; const char *s, *e; char *p; s = fcvt(val, prec, &decpt, &sign); if (prec == 0 && decpt == 0) { s = (*s < '5') ? "0" : "1"; reqd = 1; } else { reqd = strlen(s); if (reqd > decpt) reqd++; if (decpt == 0) reqd++; } if (sign) reqd++; p = sout; e = p + reqd; pad = width - reqd; if (pad > 0) { e += pad; while (pad-- > 0) *p++ = ' '; } if (sign) *p++ = '-'; if (decpt <= 0 && prec > 0) { *p++ = '0'; *p++ = '.'; e++; while ( decpt < 0 ) { decpt++; *p++ = '0'; } } while (p < e) { *p++ = *s++; if (p == e) break; if (--decpt == 0) *p++ = '.'; } if (width < 0) { pad = (reqd + width) * -1; while (pad-- > 0) *p++ = ' '; } *p = 0; return sout; } #endif namespace xod { //---------------------------------------------------------------------------- // Type definitions //---------------------------------------------------------------------------- #if __SIZEOF_FLOAT__ == 4 typedef float Number; #else typedef double Number; #endif typedef bool Logic; typedef unsigned long TimeMs; typedef uint8_t DirtyFlags; //---------------------------------------------------------------------------- // Global variables //---------------------------------------------------------------------------- TimeMs g_transactionTime; bool g_isSettingUp; //---------------------------------------------------------------------------- // Metaprogramming utilities //---------------------------------------------------------------------------- template struct always_false { enum { value = 0 }; }; //---------------------------------------------------------------------------- // Forward declarations //---------------------------------------------------------------------------- TimeMs transactionTime(); void runTransaction(); //---------------------------------------------------------------------------- // Engine (private API) //---------------------------------------------------------------------------- namespace detail { template bool isTimedOut(const NodeT* node) { TimeMs t = node->timeoutAt; // TODO: deal with uint32 overflow return t && t < transactionTime(); } // Marks timed out node dirty. Do not reset timeoutAt here to give // a chance for a node to get a reasonable result from `isTimedOut` // later during its `evaluate` template void checkTriggerTimeout(NodeT* node) { node->isNodeDirty |= isTimedOut(node); } template void clearTimeout(NodeT* node) { node->timeoutAt = 0; } template void clearStaleTimeout(NodeT* node) { if (isTimedOut(node)) clearTimeout(node); } } // namespace detail //---------------------------------------------------------------------------- // Public API (can be used by native nodes’ `evaluate` functions) //---------------------------------------------------------------------------- TimeMs transactionTime() { return g_transactionTime; } bool isSettingUp() { return g_isSettingUp; } template void setTimeout(ContextT* ctx, TimeMs timeout) { ctx->_node->timeoutAt = transactionTime() + timeout; } template void clearTimeout(ContextT* ctx) { detail::clearTimeout(ctx->_node); } template bool isTimedOut(const ContextT* ctx) { return detail::isTimedOut(ctx->_node); } } // namespace xod //---------------------------------------------------------------------------- // Entry point //---------------------------------------------------------------------------- void setup() { // FIXME: looks like there is a rounding bug. Waiting for 100ms fights it delay(100); #ifdef XOD_DEBUG DEBUG_SERIAL.begin(115200); #endif XOD_TRACE_FLN("\n\nProgram started"); xod::g_isSettingUp = true; xod::runTransaction(); xod::g_isSettingUp = false; } void loop() { xod::runTransaction(); } /*============================================================================= * * * Native node implementations * * =============================================================================*/ namespace xod { //----------------------------------------------------------------------------- // xod/core/continuously implementation //----------------------------------------------------------------------------- namespace xod__core__continuously { struct State { }; struct Node { State state; TimeMs timeoutAt; Logic output_TICK; union { struct { bool isOutputDirty_TICK : 1; bool isNodeDirty : 1; }; DirtyFlags dirtyFlags; }; }; struct output_TICK { }; template struct ValueType { using T = void; }; template<> struct ValueType { using T = Logic; }; struct ContextObject { Node* _node; }; using Context = ContextObject*; template typename ValueType::T getValue(Context ctx) { static_assert(always_false::value, "Invalid pin descriptor. Expected one of:" \ "" \ " output_TICK"); } template<> Logic getValue(Context ctx) { return ctx->_node->output_TICK; } template bool isInputDirty(Context ctx) { static_assert(always_false::value, "Invalid input descriptor. Expected one of:" \ ""); return false; } template void emitValue(Context ctx, typename ValueType::T val) { static_assert(always_false::value, "Invalid output descriptor. Expected one of:" \ " output_TICK"); } template<> void emitValue(Context ctx, Logic val) { ctx->_node->output_TICK = val; ctx->_node->isOutputDirty_TICK = true; } State* getState(Context ctx) { return &ctx->_node->state; } void evaluate(Context ctx) { emitValue(ctx, 1); setTimeout(ctx, 0); } } // namespace xod__core__continuously //----------------------------------------------------------------------------- // xod/common-hardware/text-lcd-16x2-i2c implementation //----------------------------------------------------------------------------- namespace xod__common_hardware__text_lcd_16x2_i2c { // --- Enter global namespace --- }} #include #include namespace xod { namespace xod__common_hardware__text_lcd_16x2_i2c { // --- Back to local namespace --- struct State { LiquidCrystal_I2C* lcd; }; struct Node { State state; Logic output_DONE; union { struct { bool isOutputDirty_DONE : 1; bool isNodeDirty : 1; }; DirtyFlags dirtyFlags; }; }; struct input_ADDR { }; struct input_BL { }; struct input_L1 { }; struct input_L2 { }; struct input_UPD { }; struct output_DONE { }; template struct ValueType { using T = void; }; template<> struct ValueType { using T = uint8_t; }; template<> struct ValueType { using T = Logic; }; template<> struct ValueType { using T = XString; }; template<> struct ValueType { using T = XString; }; template<> struct ValueType { using T = Logic; }; template<> struct ValueType { using T = Logic; }; struct ContextObject { Node* _node; uint8_t _input_ADDR; Logic _input_BL; XString _input_L1; XString _input_L2; Logic _input_UPD; bool _isInputDirty_UPD; }; using Context = ContextObject*; template typename ValueType::T getValue(Context ctx) { static_assert(always_false::value, "Invalid pin descriptor. Expected one of:" \ " input_ADDR input_BL input_L1 input_L2 input_UPD" \ " output_DONE"); } template<> uint8_t getValue(Context ctx) { return ctx->_input_ADDR; } template<> Logic getValue(Context ctx) { return ctx->_input_BL; } template<> XString getValue(Context ctx) { return ctx->_input_L1; } template<> XString getValue(Context ctx) { return ctx->_input_L2; } template<> Logic getValue(Context ctx) { return ctx->_input_UPD; } template<> Logic getValue(Context ctx) { return ctx->_node->output_DONE; } template bool isInputDirty(Context ctx) { static_assert(always_false::value, "Invalid input descriptor. Expected one of:" \ " input_UPD"); return false; } template<> bool isInputDirty(Context ctx) { return ctx->_isInputDirty_UPD; } template void emitValue(Context ctx, typename ValueType::T val) { static_assert(always_false::value, "Invalid output descriptor. Expected one of:" \ " output_DONE"); } template<> void emitValue(Context ctx, Logic val) { ctx->_node->output_DONE = val; ctx->_node->isOutputDirty_DONE = true; } State* getState(Context ctx) { return &ctx->_node->state; } void printLine(LiquidCrystal_I2C* lcd, uint8_t lineIndex, XString str) { lcd->setCursor(0, lineIndex); uint8_t whitespace = 16; for (auto it = str.iterate(); it; ++it, --whitespace) lcd->write(*it); // Clear the rest of the line while (whitespace--) lcd->write(' '); } void evaluate(Context ctx) { if (!isInputDirty(ctx)) return; State* state = getState(ctx); auto lcd = state->lcd; if (!state->lcd) { uint8_t addr = getValue(ctx); state->lcd = lcd = new LiquidCrystal_I2C(addr, 16, 2); lcd->begin(); } printLine(lcd, 0, getValue(ctx)); printLine(lcd, 1, getValue(ctx)); lcd->setBacklight(getValue(ctx)); emitValue(ctx, 1); } } // namespace xod__common_hardware__text_lcd_16x2_i2c } // namespace xod /*============================================================================= * * * Main loop components * * =============================================================================*/ namespace xod { // Define/allocate persistent storages (state, timeout, output data) for all nodes #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wmissing-field-initializers" constexpr uint8_t node_0_output_VAL = 0x27; constexpr Logic node_1_output_VAL = true; static XStringCString node_2_output_VAL = XStringCString("Yeklki"); constexpr XString node_3_output_VAL = XString(); constexpr Logic node_4_output_TICK = false; constexpr Logic node_5_output_DONE = false; #pragma GCC diagnostic pop xod__core__continuously::Node node_4 = { xod__core__continuously::State(), // state default 0, // timeoutAt node_4_output_TICK, // output TICK default false, // TICK dirty true // node itself dirty }; xod__common_hardware__text_lcd_16x2_i2c::Node node_5 = { xod__common_hardware__text_lcd_16x2_i2c::State(), // state default node_5_output_DONE, // output DONE default false, // DONE dirty true // node itself dirty }; void runTransaction() { g_transactionTime = millis(); XOD_TRACE_F("Transaction started, t="); XOD_TRACE_LN(g_transactionTime); // Check for timeouts detail::checkTriggerTimeout(&node_4); // defer-* nodes are always at the very bottom of the graph, so no one will // recieve values emitted by them. We must evaluate them before everybody // else to give them a chance to emit values. // // If trigerred, keep only output dirty, not the node itself, so it will // evaluate on the regular pass only if it pushed a new value again. // Evaluate all dirty nodes { // xod__core__continuously #4 if (node_4.isNodeDirty) { XOD_TRACE_F("Eval node #"); XOD_TRACE_LN(4); xod__core__continuously::ContextObject ctxObj; ctxObj._node = &node_4; // copy data from upstream nodes into context xod__core__continuously::evaluate(&ctxObj); // mark downstream nodes dirty node_5.isNodeDirty |= node_4.isOutputDirty_TICK; } } { // xod__common_hardware__text_lcd_16x2_i2c #5 if (node_5.isNodeDirty) { XOD_TRACE_F("Eval node #"); XOD_TRACE_LN(5); xod__common_hardware__text_lcd_16x2_i2c::ContextObject ctxObj; ctxObj._node = &node_5; // copy data from upstream nodes into context ctxObj._input_ADDR = node_0_output_VAL; ctxObj._input_BL = node_1_output_VAL; ctxObj._input_L1 = node_2_output_VAL; ctxObj._input_L2 = node_3_output_VAL; ctxObj._input_UPD = node_4.output_TICK; ctxObj._isInputDirty_UPD = node_4.isOutputDirty_TICK; xod__common_hardware__text_lcd_16x2_i2c::evaluate(&ctxObj); // mark downstream nodes dirty } } // Clear dirtieness and timeouts for all nodes and pins node_4.dirtyFlags = 0; node_5.dirtyFlags = 0; detail::clearStaleTimeout(&node_4); XOD_TRACE_F("Transaction completed, t="); XOD_TRACE_LN(millis()); } } // namespace xod