]> git.giacomolli.uk - onehouse/blob - driver/m31/connection.go
Apply M31 commands through the shared ledger
[onehouse] / driver / m31 / connection.go
1 package m31
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "strconv"
8 "time"
9
10 "onehouse/protocol"
11 )
12
13 const (
14 pulseWidthBase = 0x30d4
15 outputModeBase = 0x32c8
16 maximumVoltage = 10000
17 )
18
19 type connection struct {
20 owner *Driver
21 state snapshot
22 }
23
24 func (c *connection) start(ctx context.Context) error {
25 err := c.configure(ctx)
26 if err != nil {
27 return fmt.Errorf("configure outputs: %w", err)
28 }
29
30 next, err := c.read(ctx)
31 if err != nil {
32 return fmt.Errorf("read baseline: %w", err)
33 }
34
35 err = c.owner.publisher.Availability(ctx, c.owner.deviceID, protocol.Metadata{}, protocol.AvailabilityPayload{Status: "online"})
36 if err != nil {
37 return fmt.Errorf("publish online: %w", err)
38 }
39
40 err = c.publish(ctx, next, true, nil)
41 if err != nil {
42 return fmt.Errorf("publish baseline: %w", err)
43 }
44
45 return nil
46 }
47
48 func (c *connection) configure(ctx context.Context) error {
49 for _, output := range c.owner.topology.outputs {
50 mode := uint16(0)
51 if output.pulse != 0 {
52 err := c.configureRegister(ctx, pulseWidthBase+output.address, output.pulse)
53 if err != nil {
54 return fmt.Errorf("set pulse width: %w", err)
55 }
56
57 mode = 1
58 }
59
60 err := c.configureRegister(ctx, outputModeBase+output.address, mode)
61 if err != nil {
62 return fmt.Errorf("set output mode: %w", err)
63 }
64 }
65
66 return nil
67 }
68
69 func (c *connection) configureRegister(ctx context.Context, address uint16, value uint16) error {
70 err := c.owner.client.WriteRegister(ctx, address, value)
71 if err != nil {
72 return &deviceError{err: fmt.Errorf("write configuration register: %w", err)}
73 }
74
75 values, err := c.owner.client.ReadRegisters(ctx, address, 1)
76 if err != nil {
77 return &deviceError{err: fmt.Errorf("read configuration register: %w", err)}
78 }
79
80 if len(values) != 1 || values[0] != value {
81 return &deviceError{err: fmt.Errorf("configuration register readback differs")}
82 }
83
84 return nil
85 }
86
87 func (c *connection) read(ctx context.Context) (snapshot, error) {
88 inputs, err := c.owner.client.ReadInputs(ctx, uint16(len(c.owner.topology.inputs)))
89 if err != nil {
90 return snapshot{}, &deviceError{err: fmt.Errorf("read inputs: %w", err)}
91 }
92
93 observedAt := time.Now().UTC()
94 outputs, err := c.owner.client.ReadCoils(ctx, uint16(len(c.owner.topology.outputs)))
95 if err != nil {
96 return snapshot{}, &deviceError{err: fmt.Errorf("read coils: %w", err)}
97 }
98
99 var analog []uint16
100
101 if len(c.owner.topology.analog) != 0 {
102 analog, err = c.owner.client.ReadRegisters(ctx, 0, uint16(len(c.owner.topology.analog)))
103 if err != nil {
104 return snapshot{}, &deviceError{err: fmt.Errorf("read analog outputs: %w", err)}
105 }
106 }
107
108 if len(inputs) != len(c.owner.topology.inputs) || len(outputs) != len(c.owner.topology.outputs) || len(analog) != len(c.owner.topology.analog) {
109 return snapshot{}, &deviceError{err: fmt.Errorf("incomplete assembly read")}
110 }
111
112 err = validateVoltage(analog)
113 if err != nil {
114 return snapshot{}, &deviceError{err: fmt.Errorf("validate analog outputs: %w", err)}
115 }
116
117 return snapshot{
118 inputs: inputs,
119 outputs: outputs,
120 analog: analog,
121 observedAt: observedAt,
122 }, nil
123 }
124
125 func (c *connection) publish(ctx context.Context, next snapshot, baseline bool, causes map[string]string) error {
126 for index, point := range c.owner.topology.inputs {
127 if !baseline && next.inputs[index] == c.state.inputs[index] {
128 continue
129 }
130
131 value := strconv.FormatBool(next.inputs[index])
132 err := c.report(ctx, point.id, value, next.observedAt, "")
133 if err != nil {
134 return fmt.Errorf("publish input state: %w", err)
135 }
136
137 if !baseline {
138 action := `"released"`
139 if next.inputs[index] {
140 action = `"pressed"`
141 }
142
143 err = c.owner.publisher.Event(ctx, c.owner.deviceID, point.id, protocol.Metadata{SentAt: &next.observedAt}, protocol.EventPayload{
144 "action": json.RawMessage(action),
145 "value": json.RawMessage(value),
146 })
147 if err != nil {
148 return fmt.Errorf("publish input transition: %w", err)
149 }
150 }
151 }
152
153 for index, point := range c.owner.topology.outputs {
154 if point.pulse != 0 || !baseline && next.outputs[index] == c.state.outputs[index] {
155 continue
156 }
157
158 err := c.report(ctx, point.id, strconv.FormatBool(next.outputs[index]), next.observedAt, causes[point.id])
159 if err != nil {
160 return fmt.Errorf("publish output state: %w", err)
161 }
162 }
163
164 for index, point := range c.owner.topology.analog {
165 if !baseline && next.analog[index] == c.state.analog[index] {
166 continue
167 }
168
169 value := strconv.FormatFloat(float64(next.analog[index])/1000, 'f', -1, 64)
170 err := c.report(ctx, point.id, value, next.observedAt, causes[point.id])
171 if err != nil {
172 return fmt.Errorf("publish analog state: %w", err)
173 }
174 }
175
176 c.state = next
177
178 return nil
179 }
180
181 func (c *connection) report(ctx context.Context, capability string, value string, observedAt time.Time, cause string) error {
182 err := c.owner.publisher.Reported(ctx, c.owner.deviceID, capability, protocol.Metadata{
183 SentAt: &observedAt,
184 CausationID: cause,
185 }, protocol.ValuePayload{Value: json.RawMessage(value)})
186 if err != nil {
187 return fmt.Errorf("publish point: %w", err)
188 }
189
190 return nil
191 }
192
193 func (c *connection) poll(ctx context.Context) error {
194 next, err := c.read(ctx)
195 if err != nil {
196 return fmt.Errorf("read assembly state: %w", err)
197 }
198
199 err = c.publish(ctx, next, false, nil)
200 if err != nil {
201 return fmt.Errorf("publish assembly state: %w", err)
202 }
203
204 return nil
205 }
206
207 func validateVoltage(values []uint16) error {
208 for _, value := range values {
209 if value > maximumVoltage {
210 return fmt.Errorf("analog output exceeds ten volts")
211 }
212 }
213
214 return nil
215 }