Ohara Lab

Developing device servers with gRPC Frame

StreamCall / image transfer / EventBus / event subscriptions

This article follows the gRPC Frame development lifecycle: DeviceClass, server, client, and public interface/type stubs. MiniPIX and aandd_reader are implementation evidence, not device manuals. See the installation manual for installation. gRPC uses automatic dispatch, not ese774 ApiSpec.

gRPC Quick Start / Detailed gRPC Guide

1. DeviceClass and package structure

mini_pix/
├── pixet_ctrl.py                 # Device control
└── server_grpc/
    ├── __init__.py               # Public DeviceProxy entry point (separate implementation)
    ├── __init__.pyi              # DeviceProxy typing
    ├── pixet_grpc_server.py      # Server construction and startup
    ├── sync_pixet_grpc_client.py # Synchronous client
    ├── async_pixet_grpc_client.py# Asynchronous client
    └── make_pyi.py               # Client stub generation

This layout is limited to the files supplied; it is not a complete repository tree. Unlike the COBOTTA server_fastapi/ example, it does not require separate models/, routers/, and clients/ directories.

MiniPixCtrl in pixet_ctrl.py does not depend on gRPC, GUI, server, or client configuration. It provides connect() / disconnect(), acquire_frames() / acquire_integral(), and acquisition-result access and saving. Its constructor requires a logger.

from mini_pix.pixet_ctrl import MiniPixCtrl

ctrl = MiniPixCtrl(logger=logger)
ctrl.connect()
try:
    ctrl.acquire_frames(1, 1.0)
finally:
    ctrl.disconnect()

Supply logger before using this excerpt. This illustrates local invocation, not a complete executable program: PIXet installation, detector connection, and acquisition setup are environment-specific. The supplied pixet_ctrl.py also configures a Windows PIXet API directory.

2. Server construction and streaming

pixet_grpc_server.py creates MiniPixCtrl and passes it to grpc_frame.grpc_server.create_grpc_server. A subclass of the server is not mandatory; the DeviceClass can be supplied directly.

from grpc_frame.grpc_server import create_grpc_server
from mini_pix.pixet_ctrl import MiniPixCtrl

ctrl = MiniPixCtrl(logger=logger, log_level=log_level)
ctrl.connect()
server = create_grpc_server(
    ctrl_obj=ctrl,
    stream_obj=stream_obj,
    logger=logger,
    log_level=log_level,
    max_workers=10,
)
server.add_insecure_port(f"{grpc_host}:{grpc_port}")
server.start()

This is an excerpt, not a complete startup script. The stream_obj is constructed as _MiniPixStream(ctrl) below. The supplied pixet_grpc_server() also checks port binding, checks connectivity after startup, and shuts down both server and device on exit.

Streaming on the server

_MiniPixStream holds MiniPixCtrl and exposes generator-based acquire_frames() and acquire_integral() for StreamCall. It uses a worker thread and queue to forward frames received through DeviceClass callbacks.

stream_obj = _MiniPixStream(ctrl)
server = create_grpc_server(ctrl_obj=ctrl, stream_obj=stream_obj, ...)

The ellipsis abbreviates the additional arguments shown above; it is not a complete call. Normal method invocation and streaming are distinct. The DeviceClass itself does not need to use gRPC types.

3. Client inheritance and overrides

AsyncPixetGrpcClient(AsyncGrpcClient) overrides the client-facing behavior of acquire_frames() and acquire_integral() to save returned results locally as TIFF or PNG. SyncPixetGrpcClient provides the corresponding synchronous behavior.

# Excerpt from async_pixet_grpc_client.py
async def _acquire_frames_async(
    self, count, sec, name="frame.tiff", *,
    save_local=True, return_data=False,
    save_format=None, save_path=None,
):
    result = await self._grpc_acquire_frames(
        count, sec, save_format=save_format, save_path=save_path,
    )
    return self._finish_acquisition(result, name, save_local, return_data)

The public operation remains acquire_frames(...). _grpc_acquire_frames is an internal reference to the original remote call, not a public API for users. With save_local=True, files are written on the client filesystem. With return_data=True, the complete result is returned; otherwise the client returns result["rc"]. TIFF preserves raw numeric data; PNG converts it to display-oriented 16-bit grayscale. Multiple frames receive numbered filenames.

aandd_reader illustrates ordinary measurement calls and event subscription, complementing mini_pix frame streaming. This is a gRPC Frame development example, not a device-specific operating manual.

