diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp index 04c0b6d..2febe41 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 1d97e7f..db34b63 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 4d106e6..6fdc9f1 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 588a417..06be5af 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 4bbabfc..80a0d13 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/IO_ScheduledPin.h b/IO_ScheduledPin.h new file mode 100644 index 0000000..a358164 --- /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 VPIN:%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 Micros:%l"), vpin, value, micros()); + #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 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. + } + + // 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:%l"), (int)_firstVpin, + _scheduledValue, _durationMicros); + } +}; + +#endif // IO_SCHEDULED_PIN_H diff --git a/Turnouts.cpp b/Turnouts.cpp index 83603fc..5957d50 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 /* @@ -187,6 +191,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 +485,102 @@ #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; +#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 + /* 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. + // 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); +#endif + } + + bool HBridgeTurnout::setClosedInternal(bool close) { + turnUpDown(close ? _hbridgeTurnoutData.pin2 : _hbridgeTurnoutData.pin1); + _turnoutData.closed = close; + return true; + } + + void HBridgeTurnout::save() { +#ifndef DISABLE_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 56b7f82..ec2086b 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 @@ protected: }; +/************************************************************************************* + * 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 diff --git a/WifiInterface.cpp b/WifiInterface.cpp index 8b2251a..7cbeb78 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