The seams¶
What crosses each boundary of this crate, and who owns what on either side.
Three seams, and this crate declares two of them. UDSSVC_ARCH_0040 made this crate
the driver, which settles who declares what by settling who calls whom:
Seam |
Declared by |
Implemented by |
Element |
|---|---|---|---|
Service traits and callbacks |
this crate |
the consuming application |
|
|
this crate |
a binding ( |
|
The response sink |
|
this crate’s own buffer |
|
The rule behind it is the one this page has given since the seams moved: a seam is
declared by whoever the design makes responsible for it, which for a driver means the
interfaces above and below are both its own. The sink is the exception and stays awc’s,
because an I/O vocabulary shared by five crates belongs to none of them.
Two earlier arrangements are recorded rather than deleted, because each is more plausible than what replaced it.
The first held that “whoever is called declares the interface” — the byte seam declared by the binding because the binding calls in, the transport seam declared here because this crate calls out. Tidy, and wrong about the first half: a seam carrying an addressing triple, bytes, a sink and a responder is not transport-shaped, so every binding would have declared the same trait separately.
The second replaced it with “ownership follows the specifying document”, and moved five
seams into uds_session on the ground that ISO 14229-2 specifies both interfaces of the
session layer. That was right about the document and wrong about the component. ISO 14229-2
names its service user as the ISO 14229-1 layer — this crate — so there was never a third
party between them for those interfaces to sit between. Once UDSSVC_ARCH_0040 collapsed
the service user and the driver into one, four of the five seams stopped having two sides:
RequestHandler, PendingResponder and DiagnosticClient had nobody left to call
them, and Ctx stopped crossing a boundary at all. uds_session declares none of them
now, and says so in its own brief.
What survived that collapse is a library of ISO 14229-2 types and one state machine, which
this crate drives. uds_session declares no outward trait and calls nothing.
The seams under the driver. Everything inside the uds_services box is
internal; only the three interfaces on its boundary are seams.¶
Three things the diagram is meant to settle. The sink is this crate’s, over a buffer
this crate sizes and bounded also by the peer’s advertised maximum where it made one — it
was the driver’s when the driver lived in the binding, and the driver moved. uds_session
is inside the box: this crate owns the Session instance, supplies every input and
drains every action, which is why no arrow leaves the package for it. And the consuming
application touches exactly two things, the service traits and the typed client, which is
UDSSVC_ARCH_0019’s goal stated as a picture.
Request context¶
Architecture Element: There is no Ctx; the addressing triple is the pipeline's only extra input UDSSVC_ARCH_0015
|
|||||||||||||||
Every input the pipeline needs but cannot determine from the request bytes is exactly
one thing: the ISO 14229-2 addressing triple,
Rationale: the pipeline is a function of the request and its addressing, so every clause
8.7 rule is testable without a network, a clock or a session layer. That property was
never what ``Ctx`` is retired, and the whole of its reasoning is on the record because it was arrived at rather than assumed. It was proposed as a seam: while something outside this crate called the pipeline, the inputs that pipeline could not derive had to arrive somehow, and a struct carrying one field per mandatory decision node of Figures 5 and 6 is the disciplined way to write that — the field list is a checklist against the standard, and a check with no field is a check nobody implemented. It carried four things:
The three that went had been marked “under review” on a suspicion this element stated and
did not close: that this crate implements Two things this does not weaken. The checklist argument was sound and is now owed
elsewhere — nothing about deleting the struct implements Figures 5 and 6’s checks, and
the state they read being local makes it easier to forget a check, not harder, because
no empty field remains to accuse anyone. And the pipeline’s testability is unchanged for
the reason given above. Open question 6, which asked what Session and security are raw sub-function values, passed through uninterpreted.
Naming the sessions here would mean this crate deciding what
Addressing is ``uds_session::Ai``, the ISO 14229-2 triple, used directly. An earlier
version of this element defined a local two-variant physical/functional type instead, on
the ground that a binding’s addressing triple is transport-shaped — Using it directly also closes a question the set had been carrying about whether two vocabularies for one concept were worth the conversion. There is one. Whether a response-pending has been sent is deliberately not a field here, and an
earlier draft had it wrong in a way worth recording. Clause 8.7.5 guards both
suppression rules on it, so the pipeline is incorrect without the fact — but it is not an
input. Peer identity comes with the triple, and it is needed. ISO 14229-1:2020 10.6.4
requires that “an authenticated state shall be linked to a certain diagnostic channel”
and that “multiple clients can be handled on multiple channels with different
authentication settings”, so Taking |
Outcome¶
Architecture Element: Dispatch is asynchronous and returns its outcome unsettled; the driver settles it UDSSVC_ARCH_0016
|
Rationale: a negative response is a normal, specified outcome of clause 8.7 expressed in
the written bytes — a server answering 0x11 has succeeded at its job. Typing it as an
error would put the most common non-trivial path through the crate into the Silence must be a distinguishable outcome rather than “wrote nothing”, because the loop has to tell “clause 8.7 requires no response” apart from a response that was written. Only the first is a reason not to transmit. How the cases are spelled. The driver settles, not dispatch, because rule 3’s input is known only to the
driver. Whether a response-pending for this request was accepted for transmission is
learned in the driver’s loop while the handler runs ( ``Responded`` is the whole report, and no other outcome type stands behind it.
Dispatch reports to the driver loop in this crate, not across a seam, for the reason
Genuine transport failures are the binding’s concern, reach this crate as a Dispatch and the handlers it calls are asynchronous, which is what
Without this, that is machinery every integrator would have to build. It is now machinery nobody builds, because the driver is here and the handler yields into it. The honest limit: an asynchronous seam does not make blocking work non-blocking. A handler that busy-waits on a flash erase stalls the executor exactly as a synchronous one would, and the gain is real only for handlers written to yield. What changes is that yielding is now possible at the seam, where before it was not expressible at all. |
Response sink¶
Rationale: allocation-freedom cannot be retrofitted, because the signatures that make an
API alloc-free are the ones callers depend on: adding it later is a breaking change to
every handler in every application. A handler therefore writes its response into an
The sink is this crate’s, which is a change of owner rather than of design. It was
the binding driver’s while the driver lived there; Where the storage comes from is now settled, and this element left it open. It used
to say that a caller-supplied buffer and one sized at assembly alongside
There are three such buffers, not one, and the reason is The sink is ``awc``’s rather than ``embedded-io``’s, and the difference produces an
NRC. A response is written through a sink bounded at the peer’s maximum payload where
one is advertised; an over-long response fails at the write with the counts intact, and
this crate is to turn that counted failure into The bound is never below three bytes. ISO 14229-1 fixes a negative response at
three bytes; a peer that cannot receive three bytes cannot take part in UDS at all, so
a bound below that is not one the protocol admits and is raised to it. The pipeline’s
own Built for the stages that exist. This element’s account of that failure type has now been wrong three times, and the count is the point. The versions, in order:
The mechanism has survived all three, for the same reason each time: 0x14 carries no
length, so this crate needs the failure to be counted only in the sense of being
distinguishable from every other write failure, and never needs the count itself. That is
also why each error was cheap enough to survive review — nothing downstream reads the
field, so naming it wrongly broke nothing and compiled nothing. Three misses at a
member no code path depends on is evidence about this document rather than about the
design: a claim nothing tests is a claim that stays wrong. A reader should treat the
field names here as the least reliable sentences on the page and check them against
The bound itself is less solid than this element assumed. On DoIP the number is Max. data size, which ISO 13400-2:2019 Table 11 lists as an optional item of the entity status response — so a conformant entity may advertise none — and defines as “the maximum size of one logical request that this DoIP entity can process”, which is the inbound direction rather than the outbound one a response occupies. Two consequences to design around rather than assume away. There may be no bound to apply, in which case 0x14 is unreachable and a response is limited only by the sink the caller supplied; that is a correct outcome, not a degraded one. And where a bound does exist, whose it is — this server’s or the peer’s — is the transport’s question to answer, not this crate’s. What must not happen is a fabricated default, which would make a conformant server truncate valid responses in order to produce an NRC nothing asked for. The sink is a concrete type, ``ResponseSink`` — neither One consequence to design against rather than discover: a sink can fail mid-response,
after some bytes are already written.
|
The transport seam¶
One trait sits below this crate, declared here and implemented by a binding: pub trait UdsTransport {
/// What this transport's failures are. Never interpreted by this crate.
type Error;
/// The largest A_PDU this transport can carry, where its protocol caps it.
/// Participates in UDSSVC_ARCH_0013's const fold.
const MAX_PDU: usize = usize::MAX;
/// T_Data.req — hand a T_PDU to the transport, and say what follows it.
async fn t_data_req(&mut self, ai: Ai, data: &[u8], after: AfterSend)
-> Result<(), Self::Error>;
/// Fill `buffer` with the next inbound message, or return
/// `TransportEvent::Deadline` when `deadline` passes first.
async fn next_event(&mut self, buffer: &mut [u8], deadline: Option<Timestamp>)
-> Result<TransportEvent, Self::Error>;
/// The largest response the peer will accept, where it advertised one.
fn outbound_max(&self) -> Option<usize>;
/// The tP_Client reload pair this transport dictates.
fn channel_timing(&self) -> Reloads;
/// Monotonic milliseconds, 32-bit and wrapping (UDSSVC_ARCH_0041).
fn now(&self) -> Timestamp;
}
Five changes from the version above, and the first is the one that mattered. ``next_event`` fills a caller’s buffer instead of lending one. It used to return
``timeout_ms`` became a ``Timestamp`` deadline. ``max_payload`` became ``outbound_max``, and no ``inbound_max``. One number could not
answer both questions because the standard’s number is request-shaped: ISO
13400-2:2019 Table 11 defines Max. data size as “the maximum size of one logical
request that this DoIP entity can process”. A server asking what it may send is
therefore asking about the client’s advertisement, not its own, and the two are
different values belonging to different entities. Collapsing them bounds a response by
the server’s own receive capacity, which is a limit nothing in the standard imposes.
There is deliberately no ``inbound_max``. It was meant to hand this crate’s in-flight
buffer length down for the transport to refuse longer requests with, and built that way
( ``t_data_req`` gained ``after: AfterSend``. ISO 14229-5:2022 REQ 7.9 has a server close
its TCP connection after a ``Closed`` gained ``peer: Address``. A ``now_ms() -> u32`` became ``now() -> Timestamp``. Same obligation, a type that carries
Three variants were added, and two of them are conformance requirements rather than conveniences.
Rationale: this crate calls out to a transport, so this crate declares what it calls. The trait is a transport’s whole obligation to the stack: carry bytes in both directions, say how large a payload it will take, say what timing it dictates, and tell the time. It carries no notion of a service, a data identifier or a negative response code, which is what keeps a binding from needing to understand UDS. It serves both roles, and that is a change from what it replaced. The client’s exchanges and the server’s inbound requests are the same bytes on the same transport; only what this crate does with them differs. A separate client trait would have obliged a binding to implement two interfaces to the same socket. The asynchrony is Three earlier versions of this element are on the record, and the middle one is the instructive failure. It first declared The functional case that justified splitting the old client trait into two methods
survives as a property of this crate’s client surface rather than of the transport.
|
There is no handler seam¶
Architecture Element: Nothing calls into this crate; the inbound path is the driver's own UDSSVC_ARCH_0018
|
No trait exists by which something outside this crate hands it a request. The driver of
Rationale: recorded as an element rather than as an omission, because a handler seam is
the single most likely thing to be re-proposed here. Two full design cycles produced one
— first declared by each binding, then declared by Two things that used to cross this seam are now internal, and are named here because
their elements still describe them as though they crossed something: What this does not change is the pipeline’s shape. |
Response-pending¶
When The moment is known the same way. Rationale: the decision and the bytes are this crate’s (
The session layer’s constraints are where they always were and are the reason a
submission can be refused: it rejects one while a predecessor is unconfirmed
( An earlier version of this element declared a ``PendingResponder`` trait with a
cancel-safe
The asymmetry that follows is |
Clock¶
Driving Two obligations, and the second is not new:
Rationale: a transport that can report an inbound event or a timer expiry, whichever
comes first, already measures time — that capability is what the seam asks for, not
something added to it. A separate It is also where the platform integration already is. Whoever writes a transport is already reaching for sockets and an executor on that target; asking the same implementor for the clock adds no new platform surface, while a second trait would be a second thing every target has to satisfy. The seam carries a deadline, and the argument that it should carry a duration is
retired. That argument was this element’s longest and is kept on the record because it
was correct about everything except the type. It ran: a deadline in wrapping What retires it is that the arithmetic no longer has to be rederived by anyone.
The ambiguity the old argument identified was real. It was a property of An earlier version of this element declared a ``Clock`` trait here, with That last point is worth recording because it reverses a change the briefs had queued.
The reason a clock is a seam at all is unchanged, and is The cost, stated rather than discovered: “can tell the time” is now coupled to “is a
transport”, so a deployment driving the stack over a channel it would not otherwise model
as a transport still implements |