Make a donation

Donators get access to the extended part of the site + bonus: Arduino Train DNT application.
For experiments and regular updating of the site, I need funds. I also have another project that you will like. If you are interested it, support me via donations.

Introduction for beginners

Many modelers are conservative, and very wary of novelties. I hasten to reassure you, Arduino is not a new invention, it is a well-established platform with a 15-year history. At the heart of these electronic devices is a microcontroller. And the microcontroller is not a novelty either, but just in case, I'll tell a little about it.

Formally a microcontroller (or MCU for microcontroller unit) is a small computer on a single integrated circuit. Even more obscure description of these thing you can read in Wikipedia. In fact, microcontrollers are a very cheap and convenient electronic component and they surround you from all sides. They are inside the TV remote control, your cars, household appliances and even DCC railroad control systems and decoders work exclusively on them.
However, up to now microcontrollers have been associated with terrible words – programming and firmware. I hasten to reassure you, Arduino is no more difficult than connecting the wires between DC motor and the battery. The most important thing is to understand how it works and why it is needed.

I did not find a better example than a washing machine. Washing machine has many modes: quick washing, delicate washing, rinsing and others. So, when you press the buttons on their panel it’s no "magic" – it's work MCU inside your washing mashine. MCU starting the control commands runs for rotating the drum, heaters, pumps and the water valve. By sending these commands, the MCU takes into account the time, signals from various sensors of the washing machine (water level, temperature, weight, etc.). And if you a little abstraction from the process of washing, then it becomes obvious that this device performs a certain program.

It remains only to change this program to for managing trains and a layout and to find a way to explain it to the microcontroller (that is, make the firmware).
This is what Arduino can do! My friend says a great phrase:
Don’t be afraid of programming, for Arduino programming is like teaching the dog to execute commands.

Variations of Arduino PCB

What is Arduino

Arduino is an open-source electronics platform based on easy-to-use hardware and software. It's intended for anyone making interactive projects. Over the years Arduino has been the brain of thousands of projects, from everyday objects to complex scientific instruments. A worldwide community of makers – students, hobbyists, artists, programmers, and professionals – has gathered around this open-source platform, their contributions have added up to an incredible amount of accessible knowledge that can be of great help to novices and experts alike.

Platform Arduino is constantly evolving, and now the site presents modern hardware based on chips from Intel and ARM. For the URB project, these opportunities are redundant, so I use equipment from chapter ENTRY LEVEL, and only Arduino NANO.

The most important advantage of this platform is a reliable reliable software and a lot of excellent training materials. I recommend see about Arduino on YouTube, in particular the channel Jeremy Blum.

Starter details set

Minimal details set

First example especially for the model railroad beginners, shows how to just start playing with your railroad using Arduino and a smartphone. It is enough to have a any classic starter railway model kit and even a schoolboy will build this scheme in a few minutes.

All these details will also be used later for the URB project. In total you need the Arduino NANO board, the Bluetooth-module HC-06 and the motor-driver L298n connected to the rails. All arduino's details for this experiment cost less than $ 10 on AliExpress and similar online stores. If you already have any Arduino's board and shields, you can also use them.

Power supply

Power supply

The project principled divided the power into two branches, one for moving locomotives, the second for electronics and devices on the layout. Thus, with short circuits on rails and other troubles with trains, the layout control will not be affected.

When using Arduino NANO obvious choice voltage for electronics is 5 volts. L298 allows you to use for your locomotives the required supply voltage (12 or 18V), and in the sketch you can change the code PWM to adjust the characteristics of the dynamics. The ideal solution can be a computer power supply, it immediately gives out voltges both 5 and 12V with protection. Also included in the set of railways are usually 9-12V power supplies. Also you can apply two standard power supplies, to 5V and 12V, by combining their negative wires together.
But for the experiment described below, it is sufficient to have any power source with voltage from 9 V to 18 V and current greater than 0.5 A.

Arduino Motor-drivers

The motor-driver and throttle adjustment your favorite locomotive

The DC motors of the majority of locomotives, including those with DCC decoders, are controlled by the PWM method. In short, unlike a regulated transformer that changes its output voltage, in the PWM method the voltage always corresponds to the input voltage, but the number of pulses and their duration per unit of time varies. This method allows to reduce the size of the electronic DC motor controller to a small chip, and such a chip is installed, including in the DCC decoder.

For the world of Arduino this method of management is native, you do not need to study the principles and subtleties of PWM work. There is a simple code that implements this method in one line: analogWrite(pin, value).
It is important to note that not all Arduino pins of devices can work in PWM mode, usually on Arduino boards these outputs are marked with the ~ sign. For the Arduino NANO board these PWM pins are: D3, D5, D6, D9, D10, D11 (for other Arduino boards see their documentation).

However, there is one problem, the Arduino 5V output voltage and very small current, so you can direct control only one LED. But this problem has been solved for a long time – manufacturers produce a very large set of variants of specialized "amplifiers". And they are called Motor-drivers. It is enough to put them between the Arduino board and the DC motor (in our case, since we have the DC control, connect directly to the rails).

I use in my project two types of these devices: L298N and L9110. Both motor-drivers have two-channel control with the polarity change, that is, you can connect to them two independent motors (or in our case two independent tracks). The L9110 manages voltage up to 12 V and current up to 0,8A per channel, but I like the more powerful L298N, so I use the L9110 quite rarely. L298n provides with a large margin all the needs for the management of any a locomotive, its output power is more than 100W. I have not yet seen a single locomotive with such a powerful engine! And the voltage controlled by it can be installed in any range from 5 to 50V, with a current of up to 4A.

Custom PWM-thrust locos

Throttle adjustment your favorite locomotive

The URB project has many unique and very convenient features, one of them is the independent adjustment of the traction characteristics of your locomotives.

In the headings main sketch for Communicate URB unit I entered an array. The numbers listed in it correspond to the output level of PWM. By changing these numbers you can change the characteristics of the locomotive, making it, for example, very quick, or on the contrary to achieve a smooth acceleration or braking at low speeds.
By default, it is configured almost linearly: byte speedArrayA [] = {50, 70, 90, 110, 120, 130, 150, 170, 200, 230, 255};
First, increasing the lower limit, get a starting to your locomotive from the spot, and then distribute the remaining range to get the dynamics you want. But this is not all the possibilities, since we can perform any mathematical and logical operations on the array, it is not difficult to programmatically introduce inertial motion and many other variants!

Simple start

You get convenient wireless control from your phone or tablet and, much more interestingly, many additional capabilities.

An example in this figure is a typical version of robot control based on Arduino. Similar schemes you will easily find on the Internet. The only difference is that the output of the motor driver is connected to the rails, and not directly to the motor. Of course, you can use your combinations of Bluetooth modules and motor-drivers, as well as ready shields. I recommend the L298N Dual H-Bridge Motor Controller and HC-06 Bluetooth modules.

After the assembly of the scheme, you just need to write a simple program called sketch. Here we can mention one important difference between microcontrollers and a computer: the microcontroller on the board Arduino always performs only ONE program, repeating it again and again.
Sketch for this experiment is based on the example SerialEvent from Arduino IDE. In general, all the sketches of this project are written at such a level so that they can be easily converted for to your needs. Their complexity at level a examples Arduino IDE.

Install Arduino IDE (instruction) on your computer and copy to the program following code...

  • // L298
    #define IN1_PIN 4
    #define IN2_PIN 5
    #define ENA_PIN 6
    
    // VARIABLES //
    bool flag_LED = false;
    bool stringComplete = false;
    String inputString = ""; 
    
    void setup() {
    
    // Initialize Serial
      Serial.begin(9600);
      inputString.reserve(4); 
    
    // Initialize Motor Driver
      pinMode(ENA_PIN, OUTPUT); 
      pinMode(IN1_PIN, OUTPUT); 
      pinMode(IN2_PIN, OUTPUT);
      pinMode(LED_BUILTIN, OUTPUT);
    
    }
    
    void loop() {
    
      if (stringComplete) {
    
        if (inputString.charAt(0) =='a') {
    
          //THROTTLE 
          if (inputString.charAt(1) =='0') {
            if (inputString.charAt(2) =='0') {
              analogWrite(ENA_PIN, 0);
            }
            if (inputString.charAt(2) =='2') {
              analogWrite(ENA_PIN, 80);
            }
            if (inputString.charAt(2) =='4') {
              analogWrite(ENA_PIN, 100);  // SET PWM LEVEL FOR STARTING YOUR LOCO
            }
            if (inputString.charAt(2) =='6') {
              analogWrite(ENA_PIN, 150);
            }
            if (inputString.charAt(2) =='8') {
              analogWrite(ENA_PIN, 200);
            }
          }
          if (inputString.charAt(1) =='1') {
            if (inputString.charAt(2) =='0') {
              analogWrite(ENA_PIN, 255);
            }
          }
    
          // DIRECTION
          if (inputString.charAt(1) =='d') {
            if (inputString.charAt(2) =='f') { // (f) Forward
              digitalWrite(IN1_PIN, HIGH);
              digitalWrite(IN2_PIN, LOW);
            }
            if (inputString.charAt(2) =='b') { // (b) Backward
              digitalWrite(IN1_PIN, LOW);
              digitalWrite(IN2_PIN, HIGH);
            }
            if (inputString.charAt(2) =='s') { // (s) Stop button
              digitalWrite(IN1_PIN, LOW);
              digitalWrite(IN2_PIN, LOW);
              analogWrite(ENA_PIN, 0);
              flag_LED = !flag_LED;
            }
          }
        }
    
        inputString = "";
        stringComplete = false;
      }
    
      if (flag_LED) digitalWrite(LED_BUILTIN, HIGH);
      else digitalWrite(LED_BUILTIN, LOW);
    }
    
    void serialEvent() {
      while (Serial.available()) {
        char inChar = (char)Serial.read();
        inputString += inChar;
        if (inChar == 'z') {
          stringComplete = true;
        }
      }
    }
    

    Pay attention to the parsing of commands from the application!
    According to the Protocol 2, the four-character direction command has the form:  adfz— "a" → player A (Android App), "d" → Direction, "f" → Forward, "z" → flag complete command.
    Code block if (inputString.charAt(X) =='X') parse it.

When you Upload the sketch into Arduino via USB, the data from the computer goes to the same pins RX and TX as are used by Bluetooth module. Therefore, if you upload the sketch, unplug Bluetooth module wires RXD and TXD.

 

I'll tell you a little secret: Arduino UNO and Arduino NANO use the same microcontroller Atmega 328, which means that there is no difference between them, even at the output GPIO pins. That is, they are exactly the same, but Arduino NANO board is smaller size.

You can use any application from the Line Arduino Train for this experiment. So, paired your Android device with HC-06 bluetooth module:

  • Enter the network settings of the system. Select the connection Bluetooth.
  • Turn on the power supply of the URB with the installed HC-06 modules.
  • Tap discovering devices of the bluetooth. In the list of detected devices, select HC-06 and pair them with your android device (default password – 1234). Close the system settings.
  • Run application and push the blue Bluetooth button, and select your HC-06 module. Connect will be established. With this button you can break the connection and establish it again.

Run your train!

Running and troubleshooting

In this example, it's hard to make a mistake. But shit happens :) If you have collected everything, have included, but it does not work we will look for the reason. At the same time we will understand how to act in such situations.

First, if the indicate LEDs on all devices do not glow, then most likely you have wrong the polarity or problem with your power adapter. If the power is OK, the application Arduino Train is running on your smartphone, but you can not connect to the Bluetooth channel, the most common mistake is the wrong device selection. Make sure that your choise HC-06 or your Arduino device.
The connection is established – "You connected: XX:XX:XX:XX" (in the case of HC-06, the indicator glows, not blinking), but the locomotive does not move over the commands of the smartphone. First we will check our design. If you touch Stop button on application, but the Arduino Nano LED does not light up, then problem with transferring data through the serial port from Bluetooth to Arduino. Try to put the resistors or level converter or... Incorrect wiring again.

If all the previous steps are completed, but the locomotive still does not move, then with digital multimeter checking the condition motor-driver and the locomotive.

Two railway track

Update your first sketch

Any sketch presented on this site is not a dogma. Let's add to the previous sketch the second software serial port, it's add the independence of the Bluetooth channel from the USB. Now you can upload the sketch from your computer via USB to Arduino without disabling Bluetooth, which makes it convenient for experiments with sketches reprogramming.

On the first experiment, you probably already saw that the connection of the circuit by wires is a very inconvenient task. As the circuit becomes more complicated and the components added, problems are added with the correctness and reliability of connecting the wires and blocks. Therefore, I suggest you make a URB unit that solves not only these problems but also adds new tremendous functions. You can collect a similar scheme on the breadboard, but URB is much more convenient.

A small comment about the electrical diagrams for the project: you can always look at the purpose of the wires in the header of the sketch, so it is not necessary to give the schematic diagram. Furthermore pins Arduino (GPIO) are universal, and you can freely manipulate the connection of peripheral devices with wires, then changing the descriptions of the purpose of a specific pin at the sketch.

In the circuit we will add supply 5 volts from the USB connector, thus dividing the power for control and for locomotive. As result we will get rid of interference to the control electronics when trains run (also this allows to increase the voltage on the rails up to 16-18 volts if you need it).
Also in this configuration there is a very interesting opportunity for experiments with your own command variants (see the Protocol 2 of basic commands). You will run second train from PC use your custom commands. And, as a bonus, you can see commands sending from Android to Arduino in Arduino IDE Terminal.


  • // LIBRARY //
    #include <SoftwareSerial.h>
    
    // SOFTWARE SERIAL
    SoftwareSerial Bluetooth(12, 13); // RX, TX
    
    // L298
    #define IN1_PIN 2
    #define ENA_PIN 3
    #define IN2_PIN 4
    #define IN3_PIN 5
    #define ENB_PIN 6
    #define IN4_PIN 7
    
    // VARIABLES
    boolean stringComplete = false;
    String inputString = "";
    int speedLoco1, speedLoco2;
    
    void setup() {
    
    // Initialize Serial
      Serial.begin(9600);
      Bluetooth.begin(9600);
      inputString.reserve(4);
    
    // Initialize Motor Driver
      pinMode(ENA_PIN, OUTPUT);
      pinMode(IN1_PIN, OUTPUT);
      pinMode(IN2_PIN, OUTPUT);
      pinMode(IN3_PIN, OUTPUT);
      pinMode(IN4_PIN, OUTPUT);
      pinMode(ENB_PIN, OUTPUT);
    
    }
    
    void loop() {
    
      if (stringComplete) {
        
        // LINE A (BLUETOOTH)
        if (inputString.charAt(0) =='a') {
    
          //THROTTLE 
          if (inputString.charAt(1) =='0') {
            if (inputString.charAt(2) =='0') speedLoco1 = 0;
            if (inputString.charAt(2) =='2') speedLoco1 = 80;          
            if (inputString.charAt(2) =='4') speedLoco1 = 100; // STARTING PWM LEVEL            
            if (inputString.charAt(2) =='6') speedLoco1 = 150;              
            if (inputString.charAt(2) =='8') speedLoco1 = 200; 
          }               
          if (inputString.charAt(1) =='1') {
            if (inputString.charAt(2) =='0') speedLoco1 = 255;              
          }
    
          // DIRECTION
          if (inputString.charAt(1) =='d') {
            if (inputString.charAt(2) =='f') { // (f) Forward
              digitalWrite(IN3_PIN, HIGH);
              digitalWrite(IN4_PIN, LOW);
            }
            if (inputString.charAt(2) =='b') { // (b) Backward
              digitalWrite(IN3_PIN, LOW);
              digitalWrite(IN4_PIN, HIGH);
            }
            if (inputString.charAt(2) =='s') { // (s) Stop button
              digitalWrite(IN3_PIN, LOW);
              digitalWrite(IN4_PIN, LOW);
              speedLoco1 = 0;
            }
          }
        }
    
        // LINE B (COMPUTER)
        if (inputString.charAt(0) =='b') {
    
          //THROTTLE 
          if (inputString.charAt(1) =='0') {
            if (inputString.charAt(2) =='0') speedLoco2 = 0;
            if (inputString.charAt(2) =='2') speedLoco2 = 80;          
            if (inputString.charAt(2) =='4') speedLoco2 = 100; // STARTING PWM LEVEL             
            if (inputString.charAt(2) =='6') speedLoco2 = 150;              
            if (inputString.charAt(2) =='8') speedLoco2 = 200; 
          }               
          if (inputString.charAt(1) =='1') {
            if (inputString.charAt(2) =='0') speedLoco2 = 255;              
          }
    
          // DIRECTION
          if (inputString.charAt(1) =='d') {
            if (inputString.charAt(2) =='f') { // (f) Forward
              digitalWrite(IN1_PIN, HIGH);
              digitalWrite(IN2_PIN, LOW);
            }
            if (inputString.charAt(2) =='b') { // (b) Backward
              digitalWrite(IN1_PIN, LOW);
              digitalWrite(IN2_PIN, HIGH);
            }
            if (inputString.charAt(2) =='s') { // (s) Stop button
              digitalWrite(IN1_PIN, LOW);
              digitalWrite(IN2_PIN, LOW);
              speedLoco2 = 0;
            }
          }
        }  
    
        analogWrite(ENB_PIN, speedLoco1); 
        analogWrite(ENA_PIN, speedLoco2); 
    
        inputString = "";
        stringComplete = false;
      }
    
       Serial.print("LINE A - ");
       Serial.print(speedLoco1);
       Serial.print(" | LINE B - ");
       Serial.println(speedLoco2);   
    
      bluetoothEvent();
    }
    
    void serialEvent() {
      if (Serial.available()) {
        char inChar = (char)Serial.read();
        inputString += inChar;
        if (inChar == 'z') {
          stringComplete = true;
        }
      }
    }
    
    void bluetoothEvent() {
      if (Bluetooth.available()) {
        char inChar = (char)Bluetooth.read();
        inputString += inChar;
        if (inChar == 'z') {
          stringComplete = true;
        }
      }
    }
    

    Note the change in the locomotive throttle parsing code. PWM processing for the motor-driver is carried out in separate blocks. It is in its you can apply custom adjustments for movement of the train.

