Programming for Realistic Semaphore Control using PCA 9685

Here is the new version

//move_signals_17
//https://forum.arduino.cc/t/programming-for-realistic-semaphore-control-using-pca-9685/1283543

//NOTE : dangerPWM must always be less than clearPWM
//https://forum.arduino.cc/t/programming-for-realistic-semaphore-control-using-pca-9685/1283543/54

#include <Adafruit_PWMServoDriver.h>
Adafruit_PWMServoDriver signalBoard01 = Adafruit_PWMServoDriver(0x40);

enum states
{
    AT_DANGER,  //each signal can be in any one of the following states
    AT_CLEAR,
    MOVING_TO_DANGER,
    MOVING_TO_CLEAR,
    BOUNCING_AT_DANGER,
    BOUNCING_AT_CLEAR
};

struct signalData  //struct for signal variables
{
    int currentPWM;             //current PWM value
    unsigned long lastMoveTime;  //time of last move
    states currentState;         //current state of signal
    boolean moveDone;            //true if the move to a new state is finished
    boolean bounceDone;          //flag to prevent bouncing being repeated
    int bounceCount;             //counter for bounces
    boolean bounceAway;          //true if bouncing away from end position
    int  bounceDistance;         //distance to bounce
};

struct signalDefs  //signal definitions
{
    const byte swPinNum;                      //switch pin number
    const int clearPWM;                      //PWM value for clear signal
    const int dangerPWM;                     //PWM value for danger signal
    const byte pwmChannel;                    //servo channel number
    const byte dangerInterval;                //interval between steps to danger.  Lower values are faster
    const byte clearInterval;                 //unterval between steps to clear
    const unsigned int dangerBounceInterval;  //interval between danger bounces
    const byte dangerNumBounces;              //number of bounces to perform at danger
    const byte dangerBouncePercent;           //percentage of total movement to bounce when reaching danger
    const unsigned int clearBounceInterval;   //interval between danger bounces
    const byte clearNumBounces;               //number of bounces to perform at danger
    const byte clearBouncePercent;            //percentage of total movement to bounce when reaching danger
};

signalDefs signals[] = {
    {A3, 150, 115, 0, 15, 10, 100, 5, 40, 200, 0, 50 },   //bounce at clear only
    { A2, 210, 110, 1, 15, 45, 200, 5, 30, 200, 8, 30 },   //bounce at both
    { A1, 800, 110, 15, 15, 45, 200, 8, 10, 200, 0, 40 },  //bounce at danger only
};

const byte signalCount = sizeof(signals) / sizeof(signals[0]);
signalData sigVars[signalCount];

unsigned long currentTime;

void setup()
{
    Serial.begin(115200);
    Serial.println("GingerAngles Signal Control!");
    signalBoard01.begin();
    signalBoard01.setPWMFreq(50);  // Analog servos run at ~60 Hz updates
    initialse();
}

void loop()
{
    for (int s = 0; s < signalCount; s++)
    {
        currentTime = millis();
        byte currentSwitchState = digitalRead(signals[s].swPinNum);
        switch (sigVars[s].currentState)
        {
            case AT_DANGER:
                if (signals[s].dangerNumBounces > 0 && sigVars[s].bounceDone == false)
                {
                    sigVars[s].currentState = BOUNCING_AT_DANGER;
                    Serial.print(signals[s].pwmChannel);
                    Serial.println(" bouncing at danger");
                    //                    sigVars[s].bounceDone = false;
                    sigVars[s].lastMoveTime = currentTime + signals[s].dangerBounceInterval;    //force immediate bounce  BBB
                    sigVars[s].currentPWM = signals[s].dangerPWM;
                    sigVars[s].bounceCount = 0;
                    sigVars[s].bounceAway = true;
                    sigVars[s].bounceDistance = (signals[s].clearPWM - signals[s].dangerPWM) / (100 / signals[s].dangerBouncePercent);  //initial bounce back distance
                }
                else if (currentSwitchState == LOW)  //the switch is closed
                {
                    sigVars[s].currentState = MOVING_TO_CLEAR;
                    Serial.print(signals[s].pwmChannel);
                    Serial.println(" moving to clear");
                    sigVars[s].moveDone = false;
                    sigVars[s].lastMoveTime = currentTime;
                }
                break;
            case MOVING_TO_CLEAR:
                moveTowardsClear(s);
                if (sigVars[s].moveDone == true)
                {
                    sigVars[s].currentState = AT_CLEAR;
                    Serial.print(signals[s].pwmChannel);
                    Serial.println(" arrived at clear");
                    sigVars[s].moveDone = false;
                    sigVars[s].bounceDone = false;
                }
                break;
            case AT_CLEAR:
                if (signals[s].clearNumBounces > 0 && sigVars[s].bounceDone == false)
                {
                    sigVars[s].currentState = BOUNCING_AT_CLEAR;
                    Serial.print(signals[s].pwmChannel);
                    Serial.println(" bouncing at clear");
                    sigVars[s].lastMoveTime = currentTime +signals[s].clearBounceInterval;  //force immediate bounce
                    sigVars[s].currentPWM = signals[s].clearPWM;
                    sigVars[s].bounceCount = 0;
                    sigVars[s].bounceAway = true;
                    sigVars[s].bounceDistance = (signals[s].clearPWM - signals[s].dangerPWM) / (100 / signals[s].dangerBouncePercent);  //initial bounce back distance
                }
                else if (currentSwitchState == HIGH)  //the switch is open
                {
                    sigVars[s].currentState = MOVING_TO_DANGER;
                    Serial.print(signals[s].pwmChannel);
                    Serial.println(" moving to danger");
                    sigVars[s].moveDone = false;
                    sigVars[s].lastMoveTime = currentTime;
                }
                break;
            case MOVING_TO_DANGER:
                moveTowardsDanger(s);
                if (sigVars[s].moveDone == true)
                {
                    sigVars[s].currentState = AT_DANGER;
                    Serial.print(signals[s].pwmChannel);
                    Serial.println(" arrived at danger");
                    sigVars[s].moveDone = false;
                    sigVars[s].bounceDone = false;
                }
                break;
            case BOUNCING_AT_CLEAR:
                if (currentTime - sigVars[s].lastMoveTime >= signals[s].clearBounceInterval)  //time to do another bounce
                {
                    if (sigVars[s].bounceAway == true)  //bounce away from clear - PWM is smaller when bounced away from clear
                    {
                        // Serial.print("bounceDistance ");
                        // Serial.println(sigVars[s].bounceDistance);
                        // Serial.print("clear PWM value ");
                        // Serial.println(signals[s].clearPWM - sigVars[s].bounceDistance);
                        // Serial.print("bounce # ");
                        // Serial.println(sigVars[s].bounceCount);

                        signalBoard01.setPWM(signals[s].pwmChannel, 0, signals[s].clearPWM - sigVars[s].bounceDistance);
                        sigVars[s].bounceAway = false;  //next movement is back to clear
                        sigVars[s].lastMoveTime = currentTime;
                        sigVars[s].bounceCount++;
                        //sigVars[s].bounceDistance -= sigVars[s].bounceDistance / signals[s].clearNumBounces;  //adjust bounce distance
                        sigVars[s].bounceDistance -= adjustBounceDistance(sigVars[s].bounceDistance, signals[s].clearNumBounces);
                        if (sigVars[s].bounceCount >= signals[s].clearNumBounces)  //done required bounces ?
                        {
                            sigVars[s].currentState = AT_CLEAR;
                            sigVars[s].bounceDone = true;
                            signalBoard01.setPWM(signals[s].pwmChannel, 0, signals[s].clearPWM);  //bounce back to clearPWM
                            Serial.print(signals[s].pwmChannel);
                            Serial.println(" stoppped bouncing at clear");
                        }
                    }
                    else  //back to danger
                    {
                        signalBoard01.setPWM(signals[s].pwmChannel, 0, signals[s].clearPWM);  //bounce back to clear
                        sigVars[s].lastMoveTime = currentTime;
                        sigVars[s].bounceAway = true;
                    }
                }  //end time to bounce
                break;

            case BOUNCING_AT_DANGER:
                if (currentTime - sigVars[s].lastMoveTime >= signals[s].dangerBounceInterval)  //time to do another bounce
                {
                    if (sigVars[s].bounceAway == true)  //bounce away from danger - PWM is smaller when bounced away from danger
                    {
                        // Serial.print("bounceDistance ");
                        // Serial.println(sigVars[s].bounceDistance);
                        // Serial.print("danger PWM value ");
                        // Serial.println(signals[s].dangerPWM + sigVars[s].bounceDistance);
                        // Serial.print("bounce # ");
                        // Serial.println(sigVars[s].bounceCount);

                        signalBoard01.setPWM(signals[s].pwmChannel, 0, signals[s].dangerPWM + sigVars[s].bounceDistance);
                        sigVars[s].bounceAway = false;  //next movement is back to danger
                        sigVars[s].lastMoveTime = currentTime;
                        sigVars[s].bounceCount++;
                        //sigVars[s].bounceDistance -= sigVars[s].bounceDistance / signals[s].dangerNumBounces;  //adjust bounce distance
                        sigVars[s].bounceDistance -= adjustBounceDistance(sigVars[s].bounceDistance, signals[s].dangerNumBounces);

                        if (sigVars[s].bounceCount >= signals[s].dangerNumBounces)  //done required bounces ?
                        {
                            sigVars[s].currentState = AT_DANGER;
                            sigVars[s].bounceDone = true;
                            signalBoard01.setPWM(signals[s].pwmChannel, 0, signals[s].dangerPWM);  //bounce back to dangerPWM
                            Serial.print(signals[s].pwmChannel);
                            Serial.println(" stoppped bouncing at danger");
                        }
                    }
                    else  //back to danger
                    {
                        signalBoard01.setPWM(signals[s].pwmChannel, 0, signals[s].dangerPWM);  //bounce back to danger
                        sigVars[s].lastMoveTime = currentTime;
                        sigVars[s].bounceAway = true;
                    }
                }  //end time to bounce
                break;

        }  //end switch case
    }      //end for loop
}

