15 type commandHandler struct {
16 connection *connection
19 func (h *commandHandler) handle(ctx context.Context, commands []driver.Command) error {
20 pending, err := h.prepare(ctx, commands)
22 return fmt.Errorf("prepare output commands: %w", err)
25 if len(pending) == 0 {
29 if len(commands) == 1 && (pending[0].analog || pending[0].point.pulse != 0) {
30 err = h.special(ctx, pending[0])
32 return fmt.Errorf("apply special output: %w", err)
38 slices.SortFunc(pending, func(first outputRequest, second outputRequest) int {
39 return cmp.Compare(first.point.address, second.point.address)
42 active := make([]outputRequest, 0, len(pending))
44 for start := 0; start < len(pending); {
45 if pending[start].command.Expired(time.Now()) {
46 err = h.result(ctx, pending[start].command, protocol.ResultPayload{
48 Reason: "Command expired",
51 return fmt.Errorf("reject expired write: %w", err)
61 for end < len(pending) && pending[end].point.address == pending[end-1].point.address+1 && !pending[end].command.Expired(time.Now()) {
65 err = h.writeRange(ctx, pending[start:end])
67 return fmt.Errorf("fail coil writes: %w", h.fail(ctx, pending, err))
70 active = append(active, pending[start:end]...)
78 next := h.connection.state
79 next.outputs, err = h.connection.owner.client.ReadCoils(ctx, uint16(len(h.connection.owner.topology.outputs)))
81 return fmt.Errorf("fail coil readback: %w", h.fail(ctx, active, fmt.Errorf("read command coils: %w", err)))
84 if len(next.outputs) != len(h.connection.owner.topology.outputs) {
85 return fmt.Errorf("fail incomplete readback: %w", h.fail(ctx, active, fmt.Errorf("incomplete coil readback")))
88 next.observedAt = time.Now().UTC()
90 err = h.complete(ctx, active, next)
92 return fmt.Errorf("complete coil commands: %w", err)
98 func (h *commandHandler) prepare(ctx context.Context, commands []driver.Command) ([]outputRequest, error) {
99 var pending []outputRequest
101 for _, command := range commands {
102 record, exists := h.connection.owner.ledger.Lookup(command.Metadata.MessageID)
104 err := h.send(ctx, command.Metadata.MessageID, record)
106 return nil, fmt.Errorf("replay result: %w", err)
112 request, reason := newOutputRequest(command, h.connection.owner.topology, h.connection.owner.deviceID, len(commands) > 1)
114 err := h.result(ctx, command, protocol.ResultPayload{
119 return nil, fmt.Errorf("reject command: %w", err)
125 pending = append(pending, request)
131 func (h *commandHandler) send(ctx context.Context, commandID string, record driver.ResultRecord) error {
132 err := h.connection.owner.publisher.Result(ctx, h.connection.owner.deviceID, commandID, record.Metadata, record.Payload)
134 return fmt.Errorf("publish result: %w", err)
140 func (h *commandHandler) result(ctx context.Context, command driver.Command, payload protocol.ResultPayload) error {
141 record, err := h.connection.owner.ledger.Remember(command.Metadata.MessageID, payload)
143 return fmt.Errorf("remember command result: %w", err)
146 err = h.send(ctx, command.Metadata.MessageID, record)
148 return fmt.Errorf("send command result: %w", err)
154 func (h *commandHandler) writeRange(ctx context.Context, requests []outputRequest) error {
155 expires := *requests[0].command.Metadata.ExpiresAt
156 values := make([]bool, len(requests))
158 for index, request := range requests {
159 values[index] = request.value == 1
160 if request.command.Metadata.ExpiresAt.Before(expires) {
161 expires = *request.command.Metadata.ExpiresAt
165 ctx, cancel := context.WithDeadline(ctx, expires)
170 if len(requests) == 1 {
171 err = h.connection.owner.client.WriteCoil(ctx, requests[0].point.address, values[0])
173 err = h.connection.owner.client.WriteCoils(ctx, requests[0].point.address, values)
177 return fmt.Errorf("write output coils: %w", err)
183 func (h *commandHandler) fail(ctx context.Context, requests []outputRequest, cause error) error {
184 records := make([]driver.ResultRecord, len(requests))
186 for index, request := range requests {
187 record, err := h.connection.owner.ledger.Remember(request.command.Metadata.MessageID, protocol.ResultPayload{
189 Reason: "Modbus operation failed",
192 return fmt.Errorf("remember failed commands: %w", errors.Join(cause, err))
195 records[index] = record
198 for index, request := range requests {
199 err := h.send(ctx, request.command.Metadata.MessageID, records[index])
201 return fmt.Errorf("send failed commands: %w", errors.Join(cause, err))
205 return &deviceError{err: cause}
208 func (h *commandHandler) complete(ctx context.Context, requests []outputRequest, next snapshot) error {
209 records := make([]driver.ResultRecord, len(requests))
210 causes := make(map[string]string, len(requests))
212 for index, request := range requests {
215 actual = next.analog[request.point.address]
216 } else if next.outputs[request.point.address] {
220 payload := protocol.ResultPayload{Status: "completed"}
221 if actual != request.value {
222 payload.Status, payload.Reason = "failed", "Output readback differs from command"
225 record, err := h.connection.owner.ledger.Remember(request.command.Metadata.MessageID, payload)
227 return fmt.Errorf("remember output results: %w", err)
230 records[index] = record
231 causes[request.point.id] = request.command.Metadata.MessageID
234 err := h.connection.publish(ctx, next, false, causes)
236 return fmt.Errorf("publish output feedback: %w", err)
239 for index, request := range requests {
240 err = h.send(ctx, request.command.Metadata.MessageID, records[index])
242 return fmt.Errorf("publish completed results: %w", err)
249 func (h *commandHandler) special(ctx context.Context, request outputRequest) error {
250 next, err := h.applySpecial(ctx, request)
252 return fmt.Errorf("fail special command: %w", h.fail(ctx, []outputRequest{request}, err))
256 err = h.result(ctx, request.command, protocol.ResultPayload{Status: "completed"})
258 return fmt.Errorf("complete pulse: %w", err)
264 err = h.complete(ctx, []outputRequest{request}, next)
266 return fmt.Errorf("complete voltage command: %w", err)
272 func (h *commandHandler) applySpecial(ctx context.Context, request outputRequest) (snapshot, error) {
273 ctx, cancel := context.WithDeadline(ctx, *request.command.Metadata.ExpiresAt)
277 err := h.connection.owner.client.WriteCoil(ctx, request.point.address, true)
279 return snapshot{}, fmt.Errorf("write pulse: %w", err)
282 return h.connection.state, nil
285 err := h.connection.owner.client.WriteRegister(ctx, request.point.address, request.value)
287 return snapshot{}, fmt.Errorf("write voltage: %w", err)
290 next := h.connection.state
291 next.analog, err = h.connection.owner.client.ReadRegisters(ctx, 0, uint16(len(h.connection.owner.topology.analog)))
293 return snapshot{}, fmt.Errorf("read voltage feedback: %w", err)
296 if len(next.analog) != len(h.connection.owner.topology.analog) {
297 return snapshot{}, fmt.Errorf("incomplete voltage feedback")
300 err = validateVoltage(next.analog)
302 return snapshot{}, fmt.Errorf("validate voltage feedback: %w", err)
305 next.observedAt = time.Now().UTC()
310 func (h *commandHandler) unavailable(ctx context.Context, commands []driver.Command) error {
311 pending, err := h.prepare(ctx, commands)
313 return fmt.Errorf("prepare unavailable commands: %w", err)
316 for _, request := range pending {
317 err = h.result(ctx, request.command, protocol.ResultPayload{
319 Reason: "Modbus device is unavailable",
322 return fmt.Errorf("publish unavailable result: %w", err)