Transports#

This guide explains how transports connect FastCS controllers to external protocols, and how they use attribute callbacks to keep the protocol layer synchronized with attribute values.

Transport Architecture#

A transport connects a ControllerAPI to an external protocol. The ControllerAPI provides read-only access to:

  • Attributes (AttrR, AttrW, AttrRW)

  • Command methods (@command)

  • Scan methods (@scan)

  • Sub-controller APIs (hierarchical structure)

Implementing a Transport#

Subclass Transport and implement connect() and serve():

import asyncio
from dataclasses import dataclass, field
from typing import Any

from fastcs.controllers import ControllerAPI
from fastcs.transports.transport import Transport

@dataclass
class MyTransport(Transport):
    """Custom transport implementation."""

    host: str = "localhost"
    port: int = 9000

    def connect(
        self,
        controller_api: ControllerAPI,
        loop: asyncio.AbstractEventLoop,
    ) -> None:
        """Called during FastCS initialization.

        Store the controller_api and set up your protocol server.
        """
        self._controller_api = controller_api
        self._loop = loop
        self._server = MyProtocolServer(controller_api, self.host, self.port)

    async def serve(self) -> None:
        """Called to start serving.

        This runs as an async background task. It can block forever.
        """
        await self._server.start()

    @property
    def context(self) -> dict[str, Any]:
        """Optional: Add variables to the interactive shell."""
        return {"my_server": self._server}

Working with ControllerAPI#

The ControllerAPI provides access to the controller’s attributes and methods. Use walk_api() to traverse the entire controller hierarchy and register all attributes and commands. Use pattern matching to handle different attribute types.

for controller_api in root_controller_api.walk_api():
    for name, attribute in controller_api.attributes.items():
        match attribute:
            case AttrRW():
                protocol.create_read(name, attribute)
                protocol.create_write(name, attribute)
            case AttrR():
                protocol.create_read(name, attribute)
            case AttrW():
                protocol.create_write(name, attribute)

    for name, command in controller_api.command_methods.items():
        protocol.create_command(name, command)

Attributes#

Transports use attribute callbacks to keep their protocol-specific representations synchronized with attribute values:


eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1dWXfaSlx1MDAxMn7Pr8jxvF40vS/z5oXYON6JbZyZOT4yXGJb7Fx1MDAwMeFtTv77VMtcdNpaXHUwMDAyXGbOJTGc3CXaaLrrq/pq6dL/Pnz8uFx1MDAxMTxccryNf33c8Fx1MDAxZetux29cZt2Hjb/M8XtvOPL7PThFwr+P+uNhPbzyLlxiXHUwMDA2o3/985/uYOBEdzn1fvflTq/jdb1eMIJr/1xyf//48X/hv+GM3zD3VzrqrvXYVPT6VnpcdTAwMGacXFxI+cVcdTAwMGZvXHIv+jkgdzjsP0SHXHUwMDFm4Vx1MDAxOJdcbjtcXDEuJeVMcz45+1x1MDAwNGcxUsRhjEmCkURS6cnZXHUwMDA3v1x1MDAxMdzBXHUwMDE1aHLkzvNv74LwJuFcdTAwMTDBMZaYXG5OtVx1MDAxMpNr3N5tx0vcNVxuhv22t93v9IdmhP/AWuI6icZ449bbt8P+uNeYXFxcdTAwMTNcZt3eaOBcdTAwMGVhQqLrmn6nU1xynsKnw8TCXHUwMDA0bqS+4/LHiEnqeN5d8KW3dz1vZCZcdTAwMWRPjvZcdTAwMDdu3Vx1MDAwZl7mJvpcdTAwMTVmhINKI1xcn/9GY1x1MDAxYbpdr2JcdTAwMTaoN+50Jof9XsMzU79xI3qJr+s1fnzdz9WNlo78OPI9XHUwMDFhvOeZJ2OuXHUwMDEwkZRhNTlcdTAwMTOJXHUwMDE5pipz+KjfXHUwMDBiZY4rTqlEKlpwf7RcdTAwMDNyXHUwMDE2hI9tup2RXHUwMDE3LYJcdTAwMTlbOSaD0W9cdTAwMWNcdTAwMGZcdTAwMWHuyy1cdTAwMTgkSHCOtEIsmpmO32unJ6DTr7ct3zLo+3FcdTAwMTE3n+j/PkYyXHUwMDEz/mXy///9a/rVWYmMbv+QesxGx1x1MDAxZFx1MDAwNdv9btdcdTAwMGbgd52YMaXHP1xu3GGwXHUwMDA1y+j3btPnvF4j50x416ZB4J3nZoRcdTAwMDLui59LQ9Xr3PRcdTAwMWYsc9bt33uH/sswR5d+cPdjlX5e+CEmNCmdsbfz7XJcdTAwMDc3q/290e5o/OTuN4/b7Zl0hsBYOyD+jGBcdTAwMDKyXHUwMDE3XHUwMDEzvVx1MDAxMFx1MDAxN1iDRlx1MDAxMZJSxCTSXGaz2ZSGdlx1MDAxNMFcdTAwMDIhIZRQXHUwMDFjRXK5Vlx1MDAxYUmlMVhcXGkwQoRcdTAwMDQwRIOJKVxygmSe0qCCIUmJjuzAn6s0SlmRXFxrjY1gS5yc9Vx1MDAwZsp+6fCLOz45U9fnx9dZrTH06sFcdTAwMGJuk2xcdTAwMDNm0mGUKUm4wppGzMCAg1PuSKk1XGJcdTAwMDTXjIro7E/FXHUwMDAxkuJwJlxiXFzEMFY8XHUwMDEy1IlcItFcdTAwMTHspiuOJlJ1hN6L4lxiZlVcdTAwMWM0V3FcdTAwMDBcdTAwMWMoKFtcdTAwMWTp9EhxcFFANqRcdTAwMTRgfV9FNibDi1x1MDAwNjpcdTAwMTFH9+x2t76HXHUwMDFho0rteHuvdrp7vePubsRhPFx1MDAxMcnAe1xmNiYnvmdcdTAwMTC8uFIqgE3xOJNjTFwiRmnmMCB5mHBOONFJxFx1MDAwMJyKXHUwMDExo5XDsyAh81jX91x1MDAwNZKxXHUwMDFkJEml/Fx1MDAwM1xyWmFEXHUwMDE15jY0sJiwp9BAXHUwMDEwXHUwMDE4XHUwMDE1qrnEr4HDm5jRUPDg939cdFx1MDAxN7I/jC9jv1x1MDAxN1T955eBJ45+crt+J1x1MDAxNMPEczY7/q2ZgY06XGbZXHUwMDFibsSnIfDBuZ1cXND1XHUwMDFijbiJqMNDXb/nXHIrs9ia/tC/9Xtu50v+2N1x0D/zRi+jXHUwMDBmhmMvPjfe3oR8OoRcdTAwMTeA97R2cTBcdTAwMTQj/7ZcIj4/b5Hq4VPwMJ7D5knhIHBBNNNKMSyTLjZlXHUwMDFh/G+pueZcZlxiMyFZusypcjC45kyBXHUwMDBiXHUwMDBlrnqkZtdWbzqg7+dcdTAwMDC0JFx1MDAwMjNNSFxcZH/gWapcXDxjXCKxIOBcbi3buu13Kv6R37h7wGf1XHUwMDEy5Sffbr3BYFx1MDAwNa1b8TiLrFx1MDAxYsy1ozRcdTAwMDbzpsC6MZmEhlwi06DBwDhS+DBcbvRcXPFcdTAwMTgvWdu66dB4mFx1MDAwM1x1MDAxYYgjxiWiNlsnXG6woZCSXHUwMDE0cbk6LuNPW3c+ilx1MDAxYqe/z8xNMS9pM5dcdTAwMWP2cizcw9HOOOiRXHUwMDFh2Tw+2CqV77+djlx1MDAwZt05LFx1MDAxY9eORlxuUVx1MDAwNFx1MDAxY1/TXHUwMDA0iDHW4EZcdTAwMDM4kYJcdTAwMGKo1lx1MDAxMcYn9k1Ih2vDYsEzpJws6tW9r3DQ81x1MDAxYyBmXHUwMDE4yFxiplbCKlnm6Fx1MDAwNMRCgXNBNH1cdTAwMTVhLbRwu4/jy/rJjnSDs06j5FXklbe3ilx1MDAxNq5wnIVcdTAwMTZcdTAwMGUhh0omXGZ3XHUwMDAzU5WKlVx1MDAxMkqmgENcdEeYXHUwMDBmg2s55VhkwTGXhXtX4JBoXHUwMDFlcFCkJWfCglxyITOR0lx0NjDHXHUwMDA0zuvVc+b23GHjXHUwMDAxlnFcdTAwMTWM3Fx1MDAxNFx1MDAwYpM2ctmhL8fQ7XyqlfdcdTAwMWbvdL2hn/uH366OxLdcdTAwMWKZxbHX6fiDUcbMXHUwMDEx5FxiXHUwMDBlSKZcdTAwMTK8Nk2SjpxEXHUwMDA2yoyAplRcXIlcYlx1MDAwNT+RXGa3OoarSoZcYlx1MDAxMFZp46pIOkxJJpEgmEmA/Fx1MDAxY8gmTe0x9rcgu+GO7rxfXGZtPFx1MDAwN7Rh0WDWpVx1MDAxNdsyXHUwMDE2hkljW4LXh5BYumP3XHUwMDEwfK3dPDz1v3qfR335dVx1MDAwZvdu7k6XafZwXHUwMDFjXHUwMDFkrzd7RHS++qdlt/Rp/2TAdqv7p1+ah7PWXHUwMDE1UEdoRFx1MDAxOFxmXG70ajJuqYRJIEolwauA/8YyiJNcdTAwMTShXHUwMDAzwi80XHUwMDAzOok1xpjSLFrgXG74XG5cZo9cdTAwMTGUIVxy6vv3QMvfYFx1MDAwN7FaOGkopVx1MDAwNs2ms5ZwIyw0oLkgQkQpgjR7XHUwMDE1in6DpGGupJpPVkaj52WwPHdcdTAwMTIxtnw/SoRerG15OKhstyulk+ftnWs+8J6uusiN6ZeNbr9cdTAwMTGKot9cdTAwMWL5XHIvfqbpP0bfnfydJlx1MDAwMkBDsiOkXHUwMDE2RCR+p4NcdTAwMDSVXHUwMDA0K66pwkCjVOZ3Rlx1MDAxMvX7pDtZvXvub/X9c8E292vbrd1g7/BqxlwiXHSCXHUwMDFkLjVHnGrwXHUwMDAyXCJrbzCtNXJcdTAwMDBOXHUwMDA0XHUwMDFib1xmXHUwMDBilk3cIFx1MDAwN4E8KS4lx+BcdTAwMTljSS15XHUwMDFjI11SmOBcdTAwMTlWlFx1MDAxMFx1MDAxOYuQrTVgSlx1MDAwM+4urFx1MDAwMbGG9Vx1MDAwNPYnbPFhWO7cslx0XGbUj0tcdTAwMDJcdTAwMDD5M1VgqUBYw/NZOV2mXHUwMDE2zC+lyNOP94/3pd2j5qN7c9N6wjW5WW1cdTAwMWZ4S9CPnCqCOdMmwVx1MDAxN2XDzVx1MDAwN/QjXHUwMDEzzNTOXHUwMDEw+Fx1MDAwM+58gXpcXDUlqPavtset2zLrXHUwMDFk35ev79tHu0FwM6tcdTAwMTKkXHUwMDBlgd+NXHUwMDExpVx1MDAxOLEkXHUwMDBilFxmOzBPTCDCgSFEyI90IHhBgilFlVx1MDAwNHeLSFx1MDAxYT0gVjnGXHUwMDFjXHUwMDEwPkpcdTAwMDFjlJhM+VpcdTAwMDfm6cCLZZSOIcyB5thCiCD1ub6UxCD0wJNWJ1x1MDAwZrBsXHUwMDE1mCur4fmsmP5cblwi+EaKXHUwMDBlU0YxU1xmtFx1MDAxZFx1MDAwMqX2V3JcIoyjrShXXG70IMNIZ+vmfkcqWOyzXHUwMDE3hoAx6DlcZoRcdTAwMTmIsWCapeJGmjlYI6SQkVx1MDAxYc6ygSNpJEdcYiBcdTAwMTHMlNOiRUPA70vpXHUwMDExu86zXHUwMDE39HBBqFx1MDAwMpm1KDeZLVx1MDAwYvip25RcdTAwMGXDRG9cdTAwMTNcdTAwMDKeJ5RcdTAwMTNJ44846mZcdTAwMTBcZs8uVyFcdTAwMDA8JfKaXHUwMDBlXHUwMDAwp1x1MDAwN76c8G+xOpyW51x1MDAwNOtcdTAwMDY0llx1MDAwMo/XlIFcdTAwMDKM5OGFzWhHXHUwMDAz0SFcdTAwMDQoIObZrTJcdTAwMDTujsd/uVx1MDAwNcc49tA1kJNAplx1MDAwYpevXHUwMDEyQbRU1MpdeD68wWprKYhES49cdTAwMDNcdTAwMTeHXHUwMDE3XHUwMDEyMvli4CZnvv9V9Nxixv765za22pvN21Pi31x1MDAwZjtPtfrluHSx3V3BuHXxOFx1MDAwYmy1QFx1MDAxYTw1rSmRXHUwMDE4K52sR5KaOqxcYuFcXIV1XGZaMXBtgFx1MDAxYS1cXI70vlx1MDAwMM5mt9QwuUhgzK07WJjOL0diUlCuSSxcXL1SpnpcdTAwMTUs9Vx1MDAxNCNptdTLNtTFXHUwMDAx7Fx1MDAxOUpupYNcdTAwMDTXnFAg3yq5zSRmqDmiIENcdTAwMTZLjZOWem2o58ExX9hQS1Cw4FxmWe10tngpSteaMmtMxavAXWT3ivOgXHSRnMuejsbuVsDlqVx1MDAxN+xcctTl4KTl33/KKX+a67llv9mqXHUwMDBmXHUwMDAyvNuq+sPH6vjxM71dxfxy8e+fkl/mjqJCUi6Z0ihpqUHHO8ZRNlx1MDAxOVx1MDAxNlx1MDAxMFxui6m2bUFcdTAwMDXUY6Xg94GbLVxiU2uA51x1MDAwMXxcdHFEoSXMNJXWfevx7b9pkHPBiJSS/6G5lOTG9bRIRndngPpcdTAwMGLSJm+UVsZMKFx1MDAwNWtOiFx1MDAwMjgn0sphZolwYuqqXHUwMDE4J4xjnU2gr27mpFhcdTAwMGZcdTAwMTfFXGYlJ1x1MDAwZXghgiEsuGKZmOHEXHUwMDEzyaEwXFyvXZFcdTAwMDU0nJgjaEg5NTVO1qAhJ1x1MDAwNUXVoHiEpvpNOnAs7IqsRNBwilx1MDAxYmBzRZZcdTAwMWUzLKFWc2u0u99Ccr99u3vjV3WbzMpRpFx1MDAwMzZcdTAwMGWUm8KIsWTtN2PSMZv5QHKAqlx1MDAxMFx1MDAxZNtDXHUwMDEzq1x1MDAwMZGEaUHNXHUwMDA2KImZbWeE1I5cdTAwMTRcdTAwMTLUI1x1MDAwMkWJ1FxcrOVd7XaSenHSQlx1MDAxNEIm92mrI8XxnS8prMNcdTAwMWFrrP7U8o98MTWfjID+XHUwMDFhXHUwMDBls9K5xM98X5VbXHUwMDA3JXT69WFcdTAwMWNcZnnv6Vx1MDAxM5qtooJzYVpgaFxy861xfM9pWFdLicklMpOMovGas1x1MDAwMreHYelQuIUxIFx1MDAwYkzNVULmXHUwMDAx+aB41Vx1MDAxNEg03uUqkE9L8HpcdTAwMDRcdTAwMDdf1ZJKNFx1MDAwYiFyXHUwMDBiyGBdXHUwMDE08MDXXHUwMDE1T0yGl1xiXHUwMDE0WLlnTERcdTAwMDdn+1x1MDAwZv7d551n3azJbrM6blx1MDAxY11cdTAwMWPOXHUwMDEzKNBEXHUwMDEyXHUwMDFj2660XHUwMDEyXHUwMDBlVUbYV15cdTAwMTlcdTAwMDVDP1x1MDAxNeFM6aNcdTAwMDI1Y1/DWdxcdTAwMGbws1x1MDAxY1xuOp2bskaMklx1MDAwMVSMsHIoaHRCsVx1MDAxMlx1MDAxOPyyjJ5cdNtcdTAwMDaaLZ3aTDLBlrotNo/38b5cdTAwMTRNTqmqvWVcdTAwMDGlyCySzfsg+TtbwEhcdTAwMGJAXHUwMDAxfVx1MDAxM+cjMiGvcD6qgFx1MDAwMKvvXHUwMDEx61x1MDAxNvUrfI8pNjrteySHvVx1MDAxY9ej++S2rj1V6Vf3+d7NsHX/rexaXHUwMDFhj9hcbi9cdTAwMDG8jtHAWlxuXHUwMDE2X5CXXadMOUb7wSllgmmW7TdcdTAwMTa8XHUwMDEy6YBGJ6ZnXHUwMDAz55TLefamvS/87i1MXHUwMDE0zDZ6Q6VtjkZBny3QzDB+uHWJjbamXHUwMDExhZvDm1ZTadrd2nm+apVr+utndfXbXHUwMDEzhVJW3P9oqmBfxVx1MDAxOaiCIFx1MDAwMrRcdTAwMTij4P5cdTAwMTFTXHUwMDA3l1I1YJtcdTAwMWNcdTAwMTRufmJcdTAwMTLzbIk3hmmmZn+7ksj0Z1WW5kZrppCraSpzlEwgU7TGsNX3yNZRxJKqXHUwMDAyIED56jGF8/CRq8BcdTAwMTWmXHUwMDE46kxcdTAwMGKX1MCXw1x1MDAxNs5cdTAwMGaean6Dd+vtNjo/6F1dXdx/sextt7NcdTAwMDVuukxQkzxcdTAwMDeUJlx1MDAwM5XAXHUwMDBmXHUwMDFjjVx1MDAwNMOmSVx1MDAwNcJ8JrZANahPRoRSXHUwMDE0XHUwMDAzqDlbh1x1MDAxNfIwvL8wWyCm53KY6rHRhfxqKCVghZB6XHUwMDFktF/HXHUwMDE2+tfq6Kh34YsxOdkptVsn1eblxe/PXHUwMDE2suL+R7NcdTAwMDX7Ks7GXHUwMDE2mKOwKcxQStJUgSXn0lx1MDAwMaqFhJDENJq20Vx1MDAwNexcYr1cdTAwMGUsvFLVfJ6dLlx1MDAxOG1cdTAwMDJ0XHUwMDAxWSOV+TtdXHUwMDExJVx1MDAxYUlO32Sb10Js4WR80/FHd6tAXHUwMDE3pljqNF3IjHw5fFx1MDAwMZdLT+Vj7Fx1MDAxZVTdvYutg4Py/TBozZGEMPt8NdEyLiNh20bTzk1QYFx1MDAxMsqQfiGyTa2s4Vx1MDAwNWBcdTAwMTlcdTAwMWFcdTAwMWNcdKK10pjFZGyN4iSKXHUwMDBmXHUwMDE2z0NgjTjiwlqzIFh+L1x1MDAwZnDQqDKb2X9cdTAwMWRjaKPaUFx1MDAxZJZuyiebpFs5fEKH3p3+7Vx1MDAxOUNW3P9owmBfxFx1MDAxOVxiXHUwMDAz5zBRWFx1MDAxOVViRE+mXHSDduBcdTAwMWbTbFxizkuadU2AbkjwbCRcIsQ0XHUwMDFikuvwwjya5nB2vkA0zL+g1jdcdTAwMDPQXFxcdTAwMGaEc1g28UY7J6NvfVx1MDAxNV2w90D/xVRhipHOUoWldz+vyC/jJ9roXHUwMDBlvlX2g/r95v1D7aA6x1ZcZlx1MDAwM1HKXHUwMDA0XHUwMDEzXHUwMDE4XHUwMDAwKJKZRODxjqaMXHUwMDEwTVx1MDAwMedUWXqfXHUwMDBik2o0b1x1MDAxM6KIcmnDr2mvXHUwMDBl56UgXHUwMDA0XFwvReeJNLyvbpjVWYlD/lx1MDAwYkBcdTAwMThcdTAwMDFkmeSEXHUwMDA15pxcdTAwMTdUbWNcdTAwMGVrxJdcdTAwMTlqeFx1MDAxMc/NreOzTmuzQZ9OR9vXtdaVbFeW2iN9SVtcdTAwMWSKx1nYPoBcdTAwMDLZ1piaPimK0yTZNjXAjnnPXHUwMDE2lkxQqUi2k1x1MDAxNFx1MDAxMVx1MDAwZTG7mShXUphcdTAwMTZcdTAwMTWLllx1MDAwMr8v0HyZ3Vx1MDAwNlJcdDxDXHUwMDEzbtvTwLKWcfI+XHUwMDEwjFx1MDAxOFx1MDAwMv23gptcdTAwMTIrx6tQXHUwMDA2PMVcdTAwMDSljWB80MuxgZeDXHUwMDFhXHUwMDFhdp9P9fbTqLLb0aSs44XGxVx1MDAxZJC4Y6JZlJnOsVwiWo1wvoDcUsRgmaSWVGfL+NdcdTAwMTXAy4Ty+TJcIu1cdTAwMTJcdTAwMTaLWDuoY0Jy02hcdTAwMTTojVx1MDAwMP/jT33dZmFcdHBpXVx1MDAwM/wxo1Pqg9Fu+fPtdfmucX2xR8jd871cdTAwMTYzXHUwMDExXHUwMDAymGVcdTAwMDdcdTAwMGKYcNM1SKLk1iAhkFx1MDAwM7ZcdTAwMDZmXHUwMDE5a9NZMqtTMOVcdTAwMGVcdTAwMTfF/YRiXCK1XG5bg1jq+N/aePpcImf3o73ztJacgN9i48sqV1uY1SOYrtLbeX8ygut+73owXHUwMDBlrsF8d8xC/6dcdTAwMTfuunFcdTAwMDY5/jKez19cdTAwMWWGXHUwMDAymM9cdTAwMTSC/iCPJiR+RZpcdTAwMTPMNOzlkIUj97l76DW3n9uDO6/j6vFgWLdU7VmBLVx1MDAxZESkUZCIcJzkXG7aXHUwMDE0JFFg8khQqpUlNzZcdTAwMDOuyVx1MDAxYde5uK7NXHUwMDAza1gkyZHVXHLOb4VtXlx1MDAwZqDUKnXCzkP1MqDc8Zq/XHUwMDAyycuG70OJPtzqq9HFqFxcq1x1MDAxZH456nzbrlx1MDAxZc1cdTAwMDRfiZBcdTAwMTPm/qnpTZPy07GJY1NJgT2ajZ+Wd5mB4camzVx1MDAxZlNKMPDmIzl/XHUwMDFkfN/eTV8p+F7NXHUwMDBlX3C1TLGh9V1mXHUwMDA1bcBcdTAwMDQhVFx1MDAxMqTfxE1fXGK+lWNnlFc2v1x1MDAxYajNjHA5YEVl7/G0/alSXHUwMDFknm6dsnp3c+v03PJcdTAwMTJ7SyFcbmbKXHUwMDExYGNcdTAwMDVcdTAwMDdrKkRcdTAwMWGtlDiUXHUwMDEw825cbqpcYlDtXGZapZq6w2WN1ny0fp2jXHUwMDEyXHUwMDA1oEq0sFx1MDAxNreR/EpcdTAwMTTAquk9q1bP2Fx1MDAwMlx1MDAxNsb5pasrg9f0XHUwMDE4l4PYqzN//1x1MDAwNPVLT/T8cYtcdTAwMWZtb1dZuzFcdTAwMWJizWtwlaZMU9NqPun2amyKVE37J81cdTAwMTBcIjRbc1x1MDAxMr6JXHUwMDAzT6k0X9PjXFzEurMjlmBcdTAwMDHAI9hcdTAwMTZcdTAwMDdX+VxyMVx1MDAxOJhlXHSOy+q9Se3lkSnn8Wz1UTzjuJeD7Fx1MDAwMOPjwZa89IPnqiBfWzV1WZ+JOKeRndxCXCLA0lx1MDAxMlNoxuFcdTAwMGZcdTAwMDJcdTAwMWGdRbbEjq1H39rZzUdzY1x1MDAxZTRTzqR5449cdTAwMDXOsULPTFNskyt+o30jXHUwMDBiO7spZIxWXHUwMDE4xMXDnVx1MDAxYrtcdTAwMWZ+XHUwMDA02zfcwaBcdTAwMWFcdTAwMTg98DNcdTAwMDOyce97XHUwMDBmW1m5/0cz/JjUeYh8I/NemDj5/uH7/1x1MDAwMXuzV+wifQ==TransportUserHardwareAttrRWAttrRAttrWSendUpdatePublishPutIO_on_put_callbackAttrW.put_on_put_callbackIO.sendIO.updateupdate_callbackAttrR.update_on_update_callbacks

