Pages

Monday, September 16, 2024

How I radio control my semaphore signals - Part 2

 Introduction

Ten years ago, after constructing 19 semaphore signals for my railway (see How I constructed 19 semaphore signals), I put some of them under radio control using a Picaxe microprocessor and a cheap keyfob transmitter (see How I radio-controlled some of my signals).

However, whilst the Picaxe programming of the signal arms movement was excellent (thanks to my mate Greg), the system as a whole was quite cumbersome. The relays on the receiver-board needed a 12v supply, for which I used a lead-acid battery, and four signals needed to be hard-wired to the receiver/controller. As I wanted to be able to remove the signals at the end of each running session, they needed weatherproof plugs and sockets and the control box either needed to be weatherproof or removable.

What I really wanted was to make each signal self-contained, so it could be easily deployed and removed - no central control-box and no trailing wires.

 Over the past ten years, I have been looking-out for something which would be suitable. What I really needed was a compact, low-voltage (and cheap!) receiver, which could be housed beside or beneath each signal so when I spotted this key fob RC system on AliExpress,....

.... I was delighted. It seemed to fulfil my needs; it could operate off a 3.7v supply (ie one li-ion cell) and each of the four receivers had a small footprint (22.5mm x 11mm) (see - qiachip.com )

What I needed next was an Arduino microprocessor with a similarly small footprint. Whilst I could have used a Picaxe 8M, I would have also needed a board on which to mount it and also some additional components. As it turned out, a Seeeduino XIAO (21mm x 18mm) seemed to fit the bill, especially as I had been learning Arduino coding for another project (see Controlling a loco with Arduino and Bluetooth). Furthermore, it could similarly operate from a 3.7v supply.

  All I needed now was a very small servo. Again, I was fortunate in unearthing a tiny  (21mm x 15mm x 12mm) linear servo, which seemed ideal. It, too, could be powered by 3.7v.

  And, finally, I needed some small lithium-ion cells. Although I had a couple of AA (14500) cells to hand, I would need another 17 to power-up all the signals. A quick scan of eBay revealed that the cheapest cells were 16340s (ie 16mm diameter x 34mm long).

Although these claim to be 2800mAh, I know from experience that this needs to be taken with an enormous slab of salt. Also, for the price, I would have to treat them with extreme caution - especially when charging. However, at only around £1GBP per cell, I was willing to give them a try.



 Programming

So, with the hardware now sorted-out, I now needed to figure out the Arduino coding to make it work.

The receiver could set up to provide either a momentary 3.3v (HIGH) or a 0v (LOW) output - ideal for triggering the Seeeduino. In addition, it's relatively straightforward to provide outputs to control servos with Arduino coding.

My initial code simply sent the servo either up or down, with an output on Pin 6 dependent on a 0v (LOW) triggering signal on Pin 0. 

#include <servo.h> //Loads the servo command library
Servo myservo; //Names the servo "myservo"


//variables
int sigstate=0; //Flags the state of the signal (1 = raised 0 = lowered)

void setup() { //The inital set-up procedure
 myservo.attach(6); //Attaches myservo to Pin 6
 myservo.write(0); //Sends myservo to 0 degrees
 pinMode(0,INPUT_PULLUP);//Sets Pin 0 to input and sets it initially to HIGH
} //End of the setup procedure

void loop() { //The main loop procedure
  if (sigstate==0){ //Checks the signal state - If lowered (ie sigstate 0) .....
    raiseit(); // .... it raises the signal
    sigstate=1; // and sets the sigstate to raised (ie 1)
    delay (500); // Pauses half a second
  }
else { //If the signal is already raised ......
  lowerit(); // .... the signal is lowered
  sigstate=0; // and sets the sigstate flag to 0
  delay (500); // then waits half a second
}
} //End of the main loop

void raiseit(){ //The raise sub procedure
myservo.write(180); // Sends the servo to 180 degrees
}

void lowerit(){ //The lower sub procedure
myservo.write(0); // Sends the servo to 0 degrees
}

 Having satisfied myself that the hardware was performing as it should, I increased the sophistication of the programming to move the signal arms more slowly and include some sort of bounce at the end of each movement.