Since in this version the USB connection is constant, in the code I am is added data for output to the terminal: Serial.print([data for input]). Try it, it helps a lot in debugging. For example, speed value coming from the application Arduino Train are duplicated in the Arduino IDE terminal. Providing this code is placed at the very beginning of the loop block.

By consecutively typing the "bdfz", "b02z" … "b10z" commands in the terminal, you start the locomotive on the second raill path, and will by typing "bdsz"– stop it. The same simple protocol works with Android application. It's very simple, and this example is the starting point for understanding the whole project.

History of a model trains control

I'm sure that you know this story, but still I will repeat the main stages.

Trains before appearance of the DCC were driven by miniature electric motors to which electricity was supplied direct via rails. Almost all modern models of locomotives can work in this mode. The principle is very simple and understandable even for children – it is enough to press a 9 volt battery to the rails and the train moves. If you change the polarity (unfold) the batteries, the train will start moving in the opposite direction. If a controlled transformer or rheostat is used instead of a battery, it becomes possible to adjust the speed by changing the voltage on the rails. In this way, have been made most of the obsolete control panels that are still very popular.
This principle of management had a significant disadvantage: if you put several locomotives on the rails, they start moving simultaneously. This was overcome by isolating individual sections of the rail and blocking them using a relay or by installing separate consoles for each section. What caused the following problem, the complexity of the electrical wiring has greatly increased. And even now you can see well-made analog railroad models with a very large number of wires. On the other hand, despite the number of wires, the principle itself is very obvious and understandable.

With the beginning of the 1980s, the concept of DCC was introduced. The voltage on the rails will constanted, and the control signals uses of time modulation were transmitted along the same rails. Accordingly, the consoles changed, now they broadcast a control signal. The decoder installed on the locomotive converted these commands into voltage and polarity to the motor. The decoder on the locomotive also added new features – motion sounds, headlight lighting effects and cockpit lighting. Now this is the main principle in railway modeling.
DCC consoles also elegantly solve the problem of switching junctions and light signalling, there is no need to separately pull the wires to motor points – there too there are decoders. But this in theory, in practice DCC has generated many new problems: when building several lines and using several locomotives, it is necessary to install boosters that distribute the load. Because of the difference in the supply voltage of the drives and locomotives, the wires again need to be pulled through the entire layout. Each manufacturer has a different command system and compatibility is still a very narrow place. Features programming of consoles and locomotives – the main part of the discussions in profile forums. And most importantly, DCC is closed, very incomprehensible and very the cumbersome solution compared to the classical one. For example, troubleshooting in decoders, boosters or in the console itself is very difficult.
Since the beginning of the mass distribution of computers and modern electronics, the modelers have created several universal DCC standards, but in my opinion this all now looks like a giant zoo of disparate and very over-complicated devices.
Modern digital consoles are very good and similar to a specialized computer with a lot of blocks, and it seems to me that they contradict the principle of Occam's razor. Also the price of such a set is simply fantastic.

If you look at these solutions from the side – it's obvious that the DCC is just a data bus and microcontrollers installed in the console and decoders. And if you try to use Arduino, and this is a microcontroller Atmega, you can use both the classic control option and the DCC.
Combinations of DCC and Arduino designed out a lot, and they work well. But I'm of the opinion that this way is too complicated. Therefore, I returned to DC concept, only instead of the transformer I apply the ready-made block L298 with Pulse-width modulation PWM) to control locos. Also for Arduino there are a lot of reliable cheap modules. The Arduino programming language is very simple and flexible. Reprogramming Arduino gives you almost infinite possibilities for updating the layout. It's like updating the firmware of modern electronic gadgets. As the site is being updated, I will publish new ideas on automation and drawing of railway devices. Now on the site you will find all the necessary details for creating switching motors, light signals, consoles and many others. Using this information, any fashion designer can, step by step, realize his own projects.

Introduction to I2C bus

The main bus for duplex data transmission at this project. Bus is integrated into each the URB unit.

I2C Bus 3 URB units on the I2C bus

 

Pay attention to URB #3 with a Hall sensor. On it there is no ULN2003 chip and the jumper DP5 is closed. Also on it are laid conductors that parallelly connect the male connector DP1 and the group of screw contacts SP4 (See solder bridge features). This mode of URB for direct connection of Hall modules or other sensors.

  • #include <SoftwareSerial.h>
    #include <Wire.h>
    
    // GPIO PINS
    
    SoftwareSerial Bluetooth(12, 13); // RX, TX
    
    
    // VARIABLES
    boolean stringComplete = false;
    String inputString = "";
    int addressI2C;
    byte dataToI2C, dataFromI2C; 
    
    void setup() {
    
    // Initialize Serial
      Serial.begin(9600);
      Bluetooth.begin(9600); 
      inputString.reserve(16); 
    
    // Initialize I2C
      Wire.begin();
    
    }
    
    void loop() {
    
      if (stringComplete) {
        
        // SERVO
        if (inputString.charAt(0) =='j') {
          addressI2C = 2; 
    
          if (inputString.charAt(1) =='a') { 
    
            if (inputString.charAt(2) =='1') {
              dataToI2C = 30; 
              sendDataViaI2C();
            }
            if (inputString.charAt(2) =='0') {
              dataToI2C = 31; 
              sendDataViaI2C();
            }
          }  
    
          if (inputString.charAt(1) =='b') { 
    
            if (inputString.charAt(2) =='1') {
              dataToI2C = 32; 
              sendDataViaI2C();
            }
            if (inputString.charAt(2) =='0') {
              dataToI2C = 33; 
              sendDataViaI2C();
            }
          }
        }    
    
        // LED ON URB #3
        if (inputString.charAt(0) =='a') {
          addressI2C = 3; 
    
          if (inputString.charAt(1) =='d') { 
    
            if (inputString.charAt(1) =='b') {
              dataToI2C = 30; 
              sendDataViaI2C();
            }
    
            if (inputString.charAt(1) =='f') {
              dataToI2C = 31; 
              sendDataViaI2C();        
            }
          }  
        }
    
        inputString = "";
        stringComplete = false;    
      }
    
      bluetoothEvent();
     
    // --------------- I2C REQUEST --------------- // 
    
      static unsigned long prevTime = 0;
    
      if (millis() - prevTime > 25) {
        addressI2C = 3;
        Wire.requestFrom(addressI2C, 1);    // request byte from slave device 
        prevTime = millis();
        dataFromI2C = Wire.read(); 
      }
    
      if (dataFromI2C == 1) {
        dataToI2C = 34; 
        addressI2C = 2; 
        sendDataViaI2C();
        dataFromI2C = 0;
      }  
    
    }
    
    // ----------- FUNCTIONS ----------- // 
    
    void serialEvent() {
      while (Serial.available()) {
        char inChar = (char)Serial.read();
        inputString += inChar;
        if (inChar == 'z') {
          stringComplete = true;
        }
      }
    }
    
    void bluetoothEvent() {
      while (Bluetooth.available()) {
        char inChar = (char)Bluetooth.read();
        inputString += inChar;
        if (inChar == 'z') {
          stringComplete = true;
        }
      }
    }
    
    void sendDataViaI2C() {
      Wire.beginTransmission(addressI2C);
      Wire.write(dataToI2C);
      Wire.endTransmission();
    }
    

  • #include <Servo.h>
    #include <Wire.h>
    
    // GPIO PINS
    #define JUNCTION_EN 8 
    #define JUNCTION1_PIN 9
    Servo J1;
    #define JUNCTION3_PIN 11
    Servo J3;
    
    // VARIABLES
    byte dataFromI2C; 
    unsigned long millisJunction;
    
    
    void setup() {
    
    // Initialize Serial
      Serial.begin(9600);
    
    // Initialize I2C
      Wire.begin(2); 
      Wire.onReceive(receiveI2C);
    
    // Initialize Servos
      pinMode(JUNCTION1_PIN, OUTPUT); J1.attach(JUNCTION1_PIN);
      pinMode(JUNCTION3_PIN, OUTPUT); J3.attach(JUNCTION3_PIN);  
      pinMode(JUNCTION_EN, OUTPUT);  
    
    // Startup
    
      J1.write(0);
      J3.write(0);
      delay(25);
      digitalWrite(JUNCTION_EN, HIGH);  
      delay(600);
      digitalWrite(JUNCTION_EN, LOW);  
    }
    
    void loop() {
      
     // COMMAND PARSING
      if (dataFromI2C != 0) {
        switch (dataFromI2C) {
    
        case 30: J1.write(0);
                 delay(25);
                 digitalWrite(JUNCTION_EN, HIGH);
                 millisJunction = millis(); break;
    
        case 31: J1.write(180);
                 delay(25);
                 digitalWrite(JUNCTION_EN, HIGH);
                 millisJunction = millis(); break;
        
        case 32: J3.write(0);
                 delay(25);
                 digitalWrite(JUNCTION_EN, HIGH);
                 millisJunction = millis(); break;
        
        case 33: J3.write(180);
                 delay(25);    
                 digitalWrite(JUNCTION_EN, HIGH);
                 millisJunction = millis(); break;    
    
        case 34: J1.write(90);
                 J3.write(90);
                 delay(25);             
                 digitalWrite(JUNCTION_EN, HIGH);
                 millisJunction = millis(); break;
        }         
      dataFromI2C = 0;
    
      } 
    
    if (millis() > (millisJunction + 800)) digitalWrite(JUNCTION_EN, LOW);   
    
    }  
    
    // ----------- FUNCTIONS ----------- // 
    
    void receiveI2C(int howMany) {
      while (Wire.available() > 0) {
        dataFromI2C = Wire.read();
        if (dataFromI2C != 0) {
          Serial.println("I2C - ");  
          Serial.print(dataFromI2C);
        }
      }
    }
    
    

  • #include <Wire.h>
    
    // GPIO PINS
    #define J3 11
    
    // VARIABLES
    byte dataToI2C, dataFromI2C; 
    
    
    void setup() {
    
    // Initialize Serial
      Serial.begin(9600);
    
    // Initialize I2C
      Wire.begin(3); 
      Wire.onReceive(receiveI2C);
      Wire.onRequest(requestI2C);
    
    // Initialize Servos
      pinMode(J3, INPUT);
      pinMode(LED_BUILTIN, OUTPUT);
    
    }
    
    void loop() {
    
      // COMMAND PARSING
      if (dataFromI2C != 0) {
    
        switch (dataFromI2C) {
    
        case 30: digitalWrite(LED_BUILTIN, HIGH); break;
    
        case 31: digitalWrite(LED_BUILTIN, LOW); break;
    
        }         
    
      dataFromI2C = 0;
    
      } 
    
      if (digitalRead(J3) == LOW) dataToI2C = 1;    
      else dataToI2C = 0;
    
    }
    
    // ----------- FUNCTIONS ----------- // 
    
    void receiveI2C(int howMany) {
      while (Wire.available() > 0) {
        dataFromI2C = Wire.read();
        if (dataFromI2C != 0) {
          Serial.println("I2C - ");  
          Serial.print(dataFromI2C);
        }
      }
    }
      
    void requestI2C() {
      Wire.write(dataToI2C);
    }
    

You can send commands via the I2C bus to any of the end peripherals connected to any URB on the layout. According to the specification, the maximum length I2C bus when using a twisted pair of about eight meters. In practice, with a total cable length of five meters, everything works correctly.
If you need a large length of wires, you can use a repeater consisting of two URBs, creating a connection I2C-Serial-I2C.

After several successful experiments, the modeler faces the problem of lack of free I/O pins (GPIO) on Arduino. And many people try to solve this problem directly, change board UNO or NANO to bigger Arduino (DUE, Mega). It seems to me wrong, the problem still remains. The best way – to unite the microcontrollers to the network, which allows you to scale the I/O pins almost endlessly. The I2C bus is a good fit for this. It has native support with the Wire library in the Arduino, and has addressing in contrast to the serial connection solution.

The most famous example it's databus for connection of a LCD screen to Arduino by two wires. But first of all this tire is designed to build a full-fledged network for multiple devices. There is even a widespread the chip PCF 8574 for I2C bus, and it will come in handy later.

The bus I2C works with addresses in master-slave mode. This means that only the main device can send data, other I2C blocks on the bus can only respond to requests from the main, or simply execute commands. This limitation gives some inconvenience in transferring data from sensors from local URB to the communication station, which in my project is always the master by default. A detailed video about this bus.
For Arduino there is no good library that implements the multi-master mode for this bus, and, as far as I know, this is due to the great difficulties of arbitrating the bus in this mode. In general, I'm in favor of using only the built-in libraries of Arduino.

Rotating the servo over Bluetooth control from Android App or from triggered Hall sensor

 

Therefore, I place here the sketch that combines both data transfer and a query with subsequent data retrieval. This is different from the classic examples the Wire library from Arduino IDE. To power this design, just connect the USB cable. This is one of the examples of "design on the table" on which it is convenient to practice sketches. The joint operation of the URB blocks allows you to rotate the servos by commands from a computer or my application, and also rotate the drives to a position of 90 degrees as the magnet approaches the Hall sensor. Also, the URB #2 using the ULN2003 turns on servos only at the moment of their rotation. Of course, it is possible to assemble a circuit with servos, a bluetooth module and a sensor on one URB board, but this simple example illustrates how to apply the bus to a layout.

This is the second basic example for understanding the principles of the project. The I2C bus, together with a 5-volt power supply wires in this project, is called a Universal Railway Bus (URB).

Typical connections

Microcontrollers, including the Arduino platform, have universal I/O (input/output) pins. This means that any pin can be used as for reception signals or control.

Unfortunately, pins are not really universal. Each type of microcontroller has its limitations, also Arduino libraries we will use, often require only certain pins of the microcontroller. For example, in Arduino NANO and UNO, only contacts D3, D5, D6, D9, D10 and D11 can be used as PWM outputs. Support library bus I2C Wire works only with pins A4(D18) and A5(D19), but the Servo library can work with any pins. There are also AtMega 328 hardware chip limitations on the use of D0, D1, D2, A6 and A7 pins (see the Arduino documentation). These features I took into account when designing of the URB unit.

