Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e1312b8
feat(connector): add runtime-neutral serial client and server impleme…
lxsaah Sep 6, 2026
69b42ad
Refactor serial connector to unify runtime features and update depend…
lxsaah Sep 6, 2026
2a84a12
docs(serial-connector): record the runtime-neutral migration
lxsaah Sep 6, 2026
863e86e
Merge remote-tracking branch 'origin/main' into feat/platform-agnosti…
lxsaah Sep 9, 2026
1e9cd7d
docs(serial-connector): the crate contributes framing, not the transp…
lxsaah Sep 9, 2026
9f3a687
refactor(serial-connector): replace runtime-specific features with a …
lxsaah Sep 9, 2026
eb748e3
refactor(connector): move EmbassyUart to io module and update depende…
lxsaah Sep 9, 2026
4c73079
feat(ci): add job to build all binaries including examples
lxsaah Sep 9, 2026
67e3522
refactor(connector): integrate OneShot dialer for improved resource m…
lxsaah Sep 9, 2026
65fddef
refactor(docs): update changelog and example to reflect feature renam…
lxsaah Sep 9, 2026
cadcea4
docs(changelog): update to reflect changes in host read chunk size an…
lxsaah Sep 9, 2026
d2e2ca0
feat(connector): add logging for AimX serial server initialization
lxsaah Sep 9, 2026
2ab39fa
feat(connector): implement OneShotDialer and OneShotListener for poin…
lxsaah Sep 9, 2026
b1601fd
feat(connector): update serial connector features and improve build/t…
lxsaah Sep 9, 2026
bd57cd3
refactor(connector): update documentation for clarity and feature gating
lxsaah Sep 9, 2026
aa5b15b
feat(doc): enhance documentation generation for serial and TCP connec…
lxsaah Sep 9, 2026
82bc83b
refactor(connector): simplify SessionClientConnector by using OneShot…
lxsaah Sep 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,10 @@ jobs:

- name: Generate documentation
run: make doc

