hyperion/effects/
instance.rs

1use std::time::{Duration, Instant};
2
3use async_trait::async_trait;
4use thiserror::Error;
5use tokio::sync::{
6    mpsc::{Receiver, Sender},
7    Mutex,
8};
9
10use crate::{
11    image::{RawImage, RawImageError},
12    models::Color,
13};
14
15use super::EffectMessageKind;
16
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub enum ControlMessage {
19    Abort,
20}
21
22struct InstanceMethodsData {
23    crx: Receiver<ControlMessage>,
24    aborted: bool,
25}
26
27pub struct InstanceMethods {
28    tx: Sender<EffectMessageKind>,
29    led_count: usize,
30    deadline: Option<Instant>,
31    data: Mutex<InstanceMethodsData>,
32}
33
34impl InstanceMethods {
35    pub fn new(
36        tx: Sender<EffectMessageKind>,
37        crx: Receiver<ControlMessage>,
38        led_count: usize,
39        duration: Option<Duration>,
40    ) -> Self {
41        Self {
42            tx,
43            led_count,
44            deadline: duration.map(|d| Instant::now() + d),
45            data: Mutex::new(InstanceMethodsData {
46                crx,
47                aborted: false,
48            }),
49        }
50    }
51
52    fn completed(&self, data: &InstanceMethodsData) -> bool {
53        data.aborted || self.deadline.map(|d| Instant::now() > d).unwrap_or(false)
54    }
55
56    /// Returns true if the should abort
57    async fn poll_control(&self) -> Result<(), RuntimeMethodError> {
58        let mut data = self.data.lock().await;
59        match data.crx.try_recv() {
60            Ok(m) => match m {
61                ControlMessage::Abort => {
62                    data.aborted = true;
63                    return Err(RuntimeMethodError::EffectAborted);
64                }
65            },
66            Err(err) => {
67                match err {
68                    tokio::sync::mpsc::error::TryRecvError::Empty => {
69                        // No control messages pending
70                    }
71                    tokio::sync::mpsc::error::TryRecvError::Disconnected => {
72                        // We were disconnected
73                        data.aborted = true;
74                        return Err(RuntimeMethodError::EffectAborted);
75                    }
76                }
77            }
78        }
79
80        if self.completed(&data) {
81            Err(RuntimeMethodError::EffectAborted)
82        } else {
83            Ok(())
84        }
85    }
86
87    async fn wrap_result<T, E: Into<RuntimeMethodError>>(
88        &self,
89        res: Result<T, E>,
90    ) -> Result<T, RuntimeMethodError> {
91        match res {
92            Ok(t) => Ok(t),
93            Err(err) => {
94                // TODO: Log error?
95                self.data.lock().await.aborted = true;
96                Err(err.into())
97            }
98        }
99    }
100}
101
102#[async_trait]
103impl RuntimeMethods for InstanceMethods {
104    fn get_led_count(&self) -> usize {
105        self.led_count
106    }
107
108    async fn abort(&self) -> bool {
109        self.poll_control().await.is_err()
110    }
111
112    async fn set_color(&self, color: crate::models::Color) -> Result<(), RuntimeMethodError> {
113        self.poll_control().await?;
114
115        self.wrap_result(self.tx.send(EffectMessageKind::SetColor { color }).await)
116            .await
117    }
118
119    async fn set_led_colors(
120        &self,
121        colors: Vec<crate::models::Color>,
122    ) -> Result<(), RuntimeMethodError> {
123        self.poll_control().await?;
124
125        self.wrap_result(
126            self.tx
127                .send(EffectMessageKind::SetLedColors {
128                    colors: colors.into(),
129                })
130                .await,
131        )
132        .await
133    }
134
135    async fn set_image(&self, image: RawImage) -> Result<(), RuntimeMethodError> {
136        self.poll_control().await?;
137
138        self.wrap_result(
139            self.tx
140                .send(EffectMessageKind::SetImage {
141                    image: image.into(),
142                })
143                .await,
144        )
145        .await
146    }
147}
148
149#[async_trait]
150pub trait RuntimeMethods: Send {
151    fn get_led_count(&self) -> usize;
152    async fn abort(&self) -> bool;
153
154    async fn set_color(&self, color: Color) -> Result<(), RuntimeMethodError>;
155    async fn set_led_colors(&self, colors: Vec<Color>) -> Result<(), RuntimeMethodError>;
156    async fn set_image(&self, image: RawImage) -> Result<(), RuntimeMethodError>;
157}
158
159#[derive(Debug, Error)]
160pub enum RuntimeMethodError {
161    #[cfg(feature = "python")]
162    #[error("Invalid arguments to hyperion.{name}")]
163    InvalidArguments { name: &'static str },
164    #[cfg(feature = "python")]
165    #[error("Length of bytearray argument should be 3*ledCount")]
166    InvalidByteArray,
167    #[error("Effect aborted")]
168    EffectAborted,
169    #[error(transparent)]
170    InvalidImageData(#[from] RawImageError),
171}
172
173impl<T> From<tokio::sync::mpsc::error::SendError<T>> for RuntimeMethodError {
174    fn from(_: tokio::sync::mpsc::error::SendError<T>) -> Self {
175        Self::EffectAborted
176    }
177}