djio/output/
mod.rs

1// SPDX-FileCopyrightText: The djio authors
2// SPDX-License-Identifier: MPL-2.0
3
4//! Sending control data to actuators like LEDs and motorized platters
5//! or for configuring devices.
6
7use std::{
8    ops::{Deref, DerefMut},
9    time::Duration,
10};
11
12use derive_more::{Display, Error};
13use futures_core::Stream;
14use futures_util::{StreamExt as _, stream};
15use smol_str::SmolStr;
16use strum::FromRepr;
17
18use crate::{Control, ControlValue};
19
20#[cfg(feature = "blinking-led-task")]
21mod blinking_led_task;
22#[cfg(feature = "blinking-led-task")]
23pub use blinking_led_task::blinking_led_task;
24#[cfg(feature = "blinking-led-task-tokio-rt")]
25pub use blinking_led_task::spawn_blinking_led_task;
26
27#[derive(Debug, Display, Error)]
28pub enum OutputError {
29    #[display("disconnected")]
30    Disconnected,
31    #[display("send: {msg}")]
32    Send { msg: SmolStr },
33}
34
35pub type OutputResult<T> = std::result::Result<T, OutputError>;
36
37/// Simple LED
38#[derive(Debug, Clone, Copy, PartialEq, Eq, FromRepr)]
39#[repr(u8)]
40pub enum LedOutput {
41    Off = 0,
42    On = 1,
43}
44
45impl From<LedOutput> for ControlValue {
46    fn from(value: LedOutput) -> Self {
47        Self::from_bits(value as _)
48    }
49}
50
51impl From<ControlValue> for LedOutput {
52    fn from(value: ControlValue) -> Self {
53        match value.to_bits() {
54            0 => Self::Off,
55            _ => Self::On,
56        }
57    }
58}
59
60/// Dimmable LED
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62#[repr(transparent)]
63pub struct DimLedOutput {
64    pub brightness: u8,
65}
66
67impl From<DimLedOutput> for ControlValue {
68    fn from(value: DimLedOutput) -> Self {
69        let DimLedOutput { brightness } = value;
70        Self::from_bits(u32::from(brightness))
71    }
72}
73
74impl From<ControlValue> for DimLedOutput {
75    fn from(value: ControlValue) -> Self {
76        let brightness = (value.to_bits() & 0xff) as u8;
77        Self { brightness }
78    }
79}
80
81/// RGB LED
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub struct RgbLedOutput {
84    pub red: u8,
85    pub green: u8,
86    pub blue: u8,
87}
88
89impl From<RgbLedOutput> for ControlValue {
90    fn from(value: RgbLedOutput) -> Self {
91        let RgbLedOutput { red, green, blue } = value;
92        Self::from_bits((u32::from(red) << 16) | (u32::from(green) << 8) | u32::from(blue))
93    }
94}
95
96impl From<ControlValue> for RgbLedOutput {
97    fn from(value: ControlValue) -> Self {
98        let red = ((value.to_bits() >> 16) & 0xff) as u8;
99        let green = ((value.to_bits() >> 8) & 0xff) as u8;
100        let blue = (value.to_bits() & 0xff) as u8;
101        Self { red, green, blue }
102    }
103}
104
105/// First error after sending multiple outputs
106#[derive(Debug)]
107pub struct SendOutputsError {
108    /// The number of outputs that have been sent successfully before an error occurred.
109    ///
110    /// This could only be set if the outputs are sent subsequently and in order.
111    /// If `None` then it is unknown which outputs have arrived at their destination
112    /// despite the error.
113    pub sent_ok: Option<usize>,
114
115    /// The actual error that occurred.
116    pub err: OutputError,
117}
118
119pub trait ControlOutputGateway {
120    /// Send a single output
121    fn send_output(&mut self, output: &Control) -> OutputResult<()>;
122
123    /// Send multiple outputs
124    ///
125    /// The default implementation sends single outputs subsequently in order.
126    fn send_outputs(&mut self, outputs: &[Control]) -> Result<(), SendOutputsError> {
127        let mut sent_ok = 0;
128        for output in outputs {
129            match self.send_output(output) {
130                Ok(()) => {
131                    sent_ok += 1;
132                }
133                Err(err) => {
134                    return Err(SendOutputsError {
135                        sent_ok: Some(sent_ok),
136                        err,
137                    });
138                }
139            }
140        }
141        debug_assert_eq!(sent_ok, outputs.len());
142        Ok(())
143    }
144}
145
146impl<T> ControlOutputGateway for T
147where
148    T: DerefMut + ?Sized,
149    <T as Deref>::Target: ControlOutputGateway,
150{
151    fn send_output(&mut self, output: &Control) -> OutputResult<()> {
152        self.deref_mut().send_output(output)
153    }
154
155    fn send_outputs(&mut self, outputs: &[Control]) -> Result<(), SendOutputsError> {
156        self.deref_mut().send_outputs(outputs)
157    }
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum LedState {
162    Off,
163    BlinkFast,
164    BlinkSlow,
165    On,
166}
167
168impl LedState {
169    #[must_use]
170    pub const fn is_blinking(self) -> bool {
171        match self {
172            Self::BlinkFast | Self::BlinkSlow => true,
173            Self::Off | Self::On => false,
174        }
175    }
176
177    /// Initial LED output
178    ///
179    /// Blinking should always start by turning on the LED before
180    /// the periodic switching takes over.
181    #[must_use]
182    pub const fn initial_output(self) -> LedOutput {
183        self.output(BlinkingLedOutput::ON)
184    }
185
186    /// LED output depending on the current blinking state
187    #[must_use]
188    pub const fn output(self, blinking_led_output: BlinkingLedOutput) -> LedOutput {
189        match self {
190            Self::Off => LedOutput::Off,
191            Self::BlinkFast => blinking_led_output.fast(),
192            Self::BlinkSlow => blinking_led_output.slow(),
193            Self::On => LedOutput::On,
194        }
195    }
196}
197
198pub const DEFAULT_BLINKING_LED_PERIOD: Duration = Duration::from_millis(250);
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub struct BlinkingLedOutput(u8);
202
203impl BlinkingLedOutput {
204    pub const ON: Self = Self(0b11);
205
206    #[must_use]
207    pub const fn fast(self) -> LedOutput {
208        match self.0 & 0b01 {
209            0b00 => LedOutput::Off,
210            0b01 => LedOutput::On,
211            _ => unreachable!(),
212        }
213    }
214
215    #[must_use]
216    pub const fn slow(self) -> LedOutput {
217        match self.0 & 0b10 {
218            0b00 => LedOutput::Off,
219            0b10 => LedOutput::On,
220            _ => unreachable!(),
221        }
222    }
223}
224
225#[derive(Debug, Default)]
226pub struct BlinkingLedTicker(usize);
227
228impl BlinkingLedTicker {
229    const fn output_from_value(value: usize) -> BlinkingLedOutput {
230        #[expect(clippy::cast_possible_truncation)]
231        BlinkingLedOutput(!value as u8 & 0b11)
232    }
233
234    #[must_use]
235    pub fn tick(&mut self) -> BlinkingLedOutput {
236        let value = self.0;
237        self.0 = self.0.wrapping_add(1);
238        Self::output_from_value(value)
239    }
240
241    #[must_use]
242    pub const fn output(&self) -> BlinkingLedOutput {
243        Self::output_from_value(self.0)
244    }
245
246    pub fn map_into_output_stream(
247        self,
248        periodic: impl Stream<Item = ()> + 'static,
249    ) -> impl Stream<Item = BlinkingLedOutput> {
250        stream::unfold(
251            (self, Box::pin(periodic)),
252            |(mut ticker, mut periodic)| async move {
253                periodic.next().await.map(|()| {
254                    let output = ticker.tick();
255                    (output, (ticker, periodic))
256                })
257            },
258        )
259    }
260}
261
262/// Virtual LED
263///
264/// For displaying virtual LEDs or illuminated buttons in the UI.
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub struct VirtualLed {
267    pub state: LedState,
268    pub output: LedOutput,
269}
270
271impl VirtualLed {
272    pub const OFF: Self = Self::initial_state(LedState::Off);
273
274    /// Create a new virtual LED
275    #[must_use]
276    pub const fn initial_state(state: LedState) -> Self {
277        let output = state.initial_output();
278        Self { state, output }
279    }
280
281    /// Update the state
282    ///
283    /// The output is initialized accordingly to reflect the new state.
284    ///
285    /// Returns `true` if the state has changed.
286    pub fn update_state(&mut self, state: LedState) -> bool {
287        if self.state == state {
288            // Unchanged
289            return false;
290        }
291        *self = Self::initial_state(state);
292        true
293    }
294
295    /// Update the blinking output
296    ///
297    /// The output is updated accordingly while the state remains unchanged.
298    pub fn update_blinking_output(&mut self, blinking_led_output: BlinkingLedOutput) {
299        let Self { state, output } = self;
300        *output = state.output(blinking_led_output);
301    }
302}
303
304impl Default for VirtualLed {
305    fn default() -> Self {
306        Self::OFF
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use crate::{BlinkingLedOutput, BlinkingLedTicker, LedOutput};
313
314    #[test]
315    fn blinking_led_output_on() {
316        assert_eq!(LedOutput::On, BlinkingLedOutput::ON.fast());
317        assert_eq!(LedOutput::On, BlinkingLedOutput::ON.slow());
318    }
319
320    #[test]
321    fn blinking_led_ticker_initial_output_is_on() {
322        assert_eq!(BlinkingLedOutput::ON, BlinkingLedTicker::default().output());
323    }
324}