"onehouse/protocol"
)
-// Driver polls one assembly. Run has one owner and closes the supplied Client
-// on each attempt. Recoverable device failures publish offline and retry with
-// backoff. Publication or cleanup failures return to the framework runner. The
-// shared publisher must run alongside this driver. Refresh is safe during Run.
-// No operation starts a physical connection before Run.
+// Driver serializes one assembly's polling and commands. Run has one owner and
+// closes the supplied Client on each attempt. Recoverable device failures
+// publish offline and retry with backoff. Publication or cleanup failures
+// return to the framework runner. The shared publisher must run alongside this
+// driver. Refresh is safe during Run. Duplicate protection ends at ledger
+// eviction or process restart. No operation starts a physical connection before
+// Run.
type Driver struct {
deviceID string
address string
topology topology
publisher *driver.Publisher
client Client
+ queue <-chan []driver.Command
+ ledger *driver.Ledger
}
-// New validates settings. The caller supplies valid publisher and client
-// dependencies. The driver owns the client until Run returns.
-func New(settings Settings, publisher *driver.Publisher, client Client) (*Driver, error) {
+// New validates settings and requires the device's queue in router. The caller
+// supplies valid publisher, router, and client dependencies and starts the
+// router's intake separately. The driver owns the client until Run returns.
+func New(settings Settings, publisher *driver.Publisher, router *driver.Router, client Client) (*Driver, error) {
if !protocol.ValidID(settings.DeviceID) {
return nil, fmt.Errorf("device ID must be a canonical ULID")
}
return nil, fmt.Errorf("validate topology: %w", err)
}
+ queue, exists := router.Queue(settings.DeviceID)
+ if !exists {
+ return nil, fmt.Errorf("device has no command queue")
+ }
+
+ ledger, err := driver.NewLedger(settings.DeviceID)
+ if err != nil {
+ return nil, fmt.Errorf("create result ledger: %w", err)
+ }
+
return &Driver{
deviceID: settings.DeviceID,
address: settings.Address,
topology: points,
publisher: publisher,
client: client,
+ queue: queue,
+ ledger: ledger,
}, nil
}
-// Run configures and verifies output modes before online or polling. A
+// Run configures and verifies output modes before online or command work. A
// successful first read establishes a baseline without input events. Device
// failures retry after one second, doubling to thirty seconds until a complete
-// baseline succeeds.
+// baseline succeeds. Commands received during backoff fail without I/O.
func (d *Driver) Run(ctx context.Context) error {
err := d.publisher.Descriptor(ctx, d.deviceID, protocol.Metadata{}, d.topology.descriptor())
if err != nil {
}
healthy = true
+ handler := &commandHandler{connection: connection}
ticker := time.NewTicker(d.pollInterval)
defer ticker.Stop()
select {
case <-ctx.Done():
return healthy, false, ctx.Err()
+ case commands := <-d.queue:
+ err = handler.handle(ctx, commands)
+ if err != nil {
+ return healthy, false, fmt.Errorf("process commands: %w", err)
+ }
case <-ticker.C:
err = connection.poll(ctx)
if err != nil {
timer := time.NewTimer(delay)
defer timer.Stop()
- select {
- case <-ctx.Done():
- return ctx.Err()
- case <-timer.C:
- return nil
+ handler := &commandHandler{connection: &connection{owner: d}}
+
+ for {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-timer.C:
+ return nil
+ case commands := <-d.queue:
+ err := handler.unavailable(ctx, commands)
+ if err != nil {
+ return fmt.Errorf("finish unavailable commands: %w", err)
+ }
+ }
}
}