Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"context"
"encoding/binary"
"fmt"
"time"
)

// Logger is the interface to the required logging functions
Expand Down Expand Up @@ -632,12 +633,24 @@ func (mb *client) send(ctx context.Context, request *ProtocolDataUnit) (response
if err != nil {
return
}
aduResponse, err := mb.transporter.Send(ctx, aduRequest)
if err != nil {
return
}
if err = mb.packager.Verify(aduRequest, aduResponse); err != nil {
return

// Wait for correct response ID before throwing error.
var aduResponse []byte
maxTime := time.Now().Add(tcpTimeout)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tcpTimeout is a constant defined in tcpclient.go and is meaningless here.
client.go's send method is shared by all transports (TCP, RTU, ASCII). For an RTU serial client the timeout is serialTimeout. There is no single correct timeout constant to use here; this approach needs to be rethought.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point. Unfortunately it is unlikely I'll have the time for this in the near future.

for {
aduResponse, err = mb.transporter.Send(ctx, aduRequest)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling Send inside a loop re-transmits the request on every iteration. Both the TCP and RTU Send implementations write to the wire/serial port before reading the response, so every loop iteration sends a duplicate request. This is especially dangerous for write operations (writes would be executed multiple times).

The Modbus Serial Line spec defines the correct master behaviour explicitly:

In case of a reply received from an unexpected slave, the Response time-out is kept running.

So the master must keep waiting, not re-send the request. The retry logic belongs inside the transport's read path. For RTU, readIncrementally already has a state machine that could keep reading until it matches the correct slave ID or the deadline expires. For TCP, readResponse already has protocol-recovery logic that could be extended. That way, client.send stays clean and the fix applies correctly regardless of which transport is in use.

if err != nil {
return
}
err = mb.packager.Verify(aduRequest, aduResponse)
if err == nil {
break
}
if time.Now().After(maxTime) {
err = fmt.Errorf("modbus: response timeout")
return
}
Comment on lines +649 to +652
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loop never checks ctx.Done(), so it will ignore context cancellation/deadline and continue running past the context timeout - i guess it was'nt there when you issued the PR. The custom fmt.Errorf("modbus: response timeout") also hides the actual error from Verify, which callers could find useful. The ctx deadline should be the upper bound.

// Else try again until timeout.
}
response, err = mb.packager.Decode(aduResponse)
if err != nil {
Expand Down