On the other hand, I want to note the absolute superiority of microcontrollers compared to relay or discrete logic when connecting wires from peripheral devices of the layout. With Arduino there is no need to strictly follow the order and schematic of the connection, you can connect the wires to any free URB pin, and then in the sketch assign the functions of this pin. You don't need crawl under your layout with a screwdriver to change or check anything, everything can be checked or reassigned programmatically.

Ways to connect, given the connection of units by chain via the URBs is very much and you can always apply your connection option by the situation. In this section, only typical schemes for connecting URB are given. Below, in the examples of railway layouts, you will see how to use these connections together with sketches and explanations.

Options for connecting the URB unit to external computers and railway peripheries on your layout

Railway peripheries

This is my favorite part of the site. Here you will find ways and ready solutions for making mechanisms and details for reviving your layout.

If you think about what we need for a railway layout, then the answer to this question will be only three things: devices for something moving (or rotating), sensors and lightings. The specialization of each of these things will give us all solutions for the periphery. And the first chapter is devoted to moving gizmos.

In a restricted part of the site, I will constantly supplement the collection of ready-made solutions based electric drive and URB unit. There will be mechanisms of hanging barrier and mechanisms for linear or rotational movements. For Arduino platform there is a match more ready-made motors and gears. Even for a simple servo, with its inability to change the rotation speed in the usual mode (rotate speed it is maximum and constant for each type of servo), there is an interesting sketch that allows you to overcome this limitation, for example, it allows smoothly and relatively slowly raise and lower the barrier on railway crossing.

Below is collected all the fun that I already come up and tested.

Point Motor

By this mechanism, I am proud as designer! The task was to make such a reliable junction switch-mechanism, which will simply and quickly manufactured, while ensuring accuracy of moving the rod to a tenth of a millimeter.

Given the large error in the position of the lever of the cheap servo when stopping after rotation, and the need of convenient adjustment without tools at the installation site, the task seemed to me unsolvable. However, I came up with, and even much more. Shoulders slingshot, as well the mechanism, made of thin galvanized sheet metal, this material is available almost everywhere. So, by bending them with your finger you can adjust the stroke of the rod and, accordingly, the accuracy of the junction switch position. In addition, these shoulders act as a damper, parrying the errors of the mechanical transmission and securely fix the switch mechanism in the extreme positions. But that is not all. If the servo arm is stop to a position of 90 degrees, the mechanism is unlocked and you can switch the junction manually. Sketches and examples in this project are focused specifically on this mechanism.

Video instruction on the manufacture of two variants of the Point Motor. This mechanism can for linear displacement of layout elements. You can install this mechanism vertically, horizontally, at an angle, on the layout, under the layout – anywhere! Using long rods and rockers to change the movement vector, it is possible to mechanize very complex multi-junctions yards. And if you look at the Tortoise company's mechanism placed in the figure on the right, you will notice that it also uses a standard servo (blue translucent box).

Also you can also use URB to control your Point Motor with a standard magnetic coil drive or motors. Also using delays in the sketch, you not need additional electronics and limit switches.

Type modelrailway points

Sketches, instructions and installation tips about it you can always get in our Facebook community.

Animated Point-motor on Arduino Servo
Draft and placed point-motor
Example railway signaling

Signal System

Railway signals are rarely found on layout, and their availability is a sign of professionalism. Not this the modelers do not want to install them, but that not a trivial task – the algorithm for switching many signals needs to corrected for each railway line. So you need a large computer with software or branded digital sets from manufacturers. Or soldering their own boards on logic chips.
But with the Arduino everything becomes much better. Convenient direct connection of signals and sensors to the URB connectors provides easy installation. Together with the availability of information on the position of all junctions and the ease of programming Arduino all it is now easiest. Without any computers! Just turn layout power on and everything works!

In fact, programming automatic switching of light signals is a fun. I mean, get a peep of how this luminous color "magic" works itself after the your sketch has been filled and running. In this chapter I give a simple example, but even it shows ways to automate the movement of trains. Next, in the chapter PRO Sketches library everything will be even more cool! Again, I will give several principles:

  1. The best option is if the switching logic is programmed in the sketch of the URB to which the signals are connected.
  2. The communication station provides only general control of the status of the layout lines.
  3. Do not do manual control of traffic lights, let everyone do sketches on Arduinos.
On the diagram, I drew a two-wire connection of signals. If you have a three-wire signal with a ground contact, then the sketch does not need any changes, just connect the GND contact to the GND of the URB.

In this example, the railroad yard is controlled by a URB to which two Point Motors, a dual relay and two semaphores are connected. From the communication station to in the URB comes one command to switch two junctions, the rest of the action is performed by the URB. So you can split two oncoming trains in one line. By modifying this sketch you can control signal lights of the dead-end branch and other things. With the addition of sensors to appear, you can do an interlocking on your layout. Please note that this sketch is also a changed version of the sketch for URB #2 from the example of "Introduction to I2C bus", nothing complicated. Also in the sketch are added the lighting commands of the station.


  • #include <Servo.h>
    #include <Wire.h>
    
    //// GPIO PINS ////
    #define LIGHT_CH1 2 
    #define LIGHT_CH2 3 
    #define LIGHT_CH3 5 
    #define LIGHT_CH4 6 
    #define LIGHT_CH5 7 
    #define LIGHT_CH6 4 
    #define JUNCTION_EN 8
    #define JUNCTION1_PIN 9
    Servo J1;
    #define JUNCTION3_PIN 11
    Servo J3;
    #define RELAY_CH1 12 
    #define RELAY_CH2 13 
    #define SIGNAL_A1 14 
    #define SIGNAL_A2 15 
    #define SIGNAL_B1 16 
    #define SIGNAL_B2 17 
    
    //// VARIABLES ////
    byte dataFromI2C; 
    unsigned long millisJunction;
    
    
    void setup() {
    
    // Initialize Serial
      Serial.begin(9600);
    
    // Initialize I2C
      Wire.begin(2); 
      Wire.onReceive(receiveI2C);
    
    // Initialize Servos
      pinMode(JUNCTION1_PIN, OUTPUT); J1.attach(JUNCTION1_PIN);
      pinMode(JUNCTION3_PIN, OUTPUT); J3.attach(JUNCTION3_PIN);  
      pinMode(JUNCTION_EN, OUTPUT);  
    
    // Initialize Signals
      pinMode(SIGNAL_A1, OUTPUT); pinMode(SIGNAL_A2, OUTPUT); 
      pinMode(SIGNAL_B1, OUTPUT); pinMode(SIGNAL_B2, OUTPUT); 
    
    // Initialize Lighting
      pinMode(LIGHT_CH1, OUTPUT);
      pinMode(LIGHT_CH2, OUTPUT); //PWM
      pinMode(LIGHT_CH3, OUTPUT); //PWM
      pinMode(LIGHT_CH4, OUTPUT); //PWM
      pinMode(LIGHT_CH5, OUTPUT);
      pinMode(LIGHT_CH6, OUTPUT);  
    
    // Initialize Relay          
      pinMode(RELAY_CH1, OUTPUT);
      pinMode(RELAY_CH2, OUTPUT);
    
    // Startup
      J1.write(0);
      J3.write(0);
      delay(25);
      digitalWrite(JUNCTION_EN, HIGH);  
      delay(600);
      digitalWrite(JUNCTION_EN, LOW); 
      digitalWrite(RELAY_CH1, LOW); 
      digitalWrite(RELAY_CH2, HIGH);
      digitalWrite(SIGNAL_A1, HIGH); 
      digitalWrite(SIGNAL_B2, HIGH);
    
    }
    
    void loop() {
      
     // COMMAND PARSING
      if (dataFromI2C != 0) {
        switch (dataFromI2C) {
    
        case 30: digitalWrite(SIGNAL_A1, HIGH); 
                 digitalWrite(SIGNAL_A2, LOW);
                 digitalWrite(SIGNAL_B1, LOW);                 
                 digitalWrite(SIGNAL_B2, HIGH);
                 J1.write(0);
                 J3.write(0);
                 delay(25);
                 digitalWrite(JUNCTION_EN, HIGH);
                 millisJunction = millis(); 
                 digitalWrite(RELAY_CH1, LOW);                 
                 digitalWrite(RELAY_CH2, HIGH); break;
    
        case 31: digitalWrite(SIGNAL_A1, LOW); 
                 digitalWrite(SIGNAL_A2, HIGH);
                 digitalWrite(SIGNAL_B1, HIGH);                 
                 digitalWrite(SIGNAL_B2, LOW);
                 J1.write(180);
                 J3.write(180);
                 delay(25);
                 digitalWrite(JUNCTION_EN, HIGH);
                 millisJunction = millis(); 
                 digitalWrite(RELAY_CH1, HIGH);                 
                 digitalWrite(RELAY_CH2, LOW); break;
        
        case 20: digitalWrite(LIGHT_CH1, LOW); // Outdoor light OFF
                 digitalWrite(LIGHT_CH2, LOW);
                 delay(180);             
                 digitalWrite(LIGHT_CH3, LOW); break;
    
        case 21: digitalWrite(LIGHT_CH1, HIGH); // Outdoor light ON
                 delay(120);
                 digitalWrite(LIGHT_CH2, HIGH);
                 digitalWrite(LIGHT_CH3, HIGH);             
                 delay(90);             
                 digitalWrite(LIGHT_CH3, LOW);
                 delay(90);             
                 digitalWrite(LIGHT_CH3, HIGH); break;   
    
        case 22: digitalWrite(LIGHT_CH4, LOW); // Station light OFF
                 delay(60);     
                 digitalWrite(LIGHT_CH5, LOW);
                 delay(90);             
                 digitalWrite(LIGHT_CH6, LOW); break;
    
        case 23: digitalWrite(LIGHT_CH4, HIGH); // Station light ON
                 delay(120);
                 digitalWrite(LIGHT_CH5, HIGH);
                 delay(120);
                 digitalWrite(LIGHT_CH6, HIGH); break;            
        }         
    
      dataFromI2C = 0;
    
      } 
    
    if (millis() > (millisJunction + 600)) digitalWrite(JUNCTION_EN, LOW);   
    
    }  
    
    // ----------- FUNCTIONS ----------- // 
    
    void receiveI2C(int howMany) {
      while (Wire.available() > 0) {
        dataFromI2C = Wire.read();
        if (dataFromI2C != 0) {
          Serial.println("I2C - ");  
          Serial.print(dataFromI2C);
        }
      }
    }
    

I made DIY-signals which you can see in photos my layout, and even made a video about it. You can easily reproduce this strong and simple design. The draft signals design can be downloaded from Facebook.

Layout lighting and devices

Lighting adds a realism to the model railway layout. But simple light bulbs are boring, it is better if every window of all houses will individually turn on the light. Even better, if there are fade-in effects, or flashing, like a broken fluorescent tube. Also, street lights should use dimming and station buildings should also be included in lights groups. This means we need a lot of control channels and it's expensive and complica-a-a-a-ted.
And here not! With the URB it's easy, and even more so, you can add a random algorithm for lighting windows, you can also still put an ambient light sensor and your railway world will react to begin the night. You can control this and the real buttons on the control of the layout, and from the phone using my application. And you will periodically change lighting rules by simply uploading a new sketch!
My layout on assembled on a shelf, and there are 48 light channels, and this is far no limit.

Connecting LED Lighting

Each URB has 6 skrew outputs D2-D7 with a current of up to 500 mA (7 if you do not use servos). Outputs D3, D5 and D6 in the middle can be operated in PWM mode and you will use them for dimming. All pins have a voltage of 5 volts, so you can easily calculate the maximum number of LEDs possible to connect to one pin. For example, if you use cylindrically LED of white light, then its current with a serial current-limiting resistor of 150 Ohm is about 25 mA, then their total numbers will be 20 per channel. Numbers more powerful SMD LEDs, like 3528, assembling will be less per channel. Pay attention to the non-standard connection scheme with a common plus. Just in case instead of LEDs it is possible to include usual bulb lamps, only at them very much big a current.

ULN2003 DIP-16 is a cheap and widely available chip. In my experience, part of the copies of this chip (depending on the manufacturer) starts to get very hot at currents of more than 350 mA per channel. But even if it broke, you can easily replace it.

Starting with the version of URB 2.6 it is possible to connect a 12 V load. You can connect 12V LED-tapes direct to the pins.

Local URB control and effects

It is not difficult to control such a number of channels if you adhere to the main rule: all LEDs from a particular building (for example, a house) should be connected to the nearest URB. Do not use long wires! Everything will work, but you will get confused when you start to write sketches. If you have a multi-window structure, or a houses group, use PCF8574. Thanks to this rules, most of the management of the lighting channels is performed by a local URB. Only the general command comes from the communication station.
After such a long introduction, I'll explain why there are so many channels. In the real world, people turn the light on and off randomly. The simplest way to simulate this is to introduce delays, for example at station: first lights are switched on in the cafeteria and the cashier's area, after the light is on the entire first floor, then on the second and finally the platform lighting turns on.
You can see how it works on the general video of the project. The light on the platform can also flash several times, and only after then constantly glow. That is, you can do it programmatically yourself, and then how convenient it is for you to change the behavior of the layout. And of course, you can apply fade-in fade-out effects, it is especially suitable for street lights. I installed on my layout a separate URB with a photodetector, and now when the room gets dark, if the power of the layout is turned on, he turns on the evening lights. The Arduino platform also allows you to use the function random() for this.

Control from the application

Using the commands of Protocol 2, you can control the light and devices as described above. Sketches with effects and examples of mechanisms are regularly replenished in the section for donors.

 

Lighting the railway layout diagram
Using the Hall sensor to control the train

AWS and sensors

To automate the movement of trains you need sensors. I used Hall sensors as train detector triggers on the line. In comparison with other sensors, it has the advantage of a trigger "dot". The second element — the magnet — is attached to any metal part underside of a car or a locomotive (for example, to the fastening screw, etc.). I recommend small cylinder neodymium rare earth magnets. By changing their number, you can adjust the distance between the magnet on the car and the sensor on the rails. I have reliable operation at distances of 1-5 mm. This allows you to correctly and accurately set the pickup location relative to the train and relative to the location on the layout, as well as easily adjust it by moving the magnets. You can place a magnet on any car or locomotive, at the beginning, middle or end of the train, thus adjusting the stopping place to within a centimeter. Hall sensors are small, if they are neatly put between the rails, then they are like real AWS inductor. To Arduino NANO it is possible to connect up to 18 Hall sensors. But this is a standalone solution, you can not transfer data to other layout's devices. URB solves this problem elegantly. You can connect the sensors to any URB and then transmit their signals to the communication station via the I2C bus.

Also I use IR sensors to stop trains or locomotive before a dead end. But this type of sensor (as well as generally all based on reflection) has a feature – the sensor triggering depends not only on the distance to the obstacle, but also on the color of the object. That is, if the car or locomotive is light, then the sensor will work at a greater distance and vice versa. Unpleasant, if the car or locomotive is black, then the sensor may not detect it.

Example of a simple layout

This example is created primarily for the Arduino Train Junior application. Applications Arduino Train DUO and DNT also fully support this example, but their capabilities are greater.

