0103 writes a card, 0104 asks for the list, 0108 a2=7 opens the reader

Caught on the charger's own command topic while a card was removed and
added back in the Anker app. Enrolling at the charger is three MQTT
messages and no REST call: open the reader, take the UID the tap
publishes, write it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tajniak81
2026-09-03 13:30:14 +02:00
co-authored by Claude Opus 5
parent 62b0a29598
commit 4c73d4ac05
2 changed files with 63 additions and 4 deletions
@@ -122,3 +122,33 @@ func TestCardReadWithoutACard(t *testing.T) {
t.Errorf("rfidCardRead = %v, want it absent", values["rfidCardRead"])
}
}
// The two writes the Anker app sent, caught on the charger's own command topic:
// the same card removed and then added back, the action the only difference.
func TestDecodeCardWriteCommands(t *testing.T) {
for _, tc := range []struct {
name, hex string
action float64
}{
{"delete", "ff091f0003000f0103a10122a2020102a3020101a4080404dfe672151a901f", 2},
{"add", "ff091f0003000f0103a10122a2020101a3020101a4080404dfe672151a901c", 1},
} {
data, err := hex.DecodeString(tc.hex)
if err != nil {
t.Fatal(err)
}
msgType, values, err := decodeFrame(data)
if err != nil {
t.Fatalf("%s: decodeFrame: %v", tc.name, err)
}
if msgType != msgEVCardWrite {
t.Fatalf("%s: msgType = %q, want %q", tc.name, msgType, msgEVCardWrite)
}
if got, _ := values["cardWriteAction"].(float64); got != tc.action {
t.Errorf("%s: cardWriteAction = %v, want %v", tc.name, values["cardWriteAction"], tc.action)
}
if got := values["cardWritten"]; got != "04DFE672151A90" {
t.Errorf("%s: cardWritten = %v, want the UID", tc.name, got)
}
}
}
@@ -92,6 +92,20 @@ const (
// from the account: the same address get_ocpp_info reports, and the source
// number that view leaves as a bare integer.
msgEVOcppInfo = "0911"
// The three commands the app sends about cards, all captured from its own
// traffic on the charger's command topic. The pattern is the connector's
// everywhere: a command 01xx is answered by the data frame 09xx.
//
// 0103 write one card: a2 = 1 add, 2 delete; a4 = the UID -> 0903, then 0904
// 0104 ask for the list -> 0904
// 0111 ask which OCPP backend -> 0911
//
// The fourth is not its own message: opening the reader is the power-mode
// command with powerModeReadCard, which is answered by 0908.
msgEVCardWrite = "0103"
msgEVCardListReq = "0104"
msgEVOcppInfoReq = "0111"
// The reader, reporting a card held against it. Captured during an
// enrol-at-the-charger: the frame arrives the moment the card is tapped,
// carrying the UID, and the charger publishes its updated card list a second
@@ -99,7 +113,14 @@ const (
// closes with nothing tapped, which is what makes the UID field the event.
msgEVCardRead = "0908"
// powerModeRestart is the only value the power-mode command is known to take.
// powerModeReadCard opens the card reader for a tap: the app sends the
// power-mode command with this value and the charger answers 0908 — with the
// UID if a card was held against it inside the twenty seconds, and without one
// if the window closed empty. Captured from the app on an A5191; the same 7
// comes back in the answer.
powerModeReadCard uint8 = 7
// powerModeRestart is the only other value the power-mode command is known to take.
// The map documents 5 and nothing else, so nothing else is sent.
powerModeRestart uint8 = 5
)
@@ -232,6 +253,13 @@ var evCardRead = map[byte]mqttField{
0xa3: {name: "rfidCardRead"},
}
// evCardWrite names the write the app sends. a3 was 1 on both the add and the
// delete, so it keeps its own key rather than a guessed name.
var evCardWrite = map[byte]mqttField{
0xa2: {name: "cardWriteAction"}, // 1 add, 2 delete
0xa4: {name: "cardWritten"},
}
// evOcppInfo names the charger's own view of its OCPP backend.
var evOcppInfo = map[byte]mqttField{
0xa2: {name: "ocppBackendName"},
@@ -244,9 +272,10 @@ var evOcppInfo = map[byte]mqttField{
// settings report: evMessages is what stamps the timestamp a control command
// waits on, and a card list is not an acknowledgement of anything.
var evInfoMessages = map[string]map[byte]mqttField{
msgEVCards: evCards,
msgEVOcppInfo: evOcppInfo,
msgEVCardRead: evCardRead,
msgEVCards: evCards,
msgEVOcppInfo: evOcppInfo,
msgEVCardRead: evCardRead,
msgEVCardWrite: evCardWrite,
}
// evMessages selects a field map by message type. A type absent from here is one