Skip to content

feat(bare_metal): Add simple_someip polled based for bare-metal builds#126

Closed
FelicianoAngulo2 wants to merge 150 commits into
mainfrom
feat/polled-bared-metal
Closed

feat(bare_metal): Add simple_someip polled based for bare-metal builds#126
FelicianoAngulo2 wants to merge 150 commits into
mainfrom
feat/polled-bared-metal

Conversation

@FelicianoAngulo2

@FelicianoAngulo2 FelicianoAngulo2 commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

This pull request introduces new modules and features to support bare-metal and polled operation modes.
Additionallly, several improvements and refactoring to enhance flexibility, configurability, and clarity across the codebase, especially around the E2E registry and SOME/IP header handling. The most significant changes are the introduction of const-generic parameters for the E2E registry, improved documentation, and a more robust and flexible approach to handling SOME/IP headers.

Halo integration:

https://github.com/luminartech/halo/pull/4767

E2E Registry Refactor and Improvements

  • Refactored E2ERegistry to use a const-generic CAP parameter, allowing consumers to specify the registry's capacity at instantiation time for better configurability and memory usage. Updated all usages and documentation to reflect this change. [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14]
  • Changed error handling in E2ERegistry::register to report the actual capacity used, not just the default constant, improving diagnostics. [1] [2]

SOME/IP Header Handling Enhancements

  • Introduced a HEADER_SIZE constant for the SOME/IP header, replacing magic numbers throughout the code for clarity and maintainability. Updated HeaderView and all relevant parsing, encoding, and documentation to use this constant. [1] [2] [3] [4] [5] [6] [7]
  • Added a new upper_header_bytes method to HeaderView for extracting bytes needed by E2E Profile 5, improving zero-copy access and documentation.

Feature Flags and Modularization

  • Improved feature gating in Cargo.toml and the codebase: clarified which features pull in allocator-backed conveniences, and added new features/modules for bare-metal and polled operation (bare_metal_poll, single_context_mutex, polled). [1] [2] [3]
  • Updated imports and usage patterns to respect feature flags, ensuring no-alloc builds do not pull in unnecessary dependencies. [1] [2]

Documentation Improvements

  • Enhanced documentation throughout, clarifying the purpose and usage of new parameters, constants, and features, and providing guidance for consumers on configuring the registry and using new modules. [1] [2] [3] [4] [5] [6]

These changes collectively make the codebase more flexible, maintainable, and better suited for both embedded and standard environments.

JustinKovacich and others added 30 commits April 24, 2026 18:40
Covers the send path end-to-end (SOME/IP envelope, SD flags, OfferService
entry fields, and the IPv4 endpoint option) plus session-id advancement
and wrap-through-zero exercised via send_offer_service itself, and a
smoke test for Server::start_announcing. All `#[ignore]`d pending the
loopback MULTICAST-flag fix on this branch; without that fix, hosts
drop the multicast packet silently and the tests time out on recv.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Rustfmt on stable wraps the let-else continue blocks and the
assert_offer_matches signature differently than I hand-wrote.
Let cargo fmt normalize the style.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per AUTOSAR SOME/IP-SD, the reboot bit on emitted SD messages must
flip from RecentlyRebooted to Continuous once the session counter
wraps past 0xFFFF. SdStateManager already owns the counter, so it
also tracks wrap (new has_wrapped: AtomicBool latched exactly on
the 0xFFFF -> 0x0001 transition) and exposes reboot_flag() as the
single source of truth.

The four SD emission paths — SdStateManager::send_offer_service,
Server::send_unicast_offer, send_subscribe_ack_from_view, and
send_subscribe_nack_from_view — all now consume the tracked flag
instead of hardcoding Flags::new(true, true).

Coverage: four new non-ignored unit tests cover the state machine
(fresh, sub-wrap, exactly-on-wrap, monotonic-after-wrap);
assert_offer_matches takes an expected RebootFlag; the existing
wrap multicast test now asserts first-emit=RecentlyRebooted and
second-emit=Continuous across the boundary.

Responds to PR #75 feedback on src/server/sd_state.rs:90.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Round-2 Copilot review caught a real correctness bug in the prior
reboot-flag refactor: the four SD emission paths read
`reboot_flag()` BEFORE advancing the session counter, so the
message whose session_id crosses 0xFFFF -> 0x0001 (where
`has_wrapped` actually latches) still advertises
`RebootFlag::RecentlyRebooted`. The flip only lands on the NEXT
emission — violating AUTOSAR SOME/IP-SD semantics that say the
wrap message itself should carry `Continuous`.

Reordered in all four sites: call `next_session_id()` first so
`has_wrapped` latches, then read `reboot_flag()` for this specific
message. Sites:

- `SdStateManager::send_offer_service` (sd_state.rs)
- `Server::send_unicast_offer` (mod.rs)
- `Server::send_subscribe_ack_from_view` (mod.rs)
- `Server::send_subscribe_nack_from_view` (mod.rs)