A railway circle with four turnouts

 


  • void(* resetFunc) (void) = 0; // RESET FUNCTION
    
    #include <SoftwareSerial.h>
    #include <Wire.h>
    #include <Servo.h>
    
    // I/O PINS
    
    #define ENA_PIN 3
    #define IN1_PIN 2
    #define IN2_PIN 4
    #define JUNCTION_EN 8
    Servo J1;
    Servo J2;
    
    SoftwareSerial Bluetooth(12, 13); // RX, TX
    
    // VARIABLES
      
      // SERIAL EVENT
      bool stringComplete = false;
      String inputString = "";
    
      // I2C
      int addressI2C;
      byte dataToI2C, dataFromI2C; 
    
    
      // JUNCTIONS & SET DEFAULT POSITIONS
      bool switch_A = true, switch_B = true, switch_C = true, switch_D = true;
      unsigned long millisJunction;
      
      // SPEED TABLE
      byte speedArrayA [] = {70, 100, 140, 180, 210, 255}; // CUSTOM THROTTLE
      byte speedLocoA;
    
      // CHECK CHANGES 
      bool flag_change_junc; 
    
    
    void setup() {
    
    // Initialize Serial & I2C
      Serial.begin(9600);
      Bluetooth.begin(9600);
      Wire.begin();
      inputString.reserve(4);
    
    // Initialize Motor Driver
      pinMode(ENA_PIN, OUTPUT); 
      pinMode(IN1_PIN, OUTPUT); 
      pinMode(IN2_PIN, OUTPUT);
    
    // Initialize Servos
      pinMode(JUNCTION_EN, OUTPUT); 
      pinMode(9, OUTPUT);
      pinMode(10, OUTPUT);  
      J1.attach(9); J2.attach(10);  
      J1.write(180); J2.write(0);
      delay(50);
      digitalWrite(JUNCTION_EN, HIGH);
      delay(600);
      digitalWrite(JUNCTION_EN, LOW);
    }
    
    void loop() {
    
    // ----  START PARSING INCOMING APP COMMANDS  
      if (stringComplete) {
    
        // RESET LAYOUT
        if (inputString =="000z") {
          dataToI2C = 99;
          addressI2C = 2;
          sendDataViaI2C(); delay(150);
          addressI2C = 3;
          resetFunc();
        }
    
        // LOCO CONTROL FUNCTIONS
        if (inputString.charAt(0) =='a') {
          byte speedLocoA = 0; // be on the safe side set throttle to 0
    
          // Speed (0-6)
          if (inputString.charAt(1) =='0') { 
            if (inputString.charAt(2) =='0') speedLocoA = 0;
            if (inputString.charAt(2) =='2') speedLocoA = speedArrayA[0];
            if (inputString.charAt(2) =='4') speedLocoA = speedArrayA[1];
            if (inputString.charAt(2) =='6') speedLocoA = speedArrayA[2];
            if (inputString.charAt(2) =='8') speedLocoA = speedArrayA[3];
          }
          if (inputString.charAt(1) =='1') {
            if (inputString.charAt(2) =='0') speedLocoA = speedArrayA[4];
            if (inputString.charAt(2) =='2') speedLocoA = speedArrayA[5];    
          }
    
          // Direction, Stop and AWS
          if (inputString.charAt(1) =='d') {
            if (inputString.charAt(2) =='f') { // (f) Forward
              digitalWrite(IN1_PIN, HIGH);
              digitalWrite(IN2_PIN, LOW); 
            }
            if (inputString.charAt(2) =='b') { // (b) Backward
              digitalWrite(IN1_PIN, LOW);
              digitalWrite(IN2_PIN, HIGH); 
            }  
            if (inputString.charAt(2) =='s') { // (s) Stop button
               digitalWrite(IN1_PIN, LOW);
               digitalWrite(IN2_PIN, LOW); 
               speedLocoA = 0;
            } 
    /* AWS        
            if (inputString.charAt(2) =='r') { // (r) Release button | AWS
               aws_driverA = true;
               millisDeblockA = millis();       
            } 
    */
          }
    
          analogWrite(ENA_PIN,speedLocoA); // set throttle
        }
    
        // JUNCTIONS
        if (inputString.charAt(0) =='j') {    
         
          // Switch A
          if (inputString.charAt(1) =='a') { 
            if (inputString.charAt(2) =='0') {
              switch_A = false;
              J1.write(0); delay(50);
              digitalWrite(JUNCTION_EN, HIGH);
              millisJunction = millis();
            }
            if (inputString.charAt(2) =='1') {
              switch_A = true;
              J1.write(180); delay(50);
              digitalWrite(JUNCTION_EN, HIGH);
              millisJunction = millis();
            }
          } 
          // Switch B
          if (inputString.charAt(1) =='b') { 
            if (inputString.charAt(2) =='0') {
              switch_B = false;
              addressI2C = 2; dataToI2C = 30;
              sendDataViaI2C();
            }
            if (inputString.charAt(2) =='1') {
              switch_B = true;
              addressI2C = 2; dataToI2C = 31;
              sendDataViaI2C();
            }
          }
          // Switch C
          if (inputString.charAt(1) =='c') { 
            if (inputString.charAt(2) =='0') {
              switch_C = false;
              J2.write(180); delay(50);
              digitalWrite(JUNCTION_EN, HIGH);
              millisJunction = millis();
            }
            if (inputString.charAt(2) =='1') {
              switch_C = true;
              J2.write(0); delay(50);
              digitalWrite(JUNCTION_EN, HIGH);
              millisJunction = millis();
            }
          } 
          // Switch D
          if (inputString.charAt(1) =='d') { 
            if (inputString.charAt(2) =='0') {
              switch_D = false;
              addressI2C = 2; dataToI2C = 32;
              sendDataViaI2C();
            }
            if (inputString.charAt(2) =='1') {
              switch_D = true;
              addressI2C = 2; dataToI2C = 33;
              sendDataViaI2C();
            }        
          }
    
          delay(150); // delay for I2C
          flag_change_junc = true;
        }  
    
        inputString = "";
        stringComplete = false;
      
      }
    
    // ----  LOGIC BLOCK
      if (flag_change_junc) {
    
         
    
        flag_change_junc = false;
      }
    
      if (millis() > (millisJunction + 600)) digitalWrite(JUNCTION_EN, LOW);
     
      bluetoothEvent();
    }
    
    //// FUNCTIONS ////
    void bluetoothEvent() {
      if (Bluetooth.available()) {
        char inChar = (char)Bluetooth.read();
        inputString += inChar;
        if (inChar == 'z') {
          stringComplete = true;
        }
      }
    }
    
    void sendDataViaI2C() {
      Wire.beginTransmission(addressI2C);
      Wire.write(dataToI2C);
      Wire.endTransmission();
    }
    

 


  • #include <Wire.h>
    #include <Servo.h>
    
    void(* resetFunc) (void) = 0;
    
    // I/O PINS
    
    #define JUNCTION_EN 8
    Servo J1;
    Servo J2;
    
    // VARIABLES
    unsigned long millisJunction;
    byte dataFromI2C;
    
    void setup() {
    
    // Initialize I2C
      Wire.begin(2);
      Wire.onReceive(receiveI2C);
    
    // Initialize Servos
      pinMode(JUNCTION_EN, OUTPUT); 
      pinMode(9, OUTPUT);
      pinMode(10, OUTPUT);  
      J1.attach(9); J2.attach(10);  
      J1.write(180); J2.write(0);
      delay(50);
      digitalWrite(JUNCTION_EN, HIGH);
      delay(600);
      digitalWrite(JUNCTION_EN, LOW);
    }
    
    void loop() {
      // COMMAND PARSING
      if (dataFromI2C != 0) {
        switch (dataFromI2C) {
    
        // RESET
        case 99: resetFunc(); break;
    
        // JUNCTION 1
        case 30: J1.write(180);
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             break;
    
        case 31: J1.write(0);
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             break;
    
        // JUNCTION 2
        case 32: J2.write(180);
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             break;
    
        case 33: J2.write(0);
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             break;
        
        }
    
      dataFromI2C = 0;
      }
      
      if (millis() > (millisJunction + 600)) digitalWrite(JUNCTION_EN, LOW);
    }
    
    // ----------- FUNCTIONS ----------- //
    
    void receiveI2C(int howMany) {
      while (Wire.available() > 0) {
        dataFromI2C = Wire.read();
      }
    }
    

You can send commands via the I2C bus to any of the end peripherals connected to any URB on the layout. According to the specification, the maximum length I2C bus when using a twisted pair of about eight meters. In practice, with a total cable length of five meters, everything works correctly.
If you need a large length of wires, you can use a repeater consisting of two URBs, creating a connection I2C-Serial-I2C.

Modellers like a complex systems. If you look at any of your layouts or rail lines, then it's always a lot of ways and swithes. But when the modeler sees Arduino sketch of 400 lines for his layout, then this amount of code scares him. My American friend Dan, was so scared by the code of Arduino that he abandoned railway modelism and began to manufacture toy furniture:)
In fact, the amount of code Arduino directly corresponds to the complexity of your layout or lines. If you want to immediately try to apply the URB project to your complex layout, then you have to read all the materials on this site. But, in my experience and Dan's experience, it's better to do it gradually.
I've been thinking for a whole year how to simplify your entry-point into this system, and do not turn the project into a rigid design in which you can not do anything yourself. And I managed to make the code modular. The lines of the example given in this chapter are almost symmetrical. Therefore, two sketches for URB units are very easy to compare with each other, since peripheral devices are connected to units in the same way. This is an easy entry-point for understanding the project, but if you find it difficult to understand it, look at the chapters for beginners.

In this example, I will add the functionality sequentially. Thus, it will be quite easy for you to write your sketches. And there is no need to write the sketch code again. You just need to take the finished pieces of code — snippets.

Loco and junctions control

As you can see, I use in this part two URB units with which the locomotive and the junctions are controlled. I took two ready-made snippets, one for the local URB and the second for the communication station. And all this is controlled by the application of the Arduino Train Junior. Observe the following principle of placing URB units on the layout, the units should be located as close to the peripheral devices of the layout. In this case, these are servos of Point Switches.
In the Application there is a button DEFAULT, which set junctions to the position assigned user by startup, the same happens when the layout is initialized when the power is turned on. For this in header sketch use the reset options for the rail lines to the default position.

In the header of the sketches, I set the pins for the servos and the driver-motor. Of the functions, I only need control of locomotive traction and switch junction in the sketch for the Communication station. For data transfer, I use the libraries Software Serial and Wire on the Communication station and one the library Wire in the local URB.

Since the feature of the project is auto-disconnection of servos, you will be interested in the code that implements this function. Also here, the reset options for the rail lines in the default position described above are applied.

As you saw the code is not complicated. And you easily and quickly adapt it to your layout.

Adding isolated lines

One train is good, but several trains are even better. Since my project works in the DC mode, it is enough to interrupt the rails in several places and get more interesting control modes. This simple solution, as you will see in the following examples, allows for flexibility to surpass even DCC control. In the part of the website Donators Entrance, examples are given of build the layout with interlocks and traffic automation. But most importantly, Arduino in my project independently turns on and off lines depending on the position of junctions and the logic you set. In there part, I use the simplest algorithm – if two junctions allow movement the train along a line, then the relay turns on the current to it, otherwise the line is de-energized. The code that implements this algorithm in my project is called a Logic Block.

Also, in this example, you can verify the universality of Arduino pins – I will connect the dual relays to different pins of the URB units, and the sketches code will remain unchanged.

To compose algorithms for the layout, we use selection operators and Boolean Algebra. Before writing a sketch, it is convenient to make a table of states of junctions, lines (and, accordingly, relays). Similarly, you may semaphore signals are programmed. The implementation of a AWS system using sensors is considered in the following, more complex example.

And now, I just add the relay logic to the previous sketches. Done!

A railway circle with four turnouts and path isolators

 

 


  • void(* resetFunc) (void) = 0; // RESET FUNCTION
    
    #include <SoftwareSerial.h>
    #include <Wire.h>
    #include <Servo.h>
    
    // I/O PINS
    
    #define ENA_PIN 3
    #define IN1_PIN 2
    #define IN2_PIN 4
    #define JUNCTION_EN 8
    Servo J1;
    Servo J2;
    
    #define RELAY_LINE1 14
    #define RELAY_LINE2 15
    
    SoftwareSerial Bluetooth(12, 13); // RX, TX
    
    // VARIABLES
      
      // SERIAL EVENT
      bool stringComplete = false;
      String inputString = "";
    
      // I2C
      int addressI2C;
      byte dataToI2C; 
    
    
      // JUNCTIONS & SET DEFAULT POSITIONS
      bool switch_A = true, switch_B = true;
      unsigned long millisJunction;
      
      // SPEED TABLE
      byte speedArrayA [] = {70, 100, 140, 180, 210, 255}; // CUSTOM THROTTLE
      byte speedLocoA;
    
      // CHECK CHANGES 
      bool flag_change_junc = false; 
    
    
    void setup() {
    
    // Initialize Serial & I2C
      Serial.begin(9600);
      Bluetooth.begin(9600);
      Wire.begin();
      inputString.reserve(4);
    
    // Initialize Motor Driver
      pinMode(ENA_PIN, OUTPUT); pinMode(IN1_PIN, OUTPUT); pinMode(IN2_PIN, OUTPUT);
    
    // Initialize Servos
      pinMode(JUNCTION_EN, OUTPUT); 
      pinMode(9, OUTPUT);
      pinMode(10, OUTPUT);  
      J1.attach(9); J2.attach(10);  
      J1.write(180); J2.write(0);
      delay(50);
      digitalWrite(JUNCTION_EN, HIGH);
      delay(600);
      digitalWrite(JUNCTION_EN, LOW);
    
    // Initialize Relays
      pinMode(RELAY_LINE1, OUTPUT);
      pinMode(RELAY_LINE2, OUTPUT);
      digitalWrite(RELAY_LINE1, HIGH);  
      digitalWrite(RELAY_LINE2, LOW);  
    
    }
    
    void loop() {
    
    // ----  START PARSING INCOMING APP COMMANDS  
      if (stringComplete) {
    
        // RESET LAYOUT
        if (inputString =="000z") {
          dataToI2C = 99;
          addressI2C = 2;
          sendDataViaI2C(); delay(150);
          addressI2C = 3;
          resetFunc();
        }
    
        // LOCO CONTROL FUNCTIONS
        if (inputString.charAt(0) =='a') {
          byte speedLocoA = 0; // be on the safe side set throttle to 0
    
          // Speed (0-6)
          if (inputString.charAt(1) =='0') { 
            if (inputString.charAt(2) =='0') speedLocoA = 0;
            if (inputString.charAt(2) =='2') speedLocoA = speedArrayA[0];
            if (inputString.charAt(2) =='4') speedLocoA = speedArrayA[1];
            if (inputString.charAt(2) =='6') speedLocoA = speedArrayA[2];
            if (inputString.charAt(2) =='8') speedLocoA = speedArrayA[3];
          }
          if (inputString.charAt(1) =='1') {
            if (inputString.charAt(2) =='0') speedLocoA = speedArrayA[4];
            if (inputString.charAt(2) =='2') speedLocoA = speedArrayA[5];    
          }
    
          // Direction, Stop and AWS
          if (inputString.charAt(1) =='d') {
            if (inputString.charAt(2) =='f') { // (f) Forward
              digitalWrite(IN1_PIN, HIGH);
              digitalWrite(IN2_PIN, LOW); 
            }
            if (inputString.charAt(2) =='b') { // (b) Backward
              digitalWrite(IN1_PIN, LOW);
              digitalWrite(IN2_PIN, HIGH); 
            }  
            if (inputString.charAt(2) =='s') { // (s) Stop button
               digitalWrite(IN1_PIN, LOW);
               digitalWrite(IN2_PIN, LOW); 
               speedLocoA = 0;
            }   
          }
    
          analogWrite(ENA_PIN,speedLocoA); // set throttle
        }
    
        // JUNCTIONS
        if (inputString.charAt(0) =='j') {    
         
          // Switch A
          if (inputString.charAt(1) =='a') { 
            if (inputString.charAt(2) =='0') {
              switch_A = false;
              J1.write(0); delay(50);
              digitalWrite(JUNCTION_EN, HIGH);
              millisJunction = millis();
            }
            if (inputString.charAt(2) =='1') {
              switch_A = true;
              J1.write(180); delay(50);
              digitalWrite(JUNCTION_EN, HIGH);
              millisJunction = millis();
            }
          } 
          // Switch B
          if (inputString.charAt(1) =='b') { 
            if (inputString.charAt(2) =='0') {
              switch_B = false;
              addressI2C = 2; dataToI2C = 30;
              sendDataViaI2C();
            }
            if (inputString.charAt(2) =='1') {
              switch_B = true;
              addressI2C = 2; dataToI2C = 31;
              sendDataViaI2C();
            }
          }
          // Switch C
          if (inputString.charAt(1) =='c') { 
            if (inputString.charAt(2) =='0') {
              J2.write(180); delay(50);
              digitalWrite(JUNCTION_EN, HIGH);
              millisJunction = millis();
            }
            if (inputString.charAt(2) =='1') {
              J2.write(0); delay(50);
              digitalWrite(JUNCTION_EN, HIGH);
              millisJunction = millis();
            }
          } 
          // Switch D
          if (inputString.charAt(1) =='d') { 
            if (inputString.charAt(2) =='0') {
              addressI2C = 2; dataToI2C = 32;
              sendDataViaI2C();
            }
            if (inputString.charAt(2) =='1') {
              addressI2C = 2; dataToI2C = 33;
              sendDataViaI2C();
            }        
          }
    
          delay(150); // delay for I2C
          flag_change_junc = true;
        }  
    
        inputString = "";
        stringComplete = false;
      
      }
    
    // ----  LOGIC BLOCK
      if (flag_change_junc) {
    
        if (switch_A && switch_B) digitalWrite(RELAY_LINE1, LOW); // LINE 1 ENABLE
        else digitalWrite(RELAY_LINE1, HIGH);
    
        if (!switch_A && !switch_B) digitalWrite(RELAY_LINE2, LOW); // LINE 2 ENABLE
        else digitalWrite(RELAY_LINE2, HIGH);     
    
        flag_change_junc = false;
      }
    
      if (millis() > (millisJunction + 600)) digitalWrite(JUNCTION_EN, LOW);
     
      bluetoothEvent();
    }
    
    //// FUNCTIONS ////
    void bluetoothEvent() {
      if (Bluetooth.available()) {
        char inChar = (char)Bluetooth.read();
        inputString += inChar;
        if (inChar == 'z') {
          stringComplete = true;
        }
      }
    }
    
    void sendDataViaI2C() {
      Wire.beginTransmission(addressI2C);
      Wire.write(dataToI2C);
      Wire.endTransmission();
    }
    
    

 


  • #include <Wire.h>
    #include <Servo.h>
    
    void(* resetFunc) (void) = 0;
    
    // I/O PINS
    #define JUNCTION_EN 8
    Servo J1;
    Servo J2;
    
    #define RELAY_LINE3 12
    #define RELAY_LINE4 13
    
    
    // VARIABLES
    unsigned long millisJunction;
    byte dataFromI2C;
    bool switch_C = true, switch_D = true;
    bool flag_change_junc = false; // CHECK CHANGES 
    
    void setup() {
    
    // Initialize I2C
      Wire.begin(2);
      Wire.onReceive(receiveI2C);
    
    // Initialize Servos
      pinMode(JUNCTION_EN, OUTPUT); 
      pinMode(9, OUTPUT);
      pinMode(10, OUTPUT);  
      J1.attach(9); J2.attach(10);  
      J1.write(180); J2.write(0);
      delay(50);
      digitalWrite(JUNCTION_EN, HIGH);
      delay(600);
      digitalWrite(JUNCTION_EN, LOW);
    
    // Initialize Relays
      pinMode(RELAY_LINE3, OUTPUT);
      pinMode(RELAY_LINE4, OUTPUT);
      digitalWrite(RELAY_LINE3, HIGH);  
      digitalWrite(RELAY_LINE4, LOW);    
    }
    
    void loop() {
      // COMMAND PARSING
      if (dataFromI2C != 0) {
        switch (dataFromI2C) {
    
        // RESET
        case 99: resetFunc(); break;
    
        // JUNCTION 1
        case 30: J1.write(180);
             switch_C = false;
             flag_change_junc = true;
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             break;
    
        case 31: J1.write(0);
             switch_C = true; 
             flag_change_junc = true;
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             break;
    
        // JUNCTION 2
        case 32: J2.write(180);
             switch_D = false; 
             flag_change_junc = true;
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             break;
    
        case 33: J2.write(0);
             switch_D = true; 
             flag_change_junc = true;
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             break;
        
        }
    
      dataFromI2C = 0;
      }
    
    // ----  LOGIC BLOCK
      if (flag_change_junc) {
    
        if (!switch_C && !switch_D) digitalWrite(RELAY_LINE3, LOW); // LINE 3 ENABLE
        else digitalWrite(RELAY_LINE3, HIGH);
    
        if (switch_C && switch_D) digitalWrite(RELAY_LINE4, LOW); // LINE 3 ENABLE
        else digitalWrite(RELAY_LINE4, HIGH);     
    
        flag_change_junc = false;
      }
    
      if (millis() > (millisJunction + 600)) digitalWrite(JUNCTION_EN, LOW);
    }  
    
    // ----------- FUNCTIONS ----------- //
    
    void receiveI2C(int howMany) {
      while (Wire.available() > 0) {
        dataFromI2C = Wire.read();
      }
    }
    

