#include <Servo.h>
/****************************************************************************************************************************************************************
YASC - Yet Another Servo Controller
/ Written for a classic Arduino Nano using the Arduino V1.x IDE, but adaptable to any suitable board \
__________/ Implements a simple 1:1 input to output control of standard hobby servos for turnout control on Model Railroads \__________
_________/ Inputs may be toggle switches, digital signals from other processors, Berrett Hill Touch-Toggles, TTP223 Touch \_________
________/ controls; basically, anything that provides a 1/0 signal to indicate which of two positions the servo should move to \________
/ User may modify positions or servo speed on-the-fly using the Arduino Serial Monitor, or any serial Terminal Program \
______/ Settings are saved in non-volatile memory at the request of the user, and automatically retrieved at startup \______
_____/ At startup, user is presented with a menu of available commands; the menu may be recalled at any time by entering '?' \_____
____/ N.B. This code uses microseconds for servo settings. Program development would have been far less convenient, \____
___/ had we not had the use of an excellent Arduino Simulator, at Wokwi.com. \___
***** The above header references some features not present in this stripped version, they are added in later *****
*****************************************************************************************************************************************************************
Version information: Root is YASC 1.0.6; reduced for Lesson 5, then serial editing layer restored
*****************************************************************************************************************************************************************/
//MAIN Section XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
const bool HOME = 0; //alleviates having to think about ones and zeros when evaluating logic
const bool THROWN = 1; //
enum DBG {TERSE, NORMAL, VERBOSE} debugLevel = NORMAL; //default level of serial feedback is normal. Change to verbose when developing
struct TURNOUTS { //values that are static and must be stored for use after next reset/power up; serial function writeline(Turnout) must be updated if this structure changes
uint8_t servoPin; //where we send the pulse to position the servo
uint8_t buttonPin; //where we get the setting for the servo from(i.e. the Touch Toggle connection)
int16_t homePosition; //the angle needed for 'homing' the turnout
int16_t thrownPosition; //the angle needed for 'throwing' the turnout
int8_t ServoSpeed; //initially zero, causes ASAP movement; positive values are degrees/update, negative are updates/degree, allowing a broad range of speed
};
struct TURNOUTV { //values that are volatile, initialized after every reset/power up
bool state; //commanded state of the servo (input from digital)
Servo thisServo; //a holder for the instance of the servo
};
//I make the distinction of static vs volatile attributes for the turnouts, both as a memory aid(oh, yeah, this one doesn't persist...), and because it makes
//reading and writing that part which should go into the EEPROM, and come from it, dead simple.
struct TURNOUT { //a struct of structs
TURNOUTS S; //static elements
TURNOUTV V; //volatile elements
};
//do we need a fn to throw all turnouts to the half way point for setup? Don't think so. I've just set DEFHOME and DEFTHRN to the same number, run the code with all attached
//on the bench (or installed, but with no horns), then install them and connect them one at a time and adjust each at that point? Seems like that would be the best option.
//N.B. Usually, if there are more than one board to be controlled, we create multiple turnout tables, using an #ifdef to select which one gets compiled; for
//example, in a similar code I've provided to a friend, there are six separate table instances for his six Arduinos; only one code, but six Turnouts[] tables
const uint16_t DEFHOME = 1505; //starting point for Home position; 10 us equates to a little bit more than one degree
const uint16_t DEFTHRN = 1495; //starting point for Thrown position
const int8_t MAXSPEED = 100; //upper limit for speed setting; 20 indicates that, on every servo update, the angle will change by 20 degrees, or 1000 degrees/second (in theory)
const int8_t ASAP = 0; //as the word implies, move as quickly as possible to commanded position. Negative and positive values are all slower than this.
const int8_t MINSPEED = -10; //lower limit for speed setting; -50 indicates changing the angle by 1 degree every 50 servo pulse updates, or 1 degree per second aggregate
TURNOUT Turnouts[] = { //items shown are for 8 servos on a classic Nano; N.B. serial function writeline(Turnout) must be updated if TURNOUTS structure changes
{{ 2, 3, DEFHOME, DEFTHRN, ASAP}, {}}, //a servo on pin 2, button on pin 3; default speed ASAP; note the volatile elements don't get initialized
{{ 4, 5, DEFHOME, DEFTHRN, ASAP}, {}}, //a servo on pin 4, button on pin 5
{{ 6, 7, DEFHOME, DEFTHRN, ASAP}, {}},
{{ 8, 9, DEFHOME, DEFTHRN, ASAP}, {}},
{{10, 11, DEFHOME, DEFTHRN, ASAP}, {}},
{{14, 15, DEFHOME, DEFTHRN, ASAP}, {}}, //Skips D12, D13(LED)
{{16, 17, DEFHOME, DEFTHRN, ASAP}, {}},
{{18, 19, DEFHOME, DEFTHRN, ASAP}, {}}, //8 max for Nano/Uno; add rows, change pins as needed for MEGA; N.B. - servo pins must be <64 due to Servo.h limitation
};
const uint32_t LEDDuration = 500; //duration for heartbeat LED blinking; blinking is symmetrical
const uint8_t NumTurnouts = sizeof(Turnouts) / sizeof(TURNOUT); //automatically determines the number of turnouts represented in the Turnouts array - user just adds/deletes rows as desired
uint8_t PresTurnout = 0; //index used to indicate which turnout is presently active in update
uint8_t EditTurnout = 0; //index used to indicate which turnout is presently active for serial editing; due to update scanning, we cannot presume it's the same as PresTurnout!
void setup() {
serialStartup(); //opens port, emits hello message
serialMenu(); //emits the command menu for configuring turnouts
for (uint8_t i = 0; i < NumTurnouts; i++) { //Now, for each turnout in the array, do the following actions once
pinMode(Turnouts[i].S.buttonPin, INPUT_PULLUP); //make the button input pullup to ensure consistant readings, else the attached turnout will flicker if the input isn't connected.
Turnouts[i].V.state = THROWN; //the button will read as 'HOME' in update, so pre-force the state to 'THROWN' to trigger an update
}
}//end of setup ******************************************************************************************************
void loop() {
//note how, since loop repeats endlessly, we don't need a for loop in loop. By this mechanism, loop automatically cycles through
//updating all turnouts, moving them only if their state input has changed, or in future, if their end limits are user-modified
serialCheck(); //checks serial for any user commands/requests and executes them
// updateTurnout(N) either moves turnout N and returns 'false', or verifies N is where it should be and returns 'true'; we use that to conditionally move to the next turnout
if (updateTurnout(PresTurnout)) PresTurnout = (PresTurnout + 1) % NumTurnouts; //if done, move to next turnout to be managed; N.B. in this case, we must wrap around to 0
UpdateBuiltinLED(LEDDuration); //changes the onboard LED if it's time; having a flashing indicator that the software isn't 'stuck' somewhere is useful, so I usually put it in
}//end of loop **************************************************************************************************************
void serialStartup() {
Serial.begin(115200); //configures the connection to Serial Monitor, the terminal window for the IDE
Serial.flush(); //swallows any startup trash
serialHello(); //report program name, version, date
}//end serialStartup **************************************************************************************************
void serialHello() { //Just a series of Serial.print() statements
//print Hello banner here
Serial.println(F("----------------------------------------------------------------"));
Serial.println(F(" Touch-Toggle Servo Control"));
Serial.print(F(" ")); Serial.print(__DATE__);
Serial.print(F(" File: ")); Serial.println(__FILE__);
}//end serialHello **************************************************************************************************
///end MAIN section XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
//UPDATES section XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
bool updateTurnout(uint8_t To) { //returns true if move completed, or no move occurred
bool retval = false; //Default to false
bool oldState = Turnouts[To].V.state; //save old state for change check
Turnouts[To].V.state = digitalRead(Turnouts[To].S.buttonPin); //read new state
//for clarity, we have expanded the logic for the movement condition
bool S = (oldState == Turnouts[To].V.state); //Is the state the same?
if (!S) EditTurnout = To; //ensure that when editing, it's always the most recently moved turnout that is edited
bool H = Turnouts[To].V.state == HOME && (Turnouts[To].V.thisServo.readMicroseconds() == Turnouts[To].S.homePosition); //are we home and actually home?
bool T = Turnouts[To].V.state == THROWN && (Turnouts[To].V.thisServo.readMicroseconds() == Turnouts[To].S.thrownPosition); //or, are we thrown and actually thrown?
if (S && (H || T)) { //IDLE, since input hasn't changed state and position is consistent with state
if (Turnouts[To].V.thisServo.attached()) waitForPulseEnd(To); //if we're still attached, wait to ensure a pulse has been emitted
Turnouts[To].V.thisServo.detach(); //ensures movement stopped, power consumption at minimum
retval = true; //signal that we can move on to next turnout
}//end IDLE
else {//we are MOVING
if (moveit(To)) //returns true if move completed, i.e. if state position == position, false otherwise
updateReport(To); //emit report of turnout change complete; let next cycle signal complete though, after detach() has been called
}//end MOVING
return retval;
} //end of UpdateTurnout **************************************************************************************************
void waitForPulseEnd(uint8_t to) { //monitors the servo output, watching for pulse end, must only be called for a servo instance that is 'attached' or we hang forever
//must wait for high, then wait for low; could potentially delay software for 20 ms, as we can't sync to the interrupt; good thing nothing else is happening!
while (!digitalRead(Turnouts[to].S.servoPin));//waits while signal low; will fall through if already high.
while (digitalRead(Turnouts[to].S.servoPin));//waits while signal high
}//end of waitForPulseEnd *************************************************************************************************************
static int8_t passcount = 0; //used in case of slow movement, to track how many updates have happened between changes of position.
bool moveit(uint8_t To) {
bool retval = false;//default reply is 'not done yet'
Turnouts[To].V.thisServo.attach(Turnouts[To].S.servoPin); //we know we're not where we're supposed to be, so ensure servo is enabled
switch (Turnouts[To].S.ServoSpeed) {
case 0: //MOVE ASAP
//move ASAP - zero indicates a move immediate to opposite side
if (Turnouts[To].V.state == HOME) {//HOME
Turnouts[To].V.thisServo.writeMicroseconds(Turnouts[To].S.homePosition);
}//end HOME
else {//THROWN
Turnouts[To].V.thisServo.writeMicroseconds(Turnouts[To].S.thrownPosition);
} //end THROWN
delay(abs(Turnouts[To].S.homePosition - Turnouts[To].S.thrownPosition)); //delay proportional to the distance; with pos in us, removed factor of 10
//end ASAP
break;
case MINSPEED ... -1: //move 1 degree every n pulse ends
/* pass counter value cycles between 0 and speed value, which is negative. Logic is:
if pass counter < 0, add 1 and exit; else by defn pass counter is 0, so we must calculate new position,
only if not already arrived,(this should never happen, why guard against it?)
set pass counter to (speed + 1),
wait for negative transition, and write new value
*/
if (passcount < 0) passcount = passcount + 1;
else {//can only get here if passcount = 0 and we're in negative speed
int16_t pos = Turnouts[To].V.thisServo.readMicroseconds(); //get current position; needs to be local variable
int16_t target = Turnouts[To].V.state == HOME ? Turnouts[To].S.homePosition : Turnouts[To].S.thrownPosition; //determine target position
if (pos != target) {//need to move
pos = pos > target ? pos - 1 : pos + 1;
/* constrain pos to within the included limits home to thrown.
if we're going home then if home > thrown ensure pos <= home else ensure pos >= home
else if home > thrown ensure pos >= thrown else ensure pos <= thrown
*/
if (Turnouts[To].V.state == HOME) {//if we're going home
if (Turnouts[To].S.homePosition > Turnouts[To].S.thrownPosition) { //we're going up
if (pos > Turnouts[To].S.homePosition) pos = Turnouts[To].S.homePosition;
}//end going up
else { //going down
if (pos < Turnouts[To].S.homePosition) pos = Turnouts[To].S.homePosition;
}//end going down
}
else {//state is thrown
if (Turnouts[To].S.thrownPosition > Turnouts[To].S.homePosition) {//we're moving up
if (pos > Turnouts[To].S.thrownPosition) pos = Turnouts[To].S.thrownPosition;
}//end going up
else { //going down
if (pos < Turnouts[To].S.thrownPosition) pos = Turnouts[To].S.thrownPosition;
}//end going down
}//end THROWN
passcount = Turnouts[To].S.ServoSpeed + 1; //set up passcounter for next countdown
waitForPulseEnd(To); //wait to ensure pulse has been emitted
Turnouts[To].V.thisServo.writeMicroseconds(pos); //change position by N degrees
}//end need to move
}//end move 1 every n updates
break;
case 1 ... MAXSPEED: //move by n degrees after every end of pulse
{ /* Calculate new position, wait until we see negative edge, then update the position by N degrees
First, we need to determine what the next value should be for position.
- read back current position - call that position
- determine where we are going based on the home/thrown status flag, and the relevant position; call that target
- know how much to add/subtract from that position - call that delta
only if not already there, (this should never happen, why guard against it?)
if target < delta, pos - delta; else pos + delta
constrain(position, min(home,thrown), max(home,thrown))
wait for negative edge, then write result
*/
int16_t pos = Turnouts[To].V.thisServo.readMicroseconds(); //get current position; needs to be local variable
int16_t target = Turnouts[To].V.state == HOME ? Turnouts[To].S.homePosition : Turnouts[To].S.thrownPosition; //determine target position
if (pos != target) {//need to move
pos = pos > target ? pos - Turnouts[To].S.ServoSpeed : pos + Turnouts[To].S.ServoSpeed; //calc new position
if (Turnouts[To].V.state == HOME) {//if we're going home
if (Turnouts[To].S.homePosition > Turnouts[To].S.thrownPosition) { //we're going up
if (pos > Turnouts[To].S.homePosition) pos = Turnouts[To].S.homePosition;
}//end going up
else { //going down
if (pos < Turnouts[To].S.homePosition) pos = Turnouts[To].S.homePosition;
}//end going down
}//end HOME
else {//state is thrown
if (Turnouts[To].S.thrownPosition > Turnouts[To].S.homePosition) {//we're moving up
if (pos > Turnouts[To].S.thrownPosition) pos = Turnouts[To].S.thrownPosition;
}//end going up
else { //going down
if (pos < Turnouts[To].S.thrownPosition) pos = Turnouts[To].S.thrownPosition;
}//end going down
}//end THROWN
waitForPulseEnd(To); //wait to ensure pulse has been emitted
Turnouts[To].V.thisServo.writeMicroseconds(pos); //change position by N degrees
} //end need to move
}//end move by n
break;
default: //any non-valid speed gets limited
if (Turnouts[To].S.ServoSpeed < 0) {//limit to min
Turnouts[To].S.ServoSpeed = MINSPEED;
}//end limit to min
else {//limit to max
Turnouts[To].S.ServoSpeed = MAXSPEED;
}//end limit to max
break;
}//end of switch selector
if (debugLevel == VERBOSE) reportTurnout(To);//reports every pass through update, very noisy!
if (Turnouts[To].V.state == HOME) {//if position = home, signal we're done
if (Turnouts[To].S.homePosition == Turnouts[To].V.thisServo.readMicroseconds()) retval = true;
}//endif
else {//if position = thrown, signal we're done
if (Turnouts[To].S.thrownPosition == Turnouts[To].V.thisServo.readMicroseconds()) retval = true;
}//endif
return retval;
} //end moveit ********************************************************************************************************
void updateReport(uint8_t to) { //emit single line report of turnout update action
//of the form "Turnout NN moved to HOME/THROWN (NNN)" where NNN is the angle being set per the Turnout table entry
if (debugLevel != TERSE) {
Serial.print(F("Turnout ")); Serial.print(to); Serial.print(F(" moved to "));
if (Turnouts[to].V.state == HOME) Serial.print(F(" HOME ")); else Serial.print(F("THROWN"));
Serial.print(F(" (")); Serial.print(Turnouts[to].V.thisServo.readMicroseconds()); Serial.print(F(")\n"));
}//end debug level check
}//end updateReport **************************************************************************************************
void reportTurnout(uint8_t tt) {
Serial.print(F(" Turnout ")); sp(2, tt); //remind the user which turnout this config is for
Serial.print(F(" Speed: ")); sp(3, Turnouts[tt].S.ServoSpeed); //Serial.println(); //print the speed setting
Serial.print(F(" Servo Pin: ")); sp(2, Turnouts[tt].S.servoPin); //print the servo pin
Serial.print(F(" Buttn Pin: ")); sp(2, Turnouts[tt].S.buttonPin); //Serial.println(); //print the button pin
Serial.print(F(" Home A: ")); sp(4, Turnouts[tt].S.homePosition); //print the home angle
Serial.print(F(" Thrown A: ")); sp(4, Turnouts[tt].S.thrownPosition); //Serial.println(); //print the thrown angle
Serial.print(F(" State: ")); Turnouts[tt].V.state == HOME ? Serial.print(F(" HOME ")) : Serial.print(F("THROWN ")); //print the state
Serial.print(F(" Position: ")); sp(4, Turnouts[tt].V.thisServo.readMicroseconds()); Serial.println(); //print the current position
}//end reportTurnout **********************************************************************************************
void sp(uint8_t width, int16_t value) { //print integer with a consistant width; very brute-force, but not called often, and serial rate-limited anyway
if (width >= 5 && value < 10000) Serial.print(' ');
if (width >= 4 && value < 1000) Serial.print(' ');
if (width >= 3 && value < 100) Serial.print(' ');
if (width >= 2 && value < 10) Serial.print(' ');
Serial.print(value);
}//end sp *********************************************************************************************
//end UPDATES section XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
//SERIAL Section XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
void serialMenu() { //Just a series of Serial.print() statements
Serial.print(F("------------------------------------------------------------------------\n"));
Serial.print(F("| ? display (this) user menu ! three levels of error reporting |\n"));
Serial.print(F("| 0...9 select turnouts 26-35 A...Z select turnouts 0-25 |\n"));
Serial.print(F("| * describe selected turnout & copy designated turnout |\n"));
Serial.print(F("| = test position ~ swap home/thrown positions |\n"));
Serial.print(F("| \\ decrease speed by 1 / increase speed by 1 |\n"));
Serial.print(F("| [ decrease position by 1 ] increase position by 1 |\n"));
Serial.print(F("| { decrease position by 5 } increase position by 5 |\n"));
Serial.print(F("| ( set home position ) set thrown position |\n"));
Serial.print(F("| ^ print table line for code @ print full table for code |\n"));
Serial.print(F("------------------------------------------------------------------------\n"));
}//end serialMenu **************************************************************************************************
void serialCheck() { //check serial - if character received, process
if (Serial.available() > 0) { //cases listed in same order as they appear in the serial Menu; edit to maintain that order!
char cmd = Serial.read();//simply reads a single char from serial;
switch (cmd) { // characters unused: `'";:,. and space
case '!'://change debugging reporting
debugLevel = debugLevel + 1; //will automagically wrap
Serial.print("Debug Level: "); Serial.println(debugLevel);
break;
case '?'://print the user menu
serialMenu();
break;
//the following two cases convert various characters to a number between 0 and 35; if that number is less than NumTurnouts, EditTurnout is set
case 'A' ... 'Z': //commands for 26 turnouts
case '0' ... '9': //numbers access turnouts 26-35
cmd = cmdToIndex(cmd); //converts unconditionally, so only call with chars that meet the range desired
if (cmd < NumTurnouts) {
EditTurnout = cmd;
if (debugLevel == VERBOSE) reportTurnout(EditTurnout);
}
else Serial.println(F("attempt to select nonexistent turnout"));
break;
case '*': //print the setup of the present turnout
reportTurnout(EditTurnout);
break;
case '&'://copy designated turnout to present turnout; must be followed by a designator, valid A-Z, within 10 ms (so for example enter "&f" in SM), or will be ignored.
delay(10);//wait for char to be queued, just in case
if (Serial.available()) {
cmd = Serial.read(); //recycle cmd, we no longer need it
cmd = cmdToIndex(cmd); //converts unconditionally
if (cmd < NumTurnouts) {
Turnouts[EditTurnout].S.homePosition = Turnouts[(int)cmd].S.homePosition; //copy home
Turnouts[EditTurnout].S.thrownPosition = Turnouts[(int)cmd].S.thrownPosition; //copy thrown
Turnouts[EditTurnout].S.ServoSpeed = Turnouts[(int)cmd].S.ServoSpeed; //copy speed
reportTurnout(EditTurnout);
}
else Serial.println(F("attempt to copy nonexistent turnout"));
}
break;
case '='://move the points to center and then return, for testing positioning
adjustPosition(0);//0 is shorthand for this functionality
break;
case '~'://swap homePosition and thrownPosition, then let updateTurnout do it's thing
{
int16_t tempPos = Turnouts[EditTurnout].S.thrownPosition;
Turnouts[EditTurnout].S.thrownPosition = Turnouts[EditTurnout].S.homePosition;
Turnouts[EditTurnout].S.homePosition = tempPos;
}
break;
case '\\'://decrease speed setting by 1
adjustSpeed(-1);
break;
case '/'://increase speed setting by 1
adjustSpeed(1);
break;
case '['://decrease present position by 1
adjustPosition(-1);
break;
case ']'://increase present position by 1
adjustPosition(1);
break;
case '{'://decrease present position by 5
adjustPosition(-5);
break;
case '}'://increase present position by 5
adjustPosition(5);
break;
case '('://set home position
Turnouts[EditTurnout].S.homePosition = getPos(Turnouts[EditTurnout].S.homePosition); //reads value, returns if within boundaries, else returns passed value
break;
case ')'://set thrown position
Turnouts[EditTurnout].S.thrownPosition = getPos(Turnouts[EditTurnout].S.thrownPosition); //reads value, returns if within boundaries, else returns passed value
break;
case '^': //print the position table row for EditTurnout, exactly as it appears in the definitions before setup. That way, it can be copied verbatim...
writeline(EditTurnout);
break;
case '@': //print the complete position table, preserving EditTurnout.
writelines();
break;
case 0x0d://swallow CR
case 0x0a://swallow LF
break;
default: //anything else, we report
if (debugLevel != TERSE) {
Serial.print(F("Character ")); Serial.print(cmd); Serial.print(F(" hex value: ")); Serial.print(cmd, HEX); Serial.println(F(" has no function")); //advise about unrecognized command character
}//end debug level check
break;
}//end of switch case for character
}//end of if clause for serial char available
}//end serialCheck **************************************************************************************************
//subfunctions of the above
uint16_t getPos(uint16_t oldval) { //extracts a valid position value from the serial stream
uint16_t newval = Serial.parseInt();
if (newval >= MIN_PULSE_WIDTH && newval <= MAX_PULSE_WIDTH)return newval;
else return oldval;
}//end getPos **************************************************************************************************************
char cmdToIndex(char c) { //converts a char to an index value for the turnout array; NO BOUNDS CHECKING, presumes we got here via a switch statement.
if (c >= 'A') return (c - 'A'); //returns a value 0..25
if (c >= '0') return (c - '0' + 26); //returns a value 26...35
else return c; //
}//end cmdToIndex ***************************************************************************************************8
void writeline(uint8_t to) { //print the code line of the turnout indicated; code MUST be updated if the Turnout structure changes; intention is, this line can be pasted back into this software, with care.
//output line must look like: {{2, 3, 45, 135, 0}, {}}, //pin, pin, val, val, spd
Serial.print(F("{{")); sp(2, Turnouts[to].S.servoPin);
Serial.print(F(", ")); sp(2, Turnouts[to].S.buttonPin);
Serial.print(F(", ")); sp(4, Turnouts[to].S.homePosition);
Serial.print(F(", ")); sp(4, Turnouts[to].S.thrownPosition);
Serial.print(F(", ")); sp(3, Turnouts[to].S.ServoSpeed);
Serial.print(F("}, {}},\n"));
} //end writeline **************************************************************************************************
void writelines() { //generates a complete table of turnout settings, for pasting back into this software. Use with care!
for (uint8_t to = 0; to < NumTurnouts; to++) { //cycle EditTurnout, printing each line of the turnout table as we go
writeline(to);
}//end of for loop
} //end writelines **************************************************************************************************
void adjustPosition(int8_t offset) {
//uses combination of EditTurnout and state to identify which value to modify in the table(adjusted by offset value, with defined limits applied), and then we let updateTurnout do it's thing
//special value is 0, which causes turnout points to go to the position halfway between home and thrown, and then we let updateTurnout do it's thing
switch (offset) {
case 0: //special - go to middle, return
{
int16_t tmp = (Turnouts[EditTurnout].S.homePosition + Turnouts[EditTurnout].S.thrownPosition) / 2;
Turnouts[EditTurnout].V.thisServo.attach(Turnouts[EditTurnout].S.servoPin); //we know we're not where we're supposed to be, so ensure servo is enabled
Turnouts[EditTurnout].V.thisServo.writeMicroseconds(tmp); //set position
}
break;
case 1 ... 10:
case -10 ... -1:
if (Turnouts[EditTurnout].V.state == HOME) {
Turnouts[EditTurnout].S.homePosition = Turnouts[EditTurnout].S.homePosition + offset;
if (Turnouts[EditTurnout].S.homePosition < MIN_PULSE_WIDTH) Turnouts[EditTurnout].S.homePosition = MIN_PULSE_WIDTH;
else if (Turnouts[EditTurnout].S.homePosition > MAX_PULSE_WIDTH) Turnouts[EditTurnout].S.homePosition = MAX_PULSE_WIDTH;
}//end HOME
else {//THROWN
Turnouts[EditTurnout].S.thrownPosition = Turnouts[EditTurnout].S.thrownPosition + offset;
if (Turnouts[EditTurnout].S.thrownPosition < MIN_PULSE_WIDTH) Turnouts[EditTurnout].S.thrownPosition = MIN_PULSE_WIDTH;
else if (Turnouts[EditTurnout].S.thrownPosition > MAX_PULSE_WIDTH) Turnouts[EditTurnout].S.thrownPosition = MAX_PULSE_WIDTH;
}//end THROWN
break;
default:
break;
}
}//end adjustPosition **************************************************************************************************
void adjustSpeed(int8_t sp) {
//adjust the present speed setting by the passed value, then limit to the range MINSPEED <= speed <= MAXSPEED
Turnouts[EditTurnout].S.ServoSpeed = Turnouts[EditTurnout].S.ServoSpeed + sp; //change value
if (Turnouts[EditTurnout].S.ServoSpeed > MAXSPEED) Turnouts[EditTurnout].S.ServoSpeed = MAXSPEED; //check against high limit
if (Turnouts[EditTurnout].S.ServoSpeed < MINSPEED) Turnouts[EditTurnout].S.ServoSpeed = MINSPEED; //check against low limit
}// end adjustSpeed **************************************************************************************************
//end SERIAL section XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX