SDK guide

Classic Ophyd Signal driver

On this page
  1. Explicit unit metadata and hardware projection
  2. Finite receiver and execution grammar

This project is independent of and not affiliated with any upstream hardware or software vendor.

OphydDriver compiles explicit Signal bindings into the existing PythonObjectBridge; it adds no completion runtime and never discovers objects by trying get/set names. Declare a Field and a signal_bindings entry containing signal, get=True, set=<Field.writable>, profile, get_kwargs, timeout_ms and source_ref. classic_epics_v1 requires exactly get_kwargs={use_monitor:False}; classic_soft_v1 requires {}. Writes invoke Signal.set and await the original Status. A missing Status, unsuccessful completion or unresolved timeout cannot become success. Raw put/property setters are not compiled.

The get-only profile classic_epics_ro_v1 binds a Component(EpicsSignalRO, ...) declaration: it requires set=False (so the Field is read-only), get_kwargs={use_monitor:False}, compiles only the fresh read and no write, and G7 accepts only an unshadowed EpicsSignalRO imported from ophyd or ophyd.signal for it. An EpicsSignalRO Component is refused under classic_epics_v1 and an EpicsSignal Component under classic_epics_ro_v1. The registry exemplar MHS/drivers/labs/bcda-aps__apstools uses this profile for a read-only R0 monitor; a device needs at least one capability, so its only capability is an observational x-cap read whose native is a proven <signal>.get.

Configuration is copied per instance. Paths are public identifiers, at most eight segments and 200 characters, with no duplicate/prefix overlap; at most 128 bindings and timeout 1..60000 ms. The initial G7 source profile accepts only direct literal Component declarations on the selected pinned class with unshadowed classic Component/EpicsSignal or Signal imports. Nested paths can compile but remain unproven and fail G7. Local ancestry and mutation evidence is inspected without imports. Dynamic factories, async Ophyd, FormattedComponent and unsupported rebinding require separate proof; they are not inferred from a matching method name.

The retained Python bridge owns effectful call/Status lifetime only under the declared completion contract. A done flag or wait return does not prove that external native callbacks have stopped. In particular, Ophyd 1.10.7 DeviceStatus publishes done before its failure handler calls device.stop; this helper does not yet implement a reviewed terminal-thread/cleanup profile and the BEC live backend is explicitly refused. Stop can dispatch independently while a call is pending, but positive safety/reset is refused until previous effectful work settles and a fresh valid safety action completes. Output schema validation occurs before committing a declared safe-action acknowledgment. The family does not invent native stop semantics: package authors must supply source-backed stop, fresh idle observations, an operator envelope, bounded reads and a native ownership policy. BEC's exemplar requires explicit per-instance engineering_unit and velocity_unit before live construction, with fake-only placeholder defaults. It uses the native monitor-readback hold fallback and explicitly does not claim a physical emergency stop.

This helper does not establish that a native callback ended merely because a connection was closed, and it does not automatically enable every Ophyd card. Detector artifacts, asyncio Ophyd, startup script side effects and uninterruptible moves remain separate requirements.

G7 emits OPHYD_IDENTITY_UNRESOLVED when source-visible receiver/Signal escapes or dynamic selection/reinitialization require a reviewed source-bound profile. The BEC candidate currently hits this diagnostic in its base ancestry and remains blocked; passing its controlled runtime cases does not remove the source-identity blocker. The direct grammar also refuses explicit identity/storage/reflection dunders, aliased reflection builtins and class-shadowed decorators, irrespective of literal or function spelling. A future profile must identify exact pinned helper/ancestry code and prove literal factory arguments such as override_suffixes={}, rather than exempting a class name.

Explicit unit metadata and hardware projection

unit_bindings maps exact top-level Field paths (parameters.<name> or actions.<capability_id>.input|output.<name>) to explicit deployment option names. fake_unit_defaults applies only to the fake backend. Numeric Fields with no declared unit can receive a supported UCUM label. A declared unit can only be repeated exactly; native_unit conversion and sourced bounds without a fixed declared unit are refused. No magnitude conversion, rescaling or dimension change of an existing declaration occurs. Independent native channels do not acquire an inferred relation: BEC V has no source guarantee of position-units/second, so its velocity unit remains separate explicit operator metadata. Coupled-unit or conversion profiles need separate source evidence and validation.

All parameters/actions/configuration are copied before compilation. The expected complete contract is captured before an author model hook, then the complete candidate and current declarations are compared; the gate also captures declarations before constructing its instance. Every resolved schema, risk, permit, interlock, watchdog, limit, native stop, execution field and remaining model field must match. Hardware publication uses a detached data-only snapshot of the exact checked model and cannot invoke an author projection hook. This is not a sandbox for arbitrary malicious Python package code, and unsigned hardware loading still refuses before native construction.

Finite receiver and execution grammar

The direct Component proof supports ordinary instance methods and property getters whose first positional parameter is named self, including positional-only self. Renamed receivers, variadic-only receivers, static/class methods, shadowed decorators, executable method defaults and member mutation through caller object arguments require a separate profile and are refused. This is an explicit accepted source grammar, not a claim that Python requires the spelling self. Source helpers and imported dependency effects outside it remain unresolved.

Every selected Signal expression must be a direct receiver of get, set or put, or a read of classic string metadata name/pvname; direct name assignment remains the supported metadata update. Source put recognition establishes member identity only: the driver compiler still exposes writes through retained set completion and provides no raw-put bypass. Signal objects and bound methods cannot escape through assignments, loop iterables, comprehensions, NamedExpr, containers, returned values, callbacks or other calls. Those forms produce OPHYD_IDENTITY_UNRESOLVED rather than a clean declaration proof. Method names outside this finite grammar need a separately reviewed profile even when they would be harmless in a particular deployment.

Runtime dispatch is part of the same trusted contract as the model: complete native read/write/command/safety/setup/close maps are compiled from copied declarations before author hooks, compared afterward, and the trusted map is frozen. The gate snapshots both the model and runtime mapping before constructing its instance. Hooks that replace runtime bridge construction, native factory resolution or resolved output schemas are unsupported and refuse before factory creation. Changing fake factory/options during a model hook also refuses. This prevents a model that declares stop from silently dispatching destroy through an alternate bridge_config. It does not sandbox arbitrary Python action/result wrappers or prove external native callback lifetime.