From 4e582ae1661ea3351e56a60d9639f113bad1adbe Mon Sep 17 00:00:00 2001 From: Philipp Middendorf Date: Wed, 17 Dec 2025 09:43:00 +0100 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9B=20Fix=20h5=20log=20prefix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mr_t/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mr_t/server.py b/src/mr_t/server.py index 70d731d..0bbf80d 100644 --- a/src/mr_t/server.py +++ b/src/mr_t/server.py @@ -234,7 +234,7 @@ def cache_full() -> bool: if args.eiger_zmq_host_and_port is not None else receive_h5_messages( args.input_h5_file, # type: ignore - log=parent_log.bind(system="5"), + log=parent_log.bind(system="h5"), cache_full=cache_full, ) ) From eb77cff7da6f5b52b3fb7125ee3ab733068e4267 Mon Sep 17 00:00:00 2001 From: Philipp Middendorf Date: Wed, 17 Dec 2025 11:00:45 +0100 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=A8=20Add=20series=20name=20and=20bit?= =?UTF-8?q?=20depth=20to=20protocol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 23 +++--- pyproject.toml | 20 ++++++ rusty-t/src/main.rs | 14 +++- src/mr_t/eiger_stream1.py | 23 +++--- src/mr_t/h5.py | 45 ++++++++++-- src/mr_t/server.py | 146 ++++++++++++++++++++++++++------------ src/mr_t/udp.py | 5 +- uv.lock | 31 +++++++- 8 files changed, 234 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 7a68e2d..59f8649 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,17 @@ This project uses [uv](https://docs.astral.sh/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](https://docs.astral.sh/ruff/), just use `ruff format src`. +Code formatting and checking is done with [ruff](https://docs.astral.sh/ruff/), just use `ruff format src`. -## Running mr-t – normal mode +## Running mr-t — attached to a Simplon stream 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 +uv run mr_t_server --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`. +Which will receive images from the Dectris detector `$host:9999` and also listen for UDP messages on `localhost:9000`. Instead of using an actual Detector, you can also use one of the [Simplon](https://github.com/pmiddend/simplon-stub) API [mocks](https://github.com/AustralianSynchrotron/ansto-simplon-api). You can also just use plain Python, of course: @@ -29,12 +29,12 @@ Note that you have to install the dependencies mentioned in `pyproject.toml` bef 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. -## Running mr-t — simulation +## Running mr-t — feed from an HDF5 file If you already have a finished image series stored in an HDF5 file, you can tell mr-t to read images from this file, instead of waiting for images via ZMQ. A sample command line looks like this: ``` -uv run mr_t --input-h5-file ~/178_data-00000.nx5 --frame-cache-limit 5 --udp-host localhost --udp-port 9000 +uv run mr_t_server --input-h5-file $myhdf5file --frame-cache-limit 5 --udp-host localhost --udp-port 9000 ``` Note that in addition to `--input-h5-file` we are passing `--frame-cache-limit 5`. This will read at most 5 frames from the HDF5 file and wait until the other side (the FPGA) has actually pulled images from this cache. If you don't do this, and the receiving end is too slow, you will eat up a lot of RAM with all the cached images. @@ -73,9 +73,11 @@ Given a *UDP packet request* for a frame `frameno`, there are a few consideratio ### ZeroMQ messages -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 *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` (the default). We need the config because it gives us the `nimages` and `ntrigger` values, determining how many frames we have in each series. We then fill 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). +If the *ZmqHeader* contains a "header appendix" — which you can set via a Simplon API call — then this will be taken, verbatim, as the "series name". This is so the experimenter can name the current dataset with a human-readable name and not just a numeric ID. If the appendix is not given, this series name will be `series$id` with the detector-provided series ID. + +A *ZmqImage* message 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). The message also contains the image's bit depth, size and encoding (for example, if it was compressed). We want to transport that information to the receiver, so we wait for the first frame and store the frame's information for later sending. A *ZmqSeriesEnd* simply sets the current series' `ended` boolean to `True`. @@ -90,7 +92,10 @@ There are _four_ types of UDP messages that are sent back and forth between the - **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) 1. _series ID_ (32 bit unsigned integer) of the image series currently going on, or 0 if there is no image series - 2. _frame count_ (32 bit unsigned integer) of the current series (or 0 if there is no image series) + 2. _bit depth_ (8 bit unsigned integer) of the images in the series + 3. _frame count_ (32 bit unsigned integer) of the current series (or 0 if there is no image series) + 4. _length of series name_ (16 bit unsigned integer) + 5. _series name_ (raw bytes, latin1 encoded, not zero terminated) - **Packet request** (message type 2) 1. _frame number_ (32 bit unsigned integer, starting at zero) the frame number to get bytes from 2. _start byte_ (32 bit unsigned integer, starting at zero) the start byte inside the requested frame diff --git a/pyproject.toml b/pyproject.toml index 97df0ae..84571d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,5 +102,25 @@ pythonVersion = "3.13" [dependency-groups] dev = [ "ruff>=0.14.8", + "ty>=0.0.2", ] +[tool.ruff] +target-version = "py313" + +[tool.ruff.lint.isort] +force-single-line = true + +[tool.ruff.lint] +# to turn this: +# +# from X import y,z +# +# into +# +# from X import y +# from X import z +select = ["I001", "F", "E", "W", "I", "N", "UP", "YTT", "ANN", "ASYNC", "S", "FBT", "B", "A", "C4", "T10", "ICN", "LOG", "G", "PIE", "T20", "PYI", "Q", "RET", "SLF", "SLOT", "SIM", "TID", "TC", "INT", "ARG", "PTH", "TD", "FIX", "PD", "PGH", "PL", "R", "W", "FLY", "NPY", "FAST", "AIR", "FURB", "RUF"] +ignore = ["E501", "E722", "UP035", "ANN401", "S101", "S310", "FBT001", "B017", "B008", "B904", "C405", "C401", "C400", "G004", "G003", "PYI041", "SIM105", "TC001", "PGH003", "PGH004", "PLR2004", "PLR0913", "PLR0915", "PLR0912", "PLR0911", "RUF001", "LOG015"] + + diff --git a/rusty-t/src/main.rs b/rusty-t/src/main.rs index 8bffbaf..c008d8d 100644 --- a/rusty-t/src/main.rs +++ b/rusty-t/src/main.rs @@ -1,5 +1,4 @@ use std::fs::File; -use std::io::stdout; use std::io::Cursor; use std::io::Error; use std::io::ErrorKind; @@ -41,6 +40,8 @@ enum LoopState { struct PongPayload { series_id: u32, frame_count: u32, + bits_per_pixel: u8, + series_name: String } enum UdpRequest { @@ -96,12 +97,21 @@ fn decode_response(bytes: Vec) -> Result { }); } + let bits_per_pixel = reader.read_u8()?; + let frame_count = reader.read_u32::()?; + let _name_length = reader.read_u16::()?; + + let mut series_name: Vec = vec![]; + reader.read_to_end(&mut series_name)?; + return Ok(UdpResponse::UdpPong { pong_payload: Option::Some(PongPayload { series_id, frame_count, + bits_per_pixel, + series_name: String::from_utf8_lossy(&series_name[..]).to_string(), }), }); } @@ -197,7 +207,7 @@ fn main() { info!("no series: same as last series, waiting"); sleep(Duration::from_secs(2)) } else { - info!("no series: new series, switching state, waiting"); + info!("no series: new series (bit depth {0}, name {1}), switching state, waiting", payload.bits_per_pixel, payload.series_name); state = LoopState::InSeries { series_id: payload.series_id, frame_count: payload.frame_count, diff --git a/src/mr_t/eiger_stream1.py b/src/mr_t/eiger_stream1.py index 481f15c..59581f5 100644 --- a/src/mr_t/eiger_stream1.py +++ b/src/mr_t/eiger_stream1.py @@ -1,7 +1,9 @@ import asyncio import json from dataclasses import dataclass -from typing import Any, AsyncIterator, Callable, TypeAlias +from typing import Any +from typing import AsyncIterator +from typing import Callable import structlog import zmq @@ -12,7 +14,7 @@ def get_zmq_header(msg: list[zmq.Frame]) -> dict[str, Any]: return json.loads(msg[0].bytes.decode()) -ZmqAppendix: TypeAlias = str | dict[str, Any] +type ZmqAppendix = str | dict[str, Any] @dataclass(frozen=True) @@ -30,9 +32,11 @@ class ZmqSeriesEnd: @dataclass(frozen=True) class ZmqImage: data: memoryview + data_type: str + compression: str -ZmqMessage: TypeAlias = ZmqHeader | ZmqSeriesEnd | ZmqImage +type ZmqMessage = ZmqHeader | ZmqSeriesEnd | ZmqImage def decode_zmq_appendix(appendix: bytes) -> ZmqAppendix: @@ -53,7 +57,7 @@ def decode_zmq_message(parts: list[zmq.Frame]) -> ZmqMessage: htype = header["htype"] if htype == "dimage-1.0": - # meta = json.loads(parts[1].bytes.decode()) + meta = json.loads(parts[1].bytes.decode()) # shape = tuple(meta["shape"][::-1]) # Eiger shape order is reversed # dtype = meta["type"] # size = meta["size"] @@ -71,7 +75,7 @@ def decode_zmq_message(parts: list[zmq.Frame]) -> ZmqMessage: "Unexpected number of parts in image message: %s", len(parts) ) - return ZmqImage(data) + return ZmqImage(data, meta["type"], meta["encoding"]) if htype == "dheader-1.0": detail = header["header_detail"] @@ -84,7 +88,7 @@ def decode_zmq_message(parts: list[zmq.Frame]) -> ZmqMessage: has_appendix = True else: raise ValueError( - 'Unexpected number of parts for "none" detail: {}'.format(n_parts) + f'Unexpected number of parts for "none" detail: {n_parts}' ) elif detail == "basic": if n_parts == 2: @@ -93,7 +97,7 @@ def decode_zmq_message(parts: list[zmq.Frame]) -> ZmqMessage: has_appendix = True else: raise ValueError( - 'Unexpected number of parts for "basic" detail: {}'.format(n_parts) + f'Unexpected number of parts for "basic" detail: {n_parts}' ) elif detail == "all": if n_parts == 8: @@ -102,7 +106,7 @@ def decode_zmq_message(parts: list[zmq.Frame]) -> ZmqMessage: has_appendix = True else: raise ValueError( - 'Unexpected number of parts for "all" detail: {}'.format(n_parts) + f'Unexpected number of parts for "all" detail: {n_parts}' ) config = ( @@ -115,12 +119,13 @@ def decode_zmq_message(parts: list[zmq.Frame]) -> ZmqMessage: if htype == "dseries_end-1.0": return ZmqSeriesEnd() - raise ValueError("Unsupported htype: '{}'".format(htype)) + raise ValueError(f"Unsupported htype: '{htype}'") async def receive_zmq_messages( zmq_target: str, log: structlog.BoundLogger, cache_full: Callable[[], bool] ) -> AsyncIterator[ZmqMessage]: + # Somehow doesn't type-check, but it's a library issue zmq_context = zmq.asyncio.Context() # type: ignore zmq_socket = zmq_context.socket(zmq.PULL) diff --git a/src/mr_t/h5.py b/src/mr_t/h5.py index 81fac4b..fb2cdca 100644 --- a/src/mr_t/h5.py +++ b/src/mr_t/h5.py @@ -1,14 +1,46 @@ import asyncio from pathlib import Path +from typing import AsyncIterator +from typing import Callable + import h5py -from typing import AsyncIterator, Callable import structlog -from mr_t.eiger_stream1 import ZmqHeader, ZmqImage, ZmqMessage, ZmqSeriesEnd +from mr_t.eiger_stream1 import ZmqHeader +from mr_t.eiger_stream1 import ZmqImage +from mr_t.eiger_stream1 import ZmqMessage +from mr_t.eiger_stream1 import ZmqSeriesEnd parent_log = structlog.get_logger() +def _dataset_to_compression(ds: h5py.Dataset) -> str: + pl = ds.id.get_create_plist() + n_filters = pl.get_nfilters() + assert isinstance(n_filters, int) + if n_filters == 0: + return "" + has_lz4 = False + has_bs = False + for i in range(n_filters): + filter_id, _flags, _cd_vals, _name = pl.get_filter(i) + + # https://github.com/DiamondLightSource/hdf5filters/blob/master/h5lzfilter/H5Zlz4.c#L22 + if filter_id == 32004: + has_lz4 = True + elif filter_id == 32008: + has_bs = True + return ( + "bs8-lz4<" + if has_bs and has_lz4 + else "bs8<" + if has_bs + else "lz8<" + if has_lz4 + else "" + ) + + async def receive_h5_messages( input_file: Path, log: structlog.BoundLogger, cache_full: Callable[[], bool] ) -> AsyncIterator[ZmqMessage]: @@ -21,9 +53,14 @@ async def receive_h5_messages( yield ZmqHeader( series_id="1", config={"nimages": frame_count, "ntrigger": 1}, appendix=None ) - for frame_number in range(0, frame_count): + for frame_number in range(frame_count): if cache_full(): await asyncio.sleep(0.5) continue - yield ZmqImage(dataset[frame_number][:].tobytes()) # type: ignore + yield ZmqImage( + dataset[frame_number][:].tobytes(), # type: ignore + # Should be something like "uint32" + data_type=dataset.dtype.name, # type: ignore + compression=_dataset_to_compression(dataset), + ) yield ZmqSeriesEnd() diff --git a/src/mr_t/server.py b/src/mr_t/server.py index 0bbf80d..e0127b0 100644 --- a/src/mr_t/server.py +++ b/src/mr_t/server.py @@ -1,45 +1,38 @@ import asyncio -from dataclasses import dataclass +import logging import struct import sys -import logging -from typing import ( - Any, - AsyncIterable, - AsyncIterator, - Optional, - TypeAlias, - TypeVar, -) +from dataclasses import dataclass from pathlib import Path +from typing import Any, cast +from typing import AsyncIterable +from typing import AsyncIterator +from typing import Final +from typing import TypeVar import asyncudp import structlog from tap import Tap -from mr_t.eiger_stream1 import ( - ZmqHeader, - ZmqImage, - ZmqSeriesEnd, - receive_zmq_messages, -) +from mr_t.eiger_stream1 import ZmqHeader +from mr_t.eiger_stream1 import ZmqImage +from mr_t.eiger_stream1 import ZmqSeriesEnd +from mr_t.eiger_stream1 import receive_zmq_messages from mr_t.h5 import receive_h5_messages parent_log = structlog.get_logger() +UDP_PACKET_SIZE: Final = 10000 + class Arguments(Tap): udp_port: int udp_host: str - eiger_zmq_host_and_port: Optional[str] = ( # host:port of the Eiger ZMQ interface + eiger_zmq_host_and_port: str | None = ( # host:port of the Eiger ZMQ interface None ) - input_h5_file: Optional[ # hdf5 file to feed into Mr. T to mock the detector - Path - ] = None - frame_cache_limit: Optional[ # Limit the number of incoming ZeroMQ images to this number (can prevent memory overruns) - int - ] = None + input_h5_file: Path | None = None + frame_cache_limit: int | None = None @dataclass(frozen=True) @@ -47,9 +40,17 @@ class UdpPing: addr: Any +@dataclass(frozen=True) +class UdpSeriesMetadata: + series_id: int + series_name: str + bits_per_pixel: int + frame_count: int + + @dataclass(frozen=True) class UdpPong: - series_and_frame: None | tuple[int, int] + series_metadata: None | UdpSeriesMetadata @dataclass(frozen=True) @@ -68,8 +69,8 @@ class UdpPacketReply: payload: memoryview -UdpRequest: TypeAlias = UdpPing | UdpPacketRequest -UdpReply: TypeAlias = UdpPong | UdpPacketReply +type UdpRequest = UdpPing | UdpPacketRequest +type UdpReply = UdpPong | UdpPacketReply def decode_udp_request(b: bytes, addr: Any) -> None | UdpRequest: @@ -85,12 +86,23 @@ def decode_udp_request(b: bytes, addr: Any) -> None | UdpRequest: return None -def encode_udp_reply(r: UdpReply) -> bytes: # type: ignore - match r: # type: ignore - case UdpPong(series_and_frame=None): - return struct.pack(">BII", 1, 0, 0) - case UdpPong(series_and_frame=(series_id, frame_count)): - return struct.pack(">BII", 1, series_id, frame_count) +def encode_udp_reply(r: UdpReply) -> bytes: + match r: + case UdpPong(series_metadata): + if series_metadata is None: + return struct.pack(">BIBIH", 1, 0, 0, 0, 0) + encoded_name = series_metadata.series_name.encode("latin1", errors="ignore") + return ( + struct.pack( + ">BIBIH", + 1, + series_metadata.series_id, + series_metadata.bits_per_pixel, + series_metadata.frame_count, + len(encoded_name), + ) + + encoded_name + ) case UdpPacketReply( premature_end_frame, frame_number, start_byte, bytes_in_frame, payload ): @@ -122,7 +134,7 @@ class SeriesPayload: payload: bytes -SeriesMessage: TypeAlias = SeriesStart | SeriesEnd | SeriesPayload +type SeriesMessage = SeriesStart | SeriesEnd | SeriesPayload async def dummy_sender(log: structlog.BoundLogger) -> AsyncIterator[SeriesMessage]: @@ -141,6 +153,7 @@ async def dummy_sender(log: structlog.BoundLogger) -> AsyncIterator[SeriesMessag async def udp_receiver( log: structlog.BoundLogger, sock: asyncudp.Socket ) -> AsyncIterator[UdpRequest]: + log.info("starting UDP receive loop") while True: data, addr = await sock.recvfrom() # This is way too much information, logging the raw output @@ -155,15 +168,15 @@ async def udp_receiver( U = TypeVar("U") -async def _await_next(iterator: AsyncIterator[T]) -> T: +async def _await_next[T](iterator: AsyncIterator[T]) -> T: return await iterator.__anext__() -def _as_task(iterator: AsyncIterator[T]) -> asyncio.Task[T]: +def _as_task[T](iterator: AsyncIterator[T]) -> asyncio.Task[T]: return asyncio.create_task(_await_next(iterator)) -async def merge_iterators( +async def merge_iterators[T, U]( a: AsyncIterator[T], b: AsyncIterator[U] ) -> AsyncIterable[T | U]: atask = _as_task(a) @@ -183,11 +196,21 @@ async def merge_iterators( FrameNumber = int +@dataclass(frozen=True) +class FirstFrameData: + bits_per_pixel: int + compression: str + + @dataclass class CurrentSeries: # This is *not* the series ID from the detector, but rather our # own, which is strictly monotonically increasing. series_id: int + # Descriptive name that will also be used for the output file name + series_name: str + # Descriptive name for the series, given by the controls system + first_frame_data: None | FirstFrameData # How many frames in the current series frame_count: int saved_frames: dict[FrameNumber, memoryview] @@ -220,11 +243,16 @@ async def main_async() -> None: def cache_full() -> bool: return ( - len(current_series.saved_frames) > args.frame_cache_limit + len(current_series.saved_frames) >= args.frame_cache_limit if current_series is not None and args.frame_cache_limit is not None else False ) + if args.eiger_zmq_host_and_port is None and args.input_h5_file is None: + raise Exception( + "please specify either an Eiger host/port or an HDF5 file to read from" + ) + sender = ( receive_zmq_messages( zmq_target=args.eiger_zmq_host_and_port, @@ -233,7 +261,7 @@ def cache_full() -> bool: ) if args.eiger_zmq_host_and_port is not None else receive_h5_messages( - args.input_h5_file, # type: ignore + cast(Path, args.input_h5_file), log=parent_log.bind(system="h5"), cache_full=cache_full, ) @@ -248,14 +276,23 @@ def cache_full() -> bool: parent_log.debug("received ping, sending pong") sock.sendto( encode_udp_reply( - UdpPong((current_series.series_id, current_series.frame_count)) - if current_series is not None and current_series.saved_frames + UdpPong( + UdpSeriesMetadata( + series_id=current_series.series_id, + series_name=current_series.series_name, + frame_count=current_series.frame_count, + bits_per_pixel=current_series.first_frame_data.bits_per_pixel, + ) + ) + if current_series is not None + and current_series.saved_frames + and current_series.first_frame_data is not None else UdpPong(None) ), addr, ) case UdpPacketRequest(addr, frame_number, start_byte): - if current_series is None: + if current_series is None or current_series.first_frame_data is None: parent_log.warning( f"request for frame number {frame_number} ignored, not in series" ) @@ -297,7 +334,6 @@ def cache_full() -> bool: ) continue - PACKET_SIZE = 10000 parent_log.debug(f"received packet request, frame {frame_number}") if start_byte > len(this_frame): @@ -312,7 +348,9 @@ def cache_full() -> bool: frame_number=frame_number, start_byte=start_byte, bytes_in_frame=len(this_frame), - payload=this_frame[start_byte : start_byte + PACKET_SIZE], + payload=this_frame[ + start_byte : start_byte + UDP_PACKET_SIZE + ], ), ), addr, @@ -334,14 +372,20 @@ def cache_full() -> bool: assert isinstance(nimages, int) and isinstance(ntrigger, int) current_series = CurrentSeries( series_id=last_series_id + 1, + series_name=appendix + if isinstance(appendix, str) + else f"series{series_id}", + first_frame_data=None, frame_count=nimages * ntrigger, saved_frames={}, ended=False, last_complete_frame=0, ) last_series_id = current_series.series_id - parent_log.info(f"series start, new ID {current_series.series_id}") - case ZmqImage(data): + parent_log.info( + f"series start (appendix {appendix}), new ID {current_series.series_id}" + ) + case ZmqImage(data, data_type, compression): if current_series is None: parent_log.warning( "got a ZmqImage message but we have no series, what the hell went wrong here?" @@ -354,6 +398,18 @@ def cache_full() -> bool: ) current_series.last_complete_frame = new_frame_id current_series.saved_frames[new_frame_id] = data + if current_series.first_frame_data is None: + current_series.first_frame_data = FirstFrameData( + bits_per_pixel=8 + if data_type == "uint8" + else 16 + if data_type == "uint16" + else 32, + compression=compression, + ) + parent_log.info( + f"first image in series, metadata: {current_series.first_frame_data}" + ) parent_log.info(f"image {new_frame_id} received") case ZmqSeriesEnd(): if current_series is None: diff --git a/src/mr_t/udp.py b/src/mr_t/udp.py index f051d0b..a564dae 100644 --- a/src/mr_t/udp.py +++ b/src/mr_t/udp.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from typing import TypeAlias @dataclass(frozen=True) @@ -12,6 +11,6 @@ class UdpPong: series_id: int -UdpRequest: TypeAlias = UdpPing +type UdpRequest = UdpPing -UdpReply: TypeAlias = UdpPong +type UdpReply = UdpPong diff --git a/uv.lock b/uv.lock index 1f19382..c5a0a16 100644 --- a/uv.lock +++ b/uv.lock @@ -110,6 +110,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "ruff" }, + { name = "ty" }, ] [package.metadata] @@ -123,7 +124,10 @@ requires-dist = [ ] [package.metadata.requires-dev] -dev = [{ name = "ruff", specifier = ">=0.14.8" }] +dev = [ + { name = "ruff", specifier = ">=0.14.8" }, + { name = "ty", specifier = ">=0.0.2" }, +] [[package]] name = "mypy-extensions" @@ -260,6 +264,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl", hash = "sha256:a8453e9b9e636ec59bd9e79bbd4a72f025981b3ba0f5837aebf48f02f37a7f9f", size = 72510, upload-time = "2025-10-27T08:28:21.535Z" }, ] +[[package]] +name = "ty" +version = "0.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/e5/15b6aceefcd64b53997fe2002b6fa055f0b1afd23ff6fc3f55f3da944530/ty-0.0.2.tar.gz", hash = "sha256:e02dc50b65dc58d6cb8e8b0d563833f81bf03ed8a7d0b15c6396d486489a7e1d", size = 4762024, upload-time = "2025-12-16T20:13:41.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/86/65d4826677d966cf226662767a4a597ebb4b02c432f413673c8d5d3d1ce8/ty-0.0.2-py3-none-linux_armv6l.whl", hash = "sha256:0954a0e0b6f7e06229dd1da3a9989ee9b881a26047139a88eb7c134c585ad22e", size = 9771409, upload-time = "2025-12-16T20:13:28.964Z" }, + { url = "https://files.pythonhosted.org/packages/d4/bc/6ab06b7c109cec608c24ea182cc8b4714e746a132f70149b759817092665/ty-0.0.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d6044b491d66933547033cecc87cb7eb599ba026a3ef347285add6b21107a648", size = 9580025, upload-time = "2025-12-16T20:13:34.507Z" }, + { url = "https://files.pythonhosted.org/packages/54/de/d826804e304b2430f17bb27ae15bcf02380e7f67f38b5033047e3d2523e6/ty-0.0.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbca7f08e671a35229f6f400d73da92e2dc0a440fba53a74fe8233079a504358", size = 9098660, upload-time = "2025-12-16T20:13:01.278Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/5cd87944ceee02bb0826f19ced54e30c6bb971e985a22768f6be6b1a042f/ty-0.0.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3abd61153dac0b93b284d305e6f96085013a25c3a7ab44e988d24f0a5fcce729", size = 9567693, upload-time = "2025-12-16T20:13:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b1/062aab2c62c5ae01c05d27b97ba022d9ff66f14a3cb9030c5ad1dca797ec/ty-0.0.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:21a9f28caafb5742e7d594104e2fe2ebd64590da31aed4745ae8bc5be67a7b85", size = 9556471, upload-time = "2025-12-16T20:13:07.771Z" }, + { url = "https://files.pythonhosted.org/packages/0e/07/856f6647a9dd6e36560d182d35d3b5fb21eae98a8bfb516cd879d0e509f3/ty-0.0.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3ec63fd23ab48e0f838fb54a47ec362a972ee80979169a7edfa6f5c5034849d", size = 9971914, upload-time = "2025-12-16T20:13:18.852Z" }, + { url = "https://files.pythonhosted.org/packages/2e/82/c2e3957dbf33a23f793a9239cfd8bd04b6defd999bd0f6e74d6a5afb9f42/ty-0.0.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e5e2e0293a259c9a53f668c9c13153cc2f1403cb0fe2b886ca054be4ac76517c", size = 10840905, upload-time = "2025-12-16T20:13:37.098Z" }, + { url = "https://files.pythonhosted.org/packages/3b/17/49bd74e3d577e6c88b8074581b7382f532a9d40552cc7c48ceaa83f1d950/ty-0.0.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2511ac02a83d0dc45d4570c7e21ec0c919be7a7263bad9914800d0cde47817", size = 10570251, upload-time = "2025-12-16T20:13:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9b/26741834069722033a1a0963fcbb63ea45925c6697357e64e361753c6166/ty-0.0.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c482bfbfb8ad18b2e62427d02a0c934ac510c414188a3cf00e16b8acc35482f0", size = 10369078, upload-time = "2025-12-16T20:13:20.851Z" }, + { url = "https://files.pythonhosted.org/packages/94/fc/1d34ec891900d9337169ff9f8252fcaa633ae5c4d36b67effd849ed4f9ac/ty-0.0.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb514711eed3f56d7a130d4885f4b5d8e490fdcd2adac098e5cf175573a0dda3", size = 10121064, upload-time = "2025-12-16T20:13:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/e5/02/e640325956172355ef8deb9b08d991f229230bf9d07f1dbda8c6665a3a43/ty-0.0.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b2c37fa26c39e9fbed7c73645ba721968ab44f28b2bfe2f79a4e15965a1c426f", size = 9553817, upload-time = "2025-12-16T20:13:27.057Z" }, + { url = "https://files.pythonhosted.org/packages/35/13/c93d579ece84895da9b0aae5d34d84100bbff63ad9f60c906a533a087175/ty-0.0.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:13b264833ac5f3b214693fca38e380e78ee7327e09beaa5ff2e47d75fcab9692", size = 9577512, upload-time = "2025-12-16T20:13:16.956Z" }, + { url = "https://files.pythonhosted.org/packages/85/53/93ab1570adc799cd9120ea187d5b4c00d821e86eca069943b179fe0d3e83/ty-0.0.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:08658d6dbbf8bdef80c0a77eda56a22ab6737002ba129301b7bbd36bcb7acd75", size = 9692726, upload-time = "2025-12-16T20:13:31.169Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/5fff5335858a14196776207d231c32e23e48a5c912a7d52c80e7a3fa6f8f/ty-0.0.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4a21b5b012061cb13d47edfff6be70052694308dba633b4c819b70f840e6c158", size = 10213996, upload-time = "2025-12-16T20:13:14.606Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d3/896b1439ab765c57a8d732f73c105ec41142c417a582600638385c2bee85/ty-0.0.2-py3-none-win32.whl", hash = "sha256:d773fdad5d2b30f26313204e6b191cdd2f41ab440a6c241fdb444f8c6593c288", size = 9204906, upload-time = "2025-12-16T20:13:25.099Z" }, + { url = "https://files.pythonhosted.org/packages/5d/0a/f30981e7d637f78e3d08e77d63b818752d23db1bc4b66f9e82e2cb3d34f8/ty-0.0.2-py3-none-win_amd64.whl", hash = "sha256:d1c9ac78a8aa60d0ce89acdccf56c3cc0fcb2de07f1ecf313754d83518e8e8c5", size = 10066640, upload-time = "2025-12-16T20:13:04.045Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c4/97958503cf62bfb7908d2a77b03b91a20499a7ff405f5a098c4989589f34/ty-0.0.2-py3-none-win_arm64.whl", hash = "sha256:fbdef644ade0cd4420c4ec14b604b7894cefe77bfd8659686ac2f6aba9d1a306", size = 9572022, upload-time = "2025-12-16T20:13:39.189Z" }, +] + [[package]] name = "typed-argument-parser" version = "1.11.0"