Added short comments at each site pointing at the canonical
ordering note on `send_offer_service`.

Also reworded the multicast-loopback `#[ignore]` comment block and
per-test message to remove the stale branch-name reference
(`feature/firmware_someip_conversion`) — the underlying dependency
is the `lo` MULTICAST flag, not a branch-specific fix. New wording
says "skipped on hosts whose `lo` lacks the MULTICAST flag" with
the `ip link show lo` diagnostic pointer.

Coverage: the existing ignore-gated wrap test
`send_offer_service_wraps_session_id_through_zero_on_send` already
asserts the pre-wrap/post-wrap flag transition on-the-wire; with
the ordering fix it now passes in environments that run ignored
tests (would have FAILED before this commit — which is why the
bug slipped past the first round). The non-ignored state-machine
tests (`reboot_flag_flips_to_continuous_exactly_on_wrap` et al.)
are unaffected and still green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot round-3 flagged the `#[ignore]` reason on
start_announcing_emits_first_offer_within_timeout for still
carrying a branch-specific phrase ("re-enable after lo fix on
this branch"), which becomes stale once merged. Replaced with
a durable prerequisite description:

  requires loopback multicast support (MULTICAST on lo)

Matches the companion rewording in a638a4b on the sd_state.rs
multicast-loopback harness comment block.

Addresses Copilot comment 3132878961.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- inner.rs: on request_queue overflow, reject the ControlMessage by
  sending Err(Error::Capacity("request_queue")) on every oneshot it
  carries (via new ControlMessage::reject_with_capacity helper) rather
  than silently dropping it — the drop previously cancelled the oneshot
  and panicked callers awaiting with .unwrap().
- inner.rs: on pending_responses.insert saturation, destructure the
  returned (request_id, response) and send
  Err(Error::Capacity("pending_responses")) on the response sender;
  previously the sender was dropped, panicking PendingResponse::response.
- mod.rs: update the send_to_service saturation doc to match the new
  explicit-error behavior.
- mod.rs: fix the module-header footprint doc — SESSION_CAP lives in
  session.rs, not inner.rs.
- session.rs: gate the saturation warn! behind a one-shot saturation_warned
  latch; previously the warning fired on every check() against every new
  key once the map was full, which meant log-spam at the packet rate.
- error.rs: drop #[non_exhaustive] on client::Error; downstream crates
  relying on exhaustive matches shouldn't silently break before a planned
  breaking release.

New tests: reject_with_capacity_notifies_every_sender covers every
ControlMessage variant (including SendToService's dual senders);
capacity_overflow_warns_only_on_first_hit locks in the saturation latch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Commit cb1d0d1 added explicit capacity-error delivery when
pending_responses.insert overflows — without the explicit Err send,
the dropped Sender would cause PendingResponse::response().await to
panic on RecvError instead of surfacing a clean Result.

The recovery logic was inline in the SendToService run-loop arm,
which made it hard to exercise without driving 64 live sockets. Lift
it to a small `track_or_reject_pending_response` helper on Inner so
the SendToService arm delegates, and the branch is testable against
the exposed `pending_responses` map directly.

Added two tests:
- `track_or_reject_pending_response_inserts_when_room_available`:
  happy path — entry lands in the map, the sender is still live
  (receiver stays pending).
- `track_or_reject_pending_response_rejects_on_saturation`: fill
  map to PENDING_RESPONSES_CAP, invoke the helper with one more
  request, assert the map is unchanged and the caller's receiver
  resolves to Err(Error::Capacity("pending_responses")) — which is
  the invariant cb1d0d1 guarantees.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Round-2 Copilot feedback on PR #76 caught 5 resolvable issues (6th
rejected — see below):

- src/client/inner.rs: the three `push_front(...).ok()` re-enqueue
  sites (SetInterface x2, SendSD, Subscribe) silently dropped the
  returned Err. Swapped each to `if let Err(rejected) = ... { ...
  rejected.reject_with_capacity("request_queue"); }`, matching the
  primary `push_back` overflow arm. These branches are defensive —
  by construction the slot we just popped is free — but a future
  refactor that changes queue usage would otherwise reintroduce the
  silent-drop-of-oneshot-senders regression cb1d0d1 was specifically
  written to prevent.

- src/server/subscription_manager.rs: `list.push(...).ok()` on a
  freshly-allocated `SubscribersList` (first subscriber in a new
  event group) swapped to `.expect(...)` with a message naming the
  `SUBSCRIBERS_PER_GROUP >= 1` invariant. Tripwires a future cap-0
  regression at test time instead of silently losing the only
  subscriber.

- src/client/session.rs: reword the `SessionTracker` doc comment's
  reference to non-existent "module docs" — point directly at the
  `SESSION_CAP` constant instead.

Rejected: src/client/error.rs `Capacity` variant as a breaking
exhaustive-match break. This is consistent with the earlier
decision recorded on #75/#80 to accept the breaking change rather
than carry deprecated shims or wrap everything in `Error::Io` —
the current release is a breaking version bump by design, and
hiding the variant in `Io` would lose the lowercase-snake_case tag
semantics the new error was specifically designed to carry.

No production behavior changes — just defensive tripwires, docs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Address two clippy::pedantic warnings introduced by this branch's
own commits:

- missing_panics_doc on SubscriptionManager::subscribe — the
  heapless::Vec::push.expect on the first-insert path can only
  trip if SUBSCRIBERS_PER_GROUP is set to zero; document that.
- cast_possible_truncation on 'i as u32' in the saturation test
  for pending_responses — use u32::try_from with an expect that
  documents why the 64-cap fits.
Copilot round-3 on PR #76 (comment 3138669839) caught a real gap
in `track_or_reject_pending_response`: the helper only handled
`Err((key, value))` (map-full + new-key), but `heapless::IndexMap::
insert` on an existing key returns `Ok(Some(old_sender))` and the
old sender was silently dropped. If `request_id` is ever reused
while an older pending entry is still live — the documented
example is `session_counter` wrap-around — the caller awaiting
the original request would see a `RecvError` on channel
cancellation, which `PendingResponse::response()` turns into a
panic.

Reworked the `if let Err(...)` into a full `match` with all
three arms:

- `Ok(None)`            — normal insert, nothing to do.
- `Ok(Some(displaced))` — `warn!` that the slot was replaced,
                          complete the displaced sender with
                          `Err(Error::Capacity("pending_responses"))`
                          so the original caller gets a clean
                          `Result`, not a `RecvError` panic.
- `Err((_req_id, r))`   — existing saturation path (unchanged).

Also updated the doc comment on `track_or_reject_pending_response`
to describe the displacement contract.

Coverage: new test
`track_or_reject_pending_response_completes_displaced_sender`
explicitly inserts the same key twice and asserts the first
receiver resolves to `Err(Error::Capacity("pending_responses"))`
before the second sender still sits pending in the map.

Addresses Copilot comment 3138669839.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The old docstring explained why client::Error is not #[non_exhaustive]
but didn't acknowledge that adding variants to a non-#[non_exhaustive]
enum is itself a breaking change for downstream exhaustive matches.
Rewrite the note to call that out explicitly and flag future
#[non_exhaustive] as a planned breaking-release change, and record the
new Capacity variant under the Unreleased Added section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SubscriptionManager::subscribe previously returned () and logged a
warn! on capacity failure, which let handle_sd_message send
SubscribeAck to a client whose subscription had silently been dropped
— the client would believe it was subscribed but never receive any
events.

- subscribe now returns Result<(), SubscribeError> with
  SubscribersPerGroupFull / EventGroupsFull variants; existing
  capacity/refresh semantics are unchanged.
- handle_sd_message inspects the result and emits SubscribeNack with a
  reason derived from the SubscribeError variant on rejection.
- EventPublisher::register_subscriber surfaces the same Result so
  external SD dispatchers can take the same corrective action.
- SubscribeError is re-exported from server::mod.
- Tests updated to consume the Result; subscription_manager overflow
  tests now assert the specific error variant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ording

- session::check: expand the doc comment with an explicit "Capacity
  behavior" section — the saturation path does not update stored state
  for new keys, which contradicts the old "Always updates the stored
  state" wording. Describe the actual behavior (normal path updates,
  new-key inserts under saturation are silently dropped and return
  Initial repeatedly) so callers don't rely on a strict guarantee the
  implementation doesn't hold.