byte adjustBounceDistance(byte distance, byte numBounces)
{
    return distance / numBounces;
}

void moveTowardsClear(byte target)  //consider adding code to move in the reverse direction based on an entry in the struct
{
    if (currentTime - sigVars[target].lastMoveTime > signals[target].clearInterval)  //time to move ?
    {
        signalBoard01.setPWM(signals[target].pwmChannel, 0, sigVars[target].currentPWM);
        sigVars[target].lastMoveTime = currentTime;  //save time move was made
        sigVars[target].currentPWM++;
        if (sigVars[target].currentPWM >= signals[target].clearPWM)
        {
            sigVars[target].moveDone = true;
        }
    }
}

void moveTowardsDanger(byte target)  //consider adding code to move in the reverse direction based on an entry in the struct
{
    if (currentTime - sigVars[target].lastMoveTime > signals[target].dangerInterval)  //time to move ?
    {
        signalBoard01.setPWM(signals[target].pwmChannel, 0, sigVars[target].currentPWM);
        sigVars[target].lastMoveTime = currentTime;  //save time move was made
        sigVars[target].currentPWM--;
        if (sigVars[target].currentPWM <= signals[target].dangerPWM)
        {
            sigVars[target].moveDone = true;
        }
    }
}

void initialse()
{
    boolean error = false;
    for (int s = 0; s < signalCount; s++)
    {
        if (signals[s].dangerPWM > signals[s].clearPWM)
        {
            Serial.print("check clearPWM and dangerPWM values for signal ");
            Serial.println(signals[s].pwmChannel);
            error = true;
        }
        if (!error)
        {
            pinMode(signals[s].swPinNum, INPUT_PULLUP);
            sigVars[s].currentState = AT_DANGER;  //set all signals to danger
            sigVars[s].currentPWM = signals[s].dangerPWM;
            signalBoard01.setPWM(signals[s].pwmChannel, 0, sigVars[s].currentPWM);
            sigVars[s].moveDone = true;
            sigVars[s].bounceDone = true;
        }
    }
    while (error)
    {
    }
}

There is no change to the data layout so your previous definitions can be copied into it

The main change is that bouncing should now start immediately when the signal reaches its target angle rather than pausing, and I have also fixed the problem caused by the data types for the PWM values not being able to cope with large values

Enjoy your break

Hi Bob

Right, had a bit of a play with that and also updated the mechanical side with a servo mounted cam type thing...

I noticed an error in the "case AT_CLEAR" section where the bounce percentage used was the danger one...
sigVars[s].bounceDistance = (signals[s].clearPWM - signals[s].dangerPWM) / (100 / signals[s].dangerBouncePercent);
but changed this to the 'clear' version and all seems well.

I'll put the code below but the signalDefs I am using in the video also posted below are:
{ 5, 360, 200, 2, 2, 2, 130, 4, 50, 100, 2, 20 },

Adafruit_PWMServoDriver signalBoard01 = Adafruit_PWMServoDriver(0x40);

enum states
{
    AT_DANGER,  //each signal can be in any one of the following states
    AT_CLEAR,
    MOVING_TO_DANGER,
    MOVING_TO_CLEAR,
    BOUNCING_AT_DANGER,
    BOUNCING_AT_CLEAR
};

struct signalData  //struct for signal variables
{
    int currentPWM;             //current PWM value
    unsigned long lastMoveTime;  //time of last move
    states currentState;         //current state of signal
    boolean moveDone;            //true if the move to a new state is finished
    boolean bounceDone;          //flag to prevent bouncing being repeated
    int bounceCount;             //counter for bounces
    boolean bounceAway;          //true if bouncing away from end position
    int  bounceDistance;         //distance to bounce
};

struct signalDefs  //signal definitions
{
    const byte swPinNum;                      //switch pin number
    const int clearPWM;                      //PWM value for clear signal
    const int dangerPWM;                     //PWM value for danger signal
    const byte pwmChannel;                    //servo channel number
    const byte dangerInterval;                //interval between steps to danger.  Lower values are faster
    const byte clearInterval;                 //unterval between steps to clear
    const unsigned int dangerBounceInterval;  //interval between danger bounces
    const byte dangerNumBounces;              //number of bounces to perform at danger
    const byte dangerBouncePercent;           //percentage of total movement to bounce when reaching danger
    const unsigned int clearBounceInterval;   //interval between danger bounces
    const byte clearNumBounces;               //number of bounces to perform at danger
    const byte clearBouncePercent;            //percentage of total movement to bounce when reaching danger
};

signalDefs signals[] = {
  //pin no, clear PWM, danager PWM, PWM Channel, danger Interval, clear Interval, danger: Bounce interval; Bounce No; Bounce %, clear: Bounce interval; Bounce No; Bounce %
    { 3, 150, 115, 0, 15, 10, 100, 5, 40, 200, 0, 50 },
    { 4, 200, 130, 1, 15, 10, 100, 5, 40, 200, 0, 50 },
    { 5, 360, 200, 2, 2, 2, 130, 4, 50, 100, 2, 20 },
};

const byte signalCount = sizeof(signals) / sizeof(signals[0]);
signalData sigVars[signalCount];

unsigned long currentTime;

void setup()
{
    Serial.begin(115200);
    Serial.println("GingerAngles Signal Control!");
    signalBoard01.begin();
    signalBoard01.setPWMFreq(50);  // Analog servos run at ~60 Hz updates
    initialse();
}

