Designing Features
A feature is the unit a client sees first: a named group of commands and properties around one capability of the device. How you cut a device into features decides how easy the connector is to understand and automate.
Feature roles
The suffix of a feature name tells the client what kind of feature it is:
- A Controller can act on the device. It may also expose readings.
- A Provider only reports information.
- A Service doesn't operate the device during normal use: the SiLA server's own meta features, and maintenance features such as teaching or calibration.
See Naming Conventions for the full rules.
Docstrings
The feature class docstring is the first thing a client developer reads. It should do more than restate the feature name — it should explain:
- What the feature controls or provides, and the context in which it applies.
- Whether commands must be called in a specific order, e.g., “call
Initializebefore any movement command”. - Pre-conditions for commands, e.g., “the device must be homed with
Homebefore callingStartRun”. - Dependencies between commands and what state they leave the device in, e.g. "The door is always opened at the end of a centrifugation".
- Informative notes about device quirks or limitations, e.g. the resonant frequency of a centrifuge
- How this feature affects other features, if applicable.
class TemperatureController(sila.Feature):
"""
Control and monitor the temperature of the incubation chamber.
The device must be initialized with `DeviceController.Initialize`
before `StartRamp` can be called.
This feature shares the device connection with `ShakerController`. Avoid
running a temperature ramp while shaking at high speed, as vibration affects
sensor accuracy.
"""
A feature whose docstring only says "Temperature controller." forces every caller to read the source code to understand how to use it safely.
Write these explanations in plain text and refer to other endpoints by their SiLA identifier, such as DeviceController.Initialize. Clients may not use Python, so don't include Python examples. See Docstring Format for the rules on every docstring section.
Design for the user, not the manual
A vendor's interface is shaped by their firmware and their history. Part of building a good connector is asking whether that interface serves the person automating the device, and changing it when it doesn't.
A Tecan Freedom EVO RoMA arm, for example, moves by registering waypoints and then traveling through them. A client wants to say "move to this position", so the connector exposes move_to and move_by and handles the waypoint registration internally.
One feature per concern
Give each feature one domain or responsibility, so it can be understood on its own. Two concerns that share the same underlying resource still get separate features.
The file-system connector has files and folders on the same file system, and an S3 bucket next to them. Each gets its own feature:
app.register(FolderService(protocol))
app.register(FileService(protocol))
app.register(S3Service(protocol))
For an incubator, the climate and the door are separate features, not one Incubator feature. A client can then use just the part it cares about.
A concern is defined by the objective, not by the number of endpoints. get_position, move_to, and move_by all serve "control the position of a thing", so they belong on one feature.
Speak the client's language
A client should never need to know that one vendor calls a door a "shutter" and another calls it a "hatch". Rename vendor terms to the closest standard term, and use the standard command vocabulary when an operation matches. The same operation then has the same name across every UniteLabs connector.
When a device combines two operations in one mechanism, model the operation the client cares about, not the mechanics. If a door opens in order to present a plate, the client wants the plate: expose a PlateCarrier with extend and retract, not a Door with open and close. How the mechanism works is a detail of that one device; the intent is portable.
Repeated modules
Devices often contain the same module more than once. There are two ways to model that, and the right one depends on whether the instances are independent.
- Independent modules, such as pump channels or bioreactor chambers, each do their own work. Register one parametrized feature per module, so each module is addressed on its own and stays a single concern.
- Interchangeable positions on one resource, such as slots in a plate hotel or a centrifuge, are not independent. Separate features per slot would depend on each other and split one concern across many features. Expose one feature with a slot parameter instead.
A useful test: if the setup feels more like a hotel than a set of doors, use a slot parameter. See Model Repeated Modules for both patterns in code.
Cross-cutting concerns
Authentication and locking apply across many features. Rather than adding an access_token parameter to every command, model them as separate features that use SiLA client metadata. The core AuthorizationService feature is a good reference.
Base features
Some functionality repeats across connectors, so the CDK ships it as shared base features, such as SimulationControllerBase and WeighingServiceBase. A base feature abstracts a process such as weighing, so every connector for a device that can weigh exposes the same weighing interface. The same folder also contains complete features, like LockController, that you can register as-is.
When a device needs an endpoint that a standard feature doesn't define, add it to that same feature class rather than creating a new feature. See Project Structure.
Before designing a new feature, check the SiLA base feature repository for one that already exists.
Last updated