package m31 import ( "context" "encoding/json" "fmt" "strconv" "time" "onehouse/protocol" ) const ( pulseWidthBase = 0x30d4 outputModeBase = 0x32c8 maximumVoltage = 10000 ) type connection struct { owner *Driver state snapshot } func (c *connection) start(ctx context.Context) error { err := c.configure(ctx) if err != nil { return fmt.Errorf("configure outputs: %w", err) } next, err := c.read(ctx) if err != nil { return fmt.Errorf("read baseline: %w", err) } err = c.owner.publisher.Availability(ctx, c.owner.deviceID, protocol.Metadata{}, protocol.AvailabilityPayload{Status: "online"}) if err != nil { return fmt.Errorf("publish online: %w", err) } err = c.publish(ctx, next, true, nil) if err != nil { return fmt.Errorf("publish baseline: %w", err) } return nil } func (c *connection) configure(ctx context.Context) error { for _, output := range c.owner.topology.outputs { mode := uint16(0) if output.pulse != 0 { err := c.configureRegister(ctx, pulseWidthBase+output.address, output.pulse) if err != nil { return fmt.Errorf("set pulse width: %w", err) } mode = 1 } err := c.configureRegister(ctx, outputModeBase+output.address, mode) if err != nil { return fmt.Errorf("set output mode: %w", err) } } return nil } func (c *connection) configureRegister(ctx context.Context, address uint16, value uint16) error { err := c.owner.client.WriteRegister(ctx, address, value) if err != nil { return &deviceError{err: fmt.Errorf("write configuration register: %w", err)} } values, err := c.owner.client.ReadRegisters(ctx, address, 1) if err != nil { return &deviceError{err: fmt.Errorf("read configuration register: %w", err)} } if len(values) != 1 || values[0] != value { return &deviceError{err: fmt.Errorf("configuration register readback differs")} } return nil } func (c *connection) read(ctx context.Context) (snapshot, error) { inputs, err := c.owner.client.ReadInputs(ctx, uint16(len(c.owner.topology.inputs))) if err != nil { return snapshot{}, &deviceError{err: fmt.Errorf("read inputs: %w", err)} } observedAt := time.Now().UTC() outputs, err := c.owner.client.ReadCoils(ctx, uint16(len(c.owner.topology.outputs))) if err != nil { return snapshot{}, &deviceError{err: fmt.Errorf("read coils: %w", err)} } var analog []uint16 if len(c.owner.topology.analog) != 0 { analog, err = c.owner.client.ReadRegisters(ctx, 0, uint16(len(c.owner.topology.analog))) if err != nil { return snapshot{}, &deviceError{err: fmt.Errorf("read analog outputs: %w", err)} } } if len(inputs) != len(c.owner.topology.inputs) || len(outputs) != len(c.owner.topology.outputs) || len(analog) != len(c.owner.topology.analog) { return snapshot{}, &deviceError{err: fmt.Errorf("incomplete assembly read")} } err = validateVoltage(analog) if err != nil { return snapshot{}, &deviceError{err: fmt.Errorf("validate analog outputs: %w", err)} } return snapshot{ inputs: inputs, outputs: outputs, analog: analog, observedAt: observedAt, }, nil } func (c *connection) publish(ctx context.Context, next snapshot, baseline bool, causes map[string]string) error { for index, point := range c.owner.topology.inputs { if !baseline && next.inputs[index] == c.state.inputs[index] { continue } value := strconv.FormatBool(next.inputs[index]) err := c.report(ctx, point.id, value, next.observedAt, "") if err != nil { return fmt.Errorf("publish input state: %w", err) } if !baseline { action := `"released"` if next.inputs[index] { action = `"pressed"` } err = c.owner.publisher.Event(ctx, c.owner.deviceID, point.id, protocol.Metadata{SentAt: &next.observedAt}, protocol.EventPayload{ "action": json.RawMessage(action), "value": json.RawMessage(value), }) if err != nil { return fmt.Errorf("publish input transition: %w", err) } } } for index, point := range c.owner.topology.outputs { if point.pulse != 0 || !baseline && next.outputs[index] == c.state.outputs[index] { continue } err := c.report(ctx, point.id, strconv.FormatBool(next.outputs[index]), next.observedAt, causes[point.id]) if err != nil { return fmt.Errorf("publish output state: %w", err) } } for index, point := range c.owner.topology.analog { if !baseline && next.analog[index] == c.state.analog[index] { continue } value := strconv.FormatFloat(float64(next.analog[index])/1000, 'f', -1, 64) err := c.report(ctx, point.id, value, next.observedAt, causes[point.id]) if err != nil { return fmt.Errorf("publish analog state: %w", err) } } c.state = next return nil } func (c *connection) report(ctx context.Context, capability string, value string, observedAt time.Time, cause string) error { err := c.owner.publisher.Reported(ctx, c.owner.deviceID, capability, protocol.Metadata{ SentAt: &observedAt, CausationID: cause, }, protocol.ValuePayload{Value: json.RawMessage(value)}) if err != nil { return fmt.Errorf("publish point: %w", err) } return nil } func (c *connection) poll(ctx context.Context) error { next, err := c.read(ctx) if err != nil { return fmt.Errorf("read assembly state: %w", err) } err = c.publish(ctx, next, false, nil) if err != nil { return fmt.Errorf("publish assembly state: %w", err) } return nil } func validateVoltage(values []uint16) error { for _, value := range values { if value > maximumVoltage { return fmt.Errorf("analog output exceeds ten volts") } } return nil }