Controlling a loc using the Roco Z21 and Python

In previous posts, I showed how to connect to, and receive locomotive status messages from the Roco Z21 DCC command station, using Python. In this post we will send controlling commands to a locomotive. These are both for driving and for using the special locomotive functions like lighting and sound.

Roco model number 72673: an NS 1204 locomotive with DCC sound decoder

First a few considerations

  • Direction: forward and backward depend on the orientation of the locomotive. In a traditional DC analog setup the polarity of the tracks determines the direction of a locomotive. The orientation of the locomotive has no meaning: with the same polarity, the locomotive will always drive in the same direction. However in a DCC setup, the direction depends on the orientation of the locomotive. If we put the locomotive the other way on the track, it will also go the other way. That means that a script will only work correctly if the locomotive is oriented as intended!
  • Speedsteps. DCC defines three numbers of speedsteps: 14, 28 and 128. The speed parameter the locomotive will drive is related to the number of speedsteps. A specified speed of 12 is almost full speed when the number of speedsteps is 14. However, it is almost half speed when the number if speedsteps is 28. 12 would be only 10% of the maximum speed, and hence very slow if the number of speedsteps is 128. Only the oldest DCC decoders in locomotives can only use 14 speed steps. According the official specification, this setting is now deprecated. 28 steps is probably still common and all modern DCC decoders should also be able to handle the newer 128 speedsteps setting. After the case study of the NS 1200 locomotive in real life, it should be obvious that we should use 128 speedsteps to get the highest granularity. A specified speed that is higher than the specified number of speedsteps is incorrect of course.
  • Realistic speed. DCC does not specify how the different speed values are related to real world speeds. Neither does it define the top speed of a model locomotive which is often way too high. When using model trains as toys they tend to drive way too fast. In real life though, the maximum speed of a locomotive is often lower. And most track sections have a much lower maximum speed than the top speed of the locomotive.
  • Gradual acceleration and deceleration. In the case study of the NS 1200 we see that a heavy train accelerates very gradually. Also the braking capacity of a train is limited compared to e.g. road traffic. DCC decoders have configuration variables that simulate the momentum of trains when accelerating and decelerating. One can increase the values of these variables. However, I’d prefer to keep a controlled acceleration in control of my Python script.
  • No feedback: a Python script is essentially blind. It isn’t aware of obstacles on the railroad track, like another locomotive of wagon, a switch or the end of a dead-end track. If we see an obstacle, the locomotive will not stop! We’ll explore proper feedback solutions later. For now, make sure to only have a safe layout, like a round or oval layout . Or have your hand very close to the emergency stop button on the Z21!

Driving

Armed with these considerations we can start driving. The LAN_X_SET_LOCO_DRIVE command (see section 4.2 in the protocol document) exists for sending drive commands to a locomotive. The format of this command is:

  1. DataLen (2 bytes little endian): 0x0A, 0x00
  2. Header (2 bytes little endian): 0x40, 0x00
  3. Data (32 bits little endian).

The Data section is built up as follows:

  1. X-Header: 0xE4
  2. DB0: 0x1S, where S=0 for 14 speedsteps, 2 for 28 speedsteps and 3 for 128 speedsteps
  3. DB1: Adr_MSB (locomotive address, most significant byte)
  4. DB2: Adr_LSB (locomotive address, least significant byte)
  5. DB3: RVVVVVVV. R is the bit for direction (1 = forward, 0 = backward). The remaining 7 bits are for the speed
  6. XOR-byte (integrity check for X-Header and all DB bytes)

A function in Python for formatting and sending such a command could look like this:

def lanXSetLocoDrive(address, direction, res, speed): # 4.2 LAN_X_SET_LOCO_DRIVE
     # direction: 'forward' or 'backward'
     # res: 14, 28 or 128 steps (resolution)
     # speed: 0 - 13/27/127 (depending on res)
     dataLen = int.to_bytes(0x000A, 2, byteorder = 'little')
     header = int.to_bytes(0x0040, 2, byteorder = 'little')
     xHeader = int.to_bytes(0xE4, 1, byteorder = 'little')
     if res == 14:
         binres = 16
     elif res == 28:
         binres = 18
     else:
         binres = 19
     db0 = int.to_bytes(binres, 1, byteorder = 'little')
     db1 = int.to_bytes(0, 1, byteorder = 'little')
     db2 = int.to_bytes(address, 1, byteorder = 'little')
     # merge direction into speed: overrule first bit depending on direction:
     if direction == 'forward':
         speed = speed | 0b10000000 # set bit 1
     else:
         speed = speed & ~0b10000000 # clear bit 1
     db3 = int.to_bytes(speed, 1, byteorder = 'little')
     xor = 0x00E4 ^ binres ^ 0 ^ address ^ speed
     xorByte = int.to_bytes(xor, 1, byteorder = 'little')
     data = xHeader + db0 + db1 + db2 + db3 + xorByte
     command = dataLen + header + data
     s.sendto(command, Z21)

Locomotive functions

Similarly, use the LAN_X_SET_LOCO_FUNCTION command (see section 4.3 in the protocol document) for switching the locomotive functions. The format of this command is:

  1. DataLen (2 bytes little endian): 0x0A, 0x00
  2. Header (2 bytes little endian): 0x40, 0x00
  3. Data (32 bits little endian).

