15 // Driver polls one assembly. Run has one owner and closes the supplied Client
16 // on each attempt. Recoverable device failures publish offline and retry with
17 // backoff. Publication or cleanup failures return to the framework runner. The
18 // shared publisher must run alongside this driver. Refresh is safe during Run.
19 // No operation starts a physical connection before Run.
24 pollInterval time.Duration
26 publisher *driver.Publisher
30 // New validates settings. The caller supplies valid publisher and client
31 // dependencies. The driver owns the client until Run returns.
32 func New(settings Settings, publisher *driver.Publisher, client Client) (*Driver, error) {
33 if !protocol.ValidID(settings.DeviceID) {
34 return nil, fmt.Errorf("device ID must be a canonical ULID")
37 host, port, err := net.SplitHostPort(settings.Address)
38 if err != nil || host == "" {
39 return nil, fmt.Errorf("address must contain a host and TCP port")
42 number, err := strconv.Atoi(port)
43 if err != nil || number < 1 || number > 65535 {
44 return nil, fmt.Errorf("TCP port is outside its range")
47 if settings.Unit == 0 || settings.Unit > 247 {
48 return nil, fmt.Errorf("unit must be from one through 247")
51 if settings.PollInterval < 10*time.Millisecond || settings.PollInterval > time.Minute {
52 return nil, fmt.Errorf("poll interval must be from ten milliseconds through one minute")
55 points, err := newTopology(settings.Modules)
57 return nil, fmt.Errorf("validate topology: %w", err)
61 deviceID: settings.DeviceID,
62 address: settings.Address,
64 pollInterval: settings.PollInterval,
71 // Run configures and verifies output modes before online or polling. A
72 // successful first read establishes a baseline without input events. Device
73 // failures retry after one second, doubling to thirty seconds until a complete
75 func (d *Driver) Run(ctx context.Context) error {
76 err := d.publisher.Descriptor(ctx, d.deviceID, protocol.Metadata{}, d.topology.descriptor())
78 return fmt.Errorf("publish descriptor: %w", err)
84 healthy, retry, err := d.attempt(ctx)
85 if !retry || ctx.Err() != nil {
86 return fmt.Errorf("run assembly: %w", err)
89 err = d.publisher.Availability(ctx, d.deviceID, protocol.Metadata{}, protocol.AvailabilityPayload{Status: "offline"})
91 return fmt.Errorf("publish offline: %w", err)
98 err = d.wait(ctx, delay)
100 return fmt.Errorf("wait for reconnect: %w", err)
103 delay = min(delay*2, 30*time.Second)
107 // Refresh requests retained snapshots through the shared publisher. It does not
108 // repeat input events, physical commands, or pulse operations.
109 func (d *Driver) Refresh() {
110 d.publisher.Refresh()
113 func (d *Driver) attempt(ctx context.Context) (healthy bool, retry bool, err error) {
115 var failure *deviceError
117 retry = errors.As(err, &failure)
119 closeErr := d.client.Close()
122 err = fmt.Errorf("close assembly: %w", errors.Join(err, closeErr))
126 err = d.client.Connect(ctx, d.address, d.unit)
128 return false, false, &deviceError{err: fmt.Errorf("connect assembly: %w", err)}
131 connection := &connection{owner: d}
132 err = connection.start(ctx)
134 return false, false, fmt.Errorf("initialize assembly: %w", err)
138 ticker := time.NewTicker(d.pollInterval)
144 return healthy, false, ctx.Err()
146 err = connection.poll(ctx)
148 return healthy, false, fmt.Errorf("poll assembly: %w", err)
154 func (d *Driver) wait(ctx context.Context, delay time.Duration) error {
155 timer := time.NewTimer(delay)