#include <Servo.h> //Loads the servo command library
Servo myservo; //Names the servo


//variables
int sigstate=1; //Logs the state of the signal arm (1=raised, 0=lowered)
int val; //Used to record the state of the triggering input from the receiver
int x=0; //Used in For..... loops
float y; //Allows for variable y to have a floating decimal value
int z; //Also used in For.... loops

void setup() { //Initialising instructions
 myservo.attach(6); //Connects a servo output to Pin 6
 myservo.write(140); //Sends the servo to 40 degrees (ie raised)
 pinMode(0,INPUT_PULLUP); //Sets pin 0 to HIGH by default
} //End of initialising 

void loop() { //Main program loop
val=digitalRead(0); //Checks the input from the receiver on pin 0
if (val==0) { //If pin 0 has gone LOW (ie if it has received input from the rx)
  if (sigstate==0){ // ..... and if the signal is already lowered .....
    raiseit(); //... then go to the sub procedure to raise the signal
    sigstate=1; //... and set the signal state flag to raised
    delay (500); //Wait half a second
  }
else { //Alternatively, if the signal state is already raised ....
  lowerit(); //... then call the lowering sub procedure....
  sigstate=0; //.... and set the sigstate flag to lowered.
  delay (500); //Pause for half a second
}
}
}//End of Main program loop

void raiseit(){ //Start of the raising sub procedure
  x=40; //Sets the value of x to 40 (the lowered position of the servo)
  do {
    delay (10);
    myservo.write(x); //Move the servo to the value of x
    x++; //Increase x by 1
  } while (x<140); //Carry on doing the above while x is less than 140 (the upper position of the servo)

for(z=140;z>120;z--) { //The first bounce .....
  myservo.write(z);
  delay(10);
}
for (z=120;z<140;z++){ //.... and back again
  myservo.write(z);
  delay(5);
}
for(z=140;z>130;z--) { //Second (lesser) bounce .....
  myservo.write(z);
  delay(10);
}
for (z=130;z<140;z++){ //.... and back again.
  myservo.write(z);
  delay(5);
}
  myservo.write(140); //Makes sure the signal is raised
}//End of the raising sub procedure

void lowerit(){ //Start of the lowering sub procedure
  x=100; //Sets x to 100
  y=1; // and y to 1
  z=140; // and z to 140 (the raised position of the servo)
do {
  myservo.write(z); //Sends the servo to z degrees
  delay(x); //Waits for x milliseconds
  x=x-y; //Reduces x by the value of y
  if (x<0){x=5;} //Makes sure x doesn't become negative
  y=y+0.5; //Exponentially increases the value of y (it gets larger increasingly faster)
  z--; //Decreases the value of z by 1 (ie the position of the servo by 1 degree)
} while (z>40); //Do the above until z reaches the lowered value for the servo

for(z=40;z<60;z++) { //First bounce ......
  myservo.write(z);
  delay(5);
}
for (z=60;z>40;z--){ //..... and back again
  myservo.write(z);
  delay(5);
}
for(z=40;z<50;z++) { //Second (lesser) bounce ......
  myservo.write(z);
  delay(5);
}
for (z=50;z>40;z--){ //.... and back again
  myservo.write(z);
  delay(5);
}
} //End of the lowering sub procedure

This code was tested and seemed to work quite well, though the bounces needed a bit more tweaking.

Before working on improving the base, I decided to add another feature - the ability to turn on and off the signal lamp by holding down the button on the transmitter for more than two seconds. I consulted various online sources, but wasn't entirely sure how to incorporate the code into my simple procedure, and so asked ChatGPT to lend a hand. I was pleasantly surprised by the result. The code was very well written, with copious explanatory comments and notes. I can thoroughly recommend using ChatGPT to assist with Arduino programming - but the instructions you give it need to be very precise. For example - https://chatgpt.com/share/219dc577-5a3e-4659-86fc-83adb1a66580

Armed with the additional information provided by ChatGPT, I modified the coding to include the LED switching capability.

#include <servo.h> //Loads the servo command library
Servo myservo; //Creates a name for the servo

//variables
bool sigstate=1;//1 = signal raised, 0 = signal lowered
int val; //Val used to record when trigger is received from receiver
int x=0; //Variable used in various for.... loops
int z; //Ditto
int uplim=150; //Upper limit for servo (in degrees 0-180) - Change if necessary
int lowlim=50; //Lower limit for servo - Change if necessary
float y; //Variable used to count milliseconds when input is triggered from receiver
bool ledState = true;     // Variable to store the current state of the LED - initally set to ON
unsigned long triggerStartTime = 0;  // Variable to store the time when trigger pin was first held low
unsigned long triggerEndTime = 0; //As above but stores when trigger signal is no longer low

//Constants
int trigpin=0; //Arduino pin used for trigger input from receiver
int sigpin=6; //Pin used for servo output
int ledpin=3; //Pin used for LED output
const unsigned long holdTime = 2000;  // Time in milliseconds that the pin must be held low

void setup() { //Initialising instructions
 myservo.attach(sigpin); //Tells Arduino the pin used for the servo
 myservo.write(uplim); //Sets the servo to its upper limit for the start of the session
 pinMode(trigpin,INPUT_PULLUP); //Defaults the trigger input to HIGH
 pinMode (ledpin,OUTPUT); //Sets the LED pin to output mode
 digitalWrite(ledpin, ledState);  // Sets the initial state of the LED
} //End of initialising instructions

void loop() { //Start of main program loop
   if (digitalRead(trigpin) == LOW) { //Checks the state of the trigger input to start measuring the time it is help LOW
    if (triggerStartTime == 0) { //
      triggerStartTime = millis();  // Record the time when pin 0 is first held low
    }
    
    do { 
      triggerEndTime=millis(); //Records the time when the trigger input is no longer LOW
   }while (digitalRead(trigpin)==LOW);
       if (triggerEndTime - triggerStartTime >= holdTime) { // Check if pin 0 has been held low for the required time
      ledState = !ledState;               // If so, toggle the LED state (LOW to HIGH or vice versa)
      digitalWrite(ledpin, ledState); //Turns on or off the LED
      triggerStartTime = 0;
      triggerEndTime =0;      // Resets the timers
    } 
  
  else { //If the trigger isn't held LOW for the required time, then .....
    triggerStartTime = 0; // Resets the timers
    triggerEndTime =0;  
 if (sigstate==1){ //Checks if signal is raised
    lowerit(); //If so, jumps to the signal lowering sub procedure
    sigstate=0; //Updates the flag showing the state of the signal
    delay (500); //Waits for half a second
  }
else { //So, if the signal isn't raised .....
  raiseit(); //Jumps to the signal raising sub procedure
  sigstate=1; //Updates the flag to show the signal is raised
  delay (500); //Waits for half a second
}
}
}
} //End of main program loop

void raiseit(){ //Start of signal raising sub routine
  x=lowlim; //Start off at the lower limit for the signal
  do {
    delay (10); //Wait 10 milliseconds
    myservo.write(x); //Set the position of the servo to x degrees
    x++; //Increase x by 1
  } while (x<uplim); //Keeps doing the above until the upper limit is reached

for(z=uplim;z>uplim-30;z--) { //First bounce
  myservo.write(z);
  delay(10);
}
for (z=uplim-30;z<uplim;z++){ //Return to upper limit position
  myservo.write(z);
  delay(5);
}
for(z=uplim;z>uplim-20;z--) { //Second bounce
  myservo.write(z);
  delay(10);
}
for (z=uplim-20;z<uplim;z++){ //Return to upper limit position
  myservo.write(z);
  delay(5);
}
  myservo.write(uplim);
} //End of raising sub routine

