1
0
mirror of https://github.com/DCC-EX/CommandStation-EX.git synced 2025-03-14 01:53:07 +01:00
CommandStation-EX/Turnouts.cpp
Neil McKechnie 9dacd24d27
Various HAL enhancements. (#182)
* Add <D SERVO vpin position> command

Allow a PWM servo to be driven to any arbitrary position.

* Enhancements for HAL drivers

Add state change notification for external GPIO module drivers;
Allow drivers to be installed statically by declaration (as an alternative to the 'create' call).

* Create IO_HCSR04.h

HAL driver for HC-SR04 ultrasonic distance sensor (sonar).

* Enable servo commands in NO-HAL mode, but return error.

Avoid compile errors in RMFT.cpp when compiled with basic HAL by including the Turnout::createServo function as a stub that returns NULL.

* Update IO_HCSR04.h

Minor changes

* Change <D SERVO>

Give the <D SERVO> command an optional parameter of the profile.  For example, <D SERVO 100 200 3> will slowly move the servo on pin 100 to PWM position corresponding to 200.  If omitted, the servo will move immediately (no animation).

* IODevice (HAL) changes

1) Put new devices on the end of the chain instead of the beginning.  This will give better performance for devices created first (ArduinoPins and extender GPIO devices, typically).
2) Remove unused functions.

* Update IO_HCSR04.h

Allow thresholds for ON and OFF to be separately configured at creation.

* Update IODevice.cpp

Fix compile error on IO_NO_HAL minimal HAL version.

* Update IO_PCA9685.cpp

Remove unnecessary duplicated call to min() function.
2021-08-17 23:41:34 +01:00

384 lines
13 KiB
C++

