#![no_std]
pub mod transport;
mod resource;
pub use resource::ResourceRegistry;
#[cfg(feature = "profile")]
pub mod profile;
#[cfg(feature = "profile")]
pub use profile::init_dwt;
pub use telepath_macros::command;
use telepath_wire::{
framing::{cobs_decode, rzcobs_encode, FrameAccumulator},
Request, Response,
};
pub use telepath_wire::{
PacketType, ResponseStatus, WireError, CMD_ID_DISCOVERY, MAX_PAYLOAD_SIZE,
};
#[doc(hidden)]
pub use linkme as __linkme;
#[doc(hidden)]
pub use postcard_schema as __postcard_schema;
#[doc(hidden)]
pub use telepath_wire::cmd_id::derive_cmd_id as __derive_cmd_id;
pub type ShimFn = fn(
input: &[u8],
output: &mut [u8],
resources: &ResourceRegistry,
) -> Result<usize, DispatchError>;
pub type SchemaFn = fn(out: &mut [u8]) -> Result<usize, ()>;
#[derive(Clone, Copy)]
pub struct CommandMetadata {
pub name: &'static str,
pub id: u16,
pub invoke: ShimFn,
pub args_schema: SchemaFn,
pub ret_schema: SchemaFn,
pub arg_names: &'static str,
}
#[linkme::distributed_slice]
pub static TELEPATH_COMMANDS: [CommandMetadata] = [..];
pub fn commands() -> &'static [CommandMetadata] {
&TELEPATH_COMMANDS
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DispatchError {
UnknownCommand,
DeserializeError,
SerializeError,
PayloadTooLarge,
ResourceUnavailable,
}
pub struct TelepathServer<T, const N: usize> {
transport: T,
rx_accum: FrameAccumulator<N>,
tx_buf: [u8; N],
commands: &'static [CommandMetadata],
resources: ResourceRegistry,
}
impl<T, const N: usize> TelepathServer<T, N> {
pub fn new(transport: T, commands: &'static [CommandMetadata]) -> Self {
#[cfg(feature = "profile")]
profile::init_dwt();
Self {
transport,
rx_accum: FrameAccumulator::new(),
tx_buf: [0u8; N],
commands,
resources: ResourceRegistry::new(),
}
}
pub fn resource<R: 'static>(mut self, val: R) -> Self {
self.resources.insert(val);
self
}
pub fn find_command(&self, id: u16) -> Option<&CommandMetadata> {
self.commands.iter().find(|cmd| cmd.id == id)
}
pub fn dispatch(
&mut self,
cmd_id: u16,
input: &[u8],
output: &mut [u8],
) -> Result<usize, DispatchError> {
if cmd_id == telepath_wire::CMD_ID_DISCOVERY {
return self.handle_discovery(input, output);
}
let cmd = self
.find_command(cmd_id)
.ok_or(DispatchError::UnknownCommand)?;
(cmd.invoke)(input, output, &self.resources)
}
fn handle_discovery(&self, input: &[u8], output: &mut [u8]) -> Result<usize, DispatchError> {
use telepath_wire::{DiscoveryPage, DiscoveryRequest};
let offset = if input.is_empty() {
0u16
} else {
postcard::from_bytes::<DiscoveryRequest>(input)
.map_err(|_| DispatchError::DeserializeError)?
.offset
};
let total = self
.commands
.iter()
.filter(|c| c.id != telepath_wire::CMD_ID_DISCOVERY)
.count() as u16;
const PAGE_HEADER_BUDGET: usize = 16;
const ENTRIES_RAW_MAX: usize = MAX_PAYLOAD_SIZE - PAGE_HEADER_BUDGET;
let mut raw_entries = [0u8; ENTRIES_RAW_MAX];
let mut raw_cursor = 0usize;
let mut page_count = 0u32;
const SCHEMA_SCRATCH_LEN: usize = 128;
let mut args_scratch = [0u8; SCHEMA_SCRATCH_LEN];
let mut ret_scratch = [0u8; SCHEMA_SCRATCH_LEN];
let iter = self
.commands
.iter()
.filter(|c| c.id != telepath_wire::CMD_ID_DISCOVERY)
.skip(offset as usize);
for cmd in iter {
let n_args =
(cmd.args_schema)(&mut args_scratch).map_err(|_| DispatchError::SerializeError)?;
let n_ret =
(cmd.ret_schema)(&mut ret_scratch).map_err(|_| DispatchError::SerializeError)?;
let entry = telepath_wire::DiscoveryEntry {
id: cmd.id,
name: cmd.name,
args_schema: &args_scratch[..n_args],
ret_schema: &ret_scratch[..n_ret],
arg_names: cmd.arg_names,
};
let mut entry_tmp = [0u8; 300];
let entry_bytes = postcard::to_slice(&entry, &mut entry_tmp)
.map_err(|_| DispatchError::SerializeError)?;
let entry_size = entry_bytes.len();
if raw_cursor + entry_size > ENTRIES_RAW_MAX {
if raw_cursor == 0 {
return Err(DispatchError::SerializeError);
}
break; }
raw_entries[raw_cursor..raw_cursor + entry_size].copy_from_slice(entry_bytes);
raw_cursor += entry_size;
page_count += 1;
}
let mut entries_combined = [0u8; ENTRIES_RAW_MAX + 5];
let cnt_bytes = postcard::to_slice(&page_count, &mut entries_combined)
.map_err(|_| DispatchError::SerializeError)?;
let cnt_len = cnt_bytes.len();
entries_combined[cnt_len..cnt_len + raw_cursor].copy_from_slice(&raw_entries[..raw_cursor]);
let entries_len = cnt_len + raw_cursor;
let page = DiscoveryPage {
total,
offset,
entries: &entries_combined[..entries_len],
};
let written =
postcard::to_slice(&page, output).map_err(|_| DispatchError::SerializeError)?;
Ok(written.len())
}
}
impl<T: transport::Transport, const N: usize> TelepathServer<T, N> {
pub fn poll(&mut self) {
let mut byte = [0u8; 1];
loop {
let n = self.transport.read(&mut byte);
if n == 0 {
break;
}
if self.rx_accum.feed(byte[0]) {
self.process_frame();
self.rx_accum.reset();
}
}
}
fn process_frame(&mut self) {
let frame = match self.rx_accum.frame() {
Some(f) => f,
None => return,
};
let mut decoded = [0u8; N];
#[cfg(feature = "profile")]
let t0 = profile::cycles_now();
let decoded_len = match cobs_decode(frame, &mut decoded) {
Ok(n) => n,
Err(_) => return,
};
#[cfg(feature = "profile")]
{
use core::sync::atomic::Ordering;
let dt = profile::cycles_now().wrapping_sub(t0) as u64;
profile::DECODE_CYCLES.fetch_add(dt, Ordering::Relaxed);
profile::DECODED_BYTES.fetch_add(decoded_len as u32, Ordering::Relaxed);
}
let req: Request<'_> = match postcard::from_bytes(&decoded[..decoded_len]) {
Ok(r) => r,
Err(_) => return,
};
if req.kind != PacketType::Request {
return;
}
if req.args.len() > MAX_PAYLOAD_SIZE {
return;
}
let seq_no = req.seq_no;
let cmd_id = req.cmd_id;
let args = req.args;
let mut payload_buf = [0u8; N];
let (status, payload_len) = match self.dispatch(cmd_id, args, &mut payload_buf) {
Ok(n) if n > MAX_PAYLOAD_SIZE => (ResponseStatus::SystemError, 0),
Ok(n) => (ResponseStatus::Ok, n),
Err(_) => (ResponseStatus::SystemError, 0),
};
let resp = Response {
kind: PacketType::Response,
seq_no,
status,
payload: &payload_buf[..payload_len],
};
let mut serialized = [0u8; N];
let serialized_len = match postcard::to_slice(&resp, &mut serialized) {
Ok(s) => s.len(),
Err(_) => return,
};
#[cfg(feature = "profile")]
let t1 = profile::cycles_now();
let n = match rzcobs_encode(&serialized[..serialized_len], &mut self.tx_buf) {
Ok(n) => n,
Err(_) => return,
};
#[cfg(feature = "profile")]
{
use core::sync::atomic::Ordering;
let dt = profile::cycles_now().wrapping_sub(t1) as u64;
profile::ENCODE_CYCLES.fetch_add(dt, Ordering::Relaxed);
profile::ENCODED_BYTES.fetch_add(serialized_len as u32, Ordering::Relaxed);
profile::SAMPLE_COUNT.fetch_add(1, Ordering::Relaxed);
}
self.transport.write(&self.tx_buf[..n]);
}
}
#[cfg(test)]
mod tests {
extern crate std;
use super::*;
fn noop_shim(
_input: &[u8],
_output: &mut [u8],
_resources: &ResourceRegistry,
) -> Result<usize, DispatchError> {
Ok(0)
}
fn noop_schema(_out: &mut [u8]) -> Result<usize, ()> {
Ok(0)
}
static TEST_COMMANDS: [CommandMetadata; 1] = [CommandMetadata {
name: "ping",
id: 0x0001,
invoke: noop_shim,
args_schema: noop_schema,
ret_schema: noop_schema,
arg_names: "",
}];
struct FakeTransport;
#[test]
fn find_known_command() {
let server = TelepathServer::<FakeTransport, 256>::new(FakeTransport, &TEST_COMMANDS);
assert!(server.find_command(0x0001).is_some());
}
#[test]
fn find_unknown_command_returns_none() {
let server = TelepathServer::<FakeTransport, 256>::new(FakeTransport, &TEST_COMMANDS);
assert!(server.find_command(0xFFFF).is_none());
}
#[test]
fn dispatch_unknown_returns_error() {
let mut server = TelepathServer::<FakeTransport, 256>::new(FakeTransport, &TEST_COMMANDS);
let mut out = [0u8; 256];
assert_eq!(
server.dispatch(0xFFFF, &[], &mut out),
Err(DispatchError::UnknownCommand)
);
}
use telepath_wire::framing::{cobs_encode, rzcobs_decode};
use telepath_wire::{PacketType, Request, Response, ResponseStatus};
fn ping_shim(
_input: &[u8],
output: &mut [u8],
_resources: &ResourceRegistry,
) -> Result<usize, DispatchError> {
let val: u32 = 0xDEAD_BEEF;
let s = postcard::to_slice(&val, output).map_err(|_| DispatchError::SerializeError)?;
Ok(s.len())
}
static PING_COMMANDS: [CommandMetadata; 1] = [CommandMetadata {
name: "ping",
id: 0x0001,
invoke: ping_shim,
args_schema: noop_schema,
ret_schema: noop_schema,
arg_names: "",
}];
struct LoopbackTransport {
rx: std::vec::Vec<u8>,
tx: std::vec::Vec<u8>,
}
impl LoopbackTransport {
fn new(rx: std::vec::Vec<u8>) -> Self {
Self {
rx,
tx: std::vec::Vec::new(),
}
}
}
impl transport::Transport for LoopbackTransport {
fn read(&mut self, buf: &mut [u8]) -> usize {
if self.rx.is_empty() {
return 0;
}
let n = buf.len().min(self.rx.len());
buf[..n].copy_from_slice(&self.rx[..n]);
self.rx.drain(..n);
n
}
fn write(&mut self, buf: &[u8]) -> usize {
self.tx.extend_from_slice(buf);
buf.len()
}
}
#[test]
fn poll_ping_roundtrip() {
let req = Request {
kind: PacketType::Request,
seq_no: 42,
cmd_id: 0x0001,
args: &[],
};
let mut ser_buf = [0u8; 64];
let serialized = postcard::to_slice(&req, &mut ser_buf).unwrap();
let mut framed = [0u8; 64];
let n = cobs_encode(serialized, &mut framed).unwrap();
let transport = LoopbackTransport::new(framed[..n].to_vec());
let mut server = TelepathServer::<LoopbackTransport, 512>::new(transport, &PING_COMMANDS);
server.poll();
let tx = &server.transport.tx;
assert!(!tx.is_empty(), "server must have written a response");
let delim = tx
.iter()
.position(|&b| b == 0x00)
.expect("no frame delimiter");
let mut decoded = [0u8; 512];
let m = rzcobs_decode(&tx[..delim], &mut decoded).unwrap();
let resp: Response<'_> = postcard::from_bytes(&decoded[..m]).unwrap();
assert_eq!(resp.seq_no, 42);
assert_eq!(resp.status, ResponseStatus::Ok);
let val: u32 = postcard::from_bytes(resp.payload).unwrap();
assert_eq!(val, 0xDEAD_BEEF);
}
}