void lowerit(){ //Start of lowering sub routine
  x=100; //Initial value for delay in dropping the signal arm (slow to begin)
  y=1; //Start of how much the delay is decreased
  z=uplim; //Initial value for position of servo
do {
  myservo.write(z); //Sets the servo to z degrees
  delay(x); //Pauses x milliseconds
  x=x-y; //Decreases the pasue time by y seconds
  if (x<0){x=5;} //Makes sure the pause doesn't become negative
  y=y+0.5; //Exponentially increases the reduction in the pause between each servo step (so the drop gets faster)
  z--;//Decreases the position of the servo by one degree
} while (z>lowlim); //Does all the above until the lower limit is reached

for(z=lowlim;z<lowlim+20;z++) { //First bounce
  myservo.write(z);
  delay(5);
}
for (z=lowlim+20;z>lowlim;z--){ //Return to lower limit position
  myservo.write(z);
  delay(5);
}
for(z=lowlim;z<lowlim+10;z++) { //Second bounce
  myservo.write(z);
  delay(5);
}
for (z=lowlim+10;z>lowlim;z--){ //Return to lower limit position
  myservo.write(z);
  delay(5);
}
} //End of Signal Lower sub rountine

 With the hardware and software now working as intended, it was time to work on improving the signal baseplates.

 

Baseplates

 These were designed and tweaked with my go-to 3D CAD package - TinkerCAD. It may not be the most sophisticated 3D CAD package, but it is simple to use and, so far, I've never had a file which my printers can't handle.

Once all the pieces had been printed out.....


 ... the first task was to wire-up the SPDT sub-miniature slide switch as this would be inaccessible later.

The connections were shrouded in heatshrink tubing to avoid accidental short-circuits.

The switch was fixed into the holes provided with small self-tapping screws.


I decided the li-ion cells would be removable for charging. As indicated above, I was concerned about their quality and reliabilty and so wanted to be able to charge the cells externally (in fact, I will charge them outdoors as an extra precaution!).

Two pieces of 30 x 8 x 0.5mm brass strip were cut, ......

...... folded in half ........

..... and one side given an extra fold at the half-way point.

These brass contacts were then glued into the slots provided.........

...... with a few dabs of superglue to hold them in place.

A 1S BMS li-ion battery protection board was given heatshrink shrouding over its metal strip contacts ......

..... before the ends of the contacts were soldered ......

...... to the brass contacts.

The wire from the centre terminal of the slide switch was trimmed and soldered to the P+ contact on the BMS board.

A 60mm length of 1.5mm dia copper wire (stripped from some twin and earth mains cable) was bent into a U shape (the base being 10mm in length) and a piece of black hook-up wire soldered to the middle of the base.

This was then inserted into the holes beneath the battery compartment in the base, as shown.

The black wire from the copper U was then soldered to another three pieces of black hook-up wire (each approx 50mm in length) and the ends of the wires soldered to the P- terminal on the BMS board.

 The receiver was temporarily connected to a spare li-ion cell to power it up.

The button on the receiver was pressed eight times (to remove any previous settings) and then once the LED had flashed to confirm the settings were cleared, it was pressed once (for momentary mode) and the A key on the keyfob transmitter pressed to bind the receiver to the A button on the transmitter.

Thus, when the A button on the keyfob was pressed, the receiver's LED came on and went out when the button was released.

 The supplied wires on the receiver..........

..... were then removed (with a soldering iron), including the aerial lead. The connecting wires are too fat for the confined space within the base and the aerial lead, as supplied, is too long.

New (thinner) wires were then soldered to the pads on the receiver, and the aerial wire was reduced in length by around 25mm and resoldered to its pad.

The receiver was then shrouded in clear heat-shrink tubing ......

... and positioned below the battery cradle. It was a tight fit, but the space was designed for it.

The output lead from the slide switch was then soldered to the power lead for the receiver and another two red wires attached (one for the Seeeduino board and the other for the servo).


 The soldered connections were then shrouded in heat shrink tubing.

Pins 0, 3 and 6, and the VIN and GND terminals were tinned on the underside of the Seeeduino board,

One of the black leads was soldered to the GND terminal and a read lead soldered to the VIN terminal.

The servo mount was prepared ........


.... and a servo attached to it using 1.4mm diameter self tapping screws, ........

..... which I had obtained (very cheaply) as part of a set from eBay.

 A 40mm length of 1.5mm diameter brass wire (or rod) was inserted into a hole in the actuating arm of the servo and bent to prevent it from working loose.

The servo mount was then slotted into its housing, and.......