void loop()
{
    for (int s = 0; s < signalCount; s++)
    {
        currentTime = millis();
        byte currentSwitchState = digitalRead(signals[s].swPinNum);
        switch (sigVars[s].currentState)
        {
            case AT_DANGER:
                if (signals[s].dangerNumBounces > 0 && sigVars[s].bounceDone == false)
                {
                    sigVars[s].currentState = BOUNCING_AT_DANGER;
                    Serial.print(signals[s].pwmChannel);
                    Serial.println(" bouncing at danger");
                    //                    sigVars[s].bounceDone = false;
                    sigVars[s].lastMoveTime = currentTime + signals[s].dangerBounceInterval;    //force immediate bounce  BBB
                    sigVars[s].currentPWM = signals[s].dangerPWM;
                    sigVars[s].bounceCount = 0;
                    sigVars[s].bounceAway = true;
                    sigVars[s].bounceDistance = (signals[s].clearPWM - signals[s].dangerPWM) / (100 / signals[s].dangerBouncePercent);  //initial bounce back distance
                }
                else if (currentSwitchState == LOW)  //the switch is closed
                {
                    sigVars[s].currentState = MOVING_TO_CLEAR;
                    Serial.print(signals[s].pwmChannel);
                    Serial.println(" moving to clear");
                    sigVars[s].moveDone = false;
                    sigVars[s].lastMoveTime = currentTime;
                }
                break;
            case MOVING_TO_CLEAR:
                moveTowardsClear(s);
                if (sigVars[s].moveDone == true)
                {
                    sigVars[s].currentState = AT_CLEAR;
                    Serial.print(signals[s].pwmChannel);
                    Serial.println(" arrived at clear");
                    sigVars[s].moveDone = false;
                    sigVars[s].bounceDone = false;
                }
                break;
            case AT_CLEAR:
                if (signals[s].clearNumBounces > 0 && sigVars[s].bounceDone == false)
                {
                    sigVars[s].currentState = BOUNCING_AT_CLEAR;
                    Serial.print(signals[s].pwmChannel);
                    Serial.println(" bouncing at clear");
                    sigVars[s].lastMoveTime = currentTime +signals[s].clearBounceInterval;  //force immediate bounce
                    sigVars[s].currentPWM = signals[s].clearPWM;
                    sigVars[s].bounceCount = 0;
                    sigVars[s].bounceAway = true;
                    sigVars[s].bounceDistance = (signals[s].clearPWM - signals[s].dangerPWM) / (100 / signals[s].clearBouncePercent);  //initial bounce back distance
                }
                else if (currentSwitchState == HIGH)  //the switch is open
                {
                    sigVars[s].currentState = MOVING_TO_DANGER;
                    Serial.print(signals[s].pwmChannel);
                    Serial.println(" moving to danger");
                    sigVars[s].moveDone = false;
                    sigVars[s].lastMoveTime = currentTime;
                }
                break;
            case MOVING_TO_DANGER:
                moveTowardsDanger(s);
                if (sigVars[s].moveDone == true)
                {
                    sigVars[s].currentState = AT_DANGER;
                    Serial.print(signals[s].pwmChannel);
                    Serial.println(" arrived at danger");
                    sigVars[s].moveDone = false;
                    sigVars[s].bounceDone = false;
                }
                break;
            case BOUNCING_AT_CLEAR:
                if (currentTime - sigVars[s].lastMoveTime >= signals[s].clearBounceInterval)  //time to do another bounce
                {
                    if (sigVars[s].bounceAway == true)  //bounce away from clear - PWM is smaller when bounced away from clear
                    {
                        // Serial.print("bounceDistance ");
                        // Serial.println(sigVars[s].bounceDistance);
                        // Serial.print("clear PWM value ");
                        // Serial.println(signals[s].clearPWM - sigVars[s].bounceDistance);
                        // Serial.print("bounce # ");
                        // Serial.println(sigVars[s].bounceCount);

                        signalBoard01.setPWM(signals[s].pwmChannel, 0, signals[s].clearPWM - sigVars[s].bounceDistance);
                        sigVars[s].bounceAway = false;  //next movement is back to clear
                        sigVars[s].lastMoveTime = currentTime;
                        sigVars[s].bounceCount++;
                        //sigVars[s].bounceDistance -= sigVars[s].bounceDistance / signals[s].clearNumBounces;  //adjust bounce distance
                        sigVars[s].bounceDistance -= adjustBounceDistance(sigVars[s].bounceDistance, signals[s].clearNumBounces);
                        if (sigVars[s].bounceCount >= signals[s].clearNumBounces)  //done required bounces ?
                        {
                            sigVars[s].currentState = AT_CLEAR;
                            sigVars[s].bounceDone = true;
                            signalBoard01.setPWM(signals[s].pwmChannel, 0, signals[s].clearPWM);  //bounce back to clearPWM
                            Serial.print(signals[s].pwmChannel);
                            Serial.println(" stoppped bouncing at clear");
                        }
                    }
                    else  //back to danger
                    {
                        signalBoard01.setPWM(signals[s].pwmChannel, 0, signals[s].clearPWM);  //bounce back to clear
                        sigVars[s].lastMoveTime = currentTime;
                        sigVars[s].bounceAway = true;
                    }
                }  //end time to bounce
                break;

            case BOUNCING_AT_DANGER:
                if (currentTime - sigVars[s].lastMoveTime >= signals[s].dangerBounceInterval)  //time to do another bounce
                {
                    if (sigVars[s].bounceAway == true)  //bounce away from danger - PWM is smaller when bounced away from danger
                    {
                        // Serial.print("bounceDistance ");
                        // Serial.println(sigVars[s].bounceDistance);
                        // Serial.print("danger PWM value ");
                        // Serial.println(signals[s].dangerPWM + sigVars[s].bounceDistance);
                        // Serial.print("bounce # ");
                        // Serial.println(sigVars[s].bounceCount);

                        signalBoard01.setPWM(signals[s].pwmChannel, 0, signals[s].dangerPWM + sigVars[s].bounceDistance);
                        sigVars[s].bounceAway = false;  //next movement is back to danger
                        sigVars[s].lastMoveTime = currentTime;
                        sigVars[s].bounceCount++;
                        //sigVars[s].bounceDistance -= sigVars[s].bounceDistance / signals[s].dangerNumBounces;  //adjust bounce distance
                        sigVars[s].bounceDistance -= adjustBounceDistance(sigVars[s].bounceDistance, signals[s].dangerNumBounces);

                        if (sigVars[s].bounceCount >= signals[s].dangerNumBounces)  //done required bounces ?
                        {
                            sigVars[s].currentState = AT_DANGER;
                            sigVars[s].bounceDone = true;
                            signalBoard01.setPWM(signals[s].pwmChannel, 0, signals[s].dangerPWM);  //bounce back to dangerPWM
                            Serial.print(signals[s].pwmChannel);
                            Serial.println(" stoppped bouncing at danger");
                        }
                    }
                    else  //back to danger
                    {
                        signalBoard01.setPWM(signals[s].pwmChannel, 0, signals[s].dangerPWM);  //bounce back to danger
                        sigVars[s].lastMoveTime = currentTime;
                        sigVars[s].bounceAway = true;
                    }
                }  //end time to bounce
                break;

        }  //end switch case
    }      //end for loop
}

byte adjustBounceDistance(byte distance, byte numBounces)
{
    return distance / numBounces;
}

void moveTowardsClear(byte target)  //consider adding code to move in the reverse direction based on an entry in the struct
{
    if (currentTime - sigVars[target].lastMoveTime > signals[target].clearInterval)  //time to move ?
    {
        signalBoard01.setPWM(signals[target].pwmChannel, 0, sigVars[target].currentPWM);
        sigVars[target].lastMoveTime = currentTime;  //save time move was made
        sigVars[target].currentPWM++;
        if (sigVars[target].currentPWM >= signals[target].clearPWM)
        {
            sigVars[target].moveDone = true;
        }
    }
}

void moveTowardsDanger(byte target)  //consider adding code to move in the reverse direction based on an entry in the struct
{
    if (currentTime - sigVars[target].lastMoveTime > signals[target].dangerInterval)  //time to move ?
    {
        signalBoard01.setPWM(signals[target].pwmChannel, 0, sigVars[target].currentPWM);
        sigVars[target].lastMoveTime = currentTime;  //save time move was made
        sigVars[target].currentPWM--;
        if (sigVars[target].currentPWM <= signals[target].dangerPWM)
        {
            sigVars[target].moveDone = true;
        }
    }
}