- session::check saturation warn!: include sender + transport in the
  log line so diagnosing which peer lost reboot-detection state no
  longer requires out-of-band correlation.
- SubscriptionManager::subscribe dedup branch: rename the log from
  "Refreshed existing subscriber" to "already subscribed; skipping"
  and add a comment making it explicit that no per-subscriber state
  is modified. The old wording implied a refresh (TTL bump, etc) the
  code never performed.
- inner.rs test comment: tokio::sync::oneshot doesn't drop the sender
  when the receiver is dropped — it flips send() to a failure mode.
  Rewrite the comment to describe the actual stash purpose (keep
  channels open for later observation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- server::run NACK arm: stop allocating a String with format!() held
  across the SubscribeNack await; log the SubscribeError at warn!
  separately and pass a static "subscription rejected" reason string
  to send_subscribe_nack_from_view. Cleaner and avoids the borrow-
  across-await style issue Copilot flagged.
- EventPublisher::register_subscriber: add # Errors section describing
  the two SubscribeError variants, that the subscriber is NOT
  registered on Err, and that external dispatchers should NACK on Err
  just like the server's own run() loop does.
- CHANGELOG: add server::SubscribeError to Added, and list the
  breaking signature changes on SubscriptionManager::subscribe and
  EventPublisher::register_subscriber under Changed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ubscribe docs

- CHANGELOG.md: drop the two "Changed" bullets about `std` becoming the
  default feature and `thiserror`/`tracing` moving to
  `default-features = false`. Those landed in 0.6.0/0.6.1 (commit
  f161980, already on main) and are not changes made in this PR.
- server/mod.rs: when a SubscribeError rejects a subscription, match on
  the variant and pass a specific `&'static str` reason to
  `send_subscribe_nack_from_view` (`"subscribers_per_group_full"` /
  `"event_groups_full"`) instead of the generic
  `"subscription rejected"`. The NACK log line now reflects the real
  cause, and the static-str choice avoids any String-across-await
  allocation.
- server/subscription_manager.rs: rewrite the `subscribe` docs to say
  the duplicate path is idempotent / deduplicated rather than implying
  TTL-refresh semantics that don't exist today. Flag the future-TTL
  extension point explicitly so the doc and the log wording stay in
  sync if that behavior is added later.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Commit 343da67 added a `required_size() > UDP_BUFFER_SIZE` pre-check
to `EventPublisher::publish_event` but left the new branch uncovered.
Regression guard added as `publish_event_pre_encode_exceeds_udp_buffer_returns_capacity_error`:
registers a subscriber (the pre-check sits after the `subscribers.is_empty()`
early return, so the test needs one or else hits the false-positive
Ok(0) path), constructs a 1501-byte fixture (16-byte header + 1485-byte
payload, one over the cap), calls publish_event, asserts
Err(Error::Capacity("udp_buffer")). Mirrors the fixture pattern from
`send_raw_message_exceeding_udp_buffer_returns_capacity_error` on the
client side.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- src/lib.rs: UDP_BUFFER_SIZE doc now enumerates exactly which send
  paths honor this cap (SocketManager::send, publish_event,
  publish_raw_event) and explicitly calls out the SD announcement /
  SubscribeAck / SubscribeNack paths that still use heap Vec buffers
  as a known gap planned for the bare-metal no_alloc refactor.
- src/server/event_publisher.rs: reworded "stack buffer sized to MTU"
  comments at the two buffer-allocation sites — the buffer lives in
  the async future's state, not literally on the stack, and the cap
  is a UDP payload limit, not an Ethernet MTU. New wording points at
  the UDP_BUFFER_SIZE docs for the distinction.

The two `use std::vec` comments (event_publisher.rs:335,
socket_manager.rs:362) were verified to be false positives: removing
the imports breaks the lib-test build with 4 errors about `vec!`
macro not in scope. Same no_std mechanics as the prior #75-1
resolution — reply posted on the comment threads.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three doc-only Copilot round-3 nits on PR #77:

- src/client/mod.rs: the per-`SocketManager` memory-footprint blurb
  implied the second `[u8; UDP_BUFFER_SIZE]` is always allocated.
  In fact `socket_loop_future` only allocates that scratch buffer
  when the send path actually needs E2E protection (the destination
  key is in the `E2ERegistry`); plain sends pay only ~1.5 KiB.
  Reworded the always-live vs peak budget so the ~24 KiB number is
  no longer presented as the steady-state cost.

- src/client/socket_manager.rs: the E2E-overflow test comment said
  "8 over MTU", but `UDP_BUFFER_SIZE` is documented as a UDP payload
  cap, not an Ethernet-MTU-safe size. Reworded to "8 bytes over
  UDP_BUFFER_SIZE".

- src/lib.rs: the `UDP_BUFFER_SIZE` doc referenced bare
  `Error::Capacity("udp_buffer")`, which is ambiguous at the crate
  root (no `crate::Error` exists). Qualified to
  `client::Error::Capacity(...)` / `server::Error::Capacity(...)`.

Addresses Copilot comments 3138697909, 3138698042, 3138698156.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…event

Adds a unit test that registers a Profile4 E2E profile for the message
key and publishes a message whose raw encoded size fits UDP_BUFFER_SIZE
(1496 bytes) but whose protected size does not (1508 bytes, after the
12-byte Profile4 header). Asserts that publish_event returns
Error::Capacity("udp_buffer") — exercising the post-protect guard that
was previously only covered on the client send path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- publish_event: log now says "E2E-protected datagram (... header +
  protected payload)" so the 16+protected_len value is identified as
  the full SOME/IP datagram size, not the payload.
