This project uses uv to manage its dependencies. If you're using Nix, there's also a flake.nix
to get you started (nix develop .#uv2nix
works to give you a dev environment).
Code formatting is done with ruff, just use ruff format src
.
If you have uv installed (see above) running the main program should be as easy as:
uv run mr_t --eiger-zmq-host-and-port $host --udp-host localhost --udp-port 9000
Which will receive images from the Dectris detector $host:9999
and also listen for UDP messages on localhost:9000
.
You can also just use plain Python, of course:
python src/mr_t/server.py --eiger-zmq-host-and-port $host --udp-host localhost --udp-port 9000
Note that you have to install the dependencies mentioned in pyproject.toml
beforehand (to a venv
, for example).
There is a configurable --frame-cache-limit
which, if you set it, will limit the number of frames held in memory to be no higher than this number. Meaning, the ZeroMQ messages will be held until the receiver picks them up.
In server.py, Mr. T will open a UDP socket as a server (a listen socket) on startup. It will also open a ZeroMQ socket to the detector and listen for messages itself. In Python, both the listening on ZeroMQ and on UDP are implemented as async
functions returning AsyncIterator[UdpRequest]
and AsyncIterator[ZmqMessage]
, respectively, so that you can iterate over them via:
async for msg in iterator:
...
The merge_iterators
function will merge both iterators and return a union of both data types, so you can listen for both types of messages.
This message loops keeps two pieces of state:
- The current image series (
CurrentSeries
in the code), which is optional, since there might no be a current series. It consists of the from the detector, the number of frames in it, thesaved_frames
(which is a dictionary from frame number to raw image data), the last complete frame (see below) and whether it officially ended. - The last series ID, which is used as a counter (new series always get the last ID + 1)
Given a UDP ping, we simply return a pong with the current series information if we have it, or None
.
Given a UDP packet request for a frame frameno
, there are a few considerations:
- If we are not in a series at all, we ignore the packet request completely.
- If the frame requested is not in the current series'
saved_frames
cache, send a reply with no bytes in it. The client is supposed to ignore this. - If we get a packet request for an unknown frame (like in the bullet point above) and the current series has ended, then send a reply with
premature_end_frame
set to the last complete frame, indicating to the client to stop requesting new frames. - Otherwise, we have data to send to the UDP client. Send a reply with the requested frame slice.
- Afterwards, check if we have frames in
saved_frames
that are smaller than the requested frame and delete them.
A ZmqHeader message has to contain a config
dictionary, which should be there if the header_detail
for the stream subsystem of the Dectris detector is set to all
or basic
. We need the config because it gives us the nimages
and ntrigger
values, determining how many frames we have in each series. We then will the CurrentSeries
structure with a new series ID (monotonically increasing the last one, starting at 0) and mostly zero values. We also store the new series ID value in last_series_id
, so that the counter can increase next time.
A ZmqImage message only contains a memoryview
with the whole frame's data (there is a per-image config
that you can set, too, but we don't use it). The frame's ID we generate ourselves by taking the last frame's number in the saved_frames
dictionary and increasing by 1 (or taking 0 if we don't have any frames yet). We also remember the ID as the last complete frame (which is used for premature end of series).
A ZmqSeriesEnd simply sets the current series' ended
boolean to True
.
See the latest Dectris Simplon API.
Every UDP message starts with the message type (one byte) and then there's the payload which depends on the message being sent. Generally, numbers are sent in big endian.
There are four types of UDP messages that are sent back and forth between the client (the system requesting images) and the server (Mr. T):
- Ping (message type 0): has no content (so it's just 1 byte long), is sent from the client to the server. Will be answered by a Pong (see below)
- Pong (message type 1)
- series ID (32 bit unsigned integer) of the image series currently going on, or 0 if there is no image series
- frame count (32 bit unsigned integer) of the current series (or 0 if there is no image series)
- Packet request (message type 2)
- frame number (32 bit unsigned integer, starting at zero) the frame number to get bytes from
- start byte (32 bit unsigned integer, starting at zero) the start byte inside the requested frame
- Packet reply (message type 3)
- premature end frame (32 bit unsigned integer): if this is 0, then the series is still going on; if it is not equal to zero, it's the index of the last frame in the series
- frame number (32 bit unsigned integer): the frame number the payload is for
- start byte (32 bit unsigned integer): the starting byte inside the frame
- bytes in frame (32 bit unsigned integer): how many bytes in total in this frame
- payload (raw bytes): the actual bytes
The way the protocol works is as follows:
- The client keeps sending Ping to the server until it receives a Pong with a series ID that is not zero.
- In this case, the client switches to requesting image data for the series. It sends Packet request messages, starting with frame number 0, start byte 0.
- It'll optionally (this is UDP, remember?) receive a Packet reply message with frame number 0, start byte 0, the total number of bytes in bytes in frame and the some bytes inside the payload (trying to fill up the UDP packet).
- The client will save this partial frame, and request frame number 0, start byte "n+1" (where n is the number of bytes in the first reply) next. Superfluous Packet reply (and Pong) messages have to be ignored by comparing the start byte and frame number with the latest one that was requested.
- The client can also switch to the next frame number if the frame is already finished transferring, or possibly start sending Ping messages again, if the whole series is transferred.
- The client might receive Packet reply with premature end of frame set to non-zero. This means the series is over before the previously communicated frame count is reached (because we stopped the series prematurely). In that case, the client can ask for all data until the frame indicated by premature end of frame and start sending Ping again.
- All the ZMQ (v1) parsing code comes from Tim Schoof from the asapo_eiger_connector repository.
- All the HDF5 writing code comes from Tim Schoof from the asapo_eiger_connector repository.