void initialse()
{
    boolean error = false;
    for (int s = 0; s < signalCount; s++)
    {
        if (signals[s].dangerPWM > signals[s].clearPWM)
        {
            Serial.print("check clearPWM and dangerPWM values for signal ");
            Serial.println(signals[s].pwmChannel);
            error = true;
        }
        if (!error)
        {
            pinMode(signals[s].swPinNum, INPUT_PULLUP);
            sigVars[s].currentState = AT_DANGER;  //set all signals to danger
            sigVars[s].currentPWM = signals[s].dangerPWM;
            signalBoard01.setPWM(signals[s].pwmChannel, 0, sigVars[s].currentPWM);
            sigVars[s].moveDone = true;
            sigVars[s].bounceDone = true;
        }
    }
    while (error)
    {
    }
}```
2 Likes

Well spotted

I have updated my copy too

The bounce in the video doesn't look too bad but it depends on how far you want to take this

1 Like

Well i think variable speed, acceleration and deceleration, would greatly add to the realism.
Looking at the addition of the cam mechanism, and correct me it I'm wrong, i think weve increased the bounce magnitude (in PWM terms) by about x4. Is this sufficient to allow us to introduce speed variation? If not I'll revisit the mechanics of the movement to see if I can get a greater PWM range.

I will give acceleration/deceleration some consideration but I am not convinced that it will make things more realistic at the scale we are talking about

There is also the complication that 4 types of change of speed while bouncing would be needed if aiming for perfection

  1. Lower quadrant signals at danger
  2. Lower quadrant signals at clear
  3. Upper quadrant signals at danger
  4. Upper quadrant signals at clear

In each case the effect of gravity would be different due to a combination of the direction of movement (up or down) and the angle of the signal arm (90 or 45 degrees)

1 Like

I'm not sure we need a distinction between upper and lower quadrant signals TBH as the mechanics related to their operation are pretty much the same only the direction of travel alters. If you accept that the dangerPWM is always greater than clearPWM the servo mounting can be reversed or a lever added if the signal direction is different. As for the movement the signals are weighted to the danger position and therefore during and at the end of a movement from clear to danger there is a release of tension and the most apparent bounce (signal arm hitting a physical stop). Moving from danger to clear is likely slower, with hesitation and a smaller bounce (tension/stretch in cable related movement).

I looked in just now to see what is up. I think there is no good chance of programming a servo to give anything like a realistic bounce.

Why not just make the mechanical linkages less rigid, to try to physically obtain a bounce out of the equivalent of tension and stretch and such things as are present in the real system?

It would also not be precisely the same wiggle every time.

A few rubber bands and strategically placed weights coukd do it.

If this have been mentioned, never mind. I did just let a few dozen posts scroll by until I saw the latest video.

a7

It has, (no need to apologise with a topic of this size) but the idea was rejected by @gingerangles in favour of removing slop in the system and programming the servo to create realistic movements

I, however, am not convinced either that the slop in linkages can be removed or that a servo can be programmed to provide a realistic bounce especially as originally envisaged there was very little servo movement between clear and danger. This meant that the bounce distance was very small and dividing it into a series of diminishing bounces meant that there was no real chance of programming acceleration/deceleration into the bounce movement

@gingerangles has subsequently changed the mechanics of the servo connection to the signal so that the servo movement is larger, I have not yet analysed the effect on the size of bounce movements but I suspect that it will not have made enough difference to matter. If anything the revised mechanics of the servo to signal linkage may have made the bouncing more realistic in itself

It's late in the game but, what about a rotating cam 'programmed' with bouncy movements cut into the periphery a la

image

?

Use a stepper or geared-down DC motor.

2 Likes

Thanks for the input all. I can't help but reference back to one of the original videos which kicked me off on this quest...

Driving Semaphore Signals with Servos (Video#55) (youtube.com)

Particularly the results obtained at 19:45 to 20:30.

I've tried to understand the code, however it's well beyond me for the moment... I think there may well be some significant maths behind the scenes as well which I assume is embedded in the code but the code is part of a larger project, as far as I can tell, which includes all sorts of other bits and bobs to interact with computer and "DCC" control systems for model railways.

For what it is worth I'll post the sketch provided by the video's author below, perhaps it may be of some usefulness?

//#include <avr/power.h>

#include <NmraDcc.h>

#include <Wire.h>
#include <Adafruit_MCP23017.h>
#include <Adafruit_PWMServoDriver.h>
#include <TimerOne.h>
#include "Adafruit_WS2801.h"
#include "SPI.h" 
#include <Math.h>

#define SERVO 0
#define RELAY 1

#define sgn(x) ((x) < 0 ? -1 : ((x) > 0 ? 1 : 0))

//#define measureMode

#ifdef measureMode
  int loopCounter = 0;
  uint32_t loopTimer = millis();
#endif

#define THROWN 0
#define CLOSED 1

#define SERVOMIN 210 // this is the 'minimum' pulse length count (out of 4096)
#define SERVOMAX 850 // this is the 'maximum' pulse length count (out of 4096)
#define SERVOACCEL 1500 //increments / s2 used for soft start
#define SERVODECEL 1500 //increments / s2 used for soft stop

const uint16_t refreshInterval = 5000; //microseconds. Might be too low for Nano if 2 servos are processed at the same time

//#define SERVOMIN 50 // this is the 'minimum' pulse length count (out of 4096)
//#define SERVOMAX 4000 // this is the 'maximum' pulse length count (out of 4096)

//dimension considerations
// servo speed in incr/s, ranging from -1500 to 1500 (max speed of typical servo about +/- 1200 incr/s
// modeled as 16 bit integer times 20 to make room for fractions,creating a range from -30000 .. 30000
//fractions are important to keep track of actual speed in case of small accel/decel over several steps, not all resulting in a full increment

// position in incr ranging from SERVOMIN to SERVOMAX, typical range 200= 0.5ms) to 850 (= 2.07ms in 100Hz PWM)
// modeled as 16 bit with 6bit left shift and offset 150, allowing positions from 0 (150) (0.36ms) to 1023 (1173) (2.86ms)

// accel/decel in incr/s2, typical range from 1000 - 2500 (?)
// constant defined for sketch, not adjustable per channel

#define colorDark 0x00000000    //all LED's dark
#define relayON  0x0000007F     //50% blue to indicate active coil on relay
#define relayOFF 0x00000000     //same as dark for when relay is off
#define relayThrown 0x001F0000  //relay is off and on thrown position
#define relayClosed 0x00001F00  //relay is off in closed position
#define servoMinPos 0x00001F00  //servo is in minimum position
#define servoMaxPos 0x001F0000  //servo is in maximum position
#define servoMove 0x00050500    //servo is currently moving
#define aspectHalt 0x007F0000   //signal aspect color for halt (red)
#define aspectSlow 0x005F0F00   //signal aspect color yellow (may also be used for blinking)
#define aspectClear 0x00007F00  //signal aspect color green
#define level1Col 0x000F0000    //red used for level crossing blink light

#define blinkLEDInterval 800    //duration of blink cycle (half of it, it is symmetrical)

// called this way, it uses the default address 0x40
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();
Adafruit_MCP23017 mcp;

NmraDcc Dcc;

const int DccIntrpt = 0;
const int DccDataPin = 2;
const int DccAckPin = 3; //not connected
//const int LEDDataPin = 6; //comment out if using SPI
//const int LEDClockPin = 7; //comment out if using SPI

#define numChannels 24
#define numPixels 50

Adafruit_WS2801 thisStrip = Adafruit_WS2801(numPixels);
uint32_t pixCopy[numPixels];
bool ledChg = false;

uint32_t ledBlinkTimer = millis();
uint32_t timeElapsed = 0;
bool blinkFlag = false;
float faderCtr = 0;

typedef void (*ledProcess) (void *);

typedef struct
{
  uint16_t dccAddr;
  uint8_t driveType; //obsolete for SERVO only decoder
  ledProcess ledProc; //can be substituted by a 1 byte process number
  uint8_t ledPos[5];
  uint16_t portA; //can be substituted by the position in the array for a SERVO only decoder
  uint16_t portB; //only needed for relays
  uint16_t minPos; //lower boundary in increments from SERVOMINPOS to SERVOMAXPOS
  uint16_t maxPos; //upper boundary
  uint8_t  moveConfig; //determins acceleration and deceleration of servo movements in both directions. LS nibble when increasing/CL, MS nibble when decreasing/TH
                     //nibble content: bits 0,1 define stop mode 0: hard stop 1: soft stop; 2: overshoot; 3: bounceback
                     // bit 2 defines start mode: 0: immediate start; 1: soft start
                     // bit 3 defines hesitation: 0: no hesitation; 1: hesitation active
  uint8_t  oscLambda; //4 bit value for oscillation damper, 0 - 15 interpreted as 0.5 .. 8.0  LS nibble when increasing/CL, MS nibble when decreasing/TH
  uint8_t  oscFrequency; //4 bit value for oscillation frequency, 0 - 15 interpreted as 0.5 .. 8.0 Hz  LS nibble when increasing/CL, MS nibble when decreasing/TH
  uint16_t hesitatePosition; //increments per second
  uint8_t  hesitateSpeed; //increments per second
  uint16_t targetPos; //runtime data
  uint16_t currPos; //runtime data, increments shifted left by 6, with six bits for fractions
  uint16_t moveDelayTH; //milliseconds pulse time for relays
  uint16_t moveDelayCL; //servo movement increments per second for servos. Real speed depends onf PWM frequency; CL is up, TH is down the range

  uint32_t nextMove; //runtime data milliseconds when next calculation is triggered
  float    currSpeed; //runtime data, increments per millisecond. Pos val is up, neg val is down
  uint32_t timeNull; //runtime data
  uint8_t  currMoveMode; //runtime data 0: at target; 1: accelerating; 2: linear movememnt; 3: hesitating; 4: stopping
} myServo;

void relayFct(void * thisServo);
void servoFct(void * thisServo);
void servoLinear(void * thisServo);
void aspectFct(void * thisServo);
void levelCrossingRelay(void * thisServo);
void levelCrossingServo(void * thisServo);

myServo servoArray[numChannels] = {
    {50, SERVO, &servoFct, {4,0,0,0,0}, 0, 0, SERVOMIN, SERVOMAX-150, 0x00, 0x22, 0x33, 450, 0, SERVOMIN, SERVOMIN + 1, 0, 0, 0,0,0,0},
    {51, SERVO, &servoFct, {4,0,0,0,0}, 1, 1, SERVOMIN, SERVOMAX-150, 0x00, 0x11, 0x55, 450, 0, SERVOMIN, SERVOMIN + 1, 0, 0, 0,0,0,0},
    {52, SERVO, &servoFct, {4,0,0,0,0}, 2, 2, SERVOMIN, SERVOMAX-150, 0x00, 0x11, 0x55, 500, 0, SERVOMIN, SERVOMIN + 1, 0, 0, 0,0,0,0},
    {53, SERVO, &servoFct, {4,0,0,0,0}, 3, 3, SERVOMIN, SERVOMAX-150, 0x00, 0x11, 0x33, 500, 0, SERVOMIN, SERVOMIN + 1, 0, 0, 0,0,0,0},

//    {50, SERVO, &servoFct, {4,0,0,0,0}, 0, 0, SERVOMIN, SERVOMAX-150, 0xE7, 0x22, 0x33, 450, 0, SERVOMIN, SERVOMIN + 1, 500, 550, 0,0,0,0},
//    {51, SERVO, &servoFct, {4,0,0,0,0}, 1, 1, SERVOMIN, SERVOMAX, 0x58, 0x11, 0x55, 450, 0, SERVOMIN, SERVOMIN + 1, 320, 320, 0,0,0,0},
//    {52, SERVO, &servoFct, {4,0,0,0,0}, 2, 2, SERVOMIN, SERVOMAX, 0xFE, 0x11, 0x55, 500, 0, SERVOMIN, SERVOMIN + 1, 500, 500, 0,0,0,0},
//    {53, SERVO, &servoFct, {4,0,0,0,0}, 3, 3, SERVOMIN, SERVOMAX-150, 0x00, 0x11, 0x33, 500, 0, SERVOMIN, SERVOMIN + 1, 0, 0, 0,0,0,0},

    {54, SERVO, &servoFct, {5,0,0,45,46}, 4, 4, SERVOMIN, SERVOMAX-150, 0x00, 0x11, 0x55, 0,0, SERVOMIN, SERVOMIN + 1, 0, 0, 0,0,0,0},
    {55, SERVO, &servoFct, {6,0,0,0,0}, 5, 5, SERVOMIN, SERVOMAX-150, 0x00, 0x11, 0x55, 0,0, SERVOMIN, SERVOMIN + 1, 0, 0, 0,0,0,0},
    {56, SERVO, &servoFct, {7,0,0,0,0}, 6, 6, SERVOMIN, SERVOMAX-150, 0x00, 0x11, 0x55, 0,0, SERVOMIN, SERVOMIN + 1, 0, 0, 0,0,0,0},
    {57, SERVO, &servoFct, {8,0,0,0,0}, 7, 7, SERVOMIN, SERVOMAX-150, 0x00, 0x11, 0x55, 0,0, SERVOMIN, SERVOMIN + 1, 0, 0, 0,0,0,0},

    {18, SERVO, &servoFct, {9,0,0,0,0}, 8, 8, SERVOMIN, SERVOMAX, 0x00, 0x11, 0x55, 0,0, SERVOMIN, SERVOMIN + 1, 0, 0, 0,0,0,0},
    {23, SERVO, &servoFct, {10,0,0,0,0}, 9, 9, SERVOMIN, SERVOMAX, 0x00, 0x11, 0x55, 0,0, SERVOMIN, SERVOMIN + 1, 0, 0, 0,0,0,0},
    {33, SERVO, &servoFct, {11,0,0,0,0}, 10, 10, SERVOMIN, SERVOMAX, 0x00, 0x11, 0x55, 0,0, SERVOMIN, SERVOMIN + 1, 0, 0, 0,0,0,0},
    {34, SERVO, &servoFct, {12,0,0,0,0}, 11, 11, SERVOMIN, SERVOMAX, 0x00, 0x11, 0x55, 0,0, SERVOMIN, SERVOMIN + 1, 0, 0, 0,0,0,0},

    {10800, SERVO, &aspectFct, {15,0,0,0,0}, 12, 12, SERVOMIN, SERVOMAX, 0x66, 0x11, 0x55, 0,0, SERVOMIN, SERVOMIN + 1, 400, 400, 0,0,0,0},
    {10802, SERVO, &aspectFct, {16,0,0,0,0}, 13, 13, SERVOMIN, SERVOMAX, 0x00, 0x11, 0x55, 0,0, SERVOMIN, SERVOMIN + 1, 0, 0, 0,0,0,0},
    {10804, SERVO, &aspectFct, {17,0,0,0,0}, 14, 14, SERVOMIN, SERVOMAX, 0x00, 0x11, 0x55, 0,0, SERVOMIN, SERVOMIN + 1, 0, 0, 0,0,0,0},
    {10806, SERVO, &aspectFct, {18,0,0,0,0}, 15, 15, SERVOMIN, SERVOMAX, 0x00, 0x11, 0x55, 0,0, SERVOMIN, SERVOMIN + 1, 0, 0, 0,0,0,0},

    {61, RELAY, &relayFct, {21,22,23,0,0}, 0, 1, THROWN, CLOSED, 0,0,0,0,0, CLOSED, THROWN, 2000, 2000, 0, 0, 0, 0},
    {62, RELAY, &relayFct, {24,25,26,0,0}, 2, 3, THROWN, CLOSED, 0,0,0,0,0, CLOSED, THROWN, 200, 200, 0, 0, 0, 0},
    {63, RELAY, &relayFct, {27,28,29,0,0}, 4, 5, THROWN, CLOSED, 0,0,0,0,0, CLOSED, THROWN, 200, 200, 0, 0, 0, 0},
    {64, RELAY, &relayFct, {30,31,32,0,0}, 6, 7, THROWN, CLOSED, 0,0,0,0,0, CLOSED, THROWN, 200, 200, 0, 0, 0, 0},

    {65, RELAY, &relayFct, {33,34,35,0,0}, 8, 9, THROWN, CLOSED, 0,0,0,0,0, CLOSED, THROWN, 200, 200, 0, 0, 0, 0},
    {66, RELAY, &relayFct, {36,37,38,0,0}, 10, 11, THROWN, CLOSED, 0,0,0,0,0, CLOSED, THROWN, 200, 200, 0, 0, 0, 0},
    {67, RELAY, &relayFct, {39,40,41,0,0}, 12, 13, THROWN, CLOSED, 0,0,0,0,0, CLOSED, THROWN, 4000, 1000, 0, 0, 0, 0},
    {68, RELAY, &levelCrossingRelay, {42,43,44,47,48}, 14, 15, THROWN, CLOSED, 0,0,0,0,0, CLOSED, THROWN, 200, 200, 0, 0, 0, 0},

};

uint32_t Color(byte r, byte g, byte b)
{
  //Take the lowest 5 bits of each value and append them end to end
  uint32_t retVal = (((uint32_t)r<<16) + ((uint32_t)g<<8) + (uint32_t)b);
  return retVal; 
}

uint32_t blinkColor(uint32_t posCol, uint32_t negCol=0, bool inPhase=true)
{
  if (blinkFlag ^ inPhase)
    return negCol;
  else
    return posCol;
}

uint32_t fadeColor(uint32_t posCol, uint32_t negCol=0, bool inPhase=true)
{
  float dimFactHi, dimFactLo;
  uint8_t faderExp = 8;
  if (negCol != 0)
    faderExp = 7;
  if (inPhase)
  {
    dimFactHi = (pow (2, (faderExp*faderCtr)) - 1)/255;
    dimFactLo = (pow (2, (faderExp*(1-faderCtr))) - 1)/255;
  }
  else
  {
    dimFactLo = (pow (2, (faderExp*faderCtr)) - 1)/255;
    dimFactHi = (pow (2, (faderExp*(1-faderCtr))) - 1)/255;
  }
  
  uint8_t rPos, gPos, bPos;
  rPos = round(((posCol & 0x00FF0000) >> 16) * dimFactHi) & 0xFF;
  gPos = round(((posCol & 0x0000FF00) >> 8) * dimFactHi) & 0xFF;
  bPos = round((posCol & 0x000000FF) * dimFactHi) & 0xFF;
  if (negCol != 0)
  {
    rPos = rPos + (round(((negCol & 0x00FF0000) >> 16) * dimFactLo) & 0xFF);
    gPos = gPos + (round(((negCol & 0x0000FF00) >> 8) * dimFactLo) & 0xFF);
    bPos = bPos + (round((negCol & 0x000000FF) * dimFactLo) & 0xFF);
  }
  return Color(rPos, gPos, bPos);
}

void setLED(uint16_t ledNr, uint32_t newCol)
{
  if (pixCopy[ledNr] != newCol)
  {
    pixCopy[ledNr] = newCol;
    thisStrip.setPixelColor(ledNr, newCol);
    ledChg = true;
  }
}

uint16_t getServoPos(myServo * thisServo)
{
  return (thisServo->currPos >> 6) + 150;
}

void setServoPos(myServo * thisServo, uint16_t value) //150 <= value <= 1173
{
  value = min(1173, max(value, 150));
  thisServo->currPos = (value-150)<<6;
}

void relayFct(void * thisServo)  //this strip has color sequence b,g,r
{
  myServo * currServo = (myServo*)thisServo;
  uint32_t pos1Col=colorDark, pos2Col=relayOFF, pos3Col=colorDark;
  if (currServo->targetPos != currServo->currPos) //->currPos)
    pos2Col = relayON;
  else
    if (currServo->currPos == THROWN)
      pos1Col = relayThrown;
    else
      pos3Col = relayClosed;
  if (currServo->ledPos[0] > 0)
      setLED(currServo->ledPos[0]-1, pos1Col);
  if (currServo->ledPos[1] > 0)
      setLED(currServo->ledPos[1]-1, pos2Col);
  if (currServo->ledPos[2] > 0)
      setLED(currServo->ledPos[2]-1, pos3Col);
}

void servoFct(void * thisServo)
{
  myServo * currServo = (myServo*)thisServo;
  uint32_t newCol;
  if (currServo->targetPos != getServoPos(currServo))
    newCol = blinkColor(servoMove);
  else
    if (getServoPos(currServo) == SERVOMIN)
      newCol = servoMinPos;
    else
      newCol = servoMaxPos;
  if (currServo->ledPos[0] > 0)
    setLED(currServo->ledPos[0]-1, newCol);
}

void aspectFct(void * thisServo)
{
  myServo * currServo = (myServo*)thisServo;
  uint32_t newCol;
  int currAspect = round((getServoPos(currServo) - currServo->minPos) / ((currServo->maxPos - currServo->minPos) / 8));
  switch(currAspect)
  {
    case 0: newCol = aspectHalt; break; 
    case 1:;
    case 2: newCol = aspectSlow; break;
    case 3:; 
    case 4:; 
    case 5: newCol = fadeColor(aspectSlow); break; 
    default: newCol = aspectClear; break;
  }
  if (currServo->ledPos[0] > 0)
    setLED(currServo->ledPos[0]-1, newCol);
}

void levelCrossingRelay(void * thisServo)
{
  relayFct(thisServo);
  myServo * currServo = (myServo*)thisServo;
  if ((getServoPos(currServo) == THROWN) || (getServoPos(currServo) != currServo->targetPos))
  {
    if (currServo->ledPos[3] > 0)
      setLED(currServo->ledPos[3]-1, fadeColor(level1Col));
    if (currServo->ledPos[4] > 0)
      setLED(currServo->ledPos[4]-1, fadeColor(level1Col, 0, false));
  }
  else
  {
    if (currServo->ledPos[3] > 0)
      setLED(currServo->ledPos[3]-1, colorDark);
    if (currServo->ledPos[4] > 0)
      setLED(currServo->ledPos[4]-1, colorDark);
  }
}

void levelCrossingServo(void * thisServo)
{
  servoFct(thisServo);
  myServo * currServo = (myServo*)thisServo;
  if ((getServoPos(currServo) == SERVOMAX) || (getServoPos(currServo) != currServo->targetPos))
  {
    if (currServo->ledPos[3] > 0)
      setLED(currServo->ledPos[3]-1, blinkColor(level1Col));
    if (currServo->ledPos[4] > 0)
      setLED(currServo->ledPos[4]-1, blinkColor(level1Col, 0, false));
  }
  else
  {
    if (currServo->ledPos[3] > 0)
      setLED(currServo->ledPos[3]-1, colorDark);
    if (currServo->ledPos[4] > 0)
      setLED(currServo->ledPos[4]-1, colorDark);
  }
}

// This function is called whenever a normal DCC Turnout Packet is received and we're in Output Addressing Mode
void notifyDccAccTurnoutOutput(uint16_t Addr, uint8_t Direction, uint8_t OutputPower)
{
  Serial.print("notifyDccAccTurnoutOutput: ");
  Serial.print(Addr, DEC);
  Serial.print(',');
  Serial.print(Direction, DEC);
  Serial.print(',');
  Serial.println(OutputPower, HEX);

  for (int i = 0; i < numChannels; i++)
  {
    if (Addr == servoArray[i].dccAddr)
    {
      if (Direction == 0)
        servoArray[i].targetPos = (servoArray[i].driveType == SERVO) ? servoArray[i].minPos : THROWN;
      else
        servoArray[i].targetPos = (servoArray[i].driveType == SERVO) ? servoArray[i].maxPos : CLOSED;
      if (servoArray[i].driveType == RELAY)
        setServoPos(&servoArray[i], (servoArray[i].targetPos == CLOSED) ? THROWN : CLOSED);
      servoArray[i].currMoveMode = 0; //new position command, stop any current movement
      servoArray[i].nextMove = micros(); //initialize timer
    }
  }
}

// This function is called whenever a DCC Signal Aspect Packet is received
void notifyDccSigOutputState(uint16_t Addr, uint8_t State)
{
  Addr += 10000;
  Serial.print("notifyDccSigOutputState: ");
  Serial.print(Addr, DEC);
  Serial.print(',');
  Serial.println(State, HEX);
  for (int i = 0; i < numChannels; i++)
    if ((Addr == servoArray[i].dccAddr) && (servoArray[i].driveType == SERVO))
    {
      servoArray[i].targetPos = min(servoArray[i].maxPos, servoArray[i].minPos + (State * ((servoArray[i].maxPos - servoArray[i].minPos) / 8)));
      servoArray[i].currMoveMode = 0; //new position command, stop any current movement
      servoArray[i].nextMove = micros(); //initialize timer
    }
}

void processServo(myServo *thisServo)
{
//  uint32_t thisTime = micros();
  if (thisServo->currMoveMode == 0) //linear movement mode
  {
    uint16_t currPos = getServoPos(thisServo); //incr
    if (thisServo->targetPos != currPos)
    {
      if ((thisServo->nextMove < micros()))
      {
        //analyze the current status for further decision making
        bool moveUp = thisServo->targetPos > getServoPos(thisServo);
        uint8_t adjMode = moveUp ? (thisServo->moveConfig & 0x0F) : ((thisServo->moveConfig & 0xF0) >> 4);
        float linSpeed = moveUp ? (double) thisServo->moveDelayCL : (double)(-1) * thisServo->moveDelayTH; //set linSpeed to incr/s
        bool correctDir = (sgn(thisServo->currSpeed) == sgn(linSpeed)) || (thisServo->currSpeed == 0);
        bool softStop = ((adjMode & 0x03) == 1); //soft stop
        bool softStart = (adjMode & 04); //overshoot or soft stop
        bool beforeHesitate = adjMode & 0x08 ? (moveUp ? currPos < thisServo->hesitatePosition : currPos > thisServo->hesitatePosition) : false; 
        uint16_t moveEndPoint = beforeHesitate ? thisServo->hesitatePosition : thisServo->targetPos; //end point or hesitate
        float moveEndSpeed = (beforeHesitate ? (sgn(linSpeed) * thisServo->hesitateSpeed) : (adjMode & 0x02 ? (linSpeed) : (adjMode == 0 ? (linSpeed) : (0)))); //speed when arriving at end point, incr/s
        int32_t breakDistance = round(sq(linSpeed - moveEndSpeed) / (2 * (int32_t)SERVODECEL)); //s = v2 /2a
        uint16_t breakPoint = moveEndPoint - (sgn(linSpeed) * breakDistance);
        bool beforeBreakPoint = ((int)(breakPoint - currPos) * (int)sgn(linSpeed)) > 0;

        //calculate dynamic data for step duration and width 
        uint32_t stepDelay = thisServo->currSpeed == 0 ? refreshInterval : round(1000000 / abs(thisServo->currSpeed)); //calculating the duration of 1 step in micros/incr
        float stepFactor = thisServo->currSpeed == 0 ? 1 : 1 + (refreshInterval / stepDelay);  //calculate how many steps to take assuming 5ms cycle time

        if (correctDir)
        {
          if (linSpeed != 0)
          {
            if (beforeBreakPoint)
            {
              if (abs(thisServo->currSpeed) < abs(linSpeed))
              {
                if (softStart)
                //accelerate
                  thisServo->currSpeed += round((sgn(linSpeed) * stepFactor * (stepDelay * (int32_t)SERVOACCEL) / 1000000)); //v = v0 + at
                else
                  thisServo->currSpeed = linSpeed;
                if (abs(thisServo->currSpeed) > abs(linSpeed))
                  thisServo->currSpeed = linSpeed;
              }
              //keep moving
              thisServo->currPos += round(stepFactor * 64 * sgn(linSpeed)); //set the position
              thisServo->nextMove += round(stepFactor * stepDelay); //set the delay time
            }
            else
            {
              if (getServoPos(thisServo) == thisServo->targetPos) //final position
              {
                setServoPos(thisServo, thisServo->targetPos);
                thisServo->nextMove += refreshInterval; //1ms wait                
              }
              else
              {
              //accel-/decel to moveEndSpeed
                bool endAccel = false;
                if (abs(moveEndSpeed) > abs(thisServo->currSpeed))
                //accelerate
                {
                  //add comparison to target speed at position, adjust acceleration if needed
                  thisServo->currSpeed += round((sgn(linSpeed) * stepFactor * (stepDelay * (int32_t)SERVOACCEL) / 1000000)); //v = v0 + at
                  if (abs(moveEndSpeed) <= abs(thisServo->currSpeed))
                    thisServo->currSpeed = moveEndSpeed;
                  endAccel = thisServo->currSpeed == moveEndSpeed;
                }
                else
                //decelerate
                {
                  //add comparison to target speed at position, adjust deceleration if needed
                  int sgnSpeed = sgn(thisServo->currSpeed);
                  thisServo->currSpeed -= round((sgn(linSpeed) * stepFactor * (stepDelay * (int32_t)SERVODECEL) / 1000000)); //v = v0 - at
                  if (sgn(thisServo->currSpeed) != sgnSpeed)
                    thisServo->currSpeed = 0;
                  endAccel = thisServo->currSpeed == 0;
                }
                //advance to moveEndPoint
                if (endAccel)
                {
                  setServoPos(thisServo, moveEndPoint);
                  thisServo->nextMove += refreshInterval; //1ms wait                
                }
                else
                {
                  thisServo->currPos += round(stepFactor * 64 * sgn(linSpeed)); //set the position
                  thisServo->nextMove += round(stepFactor * stepDelay); //set the delay time
                }
              }
            }
            //when there, execute move end
            bool currMoveUp = thisServo->targetPos > getServoPos(thisServo);
            if ((getServoPos(thisServo) == thisServo->targetPos) || (moveUp != currMoveUp)) //overshooting when speed > 1 incr per cycle
              if ((adjMode & 0x03) > 1) //bounce back or overshooting
              {
                setServoPos(thisServo, thisServo->targetPos);
                thisServo->currMoveMode = 1; //enter oscillation phase
                thisServo->timeNull = micros();
              }
              else
                thisServo->currSpeed = 0;
          }
          else
          {
            setServoPos(thisServo, thisServo->targetPos);
            thisServo->nextMove = micros();
          }
        }
        else
        {
          if (softStop)
          {
            //decelerate and change direction. Speed is incr/s, accel/decel is incr/s2, time intervl is 1ms
            int sgnSpeed = sgn(thisServo->currSpeed);
            thisServo->currSpeed += round((sgn(linSpeed) * stepFactor * (stepDelay * (int32_t)SERVODECEL) / 1000000)); //v = v0 - at
            if (sgn(thisServo->currSpeed) != sgnSpeed)
              thisServo->currSpeed = 0;
          }
          else
            thisServo->currSpeed = 0;
          if (thisServo->currSpeed != 0)
          {
            thisServo->currPos += round(stepFactor * 64 * sgn(linSpeed)); //set the position
            thisServo->nextMove += round(stepFactor * stepDelay); //set the delay time
          }
          else
            thisServo->nextMove += refreshInterval; //standard 1ms wait
        }
//        printPosition(thisServo->currSpeed, getServoPos(thisServo));
        pwm.setPWM(thisServo->portA, 0, getServoPos(thisServo));
      }
    }
  }
  else //oscillator mode
  {
    bool moveUp = getServoPos(thisServo) == thisServo->maxPos;
    uint8_t adjMode = moveUp ? (thisServo->moveConfig & 0x0F) : ((thisServo->moveConfig & 0xF0) >> 4);
    float thisLambda = (float)((moveUp ? (thisServo->oscLambda & 0x0F) : ((thisServo->oscLambda & 0xF0) >> 4)) + 1) / 2;
    float thisFreq = (float)((moveUp ? (thisServo->oscFrequency & 0x0F) : ((thisServo->oscFrequency & 0xF0) >> 4)) + 1) / 2;
    float timePassed2 = (float)(micros() - thisServo->timeNull) / 1000;
    float timePassed = timePassed2 / 1000;
    //calculate y(t)
    float origAmpl = thisServo->currSpeed / (TWO_PI * thisFreq);
    float currAmpl = origAmpl  * exp(thisLambda * timePassed * -1);
    float currVal = round(currAmpl * sin(TWO_PI * thisFreq * timePassed));
    uint16_t pwmVal = getServoPos(thisServo); //moveUp ? (uint16_t)SERVOMAX : (uint16_t)SERVOMIN;

    if ((currAmpl / origAmpl) > 0.1) //stop oscillator if amplitude < 10% of original value
    {
      //PWM to targetPos
      if ((adjMode & 0x03) == 2) //bounce back
        //PWM to targetPos + newAmpl
        pwmVal = pwmVal + currVal;
      else //3, overshoot
        //PWM to targetPos - abs(newAmpl)
        pwmVal = moveUp ? pwmVal - abs(currVal) : pwmVal + abs(currVal);
//      printPosition(thisServo->currSpeed, pwmVal);
      pwm.setPWM(thisServo->portA, 0, pwmVal);
    } 
    else //done, back to linear mode
    {
      pwm.setPWM(thisServo->portA, 0, getServoPos(thisServo));
      thisServo->currMoveMode = 0; 
      thisServo->currSpeed = 0; 
    }
    thisServo->nextMove += refreshInterval; //standard 1ms wait
  }
}

void printPosition(float speedVal, uint16_t posVal)
{
  Serial.print(micros());
  Serial.print(", ");
  Serial.print(speedVal);
  Serial.print(", ");
  Serial.println(posVal);
}

void processServoSimple(myServo *thisServo)
{
  if (thisServo->targetPos != getServoPos(thisServo))
  {
    if ((thisServo->nextMove < micros()))
    {
      bool moveUp = thisServo->targetPos > getServoPos(thisServo);
      uint16_t stepSpeed = moveUp ? thisServo->moveDelayCL : thisServo->moveDelayTH; //incr/s 
      if (stepSpeed > 0) //valid speed settings
      {
        uint32_t stepDelay = stepSpeed > 0 ? round(1000000 / stepSpeed) : 0; //calculating the duration of 1 step
        float stepFactor = 1 + (refreshInterval / stepDelay);  //calculate how many steps to take assuming 5ms cycle time
        thisServo->currPos += round(stepFactor * 64 * (moveUp ? 1 : (-1))); //set the position
        thisServo->nextMove += round(stepFactor * stepDelay); //set the delay time
        bool nextDir = thisServo->targetPos > getServoPos(thisServo);
        if (moveUp != nextDir) //reached targetPos, so break the movement
        {
          setServoPos(thisServo, thisServo->targetPos);
          thisServo->nextMove = micros()+ refreshInterval;
        }
      }
      else //no settings, go with maximum speed to the target
      {
        setServoPos(thisServo, thisServo->targetPos);
        thisServo->nextMove = micros()+ refreshInterval;
      }
//      printPosition(thisServo->currSpeed, getServoPos(thisServo));
      pwm.setPWM(thisServo->portA, 0, getServoPos(thisServo)); //setting the PWM output
    }
  }
}

void processRelay(myServo *thisServo)
{
  switch (thisServo->currPos)
  {
  case 0:;
  case 1:
  {
    mcp.digitalWrite(thisServo->portA, thisServo->targetPos == CLOSED);
    mcp.digitalWrite(thisServo->portB, thisServo->targetPos == THROWN);
    thisServo->nextMove = millis() + ((thisServo->targetPos == CLOSED) ? thisServo->moveDelayCL : thisServo->moveDelayTH);
    thisServo->currPos = 0xFFFF;
    break;
  }
  case 0xFFFF: //moveDelayXX is milliseconds to wait before switching relay off
    if ((thisServo->nextMove < millis()) && ((thisServo->moveDelayTH > 0) || (thisServo->moveDelayCL > 0)))
    {
      thisServo->currPos = thisServo->targetPos;
      thisServo->currMoveMode = 0; //just to be sure, should be initialized like this
      mcp.digitalWrite(thisServo->portA, 1);
      mcp.digitalWrite(thisServo->portB, 1);
    }
    break;
  }
}

void processLED(myServo *thisServo)
{
  if (thisServo->ledProc != NULL)
    thisServo->ledProc(thisServo);
}

void processLocations()
{
  for (int i = 0; i < numChannels; i++)
  {
    if ((servoArray[i].targetPos != getServoPos(&servoArray[i])) || (servoArray[i].currMoveMode != 0))
    {
      switch (servoArray[i].driveType)
      {
      case SERVO:
        if (servoArray[i].moveConfig > 0)
          processServo(&servoArray[i]);
        else
          processServoSimple(&servoArray[i]);
        break;
      case RELAY:
        processRelay(&servoArray[i]);
        break;
      }
    }
    processLED(&servoArray[i]);
  }
}

void setup()
{
//  clock_prescale_set(clock_div_1); //make this a 32MHz machine????
  
  // put your setup code here, to run once:
  Serial.begin(115200);

  // Configure the DCC CV Programing ACK pin for an output
  pinMode(DccAckPin, OUTPUT);
  // Setup which External Interrupt, the Pin it's associated with that we're using and enable the Pull-Up
  Dcc.pin(DccIntrpt, DccDataPin, 1);

  // Call the main DCC Init function to enable the DCC Receiver
  Dcc.init(MAN_ID_DIY, 10, CV29_ACCESSORY_DECODER | CV29_OUTPUT_ADDRESS_MODE, 0);

  Serial.println("Init Done");

  for (int i = 0; i < numChannels; i++)
  {
    if (servoArray[i].driveType == SERVO)
      servoArray[i].currPos = (servoArray[i].currPos - 150)<<6;
  }  

  pwm.begin();

  pwm.setPWMFreq(92); // Analog servos run at ~50 - 200 Hz updates. 92 is the closest I get to 100Hz measured

  mcp.begin(); // use default address 0
  for (int i = 0; i < 16; i++)
  {
    mcp.pinMode(i, OUTPUT);
    mcp.digitalWrite(i, 1);
  }
  
  // Start up the LED counter
  thisStrip.begin();

  // Update the strip, to start they are all 'off'

  for (int i = 0; i < numPixels; i++)
  {
    thisStrip.setPixelColor(i, 0);
    pixCopy[i] = 0;
  }
  ledChg = true;
}

void loop()
{
  // You MUST call the NmraDcc.process() method frequently from the Arduino loop() function for correct library operation
  Dcc.process();
  processLocations();
  
  timeElapsed = millis() - ledBlinkTimer;
  if (timeElapsed > blinkLEDInterval)
  {
    blinkFlag = !blinkFlag; //flip flop flag
    ledBlinkTimer += blinkLEDInterval; 
    timeElapsed -= blinkLEDInterval;
  }
  if (blinkFlag)
    faderCtr = (float)timeElapsed/(float)blinkLEDInterval;  //positive slope ramp from 0 to 1
  else
    faderCtr = 1 - ((float)timeElapsed/(float)blinkLEDInterval); //negative slope ramp from 1 to 0

  if (ledChg)
  {
    thisStrip.show();
    ledChg = false;
  }

#ifdef measureMode
  if (loopTimer < millis())
  {
    Serial.println(loopCounter);
    loopCounter = 0;
    loopTimer += 1000;
  }
  loopCounter++;
#endif  
}```