- test fixture comment: "8 over MTU" → "8 bytes over UDP_BUFFER_SIZE"
  for terminology consistency with the rest of the PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…_len

`header_len + payload.len()` used unchecked `usize` addition. On a
system with large enough `payload.len()` the sum can wrap, silently
bypassing the `> UDP_BUFFER_SIZE` guard and corrupting the slice
operations that follow. Switch to `checked_add` and treat overflow the
same as exceeding `UDP_BUFFER_SIZE` — return
`Error::Capacity("udp_buffer")`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Client/server "E2E-protected payload" logs now say
  "E2E-protected datagram (header + protected payload)" since the
  logged value is the full SOME/IP datagram size, not the payload.
- publish_raw_event checked_add-fail log now describes the real
  condition (usize overflow) and includes the input lengths, instead
  of falsely pointing at UDP_BUFFER_SIZE.
- Oversize fixtures in socket_manager + event_publisher tests are now
  sized from UDP_BUFFER_SIZE (and PROFILE4_HEADER_SIZE implicitly for
  the E2E case) instead of hardcoded 1480/1485. Fixtures stay valid
  if the cap is retuned.
- client/mod.rs memory-footprint doc no longer hardcodes "(1500
  bytes)" or "~1.5 KiB" as load-bearing numbers; the scaling is now
  expressed in terms of UDP_BUFFER_SIZE with the current-default
  numbers as a parenthetical reference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The module-level "# `Send` and multithreaded executors" section showed
