Skip to content
File

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

rust160 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;
9/// RFC 6716 silence packet; keep the RTP clock running during pause.
10pub const SILENCE: &[u8] = &[0xf8, 0xff, 0xfe];
11 
12#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
13#[serde(rename_all = "lowercase")]
14pub enum Action {
15 Play,
16 Pause,
17 Restart,
18 Next,
19}
20 
21/// Deserialization enforces exactly three integer color components in 0..=255.
22#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
23#[serde(transparent)]
24pub struct Color(pub [u8; 3]);
25 
26#[derive(Debug, Deserialize)]
27#[serde(deny_unknown_fields)]
28pub struct Command {
29 pub led: Option<Color>,
30 pub action: Option<Action>,
31 pub command_id: Option<String>,
32}
33 
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum CommandError {
36 TooLarge,
37 Invalid,
38 Empty,
39 IdTooLong,
40}
41 
42impl std::fmt::Display for CommandError {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 write!(f, "invalid control command: {self:?}")
45 }
46}
47impl std::error::Error for CommandError {}
48 
49impl Command {
50 pub fn parse(bytes: &[u8]) -> Result<Self, CommandError> {
51 if bytes.len() > COMMAND_LIMIT {
52 return Err(CommandError::TooLarge);
53 }
54 let command: Self = serde_json::from_slice(bytes).map_err(|_| CommandError::Invalid)?;
55 if command.led.is_none() && command.action.is_none() {
56 return Err(CommandError::Empty);
57 }
58 if command.command_id.as_ref().is_some_and(|id| id.len() > 64) {
59 return Err(CommandError::IdTooLong);
60 }
61 Ok(command)
62 }
63}
64 
65#[derive(Debug, Serialize)]
66pub struct Ack<'a> {
67 pub event: &'static str,
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub command_id: Option<&'a str>,
70 pub result: i32,
71 pub led: Color,
72 pub paused: bool,
73}
74 
75#[derive(Debug, Serialize)]
76#[serde(rename_all = "camelCase")]
77pub struct Telemetry {
78 pub event: &'static str,
79 pub firmware: &'static str,
80 pub sequence: u64,
81 pub uptime_ms: u64,
82 pub position_ms: u32,
83 pub duration_ms: u32,
84 pub playback_revision: u32,
85 pub music: MusicStatus,
86 pub paused: bool,
87 pub rssi: Option<i32>,
88 pub hardware: Option<crate::metrics::Hardware>,
89 pub heap: u32,
90 pub audio_errors: u64,
91 pub data_errors: u64,
92 pub skipped_frames: u64,
93 pub rejected_commands: u64,
94 pub dropped_commands: u32,
95 pub stack_free: u32,
96 pub spectrum: SpectrumStatus,
97 pub led: Color,
98}
99 
100#[derive(Debug, Clone, Copy, Serialize)]
101#[serde(rename_all = "camelCase")]
102pub struct MusicStatus {
103 pub cache_bytes: u32,
104 pub index_bytes: u32,
105 pub reads: u32,
106 pub max_read_us: u32,
107}
108 
109#[derive(Debug, Clone, Copy, Serialize)]
110#[serde(rename_all = "camelCase")]
111pub struct SpectrumStatus {
112 pub source: &'static str,
113 pub fft_size: u32,
114 pub sample_rate: u32,
115 pub core: u32,
116 pub frames: u64,
117 pub errors: u64,
118 pub resets: u64,
119 pub input_dropped: u64,
120 pub output_dropped: u64,
121 pub stale: u64,
122 pub mean_us: u32,
123 pub max_us: u32,
124 pub stack_free: u32,
125}
126impl Default for SpectrumStatus {
127 fn default() -> Self {
128 Self {
129 source: "fft",
130 fft_size: crate::spectrum::FFT_SIZE as u32,
131 sample_rate: crate::spectrum::SAMPLE_RATE,
132 core: 1,
133 frames: 0,
134 errors: 0,
135 resets: 0,
136 input_dropped: 0,
137 output_dropped: 0,
138 stale: 0,
139 mean_us: 0,
140 max_us: 0,
141 stack_free: 0,
142 }
143 }
144}
145 
146/// V2: version, pause flag, reserved bytes, LE u32 transport time and song position
147/// in milliseconds, 32 normalized bands, then the LE playback revision (48 bytes total).
148pub fn spectrum(due: Due, bands: &[u8; BANDS]) -> [u8; 48] {
149 let mut bytes = [0; 48];
150 bytes[0] = 2;
151 bytes[1] = u8::from(due.paused);
152 bytes[4..8].copy_from_slice(&due.pts_ms.to_le_bytes());
153 bytes[8..12].copy_from_slice(&(due.index * FRAME_MS).to_le_bytes());
154 if !due.paused {
155 bytes[12..44].copy_from_slice(bands);
156 }
157 bytes[44..48].copy_from_slice(&due.revision.to_le_bytes());
158 bytes
159}