The Data section is built up as follows:

  1. X-Header: 0xE4
  2. DB0: 0xF8
  3. DB1: Adr_MSB (locomotive address, most significant byte)
  4. DB2: Adr_LSB (locomotive address, least significant byte)
  5. DB3: TTNNNNNN. TT are the bit for switching (00 = off, 01 = on, 10 = toggle, 11 is not allowed). NNNNNN are the bits that indicate the function to be switched
  6. XOR-byte (integrity check for X-Header and all DB bytes)

A function in Python for formatting and sending such a command could look like this:

def lanXSetLocoFunction(address, function, switchMode): # 4.3 LAN_X_SET_LOCO_FUNCTION
     # function: decimal, 1 - 63 (should not be more than 28)
     # switchMode: 'on', 'off' or 'switch'
     dataLen = int.to_bytes(0x000A, 2, byteorder = 'little')
     header = int.to_bytes(0x0040, 2, byteorder = 'little')
     xHeader = int.to_bytes(0xE4, 1, byteorder = 'little')
     db0 = int.to_bytes(0xF8, 1, byteorder = 'little')
     db1 = int.to_bytes(0, 1, byteorder = 'little')
     db2 = int.to_bytes(address, 1, byteorder = 'little')
     # merge switchmode into function: overrule first 2 bits depending on switch mode:
     if switchMode == 'off': # bits 1 and 2: 00
         function = function & (~0b11000000) # clear bits 1 and 2
     if switchMode == 'on': # bits 1 and 2: 01
         function = function & ~0b10000000 # clear bit 1
         function = function | 0b01000000 # set bit 2
     if switchMode == 'switch': # bits 1 and 2: 10
         function = function | 0b10000000 # set bit 1
         function = function & ~0b01000000 # clear bit 2
     db3 = int.to_bytes(function, 1, byteorder = 'little')
     xor = 0x00E3 ^ 0x00F8 ^ 0 ^ address ^ function
     xorByte = int.to_bytes(xor, 1, byteorder = 'little')
     data = xHeader + db0 + db1 + db2 + db3 + xorByte
     command = dataLen + header + data
     s.sendto(command, Z21)

Using the Python functions

When saving these two function in a .py file and executing in IDLE, one can send commands like these directly from the IDLE command prompt (assuming the locomotive address is 3):

lanXSetLocoFunction(3, 0, 'on') # lights
lanXSetLocoDrive(3, 'forward', 128, 50)

Alternatively, wrap the Python functions in a nice ‘main’ function like this (don’t forget to import socket and time):

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # Internet, UDP
Z21 = ('192.168.0.111', 21105)
time.sleep(10)
lanXSetLocoFunction(3, 0, 'on') # lights
lanXSetLocoFunction(3, 1, 'on') # sound
time.sleep(20)
lanXSetLocoFunction(3, 2, 'on') # horn
time.sleep(10)
lanXSetLocoFunction(3, 2, 'off') # horn
for i in range(80): # accelerate slowly to 80
    lanXSetLocoDrive(3, 'forward', 128, i)
    print('Speed: ', i)
    time.sleep(1)
lanXSetLocoFunction(3, 2, 'on') # horn
time.sleep(10)
lanXSetLocoFunction(3, 2, 'off') # horn
for i in range(78, -2, -2):# brake slowly
    lanXSetLocoDrive(3, 'forward', 128, i)
    print('Speed: ', i)
    time.sleep(1)
time.sleep(10)
lanXSetLocoFunction(3, 0, 'off') # lights
lanXSetLocoFunction(3, 1, 'off') # sound
A video recording of the script above being executed.

Notes:

  1. Don’t forget to first connect to the Z21 with the statements below: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # Internet, UDPZ21 = (‘192.168.0.111’, 21105)
  2. The Z21 will not reply with a specific message. However, the LAN_X_SET_LOCO_DRIVE or LAN_X_SET_LOCO_FUNCTION commands will change the status of the affected locomotive. The command center will broadcast a LAN_X_LOCO_INFO (section 4.4) message to all subscribers for this message type.
  3. The locomotive model used is an NS 1204 from Roco with sound decoder (model number 72673). It has plenty of light and sound functions to experiment with. Only function 0 (F0) is used for the lighting of more or less all DCC locomotives. All other functions are entirely model-specific.

Next up: receiving feedback from the layout and shunting.

6 Replies to “Controlling a loc using the Roco Z21 and Python”

  1. Hi, thanks for the tutorial.
    I tried with my Z21 the LAN_SET_BROADCAST and it works perfectly with the right connection and I can see the status of the locos but impossible to use your lanXSetLocoDrive or your lanXSetLocoFunction : I have no error but the Z21 seems to ignore them… Do I need to setup something before?

  2. Hi, glad I can help!
    The last script on this page is all that is needed to get the locomotive running. So after connecting you should be able to send commands to the locomotive.

    If you can receive the messages from Z21 then that means that the connection is okay. Since both lanXSetLocoDrive and lanXSetLocoFunction seem to fail, I would double-check the locomotive address first.

    If that is correct and you still don’t get it working, please send me the exact code that you use. Then we can look at it in more detail.

  3. Hi,
    I want to add that the z21 start (the one in the start package) is blocked… you need to buy an unlock code to use all the functions.
    Now that I have unlock my command stations all the features are running well.
    Thank you for your help.

  4. That’s a good point!

    When I bought my z21, the Start edition did not exist yet. I was aware of the lock, but I don’t really know what features are locked. My impression was that no network connection was possible at all. Obviously, _some_ network functionality still works, even without using the unlock code.

    I’m glad to hear you have it working!

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.