Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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. #365

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
6 changes: 5 additions & 1 deletion DCCEXParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -916,7 +917,10 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[])
} else
if (params == 3 && p[1] == HASH_KEYWORD_VPIN) { // <T id VPIN n>
if (!VpinTurnout::create(p[0], p[2])) return false;
} else
} else
if (params == 5 && p[1] == HASH_KEYWORD_HBRIDGE) { // <T id HBRIDGE pin1 pin2 delay>
if (!HBridgeTurnout::create(p[0], p[2], p[3], p[4])) return false;
} else
if (params >= 3 && p[1] == HASH_KEYWORD_DCC) {
// <T id DCC addr subadd> 0<=addr<=511, 0<=subadd<=3 (like <a> command).<T>
if (params==4 && p[2]>=0 && p[2]<512 && p[3]>=0 && p[3]<4) { // <T id DCC n m>
Expand Down
11 changes: 10 additions & 1 deletion EXRAIL2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion EXRAIL2.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions EXRAIL2MacroReset.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
#undef FWD
#undef GREEN
#undef HAL
#undef HBRIDGE_TURNOUT
#undef IF
#undef IFAMBER
#undef IFCLOSED
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions EXRAILMacros.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down
115 changes: 115 additions & 0 deletions IO_ScheduledPin.h
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/


#ifndef IO_SCHEDULED_PIN_H
#define IO_SCHEDULED_PIN_H

#include "IODevice.h"
#include <Arduino.h>
#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
104 changes: 104 additions & 0 deletions Turnouts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
#include "LCN.h"
#ifdef EESTOREDEBUG
#include "DIAG.h"
#endif

#ifndef IO_NO_HAL
#include "IO_ScheduledPin.h"
#endif

/*
Expand Down Expand Up @@ -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!
Expand Down Expand Up @@ -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("<H %d HBRIDGE %d %d %d>\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
Expand Down
36 changes: 36 additions & 0 deletions Turnouts.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ enum {
TURNOUT_SERVO = 2,
TURNOUT_VPIN = 3,
TURNOUT_LCN = 4,
TURNOUT_HBRIDGE = 5,
};

/*************************************************************************************
Expand Down Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion WifiInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down