That code is almost impenetrable but I will take a look

1 Like

Glad it's not just me! :joy:
Don't worry if it's too involved to get into - like I said I suspected it was made very complex by functionality not related to what we were trying to do (but thought I'd share as I obviously wasn't sure).

Hi all

Just a quick one to say i havent left this... just been very busy with other priorities on the layout. Seemingly having reached an end of the current program configuration, thanks to a huge amount of help from @UKHeliBob i thought it may be worth while pursuing the IoTT programming and, rather than back-engineer it to bypass the DCC control i thought i may as well just make use of it. With this in mind my plan is to aquire / build the necessary components to build a dcc interface for the arduino and then pick this thread up again :+1:

1 Like

See you later :grinning:

Hi, My first post. Been having a read through this as I'm wanting to replace the built in motor on a Dapol Signal with a servo (The original Dapol retailed signals used some odd setup where it didnt remember the "state" as it worked on simply breaking a wire to change between "on and off")

During Covid I worked out a stepper motor for a turn table, using a hall switch as a reset point at start up, so thought.. Hmm, this would be easy, but alas, when I started having a look and found what your doing here I see its a little more complicated than I was expecting. I've now got to try and unravel what has been written to see if I can make good use of your project as I was most impressed..

Can I clarify, are you controlling 2 signals ? (which will be perfect later on)

