Skip to content

Commit ca2ab06

Browse files
tmaCopilot
andauthored
feat: forward vendor function codes (#14)
mbproxy rejected any function code outside the eight standard ones with Illegal Function. Huawei SUN2000 installer login and optimizer file transfer use 0x41, so wlcrs/huawei_solar could not complete setup through the proxy. Unknown function codes are now forwarded as opaque PDUs. They are not cached and not retried. Read-only mode still applies only to the standard write codes. Fixes #13 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0649ac6f-b014-4168-82f9-d3e4eeeafdfc
1 parent 7e36be5 commit ca2ab06

8 files changed

Lines changed: 533 additions & 13 deletions

File tree

‎README.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ A lightweight Modbus TCP proxy with in-memory caching. Designed to reduce load o
77
- **Caching**: In-memory cache with configurable TTL
88
- **Request coalescing**: Identical concurrent requests share a single upstream fetch
99
- **Read-only mode**: Optionally block or ignore write requests
10+
- **Vendor function codes**: Forward non-standard PDUs such as Huawei `0x41` without caching or retrying them
1011
- **Auto-reconnect**: Automatic upstream reconnection on failure
1112
- **Stale data fallback**: Optionally serve stale cache on upstream errors
1213
- **Request diagnostics**: Structured lifecycle timing, retry, exception, and health state
@@ -93,6 +94,11 @@ corresponding success has occurred.
9394
- `true`: Silently ignore write requests, return success response
9495
- `deny`: Reject write requests with Modbus illegal function exception
9596

97+
Read-only mode applies only to the standard write function codes (`0x05`,
98+
`0x06`, `0x0F`, `0x10`). Other function codes, including vendor codes such as
99+
Huawei `0x41`, are forwarded as opaque PDUs. Those requests are not cached and
100+
are not retried.
101+
96102
## Docker Compose Examples
97103

98104
### Basic Setup

‎SPEC.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ Many Modbus devices (inverters, meters, battery systems) have limited polling ca
3636
- `0x06` Write Single Register
3737
- `0x0F` Write Multiple Coils
3838
- `0x10` Write Multiple Registers
39+
- Forward any other function code as an opaque PDU. This covers vendor codes
40+
such as Huawei SUN2000 `0x41` (installer login and file transfer). Opaque
41+
requests are not cached and are not retried.
3942

4043
### 2. Upstream Connection
4144
- Connect to downstream Modbus device via TCP/IP only
@@ -125,6 +128,10 @@ Three modes:
125128
- `true` (default): Silently ignore write requests, return success
126129
- `deny`: Reject write requests with Modbus exception (illegal function)
127130

131+
Read-only mode applies only to the four standard write function codes. Vendor
132+
and other non-standard function codes are always forwarded, because mbproxy
133+
cannot invent a valid response for an unknown PDU.
134+
128135
### 5. Graceful Shutdown
129136
- Handle SIGTERM/SIGINT signals
130137
- Complete in-flight requests before shutdown (with configurable timeout, default: 30s)
@@ -275,6 +282,10 @@ The cache also exposes `Coalesce(ctx, rangeKey, fetch)` for request coalescing.
275282
- Check readonly mode
276283
- If allowed: increment the write generation and invalidate every cached register/coil in the written address range before forwarding upstream
277284
- Return response
285+
5. **For other function codes**:
286+
- Forward the raw PDU upstream
287+
- Do not cache, coalesce, or retry
288+
- Do not invent a local success or exception response unless the PDU itself is missing or malformed
278289

279290
## Logging
280291

‎internal/modbus/client.go‎

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type clientSession interface {
2929
Close() error
3030
SetSlave(byte)
3131
BeginRequest(context.Context, time.Duration) func() error
32+
SendRaw(context.Context, []byte) ([]byte, error)
3233
}
3334

3435
type sessionFactory func() (clientSession, requestClient)
@@ -184,6 +185,47 @@ func (c *tcpSession) SetSlave(slaveID byte) {
184185
c.handler.SetSlave(slaveID)
185186
}
186187

188+
func (c *tcpSession) SendRaw(ctx context.Context, pdu []byte) ([]byte, error) {
189+
if len(pdu) < 1 {
190+
return nil, fmt.Errorf("empty pdu")
191+
}
192+
193+
request := &gridmodbus.ProtocolDataUnit{
194+
FunctionCode: pdu[0],
195+
Data: append([]byte(nil), pdu[1:]...),
196+
}
197+
aduRequest, err := c.handler.Encode(request)
198+
if err != nil {
199+
return nil, err
200+
}
201+
aduResponse, err := c.handler.Send(ctx, aduRequest)
202+
if err != nil {
203+
return nil, err
204+
}
205+
if err := c.handler.Verify(aduRequest, aduResponse); err != nil {
206+
return nil, err
207+
}
208+
response, err := c.handler.Decode(aduResponse)
209+
if err != nil {
210+
return nil, err
211+
}
212+
if response.FunctionCode != request.FunctionCode {
213+
exceptionCode := byte(0)
214+
if len(response.Data) > 0 {
215+
exceptionCode = response.Data[0]
216+
}
217+
return nil, &gridmodbus.Error{
218+
FunctionCode: response.FunctionCode,
219+
ExceptionCode: exceptionCode,
220+
}
221+
}
222+
223+
out := make([]byte, 1+len(response.Data))
224+
out[0] = response.FunctionCode
225+
copy(out[1:], response.Data)
226+
return out, nil
227+
}
228+
187229
func (c *tcpSession) BeginRequest(ctx context.Context, attemptTimeout time.Duration) func() error {
188230
c.handler.Timeout = attemptTimeout
189231

@@ -821,7 +863,13 @@ func ValidateRequest(req *Request) error {
821863
return newValidationError(ExcIllegalValue, "write data has %d bytes, expected %d", len(req.Data), expected)
822864
}
823865
default:
824-
return newValidationError(ExcIllegalFunction, "unsupported function code: 0x%02X", req.FunctionCode)
866+
if len(req.PDU) < 1 {
867+
return newValidationError(ExcIllegalFunction, "missing pdu for function code: 0x%02X", req.FunctionCode)
868+
}
869+
if req.PDU[0] != req.FunctionCode {
870+
return newValidationError(ExcIllegalFunction, "pdu function code 0x%02X does not match request 0x%02X", req.PDU[0], req.FunctionCode)
871+
}
872+
return nil
825873
}
826874
if uint32(req.Address)+uint32(req.Quantity) > 65536 {
827875
return newValidationError(ExcIllegalAddress, "address range exceeds 0xFFFF")
@@ -882,7 +930,7 @@ func (c *Client) executeRequest(ctx context.Context, req *Request) ([]byte, erro
882930
}
883931
return c.buildWriteResponse(req.FunctionCode, req.Address, results), nil
884932
default:
885-
return nil, fmt.Errorf("unsupported function code: 0x%02X", req.FunctionCode)
933+
return c.session.SendRaw(ctx, req.PDU)
886934
}
887935
}
888936

0 commit comments

Comments
 (0)