Pass a DeviceClass instance as ctrl_obj

Core excerpt from src/aandd_reader/server_grpc/aandd_reader_grpc_server.py:

ctrl = AanddReader(port=device_port, logger=logger, log_level=log_level)
server = create_grpc_server(
    ctrl_obj=ctrl,
    logger=logger,
    log_level=log_level,
    max_workers=10,
)
bind_addr = f"{grpc_host}:{grpc_port}"
ret = server.add_insecure_port(bind_addr)
if ret == 0:
    raise RuntimeError(f"bind failed: {bind_addr}")
server.start()

The complete source also checks readiness, waits for shutdown, calls server.stop(grace=None), and closes the controller. This example does not supply stream_obj; a separate stream object is not universally required.

Client subclasses and higher-level operations

The source defines AanddReaderGrpcClient(GrpcClient) and AsyncAanddReaderGrpcClient(AsyncGrpcClient). The synchronous client’s prepare_event_measurement() calls stream_start(), event_set_config(), set_trigger_mode(), and event_arm() in order; wait_event() returns the next subscribed event. The asynchronous client awaits the preparation operations and uses _run_in_thread() for subscription waiting. This describes the supplied implementation, not a universal implementation rule for gRPC Frame.

examples/grpc_client_stream_event.py prepares a weight_threshold event, waits once using wait_event(), and calls stream_stop() afterwards. This illustrates grouping several remote operations into a public, task-oriented client method without adding network concerns to the DeviceClass. examples/grpc_client_meas_async.py constructs AsyncAanddReaderGrpcClient(config=Config, logger=logger, log_level=log_level) and calls await client.meas(). server_grpc/make_pyi.py and the client .pyi files provide typing information.

Keep Frame-specific construction distinct

The supplied ese774 Frame example uses FastApiServer(device_cls=..., router_cls=..., api_spec=..., device_kwargs=...); the gRPC example uses create_grpc_server(ctrl_obj=...). Do not transplant one Frame’s constructor arguments into the other. Keep those details in the communication adapter rather than in the DeviceClass.

4. DeviceProxy and typing

The following excerpt is based on the supplied measure_simple.py:

from mini_pix.config import Config
from mini_pix.server_grpc import DeviceProxy

config = Config("mini_pix.yaml")
client = DeviceProxy(
    "mini_pix",
    server_ip=config.SERVER_IP,
    server_port=config.SERVER_PORT,
    set_timeout_sec=config.TIMEOUT,
    async_mode=False,
)
client.connect()
try:
    client.set_mode_all("Counting")
    client.set_threshold(5.0)
    client.set_bias(150)
    client.set_clock(9.66667)
    client.acquire_frames(
        1, 1.0, name="measurement_output/frame.tiff",
        save_local=True, return_data=False,
    )
finally:
    client.disconnect()

The original script uses a timestamped filename and XLogger. The values are example detector settings and must be checked for the actual device. The supplied __init__.pyi overloads DeviceProxy("mini_pix", ..., async_mode=False/True) with SyncPixetGrpcClient / AsyncPixetGrpcClient return types.

server_grpc/make_pyi.py uses grpc_frame.make_pyi.generate_client_pyi to generate synchronous and asynchronous client stubs and adjusts signatures for methods overridden on the client. The public __init__.py and __init__.pyi are maintained manually and are not overwritten. This is the MiniPIX package’s approach, not a mandatory layout for every gRPC device package.

5. Verification and limits

  1. Implement DeviceClass as a normal Python class independent of transport.
  2. Pass a DeviceClass instance to the gRPC server for automatic dispatch. Provide a separate streaming object when needed.
  3. Subclass the appropriate synchronous/asynchronous Frame client, overriding user-facing methods as needed. Client-side file saving belongs in the client, not in the server’s DeviceClass.
  4. Provide DeviceProxy registration/exports and .pyi in the package. Names of internal helper methods are not public interface requirements.

Device server development guide / Home

This article follows the supplied mini_pix implementation from a device-control class to a gRPC server, dedicated clients, and a user program. The gRPC Frame example uses automatic dispatch; it does not define a Pydantic / ApiSpec API as in the ese774 Frame example.

This document is based on the supplied MiniPIX sources. Compatibility with the latest Frame release and actual detector operation have not been verified. The complete server launch command and configuration contents cannot be established from these files alone.