....... the white (signal) lead from the servo soldered to Pin 6 on the Seeeduino board. At the same time, a yellow lead was soldered to Pin 3 (for the LED) and the brown lead from the negative output of the receiver was soldered to Pin 0.

The red and black leads from the servo were soldered to the last remaining red and back leads from the switch and BMS board.

The slack in the leads was then carefully taken up by routing them behind the USB port on the Seeeduino board and coiling them in the void behind the servo.

A 16340 li-ion cell was then inserted into the battery compartment ......

..... and the switch turned on to check whether everything was functioning as expected - i.e., when the A button on the transmitter was pressed, the servo responded.

So far, so good! I was now ready to attach the signal.

The correct sized socket was selected (because the base of each signal varies slightly dependent on how well, or badly, it was planed). In the event, a piece of 1mm thick plasticard had to be inserted to take up a small amount of slack.

The end of the brass rod from the servo linkage was bent and inserted into the hole in the end of the balance arm. Finding the correct length was a case of trial and improvement. None of my signals is of a uniform design, and so some tweaking was required. This also included tweaking the uplim and lowlim constants in the Arduino code to fine-tune the travel of the servo arm.

The end of the ladder was trimmed to line-up with the ends of the U shaped copper bracket (see above) and the ends filed to remove any paint, ready for tinning with solder.

The legs of the copper bracket were then soldered to the base of the ladder.

The yellow lead from the LED output pin of the Seeeduino was threaded through a hole drilled near the base of the signal post, as was the aerial lead.

The yellow lead was soldered to the end of the copper tape (self adhesive slug barrier tape) which was attached to the signal post during construction (see How I constructed 19 semaphore signals)

The end of the aerial was taped to the base of the signal post with 'invisible' Sellotape.

A 150R resistor was soldered to the positive leg of the LED in the signal lamp and the shrouded in black heatshrink tubing.

A short length of black hook-up wire was soldered to the negative lead of the LED and shrouded in black heatshrink tube. The end of the lead was then soldered to the loop at the top of the ladder.

And the end of the resistor on the positive lead was soldered to the upper end of the copper tape. Thus the LED was connected to the negative output from the battery (via the ladder) and the positive output from Pin 3 on the Seeeduino board.

The unit was turned on and the LED function tested by holding transmitter button A down for three seconds to turn the LED on and off.

The final touch was to paint the top of the baseplate with black acrylics and touch-up various unpainted parts (eg the base of the ladder and the soldered joints on the copper strip) with black or white acrylics and the signal was ready to go.


NOTE: I hadn't realised, when ordering, that the servos come as left or right handed and so, rather than redesigning the baseplate to accommodate the slight differences, I simply invert the right-handed servos. This required a rewrite of the Arduino code. The video above was taken before I rewrote this code.

#include <Servo.h> //Loads the commands to control servos
Servo myservo; //Names the servo we shall be using


//variables
bool sigstate=1; //Logs the position of the signal (1=raised, 0 = lowered)
int x=0; //Variable used in various For.... loops
int z; //Ditto
float y; //Ditto
int uplim=60; //Sets the upper limit for the servo (0 to 180 degrees) - Change as required
int lowlim=150; //Sets the lower limit for the servo (0 to 180 degrees) - Change as required
bool ledState = true;     // Variable to store the current state of the LED (Set initially to ON)
unsigned long triggerStartTime = 0;  // Stores the time when the trigger pin was first held LOW
unsigned long triggerEndTime = 0; // Stores when the trigger pin is no longer LOW

//Constants
const int ledpin=3; //Arduino pin to which the LED is connected
const int trigpin=0; //Pin to which the receiver's trigger signal is received
const int sigpin=6; //Pin to which the servo is attached
const unsigned long holdTime = 2000;  // Time in milliseconds that the pin must be held low for LED to be turned on or off (ie 2 seconds)


void setup() { //Start of the setup instructions
 myservo.attach(sigpin); //Connect a servo to the signal pin
 myservo.write(uplim); //Raise the signal
 pinMode(trigpin,INPUT_PULLUP); //Set the trigger input pin to normally HIGH
 pinMode (ledpin,OUTPUT); //Set the LED pin as an output
 digitalWrite(ledpin, ledState);  // Turn on the LED
} //End of set up instructions

