Skip to main content

Crate iso15118

Crate iso15118 

Source
Expand description

ISO 15118 — vehicle-to-grid communication in pure Rust.

The protocol an electric vehicle and a charging station speak over the CCS charging cable while deciding whether to move 350 kW between them: HomePlug link setup, SECC discovery, V2GTP framing, a schema-informed EXI codec generated from the official schemas, the ISO 15118-2 and ISO 15118-20 message sets, the message-ordering rules and timers of both, session engines for either role, and the Plug & Charge signature profile.

The two sides are named throughout as the standard names them: the EVCC is the vehicle’s communication controller and the SECC is the charging station’s.

Narrative documentation, including the exact EXI profile and how the wire format is verified, is at https://hupe1980.github.io/iso15118/.

§Sans-I/O

Nothing here opens a socket, reads a clock, or blocks. Protocol engines take bytes and a timestamp and hand back bytes, events and deadlines; the caller owns the transport, the clock, the randomness and the crypto. That is what lets the same code drive a Tokio-based charge point back end and a bare-metal EVCC on a microcontroller — and it is why a whole DC charging session runs as a unit test in microseconds, with time as a variable.

Every engine — secc::Secc, evcc::Evcc, sdp::Discovery, slac::matching::Ev and slac::matching::Evse — is driven the same way. The names differ only where the job does:

StepSession enginesDiscovery and SLAC
Bytes from the wirehandle_input(now, &bytes)handle_datagram / handle_frame
What happenedpoll_event()poll_event()
Your answerrespond (SECC) / request (EVCC)— the engine decides
Bytes for the wiretake_transmit()poll_transmit()
The next deadlinepoll_timeout(), then handle_timeout(now)same

The two column headings are a distinction worth keeping: a session engine consumes a stream and has to reassemble it, so it takes whatever bytes arrived and returns everything queued. Discovery and SLAC consume complete datagrams and Ethernet frames, where a boundary is meaningful, so they take one and hand back one at a time.

The answering step differs because the protocol does. The vehicle drives: it chooses the next request, and the charger only ever answers.

An error from handle_input has already ended the session. The engine closes itself — timers disarmed, buffers dropped, Event::Closed queued — before the error is returned, because a byte stream that stopped being a V2G session cannot be resynchronised: V2GTP has no delimiter to scan for, and after the handshake the negotiated grammar is the only reading of the bytes there is. So the error is for the log, not for a decision.

§Whose key, and which session

Plug & Charge is two independent questions, kept apart because answering one and implying the other is the dangerous shape. pnc answers “is this signature about this session” — the GenChallenge binding, in the same call as the signature, with no way to ask for only one. pnc::pki answers “is this key one to trust” — RFC 5280 path validation under ISO 15118’s own Annex F certificate profiles.

Neither answers whether a certificate was revoked: there is no OCSP here, for reasons pnc::pki sets out.

§Protocol, not policy

The engines own framing, decoding, the protocol handshake, session-id checking, message ordering and the spec 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. Everything above that line is a decision a charge point operator makes; everything below it is a decision ISO made.

Ordering includes what the standard says about stopping, and the three values of ChargingSession do not say the same thing. A FAILED_* response ends the session and leaves only SessionStopReq; a vehicle may Terminate from any established phase, because an abort is exactly the case where it cannot do the tidy thing first; but Pause takes the transport connection down with it [V2G2-739] and so is refused while the cable is live, and -20’s ServiceRenegotiation returns the flow to just after authorization and so needs a service already selected — accepted earlier it is not a shortcut but an authorization bypass.

Ordering also includes renegotiating: revising the schedule mid-charge keeps the service, the authorization and the power flow, and does not send a DC session back through its isolation test. See session.

§Plug & Charge needs TLS, and the core will not take your word for it

ISO 15118-2 forbids the Plug & Charge message sets on an unsecured session outright — [V2G2-634] to the station, [V2G2-635] to the vehicle, with [V2G2-633] leaving such a session external identification and nothing else. A sans-I/O core cannot observe the transport, so secc::SeccConfig::security and evcc::EvccConfig::security state it, and the ISO 15118-2 ordering graph refuses a contract selection without it.

The default is session::Security::None: the safe default for a security decision is the restrictive one, so forgetting produces a refused contract rather than a certificate chain on a plaintext wire.

§Half-duplex, and what that is worth

Neither side of a V2G session may send again before the other has answered, so the engines do not read ahead: secc::Secc surfaces one request and reads nothing further until secc::Secc::respond has been called, and evcc::Evcc accepts a response only if it answers the request that is outstanding. Both are protocol rules, and both are also what stops one unauthenticated peer from queueing work without bound — several requests are legal repeatedly, and the ordering graph constrains requests only.

The rule is symmetric, so the enforcement is too, and this is the half that is easy to leave out: evcc::Evcc::request refuses a second request while the first is unanswered, and secc::Secc::respond refuses a response that answers nothing or answers the wrong question. The ordering graph cannot catch either — it constrains which request, not when.

§You never write a session id

