1use core::fmt;
9
10use heapless::{String, Vec, index_map::FnvIndexMap};
11use serde::{Deserialize, Serialize};
12
13const MAX_STR_LEN: usize = 32;
15
16const MAX_DMX_PORTS: usize = 4;
18
19const MAX_BUTTON_ACTIONS: usize = 4;
21
22const MAX_CONFIG_BYTES: usize = 512;
24
25#[derive(Debug)]
27pub struct InvalidValue(pub u8);
28
29impl fmt::Display for InvalidValue {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 write!(f, "invalid enum value {}", self.0)
33 }
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(try_from = "u8", into = "u8")]
39pub enum ConnectionType {
40 WifiAp = 0,
42 WifiSta = 1,
44 Ethernet = 2,
46}
47
48impl Default for ConnectionType {
49 fn default() -> Self {
51 Self::WifiAp
52 }
53}
54
55impl From<ConnectionType> for u8 {
56 fn from(value: ConnectionType) -> Self {
58 match value {
59 ConnectionType::WifiAp => 0,
60 ConnectionType::WifiSta => 1,
61 ConnectionType::Ethernet => 2,
62 }
63 }
64}
65
66impl TryFrom<u8> for ConnectionType {
67 type Error = InvalidValue;
69
70 fn try_from(value: u8) -> Result<Self, Self::Error> {
72 match value {
73 0 => Ok(Self::WifiAp),
74 1 => Ok(Self::WifiSta),
75 2 => Ok(Self::Ethernet),
76 other => Err(InvalidValue(other)),
77 }
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
83#[serde(try_from = "u8", into = "u8")]
84pub enum IpMethod {
85 #[default]
87 Dhcp = 0,
88 Static = 1,
90}
91
92impl From<IpMethod> for u8 {
93 fn from(value: IpMethod) -> Self {
95 match value {
96 IpMethod::Dhcp => 0,
97 IpMethod::Static => 1,
98 }
99 }
100}
101
102impl TryFrom<u8> for IpMethod {
103 type Error = InvalidValue;
105
106 fn try_from(value: u8) -> Result<Self, Self::Error> {
108 match value {
109 0 => Ok(Self::Dhcp),
110 1 => Ok(Self::Static),
111 other => Err(InvalidValue(other)),
112 }
113 }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
118#[serde(try_from = "u8", into = "u8")]
119pub enum ButtonAction {
120 #[default]
122 None = 0,
123 ToggleLed = 1,
125 Reboot = 2,
127}
128
129impl From<ButtonAction> for u8 {
130 fn from(value: ButtonAction) -> Self {
132 match value {
133 ButtonAction::None => 0,
134 ButtonAction::ToggleLed => 1,
135 ButtonAction::Reboot => 2,
136 }
137 }
138}
139
140impl TryFrom<u8> for ButtonAction {
141 type Error = InvalidValue;
143
144 fn try_from(value: u8) -> Result<Self, Self::Error> {
146 match value {
147 0 => Ok(Self::None),
148 1 => Ok(Self::ToggleLed),
149 2 => Ok(Self::Reboot),
150 other => Err(InvalidValue(other)),
151 }
152 }
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
157#[serde(try_from = "u8", into = "u8")]
158pub enum DmxDirection {
159 Input = 0,
161 Output = 1,
163}
164
165impl Default for DmxDirection {
166 fn default() -> Self {
168 Self::Output
169 }
170}
171
172impl From<DmxDirection> for u8 {
173 fn from(value: DmxDirection) -> Self {
175 match value {
176 DmxDirection::Input => 0,
177 DmxDirection::Output => 1,
178 }
179 }
180}
181
182impl TryFrom<u8> for DmxDirection {
183 type Error = InvalidValue;
185
186 fn try_from(value: u8) -> Result<Self, Self::Error> {
188 match value {
189 0 => Ok(Self::Input),
190 1 => Ok(Self::Output),
191 other => Err(InvalidValue(other)),
192 }
193 }
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
198pub struct DmxPortConfig {
199 pub direction: DmxDirection,
201 pub universe: u16,
203}
204
205impl Default for DmxPortConfig {
206 fn default() -> Self {
208 Self {
209 direction: DmxDirection::Output,
210 universe: 1,
211 }
212 }
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct WifiConfig {
218 pub ssid: String<MAX_STR_LEN>,
220 pub password: String<MAX_STR_LEN>,
222}
223
224impl Default for WifiConfig {
225 fn default() -> Self {
227 Self {
228 ssid: String::new(),
229 password: String::new(),
230 }
231 }
232}
233
234mod index_map {
241 use core::fmt::{self, Write as _};
242 use core::marker::PhantomData;
243
244 use heapless::{String, index_map::FnvIndexMap};
245 use serde::de::{self, MapAccess, Visitor};
246 use serde::ser::{SerializeMap, Serializer};
247 use serde::{Deserialize, Deserializer, Serialize};
248
249 pub fn serialize<S: Serializer, T: Serialize, const N: usize>(
251 map: &FnvIndexMap<u8, T, N>,
252 serializer: S,
253 ) -> Result<S::Ok, S::Error> {
254 struct Key(u8);
256
257 impl Serialize for Key {
258 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
260 let mut text = String::<4>::new();
261 write!(&mut text, "{}", self.0).map_err(serde::ser::Error::custom)?;
262 serializer.serialize_str(text.as_str())
263 }
264 }
265
266 let mut out = serializer.serialize_map(Some(map.len()))?;
267 for (key, value) in map {
268 out.serialize_entry(&Key(*key), value)?;
269 }
270 out.end()
271 }
272
273 pub fn deserialize<'de, D: Deserializer<'de>, T: Deserialize<'de>, const N: usize>(
275 deserializer: D,
276 ) -> Result<FnvIndexMap<u8, T, N>, D::Error> {
277 struct MapVisitor<T, const N: usize>(PhantomData<T>);
279
280 impl<'de, T: Deserialize<'de>, const N: usize> Visitor<'de> for MapVisitor<T, N> {
281 type Value = FnvIndexMap<u8, T, N>;
283
284 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
286 f.write_str("a map with decimal string keys")
287 }
288
289 fn visit_map<A: MapAccess<'de>>(self, mut access: A) -> Result<Self::Value, A::Error> {
291 let mut map = FnvIndexMap::new();
292 while let Some((key, value)) = access.next_entry::<String<8>, T>()? {
293 let index: u8 = key.parse().map_err(de::Error::custom)?;
294 map.insert(index, value)
295 .map_err(|_| de::Error::custom("config map full"))?;
296 }
297 Ok(map)
298 }
299 }
300
301 deserializer.deserialize_map(MapVisitor(PhantomData))
302 }
303}
304
305#[allow(clippy::struct_field_names)]
310#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct Config {
312 pub version: u8,
314 pub connection: ConnectionType,
316 pub ip_method: IpMethod,
318 pub led_brightness: u8,
320 pub station_config: WifiConfig,
322 pub ap_config: WifiConfig,
324 #[serde(with = "index_map")]
326 pub dmx_ports: FnvIndexMap<u8, DmxPortConfig, MAX_DMX_PORTS>,
327 #[serde(with = "index_map")]
329 pub button_actions: FnvIndexMap<u8, ButtonAction, MAX_BUTTON_ACTIONS>,
330}
331
332impl Default for Config {
333 fn default() -> Self {
337 let ap_config = WifiConfig {
338 password: String::try_from("ChaosDMX").expect("default password fits"),
339 ..WifiConfig::default()
340 };
341 let mut dmx_ports = FnvIndexMap::new();
342 let _ = dmx_ports.insert(0, DmxPortConfig::default());
343 let _ = dmx_ports.insert(
344 1,
345 DmxPortConfig {
346 direction: DmxDirection::Input,
347 universe: 2,
348 },
349 );
350 let mut button_actions = FnvIndexMap::new();
355 for (key, action) in [
356 (0, ButtonAction::ToggleLed),
357 (1, ButtonAction::None),
358 (2, ButtonAction::Reboot),
359 (3, ButtonAction::None),
360 ] {
361 let _ = button_actions.insert(key, action);
362 }
363 Self {
364 version: 1,
365 connection: ConnectionType::default(),
366 ip_method: IpMethod::default(),
367 led_brightness: 25,
368 station_config: WifiConfig::default(),
369 ap_config,
370 dmx_ports,
371 button_actions,
372 }
373 }
374}
375
376impl Config {
377 pub fn to_bytes(&self) -> Result<Vec<u8, MAX_CONFIG_BYTES>, postcard::Error> {
381 let mut buf = [0u8; MAX_CONFIG_BYTES];
382 let used = postcard::to_slice(self, &mut buf)?;
383 Vec::from_slice(used).map_err(|_| postcard::Error::SerializeBufferFull)
384 }
385
386 pub fn from_bytes(data: &[u8]) -> Result<Self, postcard::Error> {
388 postcard::from_bytes(data)
389 }
390
391 pub fn apply(&self) {
398 crate::hardware::led::set_brightness(self.led_brightness);
399 }
400
401 pub fn merge_patch(&mut self, patch: ConfigPatch) -> bool {
414 if patch
415 .dmx_ports
416 .keys()
417 .any(|&key| usize::from(key) >= MAX_DMX_PORTS)
418 || patch
419 .button_actions
420 .keys()
421 .any(|&key| usize::from(key) >= MAX_BUTTON_ACTIONS)
422 {
423 return false;
424 }
425 if let Some(version) = patch.version {
426 self.version = version;
427 }
428 if let Some(connection) = patch.connection {
429 self.connection = connection;
430 }
431 if let Some(ip_method) = patch.ip_method {
432 self.ip_method = ip_method;
433 }
434 if let Some(led_brightness) = patch.led_brightness {
435 self.led_brightness = led_brightness;
436 }
437 if let Some(station_config) = patch.station_config {
438 station_config.apply(&mut self.station_config);
439 }
440 if let Some(ap_config) = patch.ap_config {
441 ap_config.apply(&mut self.ap_config);
442 }
443 for (key, port_patch) in patch.dmx_ports {
444 let mut port = self.dmx_ports.get(&key).copied().unwrap_or_default();
445 port_patch.apply(&mut port);
446 if self.dmx_ports.insert(key, port).is_err() {
447 return false;
448 }
449 }
450 for (key, action) in patch.button_actions {
451 if self.button_actions.insert(key, action).is_err() {
452 return false;
453 }
454 }
455 true
456 }
457}
458
459#[derive(Debug, Default, Deserialize)]
464pub struct WifiConfigPatch {
465 pub ssid: Option<String<MAX_STR_LEN>>,
467 pub password: Option<String<MAX_STR_LEN>>,
469}
470
471impl WifiConfigPatch {
472 fn apply(self, target: &mut WifiConfig) {
474 if let Some(ssid) = self.ssid {
475 target.ssid = ssid;
476 }
477 if let Some(password) = self.password {
478 target.password = password;
479 }
480 }
481}
482
483#[derive(Debug, Default, Deserialize)]
485pub struct DmxPortPatch {
486 pub direction: Option<DmxDirection>,
488 pub universe: Option<u16>,
490}
491
492impl DmxPortPatch {
493 fn apply(self, target: &mut DmxPortConfig) {
495 if let Some(direction) = self.direction {
496 target.direction = direction;
497 }
498 if let Some(universe) = self.universe {
499 target.universe = universe;
500 }
501 }
502}
503
504#[derive(Debug, Default, Deserialize)]
510pub struct ConfigPatch {
511 pub version: Option<u8>,
513 pub connection: Option<ConnectionType>,
515 pub ip_method: Option<IpMethod>,
517 pub led_brightness: Option<u8>,
519 pub station_config: Option<WifiConfigPatch>,
521 pub ap_config: Option<WifiConfigPatch>,
523 #[serde(default, with = "index_map")]
525 pub dmx_ports: FnvIndexMap<u8, DmxPortPatch, MAX_DMX_PORTS>,
526 #[serde(default, with = "index_map")]
528 pub button_actions: FnvIndexMap<u8, ButtonAction, MAX_BUTTON_ACTIONS>,
529}
530
531#[cfg(test)]
533mod tests {
534 use super::*;
535
536 #[test]
538 fn config_roundtrip() {
539 let config = Config::default();
540 let bytes = config.to_bytes().unwrap();
541 let restored = Config::from_bytes(&bytes).unwrap();
542 assert_eq!(config.connection, restored.connection);
543 assert_eq!(config.led_brightness, restored.led_brightness);
544 assert_eq!(
545 config.dmx_ports.get(&0).map(|port| port.universe),
546 restored.dmx_ports.get(&0).map(|port| port.universe)
547 );
548 }
549}