The diagram above shows the data flow between users, transports, attributes, and hardware. The following table gives an overview of the data flow for the transport layer.

Callback

Registered with

Triggered By

Direction

Purpose

On Update

add_on_update_callback()

attr.update(value)

Publish ↑

Update protocol representation when attribute value changes

Sync Setpoint

add_sync_setpoint_callback()

attr.put(value, sync_setpoint=True)

Publish ↑

Update transport’s setpoint display without device communication

Update Datatype

add_update_datatype_callback()

datatype property changes

Publish ↑

Update protocol metadata when datatype changes

Put

attr.put(value)

Transport receives user input

Put ↓

Forward write requests from protocol to attribute

On Update Callbacks#

Use add_on_update_callback() to update the protocol layer when an attribute’s value changes.

def create_read(name, attribute):
    protocol_read = Protocol(name)

    async def update_protocol_value(value):
        protocol_read.post(value)

    attribute.add_on_update_callback(update_protocol_value)

The callback receives the new value and should update the protocol-specific representation (e.g., posting to a PV, updating a REST endpoint cache, publishing the change to a subscriber).

Update Datatype Callbacks#

Use add_update_datatype_callback() to update protocol metadata when an attribute’s datatype changes. This is useful for protocols that expose datatype metadata (like EPICS record fields).

