/***************************************************
This sketch operates the turnouts on Arduino control boards of the the N Scale Credit
Valley Railway, as defined in file "CVRTurnouts.h".
The push button and three pin Bicolor LEDs (common anode) are mounted on panels just inside the
fascia. One 470 Ohm resistor is used on each of the LEDs. Low outputs light the LEDs.
No resistor is needed on the buttons as I use the built in PULLUP command.
Created 07/31/2021 by Tom Kvichak
Modified 10/22/2021 by Will Annand
Modified 09/12/2025 by Blair Smith Reorganized to array of structs (as emailed on 9/12)
Modified 09/16/2025 by Blair Smith data for L1/L2/L3/U1/U2/U3; created .h file for definitions (as emailed on 9/16)
Modified 02/11/2026 by Blair Smith streamlined, improved commenting for publication
Modified 02/19/2026 by Blair Smith to move turnout and shut off pulse at end; changes to CVRTurnouts.h required as well
Modified 02/20/2026 by Blair Smith added blinking of onboard LED to confirm program is running
Modified 02/20/2026 by Blair Smith added serial functionality to edit turnout positions
****************************************************/
#include <Wire.h>
#include <Adafruit_PWMServoDriver.h>
//define some simple terms to be used throughout the code, avoiding hard-coded values
const int SERVOOFFPULSE = 4096; //value used to shut off the PWM pulse
const int THROWPULSE = 370; //used for default values in CVRTurnouts.h
const int CLOSEPULSE = 285;
const bool PRESSED = LOW; //terminology for button states
const bool UNPRESSED = HIGH;
const bool THROWN = LOW; //terminology for turnout states
const bool CLOSED = HIGH;
const bool LEDON = LOW; //terminology for LED states
const bool LEDOFF = HIGH;
//Choose exactly one of the following, leaving the other 5 commented out; this approach can be used to manage any number of unique
//definitions for Arduino configurations for your layout, just keep the defined names here consistent with the names used in CVRTurnouts.h!
#define U1
//#define U2
//#define U3
//#define L1
//#define L2
//#define L3
//okay, now we can pull in the chosen definition
#include "CVRTurnouts.h" //incorporates the node def'n; then we can calculate our index limit
//now, compute how many turnouts are in this definition; again, no hard coded constant values to get wrong, or update manually
const int numTurnouts = sizeof(Turnouts) / sizeof(TURNOUT); //constant for indexing Turnouts array created in CVRTunouts.h
//now, create an index variable to be used throughout the code for indexing the turnout array - N.B. index variable is GLOBAL in scope, and is used in functions
int index; //will cycle from 0 to numTurnouts-1; used to index the turnouts array from CVRTurnouts.h
uint32_t blinktime = 0; //and, a variable used to record the last time we changed the state of the LED_BUILTIN
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(); //default is 0x40 for 1st board
void setup() {
Serial.begin(115200);
serialHello(); //*new* say hello, put up the edit menu
pinMode(LED_BUILTIN, OUTPUT); //enable output mode for onboard LED pin
pwm.begin();
pwm.setPWMFreq(60);
delay(30);
for (index = 0; index < numTurnouts; index++) { //scans through Turnout table, setting pin modes as identified, and setting initial position of turnout
pinMode(Turnouts[index].buttonPin, INPUT_PULLUP);
pinMode(Turnouts[index].ledPinC, OUTPUT);
pinMode(Turnouts[index].ledPinT, OUTPUT);
assertState(); //assert initial state of all LEDs and servos; since prev is opposite, each servo will be moved in sequence
} //end of for loop
index = 0; //reset index, start processing the first turnout in loop
} //end of setup **************************************************************************************************
// N.B. We don't use a for loop or other construct; since loop() loops, we don't need to. Every pass through loop(), we check a different turnout;
// we just use the index variable to track which turnout we're working on, just as if we were using it as an iterator within a for() loop(as I did in setup()).
void loop() {
serialCheck(); //*new* process any serial command
checkButton(); //First, check for button activity
assertState(); //Second, set LEDs and PCA9685 values per new state
index = (index + 1) % numTurnouts; //Last, housekeeping; move to next turnout cyclicly
blinkLED(); //flashes onboard LED as reassurance the program is running
} //end of loop **************************************************************************************************
void checkButton() {
bool lastButtonState = Turnouts[index].buttonState; //preserve old state for status check
Turnouts[index].buttonState = digitalRead(Turnouts[index].buttonPin); //read button
if (Turnouts[index].buttonState != lastButtonState) { //changed
if (Turnouts[index].buttonState == UNPRESSED) { //has been released
Turnouts[index].turnoutState = !Turnouts[index].turnoutState; //flip state
} //end of released action
else { //has been pressed
Serial.print(F("Button ")); //Say so; this can be commented out, or removed, if so desired
Serial.print(index);
Serial.println(F(" Pressed"));
} //end of pressed action
delay(20); //Delays a little bit only if button changed - poor man's debounce
} //end of changed action
} //end of checkButton **************************************************************************************************
void assertState() { //If the state has changed, write the new pulse width and LED states, and then delay proportional to the move size. Else, do nothing
if (Turnouts[index].turnoutState != Turnouts[index].turnoutPrevState) { //take action only if changed
if (Turnouts[index].turnoutState == THROWN) { //Thrown action
pwm.setPWM(Turnouts[index].servoPin, 0, Turnouts[index].positionT); //set thrown position
digitalWrite(Turnouts[index].ledPinT, LEDON); //thrown LED on
digitalWrite(Turnouts[index].ledPinC, LEDOFF); //closed LED off
} //end of Thrown action
else { //otherwise, Closed action
pwm.setPWM(Turnouts[index].servoPin, 0, Turnouts[index].positionC); //set closed position
digitalWrite(Turnouts[index].ledPinT, LEDOFF); //thrown LED off
digitalWrite(Turnouts[index].ledPinC, LEDON); //closed LED on
} //end of Closed action
//now, delay appropriately
int delta = Turnouts[index].positionT - Turnouts[index].positionC; //calculate pulse difference
if (delta < 0) delta = delta * -1; //ensure it's positive
delay(delta); //delay by that value; this may require scaling, as it may be too small or large to allow servo completion
pwm.setPWM(Turnouts[index].servoPin, 0, SERVOOFFPULSE); //stop the PWM output, which essentially powers down the servo
Turnouts[index].turnoutPrevState = Turnouts[index].turnoutState; //finally, capture the present state as the new previous
} //end of changed action
} //end of assertState **************************************************************************************************
void blinkLED() {
if (millis() - blinktime > 500) { //if it's been 500 ms, flip the LED state
blinktime = millis(); //record time we changed LED
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); //reads the LED, writes the opposite state back to it
} //end state change
} //end blinkLED **************************************************************************************************