a HRTB bound on `<T as TransportSocketSendFut<'a>>::Fut: Send` as the
way consumers should bind `Send`. No such trait exists in this crate
— with RPITIT the returned future type is anonymous and cannot be
named, and introducing a GAT-style escape hatch would pollute the
trait for the common single-threaded case.

Replaced with the reviewer-preferred pattern: wrap the call in an
`async move` block and require `T: Send + 'static` on the captured
state. A tokio-backed implementation whose underlying `UdpSocket` is
already `Send + Sync` produces `Send` futures automatically via
async-block capture inference, so no trait-level bound is required.
Implementations holding `!Send` state fail the `T: Send` bound at
the `tokio::spawn` call site, which is the actionable location.

Docs-only change; `cargo test --doc` passes on the new ignore-fenced
example.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
JustinKovacich and others added 12 commits April 30, 2026 15:06
Adds chainable `with_*` setters on `ServerConfig` so callers can
override individual fields starting from `ServerConfig::new(...)`
without rewriting the full struct literal. Pure additive — public
fields and the existing constructor stay available.

New methods:
- `with_major_version(u8)` — defaults to 1.
- `with_minor_version(u32)` — defaults to 0.
- `with_ttl(u32)` — TTL in seconds, defaults to 3.
- `with_event_group(u16)` — append-only; panics on capacity (matches
  the implicit contract of the public `event_group_ids` field).
