hyperion/api/
json.rs

1use std::convert::TryFrom;
2use std::sync::Arc;
3
4use thiserror::Error;
5use tokio::sync::{oneshot, Mutex};
6use validator::Validate;
7
8use crate::{
9    component::ComponentName,
10    global::{Global, InputMessage, InputMessageData, InputSourceHandle, Message},
11    image::{RawImage, RawImageError},
12    instance::{InstanceHandle, InstanceHandleError, StartEffectError},
13};
14
15/// Schema definitions as Serde serializable structures and enums
16pub mod message;
17use message::{HyperionCommand, HyperionMessage, HyperionResponse};
18
19#[derive(Debug, Error)]
20pub enum JsonApiError {
21    #[error("error broadcasting update: {0}")]
22    Broadcast(#[from] tokio::sync::broadcast::error::SendError<InputMessage>),
23    #[error("request not implemented")]
24    NotImplemented,
25    #[error("error decoding image")]
26    Image(#[from] RawImageError),
27    #[error("error validating request: {0}")]
28    Validation(#[from] validator::ValidationErrors),
29    #[error("error receiving system response: {0}")]
30    Recv(#[from] oneshot::error::RecvError),
31    #[error("error accessing the current instance: {0}")]
32    Instance(#[from] InstanceHandleError),
33    #[error("no current instance found")]
34    InstanceNotFound,
35    #[error(transparent)]
36    StartEffect(#[from] StartEffectError),
37}
38
39/// A client connected to the JSON endpoint
40pub struct ClientConnection {
41    source: InputSourceHandle<InputMessage>,
42    current_instance: Option<i32>,
43}
44
45impl ClientConnection {
46    pub fn new(source: InputSourceHandle<InputMessage>) -> Self {
47        Self {
48            source,
49            current_instance: None,
50        }
51    }
52
53    async fn current_instance(&mut self, global: &Global) -> Result<InstanceHandle, JsonApiError> {
54        if let Some(current_instance) = self.current_instance {
55            if let Some(instance) = global.get_instance(current_instance).await {
56                return Ok(instance);
57            } else {
58                // Instance id now invalid, reset
59                self.current_instance = None;
60            }
61        }
62
63        if let Some((id, inst)) = global.default_instance().await {
64            self.set_current_instance(id);
65            return Ok(inst);
66        }
67
68        Err(JsonApiError::InstanceNotFound)
69    }
70
71    fn set_current_instance(&mut self, id: i32) {
72        debug!("{}: switch to instance {}", &self.source.name(), id);
73        self.current_instance = Some(id);
74    }
75
76    #[instrument(skip(request, global))]
77    pub async fn handle_request(
78        &mut self,
79        request: HyperionMessage,
80        global: &Global,
81    ) -> Result<HyperionResponse, JsonApiError> {
82        request.validate()?;
83
84        match request.command {
85            HyperionCommand::ClearAll => {
86                // Update state
87                self.source
88                    .send(ComponentName::All, InputMessageData::ClearAll)?;
89            }
90
91            HyperionCommand::Clear(message::Clear { priority }) => {
92                // Update state
93                self.source
94                    .send(ComponentName::All, InputMessageData::Clear { priority })?;
95            }
96
97            HyperionCommand::Color(message::Color {
98                priority,
99                duration,
100                color,
101                origin: _,
102            }) => {
103                // TODO: Handle origin field
104
105                // Update state
106                self.source.send(
107                    ComponentName::Color,
108                    InputMessageData::SolidColor {
109                        priority,
110                        duration: duration.map(|ms| chrono::Duration::milliseconds(ms as _)),
111                        color,
112                    },
113                )?;
114            }
115
116            HyperionCommand::Image(message::Image {
117                priority,
118                duration,
119                imagewidth,
120                imageheight,
121                imagedata,
122                origin: _,
123                format: _,
124                scale: _,
125                name: _,
126            }) => {
127                // TODO: Handle origin, format, scale, name fields
128
129                let raw_image = RawImage::try_from((imagedata, imagewidth, imageheight))?;
130
131                self.source.send(
132                    ComponentName::Image,
133                    InputMessageData::Image {
134                        priority,
135                        duration: duration.map(|ms| chrono::Duration::milliseconds(ms as _)),
136                        image: Arc::new(raw_image),
137                    },
138                )?;
139            }
140
141            HyperionCommand::Effect(message::Effect {
142                priority,
143                duration,
144                origin: _,
145                effect,
146                python_script: _,
147                image_data: _,
148            }) => {
149                // TODO: Handle origin, python_script, image_data
150
151                let instance = self.current_instance(global).await?;
152                let (tx, rx) = oneshot::channel();
153
154                instance
155                    .send(InputMessage::new(
156                        self.source.id(),
157                        ComponentName::All,
158                        InputMessageData::Effect {
159                            priority,
160                            duration: duration.map(|ms| chrono::Duration::milliseconds(ms as _)),
161                            effect: effect.into(),
162                            response: Arc::new(Mutex::new(Some(tx))),
163                        },
164                    ))
165                    .await?;
166
167                return Ok(rx.await?.map(|_| HyperionResponse::success())?);
168            }
169
170            HyperionCommand::ServerInfo(message::ServerInfoRequest { subscribe: _ }) => {
171                // TODO: Handle subscribe field
172
173                let (adjustments, priorities) =
174                    if let Ok(handle) = self.current_instance(global).await {
175                        (
176                            handle
177                                .config()
178                                .await?
179                                .color
180                                .channel_adjustment
181                                .iter()
182                                .map(|adj| message::ChannelAdjustment::from(adj.clone()))
183                                .collect(),
184                            handle.current_priorities().await?,
185                        )
186                    } else {
187                        Default::default()
188                    };
189
190                // Read effect info
191                // TODO: Add per-instance effects
192                let effects: Vec<message::EffectDefinition> = global
193                    .read_effects(|effects| effects.iter().map(Into::into).collect())
194                    .await;
195
196                // Just answer the serverinfo request, no need to update state
197                return Ok(global
198                    .read_config(|config| {
199                        let instances = config
200                            .instances
201                            .iter()
202                            .map(|instance_config| (&instance_config.1.instance).into())
203                            .collect();
204
205                        HyperionResponse::server_info(priorities, adjustments, effects, instances)
206                    })
207                    .await);
208            }
209
210            HyperionCommand::Authorize(message::Authorize { subcommand, .. }) => match subcommand {
211                message::AuthorizeCommand::AdminRequired => {
212                    // TODO: Perform actual authentication flow
213                    return Ok(HyperionResponse::admin_required(false));
214                }
215                message::AuthorizeCommand::TokenRequired => {
216                    // TODO: Perform actual authentication flow
217                    return Ok(HyperionResponse::token_required(false));
218                }
219                _ => {
220                    return Err(JsonApiError::NotImplemented);
221                }
222            },
223
224            HyperionCommand::SysInfo => {
225                return Ok(HyperionResponse::sys_info(
226                    global.read_config(|config| config.uuid()).await,
227                ));
228            }
229
230            HyperionCommand::Instance(message::Instance {
231                subcommand: message::InstanceCommand::SwitchTo,
232                instance: Some(id),
233                ..
234            }) => {
235                if global.get_instance(id).await.is_some() {
236                    self.set_current_instance(id);
237                    return Ok(HyperionResponse::switch_to(Some(id)));
238                } else {
239                    // Note: it's an "Ok" but should be an Err. Find out how to represent errors
240                    // better
241                    return Ok(HyperionResponse::switch_to(None));
242                }
243            }
244
245            _ => return Err(JsonApiError::NotImplemented),
246        };
247
248        Ok(HyperionResponse::success())
249    }
250}
251
252impl std::fmt::Debug for ClientConnection {
253    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
254        f.debug_struct("ClientConnection")
255            .field("source", &format!("{}", &*self.source))
256            .finish()
257    }
258}