/*
* © 2013-2016 Gregg E. Berman
* © 2020, Chris Harlow. All rights reserved.
* © 2020, Harald Barth.
*
* This file is part of Asbelos DCC 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/>.
*/
//#define EESTOREDEBUG
#include "defines.h"
#include "Turnouts.h"
#include "EEStore.h"
#include "StringFormatter.h"
#include "RMFT2.h"
#ifdef EESTOREDEBUG
#include "DIAG.h"
#endif
// Keywords used for turnout configuration.
const int16_t HASH_KEYWORD_SERVO=27709;
const int16_t HASH_KEYWORD_DCC=6436;
const int16_t HASH_KEYWORD_VPIN=-415;
enum unit8_t {
TURNOUT_DCC = 1,
TURNOUT_SERVO = 2,
TURNOUT_VPIN = 3,
TURNOUT_LCN = 4,
};
///////////////////////////////////////////////////////////////////////////////
// Static function to print all Turnout states to stream in form "<H id state>"
void Turnout::printAll(Print *stream){
for (Turnout *tt = Turnout::firstTurnout; tt != NULL; tt = tt->nextTurnout)
StringFormatter::send(stream, F("<H %d %d>\n"), tt->data.id, tt->data.active);
} // Turnout::printAll
///////////////////////////////////////////////////////////////////////////////
// Object method to print configuration of one Turnout to stream, in one of the following forms:
// <H id SERVO vpin activePos inactivePos profile state>
// <H id LCN state>
// <H id VPIN vpin state>
// <H id DCC address subAddress state>
void Turnout::print(Print *stream){
uint8_t state = ((data.active) != 0);
uint8_t type = data.type;
switch (type) {
case TURNOUT_LCN:
// LCN Turnout
StringFormatter::send(stream, F("<H %d LCN %d>\n"), data.id, state);
break;
case TURNOUT_DCC:
// DCC Turnout
StringFormatter::send(stream, F("<H %d DCC %d %d %d>\n"), data.id,
(((data.dccAccessoryData.address-1) >> 2)+1), ((data.dccAccessoryData.address-1) & 3), state);
break;
case TURNOUT_VPIN:
// VPIN Digital output
StringFormatter::send(stream, F("<H %d VPIN %d %d>\n"), data.id, data.vpinData.vpin, state);
break;
case TURNOUT_SERVO:
// Servo Turnout
StringFormatter::send(stream, F("<H %d SERVO %d %d %d %d %d>\n"), data.id, data.servoData.vpin,
data.servoData.activePosition, data.servoData.inactivePosition, data.servoData.profile, state);
break;
default:
break;
}
}
///////////////////////////////////////////////////////////////////////////////
// Static function to activate/deactivate Turnout with ID 'n'.
// Returns false if turnout not found.
bool Turnout::activate(int n, bool state){
Turnout * tt=get(n);
if (!tt) return false;
tt->activate(state);
turnoutlistHash++;
return true;
}
///////////////////////////////////////////////////////////////////////////////
// Static function to check if the Turnout with ID 'n' is activated or not.
// Returns false if turnout not found.
bool Turnout::isActive(int n){
Turnout * tt=get(n);
if (!tt) return false;
return tt->isActive();
}
///////////////////////////////////////////////////////////////////////////////
// Object function to check the status of Turnout is activated or not.
bool Turnout::isActive() {
return data.active;
}
///////////////////////////////////////////////////////////////////////////////
// Object method to activate or deactivate the Turnout.
// activate is virtual here so that it can be overridden by a non-DCC turnout mechanism
void Turnout::activate(bool state) {
#ifdef EESTOREDEBUG
DIAG(F("Turnout::activate(%d)"),state);
#endif
if (data.type == TURNOUT_LCN) {
// A LCN turnout is transmitted to the LCN master.
LCN::send('T', data.id, state);
return; // The tStatus will be updated by a message from the LCN master, later.
}
data.active = state;
switch (data.type) {
case TURNOUT_DCC:
DCC::setAccessory((((data.dccAccessoryData.address-1) >> 2) + 1),
((data.dccAccessoryData.address-1) & 3), state);
break;
case TURNOUT_SERVO:
#ifndef IO_NO_HAL
IODevice::write(data.servoData.vpin, state);
#endif
break;
case TURNOUT_VPIN:
IODevice::write(data.vpinData.vpin, state);
break;
}
// Save state if stored in EEPROM
if (EEStore::eeStore->data.nTurnouts > 0 && num > 0)
EEPROM.put(num, data.tStatus);
#if defined(RMFT_ACTIVE)
RMFT2::turnoutEvent(data.id, state);
#endif
}
///////////////////////////////////////////////////////////////////////////////
// Static function to find Turnout object specified by ID 'n'. Return NULL if not found.
Turnout* Turnout::get(int n){
Turnout *tt;
for(tt=firstTurnout;tt!=NULL && tt->data.id!=n;tt=tt->nextTurnout);
return(tt);
}
///////////////////////////////////////////////////////////////////////////////
// Static function to delete Turnout object specified by ID 'n'. Return false if not found.
bool Turnout::remove(int n){
Turnout *tt,*pp=NULL;
for(tt=firstTurnout;tt!=NULL && tt->data.id!=n;pp=tt,tt=tt->nextTurnout);
if(tt==NULL) return false;
if(tt==firstTurnout)
firstTurnout=tt->nextTurnout;
else
pp->nextTurnout=tt->nextTurnout;
free(tt);
turnoutlistHash++;
return true;
}
///////////////////////////////////////////////////////////////////////////////
// Static function to load all Turnout definitions from EEPROM
// TODO: Consider transmitting the initial state of the DCC/LCN turnout here.
// (already done for servo turnouts and VPIN turnouts).
void Turnout::load(){
struct TurnoutData data;
Turnout *tt=NULL;
for(uint16_t i=0;i<EEStore::eeStore->data.nTurnouts;i++){
// Retrieve data
EEPROM.get(EEStore::pointer(), data);
int lastKnownState = data.active;
switch (data.type) {
case TURNOUT_DCC:
tt=createDCC(data.id, ((data.dccAccessoryData.address-1)>>2)+1, (data.dccAccessoryData.address-1)&3); // DCC-based turnout
break;
case TURNOUT_LCN:
// LCN turnouts are created when the remote device sends a message.
break;
case TURNOUT_SERVO:
tt=createServo(data.id, data.servoData.vpin,
data.servoData.activePosition, data.servoData.inactivePosition, data.servoData.profile, lastKnownState);
break;
case TURNOUT_VPIN:
tt=createVpin(data.id, data.vpinData.vpin, lastKnownState); // VPIN-based turnout
break;
default:
tt=NULL;
}
if (tt) tt->num = EEStore::pointer() + offsetof(TurnoutData, tStatus); // Save pointer to tStatus byte within EEPROM
// Advance by the actual size of the individual turnout struct.
EEStore::advance(data.size);
#ifdef EESTOREDEBUG
if (tt) print(tt);
#endif
}
}
///////////////////////////////////////////////////////////////////////////////
// Static function to store all Turnout definitions to EEPROM
void Turnout::store(){
Turnout *tt;
tt=firstTurnout;
EEStore::eeStore->data.nTurnouts=0;
while(tt!=NULL){
// LCN turnouts aren't saved to EEPROM
if (tt->data.type != TURNOUT_LCN) {
#ifdef EESTOREDEBUG
print(tt);
#endif
tt->num = EEStore::pointer() + offsetof(TurnoutData, tStatus); // Save pointer to tstatus byte within EEPROM
EEPROM.put(EEStore::pointer(),tt->data);
EEStore::advance(tt->data.size);
EEStore::eeStore->data.nTurnouts++;
}
tt=tt->nextTurnout;
}
}
///////////////////////////////////////////////////////////////////////////////
// Static function for creating a DCC-controlled Turnout.
Turnout *Turnout::createDCC(int id, uint16_t add, uint8_t subAdd){
if (add > 511 || subAdd > 3) return NULL;
Turnout *tt=create(id);
if (!tt) return(tt);
tt->data.type = TURNOUT_DCC;
tt->data.size = sizeof(tt->data.header) + sizeof(tt->data.dccAccessoryData);
tt->data.active = 0;
tt->data.dccAccessoryData.address = ((add-1) << 2) + subAdd + 1;
return(tt);
}
///////////////////////////////////////////////////////////////////////////////
// Static function for creating a LCN-controlled Turnout.
Turnout *Turnout::createLCN(int id, uint8_t state) {
Turnout *tt=create(id);
if (!tt) return(tt);
tt->data.type = TURNOUT_LCN;
tt->data.size = sizeof(tt->data.header) + sizeof(tt->data.lcnData);
tt->data.active = (state != 0);
return(tt);
}
///////////////////////////////////////////////////////////////////////////////
// Static function for associating a Turnout id with a virtual pin in IODevice space.
// The actual creation and configuration of the pin must be done elsewhere,
// e.g. in mySetup.cpp during startup of the CS.
Turnout *Turnout::createVpin(int id, VPIN vpin, uint8_t state){
if (vpin > VPIN_MAX) return NULL;
Turnout *tt=create(id);
if(!tt) return(tt);
tt->data.type = TURNOUT_VPIN;;
tt->data.size = sizeof(tt->data.header) + sizeof(tt->data.vpinData);
tt->data.active = (state != 0);
tt->data.vpinData.vpin = vpin;
IODevice::write(vpin, state); // Set initial state of output.
return(tt);
}
///////////////////////////////////////////////////////////////////////////////
// Method for creating a Servo Turnout, e.g. connected to PCA9685 PWM device.
Turnout *Turnout::createServo(int id, VPIN vpin, uint16_t activePosition, uint16_t inactivePosition, uint8_t profile, uint8_t state){
#ifndef IO_NO_HAL
if (activePosition > 511 || inactivePosition > 511 || profile > 4) return NULL;
Turnout *tt=create(id);
if (!tt) return(tt);
if (tt->data.type != TURNOUT_SERVO) tt->data.active = (state != 0); // Retain current state if it's an existing servo turnout.
tt->data.type = TURNOUT_SERVO;
tt->data.size = sizeof(tt->data.header) + sizeof(tt->data.servoData);
tt->data.servoData.vpin = vpin;
tt->data.servoData.activePosition = activePosition;
tt->data.servoData.inactivePosition = inactivePosition;
tt->data.servoData.profile = profile;
// Configure PWM interface device
int deviceParams[] = {(int)activePosition, (int)inactivePosition, profile, tt->data.active};
if (!IODevice::configure(vpin, IODevice::CONFIGURE_SERVO, 4, deviceParams)) {
remove(id);
return NULL;
}
return(tt);
#else
(void)id; (void)vpin; (void)activePosition; (void)inactivePosition; (void)profile; (void)state; // avoid compiler warnings
return NULL;
#endif
}
///////////////////////////////////////////////////////////////////////////////
// Support for <T id SERVO pin activepos inactive pos profile>
// and <T id DCC address subaddress>
// and <T id VPIN pin>
Turnout *Turnout::create(int id, int params, int16_t p[]) {
if (p[0] == HASH_KEYWORD_SERVO) { // <T id SERVO n n n n>
if (params == 5)
return createServo(id, (VPIN)p[1], (uint16_t)p[2], (uint16_t)p[3], (uint8_t)p[4]);
else
return NULL;
} else
if (p[0] == HASH_KEYWORD_VPIN) { // <T id VPIN n>
if (params==2)
return createVpin(id, p[1]);
else
return NULL;
} else
if (p[0]==HASH_KEYWORD_DCC) {
if (params==3 && p[1]>0 && p[1]<=512 && p[2]>=0 && p[2]<4) // <T id DCC n n>
return createDCC(id, p[1], p[2]);
else if (params==2 && p[1]>0 && p[1]<=512*4) // <T id DCC nn>
return createDCC(id, (p[1]-1)/4+1, (p[1]-1)%4);
else
return NULL;
} else if (params==2) { // <T id n n> for DCC or LCN
return createDCC(id, p[0], p[1]);
}
else if (params==3) { // legacy <T id n n n> for Servo
return createServo(id, (VPIN)p[0], (uint16_t)p[1], (uint16_t)p[2]);
}
return NULL;
}
///////////////////////////////////////////////////////////////////////////////
// Create basic Turnout object. The details of what sort of object it is
// controlling are not set here.
Turnout *Turnout::create(int id){
Turnout *tt=get(id);
if (tt==NULL) {
tt=(Turnout *)calloc(1,sizeof(Turnout));
if (!tt) return (tt);
tt->nextTurnout=firstTurnout;
firstTurnout=tt;
tt->data.id=id;
}
turnoutlistHash++;
return tt;
}
///////////////////////////////////////////////////////////////////////////////
//
// Object method to print debug info about the state of a Turnout object
//
#ifdef EESTOREDEBUG
void Turnout::print(Turnout *tt) {
tt->print(StringFormatter::diagSerial);
}
#endif
///////////////////////////////////////////////////////////////////////////////
Turnout *Turnout::firstTurnout=NULL;
int Turnout::turnoutlistHash=0; //bump on every change so clients know when to refresh their lists