- `try_with_event_group(u16) -> Result<Self, Self>` — fallible
  variant; on `Err` returns the unmodified config (heapless::Vec
  guarantees push doesn't mutate on overflow).

No `Default` impl — `service_id` / `instance_id` have no sensible
defaults.

Two new unit tests:
- `server_config_builder_chain_overrides_each_field` — exercises
  the chain end-to-end.
- `server_config_try_with_event_group_rejects_at_capacity` —
  locks in the no-mutation-on-overflow contract.

Verification:
- `cargo check --workspace --all-features` clean.
- `cargo test --workspace --all-features --lib --tests
  -- --test-threads=1` — 530 lib (+2) + 26 integration tests pass.

Phase 21 sub-task 21e per `phase_21_api_symmetry.md`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `client::ClientChannelTypes<P>`, a marker trait whose
where-clause enumerates the seven `OneshotPooled` / `BoundedPooled`
/ `UnboundedPooled` entries that `define_static_channels!` must
declare for `Client` to compile against the resulting factory. A
blanket impl makes any `ChannelFactory` that satisfies all seven
automatically satisfy the trait.

Today the trait is discoverability-only — stable Rust does not
elaborate where-clause bounds on a trait, so each `impl<…> Client<…>`
block must still repeat the seven bounds inline. The trait's value
right now is rustdoc surface: one page enumerates the required
pool entries with a copy-pasteable shape, and it is reachable from
three places:
- Crate root (re-exported in `lib.rs`).
- The `define_static_channels!` macro doc, via a new "Required
  entries for Client" cross-link.
- The trait page itself documents the elaboration limitation
  honestly so users know not to depend on it as a generic bound.

Also re-exports `ControlMessage`, `SendMessage`, `ReceivedMessage`
at crate root so the channel-pool item types are reachable as
`simple_someip::ControlMessage` etc., not only via
`simple_someip::client::ControlMessage`. (`ClientUpdate` was
already re-exported.)

Replaces the previous comment-block documentation in
`src/client/mod.rs` (lines 67-87) — the trait's rustdoc carries
the same content in a discoverable location.

When stable Rust gains where-clause elaboration on traits, the
per-impl-block repetition collapses to a single
`C: ClientChannelTypes<P>` supertrait without changing the
outward contract; the trait was designed for that future.

Verification:
- `cargo check --workspace --all-features` clean.
- `cargo test --doc --all-features` — 11 passed (unchanged by
  21d), 1 pre-existing failure (`lib.rs - transport (line 309)`)
  still pre-existing.
- `cargo test --workspace --all-features --lib --tests
  -- --test-threads=1` — 530 lib + 26 integration tests pass.

Phase 21 sub-task 21d per `phase_21_api_symmetry.md`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses the HIGH and MEDIUM findings from the adversarial review of
21a/21c/21d/21e (full review captured in conversation; key findings
below). No new behavior, no further architecture; pure surface
hygiene + scope-completion.

## HIGH

1. **server/mod.rs: stale `S` in `publisher()` rustdoc.** 21a renamed
   the Subscription generic to `Sub` everywhere except this single
   docstring on the most-read accessor. Now reads `Arc<EventPublisher
   <R, Sub, H, T>>` consistently.

2. **`with_ttl` takes `Duration`.** 21e shipped `with_ttl(u32)` which
   leaves units ambiguous at the call site. Now takes
   `core::time::Duration`; sub-second precision truncates (rounded
   down per `Duration::as_secs`); over-`u32::MAX`-seconds saturates.
   Matches the original scoping-doc intent. Two unit tests lock the
   truncation + saturation behavior.

3. **Client struct asymmetry rationale.** Reviewer flagged that
   `Client<MessageDefinitions, R, I, C>` doesn't carry F/Tm/Sp the
   way `Server<F, Tm, R, Sub, …>` does. The asymmetry is structural
   (Client's value retains no transport/timer/spawner reference once
   constructed), not an oversight. Now documented as such on
   Client's struct rustdoc with explicit cross-reference to
   `ServerDeps`.

4. **`#[must_use]` on `try_with_event_group`.** Every other
   consuming setter on `ServerConfig` is `#[must_use]`; this one
   was missed.

5. **`ServerConfig::new` widened.** F5's stated complaint was the
   noisy 4-arg `new(interface, port, sid, iid)`. 21e shipped only the
   non-noisy override setters; the four-arg `new` remained. Now:
   `new(service_id, instance_id)` with `Ipv4Addr::UNSPECIFIED` /
   port `0` defaults, plus `with_interface` / `with_local_port`
   setters. Docstring is explicit that the defaults are dev-friendly,
   not production-ready.

   Net call-site change: 43 sites across 10 files rewritten via
   `ServerConfig::new(IFACE, PORT, SID, IID)` →
   `ServerConfig::new(SID, IID).with_interface(IFACE).with_local_port(PORT)`.
   Pure semver-major mechanical rewrite; no behavior change.

## MEDIUM

6. **`ClientChannelTypes` no longer at crate root.** Reviewer
   flagged that re-exporting at crate root tempts users into
   `fn f<C: ClientChannelTypes<P>>()` which fails on stable Rust's
   trait-elaboration limit. Trait stays in `client::` (so
   `define_static_channels!` users still find it via the macro
   cross-link), but does not appear in `simple_someip::*`
   autocomplete. Comment in `lib.rs` records the rationale.

7. **`ControlMessage`/`SendMessage`/`ReceivedMessage` no longer at
   crate root.** Same reasoning — they are
   implementation-detail-with-a-public-name (reachable for the
   `define_static_channels!` macro at `simple_someip::client::*`)
   rather than first-class crate-API types. Crate-root re-export
   would lock their shape into the public-API contract.

8. **`with_spawner` split into `with_spawner` (Send) +
   `with_local_spawner` (Local).** 21c's unbounded `with_spawner<Sp2>`
   deferred the `Spawner` / `LocalSpawner` bound check to
   `Client::new_with_deps`, producing diagnostics that pointed at
   the wrong call site. Now each `with_*` enforces its bound at the
   builder method, so type errors surface at the actual mistake.

## NOT addressed (reviewer flagged, deferred)

- Reordering `Client<MessageDefinitions, R, I, C>` itself — see #3
  above; documented as deliberate.
- 21c integration test for `ClientDeps::tokio()` + `with_*` —
  doc-tests cover type-composition; behavioral coverage can wait.
- 21e duplicate-event-group + panic-message tests — tracked, low
  risk.

## Verification

- `cargo check --workspace --all-features` clean.
- `cargo test --workspace --all-features --lib --tests
  -- --test-threads=1` — 532 lib (+2 from new with_ttl tests) +
  26 integration tests pass.
- `cargo test --doc --all-features` — 12 passed (was 11; +1 from
  new `ServerConfig::new` doc-example). Pre-existing
  `lib.rs - transport (line 309)` failure unchanged.
- `cargo run -p embassy_net_client` — live host-loopback wire-test
  green.

Phase 21 sub-task cleanup per `phase_21_api_symmetry.md`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
21b — Server::new and the *_with_deps variants now return a
(Server, ServerHandles, run-future) tuple mirroring Client::new.
The single returned run-future drives the receive loop *and* the
SD OfferService announcement loop concurrently via select; the
dispatcher topology where a co-located Client emits the offers
opts in via ServerConfig::with_announce(false) instead of "just
don't call this method." Server::announcement_loop /
announcement_loop_local and the AtomicBool latch they used to
fight over are removed — single entry point makes the
double-spawn failure mode structurally impossible. The previous
ServerHandles deps-bundle is renamed to ServerStorage; the
ServerHandles name now belongs to the post-construction accessor
struct. Server::set_local_port also went away — the constructor's
bind-time back-fill made it vestigial and mutating it post-bind
would lie to peers.

21F — SubscriptionHandle's subscribe / unsubscribe futures are
promoted from RPIT to named GATs (SubscribeFuture<'a> /
UnsubscribeFuture<'a>) so Server::run can declare + Send + 'static
explicitly with for<'a> Sub::SubscribeFuture<'a>: Send bounds in
its where clause. Compile errors for tokio::spawn-vs-!Send-handle
mismatches now surface at the library boundary instead of inside
tokio::spawn's bound check. for_each_subscriber stayed RPIT (no
Server::run-side Send-bound user). Constructors call a private
run_inner that's auto-trait-inferred so embassy paths
(EmbassyNetSocket: !Sync) keep using the constructors.

Run-loop logic moved out of &self methods on Server into free
async fns in src/server/runtime.rs; Server::run / run_with_buffers
clone the cheap shared-handles into an async move so the returned
future is independent of the &self borrow. The cancel-safety
SAFETY comment on the recv_from select is preserved at the new
call site.

Net diff: -254 lines despite adding GATs, the new submodule, and
a behavioral test for with_announce(false) suppression. 543 tests
pass (518 lib + 5 embassy-net + 11 client_server integration + 9
bare-metal/static-channels witnesses + a 0-test smoke + 3 ignored
vsomeip docker-deps).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nothing in the 0.7.0→0.8.0 stretch was ever published (no v0.8.0
git tag, no crates.io release), so phase 20 cleanup + phase 21
ergonomics merge into the 0.8.0 baseline rather than getting
their own version bump. The "Breaking" framing in the phase 21
section was relative to a transient in-tree state nobody saw, so
it's been retitled around 0.7.0 (the actual external baseline)
where it remained meaningful, and dropped where it didn't.

Section structure: [0.8.0] now contains the phase 21 narrative
sub-section followed by Added/Changed/Fixed clusters labeled by
era (phase 20 cleanup vs. phase 17 baseline) so the chronological
context is still visible to a reader walking the file top-down.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five comments, four substantive fixes + one prose tweak.

Multi-spawn race latch on Server::run / run_with_buffers: a caller
who spawned the constructor's run-future *and* called server.run()
or server.run_with_buffers() afterward would have two run-futures
racing on the same SD/unicast sockets and the SD session counter,
silently corrupting wire output. Adds an Arc<AtomicBool> `started`
field to Server (initialised false in all 4 constructor paths) and
a compare_exchange first-poll latch in both run_inner and
run_with_buffers — the second-to-be-polled future short-circuits
with Err(Error::InvalidUsage("server_already_running")). New unit
test second_run_future_returns_already_running locks both paths.

TransportSocket::recv_from contract: the cancel-safety requirement
the server runtime depends on lives on the trait now (its own
"# Cancel safety" rustdoc section), not as a free-floating remark
inside runtime::recv_loop. The previous in-source comment also
claimed "must stay FusedFuture + Unpin" which is wrong — the
futures are pinned in place via pin_mut! and don't need Unpin.
Comment rewritten to match.

ClientChannelTypes rustdoc: said the trait "is re-exported at
crate root" but it intentionally isn't (only at
crate::client::ClientChannelTypes — the crate root re-export was
deliberately dropped during the 21d cleanup to avoid tempting
generic users into the bound-elaboration cliff). Doc updated.

