#include <Servo.h>
#include <EEPROM.h>
// NEED TO ADD CONDITIONAL SERIAL OUTPUT TO SUPPRESS EXCESSIVE REPORTING, or add a switch in the switch case to turn on/off some reporting. Like a loud/normal/quiet control?
// Decision - no conditional compiles. Add a debug level control value, defaults to Terse; other levels are Normal and Verbose
// Other than that, this seems like it's ready to go, at least as a beta. Now to decide whether to unfold it as a series of lessons for development. Probably not. Not enough time, nor audience.
//SECM Main Section
/***************************************************************************************************
YASC - Yet Another Servo Controller
A program to utilize a classic Nano or similar Arduino processor to
receive input from 8 digital sources and drive 8 digital outputs.
The Arduino should be mounted on a Sensor Shield for ease of wiring.
- Inputs may be driven by any mechanism that delivers a 1/0 logic level.
Suggestions include:
- Touch-Toggles,
- TTP223 touch sensors,
- SPDT switches with indicating LEDs,
- SPST toggles, if the handle is all that's needed to show directionality.
- Momentary pushbuttons could also be used, but would require a software latch.
- Outputs are servo pulse outputs
- Serial is supported for configuration
**************************************************************************************************/
/* A note on pin assignments for a Nano
D0/D1 should be reserved for serial, either for Serial Monitor or other comms
D2 - D11 assigned to servo or tortoise outputs
D12 reserved for RS485 turnaround(future)
D13 - reserved (for LED)
A0 - A5 assigned to digital inputs
A6, A7 - reserved for options selection; exact configuration TBD
If I2C is desired, A4/A5 can be freed up, and D2/D13 used instead.
If pushbuttons are desired for something, use A6/A7 and my Analog String button arrangement; see MRH, 2025 February.
N.B. If the target is a Mega 2560, then the user will have to determine what pin assignments make sense
*/
#define __YASCVERSION__ "1.0.1"
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. Set using +|- keys
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 (Touch Toggle)
Servo ourServo; //a holder for the instance of the servo
};
struct TURNOUT {
TURNOUTS S; //static elements
TURNOUTV V; //volatile elements
};
TURNOUT Turnouts[] = { //items shown are for 8 servos on a classic Nano; clearly, the list would change for different Arduinos; serial function writeline(Turnout) must be updated if this structure changes
{{2, 3, 35, 45, 0}, {}}, //a servo on pin 2, button on pin 3; moves from 45 to 135; default speed ASAP; note the volatile elements don't get initialized
{{4, 5, 35, 45, 0}, {}}, //a servo on pin 4, button on pin 5
{{6, 7, 35, 45, 0}, {}},
{{8, 9, 35, 45, 0}, {}},
{{10, 11, 35, 45, 0}, {}}, //Skips D12, D13(LED)
{{14, 15, 35, 45, 0}, {}},
{{16, 17, 35, 45, 0}, {}},
{{18, 19, 35, 45, 0}, {}}, //8 max for Nano/Uno
{{20, 21, 35, 45, 0}, {}},
{{22, 23, 35, 45, 0}, {}}, //10
{{24, 25, 35, 45, 0}, {}},
{{26, 27, 35, 45, 0}, {}},
{{28, 29, 35, 45, 0}, {}},
{{30, 31, 35, 45, 0}, {}},
{{32, 33, 35, 45, 0}, {}},
{{34, 35, 35, 45, 0}, {}},
{{36, 37, 35, 45, 0}, {}},
{{38, 39, 35, 45, 0}, {}},
{{40, 41, 35, 45, 0}, {}},
{{42, 43, 35, 45, 0}, {}}, //20
{{44, 45, 35, 45, 0}, {}},
{{46, 47, 35, 45, 0}, {}},
{{48, 49, 35, 45, 0}, {}},
{{50, 51, 35, 45, 0}, {}},
{{52, 53, 35, 45, 0}, {}},
{{54, 62, 35, 45, 0}, {}}, //Note, pins beyond 63 cannot be used for servos, as Servo.h limits pin numbers to 6 bit values.
{{55, 63, 35, 45, 0}, {}},
{{56, 64, 35, 45, 0}, {}},
{{57, 65, 35, 45, 0}, {}},
{{58, 66, 35, 45, 0}, {}}, //30
{{59, 67, 35, 45, 0}, {}},
{{60, 68, 35, 45, 0}, {}},
{{61, 69, 35, 45, 0}, {}}, //33 - that's most likely the reasonable maximum allowable for a Mega 2560, if Servo.h were fixed
};
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! Changes when updateTurnout completes a move
/* A note on servo speeds and position values
Servo pulses are emitted every 20 ms.
To move very quickly, increase the number of degrees to move between every pulse. The fastest movement, which I call ASAP, changes
the pulse width from beginning to end in one pass, and is designated by a speed value of 0. Positive speed values set the number of
degrees to move on every servo.h pulse update. Negative speed values set the number of servo.h pulse updates to be done between each
change of the servo position by 1 degree.
If servo movement in one degree increments is deemed to be too 'jerky' in slow movements, we will have to consider changing the code
to use the microsecond settings for pulse width, instead of degrees, allowing us to move in smaller increments than one degree. This
will quickly run into the resolution limitations of the cheaper servos we use for turnout control. While relatively easy to implement,
microsecond settings are much less transparent than using degrees for the casual user. Under the hood, of course, the Servo.h code
uses microseconds, so there will also always be some amount of inaccuracy, or slop, in the code when using degrees.
*/
const int16_t MAXPOS = 175; //upper limit for servo angles, used when editing
const int16_t MINPOS = 5; //lower limit for servo angles, used when editing
const int8_t MAXSPEED = 20; //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 MINSPEED = -20; //lower limit for speed setting; -20 indicates changing the angle by 1 degree every 20 servo pulse updates, or 2.5 degrees per second aggregate
void setup() {
serialStartup(); //opens port, emits hello message
readEEPROM(); //reads settings from EEPROM; function also will write EEPROM, if the EEPROM did not appear to have been configured
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[EditTurnout].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
}
for (uint8_t i = 0; i < NumTurnouts;) { //Now, for each turnout in the array, see if movement is required, and do it, continue doing it, or finish doing it, depending upon conditions found
if (updateTurnout(i)) i = i + 1; //updateTurnout returns true if it finished with the turnout, or the turnout wasn't moved
}//end of for loop
}//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 if their end limits are user-modified
serialCheck(); //handle any user commands/requests
if (updateTurnout(PresTurnout)) PresTurnout = (PresTurnout + 1) % NumTurnouts; //if done, move to next turnout to be managed
blink();
}//end of loop **************************************************************************************************************
uint32_t lastChange = 0;
void blink() { //change LED state if time has passed
uint32_t nowTime = millis();
if (nowTime - lastChange > 500) {
lastChange = nowTime; //update tracker
digitalWrite(LED_BUILTIN, digitalRead(LED_BUILTIN)); //flip LED
}
}//end blink **************************************************************************************************************
//SERIALS
/* The serial routines to date:
serialStartup() //well, duh!
serialHello() //emits hello message, code ID, and misc stuff
serialMenu() //emits a block of text identifying what editing functions are active
serialCheck() //handles all the serial editing functions
serialUpdateReport() //reports the current status of a TO that's just been moved
serialReportTurnout() //reports during the move
N.B. many serial report lines are conditioned by the debugLevel setting. When testing features, it's best to set debug to '+' via SM, or change the default to VERBOSE
*/
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.println(F(" Author: BH Smith"));
Serial.print(F(" ")); Serial.print(__DATE__);
Serial.print(F(" File: ")); Serial.println(__FILE__);
Serial.print(F(" Version: ")); Serial.println(__YASCVERSION__); //should come from rev control
}//end serialHello **************************************************************************************************
void serialMenu() { //Just a series of Serial.print() statements
Serial.print(F("---------------------------------------------------------------------\n"));
Serial.print(F("| ? display (this) user menu 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 table for code |\n"));
Serial.print(F("| < write turnout settings > recover turnout settings |\n"));
Serial.print(F("| # write EEPROM settings $ recover EEPROM settings |\n"));
Serial.print(F("| ! dump EEPROM content, hex fmt -|+ three levels of reporting |\n")); //unused: %`'";:,.
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 not in use ()-.,;:'"| and space
case '+'://increased debugging reporting
debugLevel = VERBOSE;
break;
case '-'://decreased debugging reporting
debugLevel = TERSE;
break;
case '|'://normal debugging reporting
debugLevel = NORMAL;
break;
case '?'://print the user menu
serialMenu();
break;
//the following three cases convert various characters to a number between 0 and 35; if that number is less than NumTurnouts, EditTurnout is set
case 'a' ... 'z': //accept upper or lower case
case 'A' ... 'Z': //commands for 26 turnouts
case '0' ... '9': //numbers access turnouts 26-35
cmd = cmdToIndex(cmd); //converts unconditionally
if (cmd < NumTurnouts) {
EditTurnout = cmd;
if (debugLevel == VERBOSE) serialReportTurnout(EditTurnout);
}
else Serial.println(F("attempt to select nonexistent turnout"));
break;
case '*': //print the setup of the present turnout
serialReportTurnout(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
serialReportTurnout(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
{
int 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 = setPos(Turnouts[EditTurnout].S.homePosition); //reads value, returns if within boundaries, else returns passed value
break;
case ')'://set thrown position
Turnouts[EditTurnout].S.thrownPosition = setPos(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 '<'://retrieve turnout from EEPROM
readTurnout(EditTurnout); //
break;
case '>'://write turnout to EEPROM
writeTurnout(EditTurnout); //function utilizes update, so will only write locations that need to change
break;
case '!': //dump EEPROM listing
dumpEEPROM(0, EEPROM.length());
break;
case '$'://retrieve settings from EEPROM
readEEPROM(); //
break;
case '#'://write settings to EEPROM
writeEEPROM(); //function utilizes update, so will only write locations that need to change
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
}
break;
}
}
}//end serialCheck **************************************************************************************************
void serialUpdateReport(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.ourServo.read()); Serial.print(F(")\n"));
}
}//end serialUpdateReport **************************************************************************************************
void serialReportTurnout(uint8_t tt) {
Serial.print(F(" Turnout ")); Serial.print(tt); //remind the user which turnout this config is for
Serial.print(F(" Speed: ")); Serial.print(Turnouts[tt].S.ServoSpeed); //Serial.println(); //print the speed setting
Serial.print(F(" Servo Pin: ")); Serial.print(Turnouts[tt].S.servoPin); //print the servo pin
Serial.print(F(" Buttn Pin: ")); Serial.print(Turnouts[tt].S.buttonPin); //Serial.println(); //print the button pin
Serial.print(F(" Home A: ")); Serial.print(Turnouts[tt].S.homePosition); //print the home angle
Serial.print(F(" Thrown A: ")); Serial.print(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: ")); Serial.println(Turnouts[tt].V.ourServo.read()); //print the current position
}//end serialReportTurnout **********************************************************************************************
//subfunctions of the above
uint16_t setPos(uint16_t oldval) {
uint16_t newval = Serial.parseInt();
if (newval >= MINPOS && newval <= MAXPOS)return newval;
else return oldval;
}//end setPos **************************************************************************************************************
char cmdToIndex(char c) {
if (c >= 'a') return (c - 'a'); //returns a value 0..25
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 line of the turnout indicated; code MUST be updated if the Turnout structure changes
//output line must look like: {{2, 3, 45, 135, 0}, {}}, //pin, pin, val, val, spd
Serial.print(F("{{")); Serial.print(Turnouts[to].S.servoPin);
Serial.print(F(", ")); Serial.print(Turnouts[to].S.buttonPin);
Serial.print(F(", ")); Serial.print(Turnouts[to].S.homePosition);
Serial.print(F(", ")); Serial.print(Turnouts[to].S.thrownPosition);
Serial.print(F(", ")); Serial.print(Turnouts[to].S.ServoSpeed);
Serial.print(F("}, {}},\n"));//
} //end writeline **************************************************************************************************
void writelines() {
for (uint8_t to = 0; to < NumTurnouts; to++) { //cycle EditTurnout, printing each line of the turnout table as we go
writeline(to);
}
} //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 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 let updateTurnout do it's thing
switch (offset) {
case 0: //special - go to middle, return
{
int tmp = (Turnouts[EditTurnout].S.homePosition + Turnouts[EditTurnout].S.thrownPosition) / 2;
Turnouts[EditTurnout].V.ourServo.attach(Turnouts[EditTurnout].S.servoPin); //we know we're not where we're supposed to be, so ensure servo is enabled
Turnouts[EditTurnout].V.ourServo.write(tmp); //set position
delay(tmp * 5);
}
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 < MINPOS) Turnouts[EditTurnout].S.homePosition = MINPOS;
else if (Turnouts[EditTurnout].S.homePosition > MAXPOS) Turnouts[EditTurnout].S.homePosition = MAXPOS;
}
else {
Turnouts[EditTurnout].S.thrownPosition = Turnouts[EditTurnout].S.thrownPosition + offset;
if (Turnouts[EditTurnout].S.thrownPosition < MINPOS) Turnouts[EditTurnout].S.thrownPosition = MINPOS;
else if (Turnouts[EditTurnout].S.thrownPosition > MAXPOS) Turnouts[EditTurnout].S.thrownPosition = MAXPOS;
}
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 **************************************************************************************************
//SEC UPDATES
/*
logic for moving presTurnout is as follows:
if TT hasn't changed and our position is consistent with one of the states, don't move, and reset update counter to 0
if TT has changed and we're at origin, initiate moving towards new state, and initialize update counter if speed is < 0
if TT hasn't changed, but our position is not consistent with either state, (continue) move towards new state and increment update counter if speed < 0
determine what our state is
if state not changed and position == stateposition
IDLE so just detach, in case we came from movement previous pass
else (either TT changed OR position is inconsistent)
if TT state changed
BEGIN attach servo, change position dependent on speed
else
MOVING change position dependent on speed (detail - only get here if speed not 0)
utilize waitForPulseEnd(presTurnout)
*/
bool updateTurnout(uint8_t To) {
bool retval = false; //returns true if move completed, or no move ocurred. 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, expanded logic for the movement condition
bool S = oldState == Turnouts[To].V.state;//Has the state changed?
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.ourServo.read() == Turnouts[To].S.homePosition); //are we home and actually home?
bool T = Turnouts[To].V.state == THROWN && (Turnouts[To].V.ourServo.read() == Turnouts[To].S.thrownPosition);//are we thrown and actually thrown?
if (S && (H || T)) { //IDLE, since TT hasn't changed state and position is consistent with state
if (Turnouts[To].V.ourServo.attached()) waitForPulseEnd(To); //if we're attached, wait to ensure a pulse has been emitted
Turnouts[To].V.ourServo.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
serialUpdateReport(To); //emit report of turnout change
}//end MOVING
return retval;
} //end of UpdateTurnout **************************************************************************************************
int8_t passcount = 0; //used in case of slow movement, to track how many updates have happened between changes of position.
void waitForPulseEnd(uint8_t to) { //monitors the servo output, watching for pulse end, must only be called for a servo instance that is 'attached'.
//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 *************************************************************************************************************
bool moveit(uint8_t To) {
bool retval = false;//default reply is 'not done yet'
Turnouts[To].V.ourServo.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.ourServo.write(Turnouts[To].S.homePosition);
}//end HOME
else {//THROWN
Turnouts[To].V.ourServo.write(Turnouts[To].S.thrownPosition);
} //end THROWN
delay(abs(Turnouts[To].S.homePosition - Turnouts[To].S.thrownPosition) * 10); //delay proportional to the distance; 10 results in 500 ms for a 50 degree move
//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,
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.ourServo.read(); //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) {
pos = pos > target ? pos - 1 : pos + 1; //calc new position
/* 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
*/
// constrain(pos, min(Turnouts[To].S.homePosition, Turnouts[To].S.thrownPosition), max(Turnouts[To].S.homePosition, Turnouts[To].S.thrownPosition)); //limits position change to within range of setting
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;
}
else { //going down
if (pos < Turnouts[To].S.homePosition) pos = Turnouts[To].S.homePosition;
}
}
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;
}
else { //going down
if (pos < Turnouts[To].S.thrownPosition) pos = Turnouts[To].S.thrownPosition;
}
}
passcount = Turnouts[To].S.ServoSpeed + 1; //set up passcounter for next countdown
waitForPulseEnd(To); //wait to ensure pulse has been emitted
Turnouts[To].V.ourServo.write(pos); //change position by N degrees
}
}
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.ourServo.read(); //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) {
pos = pos > target ? pos - Turnouts[To].S.ServoSpeed : pos + Turnouts[To].S.ServoSpeed; //calc new position
// constrain(pos, min(Turnouts[To].S.homePosition, Turnouts[To].S.thrownPosition), max(Turnouts[To].S.homePosition, Turnouts[To].S.thrownPosition)); //limits position change to within range of setting
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;
}
else { //going down
if (pos < Turnouts[To].S.homePosition) pos = Turnouts[To].S.homePosition;
}
}
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;
}
else { //going down
if (pos < Turnouts[To].S.thrownPosition) pos = Turnouts[To].S.thrownPosition;
}
}
waitForPulseEnd(To); //wait to ensure pulse has been emitted
Turnouts[To].V.ourServo.write(pos); //change position by N degrees
}
}
break;
default: //any non-valid speed gets limited
Serial.print(F("Invalid speed selection detected: ")); Serial.print(Turnouts[To].S.ServoSpeed); Serial.print(F("Limiting to: "));
if (Turnouts[To].S.ServoSpeed < 0) {
Serial.println(MINSPEED);
Turnouts[To].S.ServoSpeed = MINSPEED;
}
else {
Serial.println(MAXSPEED);
Turnouts[To].S.ServoSpeed = MAXSPEED;
}
break;
}//end of switch selector
if (debugLevel == VERBOSE) serialReportTurnout(To);
if (Turnouts[To].V.state == HOME) {
if (Turnouts[To].S.homePosition == Turnouts[To].V.ourServo.read()) retval = true;
}
else {
if (Turnouts[To].S.thrownPosition == Turnouts[To].V.ourServo.read()) retval = true;
}
return retval;
} //end moveit ********************************************************************************************************
//SECE EEPROM
/* Notes about the stored content of the EEPROM:
Byte addresses 0 and 1 must contain 0xAA, 0x55 respectively, or the EEPROM is presumed to be empty. This is the "signature" referred to.
byte address 2 tells us how many Turnout entries are in the table
byte address 3 tells us how many bytes make up each Turnout entry in the table
Values for each turnout variable that must be stored are then stored sequentially.
EEPROM layout:
uint16_t signature
uint8_t sizeTable
uint8_t sizeTurnout
2d array of bytes - [tableSize}{turnoutSize]
public EEPROM functions to date:
readEEPROM() - read entire EEPROM.
readTurnout(TO) - read current Turnout from EEPROM,
writeEEPROM() - write entire EEPROM, and
writeTurnout(TO) - write current Turnout to EEPROM,
dumpEEPROM(add, len) - print, in hex, in 32 byte lines, EEPROM content starting at add, running for length bytes.
All other functions should be considered private to the EEPROM managment code.
It behooves the user to write regularly, to avoid loss of settings should a read be necessary!
*/
const uint16_t SIGNATURE = 0x55aa;
const uint16_t EEPBASE = 0; //EEPROM starts at address zero in Nano, Uno, and Mega processors
const uint16_t SIGLOC = EEPBASE;
const uint16_t TABSIZLOC = SIGLOC + sizeof(SIGNATURE);
const uint16_t ITMSIZLOC = TABSIZLOC + sizeof(NumTurnouts);
const uint16_t TABLOC = ITMSIZLOC + sizeof(NumTurnouts);
void readEEPROM() {//reads settings from EEPROM; function also will write EEPROM after a dialog, if the EEPROM did not appear to have been configured
if (checkEEPROMSignature() && getTurnoutNum() && getTurnoutSize()) {
Serial.print(F("Reading Turnout Array\n"));
readTurnoutArray();//read in the array
}
else {
Serial.print(F("Writing EEPROM\n"));
writeEEPROM();
}
}//end readEEPROM *********************************************************************************************************
bool checkEEPROMSignature() { //verifies that two byte signature is present in locations 0,1 of EEPROM
uint16_t EEPSig;
EEPROM.get(SIGLOC, EEPSig);
if (SIGNATURE == EEPSig) return true;
else {
Serial.print(F("Signature error\n"));
return false;
}
}//end checkEEPROMSignature *********************************************************************************************************
bool getTurnoutNum() { //reads stored number from EEPROM; must match NumTurnouts
uint8_t sizeTable;
EEPROM.get(TABSIZLOC, sizeTable);
if (sizeTable == NumTurnouts) return true;
else {
Serial.print(F("TurnoutNum incorrect\n"));
return false;
}
}//end getTurnoutNum *********************************************************************************************************
bool getTurnoutSize() { //reads stored number from EEPROM; must match sizeof(Turnout)
uint8_t sizeTurnout;
EEPROM.get(ITMSIZLOC, sizeTurnout);
if (sizeTurnout == sizeof(TURNOUT)) return true;
else {
Serial.print(F("TURNOUT size error\n"));
return false;
}
}//end getTurnoutSize *********************************************************************************************************
void readTurnoutArray() { //reads data into Turnouts[] based on two sizes read
for (uint8_t to = 0; to < NumTurnouts; to++) {
readTurnout(to);
}
}//end readTurnoutArray *********************************************************************************************************
void readTurnout(uint8_t to) { //called numTurnout times by readTurnoutArray()
EEPROM.put(TABLOC + to * sizeof(TURNOUT), Turnouts[to]); //perform read
}//end readTurnout *********************************************************************************************************
void writeEEPROM() { //write signature, table size, element size, and table contents to EEPROM
EEPROM.put(SIGLOC, SIGNATURE); //write sig
EEPROM.put(TABSIZLOC, NumTurnouts); //write number of elements
EEPROM.put(ITMSIZLOC, sizeof(TURNOUT)); //write number of bytes in an element
for (uint8_t to = 0; to < NumTurnouts; to++) { //write Turnouts
writeTurnout(to);
}
}//end writeEEPROM *********************************************************************************************************
void writeTurnout(uint8_t to) { //writes specified Turnout to EEPROM if loop detects a change; also called repetitively by writeEEPROM()
EEPROM.put(TABLOC + to * sizeof(TURNOUT), Turnouts[to]); //perform write
} //end writeTurnout *********************************************************************************************************
void dumpEEPROM(uint16_t start, uint16_t length) { //utility to display EEPROM content on request. Call from SERIALS
uint8_t dat;
for (uint16_t add = start; add < start + length; add++) {
if (add % 32 == 0) Serial.print(F("\n "));
dat = EEPROM.read(add);
if (dat < 16) Serial.print('0');
Serial.print(dat, HEX);
if (add % 32 != 31) Serial.print(' ');
}
Serial.println();
}//end dumpEEPROM *************************************************************************************************************