Automated shunting with the Z21 and Python

In this post I’ll introduce automated shunting using the Roco Z21 DCC Command Station in Python. Previous posts demonstrated how to connect to the Z21, receiving messages, send locomotive commands and receive feedback from the layout. Now we will discuss coupling and uncoupling wagons, and switching turnouts. While coupling doesn’t need any programming, we have to look into this to ensure smooth and reliable operation. For uncoupling and switching turnouts, we will introduce Z21 commands.

Shunting
Automated shunting: coupling, uncoupling and switching turnouts.

Automatic coupling

Strictly, coupling a locomotive to a wagon should be a non-event. With earlier experience of the standard N-scale couplings, I expected to just roll a locomotive softly against a stationary wagon. The wagon shouldn’t move if I stopped the locomotive in time, which is prototypical. After all, in real life loose wagons have the brake activated. I didn’t have such luck with the standard Roco couplers unfortunately. The locomotive would just push the wagon away without coupling.

An additional requirement I had, was closer and fixed coupling between wagons. I did some experiments with Märklin close couplers. These couplers worked for close and fixed coupling, but coupling a locomotive to a wagon still required quite a high speed and would move the wagon way too much.

Roco Universal Couplers
Roco Universal Couplers: they are big, but couple fairly closely. And they couple smoothly!

After recommendations online, I switched back to Roco and tested their Universal Couplers. They don’t couple as close as some others, but they couple very smoothly. Note that the two couplers need to be properly aligned. In practice this means that neither the locomotive and the wagon shall stand in a curve. This is especially important with the Roco GeoLine track that I use, since this program only have curves and turnouts with a short radius.

No coupling in a curve.
No coupling in a curve.

Automated uncoupling

The Roco Universal Coupling is very hard to uncouple by hand. The only way is to push up two pins from below. A package of Universal couplers contains a lever that makes manual uncoupling easier. Alternatively one could use an uncoupler track, as long as the constraint of a fixed position for decoupling is acceptable. Roco offers a manual version, but automatic operation requires an electrical uncouple rail, with a DCC decoder.

An advantage of Roco GeoLine is that the motor is embedded in the rail bed. The railroad track is responsible for the wiring and does not need any additional wiring. In the case of the uncoupler, there is no place for the decoder however. There is only space under the rail bed under an adjacent track. I chose to use a standard straight track of the same length for this and keep it permanently attached to the decoupler track. This also helps aligning that the locomotive with the wagon for coupling.

The electric uncoupler from Roco Geoline from below, with DCC-decoder
The electric uncoupler from Roco Geoline from below. The DCC-decoder was installed under an adjacent track.

Automated switching of turnouts

I use are standard left and right turnouts with electrical motors and decoders in the Roco Geoline. Like the uncoupler rail, the motors are installed in the rail bedding, but also the decoders fit there. Also here, we don’t need additional wiring: the rails provide the power and signals.

The branch radius is 502,7 mm and the branch angle is 22,5 degrees. Unfortunately the GeoLine program does not contain any turnouts with a more realistic branch radius and angle. Also, switching the turnouts is noisy and not very prototypical. But for our purpose, this is good enough.

Note: instead of buying all components separately, I purchased the extension sets 51250 and 51251 for getting a decent collection of rails, turnouts and uncouplers with all needed accessories, for a more reasonable price.

Roco GeoLine turnout with motor and DCC-decoder
A turnout from Roco GeoLine from underneath, with installed motor (black) and DCC-decoder (green). Power and signal are fed by the track, no wiring needed.

Z21 functions for automated shunting

Let’s see which commands and responses the Z21 defines for shunting. We’ll use the same functions for turnouts and uncoupling rails.

LAN_X_GET_TURNOUT_INFO

The first command asks for the status of a turnout: LAN_X_GET_TURNOUT_INFO. The format of the message is:

  1. DataLen (2 bytes little endian): 0x08, 0x00 – the length of this message type is 8 bytes
  2. Header (2 bytes little endian): 0x40, 0x00 – this is an XPressNet message
  3. X-Header (1 byte): 0x43 – this is a turnout info message
  4. DB0: turnout address MSB
  5. DB1: turnout address LSB
  6. XOR-byte (integrity check for X-Header and all DB bytes)

The format of the address is MSB << 8 + LSB. In practice with only a handful of turnouts and uncouplers, we can use MSB = 0 and LSB = real address. Note that in the case of a Roco GeoLine decoder the actual address is one higher than the address specified. If we wish to receive the status from the turnout with address 1, we have to specify 0!

A Python function that creates such a command message could look like this:

def lanXGetTurnoutInfo(address): # 5.1 LAN_X_GET_TURNOUT_INFO
    if address < 1 or address > 255:
        return
    dataLen = 0x0008
    header = 0x0040
    xHeader = 0x43
    db0 = 0
    db1 = address - 1
    xor = xHeader ^ db0 ^ db1
    return \
        int.to_bytes(dataLen, 2, byteorder = 'little') + \
        int.to_bytes(header, 2, byteorder = 'little') + \
        int.to_bytes(xHeader, 1, byteorder = 'little') + \
        int.to_bytes(db0, 1, byteorder = 'little') + \
        int.to_bytes(db1, 1, byteorder = 'little') + \
        int.to_bytes(xor, 1, byteorder = 'little')

Use this function to retrieve the status for turnout with address 1 as follows:

# preparation:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
Z21 = ('192.168.0.111', 21105)

# send command:
s.sendto(lanXGetTurnoutInfo(1), Z21)

Note that this LAN_X_GET_TURNOUT_INFO only queries the turnout status. It does not switch a turnout or uncoupler.