Testing from a computer

Connecting a computer directly to URB unit is a very important feature. You can debug your sketches on time on the layout, or test the circuits separately by functional blocks.

Hidden firmware update

 

Testing URB from computer

Any modellers encountered difficulties in creating electronics for a layout. This is a lot of wires, the complexity of connecting devices and their compatibility among themselves, and most importantly the inability to add or change something in the finished layout without dramatic changes in the wiring. These shortcomings are absent in the URB concept.

All wiring between the blocks is made up of four wires. For example, if you need to put a few new junctions or sensors or streetlights on the layout, you just cut the four-wire bus in the nearest place and insert another URB. It remains only to upload the sketch, and your newly installed pieces are immediately integrated into the layout. At any time you can reprogram already installed URBs directly on a layout, you just need to leave the opportunity to reach it with a mini-USB cable. This way you can change the control or setting of any layout's devices, and replace it if necessary.

Schemes with low power you can connect directly to the USB port.

When connecting the cable USB Arduino located on the board receives 5 Volt power through the cable directly from the computer (see +5V AUTO SELECTOR on MBR0520 in the Arduino NANO scheme).
If you do not want to broke the USB port of your computer, BEFORE connect the USB cable power up the layout from its power source!

Another plus is the ability to experiment with each the URB unit independently of others, and it's still an easy way to look for possible malfunctions in electrical parts on layout. It is also possible to assemble URBs with peripheral modules separately on the table. You'll tune the prototype and then will transfer it to the layout. This allows you to realize your ideas much faster.

PRO Sketches library

You can adapt schemes and sketches of this example to ANY BIG LAYOUT. The example is designed for the Application Arduino Train DUO (DNT).

On the general picture of the experimental layout I drew the basic details, it is convenient for his presentation. But to understand the details need more schematic drawings. A few of the following pictures will be convenient for you to move from from generalities to specifics.

This is the largest chapter of this site. Therefore, you need an introduction.
Most of the materials and posts of modelers show the most spectacular part of the process of creating a layout – the layout of ways and creating details of the landscape. At the same time, the creation of trains control system is postponed until later, hoping for ready-made DDC solutions. And if the layout is large enough, it quickly becomes clear that the initial and most popular DСC systems are not designed for such a complex layout. As a result, most modelers simply do not try to make complex control of their layout. And I very often see a sad picture as on an excellent model layout several trains monotonous and boring move on parallel circles. Such "live" things as dynamic changes in routes, complex maneuvers, automatic train movement, online signaling and independent management of several players will make very expensive and difficult to install and configure logics if you use the concept of DCC. Especially bad is the control console on complex layouts, it is cumbersome, inconvenient and its cost often exceeds the price of all other electronics.

As you already know, in my project the console is the Android phone with the Arduino Trains application. In addition to the fact that you immediately get several a ready "free" and wireless management consoles, this solution transfers the configuration of the logic and algorithms of your layout to the network of Arduino blocks located directly on the layout. This is a very big difference from the usual model of building electronics on the layout, since you are building a distributed control system. This solution allows you to perform most of the control algorithms locally, which greatly simplifies and speeds up the setup and programming of sketches.

The flexibility and universality of the project URB gives you the opportunity to make decisions about the organization of the behavior of devices on your layout. Therefore, before I start programming, I must describe the logic of the behavior of the layout example.

Lines description

This experiment consists of two independent loops (green and purple) with branches and one transition (black) line with a deadlock, connecting the loops with each other. You see 12 junctions, and I assigned them alphabetic indexes pursuant to the picture. In doing so, I adhered to the rule of sequential assignment of indices from Line A to Line B. I remind you that you can assign any convenient index to any junctions on the layout in sketch at the Communication URB unit.

The layout control is DC, which means that we need ten relays. Eight for blocking paths and two for transferring power to the Transition Line. The direction of turnouts determines the state of the relay. Double relay block #1 isolate paths A1 and T1. Relay block #2 provides switching of other paths.

DC mode interconnection rail tracks Wiring diagram of two relays for transferring power to the Transition line. In a sketch, these relays are controlled by one command and, accordingly, only one pin is needed on Arduino.

Logic

Now we define state boolean variables for paths and junctions. I apply the following rule to junctions: if the junction has a direct position, then the value of the variable "1" or TRUE, if the direction of the junction on the branch is the variable "0" or FALSE. For the active / passive the path state, respectively: 1 (TRUE) / 0 (FALSE).

By using these variables, the Communication URB unit remembers the state of all lines and paths, and you can use comparison operators to program the logic of the behavior of signals, relays and other things.

You can use much more complex logical constructions and dependencies, for example for automatic train movement on a schedule or use scripts. The task of this example is to show you these possibilities.

The variable name here corresponds to the name of turnout switches in the App. Now we can draw up a table of correspondence between the positions of junctions and the state of lines. In general, you can arrange turnouts in pairs, or in other ways, but in this experiment, each the junction is controlled individually.

I accepted the condition that only when all the correspondences for each path are observed, it's becomes active. Line Transition switches to power from Line A only if junctions A, C, G, H and K are in a certain position. And accordingly vice versa. The same rules apply to a pairwise combination of junctions and the path, for example junctions A and E to path A2.

 

Periodically changing the logic in the sketch, you can change the behavior of your layout. Therefore, playing with your railway will not become boring with alltime.

Switches A B C D E F G H I J K L
Path A1   0                    
Path A2 0       0   1          
Path A3 1   1   1   1          
Line A occupy TRANS 1   0 0   0 0          
Line B occupy TRANS       1   1   0 0   0  
Path T1                 1      
Path B1               1     1 1
Path B2                   0 1 0
Path B3                   1 1 0
Just look at your layout railpaths and fill out the table. Please do not try to build an algorithm without creating a rail lines state table.
If the railpath does not depend on the position of the particular junction or if the position of the junction blocks the path, then the table cell does not fill out.
 
Optimization

I streamline the table in two ways: applying the rule described above and changing the connection scheme of the relay directly to the railway tracks.
You also can use the Karnaugh map method to optimize.

Relay block# 1 on the URB#2 will be controlled locally, depending on the positions of junctions connected to this URB unit. Thus, we exclude the lines A1 and T1.

Similarly, we transfer to the local URB#5 the blocking of paths B2 and B3, depending on the successive chain of junctions L and J.

After optimization the lines becomes very simple. And adding the dependence of the power supply to Transition Line from only one junction F, I got all the information for programming the logic.

Library snippets

In Arduino IDE, a sketch consisting of several files is supported. When compiled, they are combined into a common firmware. Therefore, the snippets library zip-archive contains a file structure.
In a simple example above, all the files are easier to unite into one sketch, which I did. The following example is quite large, so I'll use this feature of Arduino IDE in it.

Download

 

The description of the commands and functions I place directly in the comments in the sketch code. But the known function DELAY in the URB project has a strictly limited applying, and therefore I presented it's description here.

Since the DELAY function stops the execution void loop() cycle (i.e., the Arduino processor simply stops at the delay time), the data stops being transmitted via the bus and the commands are lost. Therefore, you can only use this function in the code the void setup() block.
I also use the DELAY function when transferring data from the master to local units via I2C bus ONLY at the Communication station. These delays are necessary for reliable data transmission. In this case, since the master initializes data transfer, no loss of commands occurs. In all other cases, use the millis() function, or a library based on it.
For a reliable operation of the SERVO library, I apply a very small delay, and it's does not affect the processes described above.

Communicate URB

At the beginning of each sketch, the state of Arduino pins and the periphery connected to them are described. Therefore, based on your plan and the table, you already know you have the combined behavior of the elements of your layout. This is a good starting point for creating a logical algorithm.For example, you can connect the motor driver to any URB local unit, for this it is enough to convert the input command from the application into a byte code and pass it through the bus to this local unit. But in this example I will demonstrate first of all the practice of using snippets, so the motor driver is connected to the communication station. And as you can see below, the sketch is almost the same as the example of a simple layout, only this you have two control channels.

I also consider it important to have a draft of connections, as in the figure. Even if you mix up contacts, it's later you easy to fix it right in the code. The main thing that gives you the use of the project is end-to-end standardization of methods, in other words you can not make a gross mistake.

