Ohara Lab

ese774_frame Fully Transparent Quick Start

Back to the index

This archived example shows the simplest fully transparent way to expose a locally operating DeviceClass as a device server using ese774_frame. It assumes no special exception handling is needed:

DeviceClass
    ↓
a few lines of server code
    ↓
a few lines of client code

No Pydantic/OpenAPI API definition is created.

1. Create a DeviceClass

Start with an ordinary Python instrument-control class, independent of SUSANOO and network communication. Here, a simple class stands in for real hardware.

simple_device.py

class SimpleDevice:
    def __init__(self, logger=None):
        self._logger = logger
        self._value = 0

    def add(self, a, b):
        return a + b

    def set_value(self, value):
        self._value = value

    def get_value(self):
        return self._value

This class can also be used directly:

from simple_device import SimpleDevice


device = SimpleDevice()

print(device.add(10, 20))

device.set_value(100)
print(device.get_value())

Output:

30
100

At this point, ese774_frame is not involved. The DeviceClass is the basic unit in SUSANOO.

2. Create a device server

Next, add the ese774_frame communication frame around the DeviceClass.

simple_server.py

from ese774_frame import FastApiServer
from ese774_frame.routers.device_router import DeviceRouter

from simple_device import SimpleDevice


if __name__ == "__main__":
    server = FastApiServer(
        device_cls=SimpleDevice,
        router_cls=DeviceRouter,
        config=None,
        api_spec=None,
        object_name="simple",
    )

    server.run(
        host="0.0.0.0",
        port=8000,
    )

For fully dynamic dispatch, set api_spec=None. Neither a Pydantic/OpenAPI definition nor a separate device-specific Router is required.

3. Start the device server

On the server machine, run:

python simple_server.py

The device server starts on port 8000. A client on the same machine can use 127.0.0.1; a client on another machine should specify the device server’s IP address.

4. Create a device client

When no special exception handling is required, the DeviceClass API does not need to be rewritten in the client class.

simple_client.py

from ese774_frame.clients.sync_device_client import SyncDeviceClient


class SyncSimpleDeviceClient(SyncDeviceClient):
    def __init__(
        self,
        server_ip="127.0.0.1",
        server_port=8000,
    ):
        super().__init__(
            server_ip=server_ip,
            server_port=server_port,
            api_spec=None,
            object_name="simple",
        )

There are no client implementations of add(), set_value(), or get_value(). Fully dynamic dispatch makes the DeviceClass’s public API callable over the network.

5. Use the client

simple_example.py

from simple_client import SyncSimpleDeviceClient


device = SyncSimpleDeviceClient(
    server_ip="127.0.0.1",
    server_port=8000,
)

try:
    print(device.add(10, 20))

    device.set_value(100)
    print(device.get_value())

finally:
    device.close()

Output:

30
100

Comparing local and remote use

Locally:

from simple_device import SimpleDevice


device = SimpleDevice()

device.set_value(100)
print(device.get_value())

Across the network:

from simple_client import SyncSimpleDeviceClient


device = SyncSimpleDeviceClient()

device.set_value(100)
print(device.get_value())

device.close()

In both cases, the user-facing calls are device.set_value(100) and device.get_value():

Local
    device.method(...)
        ↓
    DeviceClass.method(...)

Remote
    device.method(...)
        ↓
    ese774_frame
        ↓
    DeviceClass.method(...)

This is the transparent proxy model used by SUSANOO.

Public API

Fully dynamic dispatch generally exposes the public API of the DeviceClass:

class SimpleDevice:
    def start(self):
        ...

    def stop(self):
        ...

    def get_value(self):
        ...

These methods can be invoked as device.start(), device.stop(), and device.get_value(). Methods whose names start with _ are not exposed:

def _internal_method(self):
    ...

To exclude a public method from remote access, specify dispatch_exclude on the server:

server = FastApiServer(
    device_cls=SimpleDevice,
    router_cls=DeviceRouter,
    config=None,
    api_spec=None,
    object_name="simple",
    dispatch_exclude={
        "disconnect",
        "delete",
    },
)

For example, this can prevent clients from directly invoking a disconnect() operation whose lifecycle is managed by the device server itself.

When special handling is required

This page covers only the simplest case, where the client and server need no separate behavior. Some applications need client-side logic, for example:

Only those additional operations need to be implemented on the client. The ordinary DeviceClass API does not need to be duplicated.

Using Pydantic / OpenAPI

The basic approach described in this archived example is fully dynamic dispatch. When an explicit OpenAPI interface is required, Pydantic/ApiSpec can also be used; this requires an API definition separate from the DeviceClass’s Python API.

This is a translation of an archived example, not a claim that the example has been tested against current package versions.