Which pins in the end have you used as triggers ?

Have you given any thought to the "pull" having a moments rest as some do as the signalman pulled the lever off the latch ?

I'm looking to uses an ON/ON SPDT Switch. Along side the servo arm will be two mirco switches to work a real signal repeater to be mounted on a shelf to show the same state as the layout signal (this is a separate circuit and not via the Uno R3 board)

I'm not far down the road from you on the Robin Hood Line, just the other side of Mansfield

Best Wishes

Andy

1 Like

Hmm…
So what have I done wrong? I’m using an UNO board and it just comes up code error? (Using the last sketch posted above)

Do I have the wrong board? the error code is pasted below

/usr/local/bin/arduino-cli compile --fqbn arduino:avr:uno --build-cache-path /tmp --output-dir /tmp/3091100600/build --build-path /tmp/arduino-build-CB7A5A6AAD3ABBCE5DC80B1CBB88C124  /tmp/3091100600/Signal-bounce1

/tmp/3091100600/Signal-bounce1/Signal-bounce1.ino:1:1: error: 'Adafruit_PWMServoDriver' does not name a type
 Adafruit_PWMServoDriver signalBoard01 = Adafruit_PWMServoDriver(0x40);
 ^~~~~~~~~~~~~~~~~~~~~~~
/tmp/3091100600/Signal-bounce1/Signal-bounce1.ino: In function 'void setup()':
/tmp/3091100600/Signal-bounce1/Signal-bounce1.ino:57:5: error: 'signalBoard01' was not declared in this scope
     signalBoard01.begin();
     ^~~~~~~~~~~~~
