Service traits and identifiers¶
How an application declares what it supports, and why the compiler is the completeness check.
A server supporting two services. Dashed edges are impl blocks the
application writes; the dispatch impl is generated.¶
WriteDataByIdentifier hangs unimplemented deliberately: nothing in that diagram is
reachable by the dispatcher unless it appears in the uds_server! list. That is not a
limitation of the design so much as a fact about Rust, and UDSSVC_ARCH_0013 explains why
it forces an explicit assembly step.
The traits¶
A service supported by the server is declared by implementing that service’s trait. Each trait carries its own associated types and its own handler method. impl ReadDataByIdentifier for Ecu {
type Did = MyDid;
const MAY_RESPOND_PENDING: bool = false;
const MAX_DIDS_PER_REQUEST: usize = 4;
async fn read(
&mut self,
did: Self::Did,
out: &mut ResponseSink<'_>,
) -> Result<(), Nrc> { /* ... */ }
}
There is no Rationale: a server’s vocabulary must stay local to the services it actually supports: a
server with no routines must not have to name a routine-identifier type, and a server
supporting one service must not carry the vocabulary of all of them. Per-service traits
deliver this directly, because each trait’s associated types exist only where that trait
is implemented. They also keep each service independently testable and make the surface
discoverable — “implement |
Architecture Element: Assembly is explicit, and the macro is the crate's const evaluator UDSSVC_ARCH_0013
|
A server type lists the services it supports in one place, and that list generates the dispatch: uds_server! {
Ecu: ReadDataByIdentifier, SecurityAccess, DataTransfer;
transport = DoIpTransport<Entity<'static, TcpAcceptor, 1, 4096>, 2>,
peers = 1,
server = EcuServer,
}
Three numbers meet in that line, and are not the same number. At a sensor serving one
tester, the entity’s A service absent from the list is not supported, and a request naming it settles with
Rationale: Rust cannot ask whether a type implements a trait, so a generic dispatcher
over The load-bearing reason assembly is a macro is const evaluation, not explicitness,
and this element understated it for as long as it named only the dispatch match. An
associated const of a generic parameter is not usable as an array length:
The macro is The list also makes the set of supported services readable directly in the application, without inferring it from which methods were overridden. The trait the macro implements is ``ServiceSet``, and it was called ``UdsServer``
here until the driver landed. |
Each service trait whose handler can be in progress when a deadline passes carries an
associated constant, with no default, stating whether that service may answer
impl ReadDataByIdentifier for Ecu {
/// Annex A: only where the server cannot receive further requests
/// from the client while completing this service.
const MAY_RESPOND_PENDING: bool = false;
type Did = MyDid;
async fn read(&mut self, did: MyDid, out: &mut ResponseSink<'_>)
-> Result<(), Nrc> { .. }
}
Rationale: the constant has no default because both defaults are traps, and choosing
between them is choosing which failure to ship. A default of It is a constant rather than a method because the value is a property of the service as implemented, not of the request or the server’s current state: admissibility folds at compile time, and ISO 14229-2:2021 REQ 5.6’s unsupported-service case needs no runtime check at all, since a service that is not implemented has no impl to read the constant from. Two services do not carry it, and cannot. A response-pending is what the driver
sends while it is still awaiting a handler, so a service with nothing awaited has no
window in which one could come due. This is a statement about the shape of the trait, not about the two services. If
|
Protocol state¶
Some of ISO 14229-1’s behaviour cannot be decided from one request. A server cannot
produce It is created by the assembly step of uds_server! {
Ecu: ReadDataByIdentifier, SecurityAccess, DataTransfer;
transport = DoIpTransport<Entity<'static, TcpAcceptor, 1, 4096>, 2>,
peers = 1,
server = EcuServer,
}
Rationale: Two scopes, and the standard draws the line, not convenience.
DTC setting and communication control are not in that list, though they are
server-global too. Their state is the effect itself — DTC status updates stopped,
normal messages disabled — which the application performs and this crate cannot, so it
holds them, and resumes them on the transitions Annex J corroborates the keying without being the authority for it. It is informative, so
it obliges nothing, but it is where clause 8.7.6 sends a reader asking how multiple
clients are handled, and its J.2 recommends that “a unique Address Information should be
assigned to each communication participant to allow the detection of different clients” —
which is Those two clauses rule in opposite directions on the same question, so a design that collapses them is wrong whichever way it collapses. The per-channel scope is why the driver passes the addressing triple at all
( This sentence used to read “the per-channel scope is why Sizing is declared, not allocated. That choice is now forced rather than preferred, and the reason is worth recording
because this element argued it the weaker way. It used to say that caller-supplied
storage — the shape Where the state lives, and why the application cannot reach it. The assembly takes
What this element does not settle is what happens to this state on a session
transition. ISO 14229-1:2020 10.2 requires a return to |
ISO 14229-1 clause 15 specifies a transfer as a sequence with one live instance, a
counter, and four codes that report departures from it. All of that is bookkeeping, so
all of it is this crate’s under
The transfer state machine. Every transition labelled with a code is settled by this crate; the handler is not called.¶ The counter is arithmetic, and the wrap is the part to get right. Clause 15.4.1
initialises it to one on A repeated block is not an error, and the two directions differ. Clause 15.4.4
attaches the same sentence to both 0x24 and 0x73: “the repetition of a What the application supplies, and nothing more: whether the
``maxNumberOfBlockLength`` is an associated const, not a value a handler returns, and
the change removes a disagreement rather than saving a parameter. Clause 15.2.3.2 obliges
the server to report the number in its Direction is carried by ``TransferRequest`` rather than by a separate field, which is
what makes the paragraph above structural instead of advisory. The repeated-block rule
splits by direction — a repeated download is answered without calling the handler, a
repeated upload calls it again — so direction is not an attribute of a request that
happens to be worth knowing; it is the thing that selects the behaviour. As an enum,
This element is the first instance of One reading is flagged rather than assumed. Clause 15.5.4 gives
|
Architecture Element: The security access sequence is a state machine this crate owns UDSSVC_ARCH_0037
|
Annex I is normative and specifies security access as a four-state chart. Every state and
every transition in it is sequencing, so all of it is this crate’s under
Annex I’s four states. Labels name the code this crate settles with; the application is consulted only for a seed and a verdict on a key.¶ Any refused request discards the seed, not only a failed key. Table I.2’s rows for
transitions 9 and 10 include a The restart rule is the chart’s shape, and an earlier draft had it wrong. Clause 10.4
states that “an invalid key shall require the client to start over from the beginning
with a SecurityAccess ‘requestSeed’ message as specified in Annex I”, and Figure I.1
implements it as topology rather than as a note: transition 9 leaves state B for state
A, and transition 10 leaves state D for state C, on every An earlier version of this element drew those two transitions as self-loops, which left
the seed live and let a client retry keys against it indefinitely. That is the behaviour
the restart rule exists to prevent, and it was reachable because the element was written
from Table I.2 without Figure I.1, which is an image in the markdown conversion of the
standard. Five rules an application would have to rediscover, four of which this crate enforces. The paired sub-function. Clause 10.4.2 makes A defect was found and fixed in that pairing during implementation, and it is recorded
here because the way it hid is more instructive than the arithmetic.
The bound is now odd values below ``0x7F``, and it is derived rather than patched to
the failing case. A sub-function byte is seven bits, because bit 7 is
The zero seed. A Relock before unlock. Transition 10 has a successful key at a level other than the unlocked one “lock currently unlocked security level” before unlocking the new one. That is how “only one security level shall be active at any instant of time” is implemented, and it is invisible unless read. The attempt threshold. A wrong key under the limit is The seed policy is the application’s. Table I.1 defines It is the one rule here this crate does not enforce, because the seed’s bytes are the
application’s and this crate keeps none: What the application supplies, and why the split falls here rather than elsewhere.
Annex I Table I.1 marks It also supplies a verdict on the optional pre-conditions Table I.2 conditions
transitions 4 and 7 on, whose failure is It also supplies the storage, and that one is forced by this crate’s own shape rather
than by the annex. Annex I writes persistence as “store /// Attempts for `level`, as last stored. Read at the start of an exchange.
fn attempts(&self, level: u8) -> u8;
/// Store the count this crate computed. The application never computes one.
fn store_attempts(&mut self, level: u8, count: u8);
/// The delay timer's state for `level`: running, expired since last asked, or idle.
fn delay(&mut self, level: u8) -> Delay;
/// Begin the delay this crate decided is owed.
fn start_delay(&mut self, level: u8);
The application persists and times; it never decides. The timer reports its expiry; the crate does not infer it. An earlier draft took
“count at the limit, no delay running” to mean the delay had run out, and reset the
count. That is also the state after a restart: the count survives in non-volatile
storage, a RAM timer does not, and the next An earlier version of this element argued the storage seam from “Annex I requires
Note The source check this element asked for has been done (2026-09-16), and it found
one defect. Annex I’s normative content is Figure I.1 — an image, not text — plus
Table I.2 in disjunctive normal form, and the markdown conversion of the standard
mangles the table and drops the figure entirely. Both were recovered from the licensed
PDF with Outcome: the four rules this element already stated are confirmed verbatim, and the state chart’s topology was wrong — transitions 9 and 10, corrected above. That is precisely the part the earlier warning said had not been read, which is the argument for writing such warnings at all. One correction to that warning’s own claims. |
Architecture Element: A session transition is classified here and applied to configuration state UDSSVC_ARCH_0038
|
|||||||||||||||||||||||||
Several services change server behaviour that outlives their own exchange — the
The four classes are Figure 7’s Key notes 1 to 4. An earlier version of this element cited them as “Figure 6 Key”, which is clause 8.7.3.1’s SubFunction figure and has no such notes.
Three things in that table are worth stating in words, because each is a plausible wrong
guess. The event verb is different in all four rows — pause, stop and resume are distinct
operations, not synonyms. The two middle rows disagree about
Relocking cascades. Clause 10.2 has the locking of security access “reset any active
diagnostic functionality that was dependent on security access to be unlocked (e.g.
active inputOutputControl of a DID)”. So the relock of Rationale: the classification is a pure function of the previous and next session values, which this crate has, and misclassifying it is the whole failure mode. So this crate classifies and hands down the class, with the session entered beside it but never the pair of raw values to classify: pub enum SessionTransition {
DefaultToDefault,
DefaultToNonDefault,
NonDefaultToNonDefault,
NonDefaultToDefault,
}
/// Called after the positive response to DiagnosticSessionControl, and on
/// session timeout. Also called with `security_relocked` when a transition
/// relocked a level, so functionality gated on it can be dropped.
fn on_transition(
&mut self,
t: SessionTransition,
entered: DiagnosticSessionType,
security_relocked: bool,
);
An application receiving a classified transition cannot mistake a same-session re-entry
for a no-op, which is what it would do given two session bytes and clause 10.2 to read.
The session entered is handed down too, because what a class owes can turn on it: the
Timing. Clause 10.2’s rule 1 and clause 10.4’s transition 6 both act on the session
having been accepted, and clause 10.2 states the server “shall stop the current
diagnostic session when it has sent the DiagnosticSessionControl positive response
message”. So the transition is applied after the response is written, not before, and a
What is blocked, and on what. The table names five bodies of state, and this crate
can hold only those whose services have message types to dispatch over.
The consequence is sharper than “not yet supported”. Those requests decode to
Until those message types exist, the periodic and event columns are rules with nothing to
apply them to, and |
Identifiers¶
The identifier traits sit to one side of both roles rather than inside either. That is the whole point of them, and it is the thing a list of signatures does not show:
One vocabulary, bound by both roles. The record types supply their own extent through the codec, so no length is written anywhere.¶
Data identifiers, routine identifiers and session types are supplied by the application as enumerations implementing this crate’s identifier traits. Those traits are free-standing and role-neutral: they belong to neither the client nor the server, and implementing one requires implementing no service trait. pub trait DataIdentifier: Copy + Eq {
/// The wire value.
fn as_u16(self) -> u16;
/// From the wire. `None` means this application does not support it.
fn from_u16(value: u16) -> Option<Self>;
/// Split this identifier's data record off the front of `buf`.
fn split_record<'a>(self, buf: &'a [u8])
-> Result<(&'a [u8], &'a [u8]), RecordError>;
}
A service trait binds one as an associated type, as // server
impl ReadDataByIdentifier for Ecu {
type Did = MyDid;
async fn read(&mut self, did: MyDid, out: &mut ResponseSink<'_>)
-> Result<(), Nrc> { .. }
}
// client: implements no service trait, and names no vocabulary type
let response = client
.read_data_by_identifier(Address(0x0E80), &[MyDid::VehicleSpeed])
.await?;
Rationale: ISO 14229-1 fixes identifier ranges and a small set of standardised values,
not the catalogue: data and routine identifiers are vehicle-manufacturer or
system-supplier specific, so the library cannot own those enumerations without either
being wrong or being a Making It returns
Why the traits are free-standing rather than tied to a role. Client code implements
no server trait and must still be able to name an identifier type ( The traits are also kept separate rather than grouped into one covering all three kinds,
for the same reason This is the completeness check the design is aiming for, and it is worth being precise
about what it does and does not give. Adding a variant to the application’s identifier
enumeration breaks that application’s own exhaustive |