vsomeip_sd_compat tests: removed the no-op announce_handle shim
introduced during the 21b run-loop reshape. Announcements are now
folded into the combined run-future spawned as server_handle, so
keeping a dummy handle around made later abort/diagnostic logic
misleading.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`tracing-core` 0.1.33+ declares `extern crate alloc;` unconditionally
(`tracing-core-0.1.36/src/lib.rs:150`), so any target whose sysroot
ships `core` only — e.g. the AURIX TriCore LLVM-IR proxy used by the
halo build — fails to compile the dep tree even though simple-someip's
own bare-metal paths never touch alloc. Move tracing behind a feature
so those targets can opt out; std users keep tracing automatically via
the `std → tracing` implication so behavior is unchanged on host.

Internal call sites route through a new private `crate::log` module:
when `tracing` is on it re-exports `tracing::{debug,error,info,trace,
warn}` verbatim; when off it expands to a single `noop!` macro that
wraps `core::format_args!` in `if false { … }`. The dead block keeps
captured variables borrowed (no spurious `unused_variables` lints) and
optimizes out, leaving zero log code at the linker.

Six sites in `server/runtime.rs` were using tracing's structured-field
DSL (`error = %e, "msg"`); rewritten to plain format-args
(`"msg: {e}"`) since the no-op shim's `format_args!` can't accept the
DSL. Equivalent for log purposes.

