diff --git a/API Server/internal/plugins/builtin/ankersolix/mqttcards_test.go b/API Server/internal/plugins/builtin/ankersolix/mqttcards_test.go index 4503efc..cc0d846 100644 --- a/API Server/internal/plugins/builtin/ankersolix/mqttcards_test.go +++ b/API Server/internal/plugins/builtin/ankersolix/mqttcards_test.go @@ -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) + } + } +} diff --git a/API Server/internal/plugins/builtin/ankersolix/mqttframe.go b/API Server/internal/plugins/builtin/ankersolix/mqttframe.go index cb9719f..ed08982 100644 --- a/API Server/internal/plugins/builtin/ankersolix/mqttframe.go +++ b/API Server/internal/plugins/builtin/ankersolix/mqttframe.go @@ -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