The table the charger actually keeps its measurements in
Modbus mode never returned a reading: every status poll came back as "the charger did not answer", though the charger was answering all along. It was refusing the question. The A5191 splits its map across two tables where the spec's single 2xxxx column suggests one — 20000-20100 are input registers and reject FC03 with an illegal-address exception at every address in the range, while 21000-21005 really are holding registers and read back over FC03. We inferred one space from the spec's layout and asked for all of it with FC03. The client learns FC04, sharing a body with FC03 since the two differ only in which table the server consults, and the plugin's two measurement reads move to it. Writes stay on FC06, where the controls already live. Confirmed against an A5191 on firmware 1.0.6.1: identity, live block and the control registers all decode as the spec tabulates them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f025dc100f
commit
cf4fd14b56
3 files changed
+79
-14
No files matched your search
@@ -71,8 +71,13 @@ func dial(t *testing.T, addr string) *Client {
|
||||
}
|
||||
|
||||
// readReply builds a well-formed FC03 reply carrying the given register values.
|
||||
func readReply(values ...uint16) []byte {
|
||||
out := []byte{fcReadHold, byte(len(values) * 2)}
|
||||
func readReply(values ...uint16) []byte { return replyFor(fcReadHold, values...) }
|
||||
|
||||
// inputReply is the same for FC04, which frames its reply identically.
|
||||
func inputReply(values ...uint16) []byte { return replyFor(fcReadInput, values...) }
|
||||
|
||||
func replyFor(fc byte, values ...uint16) []byte {
|
||||
out := []byte{fc, byte(len(values) * 2)}
|
||||
for _, v := range values {
|
||||
out = binary.BigEndian.AppendUint16(out, v)
|
||||
}
|
||||
@@ -126,6 +131,47 @@ func TestReadHoldingDecodesRegisters(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadInputAsksTheInputTable(t *testing.T) {
|
||||
addr, seen := serve(t, func(pdu []byte) []byte {
|
||||
// A charger that keeps its measurements in input registers answers FC04
|
||||
// and refuses FC03, so replying to the wrong code would hide a mix-up.
|
||||
if pdu[0] != fcReadInput {
|
||||
return []byte{pdu[0] | excMask, 0x02}
|
||||
}
|
||||
return inputReply(0x0102, 0x0304)
|
||||
})
|
||||
c := dial(t, addr)
|
||||
|
||||
got, err := c.ReadInput(context.Background(), 20041, 2)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadInput: %v", err)
|
||||
}
|
||||
if len(got) != 2 || got[0] != 0x0102 || got[1] != 0x0304 {
|
||||
t.Errorf("registers = %v, want [258 772]", got)
|
||||
}
|
||||
|
||||
_ = c.Close()
|
||||
frames := <-seen
|
||||
if len(frames) != 1 {
|
||||
t.Fatalf("server saw %d frames, want 1", len(frames))
|
||||
}
|
||||
if fc := frames[0][mbapLen]; fc != fcReadInput {
|
||||
t.Errorf("function code = 0x%02x, want 0x%02x", fc, fcReadInput)
|
||||
}
|
||||
if a := binary.BigEndian.Uint16(frames[0][mbapLen+1:]); a != 20041 {
|
||||
t.Errorf("address = %d, want 20041", a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadInputRejectsHoldingReply(t *testing.T) {
|
||||
addr, _ := serve(t, func(pdu []byte) []byte { return readReply(0x0102) })
|
||||
c := dial(t, addr)
|
||||
|
||||
if _, err := c.ReadInput(context.Background(), 20041, 1); err == nil {
|
||||
t.Fatal("expected an error when the reply echoes the other function code")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadHoldingRejectsBadCounts(t *testing.T) {
|
||||
addr, _ := serve(t, func(pdu []byte) []byte { return readReply(0) })
|
||||
c := dial(t, addr)
|
||||
|
||||
Reference in new issue
Block a user