Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b06173d
feat: implement protocol recovery and connection handling in serial t…
sansmoraxz Dec 4, 2025
81d1a8b
Lint issue fix
sansmoraxz Dec 4, 2025
8fb2118
feat: add PTY tests for RTUSerialTransporter functionality
sansmoraxz Dec 7, 2025
ea9f466
feat: add PTY tests for ASCIISerialTransporter functionality
sansmoraxz Dec 7, 2025
c4c7d0d
Merge branch 'master' into feat/serial-recover
sansmoraxz Feb 19, 2026
673dfe2
consistent ascii and rtu serial recovery process with tcp
sansmoraxz Mar 27, 2026
5ed3de9
Merge remote-tracking branch 'upstream/master' into feat/serial-recover
sansmoraxz Mar 27, 2026
21047b5
refactor: recovery helper for connections
sansmoraxz Mar 27, 2026
19c2260
rtuclient: aduResponse nil guard
sansmoraxz Mar 27, 2026
3897339
expanded rtu test cases for non-happy paths
sansmoraxz Mar 27, 2026
fbe2cb7
drop darwin support for rtu tests
sansmoraxz Mar 27, 2026
a2f29cd
rewrap deadline error for rtu
sansmoraxz Mar 27, 2026
81ce48c
set rtu conn deadline before each read request instead of shared dead…
sansmoraxz Apr 8, 2026
15bfbc8
remove ECONNRESET check from serial as it's tcp only
sansmoraxz Apr 8, 2026
c931e8d
aduResponse nil check for ASCII before print
sansmoraxz Apr 8, 2026
8ec9651
remove darwin support for ascii tests
sansmoraxz Apr 17, 2026
73c84c3
doc: serial connection session management by the open caller
sansmoraxz Apr 17, 2026
b66461d
chore: remove commented code
sansmoraxz Apr 17, 2026
8e346d6
wrap ascii error
sansmoraxz Apr 17, 2026
a8fd209
Add retry interval for serial connections to avoid link flapping
sansmoraxz Apr 17, 2026
8a387f7
Add tests for serial port reconnect behavior with retry intervals
sansmoraxz Apr 17, 2026
980c964
Refactor reconnect test cases for link recovery timeout handling
sansmoraxz Apr 17, 2026
a2ab32d
ASCII pty driven tests
sansmoraxz Apr 17, 2026
b6ab4a6
ASCII decoding and reading tests
sansmoraxz Apr 17, 2026
b79526a
support serial device hot plug
sansmoraxz Apr 17, 2026
e0c387e
hot plug test recovery
sansmoraxz Apr 17, 2026
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
89 changes: 89 additions & 0 deletions ascii_transport_test.go
Comment thread
sansmoraxz marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//go:build darwin || linux || freebsd || openbsd || netbsd
Comment thread
sansmoraxz marked this conversation as resolved.
Outdated
// +build darwin linux freebsd openbsd netbsd

// Copyright 2014 Quoc-Viet Nguyen. All rights reserved.
// This software may be modified and distributed under the terms
// of the BSD license. See the LICENSE file for details.

package modbus

import (
"bytes"
"context"
"testing"
"time"
)

func TestASCIISerialTransporter_Send_PTY(t *testing.T) {
master, slavePath, err := openPTY()
if err != nil {
t.Skipf("Skipping PTY test: %v", err)
}
defer master.Close()

// Request: 01 03 00 00 00 01 (Read Holding Registers)
// ASCII: :010300000001FB\r\n
reqASCII := []byte(":010300000001FB\r\n")

// Response: 01 03 02 00 00
// ASCII: :0103020000FA\r\n
respASCII := []byte(":0103020000FA\r\n")

transporter := &asciiSerialTransporter{}
transporter.Address = slavePath
transporter.BaudRate = 19200
transporter.Timeout = 1 * time.Second
transporter.IdleTimeout = serialIdleTimeout

// Start a goroutine to read request and write response to master
go func() {
buf := make([]byte, 1024)
n, err := master.Read(buf)
if err != nil {
return
}
if !bytes.Equal(buf[:n], reqASCII) {
// t.Errorf would be racy here, just log or ignore
return
}
// Write response
_, err = master.Write(respASCII)
if err != nil {
t.Errorf("Failed to write response: %v", err)
}
}()

ctx := context.Background()
aduResponse, err := transporter.Send(ctx, reqASCII)
if err != nil {
t.Fatalf("Send failed: %v", err)
}

if !bytes.Equal(aduResponse, respASCII) {
t.Errorf("Expected response %s, got %s", respASCII, aduResponse)
}
}

func TestASCIISerialTransporter_Timeout_PTY(t *testing.T) {
master, slavePath, err := openPTY()
if err != nil {
t.Skipf("Skipping PTY test: %v", err)
}
defer master.Close()

reqASCII := []byte(":010300000001FB\r\n")

transporter := &asciiSerialTransporter{}
transporter.Address = slavePath
transporter.BaudRate = 19200
transporter.Timeout = 100 * time.Millisecond
transporter.IdleTimeout = serialIdleTimeout

// Don't write anything to master

ctx := context.Background()
_, err = transporter.Send(ctx, reqASCII)
if err == nil {
t.Fatal("Expected timeout error, got nil")
}
}
53 changes: 44 additions & 9 deletions asciiclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"context"
"encoding/hex"
"fmt"
"io"
"time"
)

Expand Down Expand Up @@ -181,17 +182,52 @@ func (mb *asciiSerialTransporter) Send(ctx context.Context, aduRequest []byte) (
mb.lastActivity = time.Now()
mb.startCloseTimer()

// Send the request
mb.logf("modbus: send % x\n", aduRequest)
if _, err = mb.port.Write(aduRequest); err != nil {
connDeadline := time.Now().Add(mb.Timeout)
linkRecoveryDeadline := time.Now().Add(mb.LinkRecoveryTimeout)

for {
// Send the request
mb.logf("modbus: send % x\n", aduRequest)
if _, err = mb.port.Write(aduRequest); err != nil {
if mb.shouldRecover(err) {
if err = mb.reconnect(ctx, err, linkRecoveryDeadline); err != nil {
return
}
continue
}

return
}
// Get the response
aduResponse, err = readASCII(mb.port, connDeadline)
mb.logf("modbus: recv % x\n", aduResponse)
Comment thread
sansmoraxz marked this conversation as resolved.
Outdated
if err != nil {
if mb.shouldRecover(err) {
if err = mb.reconnect(ctx, err, linkRecoveryDeadline); err != nil {
return
}
continue
}
// Unknown error
mb.logf("modbus: read error: %v", err)
return
}

return
}
// Get the response
}

func readASCII(r io.Reader, deadline time.Time) ([]byte, error) {
var n, length int
var data [asciiMaxSize]byte
var err error

for {
if n, err = mb.port.Read(data[length:]); err != nil {
return
if time.Now().After(deadline) {
return nil, context.DeadlineExceeded
Comment thread
sansmoraxz marked this conversation as resolved.
Outdated
}
if n, err = r.Read(data[length:]); err != nil {
return nil, err
}
length += n
if length >= asciiMaxSize || n == 0 {
Expand All @@ -204,9 +240,8 @@ func (mb *asciiSerialTransporter) Send(ctx context.Context, aduRequest []byte) (
}
}
}
aduResponse = data[:length]
mb.logf("modbus: recv % x\n", aduResponse)
return

return data[:length], nil
}

// writeHex encodes byte to string in hexadecimal, e.g. 0xA5 => "A5"
Expand Down
Loading