/tmp/3091100600/Signal-bounce1/Signal-bounce1.ino:57:5: note: suggested alternative: 'signalCount'
     signalBoard01.begin();
     ^~~~~~~~~~~~~
     signalCount
/tmp/3091100600/Signal-bounce1/Signal-bounce1.ino: In function 'void loop()':
/tmp/3091100600/Signal-bounce1/Signal-bounce1.ino:147:25: error: 'signalBoard01' was not declared in this scope
                         signalBoard01.setPWM(signals[s].pwmChannel, 0, signals[s].clearPWM - sigVars[s].bounceDistance);
                         ^~~~~~~~~~~~~
/tmp/3091100600/Signal-bounce1/Signal-bounce1.ino:147:25: note: suggested alternative: 'signalCount'
                         signalBoard01.setPWM(signals[s].pwmChannel, 0, signals[s].clearPWM - sigVars[s].bounceDistance);
                         ^~~~~~~~~~~~~
                         signalCount
/tmp/3091100600/Signal-bounce1/Signal-bounce1.ino:164:25: error: 'signalBoard01' was not declared in this scope
                         signalBoard01.setPWM(signals[s].pwmChannel, 0, signals[s].clearPWM);  //bounce back to clear
                         ^~~~~~~~~~~~~
/tmp/3091100600/Signal-bounce1/Signal-bounce1.ino:164:25: note: suggested alternative: 'signalCount'
                         signalBoard01.setPWM(signals[s].pwmChannel, 0, signals[s].clearPWM);  //bounce back to clear
                         ^~~~~~~~~~~~~
                         signalCount
