1#![no_std]
29
30pub mod transport;
31
32mod resource;
33pub use resource::ResourceRegistry;
34
35#[cfg(feature = "profile")]
36pub mod profile;
37#[cfg(feature = "profile")]
38pub use profile::init_dwt;
39
40pub use telepath_macros::command;
41use telepath_wire::{
42 framing::{cobs_decode, rzcobs_encode, FrameAccumulator},
43 Request, Response,
44};
45pub use telepath_wire::{
46 PacketType, ResponseStatus, WireError, CMD_ID_DISCOVERY, MAX_PAYLOAD_SIZE,
47};
48
49#[doc(hidden)]
52pub use linkme as __linkme;
53#[doc(hidden)]
54pub use postcard_schema as __postcard_schema;
55#[doc(hidden)]
56pub use telepath_wire::cmd_id::derive_cmd_id as __derive_cmd_id;
57
58pub type ShimFn = fn(
68 input: &[u8],
69 output: &mut [u8],
70 resources: &ResourceRegistry,
71) -> Result<usize, DispatchError>;
72
73pub type SchemaFn = fn(out: &mut [u8]) -> Result<usize, ()>;
78
79#[derive(Clone, Copy)]
84pub struct CommandMetadata {
85 pub name: &'static str,
87 pub id: u16,
90 pub invoke: ShimFn,
93 pub args_schema: SchemaFn,
96 pub ret_schema: SchemaFn,
99 pub arg_names: &'static str,
102}
103
104#[linkme::distributed_slice]
108pub static TELEPATH_COMMANDS: [CommandMetadata] = [..];
109
110pub fn commands() -> &'static [CommandMetadata] {
112 &TELEPATH_COMMANDS
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum DispatchError {
122 UnknownCommand,
124 DeserializeError,
126 SerializeError,
128 PayloadTooLarge,
130 ResourceUnavailable,
133}
134
135pub struct TelepathServer<T, const N: usize> {
148 transport: T,
149 rx_accum: FrameAccumulator<N>,
150 tx_buf: [u8; N],
151 commands: &'static [CommandMetadata],
154 resources: ResourceRegistry,
156}
157
158impl<T, const N: usize> TelepathServer<T, N> {
159 pub fn new(transport: T, commands: &'static [CommandMetadata]) -> Self {
161 #[cfg(feature = "profile")]
162 profile::init_dwt();
163 Self {
164 transport,
165 rx_accum: FrameAccumulator::new(),
166 tx_buf: [0u8; N],
167 commands,
168 resources: ResourceRegistry::new(),
169 }
170 }
171
172 pub fn resource<R: 'static>(mut self, val: R) -> Self {
178 self.resources.insert(val);
179 self
180 }
181
182 pub fn find_command(&self, id: u16) -> Option<&CommandMetadata> {
187 self.commands.iter().find(|cmd| cmd.id == id)
188 }
189
190 pub fn dispatch(
194 &mut self,
195 cmd_id: u16,
196 input: &[u8],
197 output: &mut [u8],
198 ) -> Result<usize, DispatchError> {
199 if cmd_id == telepath_wire::CMD_ID_DISCOVERY {
200 return self.handle_discovery(input, output);
201 }
202 let cmd = self
203 .find_command(cmd_id)
204 .ok_or(DispatchError::UnknownCommand)?;
205 (cmd.invoke)(input, output, &self.resources)
206 }
207
208 fn handle_discovery(&self, input: &[u8], output: &mut [u8]) -> Result<usize, DispatchError> {
217 use telepath_wire::{DiscoveryPage, DiscoveryRequest};
218
219 let offset = if input.is_empty() {
220 0u16
221 } else {
222 postcard::from_bytes::<DiscoveryRequest>(input)
223 .map_err(|_| DispatchError::DeserializeError)?
224 .offset
225 };
226
227 let total = self
228 .commands
229 .iter()
230 .filter(|c| c.id != telepath_wire::CMD_ID_DISCOVERY)
231 .count() as u16;
232
233 const PAGE_HEADER_BUDGET: usize = 16;
238 const ENTRIES_RAW_MAX: usize = MAX_PAYLOAD_SIZE - PAGE_HEADER_BUDGET;
239
240 let mut raw_entries = [0u8; ENTRIES_RAW_MAX];
241 let mut raw_cursor = 0usize;
242 let mut page_count = 0u32;
243
244 const SCHEMA_SCRATCH_LEN: usize = 128;
249
250 let mut args_scratch = [0u8; SCHEMA_SCRATCH_LEN];
252 let mut ret_scratch = [0u8; SCHEMA_SCRATCH_LEN];
253
254 let iter = self
255 .commands
256 .iter()
257 .filter(|c| c.id != telepath_wire::CMD_ID_DISCOVERY)
258 .skip(offset as usize);
259
260 for cmd in iter {
261 let n_args =
262 (cmd.args_schema)(&mut args_scratch).map_err(|_| DispatchError::SerializeError)?;
263 let n_ret =
264 (cmd.ret_schema)(&mut ret_scratch).map_err(|_| DispatchError::SerializeError)?;
265 let entry = telepath_wire::DiscoveryEntry {
266 id: cmd.id,
267 name: cmd.name,
268 args_schema: &args_scratch[..n_args],
269 ret_schema: &ret_scratch[..n_ret],
270 arg_names: cmd.arg_names,
271 };
272 let mut entry_tmp = [0u8; 300];
274 let entry_bytes = postcard::to_slice(&entry, &mut entry_tmp)
275 .map_err(|_| DispatchError::SerializeError)?;
276 let entry_size = entry_bytes.len();
277
278 if raw_cursor + entry_size > ENTRIES_RAW_MAX {
279 if raw_cursor == 0 {
280 return Err(DispatchError::SerializeError);
284 }
285 break; }
287 raw_entries[raw_cursor..raw_cursor + entry_size].copy_from_slice(entry_bytes);
288 raw_cursor += entry_size;
289 page_count += 1;
290 }
291
292 let mut entries_combined = [0u8; ENTRIES_RAW_MAX + 5];
294 let cnt_bytes = postcard::to_slice(&page_count, &mut entries_combined)
295 .map_err(|_| DispatchError::SerializeError)?;
296 let cnt_len = cnt_bytes.len();
297 entries_combined[cnt_len..cnt_len + raw_cursor].copy_from_slice(&raw_entries[..raw_cursor]);
298 let entries_len = cnt_len + raw_cursor;
299
300 let page = DiscoveryPage {
301 total,
302 offset,
303 entries: &entries_combined[..entries_len],
304 };
305 let written =
306 postcard::to_slice(&page, output).map_err(|_| DispatchError::SerializeError)?;
307 Ok(written.len())
308 }
309}
310
311impl<T: transport::Transport, const N: usize> TelepathServer<T, N> {
312 pub fn poll(&mut self) {
317 let mut byte = [0u8; 1];
318 loop {
319 let n = self.transport.read(&mut byte);
320 if n == 0 {
321 break;
322 }
323 if self.rx_accum.feed(byte[0]) {
324 self.process_frame();
325 self.rx_accum.reset();
326 }
327 }
328 }
329
330 fn process_frame(&mut self) {
332 let frame = match self.rx_accum.frame() {
333 Some(f) => f,
334 None => return,
335 };
336
337 let mut decoded = [0u8; N];
339 #[cfg(feature = "profile")]
340 let t0 = profile::cycles_now();
341 let decoded_len = match cobs_decode(frame, &mut decoded) {
342 Ok(n) => n,
343 Err(_) => return,
344 };
345 #[cfg(feature = "profile")]
346 {
347 use core::sync::atomic::Ordering;
348 let dt = profile::cycles_now().wrapping_sub(t0) as u64;
349 profile::DECODE_CYCLES.fetch_add(dt, Ordering::Relaxed);
350 profile::DECODED_BYTES.fetch_add(decoded_len as u32, Ordering::Relaxed);
351 }
352
353 let req: Request<'_> = match postcard::from_bytes(&decoded[..decoded_len]) {
355 Ok(r) => r,
356 Err(_) => return,
357 };
358
359 if req.kind != PacketType::Request {
361 return;
362 }
363
364 if req.args.len() > MAX_PAYLOAD_SIZE {
366 return;
367 }
368
369 let seq_no = req.seq_no;
370 let cmd_id = req.cmd_id;
371 let args = req.args;
372
373 let mut payload_buf = [0u8; N];
375 let (status, payload_len) = match self.dispatch(cmd_id, args, &mut payload_buf) {
376 Ok(n) if n > MAX_PAYLOAD_SIZE => (ResponseStatus::SystemError, 0),
377 Ok(n) => (ResponseStatus::Ok, n),
378 Err(_) => (ResponseStatus::SystemError, 0),
379 };
380
381 let resp = Response {
383 kind: PacketType::Response,
384 seq_no,
385 status,
386 payload: &payload_buf[..payload_len],
387 };
388 let mut serialized = [0u8; N];
389 let serialized_len = match postcard::to_slice(&resp, &mut serialized) {
390 Ok(s) => s.len(),
391 Err(_) => return,
392 };
393
394 #[cfg(feature = "profile")]
396 let t1 = profile::cycles_now();
397 let n = match rzcobs_encode(&serialized[..serialized_len], &mut self.tx_buf) {
398 Ok(n) => n,
399 Err(_) => return,
400 };
401 #[cfg(feature = "profile")]
402 {
403 use core::sync::atomic::Ordering;
404 let dt = profile::cycles_now().wrapping_sub(t1) as u64;
405 profile::ENCODE_CYCLES.fetch_add(dt, Ordering::Relaxed);
406 profile::ENCODED_BYTES.fetch_add(serialized_len as u32, Ordering::Relaxed);
407 profile::SAMPLE_COUNT.fetch_add(1, Ordering::Relaxed);
408 }
409 self.transport.write(&self.tx_buf[..n]);
410 }
411}
412
413#[cfg(test)]
418mod tests {
419 extern crate std;
420 use super::*;
421
422 fn noop_shim(
423 _input: &[u8],
424 _output: &mut [u8],
425 _resources: &ResourceRegistry,
426 ) -> Result<usize, DispatchError> {
427 Ok(0)
428 }
429
430 fn noop_schema(_out: &mut [u8]) -> Result<usize, ()> {
431 Ok(0)
432 }
433
434 static TEST_COMMANDS: [CommandMetadata; 1] = [CommandMetadata {
435 name: "ping",
436 id: 0x0001,
437 invoke: noop_shim,
438 args_schema: noop_schema,
439 ret_schema: noop_schema,
440 arg_names: "",
441 }];
442
443 struct FakeTransport;
444
445 #[test]
446 fn find_known_command() {
447 let server = TelepathServer::<FakeTransport, 256>::new(FakeTransport, &TEST_COMMANDS);
448 assert!(server.find_command(0x0001).is_some());
449 }
450
451 #[test]
452 fn find_unknown_command_returns_none() {
453 let server = TelepathServer::<FakeTransport, 256>::new(FakeTransport, &TEST_COMMANDS);
454 assert!(server.find_command(0xFFFF).is_none());
455 }
456
457 #[test]
458 fn dispatch_unknown_returns_error() {
459 let mut server = TelepathServer::<FakeTransport, 256>::new(FakeTransport, &TEST_COMMANDS);
460 let mut out = [0u8; 256];
461 assert_eq!(
462 server.dispatch(0xFFFF, &[], &mut out),
463 Err(DispatchError::UnknownCommand)
464 );
465 }
466
467 use telepath_wire::framing::{cobs_encode, rzcobs_decode};
472 use telepath_wire::{PacketType, Request, Response, ResponseStatus};
473
474 fn ping_shim(
476 _input: &[u8],
477 output: &mut [u8],
478 _resources: &ResourceRegistry,
479 ) -> Result<usize, DispatchError> {
480 let val: u32 = 0xDEAD_BEEF;
481 let s = postcard::to_slice(&val, output).map_err(|_| DispatchError::SerializeError)?;
482 Ok(s.len())
483 }
484
485 static PING_COMMANDS: [CommandMetadata; 1] = [CommandMetadata {
486 name: "ping",
487 id: 0x0001,
488 invoke: ping_shim,
489 args_schema: noop_schema,
490 ret_schema: noop_schema,
491 arg_names: "",
492 }];
493
494 struct LoopbackTransport {
496 rx: std::vec::Vec<u8>,
497 tx: std::vec::Vec<u8>,
498 }
499
500 impl LoopbackTransport {
501 fn new(rx: std::vec::Vec<u8>) -> Self {
502 Self {
503 rx,
504 tx: std::vec::Vec::new(),
505 }
506 }
507 }
508
509 impl transport::Transport for LoopbackTransport {
510 fn read(&mut self, buf: &mut [u8]) -> usize {
511 if self.rx.is_empty() {
512 return 0;
513 }
514 let n = buf.len().min(self.rx.len());
515 buf[..n].copy_from_slice(&self.rx[..n]);
516 self.rx.drain(..n);
517 n
518 }
519
520 fn write(&mut self, buf: &[u8]) -> usize {
521 self.tx.extend_from_slice(buf);
522 buf.len()
523 }
524 }
525
526 #[test]
527 fn poll_ping_roundtrip() {
528 let req = Request {
530 kind: PacketType::Request,
531 seq_no: 42,
532 cmd_id: 0x0001,
533 args: &[],
534 };
535 let mut ser_buf = [0u8; 64];
536 let serialized = postcard::to_slice(&req, &mut ser_buf).unwrap();
537 let mut framed = [0u8; 64];
538 let n = cobs_encode(serialized, &mut framed).unwrap();
539
540 let transport = LoopbackTransport::new(framed[..n].to_vec());
541 let mut server = TelepathServer::<LoopbackTransport, 512>::new(transport, &PING_COMMANDS);
542 server.poll();
543
544 let tx = &server.transport.tx;
546 assert!(!tx.is_empty(), "server must have written a response");
547
548 let delim = tx
550 .iter()
551 .position(|&b| b == 0x00)
552 .expect("no frame delimiter");
553 let mut decoded = [0u8; 512];
554 let m = rzcobs_decode(&tx[..delim], &mut decoded).unwrap();
555
556 let resp: Response<'_> = postcard::from_bytes(&decoded[..m]).unwrap();
557 assert_eq!(resp.seq_no, 42);
558 assert_eq!(resp.status, ResponseStatus::Ok);
559
560 let val: u32 = postcard::from_bytes(resp.payload).unwrap();
561 assert_eq!(val, 0xDEAD_BEEF);
562 }
563}