2b4f4f034da79ff9a476ba0cd78499d0a7ea3dc4
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4d51a34c44 |
The settings card stops emptying when one message is late
A snapshot has two halves and they travel separately: telemetry comes from the trigger, the settings only when the charger has something to say about itself. A read can land with the first and not the second — most often the first read after a reconnect — and the card was seeded from that answer alone, so it collapsed to the one control telemetry happens to carry. That is the state of one message, not the state of the charger. Three things, from the outside in. The card keeps what the charger has reported, per serial, across reads. A value stays until another replaces it. They are its own last word either way, and the same ones the server fills a grouped command's siblings from when a caller leaves them out. A charger that goes quiet is no longer written off for good. The miss counter decides whether a read waits for the settings frame at all, and it only ever rose: three unanswered requests early on and no later read waited again, however freely the charger answered afterwards. The comment said "recently enough"; the code said "ever". Answering clears it now. And the first settings are worth the wait a settings write already gives them. Stale settings and never-reported settings were both allowed four seconds. Stale has something to fall back on; never-reported is the empty card, so it gets the full wait — still bounded by the miss counter, so a charger that truly never answers costs it three times and no more. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7176867eb3 |
Tap the card on the charger and the number fills itself in
The enrolment the Anker app does, done here: 0108 a2=7 opens the reader, 0908 brings back the UID. The frames this sends are byte-for-byte the ones the app was captured sending — checksum included — which is what the new test asserts. Adding and removing now write the charger as well as the account: the device write is the app's own message, the account write is the inferred one that carries the name, and either may fail without the other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
62b0a29598 |
The reader speaks: 0904 is the card list, 0908 is the tap
Named from a live capture on an A5191: 0904 carried the nine UIDs the account's own card list answers with, and 0908 arrived the moment a card touched the reader, carrying its UID — and arrived without one when the window closed empty. 0911 names the OCPP backend the charger is pointed at. A UID is bytes, not a number, so type 0x04 now reads as hex. With the frame log on, the command topic is subscribed too: the app's own commands are the half no capture has seen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a7719fca6a |
A line per frame, for the frames nobody has named
ANKER_MQTT_FRAME_LOG logs every inbound cloud frame with its bytes, decoded or not — the ones this package drops are exactly the ones worth naming, so they are logged before the drop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4ff6242c8f |
The last message in the map, and it reboots the charger
0108 was the one thing in the MQTT inventory nobody had wired: the device power mode, whose single documented value restarts the charger. It is the only way to reboot a charger that is on neither a CSMS nor the local network — which is most of them — so the cloud transport sends it now, and "reset" reaches it too, since that is what the OCPP path has always called the same act. Nothing waits for a confirmation: the device that would send it is the device rebooting, so the command answers at once and says the charger drops off the cloud for about a minute. The gate is unchanged and now covers both spellings — an explicit confirm plus a password step-up, audited either way. Modbus still refuses, because no register does this, but its refusal now names both transports that can rather than only the CSMS. Both clients already had the reset button and its password prompt; they were hidden in every mode that reads the device, which is why the cloud never showed one. Modbus is now the only mode without it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b2d333a63f |
The charger was never asked what it is set to
The trigger buys telemetry and only telemetry, so a charger that has been read a hundred times and commanded none reports amps, volts and nothing else: no schedule, no balancing, no Modbus server, not even its firmware. The message that asks for that half is 0040, and the reference keeps it commented out because the app sends its timestamp without a value type. The app is what the charger answers, so the oddity is reproduced rather than corrected — sent when the settings half is missing or older than ten minutes, waited four seconds for, and after three unanswered requests still sent but no longer waited on. The three settings the panel has and the writer did not — swipe up, swipe down, smart touch — are writable now, which is all eleven of the 0100 commands. Nothing else in the map was missing: every named field of every message was already decoded, and the raw keys the card shows are fields the reference does not name either. Both cards drop a row with nothing in it, which turned a charger that reports only its ceiling into a charger that reports no current range at all. Half a range is still a bound. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2425a8d3d6 |
A field we have no name for is still a field it sent
Two cards on the Charging page were answering with a fraction of what the charger and the account actually report, and in both the losses happened quietly, in a parse that kept the fields it recognised and dropped the rest on the floor. Charger information asked three account-wide views and kept fourteen fields. A charger registered on its own is absent from the site view, which is the only one of the three carrying state, charge power and OCPP status — so exactly the charger that stands alone got the column of dashes, and nothing said why. The per-charger station record, get_evcharger_station_info, is what the mobile app opens when you tap a charger, and it is the one view that answers for a charger outside a station; it is now the fourth view, asked per charger, a failure there costing that charger's row and no more. Alongside it, every field each of the four views sent is kept as attrs, under the cloud's own key, nested objects joined with a dot and arrays carrying their index. First view to answer a key wins, which is the rule the named fields already merged by. Two hundred keys and two hundred and forty runes per value keep a station record with a session list from becoming the whole card. Charger readings lost data twice over. The frame decoder skipped any field byte its per-message map could not name, and a message type with no map decoded to nothing at all; those fields are now kept under the message and the byte they arrived in — 0410.c9 — decoded but unscaled, because a factor is half of a meaning and we do not have the other half. Then the projection read forty-odd names into typed fields and dropped the remainder: sessionStartedAt, the per-phase session energies, the three touch modes, the load-balance monitor and its meter flag, the solar monitor. Those land in extra, and the list maintains itself — the four accessors note every key they read, extra is what is left, and a field modelled later stops appearing there without anyone remembering to remove it. Keeping unnamed fields had one consequence worth guarding. An unmapped message now decodes to something rather than nothing, and ingest stamped settingsAt for anything that was not telemetry — the timestamp a control command waits on to say the charger acknowledged it. A frame we cannot read is not an acknowledgement, so the stamp is now conditional on the message type being one we map, while its fields are kept either way. Both cards show the remainder as what it is: the service's own key, no unit, no translation, no renaming, under a heading that says whose words these are. The blocks appear only when there is something in them, so a Modbus charger's readings card and a charger the cloud says nothing more about are unchanged. Naming one of these fields is a later commit, made from evidence; inventing a label for it today would only make a guess look settled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
90558d60b2 |
What the app can set, the cloud connection can set
The broker transport could move a session along — start, stop, boost, skip the delay, cap the current — and nothing else. Everything the charger is actually configured with sat one field away in the same messages we were already decoding: the schedule it charges on, the plug lock, auto-start, the LED, load balancing, solar charging, and the Modbus server the local transport depends on. Readable, and unreachable. The obstacle was never the cloud, it was the shape of the protocol. A setting is not a register write. It is a *command*, and a command owns a set of fields inside a message type — mostly one, but five own several, and the charger reads the whole command as the new truth. A light-off schedule sent carrying only its switch is a schedule whose start and end have just been set to midnight. So a grouped write resends the siblings the caller did not name, using the values the charger itself last reported, and refuses when it has never reported them. That last part is not caution for its own sake: load balancing and solar charging carry the serial of the meter they watch, and nothing outside the charger knows it. An empty one would be adopted. Those values do not arrive with the telemetry, either. The fast 0410 stream a realtime trigger turns on carries none of them — the settings come on 0405, 0840 and 0900, which the charger sends when it has something to acknowledge. So a grouped write may have to send a trigger first purely to make the charger talk about itself, and says so plainly when even that produces nothing. Everything a caller supplies is encoded before the cloud is touched at all. A request naming one bad value changes nothing rather than half of what it asked for, and a mistyped setting costs a validation error instead of a sign-in, a certificate fetch and a broker connection to be told no. mqttsettings.go holds one table and it is the only place a setting is defined: the wire field, the name a caller uses, the state key its current value comes from, and how a value becomes bytes. The names are the snapshot's own, so a caller can read a status, change one entry and send it back. The existing limit command now builds its frame from that table too rather than encoding field a8 a second time. Reading grew to match. The frame decoder gains the fields the grouped writes must carry back — the two load-balance settings, both monitor serials, the solar monitoring mode — plus the swipe gestures, and the snapshot exposes the rest of what is now writable. One name was wrong and is corrected: field d9 was called chargingMode after the Modbus register at 20088, but the reference has it as the solar charging mode, so it becomes solarChargeMode and moves in beside the solar settings. A mislabelled reading is bad; a mislabelled writable field is worse. Over HTTP it is one action rather than a dozen, because the charger groups the fields anyway: POST .../settings with a settings object, and settings sharing a command travel in one frame instead of overwriting each other. The other two transports refuse it by name and say which one has it, the way they already refuse each other's commands. The audit trail records the values, not just that a write happened — a setting that changes what the charger will draw, or whether it answers on the LAN at all, is worth being able to trace afterwards. Two things worth saying plainly. This is built from the reference project's message maps and checked against its own frame layout, not against hardware — there is no charger on this end to point it at. And modbusEnabled is a loaded gun: writing it off stops the charger serving the register map, and the way back is this transport, or the app. The ignore rule for the local Modbus map artifact widens to the protocol maps that now sit beside it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
576df58776 |
Go the way the owner's phone already goes
Control had two transports and neither fitted the ordinary customer. OCPP waits for the charger to dial in, which needs a public endpoint it can reach, a certificate, and a firmware willing to talk to our CSMS. Modbus TCP dials the charger, which needs the server on the charger's own network. Between them they cover a charger we host and a charger we stand next to; the common case is a charger behind someone else's router, and that had nothing. It was never unreachable, though. The charger holds a connection open to Anker's own broker — it is how the mobile app drives it from anywhere, and it is the mqttStatus register the Modbus snapshot has been reporting all along. So a third control mode joins that broker as the account: get_user_mqtt_info issues a client certificate, mTLS to aiot-mqtt-eu.anker.com:8883, and commands go out on the same topics the app publishes on. Nothing on the customer's side has to be forwarded, addressed or certificated. What travels is not an API call. The payload is a JSON envelope around a base64 binary frame the device itself speaks — marker, little-endian length, message type, name/length/type/value fields, XOR checksum — so mqttframe.go is a codec rather than a client, written from the message maps in anker-solix-api and anchored on the one frame that project documents byte for byte. A frame whose fields do not tile exactly up to the checksum is refused rather than half-read: these arrive over a link we do not control, and a truncated frame must not read as a charger reporting zeros. Two of the charger's habits shape the rest. It publishes nothing unless asked, so a status read arms a telemetry trigger and waits for the next frame, and a poll inside that window answers from what has since arrived. And a broker connection costs a fetched certificate and a TLS handshake while the plugin manager builds a throwaway instance per request — so the connection lives on the account's shared session beside the auth token, for exactly the reason the token lives there, and closes itself after five idle minutes. The transport also sees two signals no other one does: the boost flag, and the plug and start countdowns. The package doc has said since the first commit that they are never set and the derived mode must do without them. Here they are set, so a charger that has been told to start and is counting down a delay says so rather than sitting in "preparing", and "skip the delay" is offered only while there is a delay to skip. The clients generalise instead of growing a second layout. Both snapshots name the same quantities the same way, so what was Modbus-only in the readouts is now whichever transport read the charger — ModbusStatus becomes ChargerStatus on the phone, mb becomes dev on the web. What each transport can be *told* still differs, and the buttons branch on that: reset and clear-limit stay with OCPP, the timeout and phase registers with Modbus, skip-delay with the cloud. A command a transport has no equivalent for is refused by name, saying which one has it. The cost is worth saying plainly. This leans on Anker's cloud being up and on an unofficial protocol the app may change under us, where Modbus leans on nothing but the LAN. And it is checked against the reference implementation's own worked example rather than against hardware — there is no charger on this end to point it at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |