Thats pwm channel 15 working of pin A3 - fantastic @UKHeliBob
Say what now? I think you are n+1 steps ahead of me there @camsysca
So then I am fairly happy with the activation programming and I know I still have potential connectivity problems but the realistic operation is a more important element I feel, provided I can get that something like then I'm sure I can sort the connection problems - worst case it means more arduinos
So I've done a lot of googling and youtube watching trying to reach a verditic on who's done it how I'd like and I'll be honest I've struggled - there are loads of forum posts (here, RMWeb, etc) and youtube videos claiming to have done it but a distinct lack of 'end result quality' and actual useable data (code). So I'm lost.
A lot of people also seem to be using MERG kit or JMRI to control their layout meaning the code, if they do share it, is difficult to understand as it contains all the other bits and bobs related to DCC.
The best I seem to be able to find are:
The Frontington and Backwoods Railway, video 16 on youtube. Looks promising although I'm not overly keen on the eventual movement of the arm I'm thinking the setup might be of use. Code here arduino-points-controller/PointsController_v2.ino at main ยท mafu-d/arduino-points-controller ยท GitHub
and then there is the IoTT, which seems really comprehensive but again most of the coding is completely over my head: GitHub - tanner87661/IoTT-Video17: Additional material for IoTT Video#17 DCC Servo Decoder
Anyone prepared to give me a chuck with this and help me work through it for my situation... no pressure
Can you please give examples of what you mean by realistic operation ?
Sure... this is a video from the IoTT example I linked above:
At 39 seconds you can see the raising of the arm I'd like to simulate, at 52 seconds you see a dropping arm I'd like to simulate.
At 1:01 there is an example servo moving that looks like it'll do the job.
Driving Semaphore Signals with Servos (Video#55) (youtube.com)
I think what I am trying to achieve in code is (i) a non-linear-speed movement & (ii) a bounce/wobble at the end of travel.
And another reasonable real world example
Absolute Block (AB) Semaphore Stop Signal (youtube.com)
(iii) control/vary the speed.
I wondered if slow movement might be a requirement but had not thought of bounce
Let's not try to solve the whole problem at once but deal with it incrementally. Below you will find a new version of the sketch that moves the servos slowly
NOTES :
- The sketch has my switch pin numbers in it so change them to yours and feel free to add more
- The struct has new members which need values but I believe that you have got the hang of how that works. There is a full set of data in the example
- The struct now has a function in it. Welcome to the world of Object Oriented Programming
- For illustration I have given the two signals different speeds in the array data
- Each signal could have different speeds for moving up and down but that is not in the current sketch
- The sketch now detects when a switch becomes open or closed rather than when it is open or closed. This enables the slow move function to be called only when required
- The sketch is useless as it is because whilst one signal is moving the switches cannot be read. This can, of course, be changed, but quick and dirty was easy as an experiment
- The sketch has a new function to set all signals to danger by default but that will immediately be overridden by however the switches are set
//https://forum.arduino.cc/t/programming-for-realistic-semaphore-control-using-pca-9685/1283543
#include <Adafruit_PWMServoDriver.h>
Adafruit_PWMServoDriver signalBoard01 = Adafruit_PWMServoDriver(0x40);
#define SERVOMIN 100 // this is the 'minimum' pulse length count (out of 4096)
#define SERVOMAX 710 // this is the 'maximum' pulse length count (out of 4096)
struct signalData
{
byte pinNumber;
byte clear;
byte danger;
byte pwmChannel;
byte prevSwitchState;
byte currentAngle;
byte speed;
void moveSignal(byte targetAngle)
{
if (currentAngle < targetAngle)
{
while (currentAngle < targetAngle)
{
signalBoard01.setPWM(pwmChannel, 0, currentAngle);
delay(speed);
currentAngle++;
}
}
else
{
while (currentAngle > targetAngle)
{
signalBoard01.setPWM(pwmChannel, 0, currentAngle);
delay(speed);
currentAngle--;
}
}
signalBoard01.setPWM(pwmChannel, 0, targetAngle);
}
};
signalData signals[] = {
{ A3, 200, 120, 0, HIGH, 0, 20 },
{ A2, 190, 104, 1, HIGH, 0, 50 },
};
byte signalCount = sizeof(signals) / sizeof(signals[0]);
void setup()
{
Serial.begin(115200);
pinMode(signals[0].pinNumber, INPUT_PULLUP);
pinMode(signals[1].pinNumber, INPUT_PULLUP);
Serial.println("GingerAngles Signal Control!");
signalBoard01.begin();
signalBoard01.setPWMFreq(50); // Analog servos run at ~60 Hz updates
initSignals();
}
void loop()
{
for (int s = 0; s < signalCount; s++)
{
byte currentSwitchState = digitalRead(signals[s].pinNumber);
if (currentSwitchState != signals[s].prevSwitchState && currentSwitchState == LOW)
{
signals[s].moveSignal(signals[s].clear);
}
else
{
if (currentSwitchState != signals[s].prevSwitchState && currentSwitchState == HIGH)
{
signals[s].moveSignal(signals[s].danger);
}
}
signals[s].prevSwitchState = currentSwitchState;
}
delay(10);
}
void initSignals()
{
for (int s = 0; s < signalCount; s++)
{
signals[s].currentAngle = signals[s].danger;
signals[s].moveSignal(signals[s].currentAngle);
}
}
I would be interested in your feedback and, as usual, come back with any questions
Thanks @UKHeliBob - I'll take a detailed look at that and get it updated/uploaded to the Arduino and see how we go.
Hi @UKHeliBob
Would you mind explaining what's going on here - how does this entry operate as another entity(?) in the array? There doesn't seem to be a entry for this 'data wise' in the array either?
{
if (currentAngle < targetAngle)
{
while (currentAngle < targetAngle)
{
signalBoard01.setPWM(pwmChannel, 0, currentAngle);
delay(speed);
currentAngle++;
}
}
else
{
while (currentAngle > targetAngle)
{
signalBoard01.setPWM(pwmChannel, 0, currentAngle);
delay(speed);
currentAngle--;
}
}
signalBoard01.setPWM(pwmChannel, 0, targetAngle);
What does initsignals() do?
I've modified and upload the below sketch and amazingly I haven't broken it and it works! The movement is terrible though - slow and juddery - but we are proving concept right
I notice increasing the speed value in the data table seems to slow down the servos. is the value being used here appropriate for PMW?
```#include <Adafruit_PWMServoDriver.h>
Adafruit_PWMServoDriver signalBoard01 = Adafruit_PWMServoDriver(0x40);
#define SERVOMIN 100 // this is the 'minimum' pulse length count (out of 4096)
#define SERVOMAX 710 // this is the 'maximum' pulse length count (out of 4096)
struct signalData
{
byte pinNumber;
byte clear;
byte danger;
byte pwmChannel;
byte prevSwitchState;
byte currentAngle;
byte speed;
void moveSignal(byte targetAngle)
{
if (currentAngle < targetAngle)
{
while (currentAngle < targetAngle)
{
signalBoard01.setPWM(pwmChannel, 0, currentAngle);
delay(speed);
currentAngle++;
}
}
else
{
while (currentAngle > targetAngle)
{
signalBoard01.setPWM(pwmChannel, 0, currentAngle);
delay(speed);
currentAngle--;
}
}
signalBoard01.setPWM(pwmChannel, 0, targetAngle);
}
};
signalData signals[] = {
{ 3, 200, 120, 0, HIGH, 0, 120 },
{ 4, 190, 104, 1, HIGH, 0, 130 },
{ 5, 205, 125, 2, HIGH, 0, 140 },
{ 6, 205, 125, 3, HIGH, 0, 150 },
{ A3, 205, 125, 15, HIGH, 0, 160 },
};
byte signalCount = sizeof(signals) / sizeof(signals[0]);
void setup()
{
Serial.begin(9600);
pinMode(signals[0].pinNumber, INPUT_PULLUP);
pinMode(signals[1].pinNumber, INPUT_PULLUP);
pinMode(signals[2].pinNumber, INPUT_PULLUP);
pinMode(signals[3].pinNumber, INPUT_PULLUP);
pinMode(signals[4].pinNumber, INPUT_PULLUP);
Serial.println("GingerAngles Signal Control!");
signalBoard01.begin();
signalBoard01.setPWMFreq(50); // Analog servos run at ~60 Hz updates
initSignals();
}
void loop()
{
for (int s = 0; s < signalCount; s++)
{
byte currentSwitchState = digitalRead(signals[s].pinNumber);
if (currentSwitchState != signals[s].prevSwitchState && currentSwitchState == LOW)
{
signals[s].moveSignal(signals[s].clear);
}
else
{
if (currentSwitchState != signals[s].prevSwitchState && currentSwitchState == HIGH)
{
signals[s].moveSignal(signals[s].danger);
}
}
signals[s].prevSwitchState = currentSwitchState;
}
delay(10);
}
void initSignals()
{
for (int s = 0; s < signalCount; s++)
{
signals[s].currentAngle = signals[s].danger;
signals[s].moveSignal(signals[s].currentAngle);
}
}
BTW - I think I actually like the fact it wont operate on multiple servos at once here... imagine the signalman working the levers... he cant pull 2 at the same time!
The code that you posted is part of a function that I had put in the struct as I thought that it would be a useful way to write the sketch but in practice all it does is to waste memory, so not such a good idea after all. It does not need an entry in the array of structs
As to what it actually does, it works as follows
First it checks whether the servo angle needs to be increased to get nearer the target angle and, if so, it writes the current angle to the current servo , delay()s for a while then adds 1 to the current angle. It carries on like this while the current angle is less than the target angle. The target angle, whether it is for a danger or clear signal has been passed to the function
If the servo angle needs to be decreased to reach the target angle then the same thing happens but the current angle is decreased instead of being increased
It is interesting that you say that only being able to move one signal at a a time is a good thing as I saw it as a disadvantage
I have actually rewritten the sketch so that you can have any number of signals moving at a time as a responsive system is usually seen as an advantage
I will consider making one signal at a time or many at once an option
This function sets all signals to danger when the sketch starts. If any of the switches are in the clear state then this will be honoured. In practice it has no real purpose in this sketch but I was thinking ahead to future functionality. If it proves not to be useful I will remove it
Correct. Currently we are just proving that things work (or not)
You are correct, a lower value for speed makes the servo move faster but it has nothing to do with the PWM value written to the servo, only how often it is written. Your test speed values of 120 to 160 will make the servos move very slowly as there will be a delay() of between 120 to 160 milliseconds between each servo step. If the servo is moving between 200 and 120 then it will take 80 * 120 milliseconds for the servo to move from one extreme to the other so no wonder it is slow
Now a question for you
As described above the code has to work out whether to increase the servo angle or decrease it. It would be more convenient if the value at danger was always higher than the value at clear or vice versa. Is that how it would work in practice ?
I have been tinkering with the sketch to add new features and rather than add it to this long reply I will post a new one with an explanation of the changes
OK. Here is the new version
//https://forum.arduino.cc/t/programming-for-realistic-semaphore-control-using-pca-9685/1283543
//move_signals_6
#include <Adafruit_PWMServoDriver.h>
Adafruit_PWMServoDriver signalBoard01 = Adafruit_PWMServoDriver(0x40);
#define SERVOMIN 100 // this is the 'minimum' pulse length count (out of 4096)
#define SERVOMAX 710 // this is the 'maximum' pulse length count (out of 4096)
enum states
{
AT_DANGER,
AT_CLEAR,
MOVING_TO_DANGER,
MOVING_TO_CLEAR,
BOUNCING_AT_DANGER,
BOUNCING_AT_CLEAR
};
struct signalData
{
byte pinNumber; //switch pin number
byte clearPWM; //PWM value for clear signal
byte dangerPWM; //PWM value for danger signal
byte pwmChannel; //servo channel number
byte currentAngle; //used for slow move
byte dangerSpeed; //used for slow move to danger. Lower values are faster
byte clearSpeed; //used for slow move to clear
unsigned long lastMoveTime; //time of last small move
states currentState; //current state of signal
boolean dangerBounce; //true if bouncing at danger enabled
boolean safeBounce; //true if bouncing at safe enabled
boolean finishedMove; //true if the move to a new state is finished
};
signalData signals[] = {
{ A3, 200, 120, 0, 0, 10, 50, 0, AT_DANGER, true, false, false },
{ A2, 190, 104, 1, 0, 50, 20, 0, AT_DANGER, true, false, false },
};
byte signalCount = sizeof(signals) / sizeof(signals[0]);
void setup()
{
Serial.begin(115200);
Serial.println("GingerAngles Signal Control!");
signalBoard01.begin();
signalBoard01.setPWMFreq(50); // Analog servos run at ~60 Hz updates
initSignals();
}
void loop()
{
for (int s = 0; s < signalCount; s++)
{
byte currentSwitchState = digitalRead(signals[s].pinNumber);
if (currentSwitchState == LOW) //the switch is closed. Move to clear
{
signals[s].finishedMove = false;
moveSignal(s, signals[s].clearPWM, signals[s].clearSpeed);
if (signals[s].finishedMove == true)
{
signals[s].currentState = AT_CLEAR;
}
} //end switch is closed
else
{
signals[s].finishedMove = false;
moveSignal(s, signals[s].dangerPWM, signals[s].dangerSpeed);
if (signals[s].finishedMove == true)
{
signals[s].currentState = AT_DANGER;
}
} //end switch is open
} //end for loop
}
void moveSignal(byte currentSignal, byte targetAngle, byte speed)
{
if (signals[currentSignal].finishedMove == false)
{
// Serial.println("finished move is false");
unsigned long currentTime = millis();
if (currentTime - signals[currentSignal].lastMoveTime > speed) //if it is time to move
{
signalBoard01.setPWM(signals[currentSignal].pwmChannel, 0, signals[currentSignal].currentAngle);
signals[currentSignal].lastMoveTime = currentTime; //save time move was made
if (signals[currentSignal].currentAngle < targetAngle) //work out direction to move
{
signals[currentSignal].currentAngle++;
if (signals[currentSignal].currentAngle >= targetAngle)
{
signals[currentSignal].finishedMove = true;
}
} //.end angle increasing
else
{
signals[currentSignal].currentAngle--;
if (signals[currentSignal].currentAngle <= targetAngle)
{
signals[currentSignal].finishedMove = true;
}
} //end angle decreasing
} //end time to move
} //end not finished moving
}
void initSignals()
{
for (int s = 0; s < signalCount; s++)
{
pinMode(signals[s].pinNumber, INPUT_PULLUP);
signals[s].currentState = AT_DANGER; //set all signals to danger
signals[s].currentAngle = signals[s].dangerPWM;
signalBoard01.setPWM(signals[s].pwmChannel, 0, signals[s].currentAngle);
signals[s].finishedMove = true;
}
}
The main change from your point of view is the data in the struct. Please note the comments regarding the signalData struct
- You can now define different signal speeds for moving to danger and moving to clear
- Some data items have been renamed to help readability, notably dangerPWM and clearPWM
- There are also 3 new boolean data items. See my example declaration for values that work
There are mentions of bouncing in the sketch but that is not implemented. I am just thinking ahead. The same goes for the states list of enumerated values, which you need to do nothing with
The sketch allows more than one signal to be moving at a time. See previous reply
Once I have your answer to my question in the previous reply about the relationship between dangerPWM and clearPWM the sketch may undergo a radical rewrite but will still use the struct to define the values used for each signal
No doubt there will be questions on your part !
I saw the poor man's debouncing delay() in an the earlier versions but not in this latest, and do not see anything that would mean it can be left out.
a7
If you mean the delay() in loop() then that has gone for ever. Debouncing really is not needed in this application. It does not matter if the signal arm stutters when it starts to move, it may even add to the realism, and unlike many projects where pushbuttons are used this uses simple on/off switches
Do not be deceived by the mention of bouncing in the code, this refers to a requirement (not yet implemented) to have the signal arm bounce when it reaches the end of travel to make the movement more realistic
Morning... In reply to the 1st post:
I think on the whole @UKHeliBob having the ability to operate multiple switches at once would usually be an advantage. For instance servos are quite often used to control points (i.e. the junctions on railways in case you aren't aware) and although the same logic would have applied about levers (not so true any more in this day and age I guess) you generally wouldn't want to be waiting for each point to change to be able to operate the next. Having the ability to choose whether multiple operation is turned on or not would be great if it doesn't add complexity to the code. If we need to leave that out then I'd come down on the side of making multiple operation work as this will be more 'generally usable' - you can always physically activate the levers as you want the changes to occur after all.
Got it in relation to the speed operation, I've had a play with this and now have them set to '4' as a happy medium for the time being. Is a servo 'step' a movement of a degree or a change on PWM value can I ask?
To answer you question about movement:
I wrote a long answer here which was a journey through signal types and how they are operated (for my own benefit as much as yours) and subsequently deleted it as it was way too long. to answer your question succinctly:
It wouldn't be more convenient as I think we are going to end up tweaking PWM clear and danger values to suit individual servos. However, if standardising the move from danger to clear as an increasing value helps coding or code structure wise then it would be worth it.
A bit more info - in almost all cases signals move from danger (their weighted 'fail safe' position) to a 'clear' position by means of a cable pulling on a mechanism mounted on the signal. Depending upon the signal construction this action is reversed if necessary on the signal post using levers.
Here is an example, you can see the cable but it is not obvious at 1st glance as it is in front of the ladder in the background.
That is what I though too. I will leave it as it is for now with parallel operation allowed
The servo steps are a change of PWM value written to the servo rather than an angle
That is good news as I have a change of program structure in mind that will make it more logical by removing the need to test which direction the servo is moving in because it can be implied from the current state
If I get time later I will take a look at yet another version of the sketch. What I am aiming to do is twofold
- Give you a sketch that works, at least to some degree, even if at some point I can't expand on it
- Explain how it works if you have questions so that potentially you can make changes
I will continue to use the data driven approach so that even if you don't understand how the sketch works you can easily influence how it behaves
I know you said we may need to rewrite based on the direction question but just having a go with the latest revision in any case and I noticed pinMode
has disappeared from void setup
. is this no longer required?
Ignore!
Well @UKHeliBob that all seems to work with my personalised version below...
Ignore the comment re PULLUP - I'd obviously not scrolled far enough and see this is now part of the initSignals loop(right word?)
Thanks for taking the time to add the annotation - very helpful!
Within the loop there is obviously a hierarchical structure of tabs/indentations and braces. Do tabs perform any function other than to help visualise the structure of the code?
As for the braces I gather they effectively bracket sections of code or processes to control operation? So the void loop()
has a pair of {} which start and end the loop and the principles of the loop means this repeats over and over. Where these exist within the loop e.g. those either side of signals[currentSignal].finishedMove = true;
will mean the program pauses here until the condition is met?
The void initSignals()
and void moveSignal()
{}'d sections - I assume these 'ringfence' sections of code, give them a name to be called up elsewhere in the programme and run at that time?
Here's my edited version:
Adafruit_PWMServoDriver signalBoard01 = Adafruit_PWMServoDriver(0x40);
#define SERVOMIN 100 // this is the 'minimum' pulse length count (out of 4096)
#define SERVOMAX 710 // this is the 'maximum' pulse length count (out of 4096)
enum states
{
AT_DANGER,
AT_CLEAR,
MOVING_TO_DANGER,
MOVING_TO_CLEAR,
BOUNCING_AT_DANGER,
BOUNCING_AT_CLEAR
};
struct signalData
{
byte pinNumber; //switch pin number
byte clearPWM; //PWM value for clear signal
byte dangerPWM; //PWM value for danger signal
byte pwmChannel; //servo channel number
byte currentAngle; //used for slow move
byte dangerSpeed; //used for slow move to danger. Lower values are faster
byte clearSpeed; //used for slow move to clear
unsigned long lastMoveTime; //time of last small move
states currentState; //current state of signal
boolean dangerBounce; //true if bouncing at danger enabled
boolean safeBounce; //true if bouncing at safe enabled
boolean finishedMove; //true if the move to a new state is finished
};
signalData signals[] = {
{ 3, 200, 120, 0, 0, 5, 10, 0, AT_DANGER, true, false, false },
{ 4, 190, 104, 1, 0, 5, 10, 0, AT_DANGER, true, false, false },
{ 5, 205, 125, 2, 0, 5, 10, 0, AT_DANGER, true, false, false },
{ 6, 205, 125, 3, 0, 5, 10, 0, AT_DANGER, true, false, false },
{ A3, 205, 125, 15, 0, 5, 10, 0, AT_DANGER, true, false, false },
};
byte signalCount = sizeof(signals) / sizeof(signals[0]);
void setup()
{
Serial.begin(9600);
Serial.println("GingerAngles Signal Control!");
signalBoard01.begin();
signalBoard01.setPWMFreq(50); // Analog servos run at ~60 Hz updates
initSignals();
}
void loop()
{
for (int s = 0; s < signalCount; s++)
{
byte currentSwitchState = digitalRead(signals[s].pinNumber);
if (currentSwitchState == LOW) //the switch is closed. Move to clear
{
signals[s].finishedMove = false;
moveSignal(s, signals[s].clearPWM, signals[s].clearSpeed);
if (signals[s].finishedMove == true)
{
signals[s].currentState = AT_CLEAR;
}
} //end switch is closed
else
{
signals[s].finishedMove = false;
moveSignal(s, signals[s].dangerPWM, signals[s].dangerSpeed);
if (signals[s].finishedMove == true)
{
signals[s].currentState = AT_DANGER;
}
} //end switch is open
} //end for loop
}
void moveSignal(byte currentSignal, byte targetAngle, byte speed)
{
if (signals[currentSignal].finishedMove == false)
{
// Serial.println("finished move is false");
unsigned long currentTime = millis();
if (currentTime - signals[currentSignal].lastMoveTime > speed) //if it is time to move
{
signalBoard01.setPWM(signals[currentSignal].pwmChannel, 0, signals[currentSignal].currentAngle);
signals[currentSignal].lastMoveTime = currentTime; //save time move was made
if (signals[currentSignal].currentAngle < targetAngle) //work out direction to move
{
signals[currentSignal].currentAngle++;
if (signals[currentSignal].currentAngle >= targetAngle)
{
signals[currentSignal].finishedMove = true;
}
} //.end angle increasing
else
{
signals[currentSignal].currentAngle--;
if (signals[currentSignal].currentAngle <= targetAngle)
{
signals[currentSignal].finishedMove = true;
}
} //end angle decreasing
} //end time to move
} //end not finished moving
}
void initSignals()
{
for (int s = 0; s < signalCount; s++)
{
pinMode(signals[s].pinNumber, INPUT_PULLUP);
signals[s].currentState = AT_DANGER; //set all signals to danger
signals[s].currentAngle = signals[s].dangerPWM;
signalBoard01.setPWM(signals[s].pwmChannel, 0, signals[s].currentAngle);
signals[s].finishedMove = true;
}
}
That's great, Thanks. I think I am more or less following the gist - It's obviously expanded quite quickly and there is a lot going on to understand (for a newbie anyway). I dont think I could reproduce it just yet
I understand, THX.
And it will be nice in version N if that and any stuttering in the name of verisimilitude is advertent, instead of catching a ride on what, by certain rules of the Universe, won't happen, or won't happen to a satisfactory end - real contact bounce.
a7