dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 1 | //! protocol message types. spec: `IPC.md`. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 2 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 3 | use std::collections::BTreeMap; |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 4 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 5 | use serde::{Deserialize, Serialize}; |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 6 | use serde_json::Value; |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 7 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 8 | use crate::error::ProtoError; |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 9 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 10 | /// subscription topic. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 11 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 12 | #[serde(rename_all = "kebab-case")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 13 | #[non_exhaustive] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 14 | pub enum Topic { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 15 | /// loudness / peak / GR telemetry. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 16 | Meters, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 17 | Profile, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 18 | Routing, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 19 | Daemon, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 20 | /// synthetic, for `hello`; clients never subscribe. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 21 | Control, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 22 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 23 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 24 | impl Topic { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 25 | #[must_use] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 26 | pub fn as_str(self) -> &'static str { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 27 | match self { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 28 | Topic::Meters => "meters", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 29 | Topic::Profile => "profile", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 30 | Topic::Routing => "routing", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 31 | Topic::Daemon => "daemon", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 32 | Topic::Control => "control", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 33 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 34 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 35 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 36 | /// topics clients may subscribe to (excludes `Control`). |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 37 | #[must_use] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 38 | pub const fn subscribable() -> &'static [Topic] { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 39 | &[Topic::Meters, Topic::Profile, Topic::Routing, Topic::Daemon] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 40 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 41 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 42 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 43 | impl std::fmt::Display for Topic { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 44 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 45 | f.write_str(self.as_str()) |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 46 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 47 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 48 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 49 | /// routing decision for one app or stream. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 50 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 51 | #[serde(rename_all = "lowercase")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 52 | pub enum Route { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 53 | /// through the processed (filtered) sink. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 54 | Processed, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 55 | /// straight to the real hardware sink; no processing, no extra graph hop. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 56 | Bypass, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 57 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 58 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 59 | impl Route { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 60 | #[must_use] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 61 | pub fn as_str(self) -> &'static str { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 62 | match self { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 63 | Route::Processed => "processed", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 64 | Route::Bypass => "bypass", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 65 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 66 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 67 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 68 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 69 | impl std::fmt::Display for Route { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 70 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 71 | f.write_str(self.as_str()) |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 72 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 73 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 74 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 75 | /// all operations. wire form `{ "op": "<name>", "args": <args> }`; arg-less ops omit `args`. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 76 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 77 | #[serde(tag = "op", content = "args")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 78 | #[non_exhaustive] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 79 | pub enum Op { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 80 | #[serde(rename = "status")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 81 | Status, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 82 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 83 | #[serde(rename = "profile.list")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 84 | ProfileList, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 85 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 86 | #[serde(rename = "profile.use")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 87 | ProfileUse { name: String }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 88 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 89 | /// `name` omitted ⇒ active profile. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 90 | #[serde(rename = "profile.show")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 91 | ProfileShow { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 92 | #[serde(default, skip_serializing_if = "Option::is_none")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 93 | name: Option<String>, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 94 | }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 95 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 96 | /// reload all profile files from disk. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 97 | #[serde(rename = "profile.reload")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 98 | ProfileReload, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 99 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 100 | #[serde(rename = "route.list")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 101 | RouteList, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 102 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 103 | /// add/replace a persistent routing rule. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 104 | #[serde(rename = "route.set")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 105 | RouteSet { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 106 | /// typically `application.process.binary`. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 107 | app: String, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 108 | to: Route, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 109 | }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 110 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 111 | #[serde(rename = "route.unset")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 112 | RouteUnset { app: String }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 113 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 114 | /// one-shot reroute of a live stream. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 115 | #[serde(rename = "route.stream")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 116 | RouteStream { node_id: u32, to: Route }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 117 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 118 | #[serde(rename = "setting.get")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 119 | SettingGet { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 120 | /// dotted key, e.g. `compressor.threshold_db`. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 121 | key: String, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 122 | }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 123 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 124 | #[serde(rename = "setting.set")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 125 | SettingSet { key: String, value: Value }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 126 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 127 | #[serde(rename = "setting.list")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 128 | SettingList, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 129 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 130 | /// revert one key to the active profile's value. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 131 | #[serde(rename = "setting.clear")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 132 | SettingClear { key: String }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 133 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 134 | /// revert all settings to the active profile; leaves route/per-app/bypass overrides intact. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 135 | #[serde(rename = "setting.reset")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 136 | SettingReset, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 137 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 138 | /// global bypass kill switch. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 139 | #[serde(rename = "bypass.set")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 140 | BypassSet { enabled: bool }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 141 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 142 | /// per-app (Layer A) controller state for managed streams. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 143 | #[serde(rename = "per-app.list")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 144 | LayerAList, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 145 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 146 | /// Layer A enable for one app (persistent overlay override). |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 147 | #[serde(rename = "per-app.set")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 148 | PerAppSet { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 149 | /// process_binary or application_name. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 150 | app: String, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 151 | enabled: bool, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 152 | }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 153 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 154 | /// Layer A master switch (persistent overlay override). |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 155 | #[serde(rename = "per-app.master")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 156 | PerAppMaster { enabled: bool }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 157 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 158 | /// clear a stream's deference lock (user-ceiling / strict-mode) so the controller resumes. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 159 | #[serde(rename = "per-app.reset")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 160 | LayerAReset { node_id: u32 }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 161 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 162 | #[serde(rename = "subscribe")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 163 | Subscribe { topics: Vec<Topic> }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 164 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 165 | #[serde(rename = "unsubscribe")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 166 | Unsubscribe { topics: Vec<Topic> }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 167 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 168 | /// optional client protocol handshake |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 169 | #[serde(rename = "hello")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 170 | Hello { protocol: u32 }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 171 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 172 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 173 | impl Op { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 174 | /// canonical wire name. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 175 | #[must_use] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 176 | pub fn name(&self) -> &'static str { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 177 | match self { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 178 | Op::Status => "status", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 179 | Op::ProfileList => "profile.list", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 180 | Op::ProfileUse { .. } => "profile.use", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 181 | Op::ProfileShow { .. } => "profile.show", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 182 | Op::ProfileReload => "profile.reload", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 183 | Op::RouteList => "route.list", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 184 | Op::RouteSet { .. } => "route.set", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 185 | Op::RouteUnset { .. } => "route.unset", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 186 | Op::RouteStream { .. } => "route.stream", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 187 | Op::SettingGet { .. } => "setting.get", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 188 | Op::SettingSet { .. } => "setting.set", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 189 | Op::SettingList => "setting.list", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 190 | Op::SettingClear { .. } => "setting.clear", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 191 | Op::SettingReset => "setting.reset", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 192 | Op::BypassSet { .. } => "bypass.set", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 193 | Op::LayerAList => "per-app.list", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 194 | Op::PerAppSet { .. } => "per-app.set", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 195 | Op::PerAppMaster { .. } => "per-app.master", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 196 | Op::LayerAReset { .. } => "per-app.reset", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 197 | Op::Subscribe { .. } => "subscribe", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 198 | Op::Unsubscribe { .. } => "unsubscribe", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 199 | Op::Hello { .. } => "hello", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 200 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 201 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 202 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 203 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 204 | /// client-to-server request. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 205 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 206 | pub struct Request { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 207 | /// client-chosen; echoed in the paired response. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 208 | pub id: u64, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 209 | /// flattened: contributes `op` and optional `args` alongside `id`. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 210 | #[serde(flatten)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 211 | pub op: Op, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 212 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 213 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 214 | impl Request { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 215 | #[must_use] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 216 | pub fn new(id: u64, op: Op) -> Self { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 217 | Self { id, op } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 218 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 219 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 220 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 221 | /// server-to-client response, paired to a request by `id`. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 222 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 223 | pub struct Response { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 224 | pub id: u64, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 225 | #[serde(flatten)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 226 | pub payload: ResponsePayload, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 227 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 228 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 229 | impl Response { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 230 | /// # Errors |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 231 | /// serde error if `value` fails to serialize. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 232 | pub fn ok<T: Serialize>(id: u64, value: &T) -> Result<Self, serde_json::Error> { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 233 | Ok(Self { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 234 | id, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 235 | payload: ResponsePayload::Ok { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 236 | result: serde_json::to_value(value)?, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 237 | }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 238 | }) |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 239 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 240 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 241 | #[must_use] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 242 | pub fn err(id: u64, error: ProtoError) -> Self { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 243 | Self { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 244 | id, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 245 | payload: ResponsePayload::Err { error }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 246 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 247 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 248 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 249 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 250 | /// body of a `Response`. on the wire: a single `result` or `error` field inlined alongside `id`. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 251 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 252 | #[serde(untagged)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 253 | pub enum ResponsePayload { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 254 | Ok { result: Value }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 255 | Err { error: ProtoError }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 256 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 257 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 258 | /// server-to-client event on a subscribed topic. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 259 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 260 | pub struct Event { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 261 | /// e.g. `tick`, `changed`. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 262 | pub event: String, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 263 | pub topic: Topic, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 264 | /// shape depends on `topic` and `event`. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 265 | pub data: Value, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 266 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 267 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 268 | impl Event { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 269 | /// # Errors |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 270 | /// serde error if `data` fails to serialize. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 271 | pub fn new<T: Serialize>( |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 272 | topic: Topic, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 273 | event: impl Into<String>, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 274 | data: &T, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 275 | ) -> Result<Self, serde_json::Error> { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 276 | Ok(Self { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 277 | event: event.into(), |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 278 | topic, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 279 | data: serde_json::to_value(data)?, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 280 | }) |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 281 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 282 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 283 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 284 | /// one frame as received from the server: paired response or fan-out event. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 285 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 286 | #[serde(untagged)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 287 | pub enum ServerFrame { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 288 | Response(Response), |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 289 | Event(Event), |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 290 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 291 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 292 | /// result body of `status`. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 293 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 294 | pub struct Status { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 295 | /// semver. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 296 | pub version: String, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 297 | pub protocol: u32, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 298 | pub uptime_s: u64, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 299 | pub profile: String, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 300 | pub bypass: bool, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 301 | /// Layer A master switch (per-app level control, global). |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 302 | #[serde(default)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 303 | pub per_app: bool, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 304 | pub sinks: Sinks, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 305 | pub streams: Vec<StreamRoute>, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 306 | /// per-app (Layer A) controller state; empty when not managing anything. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 307 | #[serde(default, skip_serializing_if = "Vec::is_empty")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 308 | pub layer_a: Vec<LayerASnapshot>, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 309 | /// non-fatal warnings (typically profile-load: TOML parse errors, active profile missing on disk). |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 310 | /// reflects the last successful load/reload; empty when healthy. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 311 | #[serde(default, skip_serializing_if = "Vec::is_empty")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 312 | pub warnings: Vec<String>, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 313 | /// user-overlay setting overrides (dotted key → value). these shadow the active profile and |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 314 | /// persist across `profile.use`, so a stale `headroom set` can silently override it |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 315 | /// (e.g. `agc.enabled=false`). surfaced so clients can flag it. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 316 | #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 317 | pub setting_overrides: BTreeMap<String, Value>, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 318 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 319 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 320 | #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 321 | pub struct Sinks { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 322 | /// the processed virtual sink; the only sink headroom creates. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 323 | pub processed: SinkInfo, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 324 | /// hardware sink headroom forwards to, and where bypassed streams go directly. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 325 | pub real: SinkInfo, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 326 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 327 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 328 | #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 329 | pub struct SinkInfo { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 330 | #[serde(default, skip_serializing_if = "Option::is_none")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 331 | pub node_id: Option<u32>, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 332 | #[serde(default, skip_serializing_if = "Option::is_none")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 333 | pub name: Option<String>, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 334 | /// linked and accepting audio. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 335 | #[serde(default)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 336 | pub ready: bool, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 337 | /// native rate (Hz). filter matches the *real* sink's rate to skip the output-edge resample; |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 338 | /// the processed sink advertises whatever rate the filter currently runs at. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 339 | #[serde(default, skip_serializing_if = "Option::is_none")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 340 | pub sample_rate: Option<u32>, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 341 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 342 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 343 | /// one playback stream and its route. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 344 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 345 | pub struct StreamRoute { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 346 | pub node_id: u32, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 347 | /// typically `application.process.binary`. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 348 | pub app: String, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 349 | pub route: Route, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 350 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 351 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 352 | /// per-app (Layer A) controller state for one managed stream; on `status` and `per-app.list`. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 353 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 354 | pub struct LayerASnapshot { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 355 | pub node_id: u32, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 356 | pub app: String, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 357 | /// true while a tap + controller is actively managing the stream. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 358 | pub managed: bool, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 359 | /// last linear volume written (1.0 = unity). |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 360 | pub volume_lin: f32, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 361 | /// asserted gain reduction in dB (`>= 0`; `0` = no cut). |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 362 | pub reduction_db: f32, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 363 | /// user-set ceiling (linear) when ceiling-mode deference is active. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 364 | #[serde(default, skip_serializing_if = "Option::is_none")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 365 | pub user_ceiling_lin: Option<f32>, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 366 | /// strict-mode deference has locked the controller until `per-app.reset`. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 367 | pub deferred: bool, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 368 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 369 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 370 | /// entry in `profile.list`. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 371 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 372 | pub struct ProfileInfo { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 373 | pub name: String, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 374 | pub active: bool, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 375 | pub description: String, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 376 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 377 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 378 | /// result body of `route.list`. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 379 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 380 | pub struct RouteList { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 381 | /// in evaluation order. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 382 | pub rules: Vec<RouteRule>, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 383 | pub current: Vec<StreamRoute>, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 384 | /// fallback when no rule matches. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 385 | pub default_route: Route, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 386 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 387 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 388 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 389 | pub struct RouteRule { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 390 | #[serde(rename = "match")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 391 | pub match_: RouteRuleMatch, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 392 | pub route: Route, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 393 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 394 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 395 | /// present fields are AND'd; values within a field are OR'd; empty match is true. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 396 | #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 397 | pub struct RouteRuleMatch { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 398 | #[serde(default, skip_serializing_if = "Vec::is_empty")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 399 | pub process_binary: Vec<String>, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 400 | #[serde(default, skip_serializing_if = "Vec::is_empty")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 401 | pub application_name: Vec<String>, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 402 | /// `pipewire.access.portal.app_id` (Flatpak). |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 403 | #[serde(default, skip_serializing_if = "Vec::is_empty")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 404 | pub portal_app_id: Vec<String>, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 405 | #[serde(default, skip_serializing_if = "Vec::is_empty")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 406 | pub media_role: Vec<String>, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 407 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 408 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 409 | /// `hello` payload (sent on connect). |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 410 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 411 | pub struct HelloData { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 412 | /// always `"headroom"`. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 413 | pub daemon: String, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 414 | /// semver. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 415 | pub version: String, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 416 | pub protocol: u32, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 417 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 418 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 419 | /// `meters` tick payload. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 420 | #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 421 | pub struct MeterTick { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 422 | /// BS.1770 M, 400 ms, LUFS. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 423 | pub momentary_lufs: f32, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 424 | /// BS.1770 S, 3 s, LUFS. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 425 | pub shortterm_lufs: f32, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 426 | /// BS.1770 I, gated, LUFS. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 427 | pub integrated_lufs: f32, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 428 | /// max true peak across channels, dBTP. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 429 | pub true_peak_dbtp: f32, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 430 | /// compressor + limiter, dB. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 431 | pub gain_reduction_db: f32, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 432 | pub compressor_gr_db: f32, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 433 | pub limiter_gr_db: f32, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 434 | /// AGC contribution (positive = boost), dB. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 435 | pub agc_gain_db: f32, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 436 | /// when `false`, AGC is bypassed and `agc_gain_db` is inert (0) — render as "disabled", |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 437 | /// not an active 0 dB. absent ⇒ `true`. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 438 | #[serde(default = "default_true")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 439 | pub agc_enabled: bool, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 440 | /// same as [`Self::agc_enabled`] for `compressor_gr_db`. (limiter has no flag — always-on backstop.) |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 441 | #[serde(default = "default_true")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 442 | pub compressor_enabled: bool, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 443 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 444 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 445 | /// absent ⇒ enabled, matching the always-on assumption before these fields existed. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 446 | fn default_true() -> bool { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 447 | true |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 448 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 449 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 450 | /// `profile` topic events. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 451 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 452 | #[serde(tag = "event", rename_all = "snake_case")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 453 | #[non_exhaustive] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 454 | pub enum ProfileEvent { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 455 | Changed { name: String, previous: String }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 456 | /// `changed`: profiles whose definitions changed on disk. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 457 | Reloaded { changed: Vec<String> }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 458 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 459 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 460 | /// `routing` topic events. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 461 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 462 | #[serde(tag = "event", rename_all = "snake_case")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 463 | #[non_exhaustive] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 464 | pub enum RoutingEvent { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 465 | StreamRouted { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 466 | node_id: u32, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 467 | app: String, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 468 | to: Route, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 469 | }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 470 | /// stream's PipeWire node disappeared; drop state keyed by `node_id`. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 471 | StreamRemoved { node_id: u32 }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 472 | /// Layer A tap attached; daemon now manages `Props.channelVolumes` and publishes `meters/layer_a_level`. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 473 | LayerAAttached { node_id: u32, app: String }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 474 | /// Layer A tap torn down (usually stream gone); drop Layer A state for `node_id`. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 475 | LayerADetached { node_id: u32 }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 476 | RuleChanged, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 477 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 478 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 479 | /// `meters/layer_a_level` payload — emitted when the Layer A controller writes a new `channelVolumes`. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 480 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 481 | pub struct LayerALevel { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 482 | pub node_id: u32, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 483 | pub app: String, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 484 | /// linear volume written (1.0 = unity). |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 485 | pub volume_lin: f32, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 486 | /// asserted gain reduction, dB; ≤ 0 when reducing. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 487 | pub reduction_db: f32, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 488 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 489 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 490 | /// `daemon` topic events. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 491 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 492 | #[serde(tag = "event", rename_all = "snake_case")] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 493 | #[non_exhaustive] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 494 | pub enum DaemonEvent { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 495 | Started { version: String }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 496 | Shutdown, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 497 | /// events dropped on this connection. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 498 | Overflow { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 499 | lost_topic: Topic, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 500 | /// lost in this batch. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 501 | lost: u32, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 502 | /// total lost on this connection. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 503 | total_lost: u64, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 504 | }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 505 | /// non-fatal error. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 506 | Error { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 507 | /// matches `ErrorCode` when applicable. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 508 | code: String, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 509 | message: String, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 510 | }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 511 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 512 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 513 | #[cfg(test)] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 514 | mod tests { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 515 | use super::*; |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 516 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 517 | fn roundtrip<T>(v: &T) -> T |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 518 | where |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 519 | T: Serialize + for<'de> Deserialize<'de>, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 520 | { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 521 | let s = serde_json::to_string(v).expect("serialize"); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 522 | serde_json::from_str(&s).expect("deserialize") |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 523 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 524 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 525 | #[test] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 526 | fn op_status_serializes_without_args() { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 527 | let req = Request::new(1, Op::Status); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 528 | let s = serde_json::to_string(&req).unwrap(); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 529 | // Must be the flat form, no `kind` wrapper, no `args` field. |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 530 | assert_eq!(s, r#"{"id":1,"op":"status"}"#); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 531 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 532 | let back: Request = serde_json::from_str(&s).unwrap(); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 533 | assert_eq!(back, req); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 534 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 535 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 536 | #[test] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 537 | fn op_profile_use_round_trips_flat() { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 538 | let req = Request::new( |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 539 | 7, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 540 | Op::ProfileUse { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 541 | name: "night".into(), |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 542 | }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 543 | ); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 544 | let s = serde_json::to_string(&req).unwrap(); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 545 | assert_eq!(s, r#"{"id":7,"op":"profile.use","args":{"name":"night"}}"#); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 546 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 547 | let back: Request = serde_json::from_str(&s).unwrap(); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 548 | assert_eq!(back, req); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 549 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 550 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 551 | #[test] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 552 | fn route_set_serializes_canonical() { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 553 | let req = Request::new( |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 554 | 12, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 555 | Op::RouteSet { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 556 | app: "firefox".into(), |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 557 | to: Route::Processed, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 558 | }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 559 | ); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 560 | let s = serde_json::to_string(&req).unwrap(); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 561 | assert_eq!( |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 562 | s, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 563 | r#"{"id":12,"op":"route.set","args":{"app":"firefox","to":"processed"}}"# |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 564 | ); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 565 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 566 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 567 | #[test] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 568 | fn response_ok_shape() { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 569 | let resp = Response::ok(3, &serde_json::json!({ "name": "default" })).unwrap(); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 570 | let s = serde_json::to_string(&resp).unwrap(); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 571 | assert_eq!(s, r#"{"id":3,"result":{"name":"default"}}"#); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 572 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 573 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 574 | #[test] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 575 | fn response_err_shape() { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 576 | let resp = Response::err(4, ProtoError::new(crate::ErrorCode::NotFound, "missing")); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 577 | let s = serde_json::to_string(&resp).unwrap(); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 578 | assert_eq!( |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 579 | s, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 580 | r#"{"id":4,"error":{"code":"NOT_FOUND","message":"missing"}}"# |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 581 | ); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 582 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 583 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 584 | #[test] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 585 | fn server_frame_distinguishes_response_from_event() { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 586 | let resp = Response::ok(1, &serde_json::json!(null)).unwrap(); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 587 | let s = serde_json::to_string(&resp).unwrap(); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 588 | let frame: ServerFrame = serde_json::from_str(&s).unwrap(); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 589 | assert!(matches!(frame, ServerFrame::Response(_))); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 590 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 591 | let ev = Event::new(Topic::Meters, "tick", &serde_json::json!({})).unwrap(); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 592 | let s = serde_json::to_string(&ev).unwrap(); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 593 | let frame: ServerFrame = serde_json::from_str(&s).unwrap(); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 594 | assert!(matches!(frame, ServerFrame::Event(_))); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 595 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 596 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 597 | #[test] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 598 | fn meter_tick_roundtrip() { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 599 | let m = MeterTick { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 600 | momentary_lufs: -19.3, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 601 | shortterm_lufs: -20.1, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 602 | integrated_lufs: -19.8, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 603 | true_peak_dbtp: -1.4, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 604 | gain_reduction_db: -2.1, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 605 | compressor_gr_db: -0.8, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 606 | limiter_gr_db: -1.3, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 607 | agc_gain_db: 0.5, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 608 | agc_enabled: true, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 609 | compressor_enabled: true, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 610 | }; |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 611 | let back = roundtrip(&m); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 612 | assert_eq!(back, m); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 613 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 614 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 615 | #[test] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 616 | fn topic_string_canonical() { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 617 | assert_eq!(Topic::Meters.as_str(), "meters"); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 618 | assert_eq!(serde_json::to_string(&Topic::Meters).unwrap(), "\"meters\""); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 619 | let t: Topic = serde_json::from_str("\"profile\"").unwrap(); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 620 | assert_eq!(t, Topic::Profile); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 621 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 622 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 623 | #[test] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 624 | fn route_string_canonical() { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 625 | let r: Route = serde_json::from_str("\"bypass\"").unwrap(); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 626 | assert_eq!(r, Route::Bypass); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 627 | assert_eq!(serde_json::to_string(&r).unwrap(), "\"bypass\""); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 628 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 629 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 630 | #[test] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 631 | fn error_code_screaming_snake() { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 632 | let s = serde_json::to_string(&crate::ErrorCode::InvalidFrame).unwrap(); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 633 | assert_eq!(s, "\"INVALID_FRAME\""); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 634 | let c: crate::ErrorCode = serde_json::from_str("\"UNKNOWN_OP\"").unwrap(); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 635 | assert_eq!(c, crate::ErrorCode::UnknownOp); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 636 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 637 | |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 638 | #[test] |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 639 | fn subscribe_op_roundtrip() { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 640 | let req = Request::new( |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 641 | 5, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 642 | Op::Subscribe { |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 643 | topics: vec![Topic::Meters, Topic::Profile], |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 644 | }, |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 645 | ); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 646 | let back: Request = serde_json::from_str(&serde_json::to_string(&req).unwrap()).unwrap(); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 647 | assert_eq!(back, req); |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 648 | } |
dd6b8e786e1f | atagen | 2026-05-27 15:49 UTC | 649 | } |