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 finishErr := h.finish(ctx, active)
53 var failure *deviceError
55 if errors.As(finishErr, &failure) {
56 finishErr = fmt.Errorf("finish written commands: %w", failure.err)
59 return fmt.Errorf("reject expired write: %w", errors.Join(err, finishErr))
69 for end < len(pending) && pending[end].point.address == pending[end-1].point.address+1 && !pending[end].command.Expired(time.Now()) {
73 err = h.writeRange(ctx, pending[start:end])
75 return fmt.Errorf("fail coil writes: %w", h.fail(ctx, pending, err))
78 active = append(active, pending[start:end]...)
82 err = h.finish(ctx, active)
84 return fmt.Errorf("finish coil commands: %w", err)
90 func (h *commandHandler) finish(ctx context.Context, active []outputRequest) error {
95 next := h.connection.state
99 next.outputs, err = h.connection.owner.client.ReadCoils(ctx, uint16(len(h.connection.owner.topology.outputs)))
101 return fmt.Errorf("fail coil readback: %w", h.fail(ctx, active, fmt.Errorf("read command coils: %w", err)))
104 if len(next.outputs) != len(h.connection.owner.topology.outputs) {
105 return fmt.Errorf("fail incomplete readback: %w", h.fail(ctx, active, fmt.Errorf("incomplete coil readback")))
108 next.observedAt = time.Now().UTC()
110 err = h.complete(ctx, active, next)
112 return fmt.Errorf("complete coil commands: %w", err)
118 func (h *commandHandler) prepare(ctx context.Context, commands []driver.Command) ([]outputRequest, error) {
119 var pending []outputRequest
121 for _, command := range commands {
122 record, exists := h.connection.owner.ledger.Lookup(command.Metadata.MessageID)
124 err := h.send(ctx, command.Metadata.MessageID, record)
126 return nil, fmt.Errorf("replay result: %w", err)
132 request, reason := newOutputRequest(command, h.connection.owner.topology, h.connection.owner.deviceID, len(commands) > 1)
134 err := h.result(ctx, command, protocol.ResultPayload{
139 return nil, fmt.Errorf("reject command: %w", err)
145 pending = append(pending, request)
151 func (h *commandHandler) send(ctx context.Context, commandID string, record driver.ResultRecord) error {
152 err := h.connection.owner.publisher.Result(ctx, h.connection.owner.deviceID, commandID, record.Metadata, record.Payload)
154 return fmt.Errorf("publish result: %w", err)
160 func (h *commandHandler) result(ctx context.Context, command driver.Command, payload protocol.ResultPayload) error {
161 record, err := h.connection.owner.ledger.Remember(command.Metadata.MessageID, payload)
163 return fmt.Errorf("remember command result: %w", err)
166 err = h.send(ctx, command.Metadata.MessageID, record)
168 return fmt.Errorf("send command result: %w", err)
174 func (h *commandHandler) writeRange(ctx context.Context, requests []outputRequest) error {
175 expires := *requests[0].command.Metadata.ExpiresAt
176 values := make([]bool, len(requests))
178 for index, request := range requests {
179 values[index] = request.value == 1
180 if request.command.Metadata.ExpiresAt.Before(expires) {
181 expires = *request.command.Metadata.ExpiresAt
185 ctx, cancel := context.WithDeadline(ctx, expires)
190 if len(requests) == 1 {
191 err = h.connection.owner.client.WriteCoil(ctx, requests[0].point.address, values[0])
193 err = h.connection.owner.client.WriteCoils(ctx, requests[0].point.address, values)
197 return fmt.Errorf("write output coils: %w", err)
203 func (h *commandHandler) fail(ctx context.Context, requests []outputRequest, cause error) error {
204 records := make([]driver.ResultRecord, len(requests))
206 for index, request := range requests {
207 record, err := h.connection.owner.ledger.Remember(request.command.Metadata.MessageID, protocol.ResultPayload{
209 Reason: "Modbus operation failed",
212 return fmt.Errorf("remember failed commands: %w", errors.Join(cause, err))
215 records[index] = record
218 for index, request := range requests {
219 err := h.send(ctx, request.command.Metadata.MessageID, records[index])
221 return fmt.Errorf("send failed commands: %w", errors.Join(cause, err))
225 return &deviceError{err: cause}
228 func (h *commandHandler) complete(ctx context.Context, requests []outputRequest, next snapshot) error {
229 records := make([]driver.ResultRecord, len(requests))
230 causes := make(map[string]string, len(requests))
232 for index, request := range requests {
235 actual = next.analog[request.point.address]
236 } else if next.outputs[request.point.address] {
240 payload := protocol.ResultPayload{Status: "completed"}
241 if actual != request.value {
242 payload.Status, payload.Reason = "failed", "Output readback differs from command"
245 record, err := h.connection.owner.ledger.Remember(request.command.Metadata.MessageID, payload)
247 return fmt.Errorf("remember output results: %w", err)
250 records[index] = record
251 causes[request.point.id] = request.command.Metadata.MessageID
254 err := h.connection.publish(ctx, next, false, causes)
256 return fmt.Errorf("publish output feedback: %w", err)
259 for index, request := range requests {
260 err = h.send(ctx, request.command.Metadata.MessageID, records[index])
262 return fmt.Errorf("publish completed results: %w", err)
269 func (h *commandHandler) special(ctx context.Context, request outputRequest) error {
270 next, err := h.applySpecial(ctx, request)
272 return fmt.Errorf("fail special command: %w", h.fail(ctx, []outputRequest{request}, err))
276 err = h.result(ctx, request.command, protocol.ResultPayload{Status: "completed"})
278 return fmt.Errorf("complete pulse: %w", err)
284 err = h.complete(ctx, []outputRequest{request}, next)
286 return fmt.Errorf("complete voltage command: %w", err)
292 func (h *commandHandler) applySpecial(ctx context.Context, request outputRequest) (snapshot, error) {
293 ctx, cancel := context.WithDeadline(ctx, *request.command.Metadata.ExpiresAt)
297 err := h.connection.owner.client.WriteCoil(ctx, request.point.address, true)
299 return snapshot{}, fmt.Errorf("write pulse: %w", err)
302 return h.connection.state, nil
305 err := h.connection.owner.client.WriteRegister(ctx, request.point.address, request.value)
307 return snapshot{}, fmt.Errorf("write voltage: %w", err)
310 next := h.connection.state
311 next.analog, err = h.connection.owner.client.ReadRegisters(ctx, 0, uint16(len(h.connection.owner.topology.analog)))
313 return snapshot{}, fmt.Errorf("read voltage feedback: %w", err)
316 if len(next.analog) != len(h.connection.owner.topology.analog) {
317 return snapshot{}, fmt.Errorf("incomplete voltage feedback")
320 err = validateVoltage(next.analog)
322 return snapshot{}, fmt.Errorf("validate voltage feedback: %w", err)
325 next.observedAt = time.Now().UTC()
330 func (h *commandHandler) unavailable(ctx context.Context, commands []driver.Command) error {
331 pending, err := h.prepare(ctx, commands)
333 return fmt.Errorf("prepare unavailable commands: %w", err)
336 for _, request := range pending {
337 err = h.result(ctx, request.command, protocol.ResultPayload{
339 Reason: "Modbus device is unavailable",
342 return fmt.Errorf("publish unavailable result: %w", err)