def create_read(name, attribute):
    ...

    attribute.add_on_update_callback(update_protocol_value)

    def update_protocol_metadata(datatype: DataType):
        protocol_read.set_units(datatype.units)
        protocol_read.set_limits(datatype.min, datatype.max)

    attribute.add_update_datatype_callback(update_protocol_metadata)

The callback receives the new DataType instance and should update the protocol’s metadata representation (e.g., EPICS record fields like EGU, HOPR, LOPR).

Put#

When the transport receives a write request from the protocol, call await attribute.put(value) to forward it to the attribute. This triggers validation and propagates the value to the device via the IO layer. The transport should also update its own setpoint display directly rather than relying on the sync setpoint callback being called.

def create_write(name, attribute):
    protocol_setpoint = Protocol(name)

    async def handle_write(value):
        protocol_setpoint.post(value)
        await attribute.put(value)

Sync Setpoint Callbacks#

Use add_sync_setpoint_callback() to update the protocol layer’s setpoint representation when the transport receives a write request. This is called when AttrW.put is called with sync_setpoint=True.

Each transport is responsible for updating its own setpoint display while actioning the change and should not rely on its sync setpoint callback being called by the attribute, nor should it call AttrW.put with sync_setpoint=True. Setpoints should not be synced between transports in this case - this is intentional to show which transport the change came from.

def create_write(name, attribute):
    ...

    async def update_setpoint_display(value):
        protocol_setpoint.post(value)

    attribute.add_sync_setpoint_callback(update_setpoint_display)

Sync setpoint callbacks are used in specific cases:

  • When an attribute delegates to other attributes that actually communicate with the device

  • During the first update of an AttrRW, to initialize the setpoint with the first readback value

Commands#

Transports can trigger commands, which connect directly to method calls rather than stateful attributes.

def create_command(name, command):
    protocol_command = Protocol(name)

    async def handle_command():
        await command.fn()
        protocol_command.post()

Usage#

Transports are automatically registered when subclassing Transport:

from fastcs.transports import Transport

@dataclass
class MyTransport(Transport):
    # Automatically added to Transport.subclasses
    pass

This allows the transport to be used in YAML configuration files.