From 200cdce532d6abec3534622f6327efcc58ac024b Mon Sep 17 00:00:00 2001 From: Mathew Winters Date: Thu, 27 Mar 2025 19:51:20 +1300 Subject: [PATCH 1/4] Changes to remove arduino --- CMakeLists.txt | 30 ++++++++--------- src/DCCEXInbound.cpp | 59 +++++++++++++++++---------------- src/DCCEXInbound.h | 12 +++---- src/DCCEXLoco.cpp | 6 +++- src/DCCEXLoco.h | 4 ++- src/DCCEXProtocol.cpp | 30 +++++++++++------ src/DCCEXProtocol.h | 24 ++++++++++---- src/DCCEXRoutes.cpp | 6 ++-- src/DCCEXRoutes.h | 5 ++- src/DCCEXTurnouts.cpp | 5 ++- src/DCCEXTurnouts.h | 5 +-- src/DCCEXTurntables.cpp | 5 +-- src/DCCEXTurntables.h | 4 +-- src/DCCStream.h | 23 +++++++++++++ targets/arduino/ArduinoStream.h | 31 +++++++++++++++++ 15 files changed, 169 insertions(+), 80 deletions(-) create mode 100644 src/DCCStream.h create mode 100644 targets/arduino/ArduinoStream.h diff --git a/CMakeLists.txt b/CMakeLists.txt index c0b1720..37f30ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,17 +18,17 @@ else() target_include_directories(DCCEXProtocol SYSTEM PUBLIC src) endif() -# Fetch ArduinoCore-API -if(NOT TARGET ArduinoCore-API) - FetchContent_Declare( - ArduinoCore-API - GIT_REPOSITORY https://github.com/arduino/ArduinoCore-API - GIT_TAG 1.4.0) - FetchContent_Populate(ArduinoCore-API) - find_package(ArduinoCore-API) -endif() - -target_link_libraries(DCCEXProtocol PUBLIC ArduinoCore-API) +# # Fetch ArduinoCore-API +# if(NOT TARGET ArduinoCore-API) +# FetchContent_Declare( +# ArduinoCore-API +# GIT_REPOSITORY https://github.com/arduino/ArduinoCore-API +# GIT_TAG 1.4.0) +# FetchContent_Populate(ArduinoCore-API) +# find_package(ArduinoCore-API) +# endif() + +# target_link_libraries(DCCEXProtocol PUBLIC ArduinoCore-API) foreach(FILE ${SRC}) message(${FILE}) @@ -36,7 +36,7 @@ endforeach() source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SRC}) -if(PROJECT_IS_TOP_LEVEL) - add_subdirectory(docs) - add_subdirectory(tests) -endif() +# if(PROJECT_IS_TOP_LEVEL) +# add_subdirectory(docs) +# add_subdirectory(tests) +# endif() diff --git a/src/DCCEXInbound.cpp b/src/DCCEXInbound.cpp index d5264a1..914f994 100644 --- a/src/DCCEXInbound.cpp +++ b/src/DCCEXInbound.cpp @@ -33,12 +33,14 @@ // The dump() function is used to list the parameters obtained. // so this is the best place to look for how to access the results. #include "DCCEXInbound.h" -#include +#include +#include +namespace DCCExController { // Internal stuff for the parser and getters. const int32_t QUOTE_FLAG = 0x77777000; const int32_t QUOTE_FLAG_AREA = 0xFFFFF000; -enum splitState : byte { +enum splitState : uint8_t { FIND_START, SET_OPCODE, SKIP_SPACES, @@ -50,7 +52,7 @@ enum splitState : byte { int16_t DCCEXInbound::_maxParams = 0; int16_t DCCEXInbound::_parameterCount = 0; -byte DCCEXInbound::_opcode = 0; +uint8_t DCCEXInbound::_opcode = 0; int32_t *DCCEXInbound::_parameterValues = nullptr; char *DCCEXInbound::_cmdBuffer = nullptr; @@ -70,7 +72,7 @@ void DCCEXInbound::cleanup() { } } -byte DCCEXInbound::getOpcode() { return _opcode; } +uint8_t DCCEXInbound::getOpcode() { return _opcode; } int16_t DCCEXInbound::getParameterCount() { return _parameterCount; } @@ -116,7 +118,7 @@ bool DCCEXInbound::parse(char *command) { splitState state = FIND_START; while (_parameterCount < _maxParams) { - byte hot = *remainingCmd; + uint8_t hot = *remainingCmd; if (hot == 0) return false; // no > on end of command. @@ -199,30 +201,31 @@ bool DCCEXInbound::parse(char *command) { return false; // we ran out of max parameters } -void DCCEXInbound::dump(Print *out) { - out->print(F("\nDCCEXInbound Opcode='")); - if (_opcode) - out->write(_opcode); - else - out->print(F("\\0")); - out->println('\''); - - for (int i = 0; i < getParameterCount(); i++) { - if (isTextParameter(i)) { - out->print(F("getTextParameter(")); - out->print(i); - out->print(F(")=\"")); - out->print(getTextParameter(i)); - out->println('"'); - } else { - out->print(F("getNumber(")); - out->print(i); - out->print(F(")=")); - out->println(getNumber(i)); - } - } -} +// void DCCEXInbound::dump(Print *out) { +// out->print(F("\nDCCEXInbound Opcode='")); +// if (_opcode) +// out->write(_opcode); +// else +// out->print(F("\\0")); +// out->println('\''); + +// for (int i = 0; i < getParameterCount(); i++) { +// if (isTextParameter(i)) { +// out->print(F("getTextParameter(")); +// out->print(i); +// out->print(F(")=\"")); +// out->print(getTextParameter(i)); +// out->println('"'); +// } else { +// out->print(F("getNumber(")); +// out->print(i); +// out->print(F(")=")); +// out->println(getNumber(i)); +// } +// } +// } // Private methods bool DCCEXInbound::_isTextInternal(int16_t n) { return ((_parameterValues[n] & QUOTE_FLAG_AREA) == QUOTE_FLAG); } +} // namespace DCCExController \ No newline at end of file diff --git a/src/DCCEXInbound.h b/src/DCCEXInbound.h index 9d60a00..1fcec5f 100644 --- a/src/DCCEXInbound.h +++ b/src/DCCEXInbound.h @@ -30,7 +30,7 @@ #ifndef DCCEXINBOUND_H #define DCCEXINBOUND_H -#include +#include /* How to use this: 1) setup is done once with your expected max parameter count. @@ -40,7 +40,7 @@ 3) Use the get... functions to access the parameters. These parameters are ONLY VALID until you next call parse. */ - +namespace DCCExController { /// @brief Inbound DCC-EX command parser class to parse commands and provide interpreted parameters class DCCEXInbound { public: @@ -58,7 +58,7 @@ class DCCEXInbound { static bool parse(char *command); /// @brief Gets the DCC-EX OPCODE of the parsed command (the first char after the <) - static byte getOpcode(); + static uint8_t getOpcode(); /// @brief Gets number of parameters detected after OPCODE is 4 parameters! /// @return Number of parameters @@ -86,15 +86,15 @@ class DCCEXInbound { /// @brief dump list of parameters obtained /// @param out Address of output e.g. &Serial - static void dump(Print *); + // static void dump(Print *); private: static int16_t _maxParams; static int16_t _parameterCount; - static byte _opcode; + static uint8_t _opcode; static int32_t *_parameterValues; static char *_cmdBuffer; static bool _isTextInternal(int16_t n); }; - +} // namespace DCCExController #endif diff --git a/src/DCCEXLoco.cpp b/src/DCCEXLoco.cpp index a3d2df1..466fd8f 100644 --- a/src/DCCEXLoco.cpp +++ b/src/DCCEXLoco.cpp @@ -28,8 +28,11 @@ */ #include "DCCEXLoco.h" -#include +#include +#include +#include +namespace DCCExController { // class Loco // Public methods @@ -456,3 +459,4 @@ void Consist::_addLocoToConsist(ConsistLoco *conLoco) { } _locoCount++; } +} // namespace DCCExController \ No newline at end of file diff --git a/src/DCCEXLoco.h b/src/DCCEXLoco.h index 8f55a7d..4897691 100644 --- a/src/DCCEXLoco.h +++ b/src/DCCEXLoco.h @@ -30,8 +30,9 @@ #ifndef DCCEXLOCO_H #define DCCEXLOCO_H -#include +#include +namespace DCCExController { static const int MAX_FUNCTIONS = 32; const int MAX_OBJECT_NAME_LENGTH = 30; // including Loco name, Turnout/Point names, Route names, etc. names #define MAX_SINGLE_COMMAND_PARAM_LENGTH 500 // Unfortunately includes the function list for an individual loco @@ -277,4 +278,5 @@ class Consist { void _addLocoToConsist(ConsistLoco *consistLoco); }; +} // namespace DCCExController #endif \ No newline at end of file diff --git a/src/DCCEXProtocol.cpp b/src/DCCEXProtocol.cpp index 5761e2a..bedcb1c 100644 --- a/src/DCCEXProtocol.cpp +++ b/src/DCCEXProtocol.cpp @@ -41,6 +41,12 @@ Function/method prefixes #include "DCCEXProtocol.h" +#include +#include +#include +#include + +namespace DCCExController { static const int MIN_SPEED = 0; static const int MAX_SPEED = 126; @@ -80,14 +86,14 @@ DCCEXProtocol::~DCCEXProtocol() { void DCCEXProtocol::setDelegate(DCCEXProtocolDelegate *delegate) { this->_delegate = delegate; } // Set the Stream used for logging -void DCCEXProtocol::setLogStream(Stream *console) { this->_console = console; } +void DCCEXProtocol::setLogStream(DCCStream *console) { this->_console = console; } void DCCEXProtocol::enableHeartbeat(unsigned long heartbeatDelay) { _enableHeartbeat = true; _heartbeatDelay = heartbeatDelay; } -void DCCEXProtocol::connect(Stream *stream) { +void DCCEXProtocol::connect(DCCStream *stream) { _init(); this->_stream = stream; } @@ -409,7 +415,7 @@ bool DCCEXProtocol::receivedRouteList() { return _receivedRouteList; } void DCCEXProtocol::startRoute(int routeId) { // console->println(F("sendRouteAction()")); if (_delegate) { - sprintf(_outboundCommand, "", routeId); + sprintf(_outboundCommand, "", routeId); _sendCommand(); } // console->println(F("sendRouteAction() end")); @@ -706,24 +712,26 @@ void DCCEXProtocol::_init() { memset(_inputBuffer, 0, sizeof(_inputBuffer)); _nextChar = 0; // last Response time - _lastServerResponseTime = millis(); + if (_delegate) { + _lastServerResponseTime = _delegate->millis(); + } // console->println(F("init(): end")); } void DCCEXProtocol::_sendCommand() { - if (_stream) { + if (_stream && _delegate) { _stream->println(_outboundCommand); _console->print("==> "); _console->println(_outboundCommand); - *_outboundCommand = 0; // clear it once it has been sent - _lastHeartbeat = millis(); // If we sent a command, a heartbeat isn't necessary + *_outboundCommand = 0; // clear it once it has been sent + _lastHeartbeat = _delegate->millis(); // If we sent a command, a heartbeat isn't necessary } } void DCCEXProtocol::_processCommand() { if (_delegate) { // last Response time - _lastServerResponseTime = millis(); + _lastServerResponseTime = _delegate->millis(); switch (DCCEXInbound::getOpcode()) { case '@': // Screen update @@ -907,8 +915,8 @@ void DCCEXProtocol::_processScreenUpdate() { //<@ screen row "message"> } void DCCEXProtocol::_sendHeartbeat() { - if (millis() - _lastHeartbeat > _heartbeatDelay) { - _lastHeartbeat = millis(); + if (_delegate && _delegate->millis() - _lastHeartbeat > _heartbeatDelay) { + _lastHeartbeat = _delegate->millis(); sprintf(_outboundCommand, "<#>"); _sendCommand(); } @@ -1427,3 +1435,5 @@ void DCCEXProtocol::_processWriteCVResponse() { // , value -1 = erro int value = DCCEXInbound::getNumber(1); _delegate->receivedWriteCV(cv, value); } + +} // namespace DCCExController \ No newline at end of file diff --git a/src/DCCEXProtocol.h b/src/DCCEXProtocol.h index 6201c13..886ff58 100644 --- a/src/DCCEXProtocol.h +++ b/src/DCCEXProtocol.h @@ -100,8 +100,11 @@ Version information: #include "DCCEXRoutes.h" #include "DCCEXTurnouts.h" #include "DCCEXTurntables.h" -#include +#include "DCCStream.h" +#include + +namespace DCCExController { const int MAX_OUTBOUND_COMMAND_LENGTH = 100; // Max number of bytes for outbound commands // Valid track power state values @@ -121,14 +124,14 @@ enum TrackManagerMode { }; /// @brief Nullstream class for initial DCCEXProtocol instantiation to direct streams to nothing -class NullStream : public Stream { +class NullStream : public DCCStream { public: /// @brief Constructor for the NullStream object NullStream() {} /// @brief Dummy availability check /// @return Returns false (0) always - int available() { return 0; } + int available() const { return 0; } /// @brief Dummy flush method void flush() {} @@ -151,6 +154,9 @@ class NullStream : public Stream { /// @param size Size of buffer /// @return Returns size of buffer always size_t write(const uint8_t *buffer, size_t size) { return size; } + + void println(const char *format, ...) {} + void print(const char *format, ...) {} }; /// @brief Delegate responses and broadcast events to the client software to enable custom event handlers @@ -244,6 +250,8 @@ class DCCEXProtocolDelegate { /// @param row Row number /// @param message Message to display on the screen/row virtual void receivedScreenUpdate(int screen, int row, char *message) {} + + virtual uint32_t millis() { return 0; } }; /// @brief Main class for the DCCEXProtocol library @@ -265,7 +273,7 @@ class DCCEXProtocol { /// @brief Set the stream object for console output /// @param console - void setLogStream(Stream *console); + void setLogStream(DCCStream *console); /// @brief Enable heartbeat if required - can help WiFi connections that drop out /// @param heartbeatDelay Time in milliseconds between heartbeats - defaults to one minute (60000ms) @@ -273,7 +281,7 @@ class DCCEXProtocol { /// @brief Connect the stream object to interact with DCC-EX /// @param stream - void connect(Stream *stream); + void connect(DCCStream *stream); /// @brief Disconnect from DCC-EX void disconnect(); @@ -681,8 +689,8 @@ class DCCEXProtocol { int _routeCount = 0; // Count of route objects received int _turntableCount = 0; // Count of turntable objects received int _version[3] = {}; // EX-CommandStation version x.y.z - Stream *_stream; // Stream object where commands are sent/received - Stream *_console; // Stream object for console output + DCCStream *_stream; // Stream object where commands are sent/received + DCCStream *_console; // Stream object for console output NullStream _nullStream; // Send streams to null if no object provided int _bufflen; // Used to ensure command buffer size not exceeded int _maxCmdBuffer; // Max size for the command buffer @@ -707,4 +715,6 @@ class DCCEXProtocol { unsigned long _lastHeartbeat; // Time in ms of the last heartbeat, also set by sending a command }; +} // namespace DCCExController + #endif // DCCEXPROTOCOL_H diff --git a/src/DCCEXRoutes.cpp b/src/DCCEXRoutes.cpp index 4a006f3..e19d1bf 100644 --- a/src/DCCEXRoutes.cpp +++ b/src/DCCEXRoutes.cpp @@ -27,10 +27,11 @@ */ #include "DCCEXRoutes.h" -#include -// Public methods +#include +// Public methods +namespace DCCExController { Route *Route::_first = nullptr; Route::Route(int id) { @@ -140,3 +141,4 @@ void Route::_removeFromList(Route *route) { } } } +} // namespace DCCExController \ No newline at end of file diff --git a/src/DCCEXRoutes.h b/src/DCCEXRoutes.h index e0b29b8..83bad3a 100644 --- a/src/DCCEXRoutes.h +++ b/src/DCCEXRoutes.h @@ -29,8 +29,7 @@ #ifndef DCCEXROUTES_H #define DCCEXROUTES_H -#include - +namespace DCCExController { enum RouteType { RouteTypeRoute = 'R', RouteTypeAutomation = 'A', @@ -96,5 +95,5 @@ class Route { /// @param route Pointer to the route to remove void _removeFromList(Route *route); }; - +} // namespace DCCExController #endif \ No newline at end of file diff --git a/src/DCCEXTurnouts.cpp b/src/DCCEXTurnouts.cpp index 53d2bf3..2750209 100644 --- a/src/DCCEXTurnouts.cpp +++ b/src/DCCEXTurnouts.cpp @@ -27,8 +27,10 @@ */ #include "DCCEXTurnouts.h" -#include +#include + +namespace DCCExController { Turnout *Turnout::_first = nullptr; Turnout::Turnout(int id, bool thrown) { @@ -139,3 +141,4 @@ void Turnout::_removeFromList(Turnout *turnout) { } } } +} // namespace DCCExController \ No newline at end of file diff --git a/src/DCCEXTurnouts.h b/src/DCCEXTurnouts.h index 7ef5b05..857b326 100644 --- a/src/DCCEXTurnouts.h +++ b/src/DCCEXTurnouts.h @@ -29,8 +29,7 @@ #ifndef DCCEXTURNOUTS_H #define DCCEXTURNOUTS_H -#include - +namespace DCCExController { /// @brief Class to contain and maintain the various Turnout/Point attributes and methods class Turnout { public: @@ -94,4 +93,6 @@ class Turnout { void _removeFromList(Turnout *turnout); }; +} // namespace DCCExController + #endif \ No newline at end of file diff --git a/src/DCCEXTurntables.cpp b/src/DCCEXTurntables.cpp index 67bb092..aba3be6 100644 --- a/src/DCCEXTurntables.cpp +++ b/src/DCCEXTurntables.cpp @@ -27,10 +27,10 @@ */ #include "DCCEXTurntables.h" -#include +#include // class TurntableIndex - +namespace DCCExController { TurntableIndex::TurntableIndex(int ttId, int id, int angle, const char *name) { _ttId = ttId; _id = id; @@ -231,3 +231,4 @@ void Turntable::_removeFromList(Turntable *turntable) { } } } +} // namespace DCCExController \ No newline at end of file diff --git a/src/DCCEXTurntables.h b/src/DCCEXTurntables.h index d5377b0..f851a7a 100644 --- a/src/DCCEXTurntables.h +++ b/src/DCCEXTurntables.h @@ -29,8 +29,7 @@ #ifndef DCCEXTURNTABLES_H #define DCCEXTURNTABLES_H -#include - +namespace DCCExController { enum TurntableType { TurntableTypeDCC = 0, TurntableTypeEXTT = 1, @@ -187,5 +186,6 @@ class Turntable { /// @param turntable Pointer to the turntable to remove void _removeFromList(Turntable *turntable); }; +} // namespace DCCExController #endif \ No newline at end of file diff --git a/src/DCCStream.h b/src/DCCStream.h new file mode 100644 index 0000000..a23472d --- /dev/null +++ b/src/DCCStream.h @@ -0,0 +1,23 @@ +#ifndef _STREAM_H +#define _STREAM_H + +#include +#include + +namespace DCCExController { + + class DCCStream { +public: + virtual ~DCCStream() {} + + virtual int available() const = 0; + virtual int read() = 0; + virtual size_t write(const uint8_t *buffer, size_t size) = 0; + virtual void flush() = 0; + virtual void println(const char* format, ...) = 0; + virtual void print(const char* format, ...) = 0; +}; + +}; + +#endif \ No newline at end of file diff --git a/targets/arduino/ArduinoStream.h b/targets/arduino/ArduinoStream.h new file mode 100644 index 0000000..418e12c --- /dev/null +++ b/targets/arduino/ArduinoStream.h @@ -0,0 +1,31 @@ +#ifndef ARDUINOSTREAM_H +#define ARDUINOSTREAM_H +#include "../../src/DCCStream.h" +#include +#include + +class ArduinoStream : public DCCExController::DCCStream { + private: + WiFiClient &client; + public: + + ArduinoStream(WifiClient &client): client(client){ + + } + ~ArduinoStream() {} + + int available() { return client.available(); } + int read() {return client.read();} + size_t write(const uint8_t *buffer, size_t size){ + return client.write(buffer,size); + }; + void flush() {return client.flush(); }; + void println(const char *format, ...){ + Serial.println(format, ...); + }; + void print(const char *format, ...){ + Serial.print(format, ...); + }; +}; + +#endif // ARDUINOSTREAM_H \ No newline at end of file From 9758f1b4c604f96ec4124e7e331795c293572883 Mon Sep 17 00:00:00 2001 From: Mathew Winters Date: Wed, 26 Nov 2025 19:31:33 +1300 Subject: [PATCH 2/4] added esp-idf for esp32 --- .gitignore | 1 + CMakeLists.txt | 67 ++++++++++++++++++++++------------------------- idf_component.yml | 15 +++++++++++ 3 files changed, 47 insertions(+), 36 deletions(-) create mode 100644 idf_component.yml diff --git a/.gitignore b/.gitignore index 8af5f24..b23b69b 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ venv docs/html docs/latex _build +/.cache diff --git a/CMakeLists.txt b/CMakeLists.txt index 37f30ed..92cf674 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,42 +1,37 @@ -cmake_minimum_required(VERSION 3.11 FATAL_ERROR) -include(FetchContent) -list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/cmake) - -project(DCCEXProtocol LANGUAGES CXX) - -file(GLOB_RECURSE SRC src/*.cpp) -add_library(DCCEXProtocol STATIC ${SRC}) -add_library(DCCEX::Protocol ALIAS DCCEXProtocol) - -# Stuck at C++11 -target_compile_options(DCCEXProtocol PRIVATE -std=c++11) - -# Don't bother users with warnings by setting 'SYSTEM' -if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) - target_include_directories(DCCEXProtocol PUBLIC src) +cmake_minimum_required(VERSION 3.11) + +# Detect ESP-IDF build +if(DEFINED ENV{IDF_PATH}) + file(GLOB_RECURSE SRC src/*.cpp) + set(COMPONENT_SRCS ${SRC}) # Add all your source files here + set(COMPONENT_ADD_INCLUDEDIRS src) + set(COMPONENT_PRIV_INCLUDEDIRS "") + set(COMPONENT_REQUIRES "") + set(COMPONENT_PRIV_REQUIRES "") + register_component() else() - target_include_directories(DCCEXProtocol SYSTEM PUBLIC src) -endif() + include(FetchContent) + list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/cmake) + + project(DCCEXProtocol LANGUAGES CXX) -# # Fetch ArduinoCore-API -# if(NOT TARGET ArduinoCore-API) -# FetchContent_Declare( -# ArduinoCore-API -# GIT_REPOSITORY https://github.com/arduino/ArduinoCore-API -# GIT_TAG 1.4.0) -# FetchContent_Populate(ArduinoCore-API) -# find_package(ArduinoCore-API) -# endif() + file(GLOB_RECURSE SRC src/*.cpp) + add_library(DCCEXProtocol STATIC ${SRC}) + add_library(DCCEX::Protocol ALIAS DCCEXProtocol) -# target_link_libraries(DCCEXProtocol PUBLIC ArduinoCore-API) + # Stuck at C++11 + target_compile_options(DCCEXProtocol PRIVATE -std=c++11) -foreach(FILE ${SRC}) - message(${FILE}) -endforeach() + # Don't bother users with warnings by setting 'SYSTEM' + if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) + target_include_directories(DCCEXProtocol PUBLIC src) + else() + target_include_directories(DCCEXProtocol SYSTEM PUBLIC src) + endif() -source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SRC}) + foreach(FILE ${SRC}) + message(${FILE}) + endforeach() -# if(PROJECT_IS_TOP_LEVEL) -# add_subdirectory(docs) -# add_subdirectory(tests) -# endif() + source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SRC}) +endif() diff --git a/idf_component.yml b/idf_component.yml new file mode 100644 index 0000000..701a01a --- /dev/null +++ b/idf_component.yml @@ -0,0 +1,15 @@ +version: 0.1.0 +name: DCCEXProtocol +description: "DCCEXProtocol library for ESP-IDF" +# dependencies: + # Add ESP-IDF components your library depends on, for example: + # idf: "driver" + # idf: "esp_timer" + # idf: "freertos" + # idf: "cxx" + # Uncomment and add as needed +targets: + - esp32 + - esp32s2 + - esp32s3 + - esp32c3 \ No newline at end of file From 21542c825dc6eba89cbe4ee8ec029689c2f9587f Mon Sep 17 00:00:00 2001 From: Mathew Winters Date: Mon, 16 Mar 2026 20:15:15 +1300 Subject: [PATCH 3/4] Change to make usable without Arduino - eg ESPIDF, Pico SDK --- .gitignore | 4 + .vscode/tasks.json | 49 ++++++++ CMakeLists.txt | 31 +++++ .../DCCEXProtocol_Basic.ino | 59 +++++++++- .../DCCEXProtocol_CSConsist_Control.ino | 58 +++++++++- .../DCCEXProtocol_Consist_Control.ino | 57 ++++++++- .../DCCEXProtocol_Delegate.ino | 55 ++++++++- .../DCCEXProtocol_Loco_Control.ino | 55 ++++++++- .../DCCEXProtocol_Multi_Throttle_Control.ino | 56 ++++++++- .../DCCEXProtocol_Roster_etc.ino | 55 ++++++++- .../DCCEXProtocol_Serial.ino | 57 ++++++++- .../DCCEXProtocol_Track_type.ino | 55 ++++++++- .../DCCEXProtocol_Turnout_Control.ino | 55 ++++++++- src/DCCEXCSConsist.cpp | 3 + src/DCCEXCSConsist.h | 4 +- src/DCCEXInbound.cpp | 1 + src/DCCEXInbound.h | 1 + src/DCCEXLoco.cpp | 9 +- src/DCCEXLoco.h | 1 + src/DCCEXProtocol.cpp | 36 ++++-- src/DCCEXProtocol.h | 5 +- src/DCCEXRoutes.cpp | 1 + src/DCCEXRoutes.h | 1 + src/DCCEXTurnouts.cpp | 1 + src/DCCEXTurnouts.h | 1 + src/DCCEXTurntables.cpp | 1 + src/DCCEXTurntables.h | 1 + src/DCCMillis.h | 52 +++++++++ test/mocks/Arduino.h | 109 ------------------ test/mocks/MockDCCEXProtocolDelegate.h | 6 + test/mocks/MockDCCMillis.h | 12 ++ test/mocks/Print.h | 94 --------------- test/mocks/Stream.h | 62 ++++++++-- test/mocks/millis.h | 12 ++ test/setup/TestHarnessBase.hpp | 22 ++-- test/setup/TestHarnessNoDelegate.h | 10 +- test/setup/TurnoutTests.h | 2 +- 37 files changed, 815 insertions(+), 278 deletions(-) create mode 100644 .vscode/tasks.json create mode 100644 src/DCCMillis.h delete mode 100644 test/mocks/Arduino.h create mode 100644 test/mocks/MockDCCMillis.h delete mode 100644 test/mocks/Print.h create mode 100644 test/mocks/millis.h diff --git a/.gitignore b/.gitignore index 75db6ab..c1b94f5 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,7 @@ _build lcov.info test_coverage/ +# cmake cache. +.cache/ +build/ +build-*/ \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..3b284e8 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,49 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "CMake Configure Tests (Debug)", + "type": "shell", + "command": "cmake", + "args": [ + "-S", + ".", + "-B", + "build-cmake-tests-debug", + "-DCMAKE_BUILD_TYPE=Debug", + "-DDCCEX_BUILD_TESTS=ON" + ], + "group": "build" + }, + { + "label": "CMake Build Tests (Debug)", + "type": "shell", + "command": "cmake", + "args": [ + "--build", + "build-cmake-tests-debug", + "-j" + ], + "dependsOn": "CMake Configure Tests (Debug)", + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "CMake Run Tests (Debug)", + "type": "shell", + "command": "ctest", + "args": [ + "--test-dir", + "build-cmake-tests-debug", + "--output-on-failure" + ], + "dependsOn": "CMake Build Tests (Debug)", + "group": { + "kind": "test", + "isDefault": true + } + } + ] +} diff --git a/CMakeLists.txt b/CMakeLists.txt index 92cf674..5901da4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,8 @@ else() project(DCCEXProtocol LANGUAGES CXX) + option(DCCEX_BUILD_TESTS "Build native GoogleTest test target" ON) + file(GLOB_RECURSE SRC src/*.cpp) add_library(DCCEXProtocol STATIC ${SRC}) add_library(DCCEX::Protocol ALIAS DCCEXProtocol) @@ -34,4 +36,33 @@ else() endforeach() source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SRC}) + + if(DCCEX_BUILD_TESTS) + include(CTest) + enable_testing() + + FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/tags/v1.17.0.zip + ) + FetchContent_MakeAvailable(googletest) + + file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS test/*.cpp) + add_executable(DCCEXProtocolTests ${TEST_SOURCES}) + + target_compile_definitions(DCCEXProtocolTests PRIVATE NATIVE_TESTING) + target_include_directories(DCCEXProtocolTests PRIVATE test/mocks test/setup) + target_link_libraries(DCCEXProtocolTests PRIVATE DCCEXProtocol GTest::gtest GTest::gmock) + + include(GoogleTest) + gtest_discover_tests(DCCEXProtocolTests) + + add_custom_target(check + COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure + DEPENDS DCCEXProtocolTests + USES_TERMINAL + ) + + add_custom_target(run-tests DEPENDS check) + endif() endif() diff --git a/examples/DCCEXProtocol_Basic/DCCEXProtocol_Basic.ino b/examples/DCCEXProtocol_Basic/DCCEXProtocol_Basic.ino index a1c3f6d..f3ffdff 100644 --- a/examples/DCCEXProtocol_Basic/DCCEXProtocol_Basic.ino +++ b/examples/DCCEXProtocol_Basic/DCCEXProtocol_Basic.ino @@ -7,8 +7,11 @@ // Peter Akers (Flash62au), Peter Cole (PeteGSX) and Chris Harlow (UKBloke), 2023 // Luca Dentella, 2020 +#include #include #include +#include +#include // If we haven't got a custom config.h, use the example @@ -19,9 +22,59 @@ #include "config.example.h" #endif +using namespace DCCExController; + +class ArduinoDCCMillis : public DCCMillis { +public: + unsigned long millis() const override { return ::millis(); } +}; + +class ArduinoDCCStream : public DCCStream { +public: + explicit ArduinoDCCStream(Stream &stream) : _stream(stream) {} + + int available() const override { + return const_cast(_stream).available(); + } + + int read() override { return _stream.read(); } + + size_t write(const uint8_t *buffer, size_t size) override { + return _stream.write(buffer, size); + } + + void flush() override { _stream.flush(); } + + void println(const char *format, ...) override { + char message[160]; + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + _stream.println(message); + } + + void print(const char *format, ...) override { + char message[160]; + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + _stream.print(message); + } + +private: + Stream &_stream; +}; + + + // Global objects WiFiClient client; -DCCEXProtocol dccexProtocol; +ArduinoDCCMillis dccMillis; +ArduinoDCCStream dccTransport(client); +ArduinoDCCStream dccLog(Serial); +DCCEXProtocol dccexProtocol(&dccMillis); void setup() { @@ -46,12 +99,12 @@ void setup() { } Serial.println("Connected to the server"); - dccexProtocol.setLogStream(&Serial); + dccexProtocol.setLogStream(&dccLog); dccexProtocol.enableHeartbeat(); // Pass the communication to wiThrottleProtocol - dccexProtocol.connect(&client); + dccexProtocol.connect(&dccTransport); Serial.println("DCC-EX connected"); } diff --git a/examples/DCCEXProtocol_CSConsist_Control/DCCEXProtocol_CSConsist_Control.ino b/examples/DCCEXProtocol_CSConsist_Control/DCCEXProtocol_CSConsist_Control.ino index cc7e1fb..c36511e 100644 --- a/examples/DCCEXProtocol_CSConsist_Control/DCCEXProtocol_CSConsist_Control.ino +++ b/examples/DCCEXProtocol_CSConsist_Control/DCCEXProtocol_CSConsist_Control.ino @@ -5,6 +5,7 @@ // // Peter Cole (PeteGSX), 2026 +#include #include #include @@ -16,6 +17,52 @@ #include "config.example.h" #endif +using namespace DCCExController; + +class ArduinoDCCMillis : public DCCMillis { +public: + unsigned long millis() const override { return ::millis(); } +}; + +class ArduinoDCCStream : public DCCStream { +public: + explicit ArduinoDCCStream(Stream &stream) : _stream(stream) {} + + int available() const override { + return const_cast(_stream).available(); + } + + int read() override { return _stream.read(); } + + size_t write(const uint8_t *buffer, size_t size) override { + return _stream.write(buffer, size); + } + + void flush() override { _stream.flush(); } + + void println(const char *format, ...) override { + char message[160]; + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + _stream.println(message); + } + + void print(const char *format, ...) override { + char message[160]; + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + _stream.print(message); + } + +private: + Stream &_stream; +}; + + // Delegate class class MyDelegate : public DCCEXProtocolDelegate { @@ -47,7 +94,10 @@ CSConsist *csConsist = nullptr; // Global objects WiFiClient client; -DCCEXProtocol dccexProtocol; +ArduinoDCCMillis dccMillis; +ArduinoDCCStream dccTransport(client); +ArduinoDCCStream dccLog(Serial); +DCCEXProtocol dccexProtocol(&dccMillis); MyDelegate myDelegate; void setup() { @@ -74,7 +124,7 @@ void setup() { Serial.println("Connected to the server"); // Logging on Serial - dccexProtocol.setLogStream(&Serial); + dccexProtocol.setLogStream(&dccLog); // Pass the delegate instance to wiThrottleProtocol dccexProtocol.setDelegate(&myDelegate); @@ -82,7 +132,7 @@ void setup() { dccexProtocol.enableHeartbeat(); // Pass the communication to wiThrottleProtocol - dccexProtocol.connect(&client); + dccexProtocol.connect(&dccTransport); Serial.println("DCC-EX connected"); // Turn track power on for locos to move @@ -95,7 +145,7 @@ void loop() { // parse incoming messages dccexProtocol.check(); - if (!consist) { + if (!csConsist) { // Create a new CSConsist for loco address 11 in the normal direction of travel, and replicate functions across the // consist. // By default, functions will only affect the lead loco diff --git a/examples/DCCEXProtocol_Consist_Control/DCCEXProtocol_Consist_Control.ino b/examples/DCCEXProtocol_Consist_Control/DCCEXProtocol_Consist_Control.ino index ab49861..491c648 100644 --- a/examples/DCCEXProtocol_Consist_Control/DCCEXProtocol_Consist_Control.ino +++ b/examples/DCCEXProtocol_Consist_Control/DCCEXProtocol_Consist_Control.ino @@ -17,6 +17,7 @@ manual operation only. // Peter Akers (Flash62au), Peter Cole (PeteGSX) and Chris Harlow (UKBloke), 2023 // Luca Dentella, 2020 +#include #include #include @@ -28,6 +29,52 @@ manual operation only. #include "config.example.h" #endif +using namespace DCCExController; + +class ArduinoDCCMillis : public DCCMillis { +public: + unsigned long millis() const override { return ::millis(); } +}; + +class ArduinoDCCStream : public DCCStream { +public: + explicit ArduinoDCCStream(Stream &stream) : _stream(stream) {} + + int available() const override { + return const_cast(_stream).available(); + } + + int read() override { return _stream.read(); } + + size_t write(const uint8_t *buffer, size_t size) override { + return _stream.write(buffer, size); + } + + void flush() override { _stream.flush(); } + + void println(const char *format, ...) override { + char message[160]; + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + _stream.println(message); + } + + void print(const char *format, ...) override { + char message[160]; + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + _stream.print(message); + } + +private: + Stream &_stream; +}; + + // Delegate class class MyDelegate : public DCCEXProtocolDelegate { @@ -80,7 +127,11 @@ Consist *consist = nullptr; // Global objects WiFiClient client; -DCCEXProtocol dccexProtocol; +ArduinoDCCMillis dccMillis; +ArduinoDCCStream dccTransport(client); +ArduinoDCCStream dccLog(Serial); +DCCEXProtocol dccexProtocol(&dccMillis); + MyDelegate myDelegate; void setup() { @@ -109,7 +160,7 @@ void setup() { Serial.println("Connected to the server"); // Logging on Serial - dccexProtocol.setLogStream(&Serial); + dccexProtocol.setLogStream(&dccLog); // Pass the delegate instance to wiThrottleProtocol dccexProtocol.setDelegate(&myDelegate); @@ -117,7 +168,7 @@ void setup() { dccexProtocol.enableHeartbeat(); // Pass the communication to wiThrottleProtocol - dccexProtocol.connect(&client); + dccexProtocol.connect(&dccTransport); Serial.println("DCC-EX connected"); dccexProtocol.requestServerVersion(); diff --git a/examples/DCCEXProtocol_Delegate/DCCEXProtocol_Delegate.ino b/examples/DCCEXProtocol_Delegate/DCCEXProtocol_Delegate.ino index ac12307..93551e8 100644 --- a/examples/DCCEXProtocol_Delegate/DCCEXProtocol_Delegate.ino +++ b/examples/DCCEXProtocol_Delegate/DCCEXProtocol_Delegate.ino @@ -6,6 +6,7 @@ // Peter Akers (Flash62au), Peter Cole (PeteGSX) and Chris Harlow (UKBloke), 2023 // Luca Dentella, 2020 +#include #include #include @@ -18,6 +19,51 @@ #include "config.example.h" #endif +using namespace DCCExController; + +class ArduinoDCCMillis : public DCCMillis { +public: + unsigned long millis() const override { return ::millis(); } +}; + +class ArduinoDCCStream : public DCCStream { +public: + explicit ArduinoDCCStream(Stream &stream) : _stream(stream) {} + + int available() const override { + return const_cast(_stream).available(); + } + + int read() override { return _stream.read(); } + + size_t write(const uint8_t *buffer, size_t size) override { + return _stream.write(buffer, size); + } + + void flush() override { _stream.flush(); } + + void println(const char *format, ...) override { + char message[160]; + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + _stream.println(message); + } + + void print(const char *format, ...) override { + char message[160]; + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + _stream.print(message); + } + +private: + Stream &_stream; +}; + // Delegate class class MyDelegate : public DCCEXProtocolDelegate { @@ -34,7 +80,10 @@ public: // Global objects WiFiClient client; -DCCEXProtocol dccexProtocol; +ArduinoDCCMillis dccMillis; +ArduinoDCCStream dccTransport(client); +ArduinoDCCStream dccLog(Serial); +DCCEXProtocol dccexProtocol(&dccMillis); MyDelegate myDelegate; void setup() { @@ -61,10 +110,10 @@ void setup() { Serial.println("Connected to the server"); // Setup logging to serial console - dccexProtocol.setLogStream(&Serial); + dccexProtocol.setLogStream(&dccLog); // Pass the delegate instance to wiThrottleProtocol - dccexProtocol.setDelegate(&myDelegate); + dccexProtocol.setDelegate(&dccTransport); // Pass the communication to wiThrottleProtocol dccexProtocol.connect(&client); diff --git a/examples/DCCEXProtocol_Loco_Control/DCCEXProtocol_Loco_Control.ino b/examples/DCCEXProtocol_Loco_Control/DCCEXProtocol_Loco_Control.ino index 64f1d05..d956d94 100644 --- a/examples/DCCEXProtocol_Loco_Control/DCCEXProtocol_Loco_Control.ino +++ b/examples/DCCEXProtocol_Loco_Control/DCCEXProtocol_Loco_Control.ino @@ -6,6 +6,7 @@ // Peter Akers (Flash62au), Peter Cole (PeteGSX) and Chris Harlow (UKBloke), 2023 // Luca Dentella, 2020 +#include #include #include @@ -17,6 +18,51 @@ #include "config.example.h" #endif +using namespace DCCExController; + +class ArduinoDCCMillis : public DCCMillis { +public: + unsigned long millis() const override { return ::millis(); } +}; + +class ArduinoDCCStream : public DCCStream { +public: + explicit ArduinoDCCStream(Stream &stream) : _stream(stream) {} + + int available() const override { + return const_cast(_stream).available(); + } + + int read() override { return _stream.read(); } + + size_t write(const uint8_t *buffer, size_t size) override { + return _stream.write(buffer, size); + } + + void flush() override { _stream.flush(); } + + void println(const char *format, ...) override { + char message[160]; + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + _stream.println(message); + } + + void print(const char *format, ...) override { + char message[160]; + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + _stream.print(message); + } + +private: + Stream &_stream; +}; + void printRoster(); // Delegate class @@ -70,7 +116,10 @@ Loco *loco = nullptr; // Global objects WiFiClient client; -DCCEXProtocol dccexProtocol; +ArduinoDCCMillis dccMillis; +ArduinoDCCStream dccTransport(client); +ArduinoDCCStream dccLog(Serial); +DCCEXProtocol dccexProtocol(&dccMillis); MyDelegate myDelegate; void setup() { @@ -97,13 +146,13 @@ void setup() { Serial.println("Connected to the server"); // Logging on Serial - dccexProtocol.setLogStream(&Serial); + dccexProtocol.setLogStream(&dccLog); // Pass the delegate instance to wiThrottleProtocol dccexProtocol.setDelegate(&myDelegate); // Pass the communication to wiThrottleProtocol - dccexProtocol.connect(&client); + dccexProtocol.connect(&dccTransport); Serial.println("DCC-EX connected"); dccexProtocol.requestServerVersion(); diff --git a/examples/DCCEXProtocol_Multi_Throttle_Control/DCCEXProtocol_Multi_Throttle_Control.ino b/examples/DCCEXProtocol_Multi_Throttle_Control/DCCEXProtocol_Multi_Throttle_Control.ino index acec9ff..91a2cd0 100644 --- a/examples/DCCEXProtocol_Multi_Throttle_Control/DCCEXProtocol_Multi_Throttle_Control.ino +++ b/examples/DCCEXProtocol_Multi_Throttle_Control/DCCEXProtocol_Multi_Throttle_Control.ino @@ -6,6 +6,7 @@ // Peter Akers (Flash62au), Peter Cole (PeteGSX) and Chris Harlow (UKBloke), 2023 // Luca Dentella, 2020 +#include #include #include @@ -18,6 +19,51 @@ #include "config.example.h" #endif +using namespace DCCExController; + +class ArduinoDCCMillis : public DCCMillis { +public: + unsigned long millis() const override { return ::millis(); } +}; + +class ArduinoDCCStream : public DCCStream { +public: + explicit ArduinoDCCStream(Stream &stream) : _stream(stream) {} + + int available() const override { + return const_cast(_stream).available(); + } + + int read() override { return _stream.read(); } + + size_t write(const uint8_t *buffer, size_t size) override { + return _stream.write(buffer, size); + } + + void flush() override { _stream.flush(); } + + void println(const char *format, ...) override { + char message[160]; + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + _stream.println(message); + } + + void print(const char *format, ...) override { + char message[160]; + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + _stream.print(message); + } + +private: + Stream &_stream; +}; + // Delegate class class MyDelegate : public DCCEXProtocolDelegate { @@ -99,7 +145,11 @@ unsigned long lastTime = 0; // Global objects WiFiClient client; -DCCEXProtocol dccexProtocol; +ArduinoDCCMillis dccMillis; +ArduinoDCCStream dccTransport(client); +ArduinoDCCStream dccLog(Serial); +DCCEXProtocol dccexProtocol(&dccMillis); + MyDelegate myDelegate; // Define an array for two throttles @@ -130,13 +180,13 @@ void setup() { Serial.println("Connected to the server"); // Logging on Serial - dccexProtocol.setLogStream(&Serial); + dccexProtocol.setLogStream(&dccLog); // Pass the delegate instance to wiThrottleProtocol dccexProtocol.setDelegate(&myDelegate); // Pass the communication to wiThrottleProtocol - dccexProtocol.connect(&client); + dccexProtocol.connect(&dccTransport); Serial.println("DCC-EX connected"); dccexProtocol.requestServerVersion(); diff --git a/examples/DCCEXProtocol_Roster_etc/DCCEXProtocol_Roster_etc.ino b/examples/DCCEXProtocol_Roster_etc/DCCEXProtocol_Roster_etc.ino index 9a5936c..072c31b 100644 --- a/examples/DCCEXProtocol_Roster_etc/DCCEXProtocol_Roster_etc.ino +++ b/examples/DCCEXProtocol_Roster_etc/DCCEXProtocol_Roster_etc.ino @@ -6,6 +6,7 @@ // Peter Akers (Flash62au), Peter Cole (PeteGSX) and Chris Harlow (UKBloke), 2023 // Luca Dentella, 2020 +#include #include #include @@ -18,6 +19,51 @@ #include "config.example.h" #endif +using namespace DCCExController; + +class ArduinoDCCMillis : public DCCMillis { +public: + unsigned long millis() const override { return ::millis(); } +}; + +class ArduinoDCCStream : public DCCStream { +public: + explicit ArduinoDCCStream(Stream &stream) : _stream(stream) {} + + int available() const override { + return const_cast(_stream).available(); + } + + int read() override { return _stream.read(); } + + size_t write(const uint8_t *buffer, size_t size) override { + return _stream.write(buffer, size); + } + + void flush() override { _stream.flush(); } + + void println(const char *format, ...) override { + char message[160]; + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + _stream.println(message); + } + + void print(const char *format, ...) override { + char message[160]; + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + _stream.print(message); + } + +private: + Stream &_stream; +}; + void printRoster(); void printTurnouts(); void printRoutes(); @@ -69,7 +115,10 @@ bool done = false; // Global objects WiFiClient client; -DCCEXProtocol dccexProtocol; +ArduinoDCCMillis dccMillis; +ArduinoDCCStream dccTransport(client); +ArduinoDCCStream dccLog(Serial); +DCCEXProtocol dccexProtocol(&dccMillis); MyDelegate myDelegate; void printRoster() { @@ -167,13 +216,13 @@ void setup() { Serial.println("Connected to the server"); // Enable logging on Serial - dccexProtocol.setLogStream(&Serial); + dccexProtocol.setLogStream(&dccLog); // Pass the delegate instance to wiThrottleProtocol dccexProtocol.setDelegate(&myDelegate); // Pass the communication to wiThrottleProtocol - dccexProtocol.connect(&client); + dccexProtocol.connect(&dccTransport); Serial.println("DCC-EX connected"); dccexProtocol.requestServerVersion(); diff --git a/examples/DCCEXProtocol_Serial/DCCEXProtocol_Serial.ino b/examples/DCCEXProtocol_Serial/DCCEXProtocol_Serial.ino index 74b9595..c23728e 100644 --- a/examples/DCCEXProtocol_Serial/DCCEXProtocol_Serial.ino +++ b/examples/DCCEXProtocol_Serial/DCCEXProtocol_Serial.ino @@ -6,6 +6,7 @@ // // Peter Cole (PeteGSX) 2024 +#include #include // If we haven't got a custom config.h, use the example @@ -24,6 +25,51 @@ #define CLIENT Serial1 // All DCCEXProtocol commands/responses/broadcasts use this #endif +using namespace DCCExController; + +class ArduinoDCCMillis : public DCCMillis { +public: + unsigned long millis() const override { return ::millis(); } +}; + +class ArduinoDCCStream : public DCCStream { +public: + explicit ArduinoDCCStream(Stream &stream) : _stream(stream) {} + + int available() const override { + return const_cast(_stream).available(); + } + + int read() override { return _stream.read(); } + + size_t write(const uint8_t *buffer, size_t size) override { + return _stream.write(buffer, size); + } + + void flush() override { _stream.flush(); } + + void println(const char *format, ...) override { + char message[160]; + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + _stream.println(message); + } + + void print(const char *format, ...) override { + char message[160]; + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + _stream.print(message); + } + +private: + Stream &_stream; +}; + // Declare functions to call from our delegate void printRoster(); void printTurnouts(); @@ -68,7 +114,7 @@ public: CONSOLE.println("\n\n"); } - void receivedScreenUpdate(int screen, int row, char *message) override { + void receivedScreenUpdate(int screen, int row, const char *message) override { CONSOLE.println("\n\nReceived screen|row|message"); CONSOLE.print(screen); CONSOLE.print("|"); @@ -80,7 +126,10 @@ public: }; // Global objects -DCCEXProtocol dccexProtocol; +ArduinoDCCMillis dccMillis; +ArduinoDCCStream dccTransport(CLIENT); +ArduinoDCCStream dccLog(CONSOLE); +DCCEXProtocol dccexProtocol(&dccMillis); MyDelegate myDelegate; void printRoster() { @@ -161,13 +210,13 @@ void setup() { CONSOLE.println(F("")); // Direct logs to CONSOLE - dccexProtocol.setLogStream(&CONSOLE); + dccexProtocol.setLogStream(&dccLog); // Set the delegate for broadcasts/responses dccexProtocol.setDelegate(&myDelegate); // Connect to the CS via CLIENT - dccexProtocol.connect(&CLIENT); + dccexProtocol.connect(&dccTransport); CONSOLE.println(F("DCC-EX connected")); dccexProtocol.requestServerVersion(); diff --git a/examples/DCCEXProtocol_Track_type/DCCEXProtocol_Track_type.ino b/examples/DCCEXProtocol_Track_type/DCCEXProtocol_Track_type.ino index c06a114..7b986f4 100644 --- a/examples/DCCEXProtocol_Track_type/DCCEXProtocol_Track_type.ino +++ b/examples/DCCEXProtocol_Track_type/DCCEXProtocol_Track_type.ino @@ -6,6 +6,7 @@ // Peter Akers (Flash62au), Peter Cole (PeteGSX) and Chris Harlow (UKBloke), 2023 // Luca Dentella, 2020 +#include #include #include @@ -18,6 +19,51 @@ #include "config.example.h" #endif +using namespace DCCExController; + +class ArduinoDCCMillis : public DCCMillis { +public: + unsigned long millis() const override { return ::millis(); } +}; + +class ArduinoDCCStream : public DCCStream { +public: + explicit ArduinoDCCStream(Stream &stream) : _stream(stream) {} + + int available() const override { + return const_cast(_stream).available(); + } + + int read() override { return _stream.read(); } + + size_t write(const uint8_t *buffer, size_t size) override { + return _stream.write(buffer, size); + } + + void flush() override { _stream.flush(); } + + void println(const char *format, ...) override { + char message[160]; + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + _stream.println(message); + } + + void print(const char *format, ...) override { + char message[160]; + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + _stream.print(message); + } + +private: + Stream &_stream; +}; + // Delegate class class MyDelegate : public DCCEXProtocolDelegate { @@ -34,7 +80,10 @@ public: // Global objects WiFiClient client; -DCCEXProtocol dccexProtocol; +ArduinoDCCMillis dccMillis; +ArduinoDCCStream dccTransport(client); +ArduinoDCCStream dccLog(Serial); +DCCEXProtocol dccexProtocol(&dccMillis); MyDelegate myDelegate; // for changes @@ -64,13 +113,13 @@ void setup() { } Serial.println("Connected to the server"); - dccexProtocol.setLogStream(&Serial); + dccexProtocol.setLogStream(&dccLog); // Pass the delegate instance to wiThrottleProtocol dccexProtocol.setDelegate(&myDelegate); // Pass the communication to wiThrottleProtocol - dccexProtocol.connect(&client); + dccexProtocol.connect(&dccTransport); Serial.println("DCC-EX connected"); } diff --git a/examples/DCCEXProtocol_Turnout_Control/DCCEXProtocol_Turnout_Control.ino b/examples/DCCEXProtocol_Turnout_Control/DCCEXProtocol_Turnout_Control.ino index 67e2d2f..c3a3713 100644 --- a/examples/DCCEXProtocol_Turnout_Control/DCCEXProtocol_Turnout_Control.ino +++ b/examples/DCCEXProtocol_Turnout_Control/DCCEXProtocol_Turnout_Control.ino @@ -8,6 +8,7 @@ // Peter Akers (Flash62au), Peter Cole (PeteGSX) and Chris Harlow (UKBloke), 2023 // Luca Dentella, 2020 +#include #include #include @@ -20,6 +21,51 @@ #include "config.example.h" #endif +using namespace DCCExController; + +class ArduinoDCCMillis : public DCCMillis { +public: + unsigned long millis() const override { return ::millis(); } +}; + +class ArduinoDCCStream : public DCCStream { +public: + explicit ArduinoDCCStream(Stream &stream) : _stream(stream) {} + + int available() const override { + return const_cast(_stream).available(); + } + + int read() override { return _stream.read(); } + + size_t write(const uint8_t *buffer, size_t size) override { + return _stream.write(buffer, size); + } + + void flush() override { _stream.flush(); } + + void println(const char *format, ...) override { + char message[160]; + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + _stream.println(message); + } + + void print(const char *format, ...) override { + char message[160]; + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + _stream.print(message); + } + +private: + Stream &_stream; +}; + void printTurnouts(); // Delegate class @@ -57,7 +103,10 @@ Turnout *turnout2 = nullptr; // Global objects WiFiClient client; -DCCEXProtocol dccexProtocol; +ArduinoDCCMillis dccMillis; +ArduinoDCCStream dccTransport(client); +ArduinoDCCStream dccLog(Serial); +DCCEXProtocol dccexProtocol(&dccMillis); MyDelegate myDelegate; void printTurnouts() { @@ -96,13 +145,13 @@ void setup() { Serial.println("Connected to the server"); // Logging on Serial - dccexProtocol.setLogStream(&Serial); + dccexProtocol.setLogStream(&dccLog); // Pass the delegate instance to wiThrottleProtocol dccexProtocol.setDelegate(&myDelegate); // Pass the communication to wiThrottleProtocol - dccexProtocol.connect(&client); + dccexProtocol.connect(&dccTransport); Serial.println("DCC-EX connected"); dccexProtocol.requestServerVersion(); diff --git a/src/DCCEXCSConsist.cpp b/src/DCCEXCSConsist.cpp index 7a70981..7a99e81 100644 --- a/src/DCCEXCSConsist.cpp +++ b/src/DCCEXCSConsist.cpp @@ -23,6 +23,7 @@ #include "DCCEXCSConsist.h" // CSConsist public methods +namespace DCCExController { CSConsist *CSConsist::_first = nullptr; bool CSConsist::_alwaysReplicateFunctions = false; @@ -215,3 +216,5 @@ CSConsist::~CSConsist() { } } } + +} // namespace DCCExController \ No newline at end of file diff --git a/src/DCCEXCSConsist.h b/src/DCCEXCSConsist.h index b8c439f..2d7b982 100644 --- a/src/DCCEXCSConsist.h +++ b/src/DCCEXCSConsist.h @@ -24,8 +24,9 @@ #define DCCEXCSCONSIST_H #include "DCCEXLoco.h" -#include +#include +namespace DCCExController { /** * @brief Structure for a CSConsistMember */ @@ -202,5 +203,6 @@ class CSConsist { static CSConsist *_first; static bool _alwaysReplicateFunctions; }; +} // namespace DCCExController #endif // DCCEXCSCONSIST_H diff --git a/src/DCCEXInbound.cpp b/src/DCCEXInbound.cpp index 914f994..b70db76 100644 --- a/src/DCCEXInbound.cpp +++ b/src/DCCEXInbound.cpp @@ -37,6 +37,7 @@ #include namespace DCCExController { + // Internal stuff for the parser and getters. const int32_t QUOTE_FLAG = 0x77777000; const int32_t QUOTE_FLAG_AREA = 0xFFFFF000; diff --git a/src/DCCEXInbound.h b/src/DCCEXInbound.h index 1fcec5f..d9d2cab 100644 --- a/src/DCCEXInbound.h +++ b/src/DCCEXInbound.h @@ -41,6 +41,7 @@ These parameters are ONLY VALID until you next call parse. */ namespace DCCExController { + /// @brief Inbound DCC-EX command parser class to parse commands and provide interpreted parameters class DCCEXInbound { public: diff --git a/src/DCCEXLoco.cpp b/src/DCCEXLoco.cpp index f43ec00..794b710 100644 --- a/src/DCCEXLoco.cpp +++ b/src/DCCEXLoco.cpp @@ -28,9 +28,10 @@ */ #include "DCCEXLoco.h" -#include -#include +#include #include +#include +#include namespace DCCExController { // class Loco @@ -336,9 +337,9 @@ void Consist::addLoco(int address, Facing facing) { if (_locoCount == 0) { facing = FacingForward; if (_name == nullptr) { - int addressLength = (address == 0) ? 1 : log10(address) + 1; + int addressLength = snprintf(nullptr, 0, "%d", address); char *newName = new char[addressLength + 1]; - itoa(address, newName, 10); + snprintf(newName, addressLength + 1, "%d", address); setName(newName); delete[] newName; } diff --git a/src/DCCEXLoco.h b/src/DCCEXLoco.h index 8b2b908..dde2aea 100644 --- a/src/DCCEXLoco.h +++ b/src/DCCEXLoco.h @@ -33,6 +33,7 @@ #include namespace DCCExController { + static const int MAX_FUNCTIONS = 32; const int MAX_OBJECT_NAME_LENGTH = 30; // including Loco name, Turnout/Point names, Route names, etc. names #define MAX_SINGLE_COMMAND_PARAM_LENGTH 500 // Unfortunately includes the function list for an individual loco diff --git a/src/DCCEXProtocol.cpp b/src/DCCEXProtocol.cpp index b249bdb..865b117 100644 --- a/src/DCCEXProtocol.cpp +++ b/src/DCCEXProtocol.cpp @@ -43,10 +43,12 @@ Function/method prefixes #include #include +#include #include #include namespace DCCExController { + static const int MIN_SPEED = 0; static const int MAX_SPEED = 126; @@ -54,8 +56,13 @@ static const int MAX_SPEED = 126; // Public methods // Protocol and server methods -DCCEXProtocol::DCCEXProtocol(int maxCmdBuffer, int maxCommandParams, unsigned long userChangeDelay) { +DCCEXProtocol::DCCEXProtocol(DCCMillis *millisProvider, int maxCmdBuffer, int maxCommandParams, unsigned long userChangeDelay) { // Init streams + if(millisProvider == nullptr){ + // Cannot proceed without a millis provider, so do not proceed + throw std::invalid_argument("DCCEXProtocol requires a DCCMillis provider for time functions"); + } + _millisProvider = millisProvider; _stream = &_nullStream; _console = &_nullStream; @@ -101,8 +108,13 @@ void DCCEXProtocol::enableHeartbeat(unsigned long heartbeatDelay) { } void DCCEXProtocol::connect(DCCStream *stream) { - _init(); + if(!stream){ + // Cannot connect without a stream, so do not proceed + return; + } + this->_stream = stream; + _init(); } void DCCEXProtocol::disconnect() { return; } @@ -768,7 +780,7 @@ void DCCEXProtocol::_init() { memset(_inputBuffer, 0, sizeof(_inputBuffer)); _nextChar = 0; // last Response time - _lastServerResponseTime = millis(); + _lastServerResponseTime = _millisProvider->millis(); } void DCCEXProtocol::_sendCommand() { @@ -779,14 +791,13 @@ void DCCEXProtocol::_sendCommand() { _console->println(_outboundCommand); } *_outboundCommand = 0; // clear it once it has been sent - _lastHeartbeat = millis(); // If we sent a command, a heartbeat isn't necessary + _lastHeartbeat = _millisProvider->millis(); // If we sent a command, a heartbeat isn't necessary } } void DCCEXProtocol::_processCommand() { - if (_delegate) { - // last Response time - _lastServerResponseTime = _delegate->millis(); + // last Response time + _lastServerResponseTime = _millisProvider->millis(); switch (DCCEXInbound::getOpcode()) { case '@': // Screen update @@ -987,8 +998,8 @@ void DCCEXProtocol::_processScreenUpdate() { //<@ screen row "message"> } void DCCEXProtocol::_sendHeartbeat() { - if (millis() - _lastHeartbeat > _heartbeatDelay) { - _lastHeartbeat = millis(); + if (_millisProvider->millis() - _lastHeartbeat > _heartbeatDelay) { + _lastHeartbeat = _millisProvider->millis(); _sendOpcode('#'); } } @@ -1076,8 +1087,8 @@ void DCCEXProtocol::_processReadResponse() { // - -1 = error } void DCCEXProtocol::_processPendingUserChanges() { - if (millis() - _lastUserChange > _userChangeDelay) { - _lastUserChange = millis(); + if (_millisProvider->millis() - _lastUserChange > _userChangeDelay) { + _lastUserChange = _millisProvider->millis(); _setLocos(Loco::getFirst()); _setLocos(Loco::getFirstLocalLoco()); } @@ -1584,7 +1595,7 @@ void DCCEXProtocol::_cmdAppend(const char *s) { void DCCEXProtocol::_cmdAppend(int n) { char buf[12]; // Enough for -2147483648 - itoa(n, buf, 10); + snprintf(buf, sizeof(buf), "%d", n); _cmdAppend(buf); } @@ -1730,3 +1741,4 @@ void DCCEXProtocol::_sendFourParams(char opcode, int param1, int param2, int par _cmdSend(); } +}; // namespace DCCEX \ No newline at end of file diff --git a/src/DCCEXProtocol.h b/src/DCCEXProtocol.h index 6047c19..0a9cf7b 100644 --- a/src/DCCEXProtocol.h +++ b/src/DCCEXProtocol.h @@ -44,12 +44,14 @@ Version information: MOVED TO DCCEXProtocolVersion.h #include "DCCEXProtocolVersion.h" #include "DCCEXRoutes.h" #include "DCCEXTurnouts.h" +#include "DCCMillis.h" #include "DCCEXTurntables.h" #include "DCCStream.h" #include namespace DCCExController { + const int MAX_OUTBOUND_COMMAND_LENGTH = 100; // Max number of bytes for outbound commands // Valid track power state values @@ -249,7 +251,7 @@ class DCCEXProtocol { /// @param maxCmdBuffer Optional - maximum number of bytes for the command buffer (default 500) /// @param maxCommandParams Optional - maximum number of parameters to parse via the DCCEXInbound parser (default 50) /// @param userChangeDelay Optional - time in ms between sending throttle changes (default 100) - DCCEXProtocol(int maxCmdBuffer = 500, int maxCommandParams = 50, unsigned long userChangeDelay = 100); + DCCEXProtocol(DCCMillis *millisProvider, int maxCmdBuffer = 500, int maxCommandParams = 50, unsigned long userChangeDelay = 100); /// @brief Destructor for the DCCEXProtocol object ~DCCEXProtocol(); @@ -927,6 +929,7 @@ class DCCEXProtocol { unsigned long _userChangeDelay; // Delay in ms between sending throttle commands unsigned long _lastUserChange; // Time in ms of the last throttle command bool _debug = false; // Enable output of send/receive commands to console + DCCMillis *_millisProvider; // Pointer to a DCCMillis provider for time functions // Helper methods to build the outbound command /** diff --git a/src/DCCEXRoutes.cpp b/src/DCCEXRoutes.cpp index e19d1bf..3e1ae9e 100644 --- a/src/DCCEXRoutes.cpp +++ b/src/DCCEXRoutes.cpp @@ -32,6 +32,7 @@ // Public methods namespace DCCExController { + Route *Route::_first = nullptr; Route::Route(int id) { diff --git a/src/DCCEXRoutes.h b/src/DCCEXRoutes.h index 83bad3a..291de24 100644 --- a/src/DCCEXRoutes.h +++ b/src/DCCEXRoutes.h @@ -30,6 +30,7 @@ #define DCCEXROUTES_H namespace DCCExController { + enum RouteType { RouteTypeRoute = 'R', RouteTypeAutomation = 'A', diff --git a/src/DCCEXTurnouts.cpp b/src/DCCEXTurnouts.cpp index 2750209..1645887 100644 --- a/src/DCCEXTurnouts.cpp +++ b/src/DCCEXTurnouts.cpp @@ -31,6 +31,7 @@ #include namespace DCCExController { + Turnout *Turnout::_first = nullptr; Turnout::Turnout(int id, bool thrown) { diff --git a/src/DCCEXTurnouts.h b/src/DCCEXTurnouts.h index 857b326..e7593a5 100644 --- a/src/DCCEXTurnouts.h +++ b/src/DCCEXTurnouts.h @@ -30,6 +30,7 @@ #define DCCEXTURNOUTS_H namespace DCCExController { + /// @brief Class to contain and maintain the various Turnout/Point attributes and methods class Turnout { public: diff --git a/src/DCCEXTurntables.cpp b/src/DCCEXTurntables.cpp index aba3be6..8b1dbbd 100644 --- a/src/DCCEXTurntables.cpp +++ b/src/DCCEXTurntables.cpp @@ -31,6 +31,7 @@ // class TurntableIndex namespace DCCExController { + TurntableIndex::TurntableIndex(int ttId, int id, int angle, const char *name) { _ttId = ttId; _id = id; diff --git a/src/DCCEXTurntables.h b/src/DCCEXTurntables.h index f851a7a..cea026d 100644 --- a/src/DCCEXTurntables.h +++ b/src/DCCEXTurntables.h @@ -30,6 +30,7 @@ #define DCCEXTURNTABLES_H namespace DCCExController { + enum TurntableType { TurntableTypeDCC = 0, TurntableTypeEXTT = 1, diff --git a/src/DCCMillis.h b/src/DCCMillis.h new file mode 100644 index 0000000..4e7492b --- /dev/null +++ b/src/DCCMillis.h @@ -0,0 +1,52 @@ +/* -*- c++ -*- + * + * DCCEXProtocol + * + * This package implements a DCCEX native protocol connection, + * allow a device to communicate with a DCC-EX EX-CommandStation. + * + * Copyright © 2023 Chris Harlow + * Copyright © 2023 Peter Akers + * Copyright © 2023 Peter Cole + * + * This work is licensed under the Creative Commons Attribution-ShareAlike + * 4.0 International License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to + * Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + * + * Attribution — You must give appropriate credit, provide a link to the + * license, and indicate if changes were made. You may do so in any + * reasonable manner, but not in any way that suggests the licensor + * endorses you or your use. + * + * ShareAlike — If you remix, transform, or build upon the material, you + * must distribute your contributions under the same license as the + * original. + * + * All other rights reserved. + * + */ + +#ifndef DCCMILLIS_H +#define DCCMILLIS_H + +/* to remove the dependance on Arduino's millis() function + * this file provides an abstract interface for a millis() function + */ + +namespace DCCExController { + +class DCCMillis +{ +public: + virtual ~DCCMillis() = default; + /** + * @brief Get the current time in milliseconds + * @return Time in milliseconds + */ + virtual unsigned long millis() const = 0; +}; + +}; // namespace DCCExController + +#endif // DCCMILLIS_H \ No newline at end of file diff --git a/test/mocks/Arduino.h b/test/mocks/Arduino.h deleted file mode 100644 index b2af7ab..0000000 --- a/test/mocks/Arduino.h +++ /dev/null @@ -1,109 +0,0 @@ -/* - * © 2025 Peter Cole - * - * This is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * It is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this code. If not, see . - */ - -/** - * @brief Mock Arduino.h to ensure source files requiring this still function - */ - -#ifndef ARDUINO_H -#define ARDUINO_H - -#include "Print.h" -#include "Stream.h" -#include -#include -#include - -// Define states -#define HIGH 0x1 -#define LOW 0x0 - -// Define pin modes -#define INPUT 0x0 -#define OUTPUT 0x1 -#define INPUT_PULLUP 0x2 - -// Define F() macro for FlashStringHelper -#define F(str) (str) - -// Define common Arduino types -typedef uint8_t byte; - -// Declare micros so it can be mocked/returned -inline unsigned long _currentMicros = 0; - -// Declare millis so it can be mocked/returned -inline unsigned long _currentMillis = 0; - -// Define a mock class to enable EXPECT_CALL tests against these -class MockArduino { -public: - MOCK_METHOD(void, mockPinMode, (int pin, int mode), ()); - MOCK_METHOD(void, mockDigitalWrite, (int pin, int value), ()); - - // Set up a singleton instance for this class to work with gmock - static MockArduino &getInstance() { - static MockArduino instance; - return instance; - } -}; - -// Mock Arduino functions -inline void pinMode(int pin, int mode) { MockArduino::getInstance().mockPinMode(pin, mode); } -inline void digitalWrite(int pin, int value) { MockArduino::getInstance().mockDigitalWrite(pin, value); } -inline int digitalRead(int pin) { return 0; } -inline void delay(unsigned long ms) {} -inline unsigned long micros() { return _currentMicros; } -inline unsigned long millis() { return _currentMillis; } -inline void analogWrite(int pin, int value) {} -inline int analogRead(int pin) { return 0; } - -inline void advanceMicros(unsigned long us) { _currentMicros += us; } -inline void advanceMillis(unsigned long ms) { _currentMillis += ms; } - -inline void resetMicros() { _currentMicros = 0; } -inline void resetMillis() { _currentMillis = 0; } - -// Mock itoa: converts integer to string -inline char *itoa(int value, char *str, int base) { - if (base == 10) { - sprintf(str, "%d", value); - } else if (base == 16) { - sprintf(str, "%x", value); - } - return str; -} - -// Mock ltoa: converts long integer to string -inline char *ltoa(long value, char *str, int base) { - if (base == 10) { - sprintf(str, "%ld", value); - } else if (base == 16) { - sprintf(str, "%lx", value); - } - return str; -} - -// Mock utoa: converts unsigned int to string -inline char *utoa(unsigned int value, char *str, int base) { - if (base == 10) { - sprintf(str, "%u", value); - } - return str; -} - -#endif // ARDUINO_H diff --git a/test/mocks/MockDCCEXProtocolDelegate.h b/test/mocks/MockDCCEXProtocolDelegate.h index d846df0..795aaf1 100644 --- a/test/mocks/MockDCCEXProtocolDelegate.h +++ b/test/mocks/MockDCCEXProtocolDelegate.h @@ -1,6 +1,8 @@ #include #include +using namespace DCCExController; + class MockDCCEXProtocolDelegate : public DCCEXProtocolDelegate { public: // Notify when the server version has been received @@ -36,6 +38,9 @@ class MockDCCEXProtocolDelegate : public DCCEXProtocolDelegate { // Notify when a track current is received MOCK_METHOD(void, receivedTrackCurrent, (char track, int current), (override)); + // Notify when an individual track power state change is received + MOCK_METHOD(void, receivedIndividualTrackPower, (TrackPower, int), (override)); + // Notify when a track type change is received MOCK_METHOD(void, receivedTrackType, (char, TrackManagerMode, int), (override)); @@ -71,4 +76,5 @@ class MockDCCEXProtocolDelegate : public DCCEXProtocolDelegate { // Notify when a fast clock time has been received MOCK_METHOD(void, receivedFastClockTime, (int minutes), (override)); + }; \ No newline at end of file diff --git a/test/mocks/MockDCCMillis.h b/test/mocks/MockDCCMillis.h new file mode 100644 index 0000000..aa2f515 --- /dev/null +++ b/test/mocks/MockDCCMillis.h @@ -0,0 +1,12 @@ +#ifndef MOCK_DCCMILLIS_H +#define MOCK_DCCMILLIS_H + +#include +#include "millis.h" + +class MockDCCMillis : public DCCExController::DCCMillis { +public: + unsigned long millis() const override { return getMillis(); } +}; + +#endif // MOCK_DCCMILLIS_H diff --git a/test/mocks/Print.h b/test/mocks/Print.h deleted file mode 100644 index dd995f0..0000000 --- a/test/mocks/Print.h +++ /dev/null @@ -1,94 +0,0 @@ -/* - * © 2025 Peter Cole - * - * This is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * It is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this code. If not, see . - */ - -#ifndef PRINT_H -#define PRINT_H - -#include -#include -#include - -// To handle F("") macros in mocks, we need a dummy type -typedef const char *__FlashStringHelper; - -class Print { -public: - virtual ~Print() {} - - // The single "Bottleneck" method. - // Every print call must eventually call this. - virtual size_t write(uint8_t c) = 0; - - // Multi-byte write helper - virtual size_t write(const uint8_t *buffer, size_t size) { - size_t n = 0; - while (size--) { - if (write(*buffer++)) - n++; - else - break; - } - return n; - } - - // --- Print Overloads --- - - void print(const char *s) { - if (s) - while (*s) - write(*s++); - } - - void print(const std::string &s) { print(s.c_str()); } - - void print(__FlashStringHelper *s) { print((const char *)s); } - - void print(char c) { write(c); } - - void print(int n) { print(std::to_string(n).c_str()); } - - void print(long n) { print(std::to_string(n).c_str()); } - - // --- Println Overloads --- - - void println() { - write('\r'); - write('\n'); - } - - void println(const char *s) { - print(s); - println(); - } - - void println(const std::string &s) { - print(s); - println(); - } - - void println(__FlashStringHelper *s) { - print(s); - println(); - } - - void println(int n) { - print(n); - println(); - } -}; - -#endif // PRINT_H diff --git a/test/mocks/Stream.h b/test/mocks/Stream.h index 3f760bb..3d97c13 100644 --- a/test/mocks/Stream.h +++ b/test/mocks/Stream.h @@ -18,25 +18,30 @@ #ifndef STREAM_H #define STREAM_H -#include "Print.h" +#include +#include +#include +#include + +using namespace DCCExController; /** * @brief Mock Stream class to simulate Arduino Stream objects eg. Serial. * @details Utilises a separate input and output buffer to cater for bi-directional comms. */ -class Stream : public Print { +class Stream : public DCCStream { public: /** * @brief Determines if there are more characters in the buffer * @return int Length of the buffer */ - int available() const { return _inputBuffer.length(); } + int available() const override { return _inputBuffer.length(); } /** * @brief Read a char from the buffer * @return int Char */ - int read() { + int read() override { if (_inputBuffer.empty()) return -1; char c = _inputBuffer[0]; @@ -49,11 +54,17 @@ class Stream : public Print { * @param c Char to write * @return size_t */ - virtual size_t write(uint8_t c) override { + virtual size_t write(uint8_t c) { _outputBuffer += (char)c; return 1; } + + virtual size_t write(const uint8_t *buffer, size_t size) override { + _outputBuffer += std::string(reinterpret_cast(buffer), size); + return size; + }; + /** * @brief Helper to write data to the buffer using << * @tparam T @@ -61,7 +72,7 @@ class Stream : public Print { * @return Stream& */ template Stream &operator<<(const T &data) { - // We bypass write() and put this straight into input + // We bypass write() and put this straight into output _inputBuffer += data; return *this; } @@ -75,14 +86,43 @@ class Stream : public Print { /** * @brief Clear the output buffer */ - void clearOutput() { _outputBuffer.clear(); } + void clearOutput() { _outputBuffer = ""; } - /** - * @brief Clear the input buffer - */ - void clearInput() { _inputBuffer.clear(); } + + virtual void flush() override { + _outputBuffer = ""; + } + + + virtual void println(const char* format, ...) override { + va_list args; + va_start(args, format); + _appendFormat(format, args); + va_end(args); + _outputBuffer += "\r\n"; + } + + virtual void print(const char* format, ...) override { + va_list args; + va_start(args, format); + _appendFormat(format, args); + va_end(args); + } private: + void _appendFormat(const char *format, va_list args) { + va_list argsCopy; + va_copy(argsCopy, args); + int needed = vsnprintf(nullptr, 0, format, argsCopy); + va_end(argsCopy); + if (needed <= 0) { + return; + } + std::string formatted(needed + 1, '\0'); + vsnprintf(&formatted[0], formatted.size(), format, args); + _outputBuffer += formatted.c_str(); + } + std::string _inputBuffer; // Data for read() std::string _outputBuffer; // Data from write()/print() }; diff --git a/test/mocks/millis.h b/test/mocks/millis.h new file mode 100644 index 0000000..f5bc9dc --- /dev/null +++ b/test/mocks/millis.h @@ -0,0 +1,12 @@ +#ifndef MILLIS_H +#define MILLIS_H + +#include + +inline unsigned long _currentMillis = 0; + +inline void advanceMillis(unsigned long ms) { _currentMillis += ms; } +inline void resetMillis() { _currentMillis = 0; } +inline unsigned long getMillis() { return _currentMillis; } + +#endif \ No newline at end of file diff --git a/test/setup/TestHarnessBase.hpp b/test/setup/TestHarnessBase.hpp index ae24d6e..107f6fd 100644 --- a/test/setup/TestHarnessBase.hpp +++ b/test/setup/TestHarnessBase.hpp @@ -30,11 +30,15 @@ #ifndef TESTHARNESSBASE_HPP #define TESTHARNESSBASE_HPP -#include "../mocks/Arduino.h" +#include #include "../mocks/MockDCCEXProtocolDelegate.h" +#include "../mocks/MockDCCMillis.h" +#include "../mocks/Stream.h" +#include "millis.h" #include using namespace testing; +using namespace DCCExController; /// @brief Test fixture to setup and tear down tests class TestHarnessBase : public Test { @@ -43,27 +47,27 @@ class TestHarnessBase : public Test { virtual ~TestHarnessBase() {} protected: + MockDCCMillis _millisProvider; + DCCEXProtocol _dccexProtocol{&_millisProvider}; + MockDCCEXProtocolDelegate _delegate; + Stream _console; + Stream _stream; + + void SetUp() override { - millis(); _dccexProtocol.setDelegate(&_delegate); _dccexProtocol.setLogStream(&_console); _dccexProtocol.connect(&_stream); _dccexProtocol.clearRoster(); + resetMillis(); } void TearDown() override { - resetMillis(); - _stream.clearInput(); - _stream.clearOutput(); _dccexProtocol.clearAllLists(); CSConsist::clearCSConsists(); CSConsist::setAlwaysReplicateFunctions(false); } - DCCEXProtocol _dccexProtocol; - MockDCCEXProtocolDelegate _delegate; - Stream _console; - Stream _stream; }; #endif // TESTHARNESSBASE_HPP diff --git a/test/setup/TestHarnessNoDelegate.h b/test/setup/TestHarnessNoDelegate.h index 496c6c5..f791c44 100644 --- a/test/setup/TestHarnessNoDelegate.h +++ b/test/setup/TestHarnessNoDelegate.h @@ -23,10 +23,13 @@ #ifndef TESTHARNESSNODELEGATE_H #define TESTHARNESSNODELEGATE_H -#include "../mocks/Arduino.h" +#include #include +#include "../mocks/MockDCCMillis.h" +#include "../mocks/Stream.h" using namespace testing; +using namespace DCCExController; /// @brief Test fixture to setup and tear down tests class TestHarnessNoDelegate : public Test { @@ -36,19 +39,18 @@ class TestHarnessNoDelegate : public Test { protected: void SetUp() override { - millis(); _dccexProtocol.setLogStream(&_console); _dccexProtocol.connect(&_stream); } void TearDown() override { resetMillis(); - _stream.clearInput(); _stream.clearOutput(); _dccexProtocol.clearAllLists(); } - DCCEXProtocol _dccexProtocol; + MockDCCMillis _millisProvider; + DCCEXProtocol _dccexProtocol{&_millisProvider}; Stream _console; Stream _stream; }; diff --git a/test/setup/TurnoutTests.h b/test/setup/TurnoutTests.h index a9fec47..5c19dba 100644 --- a/test/setup/TurnoutTests.h +++ b/test/setup/TurnoutTests.h @@ -27,7 +27,7 @@ */ #ifndef TURNOUTTESTS_H -#define TUNROUTTESTS_H +#define TURNOUTTESTS_H #include "TestHarnessBase.hpp" From 38a0bd527cdae0445a4b6609beb2e513f656d679 Mon Sep 17 00:00:00 2001 From: Mathew Winters Date: Tue, 17 Mar 2026 17:54:00 +1300 Subject: [PATCH 4/4] Add Examples, remove throw --- src/DCCEXProtocol.cpp | 2 +- targets/ESPIDF/ESP_Millis.h | 14 ++ targets/ESPIDF/wifi_connection.cpp | 5 + targets/ESPIDF/wifi_connection.h | 255 +++++++++++++++++++++++++++++ 4 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 targets/ESPIDF/ESP_Millis.h create mode 100644 targets/ESPIDF/wifi_connection.cpp create mode 100644 targets/ESPIDF/wifi_connection.h diff --git a/src/DCCEXProtocol.cpp b/src/DCCEXProtocol.cpp index 865b117..0e49fdc 100644 --- a/src/DCCEXProtocol.cpp +++ b/src/DCCEXProtocol.cpp @@ -60,7 +60,7 @@ DCCEXProtocol::DCCEXProtocol(DCCMillis *millisProvider, int maxCmdBuffer, int ma // Init streams if(millisProvider == nullptr){ // Cannot proceed without a millis provider, so do not proceed - throw std::invalid_argument("DCCEXProtocol requires a DCCMillis provider for time functions"); + while(true); } _millisProvider = millisProvider; _stream = &_nullStream; diff --git a/targets/ESPIDF/ESP_Millis.h b/targets/ESPIDF/ESP_Millis.h new file mode 100644 index 0000000..71b7c56 --- /dev/null +++ b/targets/ESPIDF/ESP_Millis.h @@ -0,0 +1,14 @@ +#ifndef _ESP_MILLIS_H +#define _ESP_MILLIS_H + +#include +#include + +inline uint64_t millis() { return esp_timer_get_time() / 1000ULL; } + +class ESPDCCMillis : public DCCExController::DCCMillis { +public: + unsigned long millis() const override { return ::millis(); } +}; + +#endif \ No newline at end of file diff --git a/targets/ESPIDF/wifi_connection.cpp b/targets/ESPIDF/wifi_connection.cpp new file mode 100644 index 0000000..65511bc --- /dev/null +++ b/targets/ESPIDF/wifi_connection.cpp @@ -0,0 +1,5 @@ +#include "wifi_connection.h" + +namespace utilities { +QueueHandle_t tcp_fail_queue = xQueueCreate(10, sizeof(err_t)); +} // namespace utilities \ No newline at end of file diff --git a/targets/ESPIDF/wifi_connection.h b/targets/ESPIDF/wifi_connection.h new file mode 100644 index 0000000..a3de919 --- /dev/null +++ b/targets/ESPIDF/wifi_connection.h @@ -0,0 +1,255 @@ +#ifndef _WIFI_CONNECTION_H +#define _WIFI_CONNECTION_H + +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" +#include +#include +#include // For get_absolute_time(), to_ms_since_boot() +#include +#include +#include +#include + +// Declare a queue handle +namespace utilities { +extern QueueHandle_t tcp_fail_queue; + +class TCPSocketStream : public DCCExController::DCCStream { +private: + struct tcp_pcb *pcb; + struct pbuf *recv_buffer; + bool failed = false; + err_t err; + + // Heartbeat tracking + uint64_t heartbeat_sent_time = 0; + bool awaiting_heartbeat = false; + + static err_t recv_callback(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) { + TCPSocketStream *stream = static_cast(arg); + if (p == nullptr) { + // Connection closed + printf("Connection closed by remote host\n"); + tcp_close(tpcb); + stream->pcb = nullptr; + pbuf_free(p); + stream->failed = true; + stream->err = err; + // Notify main core of TCP failure + xQueueSendToBack(tcp_fail_queue, &stream->err, portMAX_DELAY); + return ERR_ABRT; + } + if (err == ERR_OK) { + if (stream->recv_buffer == nullptr) { + stream->recv_buffer = p; + } else { + pbuf_chain(stream->recv_buffer, p); // Safely chain the new buffer + } + uint8_t *data = (uint8_t *)p->payload; + if (stream->awaiting_heartbeat && data[0] == '<') { + stream->awaiting_heartbeat = false; + } + } else { + pbuf_free(p); + } + return ERR_OK; + } + +public: + explicit TCPSocketStream(struct tcp_pcb *pcb) : pcb(pcb), recv_buffer(nullptr) { + // Create a queue (example) + + tcp_recv(pcb, recv_callback); + tcp_arg(pcb, this); + + // Enable keepalive + pcb->keep_idle = 5000; // ms + pcb->keep_intvl = 1000; // ms + pcb->keep_cnt = 5; + pcb->so_options |= SOF_KEEPALIVE; + tcp_keepalive(pcb); // idle, interval, count (values in ms) + } + + // Check if data is available to read + int available() const { return recv_buffer ? recv_buffer->tot_len : 0; } + + // Read a single byte from the socket + int read() { + LOCK_TCPIP_CORE(); + if (recv_buffer == nullptr) { + UNLOCK_TCPIP_CORE(); + return -1; // No data + } + uint8_t byte = *static_cast(recv_buffer->payload); + pbuf_remove_header(recv_buffer, 1); + if (recv_buffer->len == 0) { + struct pbuf *next = recv_buffer->next; + pbuf_free(recv_buffer); + recv_buffer = next; + } + UNLOCK_TCPIP_CORE(); + return byte; + } + + // Write a buffer to the socket + size_t write(const uint8_t *buffer, size_t size) { + if (failed) { + return -1; // Already failed + } + LOCK_TCPIP_CORE(); + err_t err = tcp_write(pcb, buffer, size, TCP_WRITE_FLAG_COPY); + if (err == ERR_OK) { + tcp_output(pcb); + UNLOCK_TCPIP_CORE(); + return size; + } + UNLOCK_TCPIP_CORE(); + printf("Error writing to TCP socket: %d\n", err); + failed = true; + // Notify main core of TCP failure + xQueueSendToBack(tcp_fail_queue, &err, portMAX_DELAY); + return err; // Error + } + + // No-op for sockets (no explicit flushing needed) + void flush() {} + + // Send a string with a newline + void println(const char *format, ...) { + char buffer[256]; + + va_list args; // Declare the variable argument list + + // Initialize the variable argument list + va_start(args, format); + + // Use vsnprintf to format the string into the buffer + vsnprintf(buffer, sizeof(buffer), format, args); + + // End the variable argument list + va_end(args); + + write(reinterpret_cast(buffer), strlen(buffer)); + write(reinterpret_cast("\n"), 1); + + if (strcmp(format, "<#>") == 0) { + notifyHeartbeatSent(); + } + } + + // Send a string + void print(const char *format, ...) { + char buffer[256]; + + va_list args; // Declare the variable argument list + + // Initialize the variable argument list + va_start(args, format); + + // Use vsnprintf to format the string into the buffer + vsnprintf(buffer, sizeof(buffer), format, args); + + // End the variable argument list + va_end(args); + + write(reinterpret_cast(buffer), strlen(buffer)); + } + + // Call this externally when <#> is sent + void notifyHeartbeatSent() { + heartbeat_sent_time = esp_timer_get_time(); + awaiting_heartbeat = true; + } + + // Call this periodically (e.g. in your main loop) + void checkHeartbeatTimeout() { + if (awaiting_heartbeat) { + int64_t elapsed = esp_timer_get_time() - heartbeat_sent_time; + if (elapsed > 10000000) { // 10 seconds + printf("Heartbeat timeout\n"); + awaiting_heartbeat = false; + err_t timeout_err = ERR_TIMEOUT; + xQueueSendToBack(tcp_fail_queue, &timeout_err, portMAX_DELAY); + } + } + } + + bool isFailed() { return failed; } + + // Destructor to close the socket + ~TCPSocketStream() { + LOCK_TCPIP_CORE(); + if (pcb != nullptr) { + tcp_close(pcb); + pcb = nullptr; + } + if (recv_buffer != nullptr) { + pbuf_free(recv_buffer); + recv_buffer = nullptr; + } + UNLOCK_TCPIP_CORE(); + } +}; + +class LoggingStream : public DCCExController::DCCStream { + +public: + explicit LoggingStream(struct tcp_pcb *pcb) {} + + // Check if data is available to read + int available() const { return 0; } + + // Read a single byte from the socket + int read() { return 0; } + + // Write a buffer to the socket + size_t write(const uint8_t *buffer, size_t size) { + return 0; // Error + } + + // No-op for sockets (no explicit flushing needed) + void flush() {} + + // Send a string with a newline + void println(const char *format, ...) { + char buffer[256]; + + va_list args; // Declare the variable argument list + + // Initialize the variable argument list + va_start(args, format); + + // Use vsnprintf to format the string into the buffer + vsnprintf(buffer, sizeof(buffer), format, args); + + // End the variable argument list + va_end(args); + + printf(buffer); + printf("\n"); + } + + // Send a string + void print(const char *format, ...) { + char buffer[256]; + + va_list args; // Declare the variable argument list + + // Initialize the variable argument list + va_start(args, format); + + // Use vsnprintf to format the string into the buffer + vsnprintf(buffer, sizeof(buffer), format, args); + + // End the variable argument list + va_end(args); + + printf(buffer); + } + + // Destructor to close the socket + ~LoggingStream() {} +}; +} // namespace utilities +#endif \ No newline at end of file