Listening for Z21 data sets

Today we are going to listen for Z21 data sets in Python. In earlier posts we connected to the Z21, sent a first command and received and interpreted the related response. Now we are going to listen to different types of messages to get a feeling of how communication works.

The mission

We are going to control a locomotive to drive back and forth on a simple layout. We’re going to use a MultiMAUS throttle for this. We’ll connect the computer and print all messages that we receive to the console.

The setup

  • z21 (white), directly connected to my home network, using the default IP address: 192.168.0.111,
  • Apple MacBook Pro M1, running Mac OS 26.2 (Tahoe), connected to my home network,
  • Python 3.14.2, using IDLE as the development environment,
  • Roco MultiMAUS throttle or iPhone Z21 app to issue commands.

One would expect that a basic layout with a locomotive with DCC address 3 would be needed as well. However, DCC only sends signals to a locomotive and does not listen for acknowledgement. This means that for this test, no locomotive needs to be available to accept the commands that we will send with the throttle.

Some theory: asynchronous communication

Asynchronous communication means that there is no direct connection between a request and response. When we send a request for the serial number, the next message may be the response for this request. But if other things happen on the layout, the Z21 may send other messages first. This means that we have to apply an event driven approach which consists of:

  1. Subscribing to specific types of events
  2. Define handlers for these events
  3. Create an event loop

Let’s cook: subscribe to Z21 messages

Before we start listening to Z21 messages, we have let the Z21 know that we want to be informed about which types of messages. We’ll do this using the LAN_SET_BROADCASTFLAGS command. Sending such a command looks like this:

import socket

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

data_len = int.to_bytes(0x0008, \
    2, byteorder = 'little')
header = int.to_bytes(0x0050, \
    2, byteorder = 'little')
data = int.to_bytes(0x00010000, \
    4, byteorder = 'little')
command = data_len + header + data
s.sendto(command, Z21)

First, we create the socket so that we can start communicating with the Z21. Then we put together the command we want to send.

The format of the LAN_SET_BROADCASTFLAGS command is as follows :

  • Data Length (2 bytes, little endian): always 8 bytes (0x008)
  • Header (2 bytes, little endian): always 0x0050
  • Data (4 remaining bytes, little endian): flags about which kind of message we’d like to subscribe to.

For now, we’ll subscribe to the messages about the status of any locomotives on the layout. This is expressed by the value of 0x00010000.

Finally we’ll put the command together and send it to the Z21.

More cooking: handling incoming messages

Once a message is received from the Z21, the handle_message function will split the incoming message into individual data sets. Remember that we developed a split_data_sets function for this earlier.

After that, it will print each data set to the console.

The code looks like this:

def handle_message(message):
    for data_set in split_data_sets(message):
        print(data_set)

Even more cooking: event loop

Now we will have to start listening to incoming messages from the Z21 and processing the messages, repeatedly. The anatomy of such a function could look like this:

Event loop:

  1. Receive event: capture message from Z21
  2. Handle event: process the message

A quick and dirty implementation could look like this:

while True:
    message, sender = s.recvfrom(1024)
    if sender == Z21:
        handle_message(message)

The dirtiest way of creating a loop is one with no stop condition. In the loop we’ll listen for a message from a socket and verify that it was actually the Z21 that sent it. If so, we’ll split the message into a list of data sets and for each data set, call the event handler.

The result

When I used the throttle to control the locomotive, the program printed a number of data sets like the ones below:

b'\x0f\x00@\x00\xef\x00\x03\x0c\xa8\x00\x00\x00\x00\x00H'
b'\x0f\x00@\x00\xef\x00\x03\x0c\x94\x00\x00\x00\x00\x00t'

Then I used the Stop button on the Z21 to shut off the layout and switch it on again. The program produced the following output:

b'\x07\x00@\x00a\x00a'
b'\x07\x00@\x00a\x01`'

The final source code

This is the full source code that we put together:

import socket

def split_data_sets(input_string):
    if len(input_string) < 2:
        return []
    first_data_set_length = \
        int.from_bytes(input_string[0:2], \
        byteorder = "little")
    first_data_set = \
        input_string[:first_data_set_length]
    remainder = input_string[first_data_set_length:]
    return [ first_data_set ] + \
        split_data_sets(remainder)

def handle_message(message):
    for data_set in split_data_sets(message):
        print("Data set: ", data_set)

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

data_len = int.to_bytes(0x0008, \
    2, byteorder = 'little')
header = int.to_bytes(0x0050, \
    2, byteorder = 'little')
data = int.to_bytes(0x00010000, \
    4, byteorder = 'little')
command = data_len + header + data
s.sendto(command, Z21)

while True:
    message, sender = s.recvfrom(1024)
    if sender == Z21:
        handle_message(message)

Limitations

I think we have a good approach for capturing all Z21 messages. Areas for improvement are:

  • Stop conditions for the loop. Now we can abort with Ctrl-C on the keyboard but this only works if a message is received. We should find a better way.
  • Dispatchers for the data sets. Currently we just print the content of the received data sets on the console. We’ll need to develop handler functions for the different types of data sets and dispatcher functions to call these handlers.
  • Multiple ports. According to the published protocol, both ports 21105 and 21106 can be used. We’ll need to make sure that we will also listen to port 21106.

We’ll work on that for the next projects!

Comments

Leave a Reply

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