Receiving Loc Information From Z21

Speech is silver, silence is golden, they say… Now that we have figured out how to communicate with the Roco Z21 DCC command station with a Python script (see here), it would be very enticing to figure out how to actually play with a locomotive. But let’s hold our horses for a moment, for two specific reasons:

  1. Blind: unlike with manual control where we have visual feedback on where the locomotive is on the track and foreseeing potential problems, an automated script is blind. If we see that a locomotive is approaching a tight curve with too high speed or it approaches the end of the track, the script has no knowledge of this and won’t slow down or stop the locomotive. We can see a (model?) train wreck happen in front of our eyes if we don’t have an emergency stop button nearby.
  2. Damage: like in real life where the consequences of a train wreck are usually severe, the same is true for a model railroad. The damage won’t be as drastic in terms of money or even loss of life, but still it would be rather heart breaking to see a delicate model of over €300 getting severe damage. Especially if it is your favorit model and it is no longer in production.

The only information a program can get about what happens on the layout is a series of messages from the Z21. These can be responses from our own commands, but also other events like responses on commands from other throttles. (Remember that multiple throttles can control multiple locomotives at the same time.) So before we will even begin setting a locomotive in motion we have to make sure that the program can handle any relevant message that it receives from the Z21.

The Z21 can send all ‘throttle information’ in terms of status updates for locomotives, switches and decouplers (X-bus) as well as feedback information from the layout about e.g. occupancy of sections (R-bus), across  across the (wireless) network. To actually have the Z21 sending this information to our program, our program has to subscribe to this information. The subscription will time out after 60 seconds and any new command will reset this counter.

So in order to listen to the Z21 we will create a connection, send an initial command for subscribing and start capturing and interpreting all incoming messages. Normally we would need to create a separate task that repeats this command to refresh the subscription within every 60 seconds to keep it alive. But for now we will live with the time limit and simply capture all messages as long as the Z21 sends them.

Let’s cook!

Establish a connection

No news here:

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

Subscribe to messages from the Z21

Create a subscription using the LAN_SET_BROADCASTFLAGS command (see section 2.16 in the protocol document). The format of this command is:

  1. DataLen (2 bytes little endian): 0x08, 0x00
  2. Header (2 bytes little endian): 0x50, 0x00
  3. Data (32 bits little endian): Broadcast Flags

The Broadcast Flags specify which data we actually would like to receive. Each bit has its own meaning and can be True (yes we want to get this information) or False. For now we want to receive all LAN_X_LOCO_INFO messages (0x00000001), for all locomotives on the track (0x00010000). We’ll simply combine these values using a logical OR to get everything properly in the Data section of the command. We’ll let Python handle all that, including the little endian byte order.

# 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(0x00010001, 4, byteorder = 'little')
command = dataLen + header + data
s.sendto(command, Z21)

Receive messages

We already solved how to receive entire UDP packets from the Z21.

# infinite loop (Z21 will stop sending after 60 seconds)
while True:
        incomingPacket, sender = s.recvfrom(1024)
        if sender != Z21:
                continue # ignore message
        SlicePacket(incomingPacket)

But we assumed that a packet contains only one single message. Since this may not be the case, we’ll add a function that extracts all messages by slicing an incoming UDP packet. All messages in a packet are simply added to each other and since the first 2 bytes always indicate the length of the message, it is easy to isolate the first message from the rest. Then the rest can be handled likewise, until there is no rest left.

The following function picks up a packet and isolates all messages:

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

Interpret messages

Last but not least, we’ll need to interpret the message. In this case, we only concentrate on LAN_X_LOCO_INFO messages. The format of this message type is:

  1. DataLen (2 bytes little endian): (any value between 14 and 21), 0x00
  2. Header (2 bytes little endian): 0x40, 0x00
  3. X-Header (1 byte): 0xEF
  4. DB0 … DBn: locomotive information
  5. XOR-byte (integrity check for X-Header and all DB bytes)

Note: the format of DataLen, Header, Data is for all Z21 messages. The format for the Data section broken into X-Header, DB bytes and the final XOR-byte is for all XPressNet messages. These message types are identified by 0x40 in the third byte and the LAN_X_ prefix in the names. Note also that all data in XPressNet messages are in big endian rather than little endian for the rest of the Z21 data.

