Architecture
The layering of the iso15118 crate: bit-level EXI, generated message sets, message dispatch, the session layer, and the sans-I/O role drivers on top.
The crate is strictly layered, and every layer is usable on its own. v2gtp and sdp are plain byte codecs; exi is a usable schema-informed EXI implementation in its own right.
evcc / secc role drivers: sessions, timers, decisions
session clock, spec timers, message-ordering graphs
message V2GTP payload type + negotiated protocol → typed message
iso2 / iso20 generated message types and codecs
exi schema-informed EXI: documents and fragments
v2gtp framing sdp discovery
slac HomePlug link setup + matching state machines
pnc — Plug & Charge signatures, cross-cuttingSans-I/O, concretely
Every engine takes bytes and a timestamp and hands back bytes, events and deadlines. The clock is session::Instant, a monotonic millisecond count you supply. Nothing reads a clock, opens a socket, or spawns a task.
The payoff is not architectural purity. It is that a whole DC charging session runs as a unit test in microseconds with time as a variable, so every spec timer is a plain assertion rather than a field observation — and that the same protocol logic runs under an async runtime and on a microcontroller with neither.
Two kinds of deadline
A per-message timeout bounds the answer to one request: 250 ms for a DC charge loop, seconds for anything that reaches a back end.
A loop budget bounds a whole phase. Several phases are not one exchange but a repetition the peer keeps making while the answer is ..._Ongoing — authorization waiting on a driver, the DC isolation test, schedule exchange waiting on a tariff. V2G_EVCC_CableCheck_Timeout is that kind, and it is the one implementations leave as a documented constant, because the per-message timeout looks like it already covers the case and does not: it restarts on every repeat, so a station that answers Ongoing promptly for ever is never late.
The budget therefore starts on the first request of a phase and is not restarted by the repeats. Both drivers arm it, because the two sides bound different things: the vehicle bounds how long it waits, the station bounds its own indecision.
...and every deadline is two numbers, not one
Those two things are bounded by different values, and the difference is the point of having both. ISO 15118-2 Table 109 gives the same loop V2G_EVCC_Ongoing_Timeout = 60 s for the vehicle and V2G_SECC_Ongoing_Performance_Time = 55 s for the station, and [V2G2-713] says what the station is to do when its own runs out: answer FAILED and stop the session. The gap is the room in which that answer fits. Table 111 does the same for the rest — 18 s against 20 s for communication setup, 38 against 40 for the DC isolation test, 5 against 7 for the pre-charge.
A core that used one number for both roles would give the station a deadline it can only reach after the vehicle has already abandoned the session: a timer that can never fire in time to say anything, which is the same as not having one. So session::Role is a parameter of every budget, and the drivers pass their own:
flow.loop_timeout(Role::Secc) // the station's half — always the shorter
flow.loop_timeout(Role::Evcc) // the vehicle'sThe ordering within each pair is asserted at compile time, because inverting one would not fail any other test — it would just quietly make the station's timer unreachable.
And once the station's timer can fire in time, it has something to do with the room: [V2G2-713] says a station that runs out of deciding time answers plain FAILED and stops the session, rather than dropping the socket and leaving the vehicle to report a timeout five seconds later. Secc surfaces that as Event::Overdue, carrying the request to answer — and it arrives on the vehicle's next request rather than the instant the timer fires, because a loop budget expires between exchanges, with the station idle and nothing outstanding.
It is deliberately not Event::Refused. That one means the vehicle sent something the flow does not allow and earns FAILED_SequenceError. This one is the station's own indecision, and blaming the vehicle for it in the response code would send a field engineer to the wrong end of the cable.
Half-duplex, because the protocol is
Neither side may send again before the other has answered. The engines enforce that rather than assuming it:
Seccsurfaces one request and reads nothing further untilrespondhas been called. Anything the vehicle sent early waits, bounded by a cap on undrained frames.Evccaccepts a response only if it answers the request that is outstanding.
Both are protocol rules. Both are also what stops one unauthenticated peer from queueing work without bound — several requests are legal repeatedly (a charge loop turns, an authorization retries), and the ordering graph constrains requests only, so without these checks a peer could build an event queue as large as it cared to.
Where the line between protocol and policy is
The engines own framing, decoding, the protocol handshake, session-id stamping and checking, message ordering and the timers. They own nothing about charging: whether to authorize a vehicle, which schedule to offer, how much current to deliver. Those arrive as events and are answered with a message the application builds.
That is where the line is because everything above it is a decision a charge point operator makes and everything below it is a decision ISO made. Only the second kind belongs in a library.
The layers
protocol
Which generation, and which set of them. Protocol is what one session negotiated; Protocols is what a piece of equipment implements — an ordered, Copy, allocation-free set that is also what an EVCC offers and an SECC accepts.
It is its own layer rather than a detail of the handshake because it is the one fact that outlives the session: a charge-detail record, a metric label, a compliance feed and a datasheet all have to name a generation, and every one of them should name it the same way. Protocol::as_str is that name — "din70121", "iso15118-2", "iso15118-20" — with Display, FromStr and serde agreeing on it and refusing the ambiguous bare "iso15118".
exi
Bit I/O over caller-owned slices, the EXI built-in datatypes, the value string table, the grammar runtime and the document/fragment split. This is the layer that touches unauthenticated bytes first, and it is the one with the most adversarial thinking in it. See The EXI profile.
iso2 and iso20
Generated message types and codecs, committed to the repository so a reader can see the codec without running the generator. The generator is still the source of truth — see Code generation.
Generated code carries no state tables. A content model's whole event-code arithmetic is five short integer slices that a shared interpreter drives. The equivalent unrolled state machine for one maxOccurs="2048" particle would be 2049 states, and ISO 15118-20 has several; here each costs one extra u32. That is what makes the generated code fit on an ECU.
message
A V2GTP frame carries a payload type and a blob. Turning that pair into a typed message needs a fact the frame does not contain: which protocol generation the session agreed on. Payload type 0x8001 is the supportedAppProtocol handshake before negotiation, a DIN SPEC 70121 message if DIN won, and an ISO 15118-2 message if -2 won. Sniffing is not an option; the session has to remember. message::Message is that dispatch, done once, in one place.
session
The clock, the spec timers, V2GTP stream framing, the message-ordering graphs — one per protocol generation, shared by both roles — and the ISO 15118-2 rule that a ChargingProfile must fit the SAScheduleTuple it names. The sequencers hold no buffers and no keys, so a session snapshot is small, Clone and serde-serialisable, which is what ISO 15118-20 pause and resume across a power cycle needs. See Sessions and ordering.
secc and evcc
The role drivers: they join a Connection, a Timers set and a Flow into something with an event loop, and surface the decisions the application has to make.
slac and sdp
Below the IP link: which station this cable is plugged into, and which port to connect to. Both are separate engines with the same shape as the session drivers. See SLAC and Discovery and framing.
pnc
Cross-cutting, because a signature covers elements from whichever message set is in use. It computes what to hash and what to sign and checks the result; it contains no cryptography of its own. See Plug & Charge.