Skip to content

feat!: the DoIP client transport, a uds_client! client over a Tester end to end (W9) - #62

Merged
zheylmun merged 25 commits into
mainfrom
feat/doip-client-transport
Oct 12, 2026
Merged

zheylmun merged 25 commits into
mainfrom
feat/doip-client-transport

Conversation

@zheylmun

@zheylmun zheylmun commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Issue URL

Towards #17 (items 2 and 3).

Summary

W9 of the stub-API integration: the DoIP client transport, the last piece of new code before W13 removes simple_doip's old client. A uds_client! client now reads from a uds_server! server over real loopback sockets (Tester ↔ Entity). It survives the prescribed close and a lost response by reconnecting, without the application retrying.

Based on main after W12 (#55).

uds_on_ip: DoIpClientTransport<C: TesterConnection, QUEUE, PEERS = 1, WAITING = 2>

It implements UdsTransport and ClientTransport, and absorbs what the tester asks of its caller:

  • One request at a time. A request made while the tester carries another waits in QUEUE bytes, up to WAITING of them, so a keep-alive and a call's request never collide. WAITING is at least PEERS + 1, checked at compile time: a keep-alive per server, a functional one, and a call's request, one of them carried.
  • Refusals. A request the tester refuses is accepted and confirmed failed by the next event, as DoIpTransport does.
  • Its own address. A message to any other address is dropped.
  • Closes. A connection's end is one Closed per server tracked on it. expected is true where the server's last message was a positive DiagnosticSessionControl or ECUReset response (ISO 14229-5:2022 REQ 7.9, 7.11).
  • Reconnecting (REQ 7.8, 7.10).
    • A request made while the connection is closed is accepted at once.
    • The next next_event reconnects for it through TesterConnection::reconnect, bounded by the call's deadline.
    • A call dropped meanwhile leaves the request owed its confirm.
  • The late-reply guard. A physical request expecting a response, to a server whose last such request was confirmed and never answered, goes on a new connection. That answer, arriving after the client gave up on it, can't be taken for this one's. The client's own service check can't tell a late 62 F1 90 from an answer to a read of F18C.
    • Suppressed requests (SPRMIB) neither arm nor trigger it.
    • Nor do services ISO 14229-1 doesn't define, or functional requests.
  • PEERS is how many servers behind the entity it tracks. A new server takes an idle one's slot, and a request to another while every tracked one is busy is refused visibly.
  • Periodic responses (0x8004, REQ 7.16) to this tester become TransportEvent::Periodic.
  • with_max_data_size(Option<u32>) takes the entity status response's value; outbound_max is it less the 4 address bytes.
  • Errors. ClientTransportError<C> is named by its connection.

simple_doip (breaking)

  • DiagnosticConnection::request and DiagnosticEntity::request are plain functions, no longer futures: Result<(), ConnectionRefusal> and Result<(), Refusal>.
    • Both implementations already completed on their first poll, and every caller relied on that. Now the type says so.
    • ISO 13400-2:2019 8.3.1/8.3.2 model the request this way: it hands the PDU over, and completion comes later as the confirm.
  • ConnectionRefusal is Closed or Refused(Refusal); tester::Error is gone. A generic layer can now tell "reconnect" from "queue" from "failed", and Refusal stays the entity's vocabulary.
  • TesterConnection::address() added.
  • The Tester reports Closed once per end, then waits for the caller's deadline. Previously it repeated Closed on every call.
  • TesterConnection::reconnect(deadline) returns Reconnection::{Connected, Deadline}. A reconnect stopped at its deadline leaves the back-off running from the loss.

uds_services (breaking)

  • ClientTransport: UdsTransport with close(). Client requires it and gains Client::close. Ending a session is the client role's alone, so the shared UdsTransport seam stays role-neutral.
  • The driver fails an exchange on Closed only once its response window is open. One still awaiting its DataConf waits for it.
  • Only an expected close releases a server's keep-alive. A failure or the guard's reconnect ends a connection, not the server's session.

Deviations from the brief

All are recorded in their commits, and most are the checkpoint rulings:

  • The Tester no longer repeats Closed.

  • Ending a session moved into uds_services.

  • W8's driver changed in the two ways above.

  • request became synchronous and reconnect takes a deadline. These came from a review after the checkpoint.

  • The embedded probe's size moved three times, each measured to one commit:

    • +4 B from Refusal::NotConnected's arm in DoIpTransport::refuse, and −8 B when the review moved it out of Refusal;
    • −132 B when request stopped being a future;
    • +16 B for keeping the client transport's size in the image (below).

    Both are re-recorded in their own commits rather than the causing ones.

Testing

From the review

  • Required, both fixed with the reviewer's repros as tests. A close with requests waiting for a reconnect reconnected for them; a failed reconnect left their servers' slots Awaiting::Confirm. One routine now fails every waiting request and frees its server, used by the connection's end, a failed reconnect and close(); a waiting request the connection refuses when its turn comes frees its server too.
  • A failed request waits in place. No second array of owed confirms and no capacity argument in a doc comment: a request that fails waiting is marked where it is, its bytes released, and popped before any request is sent. Refusals are the one bounded queue left, and one past the bound is an Err to the caller, as before.
  • WAITING is a parameter, not a hidden 4, with the PEERS + 1 relation checked by new.
  • Refusal::NotConnected is gone; a connection refuses with ConnectionRefusal.
  • The client transport is in the size gate. The probe keeps size_of of the sensor's client transport (a Tester over a stub connector, QUEUE 4096) in a static that tools/check_size.py reads from the image and holds exactly, as RAM is: client_transport 12 632 B, in size-baseline.json.
  • ARCHITECTURE.md §7.3 draws the client transport's connection states and what each edge owes.

For 00-common / follow-ups

  • Settled text that no longer holds:
    • "the Tester repeats Closed";
    • the refusal wording;
    • request's async signature;
    • reconnect's signature;
    • the W8 summary's handling of Closed, and ClientTransport.
  • Resolves Acceptance criteria before dft switches: four uds_on_ip behaviors learned on Iris hardware #17 items 2 and 3. Also resolves the open question "Should a reset discard a message already arriving?", whose late-reply case now has its guard.
  • simple_doip/ARCHITECTURE.md's UDP section still says "W9's outbound_max"; that's a W12 sentence I left alone.

🤖 Generated with Claude Code

DiagnosticConnection::request now fails with service::Refusal, the vocabulary
DiagnosticEntity::request already used, so a layer above generic over the
connection can tell a closed connection (reconnect) from a request it must queue
or confirm failed. The trait's MAX_PDU doc already promised PduTooLarge.

- Refusal gains NotConnected; an entity never returns it.
- The Tester's RequestPending is NoRoom, MessageTooLarge is PduTooLarge
  { len, max }; tester::Error is removed.
- The Tester's next_event error is Infallible: it never fails.
- TesterConnection::address(), the address routing activation registers, so a
  caller can check an indication's target.
- uds_on_ip: the refusal-to-DoIP_Result mapping moves into mapping.rs to be
  shared; NotConnected is NO_SOCKET.

BREAKING CHANGE: DiagnosticConnection::request returns Result<(), Refusal>;
tester::Error is gone; TesterConnection implementors add address().
After a connection ends, Tester::next_event reports Closed once and then waits for
the caller's deadline (forever with none), reporting Deadline, until a reconnect
succeeds; request's Refusal::NotConnected still says the connection is closed. An
entity already reports each connection's end once and then waits on something real,
so the two connection traits now behave alike, and TransportEvent::Closed one layer
up is edge-triggered the same way.

A caller that does not reconnect at once, such as uds_on_ip's client transport, which
reconnects on its next request, can wait out its deadline over a closed connection
without a timer of its own; with the repeated Closed it could only spin.

One Closed per end of an established connection: a reconnect that fails with no
connection open ends nothing and reports nothing.

deviation: 00-common.md's settled "the Tester repeats Closed until a reconnect" (W6)
no longer holds, by the user's ruling at W9's checkpoint, because the client
transport needs the connection to wait out a deadline while closed.

BREAKING CHANGE: TesterConnection's next_event reports Closed once per end.
Ending a session is the client role's alone: a server is closed to, and the one
close it makes, the prescribed close, its transport makes itself. So it is stated
here, in the client's bound, and nowhere on the shared UdsTransport seam.

- ClientTransport: UdsTransport with close(&mut self). A request accepted and not
  yet confirmed is still confirmed, failed; the close is reported by no Closed.
- Client requires T: ClientTransport and gains close(&mut self): it settles a
  dropped call, abandons a functional window set aside to drain, ends every
  keep-alive (UDSS_LLR_0184), closes the transport, and reads the failed
  confirmations still owed before returning, so the next call reads none of them.
- The test transports implement it.

deviation: the brief has this transport expose its own close, because W8 was
taken as finished; at W9's checkpoint the user ruled the asymmetry belongs to
uds_services, changed here.

BREAKING CHANGE: a transport used with a Client implements ClientTransport.
DoIpClientTransport<C: TesterConnection, QUEUE> implements UdsTransport and
ClientTransport, the client side of UDSonIP over one DoIP tester connection
(ISO 14229-5:2022 REQ 4.3 Table 4, REQ 4.4 Table 5). It absorbs what the
connection asks of its caller:

- one request at a time: a request made while the connection carries another
  waits in QUEUE bytes, checked at compile time to hold the connection's MAX_PDU,
  and is sent once the one before is confirmed, so a keep-alive and a call's
  request never collide;
- refusals: a refused request is accepted and confirmed failed by the next event,
  with the DoIP_Result the server side uses;
- its own address: a message to another address is dropped;
- a connection's end: one Closed per server addressed on it, then each waiting
  request's failed confirm; a close the client made is reported by no Closed.

MAX_PDU is the connection's; the clock is the connection's; outbound_max is None.
Reconnecting, the late-reply guard and periodic responses follow.
ISO 14229-5:2022 REQ 7.8 and REQ 7.10: after a server closes the connection, a new
connection and routing activation come before diagnostic communication continues.
The transport reconnects, through TesterConnection::reconnect, for the next request
that needs a connection, and only then sends it. A reconnect that fails refuses the
request with ClientTransportError::Reconnect and leaves the connection closed for a
later request to try again. TimeoutA alone reconnects nothing.

A connection found ended by the request, its Closed not yet read, is reported all
the same: one Closed per server addressed on it, ahead of anything from the new
connection.

Closed's expected, on the client side: the server's last message on the connection
was a positive DiagnosticSessionControl or ECUReset response, after which REQ 7.9
and REQ 7.11 have a server close, so its session is not the one the client knew.
Any other end is not expected.
…tion

The open question "Should a reset discard a message already arriving?": a response
that arrives after the client's window closed would be taken for the next request's
answer, and the client's own service check passes it where the two requests share a
service and their first data byte (a late 62 F1 90 for a read of F18C). The guard is
here, with no timer of its own: a physical request that expects a response, to a
server whose last such request was confirmed and never answered, goes on a new
connection, the old one's end reported as Closed. This covers both the client's
repeat after a timeout and its next call after giving up.

A response-pending message (0x78) is not an answer. A request whose positive response
is suppressed (SPRMIB, ISO 14229-1:2020 9.2.2) expects none, so it neither arms the
guard nor triggers it; a service ISO 14229-1 does not define arms nothing; functional
requests arm nothing.

Also:
- a periodic response (payload type 0x8004, ISO 14229-5:2022 REQ 7.16) to this tester
  is TransportEvent::Periodic; one truncated, too short or to another tester is
  dropped;
- with_max_data_size(Option<u32>) takes the entity status response's Max. data size
  (ISO 13400-2:2019 Table 11); outbound_max is it less the diagnostic message's
  addresses, None until told.
…ribed close ends the keep-alive

Two changes to how the client takes TransportEvent::Closed, both from W9's
checkpoint:

- A close fails a physical exchange only once its response window is open, since
  its answer cannot come. One still awaiting its confirmation waits for it: the
  transport owes it, and it says whether the request went, a failed one repeated as
  ISO 14229-2:2021 Table 9 repeats any. A DoIP client transport that finds the
  connection ended only when the next request is made reconnects, sends, and then
  reports the old connection's close; the call no longer fails for it.
- Only an expected close releases a peer's keep-alive (UDSS_LLR_0184): it follows a
  positive DiagnosticSessionControl or ECUReset response, after which the server's
  session is not the one the client knew. Any other close ends a connection and not
  the server's session, which outlives it, so the keep-alive goes on over the next
  connection. The transport's late-reply guard reconnecting in an extended session
  no longer costs the session its keep-alive.

TransportEvent::Closed's expected now gives both roles' meanings. The scripted test
transport gains Step::Leave for a prescribed close; the two tests pinning release
on a close use it.

deviation: W8's driver (#51) failed any exchange on a Closed and released the
keep-alive on every close; changed here by the user's ruling at W9's checkpoint.
…ort, end to end

client_path.rs runs a uds_client! client over DoIpClientTransport over the Tester,
against a uds_server! server over DoIpTransport over the sensor's Entity, on
loopback sockets:

- a client reads a data identifier, and ends its session with Client::close;
- it survives the prescribed close (ISO 14229-5:2022 REQ 7.8, REQ 7.9): after a
  positive response to a session change that leaves the server's software, the next
  read reconnects and is answered, without the application retrying;
- issue #17 item 3: a read whose response never arrives in its window, then good
  reads of an identifier sharing its service and first data byte, each answered with
  its own record. Without the transport's late-reply guard the repeat is answered
  busyRepeatRequest by a server still serving the lost read;
- issue #17 item 2: with the tester's address still held by the connection the lost
  response was awaited on, the next read's reconnect gives it up, waits the back-off
  on the mock clock, then connects, and is answered.

The harness gains SensorClientTransport, Loopback::tester_backing_off and
client_transport, and ticking, which moves the mock clock while a call waits on a
timer; uds_on_ip becomes a dependency of the harness's library.
…cted

Text 54004 -> 54008 bytes; data, bss and main's frame unchanged. Measured at each
commit: the four bytes are the first commit's, where DoIpTransport::refuse's
match on service::Refusal gains the NotConnected arm. The client transport, which
the probe does not build, moves nothing.

deviation: re-recorded here rather than in the commit that caused it, which was
already made when the gate showed the move.
ARCHITECTURE.md: the connection seam is built for both roles (the typed refusal,
Closed once per end, DoIpClientTransport's queue, closes and reconnecting); the
client's prescribed close and its lost response get their own paths in 7.2; a
client surfaces a periodic response; ending a session is uds_services'
ClientTransport, below which reconnecting stays. The "client role is not built"
prerequisite is gone. The crate docs, its README and the workspace README's status
say so.
…econnect is never expected

From the whole-branch review:

- A call bounded by dropping it while t_data_req waited out a reconnect (back-off,
  connect, activation) left its request accepted by the session layer but by no
  transport, so it was never confirmed and its channel waited for good. A request
  is now accepted on the first poll, waiting where the connection has ended, and
  the next next_event reconnects for it, then sends it; a dropped reconnect leaves
  it waiting for the next. A reconnect that fails is next_event's error and
  confirms every waiting request failed (DoIP_NO_SOCKET).
- The late-reply guard's reconnect could report Closed { expected: true } where
  the server's last message was a positive DiagnosticSessionControl response, and
  so end the client's keep-alive, against the checkpoint's ruling. A close the
  transport makes is never expected; and a request the entity acknowledged after
  such a response shows the connection outlived it, so a later end is not the
  prescribed close either.

Tests: a request survives a dropped reconnect (scripted), the guard's close is not
expected, an acknowledged request clears the expectation, and on loopback a read
dropped in the back-off leaves the next read answered (it hung before this).
…t a future

DiagnosticConnection::request and DiagnosticEntity::request are plain functions
returning Result<(), Refusal>. ISO 13400-2:2019 8.3.1 hands the PDU over and 8.3.2
reports its completion later, as DoIP_Data.confirm; nothing waits in between. Both
implementations already completed on their first poll, and every caller relied on it,
but prose held the contract: the Tester's #[expect] ("accepting on the first poll is
the contract"), the entity's poll_fn, and the client transport's own note that its
send path waits on nothing. An implementor reading the async signature could await in
request and silently break the client transport's cancel safety and its
accept-before-reconnect guarantee. Now acceptance is exactly when request returns.

- The Tester's #[expect] and the entity's poll_fn are gone, as are two tests of "a
  request dropped unpolled was never made", a case that no longer exists.
- uds_on_ip: DoIpClientTransport's send path is synchronous, so its t_data_req awaits
  nothing; DoIpTransport's t_data_req likewise. Both return a ready future, the request
  made when called.

BREAKING CHANGE: DiagnosticConnection::request and DiagnosticEntity::request are no
longer async; implementors drop async, callers drop .await.
…rameter, and none is dropped silently

DoIpClientTransport<C, QUEUE, PEERS = 1>: PEERS is how many servers behind the entity
the transport tracks at once, for the late-reply guard and for their closes. It was a
private constant of 4, and a fifth server addressed got no Closed and no guard, with
nothing said. Now:

- a server takes the place of one with nothing awaited and no prescribed close
  pending, whose Closed would change nothing at the client;
- where every tracked server is busy, a physical request to another is refused,
  confirmed failed, rather than sent untracked;
- the events owed are split into closes (PEERS) and failed confirms (the waiting
  requests a connection's end fails, plus REFUSALS), so neither overflows: a
  connection ends at most once between reconnects, the transport reconnects only once
  nothing is owed, and refusals are capped. That bound was prose beside a push whose
  failure was ignored.
- closes are reported before any failed confirm owed.

PEERS defaults to 1, the sensor's one server; a gateway client sizes it as its
physical channels. ClientTransportError::Refused stays: it is the cap on refusals a
misbehaving caller can pile up, not a capacity artefact.

BREAKING CHANGE: DoIpClientTransport gains a third generic parameter (defaulted).
…nnection

ClientTransportError<C: TesterConnection> replaces ClientTransportError<E, R, X>: its
variants carry C's own Error, ReconnectError and CloseError. A caller naming the
client's call error over the Tester wrote
ClientError<ClientTransportError<Infallible, ConnectError<io::Error>, io::Error>>,
three types the connection already determines, in an order nothing but the definition
told it; it now writes ClientTransportError<SensorTester>. Debug is written by hand,
since a derive would bound C itself. The no_std proof that it is a core::error::Error
is checked generically, for every connection.

BREAKING CHANGE: ClientTransportError takes the connection as its one parameter.
…future

Text 54008 -> 53876 bytes; data, bss and main's frame unchanged. Measured at
6097f8db, the commit that made DiagnosticEntity::request a plain function: the
entity's poll_fn future and DoIpTransport's await on it are gone from the server path.
The client-side commits after it move nothing in the probe.

deviation: re-recorded here rather than in that commit, which was made before the gate
showed the move.
…y no Closed

ClientTransport::close forgot the servers it tracked only after the connection's close
completed. Dropped before then, as TesterConnection::close's doc invites to bound it,
the connection later reported its Closed and the transport passed one on for every
server, contradicting ClientTransport::close's contract. It now forgets them first.
…losing fails

A transport's close that failed returned before the loop that reads the failed
confirmations still owed, though the connection is closed either way, so the next call
read them. The close's error is now kept, the confirmations read, then the error
returned.
…client transport honours it

TesterConnection::reconnect(deadline) stops once the deadline passes, during the
back-off, the connect or routing activation, and reports Reconnection::Deadline, the
connection closed and the back-off still running from the loss, so a later reconnect
takes up where it stopped. The Tester races the attempt against the deadline on
embassy-time's clock.

DoIpClientTransport passes its next_event's deadline to the reconnect it makes for a
waiting request, and reports Deadline where it passes first, the request still
waiting, owed its confirm. Before, the reconnect ran unbounded inside next_event, and
the client's idle_until could overshoot its time by the back-off and the connect after
an unexpected close in session, when a keep-alive needed a new connection; the
transport has no timer of its own to bound it with.

BREAKING CHANGE: TesterConnection::reconnect takes Option<Timestamp> and returns
Result<Reconnection, ReconnectError>.
Comment on lines +22 to +54
/// One transport drives one [`TesterConnection`], to one entity, and every server reached
/// through it. It absorbs what the connection asks of its caller:
///
/// - **One request at a time.** A request made while the connection carries another
/// waits in the transport, in up to `QUEUE` bytes, and is sent once the one before it
/// is confirmed, so a keep-alive and a call's request never collide. `QUEUE` is at
/// least [`MAX_PDU`](simple_doip::service::DiagnosticConnection::MAX_PDU),
/// which [`Self::new`] checks at compile time, so a request always finds room behind a
/// keep-alive.
/// - **Refusals.** A request the connection refuses is accepted here all the same and
/// confirmed failed by the next [`UdsTransport::next_event`], as ISO 13400-2:2019 8.3.1
/// confirms every request.
/// - **Its own address.** A message to any address but the tester's is not this
/// tester's to take, and is dropped.
/// - **Reconnecting.** ISO 14229-5:2022 REQ 7.8 and REQ 7.10 have a client open a new
/// connection and activate routing again after the server closes it. A request made
/// while the connection is closed is accepted at once and waits; the next
/// [`UdsTransport::next_event`] reconnects for it through
/// [`TesterConnection::reconnect`], which gives the old connection up and waits out its
/// back-off first, and then sends it. The reconnect stops at the call's deadline, and a
/// call dropped meanwhile stops it too; either leaves the request waiting, owed its
/// confirm, for a later event to reconnect for. A request withdrawn with `DoIP_TIMEOUT_A`
/// leaves the connection up and reconnects nothing.
/// - **A late reply.** A physical request that expects a response, to a server whose
/// last such request was confirmed and never answered, goes on a new connection, so
/// that answer, arriving after the client gave up on it, cannot be taken for this
/// one's. A response-pending message is not an answer. A request whose positive
/// response is suppressed expects none, so it neither leaves a reply to arrive late
/// nor gives up a connection a reply is awaited on.
/// - **Periodic responses.** A message of ISO 14229-5:2022 REQ 7.16's payload type
/// ([`PERIODIC_RESPONSE_PAYLOAD_TYPE`])
/// to this tester is a [`TransportEvent::Periodic`]; one truncated or too short to
/// carry an identifier is dropped.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question Does this deserve its own architecture diagram?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added as ARCHITECTURE.md §7.3 (f2fad1a): the link's two states, the edges into Closed and what each owes the session layer, the one edge out, each with its clause. The transport's doc points at it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had imagined we could reduce some of this text with a link to the architecture but it seems like we want to keep this block.

Comment thread crates/uds_on_ip/src/client.rs Outdated
Comment thread crates/uds_on_ip/src/client.rs Outdated
Comment thread crates/uds_on_ip/src/client.rs Outdated
Comment thread crates/uds_on_ip/src/client.rs
Comment thread crates/simple_doip/src/service.rs Outdated
Comment thread crates/uds_on_ip/src/client.rs
…sts and frees their servers

The teardown of the requests waiting for a reconnect lived only in the
connection's end, which runs once on the up-to-closed edge, while a closed
link keeps accepting requests. So a close with a request waiting left it
waiting, and the client's drain reconnected for it; and a reconnect that
failed confirmed the waiting requests failed but left their servers
awaiting a confirm, so no other server could take the slot.

One routine fails every waiting request, used by the connection's end, a
failed reconnect and close; and a failed request, there or refused when its
turn comes, leaves its server awaiting nothing.
…is sized by the caller

A request that failed waiting was copied into a second array of owed
confirms, sized by an argument in a doc comment that a release build
could only break silently. It now fails where it waits, its bytes
released, and is popped from the front of the queue before any request
is sent; the transport owes nothing it cannot hold.

The queue's length was a hidden 4 beside the `PEERS` the caller sizes,
and a `uds_client!` client with five physical channels sends more
keep-alives back to back than that. It is `WAITING`, a fourth parameter
defaulting to 2, and `new` checks at compile time that it is past `PEERS`.
…al, not the entity's

`Refusal::NotConnected` was a variant an entity never returns, on an enum
whose doc says it is exhaustive, so every caller of the entity's request
carried an arm for a case that cannot happen, the server transport's image
among them.

`DiagnosticConnection::request` is refused with `ConnectionRefusal`:
`Closed`, or `Refused(Refusal)` as an entity refuses. `Refusal` is the
entity's vocabulary again, and a layer generic over the connection still
tells reconnect from wait from failed.
The probe is the sensor's server path, and nothing measured the client
role: a `DoIpClientTransport` owns a `QUEUE` of at least the connection's
`MAX_PDU` and `new` returns it by value, so its size is what an integrator's
frame pays. The probe keeps `size_of` of the sensor's client transport, over
a `Tester` on a stub connector, in a `static` the checker reads out of the
image, held exactly as RAM is: 12 632 bytes, recorded.
ARCHITECTURE.md §7.3: the link's two states, the edges into Closed and
what each owes the session layer, and the one edge out, with the ISO
14229-5:2022 clause behind each. The transport's doc points at it, and
§3.4 no longer says a client's periodic responses are unimplemented.

@JustinKovacich JustinKovacich left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with one comment.

@zheylmun
zheylmun merged commit 3d96bf9 into main Oct 12, 2026
27 of 29 checks passed
@zheylmun
zheylmun deleted the feat/doip-client-transport branch October 12, 2026 16:55
zheylmun added a commit that referenced this pull request Oct 12, 2026
…refusal a connection gives

"`automotive-wire-codec` 0.4, and `embedded-io` is gone" could be read as
both going. And a closed connection refuses `ConnectionRefusal::Closed`
since #62, which the changelog and one test's doc still called
`NotConnected`.
zheylmun added a commit that referenced this pull request Oct 12, 2026
… with the migration guide and 0.7.0 changelogs (W13) (#63)

## Issue URL

Closes #25, closes #26, closes #33, closes #34, closes #48. Based on
`main` after W9 (#62).

## What this does

Removes `simple_doip`'s three old I/O surfaces, which `Tester`, `Entity`
and the `uds_on_ip` transports replace. It also writes the migration
guide and every crate's 0.7.0 changelog. This is the breaking release;
publishing stays held in `ci.yml`.

- **Removed:**
- The modules `client`, `client_inner`, the old `connection`,
`socket_manager`, `server`, `bare_metal_entity`, and `error`, which only
those modules used.
- The `client` and `server` features, and the `async-trait` and
`futures` dependencies.
  - `impl Default for OwnedMessage`, which only the old client used.
- The tests `integration_test`, `tester_interop` and
`udp_identification`, and the examples `simple_client` and
`echo_server`.
- **`bare_metal_entity` was not feature-gated, so its removal breaks
default-feature users.**
- **Ported first:** every behaviour that only the old tests covered now
has a test on the new code. The UDP ports keep the old tests' bytes:
- A request for another entity's EID or VIN gets silence, and the entity
still answers the next request.
- A truncated datagram gets nothing, and a UDP alive check gets NACK
`0x01` (the old test expected silence). Neither stops the next answer.
  - Connections that close without sending anything free their sockets.
  - What arrived before the entity closed is reported before `Closed`.
  - An acknowledgement late in the 2 s window still confirms `Ok`.
  - Routing activation passes over unrelated messages.
- A response that arrives before its acknowledgement is still indicated.
- **Kept:** the `codec` feature and `MessageCodec`.
`tests/entity_interop.rs` and a fuzz target use them; the fuzz crate now
needs only `codec`. tokio is a dev-dependency only: `codec` no longer
enables it, and `tracing-subscriber` is gone from the crate and the
workspace, both from the review.
- **Docs:**
- `simple_doip/ARCHITECTURE.md` drops the old modules' sections,
including §2.5.2 ("What the old code did"), and is renumbered.
- In `uds_on_ip/ARCHITECTURE.md`, the "two server seams" prerequisite is
resolved and invariant 2 now holds.
  - The READMEs describe `Tester` and `Entity`.
- **`crates/simple_doip/MIGRATING.md`:** maps `Client` → `Tester` (and
`uds_client!` over `DoIpClientTransport`), `Server` → `Entity` with
`with_discovery` (and `uds_server!` over `DoIpTransport`), and
`bare_metal_entity` → `Entity` on an `edge-nal` adapter.
- The `bare_metal_entity` section is written for a firmware that drove
it from its network stack's callbacks. It says what such a firmware must
now supply and what changes on the wire.
- The whole guide is compiled, as `testing/doip-loopback`'s doctests:
the `Tester` and `Entity` examples, the `uds_client!` and `uds_server!`
ones over `uds_on_ip`, and the embassy-net one over the adapter. The
loopback crate is where every crate the guide reaches can be a
dev-dependency; from the review.
- **Changelogs** (under `[Unreleased]`, version 0.7.0):
- `simple_doip` covers everything since 0.6.0 and `uds_protocol`
everything since 0.1.0; both were checked with `cargo semver-checks`
against the published versions.
- `uds_session`, `uds_services` and `uds_on_ip` get first-release
entries.

The embedded probe's size is unchanged, since the probe never linked the
old code.

## Issues

- #25, #26 and #33 are fixed by deleting the tokio server. `Entity`
implements Table 12's timers, alive checks per Figures 26–28, and the
source-address checks, with tests.
- #34 is fixed by deleting the old client. `Tester` confirms `Ok` only
on acknowledgement code `0x00`, and `tests/tester.rs` covers a reserved
code.
- #48's last item was this deletion.
- #35 stays open: its NACK-path test gap is closed, but the
`Reserved(u8)` aliasing remains.
- #23 and #36: no remaining item names the old modules.

## Notes for review

- Halo's `rust_doip_shim` depends on the separate
`luminartech/simple_doip` repo at tag `v0.1.0`, not on this workspace,
so this PR cannot break its build. Halo is not changed.
- The `codec`, `alloc` and `std` layers are now used only by one test
and the fuzz crate. Moving `MessageCodec` behind a test-only feature was
deferred. Doing it after 0.7.0 would take another breaking release.
- `ci.yml:227`: a one-word comment edit (it no longer names
`tester_interop`).

## Testing

- [x] `just check-all`: fmt, the tests, clippy `-D warnings`, the
embedded build and size gate, and rustdoc, rerun after the rebase onto
#62 and the review changes.
- [x] `just miri`: 1,278 tests, including the tree-borrows run of the
connection tests.
- [x] `cargo +1.91 check --workspace --all-features --all-targets`
- [x] The quality gate with pedantic clippy, the feature powerset (38
combinations) and `-D missing_docs`, all clean.
- [x] `/code-review` and the Rust design review; their findings are
fixed in the last two commits.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Acceptance criteria before dft switches: four uds_on_ip behaviors learned on Iris hardware

2 participants