# `make check` runs every crate-level leg but not `examples`, so the demo
# binaries — the only end-to-end proof of the embedded path — were the one
# artifact CI never compiled. `all` is `build test examples`; the first two
# re-run here against a warm cache.
- name: Build everything, including the example binaries
run: make all
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 21 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,10 @@ build:
cargo build --package aimdb-websocket-connector --features "server,client"
@printf "$(YELLOW) → Building UDS connector$(NC)\n"
cargo build --package aimdb-uds-connector
@printf "$(YELLOW) → Building serial connector (neutral: framer + sugar, no adapter)$(NC)\n"
cargo build --package aimdb-serial-connector --no-default-features --features "connector"
@printf "$(YELLOW) → Building serial connector (tokio)$(NC)\n"
cargo build --package aimdb-serial-connector --no-default-features --features "tokio-runtime"
cargo build --package aimdb-serial-connector --no-default-features --features "std"
@printf "$(YELLOW) → Building TCP connector (tokio)$(NC)\n"
cargo build --package aimdb-tcp-connector --no-default-features --features "tokio-runtime"
@printf "$(YELLOW) → Building WASM adapter$(NC)\n"
Expand Down Expand Up @@ -218,9 +220,9 @@ test:
@printf "$(YELLOW) → Testing UDS connector$(NC)\n"
cargo test --package aimdb-uds-connector
@printf "$(YELLOW) → Testing serial connector (tokio: COBS framing + AimX round-trip over a duplex)$(NC)\n"
cargo test --package aimdb-serial-connector --no-default-features --features "_test-tokio"
cargo test --package aimdb-serial-connector --no-default-features --features "std"
@printf "$(YELLOW) → Testing serial connector (embassy: COBS framing + client-engine smoke on the EmbassyAdapter clock)$(NC)\n"
cargo test --package aimdb-serial-connector --no-default-features --features "embassy-runtime"
cargo test --package aimdb-serial-connector --no-default-features --features "_test-embassy"
@printf "$(YELLOW) → Testing TCP connector (tokio: length-prefix framing + AimX loopback)$(NC)\n"
cargo test --package aimdb-tcp-connector --no-default-features --features "_test-tokio"
@printf "$(YELLOW) → Testing TCP connector (embassy: socket recycle + concurrent slots + redial over an embassy-net loopback)$(NC)\n"
Expand Down Expand Up @@ -341,11 +343,11 @@ clippy:
@printf "$(YELLOW) → Clippy on UDS connector$(NC)\n"
cargo clippy --package aimdb-uds-connector --all-targets -- -D warnings
@printf "$(YELLOW) → Clippy on serial connector (tokio)$(NC)\n"
cargo clippy --package aimdb-serial-connector --no-default-features --features "_test-tokio" --all-targets -- -D warnings
cargo clippy --package aimdb-serial-connector --no-default-features --features "std" --all-targets -- -D warnings
@printf "$(YELLOW) → Clippy on serial connector (embassy)$(NC)\n"
cargo clippy --package aimdb-serial-connector --target thumbv7em-none-eabihf --no-default-features --features "embassy-runtime" -- -D warnings
cargo clippy --package aimdb-serial-connector --target thumbv7em-none-eabihf --no-default-features --features "_test-embassy" -- -D warnings
@printf "$(YELLOW) → Clippy on serial connector (embassy + defmt)$(NC)\n"
cargo clippy --package aimdb-serial-connector --target thumbv7em-none-eabihf --no-default-features --features "embassy-runtime,defmt" -- -D warnings
cargo clippy --package aimdb-serial-connector --target thumbv7em-none-eabihf --no-default-features --features "_test-embassy,defmt" -- -D warnings
@printf "$(YELLOW) → Clippy on TCP connector (tokio)$(NC)\n"
cargo clippy --package aimdb-tcp-connector --no-default-features --features "_test-tokio" --all-targets -- -D warnings
@printf "$(YELLOW) → Clippy on TCP connector (embassy)$(NC)\n"
Expand Down Expand Up @@ -382,12 +384,23 @@ doc:
cargo doc --package aimdb-persistence --no-deps
cargo doc --package aimdb-persistence-sqlite --no-deps
cargo doc --package aimdb-websocket-connector --features "tokio-runtime" --no-deps
cargo doc --package aimdb-uds-connector --no-deps
@# The serial and TCP connectors document a different item set per feature:
@# `std` adds the host-only port/socket helpers, `connector` is the neutral
@# half an MCU gets. Both legs run - a link from an ungated item to a gated
@# one only breaks in the leg that lacks it (see the embedded section below).
cargo doc --package aimdb-serial-connector --no-default-features --features "std" --no-deps
cargo doc --package aimdb-tcp-connector --no-default-features --features "std" --no-deps
cargo doc --package aimdb-client --features "transport-serial,transport-tcp" --no-deps
cargo doc --package aimdb-derive --no-deps
@cp -r target/doc/* target/doc-final/cloud/
@printf "$(YELLOW) → Building embedded documentation$(NC)\n"
cargo doc --package aimdb-core --no-default-features --features alloc --no-deps
cargo doc --package aimdb-embassy-adapter --features "embassy-runtime,net" --no-deps
cargo doc --package aimdb-mqtt-connector --no-default-features --features "embassy-runtime" --no-deps
cargo doc --package aimdb-knx-connector --no-default-features --features "embassy-runtime" --no-deps
cargo doc --package aimdb-serial-connector --no-default-features --features "connector" --no-deps
cargo doc --package aimdb-tcp-connector --no-default-features --features "connector" --no-deps
@cp -r target/doc/* target/doc-final/embedded/
@printf "$(YELLOW) → Building WASM/browser documentation$(NC)\n"
cargo doc --package aimdb-wasm-adapter --target wasm32-unknown-unknown --features "wasm-runtime" --no-deps
Expand Down Expand Up @@ -456,9 +469,9 @@ test-embedded:
@printf "$(YELLOW) → Checking aimdb-knx-connector (Embassy + defmt) on thumbv7em-none-eabihf target$(NC)\n"
cargo check --package aimdb-knx-connector --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "embassy-runtime,defmt"
@printf "$(YELLOW) → Checking aimdb-serial-connector (Embassy: full no_std AimX serial client+server) on thumbv7em-none-eabihf target$(NC)\n"
cargo check --package aimdb-serial-connector --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "embassy-runtime"
cargo check --package aimdb-serial-connector --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "_test-embassy"
@printf "$(YELLOW) → Checking aimdb-serial-connector (Embassy + defmt) on thumbv7em-none-eabihf target$(NC)\n"
cargo check --package aimdb-serial-connector --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "embassy-runtime,defmt"
cargo check --package aimdb-serial-connector --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "_test-embassy,defmt"
@printf "$(YELLOW) → Checking aimdb-tcp-connector (Embassy TCP client) on thumbv7em-none-eabihf target$(NC)\n"
cargo check --package aimdb-tcp-connector --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "embassy-runtime"
@printf "$(YELLOW) → Checking aimdb-tcp-connector (Embassy TCP client + defmt) on thumbv7em-none-eabihf target$(NC)\n"
Expand Down
2 changes: 1 addition & 1 deletion aimdb-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ aimdb-core = { version = "1.1.0", path = "../aimdb-core", features = [
# binary links only what it needs.
aimdb-uds-connector = { version = "0.1.0", path = "../aimdb-uds-connector", optional = true }
aimdb-serial-connector = { version = "0.1.0", path = "../aimdb-serial-connector", default-features = false, features = [
"tokio-runtime",
"std",
], optional = true }
aimdb-tcp-connector = { version = "0.1.0", path = "../aimdb-tcp-connector", default-features = false, features = [
"tokio-runtime",
Expand Down
2 changes: 1 addition & 1 deletion aimdb-client/src/endpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ pub fn dial(endpoint: &str) -> ClientResult<Box<dyn Dialer>> {
Scheme::Serial => {
#[cfg(feature = "transport-serial")]
{
Ok(Box::new(aimdb_serial_connector::SerialDialer::new(
Ok(Box::new(aimdb_serial_connector::SerialPortDialer::new(
parsed.target,
parsed.baud.unwrap_or(DEFAULT_SERIAL_BAUD),
)))
Expand Down
21 changes: 15 additions & 6 deletions aimdb-core/src/session/connector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ use crate::builder::AimDb;
use crate::connector::ConnectorBuilder;
use crate::session::{
pump_client, run_client, serve, ClientConfig, Dialer, Dispatch, EnvelopeCodec, Listener,
SessionConfig,
OneShot, SessionConfig,
};
use crate::DbResult;
use crate::{DbError, DbResult};

/// The default scheme a session connector registers when none is given.
pub const DEFAULT_SCHEME: &str = "remote";
Expand All @@ -48,7 +48,7 @@ type BuildFuture<'a> = Pin<Box<dyn Future<Output = DbResult<Vec<BoxFuture>>> + S
/// it in a one-line sugar constructor (e.g. `UdsClient`).
pub struct SessionClientConnector<D, C> {
scheme: String,
dialer: D,
dialer: OneShot<D>,
codec: C,
config: ClientConfig,
}
Expand All @@ -59,7 +59,7 @@ impl<D, C> SessionClientConnector<D, C> {
pub fn new(dialer: D, codec: C) -> Self {
Self {
scheme: DEFAULT_SCHEME.to_string(),
dialer,
dialer: OneShot::new(dialer),
codec,
config: ClientConfig::default(),
}
Expand All @@ -81,13 +81,22 @@ impl<D, C> SessionClientConnector<D, C> {

impl<D, C> ConnectorBuilder for SessionClientConnector<D, C>
where
D: Dialer + Clone + Send + Sync + 'static,
D: Dialer + Send + 'static,
C: EnvelopeCodec + Clone + 'static,
{
fn build<'a>(&'a self, db: &'a AimDb) -> BuildFuture<'a> {
Box::pin(async move {
// Taken on first poll, not at call time: a `build()` future dropped
// before it is polled must leave the dialer where it was.
let dialer = self
.dialer
.take()
.ok_or_else(|| DbError::InvalidOperation {
operation: "SessionClientConnector::build".to_string(),
reason: "the moved-in dialer was already taken; build() ran twice".to_string(),
})?;
let (handle, engine_fut) = run_client(
self.dialer.clone(),
dialer,
self.codec.clone(),
self.config.clone(),
db.runtime_ops(),
Expand Down
59 changes: 59 additions & 0 deletions aimdb-core/src/session/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,65 @@ impl<T> From<T> for OneShot<T> {
}
}

/// Hands out one pre-built connection, then refuses.
///
/// The [`Dialer`] a point-to-point transport wants: a moved-in UART, pipe or
/// socket has nothing to redial, so the second attempt is
/// [`TransportError::Closed`] — which [`run_client`](super::run_client) treats
/// as terminal — rather than a silent reconnect loop.
pub struct OneShotDialer<C> {
conn: OneShot<C>,
}

impl<C> OneShotDialer<C> {
/// Hold `conn` for the first [`connect`](Dialer::connect).
pub fn new(conn: C) -> Self {
Self {
conn: OneShot::new(conn),
}
}
}

impl<C: Connection + 'static> Dialer for OneShotDialer<C> {
fn connect(&self) -> BoxFut<'_, TransportResult<Box<dyn Connection>>> {
Box::pin(async move {
self.conn
.take()
.map(|c| Box::new(c) as Box<dyn Connection>)
.ok_or(TransportError::Closed)
})
}
}

/// Hands out one pre-built connection, then parks forever.
///
/// The [`Listener`] dual of [`OneShotDialer`]: [`serve`](super::serve) loops on
/// `accept`, and a point-to-point link has no second peer, so parking is the
/// correct end state rather than an error the loop would spin on.
pub struct OneShotListener<C> {
conn: OneShot<C>,
}

impl<C> OneShotListener<C> {
/// Hold `conn` for the first [`accept`](Listener::accept).
pub fn new(conn: C) -> Self {
Self {
conn: OneShot::new(conn),
}
}
}

impl<C: Connection + 'static> Listener for OneShotListener<C> {
fn accept(&mut self) -> BoxFut<'_, TransportResult<Box<dyn Connection>>> {
Box::pin(async move {
match self.conn.take() {
Some(c) => Ok(Box::new(c) as Box<dyn Connection>),
None => core::future::pending().await,
}
})
}
}

// ===========================================================================
// Compile-time assertions.
// ===========================================================================
Expand Down
3 changes: 2 additions & 1 deletion aimdb-core/src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ pub use endpoint::{split_host_port, split_host_port_opt, EndpointError};
#[cfg(feature = "connector-session")]
pub use io::{
ByteStream, Datagram, DatagramBinder, Delay, FrameFault, FramedConnection, Framer,
FramerFactory, FramingDialer, FramingListener, IoError, OneShot, StreamDialer, StreamListener,
FramerFactory, FramingDialer, FramingListener, IoError, OneShot, OneShotDialer,
OneShotListener, StreamDialer, StreamListener,
};
#[cfg(feature = "connector-session")]
pub use pump::{pump_sink, pump_source};
Expand Down
97 changes: 95 additions & 2 deletions aimdb-core/tests/session_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ use futures::StreamExt;

use aimdb_core::session::{
run_client, serve, AuthError, BoxFut, BoxStream, ClientConfig, CodecError, Connection, Dialer,
Dispatch, EnvelopeCodec, Inbound, Listener, Outbound, Payload, PeerInfo, RpcError, Session,
SessionConfig, SessionCtx, SessionLimits, SubUpdate, TransportError, TransportResult,
Dispatch, EnvelopeCodec, Inbound, Listener, OneShotDialer, OneShotListener, Outbound, Payload,
PeerInfo, RpcError, Session, SessionConfig, SessionCtx, SessionLimits, SubUpdate,
TransportError, TransportResult,
};

/// Engine-test clock (aimdb-core can't depend on a runtime adapter — that
Expand Down Expand Up @@ -1005,3 +1006,95 @@ async fn a_concurrent_batch_of_timeouts_leaves_the_connection_usable() {
drop(peer);
let _ = client.await;
}

// ===========================================================================
// One-shot transports — the moved-in dual of the re-dialable/re-bindable pair.
//
// Ported here from `aimdb-embassy-adapter`'s connector smoke test when the
// Embassy-specific spine was retired: the types are runtime-neutral now, and
// `serve` has no other test of its own.
// ===========================================================================

/// A moved-in stream has nothing to redial, so the second attempt must be an
/// error — and specifically `Closed`, which `run_client` treats as terminal
/// rather than backing off into a permanently-failing redial loop.
#[tokio::test]
async fn a_one_shot_dialer_hands_out_one_connection_then_reports_closed() {
let (a, _b) = conn_pair();
let dialer = OneShotDialer::new(a);

assert!(dialer.connect().await.is_ok(), "first connect hands it out");
assert_eq!(
dialer.connect().await.err(),
Some(TransportError::Closed),
"`Closed` is what stops the engine; `Io` would earn a backoff and retry"
);
}

/// The listener dual: `serve` loops on `accept`, so a point-to-point link with
/// no second peer must park rather than error — an erroring accept would tear
/// the server down.
#[tokio::test]
async fn a_one_shot_listener_parks_after_its_only_accept() {
let (a, _b) = conn_pair();
let mut listener = OneShotListener::new(a);

assert!(listener.accept().await.is_ok(), "first accept yields");
assert!(
tokio::time::timeout(Duration::from_millis(50), listener.accept())
.await
.is_err(),
"the second accept must park, not resolve"
);
}

/// End to end: `serve` handles the one peer the listener has, and when that
/// peer hangs up it goes back to waiting on the parked accept instead of
/// returning — the exact shape a UART server runs in.
#[tokio::test]
async fn serve_over_a_one_shot_listener_handles_the_peer_then_keeps_waiting() {
let writes: WriteLog = Arc::new(Mutex::new(Vec::new()));
let dispatch = Arc::new(EchoDispatch {
writes: writes.clone(),
});

let (server_end, mut client_end) = conn_pair();
let server = tokio::spawn(serve(
OneShotListener::new(server_end),
Arc::new(LineCodec),
dispatch,
SessionConfig::default(),
));

// One fire-and-forget write, then hang up.
client_end
.send(
b"WRITE
topic
hello",
)
.await
.expect("send the write");
drop(client_end);

// The session ran: the write reached the dispatch.
let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
loop {
if !writes.lock().unwrap().is_empty() {
break;
}
assert!(
tokio::time::Instant::now() < deadline,
"the write never reached the dispatch"
);
tokio::task::yield_now().await;
}

// ...and `serve` is still running, parked on the second accept.
assert!(
tokio::time::timeout(Duration::from_millis(100), server)
.await
.is_err(),
"serve must keep waiting on the parked accept, not return"
);
}
11 changes: 7 additions & 4 deletions aimdb-embassy-adapter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,20 @@ embassy-runtime = ["embassy-executor", "embassy-time", "embassy-sync"]
embassy-net-support = ["embassy-net"] # Network stack support for connectors

connectors = ["aimdb-core/connector-session"]
# The framed `Connection` spine plus the non-socket byte sources in `io`
# (`EmbassyUart`): core's I/O traits over `embedded-io-async`, no network stack.
connector-io = ["connectors", "dep:embedded-io-async"]

# Embassy sockets, dialers and UART halves behind core's runtime-neutral I/O
# traits, so connector crates need no `embassy-net` dependency. Deliberately
# Embassy sockets and dialers behind core's runtime-neutral I/O traits, so
# connector crates need no `embassy-net` dependency. Deliberately
# does not imply `embassy-time`: that turns on `defmt-timestamp-uptime`, whose
# `_defmt_timestamp` collides with every host test binary's `defmt::timestamp!`.
net = [
"connectors",
# The UART in `io` is `connector-io`'s, not this feature's; `net` enables it
# so one feature still gets a caller every byte source the adapter offers.
"connector-io",
"embassy-net-support",
"embassy-net/udp",
"dep:embedded-io-async",
"dep:embassy-futures",
]

Expand Down
Loading