void loop() { //Main program loop
   if (digitalRead(trigpin) == LOW) { //Check if the trigger input is LOW
    if (triggerStartTime == 0) { //If the start time hasn't already been set .....
      triggerStartTime = millis();  // ... set it to record the when the pin first went LOW
    }
 
    do {
      triggerEndTime=millis();
   }while (digitalRead(trigpin)==LOW); //Keep checking the trigger pin is LOW
       if (triggerEndTime - triggerStartTime >= holdTime) {   // Check if the trigger pin has been held low for the required time
      ledState = !ledState;   // If so, toggle the LED state (from on to off or vice versa)
      digitalWrite(ledpin, ledState); //Turn on or off the LED
      triggerStartTime = 0; //Reset the start time ....
      triggerEndTime =0;      // ..... and the end time to zero
    } 
  
  else { //If the trigger pin hasn't been held low for the required time, then .....
    triggerStartTime = 0; //Reset the start ....
    triggerEndTime =0;  //.... and end times to zero
 if (sigstate==1){ //If the signal is raised ......
    lowerit(); // .... then carry out the lowering sub routine
    sigstate=0; //And set the state of the signal to lowered
    delay (500); // Wait half a second
  }
else {  //If the signal isn't raised ......
  raiseit(); //...jump to the raising signal sub routine
  sigstate=1; //Set the signal state flag to raised
  delay (500); //Wait half a second
}
}
}
}//End of main loop

void raiseit(){ //Start of signal raising sub rountine
  x=lowlim; //Set x to the lower limit of the servo
  do {
    delay (10); //wait ten milliseconds
    myservo.write(x); //Set the servo to x degrees
    x--; //Decrease x by one degree
  } while (x>uplim); //Keep doing the above until the upper limit is reached

for(z=uplim;z<uplim+40;z++) { //First bounce
  myservo.write(z);
  delay(5);
}
for (z=uplim+40;z>uplim;z--){ //Return to the upper limit
  myservo.write(z);
  delay(5);
}
for(z=uplim;z<uplim+40;z++) { //Second bounce
  myservo.write(z);
  delay(10);
}
for (z=uplim+40;z>uplim;z--){ //Return to the upper limit
  myservo.write(z);
  delay(10);
}
  myservo.write(uplim); //Double check the signal is at the upper limit (not really necessary)
} //End of the signal raising sub routine

void lowerit(){ //Start of the signal lowering sub routine
  x=100; //Initially set x to 100
  y=1; //Initially set y to 1
  z=uplim; //Initially set z to the upper limit
do {
  myservo.write(z); //Set the servo position to z
  delay(x); //Wait for x milliseconds
  x=x-y; //Decrease x by y milliseconds
  if (x<0){x=5;} //Makes sure x doesn't become negative
  y=y+0.5; //Exponentially increase the value of y (to gradually decrease the delay between each drop of a degree)
  z++; //Increase z by one degree
} while (z<lowlim); //Keep doing the above until the lower limit is reached

for(z=lowlim;z>lowlim-20;z--) { //First bounce
  myservo.write(z);
  delay(5);
}
for (z=lowlim-20;z<lowlim;z++){ //Return to lower limit
  myservo.write(z);
  delay(5);
}
for(z=lowlim;z>lowlim-10;z--) { //Second bounce
  myservo.write(z);
  delay(5);
}
for (z=lowlim-10;z<lowlim;z++){ //Return to lower limit
  myservo.write(z);
  delay(5);
}
myservo.write(lowlim); //Double checks the servo has reached the lower limit
} //End of lower signal sub routine

It will take me a while to motorise all my signals. So far, I have only three completed - each one is taking me around a day - but that is because I am also in the process of tweaking and refining the coding and design of the baseplate. Hopefully, once this has been done I will be able to speed-up the process.

 

The .STL file for the baseplate can be downloaded FOC from - https://gardenrails.org/forum/viewtopic.php?t=15008

No comments:

Post a Comment