The charger assigns one in SessionSetupRes and every message of the session repeats it, so it is not a per-message decision. secc::Secc::respond stamps it into every response and evcc::Evcc::request into every request — the all-zero id before setup, the assigned one after. Leave the field empty and the driver fills it.

Rejoining a paused session is the one case where the vehicle picks the id, and it says so once in evcc::EvccConfig::rejoin. That is worth more than it looks: ISO 15118-2 tolerates a short or absent SessionID and ISO 15118-20 requires exactly eight bytes, so a field left to the application is a field that is right in one generation and wrong in the other.

§Layering

Each layer is usable on its own — v2gtp and sdp are plain byte codecs, and exi is a standalone schema-informed EXI implementation.

evcc / secc      role drivers: sessions, timers, decisions
session          clock, spec timers and loop budgets, 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
                 protocol — which generation, cross-cutting
                 pnc — Plug & Charge signatures, cross-cutting

§Feature flags

Everything is additive — every combination compiles, and every flag gates real code.

GroupFlags
Environmentstd (default; the core is no_std + alloc)
Rolesevcc, secc
Protocolsiso2, iso20 (= iso20-ac+iso20-dc+iso20-wpt+iso20-acdp)
Linksdp, slac
Securitypnc, pnc-rustcrypto
Integrationserde, tracing

alloc is a requirement rather than a feature: in bit-packed EXI a string is a run of bit-shifted code points, so a decoded value can never borrow from the input. A role driver needs a protocol to drive, so secc or evcc on their own compile and gate nothing.

§Which generation, and how to say so

“ISO 15118” names two incompatible protocols. Protocol is the one a session settled on; Protocols is the set a piece of equipment implements, which is a different question and the one a datasheet or a regulatory feed asks.

Protocol::as_str gives a short stable name — "din70121", "iso15118-2", "iso15118-20" — and FromStr reads it back, so a charge-detail record, a metric label or a database column can hold the answer without every consumer inventing its own spelling. The generation is in the name on purpose: the vocabularies around this protocol are not so careful, and one of them carries legal weight. See protocol.

use iso15118::{Protocol, Protocols};

assert_eq!(Protocol::Iso20.as_str(), "iso15118-20");
assert_eq!("iso15118-2".parse(), Ok(Protocol::Iso2));

// A whole set round-trips too — a configuration file's worth.
let station: Protocols = "iso15118-20,iso15118-2".parse()?;
assert_eq!(station, Protocols::ISO);
assert_eq!(station.best(), Some(Protocol::Iso20));

§Example: the protocol handshake

The vehicle offers the protocols it speaks; the charger picks one. This is the first EXI-encoded exchange of every session, whichever generation wins — and secc::Secc runs it for you, because it is pure protocol.

use iso15118::Protocol;
use iso15118::app_protocol::{SupportedAppProtocolReq, SupportedAppProtocolRes};
use iso15118::exi::ExiDocument;

// EVCC: advertise -20 first, then -2 as a fallback.
let req = SupportedAppProtocolReq::advertising([Protocol::Iso20, Protocol::Iso2]);
let on_the_wire = req.to_vec()?;

// SECC: this charger only does ISO 15118-2.
let req = SupportedAppProtocolReq::from_bytes(&on_the_wire)?;
let agreed = req.negotiate(Protocol::Iso2).expect("a protocol in common");
assert_eq!(agreed.protocol, Protocol::Iso2);

let res = SupportedAppProtocolRes::accept(agreed);
assert!(res.response_code.is_ok());

Re-exports§

pub use protocol::ParseProtocolError;
pub use protocol::Protocol;
pub use protocol::Protocols;

Modules§

app_protocol
The supportedAppProtocol handshake (ISO 15118-2 §8.2, Annex C).
evccevcc
The vehicle’s side of a session — sans I/O.
exi
Schema-informed EXI codec — the layer every V2G message rides on.
iso2iso2
ISO 15118-2 message set: one V2G_Message root over a body choice of thirty-four messages.
iso20iso20-common
ISO 15118-20 message sets, one module per schema. ISO 15118-20 message types, generated from the V2G XSD schemas.
message
One type for “a V2G message”, whichever schema set it came from.
pncpnc
Plug & Charge: the ISO 15118 profile of XML Signature, over EXI.
protocol
Which generation of the protocol a session speaks — and which set of them a piece of equipment implements.
sdpsdp
SECC Discovery Protocol (ISO 15118-2 §7.9, ISO 15118-20 §9.4).
seccsecc
The charging station’s side of a session — sans I/O.
session
Session state: the clock, the spec timers, and the message-ordering rules.
slacslac
SLAC — Signal Level Attenuation Characterization (ISO 15118-3).
v2gtp
V2G Transfer Protocol framing (ISO 15118-2 §7.8.3, ISO 15118-20 §9.3).

Enums§

Error
Anything that can go wrong below the state machines.

Constants§

MAX_EXI_PAYLOAD_LEN
Default ceiling on the EXI payload of a single V2GTP frame.

Type Aliases§

Result
Result alias used across the crate.