Helper classes: lifecycle hooks and access pattern

Looking at the STAR architecture doc, the split between capabilities and helper classes is really clean. Nice pattern. Own files, own tests, clean classes. And they’re naturally ready to be promoted to full capabilities when other devices turn out to have similar subsystems (e.g., another device gets an autoload → extract an AutoloadBackend ABC).

Two questions:

1. Lifecycle. Capabilities participate in teardown via _on_stop(), the shaker stops, the heater deactivates, all before the connection closes. The helpers don’t. If stop() is called while the wash station is filling or the cover is locked, they don’t clean up. Should helpers have _on_stop() hooks even without being full capabilities?

2. Access. From the architecture doc:

# Subsystems — on the driver
 await star._driver.autoload.load_carrier(carrier_end_rail=10)
 await star._driver.cover.lock()
 await star._driver.wash_station.drain(station=1)

Loading carriers and locking covers are user operations, but they’re behind _driver (private attribute). If they lived on the Device, star.cover.lock() instead of star._driver.cover.lock(), that would also solve the lifecycle question, since the Device already manages _on_setup() / _on_stop() for everything it owns. But maybe
I’m missing why they need to live on a separate _driver attribute?

1 Like

Very good points @vcjdeboer !

I don’t know where to ask this question because there are so many threads, but seeing this here now:

Why would _driver be marked as private?

This indicates that it should not be used by PLR programmers but every aP I use requires access to driver-specific features:
No Capability abstraction can cover every unique feature of a device. But we in the lab purchase device A over device B because of device A’s special features.

As a result, I cannot imagine a production aP that would not require the PLR programmer to access the currently marked “private” driver.


good point, I missed that.

Should helpers have _on_stop() hooks even without being full capabilities?

definitely, 100%. Simple oversight

yes driver should be a public attribute for sure

I considered this, but ran into some problems:

  • which object should own these subsystems that are not capabilities: Device or Driver?
  • I don’t like Device owning universal capabilities on the one hand, and subsystems (necessarily device specific) on the other hand. Driver is naturally specific to a device
  • I think the Device functionaries should be accessed through either capabilities or drivers. Device.do_something really mixes the responsibilities. I think Device should only manage its resource model, capabilities and its driver.

I think the word “subsystem” is creating a false category. star.iswap.pickup_plate() and star.absorbance.read() have the same shape, device.thing.action() or like before device.attribute.method(). Both need lifecycle hooks. Both are user-facing. The only difference is whether a shared ABC exists yet. A Cover is just a capability that only one device has today. Doesn’t that make them structurally the same thing?

Agreed. But device.cover.do_something() doesn’t mix anything, the Cover is a named, scoped object with its own lifecycle. The Device isn’t doing the thing; the Cover is. Isn’t the scoping through the attribute what keeps responsibilities clean?

I think everything user-facing, device.iswap.pickup_plate(), device.cover.open()is a capability, and the Device owns it. Everything internal, USB connection, command serialization, firmware protocol, internal state; is the driver. What’s the concrete example of something user-facing that should not be a capability?

I think we agree. iSwap, head96 those are attributes that hold capabilities. So the Device manages resource model, attributes holding capabilities (universal and device-specific, all with lifecycle hooks), and the driver (internal, not user-facing).

Cover is a special helper case, a device-specific capability without an ABC, but I would still make it an attribute that holds a capability with lifecycle hooks.

it makes it the same thing as a CapabilityBackend in my opinion, which are not accessed from Device directly.

Agreed on life cycle hooks, it should mirror CapabilityBackend without actually being one.

what if we actually add a cover capability in the future?

capabilities are defined to be universal, and autoload/cover/wash stations right now are not universal. they are specific to a star. The driver is still doing the internal things you mentioned (+ some commands that do not clearly fit into a subsystem or capability backend like request machine configuration)