Repository navigation
feat!: the DoIP client transport, a uds_client! client over a Tester end to end (W9) - #62
Merged
Merged
Conversation
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.
…ransport tracks PEERS servers
…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>.
JustinKovacich
requested changes
Oct 12, 2026
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. |
Contributor
There was a problem hiding this comment.
Question Does this deserve its own architecture diagram?
Contributor
Author
There was a problem hiding this comment.
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.
Contributor
There was a problem hiding this comment.
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.
…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
approved these changes
Oct 12, 2026
JustinKovacich
left a comment
Contributor
There was a problem hiding this comment.
LGTM with one comment.
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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. Auds_client!client now reads from auds_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
mainafter W12 (#55).uds_on_ip:DoIpClientTransport<C: TesterConnection, QUEUE, PEERS = 1, WAITING = 2>It implements
UdsTransportandClientTransport, and absorbs what the tester asks of its caller:QUEUEbytes, up toWAITINGof them, so a keep-alive and a call's request never collide.WAITINGis at leastPEERS + 1, checked at compile time: a keep-alive per server, a functional one, and a call's request, one of them carried.DoIpTransportdoes.Closedper server tracked on it.expectedis true where the server's last message was a positiveDiagnosticSessionControlorECUResetresponse (ISO 14229-5:2022 REQ 7.9, 7.11).next_eventreconnects for it throughTesterConnection::reconnect, bounded by the call's deadline.62 F1 90from an answer to a read ofF18C.PEERSis 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.0x8004, REQ 7.16) to this tester becomeTransportEvent::Periodic.with_max_data_size(Option<u32>)takes the entity status response's value;outbound_maxis it less the 4 address bytes.ClientTransportError<C>is named by its connection.simple_doip(breaking)DiagnosticConnection::requestandDiagnosticEntity::requestare plain functions, no longer futures:Result<(), ConnectionRefusal>andResult<(), Refusal>.ConnectionRefusalisClosedorRefused(Refusal);tester::Erroris gone. A generic layer can now tell "reconnect" from "queue" from "failed", andRefusalstays the entity's vocabulary.TesterConnection::address()added.Closedonce per end, then waits for the caller's deadline. Previously it repeatedClosedon every call.TesterConnection::reconnect(deadline)returnsReconnection::{Connected, Deadline}. A reconnect stopped at its deadline leaves the back-off running from the loss.uds_services(breaking)ClientTransport: UdsTransportwithclose().Clientrequires it and gainsClient::close. Ending a session is the client role's alone, so the sharedUdsTransportseam stays role-neutral.Closedonly once its response window is open. One still awaiting itsDataConfwaits for it.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.
requestbecame synchronous andreconnecttakes a deadline. These came from a review after the checkpoint.The embedded probe's size moved three times, each measured to one commit:
Refusal::NotConnected's arm inDoIpTransport::refuse, and −8 B when the review moved it out ofRefusal;requeststopped being a future;Both are re-recorded in their own commits rather than the causing ones.
Testing
testing/doip-loopback/tests/client_path.rs, end to end over loopback sockets:uds_on_ip/tests/client_transport.rs: 39 tests over a scriptedTesterConnection.just check-allpasses (1408 tests);cargo +1.91 check --workspace --all-features --all-targetspasses.just miripasses on the rebased head (1291 tests).From the review
Awaiting::Confirm. One routine now fails every waiting request and frees its server, used by the connection's end, a failed reconnect andclose(); a waiting request the connection refuses when its turn comes frees its server too.Errto the caller, as before.WAITINGis a parameter, not a hidden 4, with thePEERS + 1relation checked bynew.Refusal::NotConnectedis gone; a connection refuses withConnectionRefusal.size_ofof the sensor's client transport (aTesterover a stub connector,QUEUE4096) in astaticthattools/check_size.pyreads from the image and holds exactly, as RAM is:client_transport12 632 B, insize-baseline.json.ARCHITECTURE.md§7.3 draws the client transport's connection states and what each edge owes.For 00-common / follow-ups
Closed";request's async signature;reconnect's signature;Closed, andClientTransport.simple_doip/ARCHITECTURE.md's UDP section still says "W9'soutbound_max"; that's a W12 sentence I left alone.🤖 Generated with Claude Code