tpt-e-link
RustTransport-agnostic, no_std, zero-allocation protocol crate shared between TPT host and embedded device firmware — the single source of truth for the wire format.
Languages
tpt-e-link
tpt-e-link is the foundational, transport-agnostic communication protocol
for the TPT Embedded Edge Ecosystem. It is the single source of truth for how
hosts (like tpt-basestation) and devices (like tpt-e-node) communicate.
It is designed to be strictly typed, no_std compatible, and deterministic,
ensuring that the host and device can never fall out of sync regarding the
"language" they speak.
Goals
- Zero-Desync Guarantee — by sharing this exact crate between host and device, protocol changes cause compile-time errors rather than runtime bugs.
- Transport Agnostic — defines what is being sent, not how. The protocol works seamlessly over UART, BLE, TCP, or WebSockets without changing the core logic.
no_std& Zero-Allocation — the core serialization and parsing logic runs on bare-metal microcontrollers with <64KB RAM, avoiding heap allocations entirely.- Mock-First — a pure-Rust mock transport layer (behind the
stdfeature) lets the entire protocol be tested in CI without physical hardware.
Non-Goals
- Hardware Initialization —
tpt-e-linkdoes not know what a GPIO pin or a UART peripheral is. That is the job oftpt-e-node. - Cloud Connectivity — this is strictly a local, peer-to-peer protocol.
Quickstart
use tpt_e_link::frame::{encode, FrameDecoder, MAX_FRAME_SIZE};
use tpt_e_link::{Arch, Capability, DeviceManifest, LinkMessage, Version};
// Build a manifest and encode it as a wire frame.
let mut manifest = DeviceManifest::new(*b"node-001", Version::new(0, 1, 0), Arch::Esp32Xtensa);
manifest.push_capability(Capability::Wifi).unwrap();
let mut buf = [0u8; MAX_FRAME_SIZE];
let len = encode(&LinkMessage::Handshake(manifest), &mut buf).unwrap();
let frame = &buf[..len]; // send this over UART/BLE/TCP/...
// On the receiving end, feed bytes to a `FrameDecoder` as they arrive.
let mut decoder = FrameDecoder::new();
decoder.feed(frame).unwrap();
if let Ok(Some(LinkMessage::Handshake(manifest))) = decoder.try_parse() {
println!("device {:?} reporting in", manifest.device_id);
}
See examples/loopback.rs for a complete,
runnable end-to-end demo (handshake → telemetry → heartbeat) over the mock
transport:
cargo run --example loopback --features std
Feature flags
- default (no features): pure
no_std, zero-alloc core — message types, postcard serialization, and the framing layer. std: unlocks a mock TCP transport (transport::mock) for host-side integration testing in CI, without requiring physical hardware.defmt: derivesdefmt::Formatfor every public type, for firmware that logs over RTT.
Crate layout
message— theLinkMessageenvelope enum and its payload types (DeviceManifest,TelemetryFrame,LogLine,CommandPayload,AckPayload).transport— theLinkTransporttrait (nativeasync fnin trait) and, understd, a mock transport implementation.frame— the wire framing layer: magic bytes, length prefix, message type, postcard payload, and CRC32, including partial-read and resync-on-garbage handling.
Wire compatibility
postcard/serde serialize enums by declaration-order index, not by name.
Capability, Arch, LogLevel, and MessageType all pin explicit
discriminants for this reason — only append new variants at the end;
reordering or removing one silently changes what already-deployed device
firmware transmits.
DeviceManifest::protocol_version carries the crate's
PROTOCOL_VERSION wire format version, separate from a device's own
firmware version, so a host/device pair running mismatched tpt-e-link
versions can detect it explicitly instead of getting opaque decode/CRC
errors. Bump PROTOCOL_VERSION whenever a change isn't backward
compatible. tests/wire_format.rs pins the exact encoded bytes of every
message type as a regression guard against accidental wire-format changes.
MSRV
Rust 1.75+ (required for native async fn in traits).
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.