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:
co-authored by
Claude Opus 5
parent
f025dc100f
commit
cf4fd14b56
@@ -19,8 +19,12 @@ package ankersolix
|
||||
// rather than letting a caller discover it.
|
||||
//
|
||||
// The spec tabulates addresses, types and gains but does not name the function
|
||||
// codes; the whole map lives in one 2xxxx space with RO and RW entries side by
|
||||
// side, which is the holding-register convention, so FC03/FC06 is what this uses.
|
||||
// codes. An A5191 on firmware 1.0.6.1 answers the measurement block (20000-20100)
|
||||
// on FC04 only — FC03 there is refused with an illegal-address exception, for
|
||||
// every address in the range — while the controls at 21000-21005 do read back
|
||||
// over FC03. So the map is two tables, not the one 2xxxx space it looks like:
|
||||
// input registers for what the charger reports, holding registers for what it
|
||||
// accepts, which is FC04 to read and FC06 to write.
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -76,8 +80,8 @@ const (
|
||||
regPhaseCountSet = 21005 // 0 automatic, 1 fixed single, 2 fixed three
|
||||
)
|
||||
|
||||
// The two blocks read in one request each. Both are well inside FC03's limit of
|
||||
// 125 registers, and splitting them keeps the hot path (live state) small.
|
||||
// The two blocks read in one request each. Both are well inside the 125-register
|
||||
// limit of a read, and splitting them keeps the hot path (live state) small.
|
||||
const (
|
||||
identityStart = regProductNumber
|
||||
identityCount = 41 // 20000-20040
|
||||
@@ -202,14 +206,14 @@ func ModbusDial(ctx context.Context, cfg ModbusConfig) (*modbus.Client, error) {
|
||||
func ModbusRead(ctx context.Context, c *modbus.Client, withIdentity bool) (ModbusSnapshot, error) {
|
||||
var snap ModbusSnapshot
|
||||
|
||||
live, err := c.ReadHolding(ctx, liveStart, liveCount)
|
||||
live, err := c.ReadInput(ctx, liveStart, liveCount)
|
||||
if err != nil {
|
||||
return snap, err
|
||||
}
|
||||
decodeLive(&snap, live)
|
||||
|
||||
if withIdentity {
|
||||
ident, err := c.ReadHolding(ctx, identityStart, identityCount)
|
||||
ident, err := c.ReadInput(ctx, identityStart, identityCount)
|
||||
if err != nil {
|
||||
// Identity is a nicety; live state is the point. Report what we have.
|
||||
return snap, nil
|
||||
|
||||
Reference in New Issue
Block a user