Embedded devices
Run a no_std endpoint on your board's smoltcp stack.
This guide assumes your board already has a working
smoltcp Device and configured Interface with an
IP address and route. minip2p does not set up the MAC, PHY, DHCP, SLAAC, or
interrupt controller. You supply the network device, monotonic timer, and
hardware entropy.
An embedded endpoint runs the same TCP, Noise XX, Yamux, Identify, Ping, and application protocols as a hosted TCP endpoint. Only the I/O provider and driving loop change.
Add the dependency
Disable default features. smoltcp pulls in portable TCP and the shared
embedded stack:
cargo add minip2p-rs --no-default-features --features smoltcp
Add only what you need:
| Feature | Adds |
|---|---|
pubsub |
Gossipsub or floodsub, plus optional signed-beacon discovery |
portable-mdns |
Portable mDNS and the shared discovery book; already included by smoltcp |
portable-autonat |
AutoNAT probing over the portable endpoint |
portable-relay |
Relay-only circuit connections; includes portable AutoNAT |
The quiche adapter needs std, so embedded endpoints cannot use QUIC or DCUtR.
They can still use relay circuits.
What you supply
You own three things:
- A
smoltcp::phy::Deviceand configuredsmoltcp::iface::Interface. - A monotonic millisecond counter used to build
Nowvalues. - An
EntropySourcefrom a cryptographically secure hardware RNG or a securely seeded DRBG.
Do not use a predictable PRNG. Identity, Noise, and protocol nonces all depend on this entropy; if it fails, the operation must fail.
Wrap the network objects in one shared stack:
use minip2p::SmoltcpStack;
let device = board_network_device();
let interface = configured_smoltcp_interface(&mut device);
let stack = SmoltcpStack::new(device, interface);
SmoltcpStack is single-threaded on purpose. Clones let the TCP and mDNS
adapters install sockets on the same interface, but the endpoint polls them
serially. Do not hold stack.borrow_mut() while calling the endpoint.
Build the endpoint
Load or create an Ed25519 identity, inject entropy, add a TCP listener, and
register application protocols before build:
use minip2p::Endpoint;
let identity = load_device_identity()?;
let entropy = HardwareEntropy::new(board_rng());
let mut endpoint = Endpoint::portable(&identity, entropy)
.agent_version("sensor-node/0.1.0")
.smoltcp(stack)
.listen("/ip4/0.0.0.0/tcp/4001")
.protocol("/example/sensor/1.0.0")
.build()?;
Prefer a real interface address over 0.0.0.0 when Identify and discovery
should advertise something peers can dial right away. When DHCP or SLAAC
changes the interface addresses, update the shared smoltcp interface; the
adapters see that on later polls.
Optional services go on the same builder. The following example requires the
smoltcp and pubsub features because signed discovery uses pubsub:
let mut endpoint = Endpoint::portable(&identity, entropy)
.smoltcp(stack)
.listen("/ip4/0.0.0.0/tcp/4001")
.mdns()
.discovery()
.build()?;
For this combination, install with
cargo add minip2p-rs --no-default-features --features smoltcp,pubsub. mDNS
does not use pubsub.
Drive time and network progress
No executor, no background thread. Sample the monotonic clock once, pass that
same Now into the endpoint, handle every event, then use next_deadline to
decide how long the board may sleep:
use minip2p::{Now, SmoltcpEvent};
loop {
let now = Now::from_millis(monotonic_millis());
for event in endpoint.poll(now)? {
match event {
SmoltcpEvent::Endpoint(event) => handle_endpoint_event(event),
SmoltcpEvent::Discovery(event) => handle_discovery_event(event),
other => handle_optional_service_event(other),
}
}
let wait_ms = endpoint
.next_deadline(now)
.map(|deadline| deadline.millis_until(now));
wait_for_network_irq_or_timer(wait_ms);
}
Wake early when the network stack gets a frame or transmit capacity frees up.
PollDeadline::IMMEDIATE means poll again without sleeping; None means
minip2p has no timer armed, but a network interrupt must still wake the loop.
Budget memory explicitly
Defaults aim at small systems; they are not a promise every board can afford
the same concurrency. Set limits before build:
use minip2p::{SmoltcpConfig, TcpConfig};
let provider = SmoltcpConfig {
rx_buffer: 4 * 1024,
tx_buffer: 4 * 1024,
backlog: 1,
max_sockets: 4,
..SmoltcpConfig::default()
};
let transport = TcpConfig {
max_connections: 3,
max_buffered_send: 16 * 1024,
..TcpConfig::default()
};
let endpoint = Endpoint::portable(&identity, entropy)
.smoltcp(stack)
.smoltcp_config(provider)
.tcp_config(transport)
.listen("/ip4/0.0.0.0/tcp/4001")
.build()?;
The provider reserves rx_buffer + tx_buffer bytes per socket, up to
max_sockets. Backlog sockets count toward that ceiling. Smaller buffers save
RAM but hurt throughput on high-latency links; smaller socket and connection
caps reject excess work instead of allocating past the budget.
Also budget stream payloads and pubsub queues. Treat backpressure as normal: wait for later polls instead of spinning or growing an unbounded host queue.
Bring-up checklist
- Confirm the interface can exchange ordinary IP and TCP traffic before adding libp2p.
- Keep one persistent Ed25519 secret per device when peer identity must survive reboot.
- Verify the entropy source’s startup and continuous health checks.
- Drive the endpoint after receive interrupts and at every reported deadline.
- Advertise a real interface address, not a wildcard bind address.
- Measure flash, static RAM, heap high-water mark, and stack use on the actual target with production feature flags.
- Test stalled peers, socket exhaustion, link loss, address changes, and long idle periods.
just check-nostd builds the portable crates for thumbv7em-none-eabi. The
smoltcp end-to-end tests cover TCP, Noise XX, Yamux, Identify, Ping, streams,
discovery, and mDNS across two in-memory network devices. Those checks prove
portability and protocol behavior; they do not replace testing the real board
driver, timer, RNG, and memory budget.
Next: Register a protocol or review the feature matrix.