LAN_X_TURNOUT_INFO

The response to a LAN_X_GET_TURNOUT_INFO is an incoming message of type LAN_X_TURNOUT_INFO. The format of this message type is:

  1. DataLen (2 bytes little endian): 0x09, 0x00 – the length of this message type is 9 bytes
  2. Header (2 bytes little endian): 0x40, 0x00 – this is an XPressNet message
  3. X-Header (1 byte): 0x43 – this is a turnout info message
  4. DB0: turnout address MSB
  5. DB1: turnout address LSB
  6. DB2: turnout status
  7. XOR-byte (integrity check for X-Header and all DB bytes)

The format of the address is the same as in LAN_GET_TURNOUT_INFO. The turnout status byte is 000000ZZ, where ZZ is:

  • 00 for a turnout that has not been switched yet
  • 01 for a turnout that is in mode P = 0 (position 1, branched)
  • 10 for a turnout that is in mode P = 1 (position 2, straight)
  • 11 is illegal and shall never happen.

A Python function that decodes this message could look like this:

def lanXTurnoutInfo(message): # 5.3 LAN_X_TURNOUT_INFO
    if message[2] != 0x40 or message[4] != 0x43:
        return
    db = message[5:]
    address = int.from_bytes(db[0:2], byteorder = 'big') + 1
    status = \
        'not set' if db[2] == 0 else \
        'branched' if db[2] == 1 else \
        'straight' if db[2] == 2 else \
        'error'
    return address, status

Note that the status has no meaning for an uncoupler.

LAN_X_SET_TURNOUT

For switching a turnout, use the LAN_X_SET_TURNOUT command . The format of this command is:

  1. DataLen (2 bytes little endian): 0x09, 0x00 – the length of this message type is 9 bytes
  2. Header (2 bytes little endian): 0x40, 0x00 – this is an XPressNet message
  3. X-Header (1 byte): 0x53 – this is a set turnout message
  4. DB0: turnout address MSB
  5. DB1: turnout address LSB
  6. DB2: required state
  7. XOR-byte (integrity check for X-Header and all DB bytes)

The format for the required state (DB2) is 10Q0A00P where:

  • Q = 0 means don’t use queue
  • Q = 1 means use queue
  • A = 0 means deactivate
  • A = 1 means activate
  • P = 0 means position 1
  • P = 1 means position 2

The queue is a new feature from Z21 firmware version 1.24. Without queuing, our program has to activate a switch, wait 100 milliseconds, deactivate the switch and wait another 50 milliseconds. This behaviour looks like if we would push a button manually. Also, we can’t set any other turnouts in this time period. With queuing, we can just set turnouts as we like and the Z21 itself takes care of timing and avoids mixing of commands for different turnouts. The only constraint is that we cannot mix Q = 0 and Q = 1 commands. I’d recommend to always use Q = 1.

Activating (A = 1) means we give a signal to the switch motor, like pushing a button. A = 0 means we would release the button. With the queuing enabled, we won’t have to send a separate ‘deactivate’ command.

P = 0, which is position 1, will set the turnout to position ‘branched’ while P = 1, position 2, will set the turnout to ‘straight.’

A Python function that creates a turnout command could look like this:

def lanXSetTurnout(address, pos): # 5.2 LAN_X_SET_TURNOUT
    if address < 1 or address > 255:
        return
    if pos != 'straight' and pos != 'branched':
        return
    dataLen = 0x0009
    header = 0x0040
    xHeader = 0x53
    db0 = 0
    db1 = address - 1
    # db2: 10Q0A00P where
    # Q = 1, A = 1, P = 0 (branched) or 1 (straight)
    db2 = 0b10101000 if pos == 'branched' else 0b10101001
    xor = xHeader ^ db0 ^ db1 ^ db2
    return \
        int.to_bytes(dataLen, 2, byteorder = 'little') + \
        int.to_bytes(header, 2, byteorder = 'little') + \
        int.to_bytes(xHeader, 1, byteorder = 'little') + \
        int.to_bytes(db0, 1, byteorder = 'little') + \
        int.to_bytes(db1, 1, byteorder = 'little') + \
        int.to_bytes(db2, 1, byteorder = 'little') + \
        int.to_bytes(xor, 1, byteorder = 'little')

A main program could use this function as follows:

# preparation:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
Z21 = ('192.168.0.111', 21105)

# send command:
s.sendto(lanXSetTurnout(1, 'straight'), Z21)

The same commands can be used for uncoupling. ‘Straight’ or ‘branched’ have no meaning, so either of them works.

Conclusion

With this, and all previous posts related to the Z21 Command Station, I have covered all the basics that we will need for more advanced, and more interesting explorations within model railroad control. The Z21 API contains many more functions, like for programming DCC decoders and Railcom. If there is interest, I might look into these functions as well. But these are not needed for the basic operations I plan to do within model railroad control. Instead I will focus on higher order experiments that will use the functions I have explored so far.

One Reply to “Automated shunting with the Z21 and Python”

  1. Did you consider using automatic couplings like these: http://www.krois-modell.at/produkt/digikupp/h0/
    Here’s a video which shows one of these couplings: https://www.youtube.com/watch?v=5Yp-0-Zu-ZE
    Some model railroad shops will change the couplings (and do the programming of the decoder), so you don’t need to do that for yourself.
    Additionally, Roco has locos with automatic couplings in their store: https://www.roco.cc/de/product/243666-2062-0-0-0-0-0-002003-1/products.html
    (It’s still not clear which coupling that will be (short or universal) – I’m waiting for that model, because I always wanted to have an ÖBB 2062.)

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.