Train feedback with the Roco Z21 and Python

So far we can connect to the Z21 DCC command station, listen to messages from the Z21 and send commands to control a locomotive, all using Python. In the posts where I described this I warned several times that if we don’t add some form of feedback from the layout, a script is blind. If two locomotives are on a crash course, a script will not stop the locomotives. You will witness an infamous ‘train wreck in slow motion,’ with potentially significant financial damage. Very realistic indeed!

In real life, various safety measures have been taken to avoid accidents. Some of those measures build on getting feedback from the infrastructure. We are going to add feedback capabilities to our Python scripts as well, so that we also can ensure problem-free operation on our layout.

Types of feedback

DCC solves the problem of controlling multiple trains independently on a layout. XPressNet sorts out how multiple people can control these trains using multiple throttles using the same DCC command station. Feedback was not part of the original automation efforts for model trains. Roco’s throttle does not provide any feedback whatsoever and the messages we received from the Z21 are from the command station itself and not from the locomotive or infrastructure.

However, in the practice of model trains, we can identify a few types of feedback:

  • From the locomotive: Lenz, the company that also developed XPressNet, developed RailCom as a protocol on top of DCC that decoders in locomotives can use to communicate back to the command station.
  • From specific locations in the layout: a layout can contain sensors that respond to actuators at different locations. Typically, a passing train provides one or more pulses. Examples of these are a switch rail that closes every time a wheel pair rolls over it, a reed contact in or next to a rail that closes when a magnet mounted under a train passes and visible or IR light that changes state when a train blocks a light beam or reflects a light beam to a sensor.
  • From segments in the layout: a new capability for feedback that DCC enabled is occupation detection. When dividing the layout in different isolated sections, one can detect whether a train is in this segment by verifying whether any current is drawn between the rails. A train or wagon that contains an electrical device (DCC decoder or lighting) and draws a current will mark the segment as occupied. When such a train or wagon is not present, or only wagons that do not draw any current are present, the section is free.

Feedback support by equipment

Let’s see what our equipment supports when it comes to these kinds of feedback. the Z21 supports RailCom and so do the decoders that I have in my locomotives (Roco branded in the Roco locomotives, LokSound in the Artitec MUs). The protocol for RailCom seems to be under development, though and new firmware updates may be necessary to get more capabilities there. We don’t need any additional equipment.

The Z21 has an R-bus specifically for attaching feedback modules. These will pick up feedback information from the layout. When I started exploring, Roco sold a feedback module with 16 connections (10787). This device can register pulses provided by tracks with built in switches that a passing wheel set will trigger itpasses the switch. Likely also other switches like reed contacts in the rail and magnets under a wagon will work with this module. Roco has such switching rails and reed contacts for embedding in their rail system, in its catalog.

A new Z21 detector (10808) replaces this module. This detector provides feedback based on occupancy detection. The principle is that a track section is isolated and powered separately. Whenever something is on the section that draws current (like a DCC decoder or interior lighting in a coach) the status of this track section is ‘occupied.’ Otherwise it is ‘free.’ This feedback module also supports RailCom.

A Dutch company called Digikeijs provides various DCC equipment. They provide feedback modules for R-bus, like the occupancy detection module (DR4088RB-CS) and module for switches (DR4088RB-OPTO). The main functional difference of the CS version with Roco’s own detector is that the Digikeijs product has 16 channels. Also, the product is significantly cheaper. It doesn’t seem to support RailCom though.

This is the device I’m going to use in my experiment.

Digikeijs DR4088RB feedback module
Digikeijs DR4088RB feedback module

The experiment

In this experiment I’m going to use an oval like in the previous one. The difference is that I split up the oval in 2 segments. For that, I’m going to isolate the inner rail in two places and wire them separately. There is no need to also isolate the outer rail. I connected the wires for the inner rail to the feedback module (sections 1 and 2). Then I connected the feedback module to rail contact of the Z21 to have a powered inner rail again. Also, I connected the feedback module to the R-bus to communicate about segment status.

See the oval and the wiring below:

Oval with sections and feedback module
The oval as implemented. The inner rail is isolated exactly half way in the curves. (If only the cables would be tidy for once…)

Or in a more schematic way:

Schematic overview of the oval with sections and wiring
Schematic overview of the oval with sections and wiring: green and orange are the sections. FM (blue) is the feedback module. The black wire is from the feeder to the Z21. The red wires go to the feedback module and from the feedback module to the other feeder contact on the Z21. The blue dotted line represents the R-bus cable.

Picking up the feedback messages