Let's go!


  • void(* resetFunc) (void) = 0; // RESET FUNCTION
    
    #include <SoftwareSerial.h>
    #include <Wire.h>
    #include <Servo.h>
    
    // I/O PINS
    
    #define ENA_PIN 3
    #define IN1_PIN 2
    #define IN2_PIN 4
    #define IN3_PIN 5
    #define IN4_PIN 7
    #define ENB_PIN 6
    
    #define JUNCTION_EN 8
    Servo J1;
    Servo J2;
    Servo J3;
    
    SoftwareSerial Bluetooth(12, 13); // RX, TX
    
    // VARIABLES
      
      // SERIAL EVENT
      bool stringComplete = false;
      String inputString = "";
    
      // I2C
      int addressI2C;
      byte dataToI2C, dataFromI2C; 
    
      // APP SETTINGS 
      bool sensorsEnable = true;
      bool randomLight = false;
      bool junctionARM = false;  
    
      // AWS
      bool awsDriver_A, awsDriver_B;
      unsigned long millisDeblockA, millisDeblockB;
    
      // SENSORS FLAG
      bool sensor_1;
    
    
      // JUNCTIONS & SET DEFAULT POSITIONS
      bool switch_A = true, switch_B = true, switch_C = true, switch_D = true,
      switch_E = true, switch_F = true, switch_G = true, switch_H = true,
      switch_I = true, switch_J = true, switch_K = true, switch_L = true;
      //1 2 3 4 5 6 7 8 9 10 11 12 | 13 14 15 16 17 18 19 20 21 22 23 24 25 
      //A B C D E F G H I  J  K  L | M  N  O  P  Q  R  S  T  U  V  W  X  Y
      unsigned long millisJunction;
    
      // SPEED TABLE
      byte speedArrayA [] = {50, 70, 90, 110, 120, 130, 150, 170, 200, 230, 255};
      byte speedArrayB [] = {50, 80, 120, 160, 170, 180, 190, 200, 220, 240, 255};
    
      // CHECK CHANGES 
      bool flag_change_junc; 
    
    
    void setup() {
    
    // Initialize Serial & I2C
      Serial.begin(9600);
      Bluetooth.begin(9600);
      Wire.begin();
      inputString.reserve(4);
    
    // Initialize Motor Driver
      pinMode(ENA_PIN, OUTPUT); pinMode(IN1_PIN, OUTPUT); pinMode(IN2_PIN, OUTPUT);
      pinMode(IN3_PIN, OUTPUT); pinMode(IN4_PIN, OUTPUT); pinMode(ENB_PIN, OUTPUT);
    
    // Initialize Servos
      pinMode(JUNCTION_EN, OUTPUT);
      pinMode(9, OUTPUT);
      pinMode(10, OUTPUT);
      pinMode(11, OUTPUT);
      J1.attach(9); J2.attach(10); J3.attach(11); 
    
      // DEFAULT POSITION  
      J1.write(180); J2.write(0); J3.write(180);
      delay(50);
      digitalWrite(JUNCTION_EN, HIGH);
      delay(600);
      digitalWrite(JUNCTION_EN, LOW);
    
    }
    
    void loop() {
    
    // ----  START PARSING INCOMING APP COMMANDS	
      if (stringComplete) {
    
        if (inputString =="000z") {
          dataToI2C = 99;
          addressI2C = 2;
          sendDataViaI2C(); delay(150);
          addressI2C = 3;
          sendDataViaI2C(); delay(150);
          addressI2C = 4;
          sendDataViaI2C(); delay(150);
          addressI2C = 5;
          sendDataViaI2C();
          resetFunc();
        }
    
        // CONTROL FUNCTIONS
        if (inputString.charAt(0) =='a') controlPlayerA();
        if (inputString.charAt(0) =='b') controlPlayerB();
        if (inputString.charAt(0) =='j') controlJunctions();
        if (inputString.charAt(0) =='l') controlLights();
        if (inputString.charAt(0) =='s') scriptsAndSettings();
        inputString = "";
        stringComplete = false;
      
      }
    
    // ---- START COLLECT SENSORS STATE ----
      if (sensorsEnable) {
        // I2C REQUEST
        static unsigned long prevTime = 0;
        
        if (millis() - prevTime > 25) {
        addressI2C = 2;
        Wire.requestFrom(addressI2C, 1); 
        prevTime = millis();
        dataFromI2C = Wire.read();
        }
    
        if (dataFromI2C > 0) {
          if (dataFromI2C == 1) sensor_1 = true; 
          else sensor_1 = false;
        dataFromI2C = 0;
        }
      }
    
    // ----  MAIN BLOCK
      if (flag_change_junc) {
        addressI2C = 5;
    
        if (switch_F) {
          dataToI2C = 50; // POWER FROM B
          sendDataViaI2C(); delay(150);    	
        }
        else {
          dataToI2C = 51; // POWER FROM A
          sendDataViaI2C(); delay(150);        	
        }
    
        if (!switch_A && !switch_E) {
          dataToI2C = 52; // PATH A2 ENABLE
          sendDataViaI2C(); delay(150);
        }
    
        if (switch_C && switch_E) {
          dataToI2C = 53; // PATH A3 ENABLE
          sendDataViaI2C(); delay(150);
        }
    
        if (!switch_C && !switch_G) {
          dataToI2C = 54; // TRANSITION ENABLE FROM LINE A
          sendDataViaI2C(); delay(150);
        }
    
        if (!switch_H && !switch_K) {
          dataToI2C = 55; // TRANSITION ENABLE FROM LINE B
          sendDataViaI2C(); delay(150);
        }
    
        if (switch_H && switch_K) {
          dataToI2C = 56; // PATH B1 ENABLE
          sendDataViaI2C(); delay(150);
        }
    
        flag_change_junc = false;
      }
    
      // AWS & SENSORS
    
      if (!awsDriver_A && sensorsEnable) {  
        if (sensor_1) {
          analogWrite(ENA_PIN, 0);
          Bluetooth.print("aws_az"); // Feedback to App
          Serial.print("aws_az"); 
        }
      }
    
      if (millis() > (millisDeblockA + 5000)) awsDriver_A = false;  
      if (millis() > (millisDeblockB + 5000)) awsDriver_B = false; 
      if (millis() > (millisJunction + 600)) digitalWrite(JUNCTION_EN, LOW);
     
      bluetoothEvent();
    }
    
    //// FUNCTIONS ////
    void serialEvent() {
      if (Serial.available()) {
        char inChar = (char)Serial.read();
        inputString += inChar;
        if (inChar == 'z') {
          stringComplete = true;
        }
      }
    }
    
    void bluetoothEvent() {
      if (Bluetooth.available()) {
        char inChar = (char)Bluetooth.read();
        inputString += inChar;
        if (inChar == 'z') {
          stringComplete = true;
        }
      }
    }
    
    void sendDataViaI2C() {
      Wire.beginTransmission(addressI2C);
      Wire.write(dataToI2C);
      Wire.endTransmission();
    }
    

  • void controlPlayerA() {
      byte speedLocoA = 0; // Set throttle to 0 when change direction
      // Speed (0-11)
      if (inputString.charAt(1) =='0') { 
        if (inputString.charAt(2) =='0') speedLocoA = 0;
        if (inputString.charAt(2) =='1') speedLocoA = speedArrayA[0];
        if (inputString.charAt(2) =='2') speedLocoA = speedArrayA[1];
        if (inputString.charAt(2) =='3') speedLocoA = speedArrayA[2];
        if (inputString.charAt(2) =='4') speedLocoA = speedArrayA[3];
        if (inputString.charAt(2) =='5') speedLocoA = speedArrayA[4];
        if (inputString.charAt(2) =='6') speedLocoA = speedArrayA[5];
        if (inputString.charAt(2) =='7') speedLocoA = speedArrayA[6];
        if (inputString.charAt(2) =='8') speedLocoA = speedArrayA[7];
        if (inputString.charAt(2) =='9') speedLocoA = speedArrayA[8];
      }
      if (inputString.charAt(1) =='1') {
        if (inputString.charAt(2) =='0') speedLocoA = speedArrayA[9];
        if (inputString.charAt(2) =='1') speedLocoA = speedArrayA[10];
      }
    
      // Direction, Stop and AWS
      if (inputString.charAt(1) =='d') {
        if (inputString.charAt(2) =='f') { // (f) Forward
          digitalWrite(IN1_PIN, HIGH);
          digitalWrite(IN2_PIN, LOW); 
        }
        if (inputString.charAt(2) =='b') { // (b) Backward
          digitalWrite(IN1_PIN, LOW);
          digitalWrite(IN2_PIN, HIGH); 
        }  
        if (inputString.charAt(2) =='s') { // (s) Stop button
           digitalWrite(IN1_PIN, LOW);
           digitalWrite(IN2_PIN, LOW); 
           speedLocoA = 0;
        } 
        if (inputString.charAt(2) =='r') { // (r) Release button | AWS
           awsDriver_A = true;
           millisDeblockA = millis();       
        } 
      }
      analogWrite(ENA_PIN,speedLocoA); // set throttle
    }
    
    void controlPlayerB() {
      byte speedLocoB = 0;
      // Speed (0-11)
      if (inputString.charAt(1) =='0') { 
        if (inputString.charAt(2) =='0') speedLocoB = 0;
        if (inputString.charAt(2) =='1') speedLocoB = speedArrayA[0];
        if (inputString.charAt(2) =='2') speedLocoB = speedArrayA[1];
        if (inputString.charAt(2) =='3') speedLocoB = speedArrayA[2];
        if (inputString.charAt(2) =='4') speedLocoB = speedArrayA[3];
        if (inputString.charAt(2) =='5') speedLocoB = speedArrayA[4];
        if (inputString.charAt(2) =='6') speedLocoB = speedArrayA[5];
        if (inputString.charAt(2) =='7') speedLocoB = speedArrayA[6];
        if (inputString.charAt(2) =='8') speedLocoB = speedArrayA[7];
        if (inputString.charAt(2) =='9') speedLocoB = speedArrayA[8];
      }
      if (inputString.charAt(1) =='1') {
        if (inputString.charAt(2) =='0') speedLocoB = speedArrayA[9];
        if (inputString.charAt(2) =='1') speedLocoB = speedArrayA[10];
      }
    
      // Direction, Stop and AWS
      if (inputString.charAt(1) =='d') {
        if (inputString.charAt(2) =='f') { // Forward
          digitalWrite(IN3_PIN, HIGH);
          digitalWrite(IN4_PIN, LOW); 
        }
        if (inputString.charAt(2) =='b') { // Backward
          digitalWrite(IN3_PIN, LOW);
          digitalWrite(IN4_PIN, HIGH); 
        }  
        if (inputString.charAt(2) =='s') { // Stop button
           digitalWrite(IN3_PIN, LOW);
           digitalWrite(IN4_PIN, LOW); 
           speedLocoB = 0;
        } 
        if (inputString.charAt(2) =='r') { // AWS (release) button
           awsDriver_B = true;
           millisDeblockB = millis();
        } 
      }
      analogWrite(ENB_PIN,speedLocoB);
    }
    

    It's just a snippet that's not changed. If you need to install a motor-driver to a local URB, you need to change digitalWrite and analogWrite commands to byte commands and transfer them via I2C to this local URB.


  • void controlJunctions() {
    
      //1 2 3 4 5 6 7 8 9 10 11 12 | 13 14 15 16 17 18 19 20 21 22 23 24 25 
      //A B C D E F G H I  J  K  L | M  N  O  P  Q  R  S  T  U  V  W  X  Y  
    
      // Switch A URB#2 J1
      if (inputString.charAt(1) =='a') { // Branch direction
        if (inputString.charAt(2) =='0') {
          switch_A = false;
          Bluetooth.print("a0z"); // Feedback to App
          Serial.print("a0z");
          addressI2C = 2; dataToI2C = 30; sendDataViaI2C();
        }
        if (inputString.charAt(2) =='1') { // Throw direction
          switch_A = true;
          Bluetooth.print("a1z"); // Feedback to App
          Serial.print("a1z");      
          addressI2C = 2; dataToI2C = 31; sendDataViaI2C();
        } 
      }
    
      // Switch B URB#2 J2
      if (inputString.charAt(1) =='b') { 
        if (inputString.charAt(2) =='0') {
          switch_B = false;
          Bluetooth.print("b0z"); 
          Serial.print("b0z");
          addressI2C = 2; dataToI2C = 32; sendDataViaI2C();
        }
        if (inputString.charAt(2) =='1') {
          switch_B = true;
          Bluetooth.print("b1z");
          Serial.print("b1z");      
          addressI2C = 2; dataToI2C = 33; sendDataViaI2C();
        } 
      }
    
      // Switch C URB#1 J1
      if (inputString.charAt(1) =='c') { 
        if (inputString.charAt(2) =='0') {
          switch_C = false;
          Bluetooth.print("c0z");
          Serial.print("c0z");
          J1.write(0); delay(50);
          digitalWrite(JUNCTION_EN, HIGH);
          millisJunction = millis();
        }
        if (inputString.charAt(2) =='1') {
          switch_C = true;
          Bluetooth.print("c1z");
          Serial.print("c1z");      
          J1.write(180); delay(50);
          digitalWrite(JUNCTION_EN, HIGH);
          millisJunction = millis();
        } 
      }
    
      // Switch D URB#1 J3
      if (inputString.charAt(1) =='d') { 
        if (inputString.charAt(2) =='0') {
          switch_D = false;
          Bluetooth.print("d0z"); 
          Serial.print("d0z");
          J3.write(0); delay(50);
          digitalWrite(JUNCTION_EN, HIGH);
          millisJunction = millis();
        }
        if (inputString.charAt(2) =='1') {
          switch_D = true;
          Bluetooth.print("d1z");
          Serial.print("d1z");      
          J1.write(180); delay(50);
          digitalWrite(JUNCTION_EN, HIGH);
          millisJunction = millis();
        } 
      }
    
      // Switch E URB#4 J1
      if (inputString.charAt(1) =='e') { 
        if (inputString.charAt(2) =='0') {
          switch_E = false;
          Bluetooth.print("e0z");
          Serial.print("e0z");
          addressI2C = 4; dataToI2C = 30; sendDataViaI2C();
        }
        if (inputString.charAt(2) =='1') {
          switch_E = true;
          Bluetooth.print("e1z");
          Serial.print("e1z");      
          addressI2C = 4; dataToI2C = 31; sendDataViaI2C();
        } 
      }
    
      // Switch F URB#4 J2
      if (inputString.charAt(1) =='f') { 
        if (inputString.charAt(2) =='0') {
          switch_F = false;
          Bluetooth.print("f0z");
          Serial.print("f0z");
          addressI2C = 4; dataToI2C = 32; sendDataViaI2C();
        }
        if (inputString.charAt(2) =='1') {
          switch_F = true;
          Bluetooth.print("f1z");
          Serial.print("f1z");      
          addressI2C = 4; dataToI2C = 33; sendDataViaI2C();
        } 
      }
    
      // Switch G URB#4 J3
      if (inputString.charAt(1) =='g') { 
        if (inputString.charAt(2) =='0') {
          switch_G = false;
          Bluetooth.print("g0z");
          Serial.print("g0z");
          addressI2C = 4; dataToI2C = 34; sendDataViaI2C();
        }
        if (inputString.charAt(2) =='1') {
          switch_G = true;
          Bluetooth.print("g1z");
          Serial.print("g1z");      
          addressI2C = 4; dataToI2C = 35; sendDataViaI2C();
        } 
      }
    
      // Switch H URB#1 J2
      if (inputString.charAt(1) =='h') { 
        if (inputString.charAt(2) =='0') {
          switch_H = false;
          Bluetooth.print("h0z");
          Serial.print("h0z");
          J2.write(0); delay(50);
          digitalWrite(JUNCTION_EN, HIGH);
          millisJunction = millis();
        }
        if (inputString.charAt(2) =='1') {
          switch_H = true;
          Bluetooth.print("h1z");
          Serial.print("h1z");      
          J2.write(180); delay(50);
          digitalWrite(JUNCTION_EN, HIGH);
          millisJunction = millis();
        } 
      }
    
      // Switch I URB#2 J3
      if (inputString.charAt(1) =='i') { 
        if (inputString.charAt(2) =='0') {
          switch_I = false;
          Bluetooth.print("i0z");
          Serial.print("i0z");
          addressI2C = 2; dataToI2C = 34; sendDataViaI2C();
        }
        if (inputString.charAt(2) =='1') {
          switch_I = true;
          Bluetooth.print("i1z");
          Serial.print("i1z");      
          addressI2C = 2; dataToI2C = 35; sendDataViaI2C();
        } 
      }
    
      // Switch J URB#5 J1
      if (inputString.charAt(1) =='j') { 
        if (inputString.charAt(2) =='0') {
          switch_J = false;
          Bluetooth.print("j0z");
          Serial.print("j0z");
          addressI2C = 5; dataToI2C = 30; sendDataViaI2C();
        }
        if (inputString.charAt(2) =='1') {
          switch_J = true;
          Bluetooth.print("j1z");
          Serial.print("j1z");      
          addressI2C = 5; dataToI2C = 31; sendDataViaI2C();
        } 
      }
    
      // Switch K URB#5 J3
      if (inputString.charAt(1) =='k') { 
        if (inputString.charAt(2) =='0') {
          switch_K = false;
          Bluetooth.print("k0z");
          Serial.print("k0z");
          addressI2C = 5; dataToI2C = 34; sendDataViaI2C();
        }
        if (inputString.charAt(2) =='1') {
          switch_K = true;
          Bluetooth.print("k1z");
          Serial.print("k1z");      
          addressI2C = 5; dataToI2C = 35; sendDataViaI2C();
        } 
      }
      
      // Switch L URB#5 J2
      if (inputString.charAt(1) =='l') { 
        if (inputString.charAt(2) =='0') {
          switch_L = false;
          Bluetooth.print("l0z");
          Serial.print("l0z");
          addressI2C = 5; dataToI2C = 32; sendDataViaI2C();
        }
        if (inputString.charAt(2) =='1') {
          switch_L = true;
          Bluetooth.print("l1z");
          Serial.print("l1z");      
          addressI2C = 5; dataToI2C = 33; sendDataViaI2C();
        } 
      }
    
      delay(150);
      flag_change_junc = true;
    }  
    

    And again, do not be frightened by this long code. It's easier, I took a snippet and just copied the fragment by number of junctions. It was possible to make it a cycle, but our principle is lucidness, so cycles and other complex constructions will be in another section. Here everything is simple – the command to switch the junction and change the corresponding Boolean variable. For local URB just follow the rule:
    Switch X state "false" → I2C command "30" → J1 on Local URB → Branch
    Switch X state "true" → I2C command "31" → J1 on Local URB → Throw
    Switch X state "false" → I2C command "32" → J2 on Local URB → Branch
    Switch X state "true" → I2C command "33" → J2 on Local URB → Throw
    Switch X state "false" → I2C command "34" → J3 on Local URB → Branch
    Switch X state "true" → I2C command "35" → J3 on Local URB → Throw
    If you use this rule and strictly sequentially alphabetically describe the management of junctions, you will never go wrong.


  • void controlLights() {
      // OFF	
      if (inputString.charAt(2) =='0') { 
        switch (inputString.charAt(1)) {
        case '-': dataToI2C = 100; // ALL OFF
          addressI2C = 2; sendDataViaI2C(); 
          delay(150);
          addressI2C = 3; sendDataViaI2C(); 
        break; 
        case 'a': dataToI2C = 110; // OFF Channel 1
          addressI2C = 2; sendDataViaI2C();   
        break; 
        case 'b': dataToI2C = 112; // OFF Channel 2
          addressI2C = 2; sendDataViaI2C();
        break; 
        case 'c': dataToI2C = 114; // OFF Channel 3
          addressI2C = 3; sendDataViaI2C();
        break; 
        case 'd': dataToI2C = 116; // OFF Channel 4
          addressI2C = 3; sendDataViaI2C();    
        break; 
        case 'e': dataToI2C = 118; // OFF Channel 5
          addressI2C = 3; sendDataViaI2C();    
        break; 
        case 'f': dataToI2C = 120; // OFF Channel 6 
          addressI2C = 3; sendDataViaI2C();     
        break;               
        case 'g': dataToI2C = 122; // OFF Channel 7
          addressI2C = 3; sendDataViaI2C();    
        break; 
        case 'h': dataToI2C = 124; // OFF Channel 8
          addressI2C = 3; sendDataViaI2C();    
        break; 
        case 'i': dataToI2C = 126; // OFF Channel 9
          addressI2C = 3; sendDataViaI2C();    
        break; 
        }
      }  	
    
      // ON	
      if (inputString.charAt(2) =='1') { 
        switch (inputString.charAt(1)) {
        case '-': dataToI2C = 101; // ALL ON
          addressI2C = 2; sendDataViaI2C(); 
          delay(150);
          addressI2C = 3; sendDataViaI2C(); 
        break; 
        case 'a': dataToI2C = 111; // ON Channel 1
          addressI2C = 2; sendDataViaI2C(); 
        break; 
        case 'b': dataToI2C = 113; // ON Channel 2
          addressI2C = 2; sendDataViaI2C(); 
        break; 
        case 'c': dataToI2C = 115; // ON Channel 3
          addressI2C = 3; sendDataViaI2C(); 
        break; 
        case 'd': dataToI2C = 117; // ON Channel 4
          addressI2C = 3; sendDataViaI2C(); 
        break; 
        case 'e': dataToI2C = 119; // ON Channel 5
          addressI2C = 3; sendDataViaI2C(); 
        break; 
        case 'f': dataToI2C = 121; // ON Channel 6   
          addressI2C = 3; sendDataViaI2C(); 
        break;               
        case 'g': dataToI2C = 123; // ON Channel 7
          addressI2C = 3; sendDataViaI2C(); 
        break; 
        case 'h': dataToI2C = 125; // ON Channel 8
          addressI2C = 3; sendDataViaI2C(); 
        break; 
        case 'i': dataToI2C = 127; // ON Channel 9
          addressI2C = 2; sendDataViaI2C(); 
        break; 
        }
      } 
    
      // RANDOM
      if (inputString.charAt(1) =='r') {
        if (inputString.charAt(2) =='1') { // Random ON
          dataToI2C = 201; 
          addressI2C = 2; sendDataViaI2C(); 
          delay(150);
          addressI2C = 3; sendDataViaI2C();
          randomLight = true;
        }   
        if (inputString.charAt(2) =='0') { // Random OFF
          dataToI2C = 200;
          addressI2C = 2; sendDataViaI2C(); 
          delay(150);
          addressI2C = 3; sendDataViaI2C(); 
          randomLight = false;           
        }
      }   
     
      dataToI2C = 0; 
    } 
    

    And in this function the same principles as principles all URB project. In this case, lighting management from local URB#2 and URB#3.


  • void scriptsAndSettings() {
      // Scripts
      if (inputString.charAt(1) =='0') {
        if (inputString.charAt(2) =='1') { // Button 1
          // Write automate code there... 	
        }
        if (inputString.charAt(2) =='2') { // Button 2
          // Write automate code there... 	
        }    
      }
      // Settings
      if (inputString.charAt(1) =='j') {
        if (inputString.charAt(2) =='1') { // Arm mode junctions - ON
          dataToI2C = 211;
          addressI2C = 2; sendDataViaI2C(); delay(150);
          addressI2C = 3; sendDataViaI2C(); delay(150);
          addressI2C = 4; sendDataViaI2C(); delay(150);
          addressI2C = 5; sendDataViaI2C(); junctionARM = true;                  
          
        }  
        if (inputString.charAt(2) =='0') { // Arm mode junctions - OFF 
          dataToI2C = 210;
          addressI2C = 2; sendDataViaI2C(); delay(150);
          addressI2C = 3; sendDataViaI2C(); delay(150);
          addressI2C = 4; sendDataViaI2C(); delay(150);
          addressI2C = 5; sendDataViaI2C(); junctionARM = false; 
        }  
      }
      if (inputString.charAt(1) =='s') {
        if (inputString.charAt(2) =='1') sensorsEnable = true; // Sensors ON
        if (inputString.charAt(2) =='0') sensorsEnable = false; // Sensors OFF   
      }
    }
    