/tmp/3091100600/Signal-bounce1/Signal-bounce1.ino:183:25: error: 'signalBoard01' was not declared in this scope
                         signalBoard01.setPWM(signals[s].pwmChannel, 0, signals[s].dangerPWM + sigVars[s].bounceDistance);
                         ^~~~~~~~~~~~~
/tmp/3091100600/Signal-bounce1/Signal-bounce1.ino:183:25: note: suggested alternative: 'signalCount'
                         signalBoard01.setPWM(signals[s].pwmChannel, 0, signals[s].dangerPWM + sigVars[s].bounceDistance);
                         ^~~~~~~~~~~~~
                         signalCount
/tmp/3091100600/Signal-bounce1/Signal-bounce1.ino:201:25: error: 'signalBoard01' was not declared in this scope
                         signalBoard01.setPWM(signals[s].pwmChannel, 0, signals[s].dangerPWM);  //bounce back to danger
                         ^~~~~~~~~~~~~
/tmp/3091100600/Signal-bounce1/Signal-bounce1.ino:201:25: note: suggested alternative: 'signalCount'
                         signalBoard01.setPWM(signals[s].pwmChannel, 0, signals[s].dangerPWM);  //bounce back to danger
                         ^~~~~~~~~~~~~
                         signalCount
/tmp/3091100600/Signal-bounce1/Signal-bounce1.ino: In function 'void moveTowardsClear(byte)':
/tmp/3091100600/Signal-bounce1/Signal-bounce1.ino:221:9: error: 'signalBoard01' was not declared in this scope
         signalBoard01.setPWM(signals[target].pwmChannel, 0, sigVars[target].currentPWM);
         ^~~~~~~~~~~~~
/tmp/3091100600/Signal-bounce1/Signal-bounce1.ino:221:9: note: suggested alternative: 'signalCount'
         signalBoard01.setPWM(signals[target].pwmChannel, 0, sigVars[target].currentPWM);
         ^~~~~~~~~~~~~
         signalCount
/tmp/3091100600/Signal-bounce1/Signal-bounce1.ino: In function 'void moveTowardsDanger(byte)':
/tmp/3091100600/Signal-bounce1/Signal-bounce1.ino:235:9: error: 'signalBoard01' was not declared in this scope
         signalBoard01.setPWM(signals[target].pwmChannel, 0, sigVars[target].currentPWM);
         ^~~~~~~~~~~~~
/tmp/3091100600/Signal-bounce1/Signal-bounce1.ino:235:9: note: suggested alternative: 'signalCount'
         signalBoard01.setPWM(signals[target].pwmChannel, 0, sigVars[target].currentPWM);
         ^~~~~~~~~~~~~
         signalCount
/tmp/3091100600/Signal-bounce1/Signal-bounce1.ino: In function 'void initialse()':
/tmp/3091100600/Signal-bounce1/Signal-bounce1.ino:261:13: error: 'signalBoard01' was not declared in this scope
             signalBoard01.setPWM(signals[s].pwmChannel, 0, sigVars[s].currentPWM);
             ^~~~~~~~~~~~~
/tmp/3091100600/Signal-bounce1/Signal-bounce1.ino:261:13: note: suggested alternative: 'signalCount'
             signalBoard01.setPWM(signals[s].pwmChannel, 0, sigVars[s].currentPWM);
             ^~~~~~~~~~~~~
             signalCount

Error during build: exit status 1

Would be great if someone can help please. (Im wondering if im missing the PCA9685 board I cant use this ? as previously ive just driven off the UNO on its own.

Andy

1 Like

If you don't have that board, you will have to adjust your sketch to use what you have. It however is not the cause of your error.

The code from post #113 does compile with some minor adjustments (possibly due to different versions of libraries). You however don't seem to be using that code.

There is no signalBoard01 in the code in post #113.

1 Like