We already learned how to connect to the command station and how to listen for messages. What is new there is that we have to subscribe to R-bus messages. We’ll do that here:

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # Internet, UDP
Z21 = ('192.168.0.111', 21105)
2.16 LAN_SET_BROADCASTFLAGS
dataLen = int.to_bytes(0x0008, 2, byteorder = 'little')
header = int.to_bytes(0x0050, 2, byteorder = 'little')
data = int.to_bytes(0x0000002, 4, byteorder = 'little') # subscribe to R-bus messages only
command = dataLen + header + data
s.sendto(command, Z21)

The message type we are going to receive is LAN_RMBUS_DATACHANGED (section 7.1 in the protocol document). The format of this message is:

  1. DataLen (2 bytes little endian): 0x0F, 0x00
  2. Header (2 bytes little endian): 0x80, 0x00
  3. Data.

The Data section is built up as follows:

  1. Group Index (1 byte), values 0 or 1
  2. Feedback status (10 bytes)

The Z21 accepts a total of 20 feedback modules of 8 sections each. The feedback modules are categorised in 2 groups (0 or 1) and daisy-chained within a group, up to 10 feedback modules. Assuming that each module has 8 channels, each bit represents a channel. In case of the Digikeijs which has 16 channels, this translates in 2 feedback modules with 8 channels each.

Each message specifies which group the message is for, and includes a status snapshot for all feedback modules and channels in this group at once. What we’ll have to do is test the status for each section in this large array of 80 bits. Which sections are interesting, depends on how we wired the layout. We test the status like this (only implemented for the first two modules, or the first Digikeijs module):

# 7.1 LAN_RMBUS_DATACHANGED
groupIndex = message[4]
feedbackStatus = message[5:]
fm1section1 = feedbackStatus[0] & 0b00000001 # channel 1 on DR4088RB
fm1section2 = feedbackStatus[0] & 0b00000010
fm1section3 = feedbackStatus[0] & 0b00000100
fm1section4 = feedbackStatus[0] & 0b00001000
fm1section5 = feedbackStatus[0] & 0b00010000
fm1section6 = feedbackStatus[0] & 0b00100000
fm1section7 = feedbackStatus[0] & 0b01000000
fm1section8 = feedbackStatus[0] & 0b10000000 # channel 8 or DR4088RB
fm2section1 = feedbackStatus[1] & 0b00000001 # channel 9 on DR4088RB
fm2section2 = feedbackStatus[1] & 0b00000010
fm2section3 = feedbackStatus[1] & 0b00000100
fm2section4 = feedbackStatus[1] & 0b00001000
fm2section5 = feedbackStatus[1] & 0b00010000
fm2section6 = feedbackStatus[1] & 0b00100000
fm2section7 = feedbackStatus[1] & 0b01000000
fm2section8 = feedbackStatus[1] & 0b10000000 # channel 16 or DR4088RB
# test all sections for feedback module 3 via feedbackStatus[2], up to module 10 via feedbackStatus[9]

Once we have isolated the status for the sections we are interested in we can do some follow up work. For example:

if fm2section1 and not fm2section2:
   print('Group index ', groupIndex, ': locomotive is in section 1')
if not fm2section1 and fm2section2:
   print('Group index ', groupIndex, ': locomotive is in section 2')
if fm2section1 and fm2section2:
   print('Group index ', groupIndex, ': locomotive enters the next section but has not left the old section yet')
if not fm2section1 and not fm2section2:
   print('Group index ', groupIndex, ', locomotive has been abducted by an alien')

Note: we can also send a command LAN_RMBUS_GETDATA (section 7.2 in the protocol document). The structure is like this:

  1. DataLen (2 bytes little endian): 0x05, 0x00
  2. Header (2 bytes little endian): 0x81, 0x00
  3. Data: Group index (1 byte), where the value shall be just 0 or 1.

The Z21 will respond with a LAN_RMBUS_DATACHANGED message with the current status of all channels in the specified group. Since we are already have a subscription to this message type, we won’t need this LAN_RMBUS_GETDATA command.

Putting it all together

We’re going to create a script that just listens to the LAN_RMBUS_DATACHANGED messages. It picks up the messages as a locomotive rides around the track a couple of times. This is the script:

import socket

def slicePacket(data):
    while len(data) > 0:
            dataLen = int.from_bytes(data[0:2], byteorder='little')
            handleMessage(data[0:dataLen])
            data = data[dataLen:]