Local URB#2

In the sketch of this local unit, there are several interesting techniques.

Firstly, if you remember, I was writing on local logic processing. A relay is connected to this unit, which, depending on the position of junctions B and I, turns on the power on the paths, respectively, A1 and T1, as well as the signal. That is, in the main logic block on the Communication station, we exclude the processing of these devices.

Secondly there is a sensor AWS. And this is an example of its application.

Thirdly, the connection 2 channels of lighting with the duplication of the outputs of the ULN2003 chip. This mode is convenient for switching on incandescent lamps, which consume much more current, in comparison with LED lighting.

And there are 5 free pins that you can use at your discretion.


  • #include <Wire.h>
    #include <Servo.h>
    
    void(* resetFunc) (void) = 0;
    
    // I/O PINS
    #define LIGHT_1_A 2
    #define LIGHT_1_B 4
    #define LIGHT_2_A 3
    #define LIGHT_2_B 6
    #define JUNCTION_EN 8
    Servo J1;
    Servo J2;
    Servo J3;
    #define RELAY_T1 12
    #define RELAY_A1 13
    #define SIGNAL_A1_GREEN 14
    #define SIGNAL_A1_RED 15
    #define SENSOR_A1 17
    
    
    // VARIABLES
    unsigned long millisJunction;
    byte dataToI2C, dataFromI2C;
    boolean randomLight, junctionARM;
    
      // JUNCTIONS & SET DEFAULT POSITIONS
      bool switch_A = true, switch_B = true, switch_I = true;
      // CHECK CHANGES
      bool flag_change_junc;
    
    void setup() {
    
    // Initialize I2C
      Wire.begin(2);
      Wire.onReceive(receiveI2C);
      Wire.onRequest(requestI2C);
    
    // Initialize Serial (for debugging)
      Serial.begin(9600);
    
    // Initialize Servos
      pinMode(JUNCTION_EN, OUTPUT);
      pinMode(9, OUTPUT);
      pinMode(10, OUTPUT);
      pinMode(11, OUTPUT);      
      J1.attach(9); J2.attach(10); J3.attach(11);
    
    // Initialize lights
      pinMode(LIGHT_1_A, OUTPUT);
      pinMode(LIGHT_1_B, OUTPUT);
      pinMode(LIGHT_2_A, OUTPUT);
      pinMode(LIGHT_2_B, OUTPUT);
    
    // Initialize signals
      pinMode(SIGNAL_A1_RED, OUTPUT);
      pinMode(SIGNAL_A1_GREEN, OUTPUT);
    
    // Initialize relay
      pinMode(RELAY_T1, OUTPUT);
      pinMode(RELAY_A1, OUTPUT);
    
    // Initialize sensor
      pinMode(SENSOR_A1, INPUT);
    
    // DEFAULT POSITION
       digitalWrite(RELAY_A1, HIGH); // LINE A1 OFF
       digitalWrite(SIGNAL_A1_RED, HIGH);
       digitalWrite(SIGNAL_A1_GREEN, LOW);
       J1.write(180); J2.write(0); J3.write(180);
       delay(50);
       digitalWrite(JUNCTION_EN, HIGH);
       delay(600);
       digitalWrite(JUNCTION_EN, LOW);
    }
    
    void loop() {
      // COMMAND PARSING
      if (dataFromI2C != 0) {
        switch (dataFromI2C) {
    
        // RESET
        case 99: resetFunc(); break;
    
        // J1
        case 30: J1.write(180);
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             switch_A = false;
             flag_change_junc = true;
             break;
    
        case 31: J1.write(0);
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             switch_A = true;
             flag_change_junc = true;
             break;
    
        // J2
        case 32: J2.write(180);
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             switch_B = false;
             flag_change_junc = true;
             break;
    
        case 33: J2.write(0);
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             switch_B = true;
             flag_change_junc = true;
             break;
    
        // J3
        case 34: J3.write(180);
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             switch_I = false;
             flag_change_junc = true;
             break;
    
        case 35: J3.write(0);
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             switch_I = true;
             flag_change_junc = true;
             break;
    
        // ALL LIGHTS
        case 100: // OFF
             digitalWrite(LIGHT_1_A, LOW);
             digitalWrite(LIGHT_1_B, LOW);
             digitalWrite(LIGHT_2_A, LOW);
             digitalWrite(LIGHT_2_B, LOW);
             break;
        case 101: // ON
             digitalWrite(LIGHT_1_A, HIGH);
             digitalWrite(LIGHT_1_B, HIGH);
             digitalWrite(LIGHT_2_A, HIGH);
             digitalWrite(LIGHT_2_B, HIGH);
             break;
    
        // LIGHT CANNEL 1
        case 110: // OFF
             digitalWrite(LIGHT_1_A, LOW);
             digitalWrite(LIGHT_1_B, LOW);
             break;
        case 111: // ON
             digitalWrite(LIGHT_1_A, HIGH);
             digitalWrite(LIGHT_1_B, HIGH);
             break;
    
        // LIGHT CANNEL 2
        case 112: // OFF
             digitalWrite(LIGHT_2_A, LOW);
             digitalWrite(LIGHT_2_B, LOW);
             break;
        case 113: // ON
             digitalWrite(LIGHT_2_A, HIGH);
             digitalWrite(LIGHT_2_B, HIGH);
             break;
        }
    
      dataFromI2C = 0;
      }
    
    // ----  MAIN BLOCK
      if (flag_change_junc) {
    
        if (switch_B) {
          digitalWrite(RELAY_A1, HIGH); // LINE A1 OFF
          digitalWrite(SIGNAL_A1_RED, HIGH);
          digitalWrite(SIGNAL_A1_GREEN, LOW);
        }
        else {
          digitalWrite(RELAY_A1, LOW); // LINE A1 ENABLE
          digitalWrite(SIGNAL_A1_RED, LOW);
          digitalWrite(SIGNAL_A1_GREEN, HIGH);      
        }
    
        if (switch_I) digitalWrite(RELAY_T1, LOW); // LINE T1 ENABLE
        else digitalWrite(RELAY_T1, HIGH); // LINE T1 OFF
    
    
        flag_change_junc = false;
      }
    
    // ----  SENSOR (LOW - TRIGGERED)
    
      if (digitalRead(SENSOR_A1) == LOW) dataToI2C = 1;
      else dataToI2C = 0;
    
    
      if (millis() > (millisJunction + 600)) digitalWrite(JUNCTION_EN, LOW);
    }
    
    // ----------- FUNCTIONS ----------- //
    
    void receiveI2C(int howMany) {
      while (Wire.available() > 0) {
        dataFromI2C = Wire.read();
      }
    }
    
    void requestI2C() {
      Wire.write(dataToI2C);
    }
    

Local URB#3

It was possible not to use this URB unit. But this is a good example of adding an additional module to the finished layout, for example when updating it with the addition of new buildings. It also shows the use of external power supply 12 volts for standard LED stripes.


  • #include <Wire.h>
    
    void(* resetFunc) (void) = 0;
    
    // I/O PINS
    #define LIGHT_3 2
    #define LIGHT_4 3
    #define LIGHT_5 4
    #define LIGHT_6 5
    #define LIGHT_7 6
    #define LIGHT_8 7
    #define LIGHT_9 8
    
    // VARIABLES
    byte dataFromI2C;
    bool randomLight;
    unsigned long now;
    unsigned long loopTime;
    int rnd = 1;
    
    void setup() {
    
    // Initialize I2C
      Wire.begin(3);
      Wire.onReceive(receiveI2C);
    
    // Initialize Serial (for debugging)
      Serial.begin(9600);
    
    // Initialize lights
      pinMode(LIGHT_3, OUTPUT);
      pinMode(LIGHT_4, OUTPUT);
      pinMode(LIGHT_5, OUTPUT);
      pinMode(LIGHT_6, OUTPUT);
      pinMode(LIGHT_7, OUTPUT);
      pinMode(LIGHT_8, OUTPUT);
    
    // DEFAULT POSITION
       digitalWrite(LIGHT_3, LOW);
       digitalWrite(LIGHT_4, LOW);
       digitalWrite(LIGHT_5, LOW);
       digitalWrite(LIGHT_6, LOW);
       digitalWrite(LIGHT_7, LOW);
       digitalWrite(LIGHT_8, LOW);
    
       now = millis();
       loopTime = now;   
    }
    
    void loop() {
      // COMMAND PARSING
      if (dataFromI2C != 0) {
        switch (dataFromI2C) {
    
        // RESET
        case 99: resetFunc(); break;
    
        // ALL LIGHTS
        case 100: // OFF
             digitalWrite(LIGHT_3, LOW);
             delay(200);
             digitalWrite(LIGHT_4, LOW);
             digitalWrite(LIGHT_5, LOW);
             delay(250);
             digitalWrite(LIGHT_6, LOW);
             delay(350);         
             digitalWrite(LIGHT_7, LOW);
             delay(100);          
             digitalWrite(LIGHT_8, LOW);
             break;
        case 101: // ON
             digitalWrite(LIGHT_3, HIGH);
             digitalWrite(LIGHT_4, HIGH);
             digitalWrite(LIGHT_5, HIGH);
             delay(450);         
             digitalWrite(LIGHT_6, HIGH);
             digitalWrite(LIGHT_7, HIGH);
             delay(150);         
             digitalWrite(LIGHT_8, HIGH);
             break;
    
        // LIGHT CANNEL 3
        case 114: // OFF
             digitalWrite(LIGHT_3, LOW);
             break;
        case 115: // ON
             digitalWrite(LIGHT_3, HIGH);
             break;
    
        // LIGHT CANNEL 4
        case 116: // OFF
             digitalWrite(LIGHT_4, LOW);
             break;
        case 117: // ON
             digitalWrite(LIGHT_4, HIGH);
             break;
    
        // LIGHT CANNEL 5
        case 118: // OFF
             digitalWrite(LIGHT_5, LOW);
             break;
        case 119: // ON
             digitalWrite(LIGHT_5, HIGH);
             break;
    
        // LIGHT CANNEL 6
        case 120: // OFF
             digitalWrite(LIGHT_6, LOW);
             break;
        case 121: // ON
             digitalWrite(LIGHT_6, HIGH);
             break;
    
        // LIGHT CANNEL 7
        case 122: // OFF
             digitalWrite(LIGHT_7, LOW);
             break;
        case 123: // ON
             digitalWrite(LIGHT_7, HIGH);
             break;
    
        // LIGHT CANNEL 8
        case 124: // OFF
             digitalWrite(LIGHT_8, LOW);
             break;
        case 125: // ON
             digitalWrite(LIGHT_8, HIGH);
             break;
    
        // LIGHT CANNEL 9
        case 126: // OFF
             digitalWrite(LIGHT_9, LOW);
             break;
        case 127: // ON
             digitalWrite(LIGHT_9, HIGH);
             break;                                    
    
        case 201: // Random ON
             randomLight = true;
             break;
        case 200: // Random OFF
             randomLight = false;
             break;         
    
        dataFromI2C = 0;
        }
      }  
    
    // ----  MAIN BLOCK
      if (randomLight) {
        if (now >= (loopTime + 500 * rnd)) {
          
          if (rnd == 1) digitalWrite(LIGHT_3, HIGH);
          if (rnd == 2) digitalWrite(LIGHT_4, HIGH);
          if (rnd == 3) digitalWrite(LIGHT_7, HIGH);
          if (rnd == 4) {
            digitalWrite(LIGHT_3, HIGH);
            digitalWrite(LIGHT_4, HIGH);
            digitalWrite(LIGHT_7, HIGH);
          } 
    
          loopTime = now;
          rnd = random(1, 5);
        }
      }
    }
    
    // ----------- FUNCTIONS ----------- //
    
    void receiveI2C(int howMany) {
      while (Wire.available() > 0) {
        dataFromI2C = Wire.read();
      }
    }
    

    This URB unit manages only lighting, sometimes it is very convenient and allows you to use the operator DELAY. In this sketch, also have the function RANDOME – implemented lighting randomly and pay attention to the delays of all channels with a general on/off. In the extended part of the site there are snippets with FADE-IN/FADE-OUT effects.

