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#[doc(hidden)]
58pub use telepath_wire::encode_app_error as __encode_app_error;
59
60pub type ShimFn = fn(
72 input: &[u8],
73 output: &mut [u8],
74 resources: &ResourceRegistry,
75) -> Result<DispatchOutcome, DispatchError>;
76
77pub type SchemaFn = fn(out: &mut [u8]) -> Result<usize, ()>;
82
83#[derive(Clone, Copy)]
88pub struct CommandMetadata {
89 pub name: &'static str,
91 pub id: u16,
94 pub invoke: ShimFn,
97 pub args_schema: SchemaFn,
100 pub ret_schema: SchemaFn,
103 pub arg_names: &'static str,
106}
107
108#[linkme::distributed_slice]
112pub static TELEPATH_COMMANDS: [CommandMetadata] = [..];
113
114pub fn commands() -> &'static [CommandMetadata] {
116 &TELEPATH_COMMANDS
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum DispatchError {
126 UnknownCommand,
128 DeserializeError,
130 SerializeError,
132 PayloadTooLarge,
134 ResourceUnavailable,
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum DispatchOutcome {
154 Ok(usize),
156 AppError(usize),
159}
160
161pub struct TelepathServer<T, const N: usize> {
174 transport: T,
175 rx_accum: FrameAccumulator<N>,
176 tx_buf: [u8; N],
177 commands: &'static [CommandMetadata],
180 resources: ResourceRegistry,
182}
183
184impl<T, const N: usize> TelepathServer<T, N> {
185 pub fn new(transport: T, commands: &'static [CommandMetadata]) -> Self {
187 #[cfg(feature = "profile")]
188 profile::init_dwt();
189 Self {
190 transport,
191 rx_accum: FrameAccumulator::new(),
192 tx_buf: [0u8; N],
193 commands,
194 resources: ResourceRegistry::new(),
195 }
196 }
197
198 pub fn resource<R: 'static>(mut self, val: R) -> Self {
204 self.resources.insert(val);
205 self
206 }
207
208 pub fn find_command(&self, id: u16) -> Option<&CommandMetadata> {
213 self.commands.iter().find(|cmd| cmd.id == id)
214 }
215
216 pub fn dispatch(
221 &mut self,
222 cmd_id: u16,
223 input: &[u8],
224 output: &mut [u8],
225 ) -> Result<DispatchOutcome, DispatchError> {
226 if cmd_id == telepath_wire::CMD_ID_DISCOVERY {
227 return self
228 .handle_discovery(input, output)
229 .map(DispatchOutcome::Ok);
230 }
231 let cmd = self
232 .find_command(cmd_id)
233 .ok_or(DispatchError::UnknownCommand)?;
234 (cmd.invoke)(input, output, &self.resources)
235 }
236
237 fn handle_discovery(&self, input: &[u8], output: &mut [u8]) -> Result<usize, DispatchError> {
246 use telepath_wire::{DiscoveryPage, DiscoveryRequest};
247
248 let offset = if input.is_empty() {
249 0u16
250 } else {
251 postcard::from_bytes::<DiscoveryRequest>(input)
252 .map_err(|_| DispatchError::DeserializeError)?
253 .offset
254 };
255
256 let total = self
257 .commands
258 .iter()
259 .filter(|c| c.id != telepath_wire::CMD_ID_DISCOVERY)
260 .count() as u16;
261
262 const PAGE_HEADER_BUDGET: usize = 16;
267 const ENTRIES_RAW_MAX: usize = MAX_PAYLOAD_SIZE - PAGE_HEADER_BUDGET;
268
269 let mut raw_entries = [0u8; ENTRIES_RAW_MAX];
270 let mut raw_cursor = 0usize;
271 let mut page_count = 0u32;
272
273 const SCHEMA_SCRATCH_LEN: usize = 128;
278
279 let mut args_scratch = [0u8; SCHEMA_SCRATCH_LEN];
281 let mut ret_scratch = [0u8; SCHEMA_SCRATCH_LEN];
282
283 let iter = self
284 .commands
285 .iter()
286 .filter(|c| c.id != telepath_wire::CMD_ID_DISCOVERY)
287 .skip(offset as usize);
288
289 for cmd in iter {
290 let n_args =
291 (cmd.args_schema)(&mut args_scratch).map_err(|_| DispatchError::SerializeError)?;
292 let n_ret =
293 (cmd.ret_schema)(&mut ret_scratch).map_err(|_| DispatchError::SerializeError)?;
294 let entry = telepath_wire::DiscoveryEntry {
295 id: cmd.id,
296 name: cmd.name,
297 args_schema: &args_scratch[..n_args],
298 ret_schema: &ret_scratch[..n_ret],
299 arg_names: cmd.arg_names,
300 };
301 let mut entry_tmp = [0u8; 300];
303 let entry_bytes = postcard::to_slice(&entry, &mut entry_tmp)
304 .map_err(|_| DispatchError::SerializeError)?;
305 let entry_size = entry_bytes.len();
306
307 if raw_cursor + entry_size > ENTRIES_RAW_MAX {
308 if raw_cursor == 0 {
309 return Err(DispatchError::SerializeError);
313 }
314 break; }
316 raw_entries[raw_cursor..raw_cursor + entry_size].copy_from_slice(entry_bytes);
317 raw_cursor += entry_size;
318 page_count += 1;
319 }
320
321 let mut entries_combined = [0u8; ENTRIES_RAW_MAX + 5];
323 let cnt_bytes = postcard::to_slice(&page_count, &mut entries_combined)
324 .map_err(|_| DispatchError::SerializeError)?;
325 let cnt_len = cnt_bytes.len();
326 entries_combined[cnt_len..cnt_len + raw_cursor].copy_from_slice(&raw_entries[..raw_cursor]);
327 let entries_len = cnt_len + raw_cursor;
328
329 let page = DiscoveryPage {
330 total,
331 offset,
332 entries: &entries_combined[..entries_len],
333 };
334 let written =
335 postcard::to_slice(&page, output).map_err(|_| DispatchError::SerializeError)?;
336 Ok(written.len())
337 }
338}
339
340impl<T: transport::Transport, const N: usize> TelepathServer<T, N> {
341 pub fn poll(&mut self) {
346 let mut byte = [0u8; 1];
347 loop {
348 let n = self.transport.read(&mut byte);
349 if n == 0 {
350 break;
351 }
352 if self.rx_accum.feed(byte[0]) {
353 self.process_frame();
354 self.rx_accum.reset();
355 }
356 }
357 }
358
359 fn process_frame(&mut self) {
361 let frame = match self.rx_accum.frame() {
362 Some(f) => f,
363 None => return,
364 };
365
366 let mut decoded = [0u8; N];
368 #[cfg(feature = "profile")]
369 let t0 = profile::cycles_now();
370 let decoded_len = match cobs_decode(frame, &mut decoded) {
371 Ok(n) => n,
372 Err(_) => return,
373 };
374 #[cfg(feature = "profile")]
375 {
376 use core::sync::atomic::Ordering;
377 let dt = profile::cycles_now().wrapping_sub(t0) as u64;
378 profile::DECODE_CYCLES.fetch_add(dt, Ordering::Relaxed);
379 profile::DECODED_BYTES.fetch_add(decoded_len as u32, Ordering::Relaxed);
380 }
381
382 let req: Request<'_> = match postcard::from_bytes(&decoded[..decoded_len]) {
384 Ok(r) => r,
385 Err(_) => return,
386 };
387
388 if req.kind != PacketType::Request {
390 return;
391 }
392
393 if req.args.len() > MAX_PAYLOAD_SIZE {
395 return;
396 }
397
398 let seq_no = req.seq_no;
399 let cmd_id = req.cmd_id;
400 let args = req.args;
401
402 let mut payload_buf = [0u8; N];
404 let (status, payload_len) = match self.dispatch(cmd_id, args, &mut payload_buf) {
405 Ok(DispatchOutcome::Ok(n)) if n > MAX_PAYLOAD_SIZE => (ResponseStatus::SystemError, 0),
406 Ok(DispatchOutcome::Ok(n)) => (ResponseStatus::Ok, n),
407 Ok(DispatchOutcome::AppError(n)) if n > MAX_PAYLOAD_SIZE => {
408 (ResponseStatus::SystemError, 0)
409 }
410 Ok(DispatchOutcome::AppError(n)) => (ResponseStatus::AppError, n),
411 Err(_) => (ResponseStatus::SystemError, 0),
412 };
413
414 let resp = Response {
416 kind: PacketType::Response,
417 seq_no,
418 status,
419 payload: &payload_buf[..payload_len],
420 };
421 let mut serialized = [0u8; N];
422 let serialized_len = match postcard::to_slice(&resp, &mut serialized) {
423 Ok(s) => s.len(),
424 Err(_) => return,
425 };
426
427 #[cfg(feature = "profile")]
429 let t1 = profile::cycles_now();
430 let n = match rzcobs_encode(&serialized[..serialized_len], &mut self.tx_buf) {
431 Ok(n) => n,
432 Err(_) => return,
433 };
434 #[cfg(feature = "profile")]
435 {
436 use core::sync::atomic::Ordering;
437 let dt = profile::cycles_now().wrapping_sub(t1) as u64;
438 profile::ENCODE_CYCLES.fetch_add(dt, Ordering::Relaxed);
439 profile::ENCODED_BYTES.fetch_add(serialized_len as u32, Ordering::Relaxed);
440 profile::SAMPLE_COUNT.fetch_add(1, Ordering::Relaxed);
441 }
442 self.transport.write(&self.tx_buf[..n]);
443 }
444}
445
446#[cfg(test)]
451mod tests {
452 extern crate std;
453 use super::*;
454
455 fn noop_shim(
456 _input: &[u8],
457 _output: &mut [u8],
458 _resources: &ResourceRegistry,
459 ) -> Result<DispatchOutcome, DispatchError> {
460 Ok(DispatchOutcome::Ok(0))
461 }
462
463 fn noop_schema(_out: &mut [u8]) -> Result<usize, ()> {
464 Ok(0)
465 }
466
467 static TEST_COMMANDS: [CommandMetadata; 1] = [CommandMetadata {
468 name: "ping",
469 id: 0x0001,
470 invoke: noop_shim,
471 args_schema: noop_schema,
472 ret_schema: noop_schema,
473 arg_names: "",
474 }];
475
476 struct FakeTransport;
477
478 #[test]
479 fn find_known_command() {
480 let server = TelepathServer::<FakeTransport, 256>::new(FakeTransport, &TEST_COMMANDS);
481 assert!(server.find_command(0x0001).is_some());
482 }
483
484 #[test]
485 fn find_unknown_command_returns_none() {
486 let server = TelepathServer::<FakeTransport, 256>::new(FakeTransport, &TEST_COMMANDS);
487 assert!(server.find_command(0xFFFF).is_none());
488 }
489
490 #[test]
491 fn dispatch_unknown_returns_error() {
492 let mut server = TelepathServer::<FakeTransport, 256>::new(FakeTransport, &TEST_COMMANDS);
493 let mut out = [0u8; 256];
494 let result = server.dispatch(0xFFFF, &[], &mut out);
495 assert_eq!(result, Err(DispatchError::UnknownCommand));
496 }
497
498 use telepath_wire::framing::{cobs_encode, rzcobs_decode};
503 use telepath_wire::{PacketType, Request, Response, ResponseStatus};
504
505 fn ping_shim(
507 _input: &[u8],
508 output: &mut [u8],
509 _resources: &ResourceRegistry,
510 ) -> Result<DispatchOutcome, DispatchError> {
511 let val: u32 = 0xDEAD_BEEF;
512 let s = postcard::to_slice(&val, output).map_err(|_| DispatchError::SerializeError)?;
513 Ok(DispatchOutcome::Ok(s.len()))
514 }
515
516 static PING_COMMANDS: [CommandMetadata; 1] = [CommandMetadata {
517 name: "ping",
518 id: 0x0001,
519 invoke: ping_shim,
520 args_schema: noop_schema,
521 ret_schema: noop_schema,
522 arg_names: "",
523 }];
524
525 struct LoopbackTransport {
527 rx: std::vec::Vec<u8>,
528 tx: std::vec::Vec<u8>,
529 }
530
531 impl LoopbackTransport {
532 fn new(rx: std::vec::Vec<u8>) -> Self {
533 Self {
534 rx,
535 tx: std::vec::Vec::new(),
536 }
537 }
538 }
539
540 impl transport::Transport for LoopbackTransport {
541 fn read(&mut self, buf: &mut [u8]) -> usize {
542 if self.rx.is_empty() {
543 return 0;
544 }
545 let n = buf.len().min(self.rx.len());
546 buf[..n].copy_from_slice(&self.rx[..n]);
547 self.rx.drain(..n);
548 n
549 }
550
551 fn write(&mut self, buf: &[u8]) -> usize {
552 self.tx.extend_from_slice(buf);
553 buf.len()
554 }
555 }
556
557 fn app_error_shim(
559 _input: &[u8],
560 output: &mut [u8],
561 _resources: &ResourceRegistry,
562 ) -> Result<DispatchOutcome, DispatchError> {
563 use telepath_wire::{encode_app_error, AppErrorPayload};
564 let payload = AppErrorPayload {
565 code: 42,
566 message: "test error",
567 };
568 let n = encode_app_error(&payload, output).map_err(|_| DispatchError::SerializeError)?;
569 Ok(DispatchOutcome::AppError(n))
570 }
571
572 static APP_ERROR_COMMANDS: [CommandMetadata; 1] = [CommandMetadata {
573 name: "app_error_cmd",
574 id: 0x0002,
575 invoke: app_error_shim,
576 args_schema: noop_schema,
577 ret_schema: noop_schema,
578 arg_names: "",
579 }];
580
581 #[test]
582 fn poll_app_error_roundtrip() {
583 let req = Request {
585 kind: PacketType::Request,
586 seq_no: 7,
587 cmd_id: 0x0002,
588 args: &[],
589 };
590 let mut ser_buf = [0u8; 64];
591 let serialized = postcard::to_slice(&req, &mut ser_buf).unwrap();
592 let mut framed = [0u8; 64];
593 let n = cobs_encode(serialized, &mut framed).unwrap();
594
595 let transport = LoopbackTransport::new(framed[..n].to_vec());
596 let mut server =
597 TelepathServer::<LoopbackTransport, 512>::new(transport, &APP_ERROR_COMMANDS);
598 server.poll();
599
600 let tx = &server.transport.tx;
602 assert!(!tx.is_empty(), "server must have written a response");
603
604 let delim = tx
605 .iter()
606 .position(|&b| b == 0x00)
607 .expect("no frame delimiter");
608 let mut decoded = [0u8; 512];
609 let m = rzcobs_decode(&tx[..delim], &mut decoded).unwrap();
610
611 let resp: Response<'_> = postcard::from_bytes(&decoded[..m]).unwrap();
612 assert_eq!(resp.seq_no, 7);
613 assert_eq!(resp.status, ResponseStatus::AppError);
614
615 let app_err = telepath_wire::decode_app_error(resp.payload).unwrap();
616 assert_eq!(app_err.code, 42);
617 assert_eq!(app_err.message, "test error");
618 }
619
620 #[test]
621 fn poll_ping_roundtrip() {
622 let req = Request {
624 kind: PacketType::Request,
625 seq_no: 42,
626 cmd_id: 0x0001,
627 args: &[],
628 };
629 let mut ser_buf = [0u8; 64];
630 let serialized = postcard::to_slice(&req, &mut ser_buf).unwrap();
631 let mut framed = [0u8; 64];
632 let n = cobs_encode(serialized, &mut framed).unwrap();
633
634 let transport = LoopbackTransport::new(framed[..n].to_vec());
635 let mut server = TelepathServer::<LoopbackTransport, 512>::new(transport, &PING_COMMANDS);
636 server.poll();
637
638 let tx = &server.transport.tx;
640 assert!(!tx.is_empty(), "server must have written a response");
641
642 let delim = tx
644 .iter()
645 .position(|&b| b == 0x00)
646 .expect("no frame delimiter");
647 let mut decoded = [0u8; 512];
648 let m = rzcobs_decode(&tx[..delim], &mut decoded).unwrap();
649
650 let resp: Response<'_> = postcard::from_bytes(&decoded[..m]).unwrap();
651 assert_eq!(resp.seq_no, 42);
652 assert_eq!(resp.status, ResponseStatus::Ok);
653
654 let val: u32 = postcard::from_bytes(resp.payload).unwrap();
655 assert_eq!(val, 0xDEAD_BEEF);
656 }
657}