6. Create ControllerAPI Abstraction Layer#
Date: 2025-03-10
Related: PR #87
Status#
Accepted
Context#
Transports currently access Controller instances directly to extract attributes, methods, and metadata for serving over their protocols. This creates a few problems:
Tight Coupling: Transports are coupled to internal Controller structure, making evolution difficult
Code Duplication: Every transport re-implemented similar traversal logic for discovering attributes and methods
No Encapsulation: Transports have direct access to mutable controller state
No Static View: No complete, immutable snapshot of controller API after initialization
Decision#
Introduce ControllerAPI as an abstraction layer that provides transports with a complete, static, read-only representation of a controller’s capabilities after initialization.
All transports now work with ControllerAPI instead of direct Controller access. A single create_controller_api() function handles all API extraction, replaces custom traversal logic in each transport.
Key architectural changes:
ControllerAPIdataclass represents the complete, hierarchical structure of what a controller exposesSeparate dictionaries for attributes, command_methods, put_methods, and scan_methods
walk_api()method provides depth-first traversal of the API treeBackend creates ControllerAPI during initialization and passes to transports
Consequences#
Benefits#
Encapsulation: Transports work with read-only API, cannot modify controller internals
Single Source of Truth: One canonical representation of controller capabilities
Reduced Code Duplication: Traversal and extraction logic written once, used by all transports
Separation of Concerns: Controllers focus on device logic, ControllerAPI handles representation, transports focus on protocol
Testability: Transports can be tested with synthetic ControllerAPIs; controllers tested independently
Evolution Independence: Controller internals can change without affecting transports
Migration Pattern#
Before (Direct Controller access):
class EpicsCAIOC:
def __init__(self, pv_prefix: str, controller: Controller):
# Each transport traverses controller itself
for attr_name in dir(controller):
attr = getattr(controller, attr_name)
if isinstance(attr, Attribute):
self._create_pv(f"{pv_prefix}{attr_name}", attr)
After (ControllerAPI abstraction):
class EpicsCAIOC:
def __init__(self, pv_prefix: str, controller_api: ControllerAPI):
# Transport receives ready-to-use API structure
for attr_name, attr in controller_api.attributes.items():
self._create_pv(f"{pv_prefix}{attr_name}", attr)
# Walk sub-controllers using standard method
for sub_api in controller_api.walk_api():
# Process sub-controllers with consistent structure