Skip to content
File

Blob: archive/fft-benchmark/firmware/crates/radio-core/src/protocol.rs

rust110 lines
1//! Browser wire contracts. Maintenance commands are deliberately a separate API.
2use crate::{
3 music::{BANDS, FRAME_MS},
4 playback::Due,
5};
6use serde::{Deserialize, Serialize};
7 
8pub const COMMAND_LIMIT: usize = 512;
9pub const ROBOT_ID: u16 = 2;
10pub const SPECTRUM_ID: u16 = 4;
11/// RFC 6716 silence packet; keep the RTP clock running during pause.
12pub const SILENCE: &[u8] = &[0xf8, 0xff, 0xfe];
13 
14#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
15#[serde(rename_all = "lowercase")]
16pub enum Action {
17 Play,
18 Pause,
19 Restart,
20}
21 
22/// Deserialization enforces exactly three integer color components in 0..=255.
23#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
24#[serde(transparent)]
25pub struct Color(pub [u8; 3]);
26 
27#[derive(Debug, Deserialize)]
28#[serde(deny_unknown_fields)]
29pub struct Command {
30 pub led: Option<Color>,
31 pub action: Option<Action>,
32 pub command_id: Option<String>,
33}
34 
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum CommandError {
37 TooLarge,
38 Invalid,
39 Empty,
40 IdTooLong,
41}
42 
43impl std::fmt::Display for CommandError {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 write!(f, "invalid control command: {self:?}")
46 }
47}
48impl std::error::Error for CommandError {}
49 
50impl Command {
51 pub fn parse(bytes: &[u8]) -> Result<Self, CommandError> {
52 if bytes.len() > COMMAND_LIMIT {
53 return Err(CommandError::TooLarge);
54 }
55 let command: Self = serde_json::from_slice(bytes).map_err(|_| CommandError::Invalid)?;
56 if command.led.is_none() && command.action.is_none() {
57 return Err(CommandError::Empty);
58 }
59 if command.command_id.as_ref().is_some_and(|id| id.len() > 64) {
60 return Err(CommandError::IdTooLong);
61 }
62 Ok(command)
63 }
64}
65 
66#[derive(Debug, Serialize)]
67pub struct Ack<'a> {
68 pub event: &'static str,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub command_id: Option<&'a str>,
71 pub result: i32,
72 pub led: Color,
73 pub paused: bool,
74}
75 
76#[derive(Debug, Serialize)]
77#[serde(rename_all = "camelCase")]
78pub struct Telemetry {
79 pub event: &'static str,
80 pub firmware: &'static str,
81 pub sequence: u64,
82 pub random: u32,
83 pub uptime_ms: u64,
84 pub position_ms: u32,
85 pub duration_ms: u32,
86 pub paused: bool,
87 pub rssi: i32,
88 pub heap: u32,
89 pub audio_errors: u64,
90 pub data_errors: u64,
91 pub skipped_frames: u64,
92 pub rejected_commands: u64,
93 pub dropped_commands: u32,
94 pub stack_free: u32,
95 pub led: Color,
96}
97 
98/// Version, pause flag, reserved bytes, LE time/position, then 32 normalized bands.
99pub fn spectrum(due: Due, bands: &[u8; BANDS]) -> [u8; 44] {
100 let mut bytes = [0; 44];
101 bytes[0] = 1;
102 bytes[1] = u8::from(due.paused);
103 bytes[4..8].copy_from_slice(&due.pts_ms.to_le_bytes());
104 bytes[8..12].copy_from_slice(&(due.index * FRAME_MS).to_le_bytes());
105 if !due.paused {
106 bytes[12..].copy_from_slice(bands);
107 }
108 bytes
109}