Verified:
- `cargo tree --target thumbv7em-none-eabihf --no-default-features
  --features client,server,bare_metal --edges normal` shows zero
  tracing-related entries (the failing extern crate alloc no longer
  reachable).
- cortex-m4f release rlib still has zero `__rust_alloc` references.
- 532 lib + 20 integration tests pass under default features.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…estCallback

Two related changes to land the no-alloc bare-metal server path:
1.  drop _alloc from the server feature and gate every alloc usage (use alloc::sync::Arc, Wrappable handles, SocketOptions, sd-protocol import, run_inner's 64 KiB vec! and new_with_deps/new_passive_with_deps' Arc-wrap constructors) so client+server+bare_metal builds with zero __rust_alloc/__rg_alloc symbol references. The Server struct's started latch is now a feature-gated StartedLatch type alias (Arc<AtomicBool> under _alloc, &'static AtomicBool on bare metal) passed through ServerStorage; the H/Hsd/Hep default generics use cfg'd DefaultSocketHandle/DefaultSdStateHandle/DefaultEventPublisherHandle aliases so Arc names don't need to resolve under no-alloc. StaticSubscriptionHandle returns core::future::Ready instead of alloc::boxed::Box::pin (the lock closures are synchronous).
2. Add NonSdRequestCallback fn-pointer on ServerStorage/Server, threaded through run_with_buffers/run_combined/recv_loop, invoked in place of the historical 'non-SD ignored' branch — surfaces method requests / fire-and-forget calls (e.g. halo's HWP1* requests) to the consumer FFI without requiring a ChannelFactory-backed ServerUpdates channel.
@FelicianoAngulo2 FelicianoAngulo2 self-assigned this Jun 8, 2026
@FelicianoAngulo2
FelicianoAngulo2 force-pushed the feat/polled-bared-metal branch from 6d4b507 to e7c956c Compare June 8, 2026 21:07
@FelicianoAngulo2
FelicianoAngulo2 force-pushed the feat/polled-bared-metal branch from 2844504 to 4c2531d Compare June 9, 2026 13:58
@FelicianoAngulo2 FelicianoAngulo2 changed the title Feat/polled bared metal feat(bare_metal): Add someip polling processing feature Jun 9, 2026
@FelicianoAngulo2 FelicianoAngulo2 changed the title feat(bare_metal): Add someip polling processing feature feat(bare_metal): Add simple_someip rust feature into halo standalone build Jun 24, 2026
@FelicianoAngulo2 FelicianoAngulo2 changed the title feat(bare_metal): Add simple_someip rust feature into halo standalone build feat(bare_metal): Add simple_someip for bare-metal builds Jun 24, 2026
@FelicianoAngulo2 FelicianoAngulo2 changed the title feat(bare_metal): Add simple_someip for bare-metal builds feat(bare_metal): Add simple_someip polled based for bare-metal builds Jun 24, 2026
JustinKovacich added a commit that referenced this pull request Jun 26, 2026
Port-target branch off the #133 tip with a handoff for Feliciano: the
keep/drop/adapt classification of #126's 7 commits, the union
NonSdRequestCallback (already in the stack), the target server/E2E/
subscription APIs to adapt polled.rs against, the divergent-API touch
points in polled.rs, and the rationale for NOT adopting the const-generic
E2ERegistry refactor into the stack. polled.rs is a port (not a rebase)
because it sits on #126's own divergent server/e2e rework.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@JustinKovacich
JustinKovacich deleted the branch main June 29, 2026 20:40
@JustinKovacich

Copy link
Copy Markdown
Contributor

Auto-closed when its stacked base branch was deleted during post-0.8.0 branch cleanup. Base content is now in main, so retargeted to main and reopened. Head branch and commits were never touched.

@JustinKovacich
JustinKovacich changed the base branch from feature/phase21_api_symmetry to main June 29, 2026 20:45
@JustinKovacich

Copy link
Copy Markdown
Contributor

Superseded by #134 (itself folded into main/0.8.0). The polled foundation here (SD datagram builders, sync SOME/IP helpers, SingleContextRawMutex, const-generic E2ERegistry) landed in main via the 0.8.0 consolidation. The one divergent piece — removing the non-SD observer callback from Server — conflicts with main's direction (main keeps it). Closing as superseded; branch retained. @Feliciano: reopen/cherry-pick if any specific commit is still wanted.

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.

2 participants