From 7a9dee9bed5713502e7d653c041bdbafbba0b631 Mon Sep 17 00:00:00 2001 From: Sergei Kotlyachkov Date: Mon, 6 Nov 2023 21:39:29 -0500 Subject: [PATCH 1/5] Add AT check delay for longer init delays in ESP32 versions --- WifiInterface.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/WifiInterface.cpp b/WifiInterface.cpp index 8b2251a0..7cbeb782 100644 --- a/WifiInterface.cpp +++ b/WifiInterface.cpp @@ -36,6 +36,11 @@ const unsigned long LOOP_TIMEOUT = 2000; bool WifiInterface::connected = false; Stream * WifiInterface::wifiStream; +#ifndef WIFI_AT_CHECK_TIMEOUT +// Some ESP32 AT firmware versions take time to initialize and do not respond to AT commands right away. +#define WIFI_AT_CHECK_TIMEOUT 2000 +#endif + #ifndef WIFI_CONNECT_TIMEOUT // Tested how long it takes to FAIL an unknown SSID on firmware 1.7.4. // The ES should fail a connect in 15 seconds, we don't want to fail BEFORE that @@ -192,7 +197,7 @@ wifiSerialState WifiInterface::setup2(const FSH* SSid, const FSH* password, } StringFormatter::send(wifiStream, F("AT\r\n")); // Is something here that understands AT? - if(!checkForOK(200, true)) + if(!checkForOK(WIFI_AT_CHECK_TIMEOUT, true)) return WIFI_NOAT; // No AT compatible WiFi module here StringFormatter::send(wifiStream, F("ATE1\r\n")); // Turn on the echo, se we can see what's happening From dba5e88d041e4ce59838cce196c3843f8f54ea70 Mon Sep 17 00:00:00 2001 From: Sergei Kotlyachkov Date: Mon, 6 Nov 2023 21:54:25 -0500 Subject: [PATCH 2/5] Add support for HBRIDGE Turnouts, allowing control of Kato turnouts that require reverse of polarity and short power application, easily configurable through 2-pin controlled Motor H-Bridge. --- DCCEXParser.cpp | 6 ++- EXRAIL2.cpp | 11 +++++- EXRAIL2.h | 3 +- EXRAIL2MacroReset.h | 2 + EXRAILMacros.h | 3 ++ Turnouts.cpp | 91 +++++++++++++++++++++++++++++++++++++++++++++ Turnouts.h | 36 ++++++++++++++++++ 7 files changed, 149 insertions(+), 3 deletions(-) diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp index 04c0b6d3..2febe418 100644 --- a/DCCEXParser.cpp +++ b/DCCEXParser.cpp @@ -162,6 +162,7 @@ const int16_t HASH_KEYWORD_T='T'; const int16_t HASH_KEYWORD_X='X'; const int16_t HASH_KEYWORD_LCN = 15137; const int16_t HASH_KEYWORD_HAL = 10853; +const int16_t HASH_KEYWORD_HBRIDGE=-20585; const int16_t HASH_KEYWORD_SHOW = -21309; const int16_t HASH_KEYWORD_ANIN = -10424; const int16_t HASH_KEYWORD_ANOUT = -26399; @@ -916,7 +917,10 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[]) } else if (params == 3 && p[1] == HASH_KEYWORD_VPIN) { // if (!VpinTurnout::create(p[0], p[2])) return false; - } else + } else + if (params == 5 && p[1] == HASH_KEYWORD_HBRIDGE) { // + if (!HBridgeTurnout::create(p[0], p[2], p[3], p[4])) return false; + } else if (params >= 3 && p[1] == HASH_KEYWORD_DCC) { // 0<=addr<=511, 0<=subadd<=3 (like command). if (params==4 && p[2]>=0 && p[2]<512 && p[3]>=0 && p[3]<4) { // diff --git a/EXRAIL2.cpp b/EXRAIL2.cpp index 0e17ea91..d16e955c 100644 --- a/EXRAIL2.cpp +++ b/EXRAIL2.cpp @@ -238,7 +238,16 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) { setTurnoutHiddenState(VpinTurnout::create(id,pin)); break; } - + + case OPCODE_HBRIDGETURNOUT: { + VPIN id=operand; + VPIN pin1=getOperand(progCounter, 1); + VPIN pin2=getOperand(progCounter, 2); + uint16_t delay=getOperand(progCounter, 3); + setTurnoutHiddenState(HBridgeTurnout::create(id,pin1, pin2, delay)); + break; + } + case OPCODE_AUTOSTART: // automatically create a task from here at startup. // Removed if (progCounter>0) check 4.2.31 because diff --git a/EXRAIL2.h b/EXRAIL2.h index 4d106e69..6fdc9f12 100644 --- a/EXRAIL2.h +++ b/EXRAIL2.h @@ -51,7 +51,8 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE, OPCODE_POM, OPCODE_START,OPCODE_SETLOCO,OPCODE_SENDLOCO,OPCODE_FORGET, OPCODE_PAUSE, OPCODE_RESUME,OPCODE_POWEROFF,OPCODE_POWERON, - OPCODE_ONCLOSE, OPCODE_ONTHROW, OPCODE_SERVOTURNOUT, OPCODE_PINTURNOUT, + OPCODE_ONCLOSE, OPCODE_ONTHROW, OPCODE_SERVOTURNOUT, + OPCODE_PINTURNOUT, OPCODE_HBRIDGETURNOUT, OPCODE_PRINT,OPCODE_DCCACTIVATE, OPCODE_ONACTIVATE,OPCODE_ONDEACTIVATE, OPCODE_ROSTER,OPCODE_KILLALL, diff --git a/EXRAIL2MacroReset.h b/EXRAIL2MacroReset.h index 588a417b..06be5af8 100644 --- a/EXRAIL2MacroReset.h +++ b/EXRAIL2MacroReset.h @@ -62,6 +62,7 @@ #undef FWD #undef GREEN #undef HAL +#undef HBRIDGE_TURNOUT #undef IF #undef IFAMBER #undef IFCLOSED @@ -187,6 +188,7 @@ #define FWD(speed) #define GREEN(signal_id) #define HAL(haltype,params...) +#define HBRIDGE_TURNOUT(id,pin1,pin2,dly,description...) #define IF(sensor_id) #define IFAMBER(signal_id) #define IFCLOSED(turnout_id) diff --git a/EXRAILMacros.h b/EXRAILMacros.h index 4bbabfce..80a0d13b 100644 --- a/EXRAILMacros.h +++ b/EXRAILMacros.h @@ -174,6 +174,8 @@ void RMFT2::printMessage(uint16_t id) { #define TURNOUT(id,addr,subaddr,description...) O_DESC(id,description) #undef TURNOUTL #define TURNOUTL(id,addr,description...) O_DESC(id,description) +#undef HBRIDGE_TURNOUT +#define HBRIDGE_TURNOUT(id,pin1,pin2,delay_ms,description...) O_DESC(id,description) #undef PIN_TURNOUT #define PIN_TURNOUT(id,pin,description...) O_DESC(id,description) #undef SERVO_TURNOUT @@ -293,6 +295,7 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = { #define FWD(speed) OPCODE_FWD,V(speed), #define GREEN(signal_id) OPCODE_GREEN,V(signal_id), #define HAL(haltype,params...) +#define HBRIDGE_TURNOUT(id,pin1,pin2,delay,description...) OPCODE_HBRIDGETURNOUT,V(id),OPCODE_PAD,V(pin1),OPCODE_PAD,V(pin2),OPCODE_PAD,V(delay), #define IF(sensor_id) OPCODE_IF,V(sensor_id), #define IFAMBER(signal_id) OPCODE_IFAMBER,V(signal_id), #define IFCLOSED(turnout_id) OPCODE_IFCLOSED,V(turnout_id), diff --git a/Turnouts.cpp b/Turnouts.cpp index 83603fcf..a6b6aac2 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -187,6 +187,10 @@ // VPIN turnout tt = VpinTurnout::load(&turnoutData); break; + case TURNOUT_HBRIDGE: + // HBRIDGE turnout + tt = HBridgeTurnout::load(&turnoutData); + break; default: // If we find anything else, then we don't know what it is or how long it is, // so we can't go any further through the EEPROM! @@ -477,6 +481,93 @@ #endif } +/************************************************************************************* + * HBridgeTurnout - Turnout controlled through a pair of HAL pins. + * Typically connected to Motor H-Bridge. Delay is used to quickly turn on/off power. + *************************************************************************************/ + + // Constructor + HBridgeTurnout::HBridgeTurnout(uint16_t id, VPIN pin1, VPIN pin2, uint16_t millisDelay, bool closed) : + Turnout(id, TURNOUT_HBRIDGE, closed) + { + _hbridgeTurnoutData.pin1 = pin1; + _hbridgeTurnoutData.pin2 = pin2; + _hbridgeTurnoutData.millisDelay = millisDelay; + } + + // Create function + /* static */ Turnout *HBridgeTurnout::create(uint16_t id, VPIN pin1, VPIN pin2, uint16_t millisDelay, bool closed) { + Turnout *tt = get(id); + if (tt) { + // Object already exists, check if it is usable + if (tt->isType(TURNOUT_HBRIDGE)) { + // Yes, so set parameters + HBridgeTurnout *hbt = (HBridgeTurnout *)tt; + hbt->_hbridgeTurnoutData.pin1 = pin1; + hbt->_hbridgeTurnoutData.pin2 = pin2; + hbt->_hbridgeTurnoutData.millisDelay = millisDelay; + // Don't touch the _closed parameter, retain the original value. + return tt; + } else { + // Incompatible object, delete and recreate + remove(id); + } + } + tt = (Turnout *)new HBridgeTurnout(id, pin1, pin2, millisDelay, closed); + return tt; + } + + // Load a VPIN turnout definition from EEPROM. The common Turnout data has already been read at this point. + /* static */ Turnout *HBridgeTurnout::load(struct TurnoutData *turnoutData) { +#ifndef DISABLE_EEPROM + HBridgeTurnoutData hbridgeTurnoutData; + // Read class-specific data from EEPROM + EEPROM.get(EEStore::pointer(), hbridgeTurnoutData); + EEStore::advance(sizeof(hbridgeTurnoutData)); + + // Create new object + HBridgeTurnout *tt = new HBridgeTurnout(turnoutData->id, hbridgeTurnoutData.pin1, + hbridgeTurnoutData.pin2, hbridgeTurnoutData.millisDelay, turnoutData->closed); + + return tt; +#else + (void)turnoutData; + return NULL; +#endif + } + + // Report 1 for thrown, 0 for closed. + void HBridgeTurnout::print(Print *stream) { + StringFormatter::send(stream, F("\n"), _turnoutData.id, _hbridgeTurnoutData.pin1, _hbridgeTurnoutData.pin2, + !_turnoutData.closed); + } + + void HBridgeTurnout::turnUpDown(VPIN pin) { + // HBridge turnouts require very small, prescribed time to keep pin1 or pin2 in HIGH state. + // Otherwise internal coil of the turnout will burn. + IODevice::write(pin, HIGH); + // HARD LIMIT to maximum 0.5 second to avoid burning the coil + delay(min(_hbridgeTurnoutData.millisDelay, 500)); + IODevice::write(pin, LOW); + } + + bool HBridgeTurnout::setClosedInternal(bool close) { + turnUpDown(close ? _hbridgeTurnoutData.pin2 : _hbridgeTurnoutData.pin1); + _turnoutData.closed = close; + return true; + } + + void HBridgeTurnout::save() { +#ifndef DISAB LE_EEPROM + // Write turnout definition and current position to EEPROM + // First write common servo data, then + // write the servo-specific data + EEPROM.put(EEStore::pointer(), _turnoutData); + EEStore::advance(sizeof(_turnoutData)); + EEPROM.put(EEStore::pointer(), _hbridgeTurnoutData); + EEStore::advance(sizeof(_hbridgeTurnoutData)); +#endif + } /************************************************************************************* * LCNTurnout - Turnout controlled by Loconet diff --git a/Turnouts.h b/Turnouts.h index 56b7f829..ec2086b6 100644 --- a/Turnouts.h +++ b/Turnouts.h @@ -37,6 +37,7 @@ enum { TURNOUT_SERVO = 2, TURNOUT_VPIN = 3, TURNOUT_LCN = 4, + TURNOUT_HBRIDGE = 5, }; /************************************************************************************* @@ -284,6 +285,41 @@ class VpinTurnout : public Turnout { }; +/************************************************************************************* + * HBridgeTurnout - Turnout controlled through a pair of HAL pins. + * + * Hard limited to maximum 0.5 second to avoid burning the coil + * Typical millisDelay should be within between 50 and 100 + *************************************************************************************/ +class HBridgeTurnout : public Turnout { +private: + // HBridgeTurnoutData contains data specific to this subclass that is + // written to EEPROM when the turnout is saved. + struct HBridgeTurnoutData { + VPIN pin1; + VPIN pin2; + uint16_t millisDelay; + } _hbridgeTurnoutData; // 6 bytes + + // Constructor + HBridgeTurnout(uint16_t id, VPIN pin1, VPIN pin2, uint16_t millisDelay, bool closed); + +public: + // Create function + static Turnout *create(uint16_t id, VPIN pin1, VPIN pin2, uint16_t millisDelay, bool closed=true); + + // Load a HBRIDGE turnout definition from EEPROM. The common Turnout data has already been read at this point. + static Turnout *load(struct TurnoutData *turnoutData); + void print(Print *stream) override; + +protected: + bool setClosedInternal(bool close) override; + void save() override; + +private: + void turnUpDown(VPIN pin); + +}; /************************************************************************************* * LCNTurnout - Turnout controlled by Loconet From faf9f76c424304e98164480c53c54a9417ce4dfa Mon Sep 17 00:00:00 2001 From: Sergei Kotlyachkov Date: Mon, 6 Nov 2023 22:29:41 -0500 Subject: [PATCH 3/5] Remove space that was added by mistake when merging --- Turnouts.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Turnouts.cpp b/Turnouts.cpp index a6b6aac2..4b44ad4a 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -558,7 +558,7 @@ } void HBridgeTurnout::save() { -#ifndef DISAB LE_EEPROM +#ifndef DISABLE_EEPROM // Write turnout definition and current position to EEPROM // First write common servo data, then // write the servo-specific data From a30311caedde2599b3da9da0a4268c40f4fcdec5 Mon Sep 17 00:00:00 2001 From: Sergei Kotlyachkov Date: Thu, 9 Nov 2023 21:16:17 -0500 Subject: [PATCH 4/5] Refactor usage of delay() into an Helper IO class that schedules pin bouncing back --- IO_ScheduledPin.h | 115 ++++++++++++++++++++++++++++++++++++++++++++++ Turnouts.cpp | 19 ++++++-- 2 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 IO_ScheduledPin.h diff --git a/IO_ScheduledPin.h b/IO_ScheduledPin.h new file mode 100644 index 00000000..e9040e88 --- /dev/null +++ b/IO_ScheduledPin.h @@ -0,0 +1,115 @@ +/* + * © 2023, Sergei Kotlyachkov. All rights reserved. + * + * This file is part of DCC++EX API + * + * 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 CommandStation. If not, see . + */ + + +#ifndef IO_SCHEDULED_PIN_H +#define IO_SCHEDULED_PIN_H + +#include "IODevice.h" +#include +#include "defines.h" + +/** + * Bounces back single Arduino Pin to specified state after set period of time. + * + * It will establish itself as owner of the pin over ArduinoPins class that typically responds to it and + * activates itself during loop() phase. It restores scheduled state and does not try again until + * another write() + * + * Example usage: + * Create: ScheduledPin::create(5, LOW, 20000); + * + * Then, when neeeded, just call: + * IODevice::write(5, HIGH); // this will call fastWriteDigital(5, HIGH) + * + * In 20 milliseconds, it will also call fastWriteDigital(5, LOW) + * + * In edge case where write() is called twice before responding in the loop, + * the schedule will restart and double the bounce back time. + */ +class ScheduledPin : public IODevice { +private: + int _scheduledValue; + uint32_t _durationMicros; + +public: + // Static function to handle create calls. + static void create(VPIN pin, int scheduledValue, uint32_t durationMicros) { + new ScheduledPin(pin, scheduledValue, durationMicros); + } + +protected: + // Constructor. + ScheduledPin(VPIN pin, int scheduledValue, uint32_t durationMicros) : IODevice(pin, 1) { + _scheduledValue = scheduledValue; + _durationMicros = durationMicros; + // Typically returned device will be ArduinoPins + IODevice* controlledDevice = IODevice::findDevice(pin); + if (controlledDevice != NULL) { + addDevice(this, controlledDevice); + } + else { + DIAG(F("ScheduledPin Controlled device not found for pin:%d"), pin); + _deviceState = DEVSTATE_FAILED; + } + } + + // Device-specific initialisation + void _begin() override { + #ifdef DIAG_IO + _display(); + #endif + pinMode(_firstVpin, OUTPUT); + ArduinoPins::fastWriteDigital(_firstVpin, _scheduledValue); + } + + void _write(VPIN vpin, int value) override { + if (_deviceState == DEVSTATE_FAILED) return; + if (vpin != _firstVpin) { + #ifdef DIAG_IO + DIAG(F("ScheduledPin Error VPIN:%u not equal to %u"), vpin, _firstVpin); + #endif + return; + } + #ifdef DIAG_IO + DIAG(F("ScheduledPin Write VPIN:%u Value:%d"), vpin, value); + #endif + unsigned long currentMicros = micros(); + delayUntil(currentMicros + _durationMicros); + ArduinoPins::fastWriteDigital(_firstVpin, value); + } + + + void _loop(unsigned long currentMicros) { + if (_deviceState == DEVSTATE_FAILED) return; + #ifdef DIAG_IO + DIAG(F("ScheduledPin Write VPIN:%u Value:%d"), _firstVpin, _scheduledValue); + #endif + ArduinoPins::fastWriteDigital(_firstVpin, _scheduledValue); + delayUntil(currentMicros + 0x7fffffff); // Largest time in the future! Effectively disable _loop calls. + } + + // Display information about the device, and perhaps its current condition (e.g. active, disabled etc). + void _display() { + DIAG(F("ScheduledPin Configured:%u value=%d duration=%ld"), (int)_firstVpin, + (int)_firstVpin, _scheduledValue, _durationMicros); + } +}; + +#endif // IO_SCHEDULED_PIN_H diff --git a/Turnouts.cpp b/Turnouts.cpp index 4b44ad4a..5957d50b 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -36,6 +36,10 @@ #include "LCN.h" #ifdef EESTOREDEBUG #include "DIAG.h" +#endif + +#ifndef IO_NO_HAL +#include "IO_ScheduledPin.h" #endif /* @@ -493,6 +497,14 @@ _hbridgeTurnoutData.pin1 = pin1; _hbridgeTurnoutData.pin2 = pin2; _hbridgeTurnoutData.millisDelay = millisDelay; +#ifndef IO_NO_HAL + // HARD LIMIT to maximum 0.5 second to avoid burning the coil + // Also note 1000x multiplier because ScheduledPin works with microSeconds. + ScheduledPin::create(pin1, LOW, 1000*min(millisDelay, 500)); + ScheduledPin::create(pin2, LOW, 1000*min(millisDelay, 500)); +#else + DIAG(F("H-Brdige Turnout %d will be disabled because HAL is off"), id); +#endif } // Create function @@ -545,10 +557,11 @@ void HBridgeTurnout::turnUpDown(VPIN pin) { // HBridge turnouts require very small, prescribed time to keep pin1 or pin2 in HIGH state. // Otherwise internal coil of the turnout will burn. + // If HAL is disabled (and therefore SchedulePin class), we can not turn this on, + // otherwise coil will burn and device will be lost. +#ifndef IO_NO_HAL IODevice::write(pin, HIGH); - // HARD LIMIT to maximum 0.5 second to avoid burning the coil - delay(min(_hbridgeTurnoutData.millisDelay, 500)); - IODevice::write(pin, LOW); +#endif } bool HBridgeTurnout::setClosedInternal(bool close) { From 6e9a3ebfb4fc18aa97fc4af7af91457b369c4101 Mon Sep 17 00:00:00 2001 From: Sergei Kotlyachkov Date: Fri, 10 Nov 2023 23:47:20 -0500 Subject: [PATCH 5/5] While verifying with real hardware, added micros() into the logs for ScheduledPin --- IO_ScheduledPin.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/IO_ScheduledPin.h b/IO_ScheduledPin.h index e9040e88..a358164f 100644 --- a/IO_ScheduledPin.h +++ b/IO_ScheduledPin.h @@ -65,7 +65,7 @@ class ScheduledPin : public IODevice { addDevice(this, controlledDevice); } else { - DIAG(F("ScheduledPin Controlled device not found for pin:%d"), pin); + DIAG(F("ScheduledPin Controlled device not found for VPIN:%d"), pin); _deviceState = DEVSTATE_FAILED; } } @@ -88,7 +88,7 @@ class ScheduledPin : public IODevice { return; } #ifdef DIAG_IO - DIAG(F("ScheduledPin Write VPIN:%u Value:%d"), vpin, value); + DIAG(F("ScheduledPin Write VPIN:%u Value:%d Micros:%l"), vpin, value, micros()); #endif unsigned long currentMicros = micros(); delayUntil(currentMicros + _durationMicros); @@ -99,7 +99,7 @@ class ScheduledPin : public IODevice { void _loop(unsigned long currentMicros) { if (_deviceState == DEVSTATE_FAILED) return; #ifdef DIAG_IO - DIAG(F("ScheduledPin Write VPIN:%u Value:%d"), _firstVpin, _scheduledValue); + DIAG(F("ScheduledPin Bounce VPIN:%u Value:%d Micros:%l"), _firstVpin, _scheduledValue, micros()); #endif ArduinoPins::fastWriteDigital(_firstVpin, _scheduledValue); delayUntil(currentMicros + 0x7fffffff); // Largest time in the future! Effectively disable _loop calls. @@ -107,8 +107,8 @@ class ScheduledPin : public IODevice { // Display information about the device, and perhaps its current condition (e.g. active, disabled etc). void _display() { - DIAG(F("ScheduledPin Configured:%u value=%d duration=%ld"), (int)_firstVpin, - (int)_firstVpin, _scheduledValue, _durationMicros); + DIAG(F("ScheduledPin Configured:%u Value:%d Duration:%l"), (int)_firstVpin, + _scheduledValue, _durationMicros); } };