To process only messages of the LAN_X_LOCO_INFO type, we only look for messages that have the values of 0x40 in the third byte and 0xEF in the fifth byte. We also ignore all other bytes except the DB bytes. We could evaluate the XOR-byte to verify the integrity of the message, but we’ll do that another time.

For now, I’d like to have reported:

  1. The locomotive address
  2. The direction
  3. The speed
  4. The number of speed steps (14, 28 or 128 steps)
  5. The status of the main light (F0)

The code for this could look like this:

def handleMessage(message):
        if message[2] != 0x40 or message[4] != 0xEF:
                return
        print('LAN_X_LOCO_INFO message received')
        db = message[5:]
        # address
        address = int.from_bytes(db[0:2], byteorder = 'big')
        print('Address: ', address)
        # direction
        direction = db[3] & 0b10000000
        if direction != 0:
                print('Direction: forward')
        else:
                print('Direction: backward')
        # speed
        speed = db[3] & 0b01111111
        print('Speed: ', speed)
        # speed steps
        speedSteps = db[2] & 0b00000111
        if speedSteps == 4:
                print('Speed steps: 128')
        elif speedSteps == 2:
                print('Speed steps: 28')
        else:
        print('Speed steps: 14')
        # F0
        f0 = db[4] & 0b00010000
        if f0 != 0:
        print('F0 (main lights): ON')
        else:
        print('F0 (main lights): OFF')

I tried the script with a test setup of my z21 with its Wifi base station, a simple piece of straight track and my trusty locomotive. For controlling the locomotive I used the Multimaus. I could also have used the Z21 app on my iPhone or iPad.

Test setup on the floor
Test setup on the floor. Mind the cables…

Time to test

When using the Multimaus, I got the following output (example):

LAN_X_LOCO_INFO message received
Address: 22
Direction: forward
Speed: 2
Speed steps: 128
F0 (main lights): ON
LAN_X_LOCO_INFO message received
Address: 22
Direction: forward
Speed: 0
Speed steps: 128
F0 (main lights): ON

With this kind of information a program can detect that e.g. the speed is too high for a specific section. It can then issue commands to adjust the speed. Also, knowing of two locomotives on a collision course, it can automatically switch off the power on the track.

Of course the script would need more information about where the locomotive is on the track. We will use occupancy detectors and other feedback techniques beyond DCC later on for that. Also, the script will need to know what the status of switches is so that it can change them to set up the correct route for the train. In other words, plenty of possibilities to expand on this basic functionality.

Below is the full listing of this program with an extended version of the handleMessage function. This version will isolate all relevant information from this message type, but I stripped all print statements. This version could be the start of a dedicated handler function for only this message type. We could create other handlers for other message types.

In next posts, we are going to control the locomotive from a Python script, read feedback information from the layout and start shunting.

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] != 0x40 or message[4] != 0xEF:
                return
        db = message[5:]
        address = int.from_bytes(db[0:2], byteorder = 'big')
        speedSteps = db[2] & 0b00000111
        direction = db[3] & 0b10000000
        speed = db[3] & 0b01111111
        doubletraction = db[4] & 0b01000000
        smartsearch = db[4] & 0b00100000
        f0 = db[4] & 0b00010000
        f1 = db[4] & 0b00000001
        f2 = db[4] & 0b00000010
        f3 = db[4] & 0b00000100
        f4 = db[4] & 0b00001000
        f5 = db[5] & 0b00000001
        f6 = db[5] & 0b00000010
        f7 = db[5] & 0b00000100
        f8 = db[5] & 0b00001000
        f9 = db[5] & 0b00010000 
        f10 = db[5] & 0b00100000
        f11 = db[5] & 0b01000000
        f12 = db[5] & 0b10000000
        f13 = db[6] & 0b00000001
        f14 = db[6] & 0b00000010
        f15 = db[6] & 0b00000100
        f16 = db[6] & 0b00001000
        f17 = db[6] & 0b00010000
        f18 = db[6] & 0b00100000
        f19 = db[6] & 0b01000000
        f20 = db[6] & 0b10000000
        f21 = db[7] & 0b00000001 
        f22 = db[7] & 0b00000010
        f23 = db[7] & 0b00000100
        f24 = db[7] & 0b00001000
        f25 = db[7] & 0b00010000
        f26 = db[7] & 0b00100000
        f27 = db[7] & 0b01000000
        f28 = db[7] & 0b10000000

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
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(0x00010001, 4, byteorder = 'little')
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();

5 Replies to “Receiving Loc Information From Z21”

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.