def handleMessage(message):
    if message[2] != 0x80:
        print('A message was received that we are not interested in right now')
        return
    # 7.1 LAN_RMBUS_DATACHANGED
    groupIndex = message[4]
    feedbackStatus = message[5:]
    fm1section1 = feedbackStatus[0] & 0b00000001 # channel 1 on DR4088RB
    fm1section2 = feedbackStatus[0] & 0b00000010
    fm1section3 = feedbackStatus[0] & 0b00000100
    fm1section4 = feedbackStatus[0] & 0b00001000
    fm1section5 = feedbackStatus[0] & 0b00010000
    fm1section6 = feedbackStatus[0] & 0b00100000
    fm1section7 = feedbackStatus[0] & 0b01000000
    fm1section8 = feedbackStatus[0] & 0b10000000 # channel 8 or DR4088RB

    fm2section1 = feedbackStatus[1] & 0b00000001 # channel 9 on DR4088RB
    fm2section2 = feedbackStatus[1] & 0b00000010
    fm2section3 = feedbackStatus[1] & 0b00000100
    fm2section4 = feedbackStatus[1] & 0b00001000
    fm2section5 = feedbackStatus[1] & 0b00010000
    fm2section6 = feedbackStatus[1] & 0b00100000
    fm2section7 = feedbackStatus[1] & 0b01000000
    fm2section8 = feedbackStatus[1] & 0b10000000 # channel 16 or DR4088RB

    # test all sections for feedback module 3 via feedbackStatus[2], up to module 10 via feedbackStatus[9]

    if fm2section1 and not fm2section2:
        print('Group index ', groupIndex, ': locomotive is in section 1')
    if not fm2section1 and fm2section2:
        print('Group index ', groupIndex, ': locomotive is in section 2')
    if fm2section1 and fm2section2:
        print('Group index ', groupIndex, ': locomotive enters the next section but has not left the old section yet')
    if not fm2section1 and not fm2section2:
        print('Group index ', groupIndex, ', locomotive has been abducted by an alien')
    print('\n')
    return

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # Internet, UDP
Z21 = ('192.168.0.111', 21105)

# 2.16 LAN_SET_BROADCASTFLAGS
dataLen = int.to_bytes(0x0008, 2, byteorder = 'little')
header = int.to_bytes(0x0050, 2, byteorder = 'little')
data = int.to_bytes(0x0000002, 4, byteorder = 'little') # subscribe to R-bus messages only
command = dataLen + header + data
s.sendto(command, Z21)

while True:
    incomingPacket, sender = s.recvfrom(1024)
    if sender != Z21:
        continue # ignore message
    slicePacket(incomingPacket)

s.close();

The output after a few rounds, starting in section 1:

Group index  0 : locomotive enters the next section but has not left the old section yet
Group index  0 : locomotive is in section 2

Group index  0 : locomotive enters the next section but has not left the old section yet
Group index  0 : locomotive is in section 1

Group index  0 : locomotive enters the next section but has not left the old section yet
Group index  0 : locomotive is in section 1

Group index  0 : locomotive enters the next section but has not left the old section yet
Group index  0 : locomotive is in section 2

Group index  0 : locomotive enters the next section but has not left the old section yet
Group index  0 : locomotive is in section 1

Group index  0 : locomotive enters the next section but has not left the old section yet
Group index  0 : locomotive is in section 1

Group index  0 : locomotive enters the next section but has not left the old section yet
Group index  0 : locomotive is in section 1

What I notice is that the Z21 sends some messages a couple of times. E.g. when the locomotive enters a new segment, we expect to get (a) a message that it occupies two segments and (b) that it occupies only the new segment when it leaves the old one. We see that (a) and (b) are repeated a couple of times, seemingly for no reason. The final result will be right, but we need to be careful if we are going to build on these events later.

Final words

This experiment was a bit more challenging, especially because of the wiring we need to do. The software side is very simple though. I expect that feedback from pulses with the proper feedback modules work in the same way on the software side. We just have to be aware how to interpret the status changes from the various parts of the layout.

We have to remember that this kind of feedback does not take into account which locomotive is triggering the feedback. RailCom might be a solution for this. I’ll have to research this later, possibly with a more advanced feedback module.

With this feedback functionality we can take safety measures and let trains drive in a more meaningful way. We’ll research how to change switches and decoupling rails in a next session and then we are ready for some higher level shunting experiments.

3 Replies to “Train feedback with the Roco Z21 and Python”

  1. A bit about Railcom detection. The Z21 detector (10808) supports railcom but only if connected to a Black Z21 (uppercase “Z” ) via CAN bus. You can connect the detector 10808 to your white z21 (lowercase “z”) via R-Bus, but railcom data is not available.
    Another Railcom detector is the Digikeijs DR5088RC, but is loconet, and cannot be connected to the white z21 (can be to Black Z21).
    As far as I know the only railcom detector supported by white z21 is the Blücher GBM16XN with XpressNet Interface.
    PS: thanks for your posts! You helped me to understand the Z21 protocol (I don’t speak german).

  2. Thank you for your reaction! I wasn’t aware of these limitations regarding the white z21. I hope this will be removed in the near future.
    Surprising indeed that Roco published the protocol documentation only in German. I’m glad I can help out for the non-German speaking audience.

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.