Local URB#4

A typical sketch for a local URB unit. A small recommendation – do not save on number of URB units (it is very cheap), do not try to use all the outputs of Arduino. Better add one more local unit. This approach greatly simplifies programming and error detection.


  • #include <Wire.h>
    #include <Servo.h>
    
    void(* resetFunc) (void) = 0;
    
    // I/O PINS
    #define JUNCTION_EN 8
    Servo J1;
    Servo J2;
    Servo J3;
    #define SIGNAL_A3_GREEN 14
    #define SIGNAL_A3_RED 15
    #define SIGNAL_A2_GREEN 16
    #define SIGNAL_A2_RED 17
    
    
    // VARIABLES
    unsigned long millisJunction;
    byte dataFromI2C;
    boolean junctionARM;
    
      // JUNCTIONS & SET DEFAULT POSITIONS
      bool switch_E = true, switch_G = true;
      // CHECK CHANGES
      bool flag_change_junc;
    
    void setup() {
    
    // Initialize I2C
      Wire.begin(4);
      Wire.onReceive(receiveI2C);
    
    // Initialize Serial (for debugging)
      Serial.begin(9600);
    
    // Initialize Servos
      pinMode(JUNCTION_EN, OUTPUT);
      pinMode(9, OUTPUT);
      pinMode(10, OUTPUT);
      pinMode(11, OUTPUT);      
      J1.attach(9); J2.attach(10); J3.attach(11);
    
    // Initialize signals
      pinMode(SIGNAL_A2_RED, OUTPUT);
      pinMode(SIGNAL_A2_GREEN, OUTPUT);
      pinMode(SIGNAL_A3_RED, OUTPUT);
      pinMode(SIGNAL_A3_GREEN, OUTPUT);  
    
    // DEFAULT POSITION
       digitalWrite(SIGNAL_A2_RED, HIGH);
       digitalWrite(SIGNAL_A2_GREEN, LOW);
       digitalWrite(SIGNAL_A3_RED, LOW);
       digitalWrite(SIGNAL_A3_GREEN, HIGH);   
       J1.write(180); J2.write(0); J3.write(180);
       delay(50);
       digitalWrite(JUNCTION_EN, HIGH);
       delay(600);
       digitalWrite(JUNCTION_EN, LOW);
    }
    
    void loop() {
      // COMMAND PARSING
      if (dataFromI2C != 0) {
        switch (dataFromI2C) {
    
        // RESET
        case 99: resetFunc(); break;
    
        // J1
        case 30: J1.write(180);
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             switch_E = false;
             flag_change_junc = true;
             break;
    
        case 31: J1.write(0);
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             switch_E = true;
             flag_change_junc = true;
             break;
    
        // J2
        case 32: J2.write(180);
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             break;
    
        case 33: J2.write(0);
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             break;
    
        // J3
        case 34: J3.write(180);
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             switch_G = false;
             flag_change_junc = true;
             break;
    
        case 35: J3.write(0);
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             switch_G = true;
             flag_change_junc = true;
             break;
        }
    
      dataFromI2C = 0;
      }
    
    // ----  MAIN BLOCK
      if (flag_change_junc) {
    
        if (switch_G) {
    
          if (switch_E) {
            digitalWrite(SIGNAL_A2_RED, HIGH);
            digitalWrite(SIGNAL_A2_GREEN, LOW);
            digitalWrite(SIGNAL_A3_RED, LOW);
            digitalWrite(SIGNAL_A3_GREEN, HIGH);
          }
          else {
            digitalWrite(SIGNAL_A2_RED, LOW);
            digitalWrite(SIGNAL_A2_GREEN, HIGH);
            digitalWrite(SIGNAL_A3_RED, HIGH);
            digitalWrite(SIGNAL_A3_GREEN, LOW);        
          }          
        }
        else {
          digitalWrite(SIGNAL_A2_RED, HIGH);
          digitalWrite(SIGNAL_A2_GREEN, LOW);
          digitalWrite(SIGNAL_A3_RED, HIGH);
          digitalWrite(SIGNAL_A3_GREEN, LOW);      
        }
    
        flag_change_junc = false;
      }
    
      if (millis() > (millisJunction + 600)) digitalWrite(JUNCTION_EN, LOW);
    }
    
    // ----------- FUNCTIONS ----------- //
    
    void receiveI2C(int howMany) {
      while (Wire.available() > 0) {
        dataFromI2C = Wire.read();
      }
    }
    

    I specifically connected to this URB unit a minimum of external devices to demonstrate the local algorithm of signals.

Local URB#5

Since the relay unit and signals depends on the algorithm performed by the Communication Station, this URB unit not operates logic locally. Signals are connected in the same way as on URB#4

As you noticed from the figures, it is not necessary to install all the components and connectors to the URB board, it is necessary to solder only the components necessary for a particular case.


  • #include <Wire.h>
    #include <Servo.h>
    
    void(* resetFunc) (void) = 0;
    
    // I/O PINS
    #define RELAY_A2 2
    #define RELAY_A3 3
    #define RELAY_POWER_TRANS 4
    #define RELAY_POWER_TRANS_ 5
    #define RELAY_TRANSITION 6
    #define RELAY_B1 7
    #define JUNCTION_EN 8
    Servo J1;
    Servo J2;
    Servo J3;
    #define RELAY_B2 12
    #define RELAY_B3 13
    #define SIGNAL_B2_GREEN 14
    #define SIGNAL_B2_RED 15
    #define SIGNAL_B3_GREEN 16
    #define SIGNAL_B3_RED 17
    
    
    // VARIABLES
    unsigned long millisJunction;
    byte dataFromI2C;
    boolean junctionARM;
    
      // JUNCTIONS & SET DEFAULT POSITIONS
      bool switch_J = true, switch_L = true, switch_K = true;
      
      // CHECK CHANGES
      bool flag_change_junc;
    
    void setup() {
    
    // Initialize I2C
      Wire.begin(5);
      Wire.onReceive(receiveI2C);
    
    // Initialize Serial (for debugging)
      Serial.begin(9600);
    
    // Initialize Servos
      pinMode(JUNCTION_EN, OUTPUT);
      pinMode(9, OUTPUT);
      pinMode(10, OUTPUT);
      pinMode(11, OUTPUT);      
      J1.attach(9); J2.attach(10); J3.attach(11);
    
    // Initialize relays
      pinMode(RELAY_A2, OUTPUT);
      pinMode(RELAY_A3, OUTPUT);
      pinMode(RELAY_POWER_TRANS, OUTPUT);
      pinMode(RELAY_POWER_TRANS_, OUTPUT);
      pinMode(RELAY_TRANSITION, OUTPUT);
      pinMode(RELAY_B1, OUTPUT);
      pinMode(RELAY_B2, OUTPUT);
      pinMode(RELAY_B3, OUTPUT);     
    
    // Initialize signals
      pinMode(SIGNAL_B2_RED, OUTPUT);
      pinMode(SIGNAL_B2_GREEN, OUTPUT);
      pinMode(SIGNAL_B3_RED, OUTPUT);
      pinMode(SIGNAL_B3_GREEN, OUTPUT);  
    
    // DEFAULT POSITION
       digitalWrite(SIGNAL_B2_RED, HIGH);
       digitalWrite(SIGNAL_B2_GREEN, LOW);
       digitalWrite(SIGNAL_B3_RED, HIGH);
       digitalWrite(SIGNAL_B3_GREEN, LOW);   
       J1.write(180); J2.write(0); J3.write(180);
       delay(50);
       digitalWrite(JUNCTION_EN, HIGH);
       delay(600);
       digitalWrite(JUNCTION_EN, LOW);
    
       digitalWrite(RELAY_A2, HIGH); // OFF
       digitalWrite(RELAY_A3, LOW); // ON
       digitalWrite(RELAY_POWER_TRANS, LOW);  // POWER 
       digitalWrite(RELAY_POWER_TRANS_, LOW); // FROM LINE A
       digitalWrite(RELAY_TRANSITION, HIGH); // OFF
       digitalWrite(RELAY_B1, LOW); // ON
       digitalWrite(RELAY_B2, HIGH); // OFF
       digitalWrite(RELAY_B3, HIGH); // OFF   
    }
    
    void loop() {
      // COMMAND PARSING
      if (dataFromI2C != 0) {
        switch (dataFromI2C) {
    
        // RESET
        case 99: resetFunc(); break;
    
        // J1
        case 30: J1.write(180);
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             switch_J = false;
             flag_change_junc = true;
             break;
    
        case 31: J1.write(0);
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             switch_J = true;
             flag_change_junc = true;
             break;
    
        // J2
        case 32: J2.write(180);
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             switch_L = true;
             flag_change_junc = true;         
             break;
    
        case 33: J2.write(0);
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             switch_L = true;
             flag_change_junc = true;         
             break;
    
        // J3
        case 34: J3.write(180);
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             switch_K = false;
             flag_change_junc = true;
             break;
    
        case 35: J3.write(0);
             delay(50);
             digitalWrite(JUNCTION_EN, HIGH);
             millisJunction = millis();
             switch_K = true;
             flag_change_junc = true;
             break;
    
        case 50: // POWER FROM B
             digitalWrite(RELAY_POWER_TRANS, HIGH);
             digitalWrite(RELAY_POWER_TRANS_, HIGH);
             break;
    
        case 51: // POWER FROM A
             digitalWrite(RELAY_POWER_TRANS, LOW);
             digitalWrite(RELAY_POWER_TRANS_, LOW);
             break;
    
        case 52: // PATH A2 ENABLE
             digitalWrite(RELAY_A2, LOW); // ON
             digitalWrite(RELAY_A3, HIGH); // OFF
             digitalWrite(RELAY_TRANSITION, HIGH); // OFF         
             break;
    
        case 53: // PATH A3 ENABLE
             digitalWrite(RELAY_A2, HIGH); // OFF
             digitalWrite(RELAY_A3, LOW); // ON
             digitalWrite(RELAY_TRANSITION, HIGH); // OFF  
             break;   
    
        case 54: // TRANSITION ENABLE FROM LINE A
             digitalWrite(RELAY_A2, HIGH); // OFF
             digitalWrite(RELAY_A3, HIGH); // OFF
             digitalWrite(RELAY_TRANSITION, LOW); // ON 
             break;  
    
        case 55: // TRANSITION ENABLE FROM LINE B
             digitalWrite(RELAY_TRANSITION, LOW); // ON
             digitalWrite(RELAY_B1, HIGH); // OFF
             break; 
    
        case 56: // PATH B1 ENABLE
             digitalWrite(RELAY_TRANSITION, HIGH); // OFF
             digitalWrite(RELAY_B1, LOW); // ON
             break;                  
        }
    
      dataFromI2C = 0;
      }
    
    // ----  MAIN BLOCK
      if (flag_change_junc) {
    
        // SIGNALS & LOCAL RELAYS
        if (!switch_L) {
    
          if (switch_J) {
            digitalWrite(SIGNAL_B2_RED, HIGH);
            digitalWrite(SIGNAL_B2_GREEN, LOW);
            digitalWrite(SIGNAL_B3_RED, LOW);
            digitalWrite(SIGNAL_B3_GREEN, HIGH);
            digitalWrite(RELAY_B2, HIGH); // OFF
            digitalWrite(RELAY_B3, LOW); // ON               
          }
          else {
            digitalWrite(SIGNAL_B2_RED, LOW);
            digitalWrite(SIGNAL_B2_GREEN, HIGH);
            digitalWrite(SIGNAL_B3_RED, HIGH);
            digitalWrite(SIGNAL_B3_GREEN, LOW);
            digitalWrite(RELAY_B2, LOW); // ON
            digitalWrite(RELAY_B3, HIGH); // OFF                 
          }          
        }
        else {
          digitalWrite(SIGNAL_B2_RED, HIGH);
          digitalWrite(SIGNAL_B2_GREEN, LOW);
          digitalWrite(SIGNAL_B3_RED, HIGH);
          digitalWrite(SIGNAL_B3_GREEN, LOW); 
          digitalWrite(RELAY_B2, HIGH); // OFF
          digitalWrite(RELAY_B3, HIGH); // OFF            
        }
    
        flag_change_junc = false;
      }
     
      if (millis() > (millisJunction + 600)) digitalWrite(JUNCTION_EN, LOW);
    }
    
    // ----------- FUNCTIONS ----------- //
    
    void receiveI2C(int howMany) {
      while (Wire.available() > 0) {
        dataFromI2C = Wire.read();
      }
    }
    

    The general signal for swithcing of lines A2, A3, LINE TRANSITION and B1 is distributed to the end relays in the MAIN block. The rest of the sketch is similar to URB#4

 

 

All Layout Sketches

Conclusion

A few practical tips and trends in URB project development.

After successfully launched an experiments, even if you did simple layout with control over 4 servos and used two URB units, you already noticed some confusion. For example, typed the command: "jd0z" and the servo rotate to another angle instead of the expected one. Really the chain: the command on the serial port → its conversion to the byte-command and resend it via I2C → again processing already on the local URB → and at the end the rotation of the servo-drive – quite long way. And at every stage you can mix up something. So, the rule is the following: setting the final position of the servo (adjustments rotate angle) or the color of the signal should be made on the final URB unit to which they are connected.
In other words, you can build the correct logical model, but signals sent from the communication station cause an inverted response - instead of the expected red signal, is turned on the green signal. To fix this, you can change the state of the variable in the main logical block on the Communication station, or change the state of the outputs of the local URB (by changing the description of the outputs in the header of the sketch). I suggest, in order to avoid mistakes, always use the second method.

Therefore, the formalization of the sketch design process is necessary. Part of this process is provided by the Protocol 2. But we need to introduce a few more simple rules, and at the same time discuss the data types and variables used in the project. I repeat here the text given earlier. Boolean type is very convenient for devices on a layout with two states, for example on or off states. We will apply this type to the junction (TRUE or 1 — straight direction, FALSE or 0 — branch direction), two mode signals (TRUE or 1 — green signal, FALSE or 0 — red signal), relay (TRUE or 1 — closed contacts, FALSE or 0 — open contacts ) and etc. This type of data forms the basis for the logic of the layout behavior, using three types of comparison of Boolean values: AND, OR, NOT – a control algorithm is created.

Next type byte. This is the datatype for transfer by I2C of data and PWM commands of the locomotive speed in case the motor-driver is connected to the local URB unit. For data transfer via I2C, we introduce the following rules: the range of numbers from 30 to 50 is reserved for the control of junctions, the range of numbers from 100 to 199 for lighting control on layout, the range of numbers from 200 to 255 for controlling the motor driver in case it is connected to a local URB. If the number 99 is received, the local URB will be reseted. These are my recommendations, you are free to assign these numbers at your discretion.

I also have many other projects. The problem of their implementation – the absence of funds for development and tested. You can see them on the old version of the site and on popular aggregators such as Instructables, Hakster and so one. Gradually adding the functions of automation, interlocking and the independent movement of trains of your layout on a schedule, I’ll try their to add is to URB project.

3-way tonnel entrance DIY model railway signal

 

At the moment, several updates are already published in the site section for Donators. Also there is a sketch code that implements a variety of lighting effects and a full description of my layout, which you can see on the video. Detailed DIY-instructions for the manufacture of original lanterns, signals, building lighting systems, a line status display panel and much more, and these instructions will be supplemented. In the future, I will publish in this part ready-made turntable modules, barrier management systems and others. In the development is a funny idea of a mechanical portal, like portals in computers railway simulators (more in our group on Facebook).

If you have your own idea, and you want to develop it with my assistance, then you can also use Patreon website.