1use std::{
2 path::PathBuf,
3 process::{Command, Stdio},
4};
5
6use serde_derive::{Deserialize, Serialize};
7use validator::Validate;
8
9use crate::{api::types::PriorityInfo, component::ComponentName, models::Color as RgbColor};
10
11#[derive(Debug, Deserialize, Validate)]
13pub struct Adjustment {
14 #[validate(nested)]
15 pub adjustment: ChannelAdjustment,
16}
17
18#[derive(Debug, Serialize, Deserialize, Validate)]
19#[serde(rename_all = "camelCase")]
20pub struct ChannelAdjustment {
21 pub id: Option<String>,
22 pub white: RgbColor,
23 pub red: RgbColor,
24 pub green: RgbColor,
25 pub blue: RgbColor,
26 pub cyan: RgbColor,
27 pub magenta: RgbColor,
28 pub yellow: RgbColor,
29 #[validate(range(min = 0, max = 100))]
30 pub backlight_threshold: u32,
31 pub backlight_colored: bool,
32 #[validate(range(min = 0, max = 100))]
33 pub brightness: u32,
34 #[validate(range(min = 0, max = 100))]
35 pub brightness_compensation: u32,
36 #[validate(range(min = 0.1, max = 5.0))]
37 pub gamma_red: f32,
38 #[validate(range(min = 0.1, max = 5.0))]
39 pub gamma_green: f32,
40 #[validate(range(min = 0.1, max = 5.0))]
41 pub gamma_blue: f32,
42}
43
44impl From<crate::models::ChannelAdjustment> for ChannelAdjustment {
45 fn from(adj: crate::models::ChannelAdjustment) -> Self {
46 Self {
47 id: Some(adj.id),
48 white: adj.white,
49 red: adj.red,
50 green: adj.green,
51 blue: adj.blue,
52 cyan: adj.cyan,
53 magenta: adj.magenta,
54 yellow: adj.yellow,
55 backlight_threshold: adj.backlight_threshold,
56 backlight_colored: adj.backlight_colored,
57 brightness: adj.brightness,
58 brightness_compensation: adj.brightness_compensation,
59 gamma_red: adj.gamma_red,
60 gamma_green: adj.gamma_green,
61 gamma_blue: adj.gamma_blue,
62 }
63 }
64}
65
66#[derive(Debug, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub enum AuthorizeCommand {
69 RequestToken,
70 CreateToken,
71 RenameToken,
72 DeleteToken,
73 GetTokenList,
74 Logout,
75 Login,
76 TokenRequired,
77 AdminRequired,
78 NewPasswordRequired,
79 NewPassword,
80 AnswerRequest,
81 GetPendingTokenRequests,
82}
83
84#[derive(Debug, Deserialize, Validate)]
85#[serde(rename_all = "camelCase")]
86pub struct Authorize {
87 pub subcommand: AuthorizeCommand,
88 #[validate(length(min = 8))]
89 pub password: Option<String>,
90 #[validate(length(min = 8))]
91 pub new_password: Option<String>,
92 #[validate(length(min = 36))]
93 pub token: Option<String>,
94 #[validate(length(min = 5))]
95 pub comment: Option<String>,
96 #[validate(length(min = 5, max = 5))]
97 pub id: Option<String>,
98 pub accept: Option<bool>,
99}
100
101#[derive(Debug, Deserialize, Validate)]
102pub struct Clear {
103 #[validate(range(min = -1, max = 253))]
104 pub priority: i32,
105}
106
107#[derive(Debug, Deserialize, Validate)]
108pub struct Color {
109 #[validate(range(min = 1, max = 253))]
110 pub priority: i32,
111 #[validate(range(min = 0))]
113 pub duration: Option<i32>,
114 #[validate(length(min = 4, max = 20))]
116 pub origin: Option<String>,
117 pub color: RgbColor,
118}
119
120#[derive(Debug, Deserialize)]
121pub struct ComponentStatus {
122 pub component: ComponentName,
123 pub state: bool,
124}
125
126#[derive(Debug, Deserialize, Validate)]
127pub struct ComponentState {
128 pub componentstate: ComponentStatus,
129}
130
131#[derive(Debug, Deserialize)]
132#[serde(rename_all = "lowercase")]
133pub enum ConfigCommand {
134 SetConfig,
135 GetConfig,
136 GetSchema,
137 Reload,
138}
139
140#[derive(Debug, Deserialize, Validate)]
141pub struct Config {
142 pub subcommand: ConfigCommand,
143 #[serde(default)]
144 pub config: serde_json::Map<String, serde_json::Value>,
145}
146
147#[derive(Debug, Deserialize)]
148pub struct ImageData(#[serde(deserialize_with = "crate::serde::from_base64")] pub Vec<u8>);
149
150#[derive(Debug, Deserialize, Validate)]
151#[serde(rename_all = "camelCase")]
152pub struct EffectCreate {
153 pub name: String,
154 pub script: String,
155 pub args: serde_json::Map<String, serde_json::Value>,
156 pub image_data: Option<ImageData>,
157}
158
159#[derive(Debug, Deserialize, Validate)]
160#[serde(rename_all = "camelCase")]
161pub struct EffectDelete {
162 pub name: String,
163}
164
165#[derive(Debug, Deserialize)]
166pub struct EffectRequest {
167 pub name: String,
169 #[serde(default)]
171 pub args: serde_json::Map<String, serde_json::Value>,
172}
173
174#[derive(Debug, Deserialize, Validate)]
176#[serde(rename_all = "camelCase")]
177pub struct Effect {
178 #[validate(range(min = 1, max = 253))]
179 pub priority: i32,
180 #[validate(range(min = 0))]
181 pub duration: Option<i32>,
182 #[validate(length(min = 4, max = 20))]
183 pub origin: Option<String>,
184 pub effect: EffectRequest,
185 pub python_script: Option<String>,
186 pub image_data: Option<ImageData>,
187}
188
189#[derive(Debug, Deserialize)]
190#[serde(rename_all = "lowercase")]
191#[derive(Default)]
192pub enum ImageFormat {
193 #[default]
194 Auto,
195}
196
197#[derive(Debug, Deserialize, Validate)]
198pub struct Image {
199 #[validate(range(min = 1, max = 253))]
200 pub priority: i32,
201 #[validate(length(min = 4, max = 20))]
202 pub origin: Option<String>,
203 #[validate(range(min = 0))]
204 pub duration: Option<i32>,
205 pub imagewidth: u32,
206 pub imageheight: u32,
207 #[serde(deserialize_with = "crate::serde::from_base64")]
208 pub imagedata: Vec<u8>,
209 #[serde(default)]
210 pub format: ImageFormat,
211 #[validate(range(min = 25, max = 2000))]
212 pub scale: Option<i32>,
213 pub name: Option<String>,
214}
215
216#[derive(Debug, Deserialize)]
217#[serde(rename_all = "camelCase")]
218pub enum InstanceCommand {
219 CreateInstance,
220 DeleteInstance,
221 StartInstance,
222 StopInstance,
223 SaveName,
224 SwitchTo,
225}
226
227#[derive(Debug, Deserialize, Validate)]
228pub struct Instance {
229 pub subcommand: InstanceCommand,
230 #[validate(range(min = 0, max = 255))]
231 pub instance: Option<i32>,
232 #[validate(length(min = 5))]
233 pub name: Option<String>,
234}
235
236#[derive(Debug, Deserialize)]
237#[serde(rename_all = "lowercase")]
238pub enum LedColorsSubcommand {
239 #[serde(rename = "ledstream-stop")]
240 LedStreamStop,
241 #[serde(rename = "ledstream-start")]
242 LedStreamStart,
243 TestLed,
244 #[serde(rename = "imagestream-start")]
245 ImageStreamStart,
246 #[serde(rename = "imagestream-stop")]
247 ImageStreamStop,
248}
249
250#[derive(Debug, Deserialize, Validate)]
251pub struct LedColors {
252 pub subcommand: LedColorsSubcommand,
253 pub oneshot: Option<bool>,
254 #[validate(range(min = 50))]
255 pub interval: Option<u32>,
256}
257
258#[derive(Debug, Deserialize)]
259#[serde(rename_all = "camelCase")]
260pub enum LedDeviceCommand {
261 Discover,
262 GetProperties,
263 Identify,
264}
265
266#[derive(Debug, Deserialize, Validate)]
267pub struct LedDevice {
268 pub subcommand: LedDeviceCommand,
269 pub led_device_type: String,
270 pub params: Option<serde_json::Map<String, serde_json::Value>>,
271}
272
273#[derive(Debug, Deserialize)]
274#[serde(rename_all = "lowercase")]
275pub enum LoggingCommand {
276 Stop,
277 Start,
278 Update,
279}
280
281#[derive(Debug, Deserialize, Validate)]
282pub struct Logging {
283 pub subcommand: LoggingCommand,
284 pub oneshot: Option<bool>,
285 pub interval: Option<u32>,
286}
287
288#[derive(Debug, Deserialize)]
289#[serde(rename_all = "snake_case")]
290pub enum MappingType {
291 MulticolorMean,
292 UnicolorMean,
293}
294
295#[derive(Debug, Deserialize, Validate)]
296#[serde(rename_all = "camelCase")]
297pub struct Processing {
298 pub mapping_type: MappingType,
299}
300
301#[derive(Debug, Deserialize, Validate)]
302pub struct ServerInfoRequest {
303 pub subscribe: Option<Vec<serde_json::Value>>,
304}
305
306#[derive(Debug, Deserialize, Validate)]
307pub struct SourceSelect {
308 #[validate(range(min = 0, max = 255))]
309 pub priority: i32,
310 pub auto: Option<bool>,
311}
312
313#[derive(Debug, Serialize, Deserialize)]
314pub enum VideoMode {
315 #[serde(rename = "2D")]
316 Mode2D,
317 #[serde(rename = "3DSBS")]
318 Mode3DSBS,
319 #[serde(rename = "3DTAB")]
320 Mode3DTAB,
321}
322
323#[derive(Debug, Deserialize, Validate)]
324#[serde(rename_all = "camelCase")]
325pub struct VideoModeRequest {
326 pub video_mode: VideoMode,
327}
328
329#[derive(Debug, Deserialize)]
331#[serde(rename_all = "lowercase", tag = "command")]
332pub enum HyperionCommand {
333 Adjustment(Adjustment),
334 Authorize(Authorize),
335 Clear(Clear),
336 ClearAll,
338 Color(Color),
339 ComponentState(ComponentState),
340 Config(Config),
341 #[serde(rename = "create-effect")]
342 EffectCreate(EffectCreate),
343 #[serde(rename = "delete-effect")]
344 EffectDelete(EffectDelete),
345 Effect(Effect),
346 Image(Image),
347 Instance(Instance),
348 LedColors(LedColors),
349 LedDevice(LedDevice),
350 Logging(Logging),
351 Processing(Processing),
352 ServerInfo(ServerInfoRequest),
353 SourceSelect(SourceSelect),
354 SysInfo,
355 VideoMode(VideoModeRequest),
356}
357
358#[derive(Debug, Deserialize)]
360pub struct HyperionMessage {
361 pub tan: Option<i32>,
363 #[serde(flatten)]
364 pub command: HyperionCommand,
365}
366
367impl Validate for HyperionMessage {
368 fn validate(&self) -> Result<(), validator::ValidationErrors> {
369 match &self.command {
370 HyperionCommand::Adjustment(adjustment) => adjustment.validate(),
371 HyperionCommand::Authorize(authorize) => authorize.validate(),
372 HyperionCommand::Clear(clear) => clear.validate(),
373 HyperionCommand::ClearAll => Ok(()),
374 HyperionCommand::Color(color) => color.validate(),
375 HyperionCommand::ComponentState(component_state) => component_state.validate(),
376 HyperionCommand::Config(config) => config.validate(),
377 HyperionCommand::EffectCreate(effect_create) => effect_create.validate(),
378 HyperionCommand::EffectDelete(effect_delete) => effect_delete.validate(),
379 HyperionCommand::Effect(effect) => effect.validate(),
380 HyperionCommand::Image(image) => image.validate(),
381 HyperionCommand::Instance(instance) => instance.validate(),
382 HyperionCommand::LedColors(led_colors) => led_colors.validate(),
383 HyperionCommand::LedDevice(led_device) => led_device.validate(),
384 HyperionCommand::Logging(logging) => logging.validate(),
385 HyperionCommand::Processing(processing) => processing.validate(),
386 HyperionCommand::ServerInfo(server_info) => server_info.validate(),
387 HyperionCommand::SourceSelect(source_select) => source_select.validate(),
388 HyperionCommand::SysInfo => Ok(()),
389 HyperionCommand::VideoMode(video_mode) => video_mode.validate(),
390 }
391 }
392}
393
394#[derive(Clone, Debug, Serialize, Deserialize)]
396pub struct EffectDefinition {
397 pub name: String,
399 pub file: String,
401 pub script: String,
403 pub args: serde_json::Value,
405}
406
407impl From<&crate::effects::EffectDefinition> for EffectDefinition {
408 fn from(value: &crate::effects::EffectDefinition) -> Self {
409 Self {
410 name: value.name.clone(),
411 file: value.file.to_string_lossy().to_string(),
412 script: value.script.clone(),
413 args: value.args.clone(),
414 }
415 }
416}
417
418#[derive(Debug, Clone, Copy, Serialize)]
419pub enum LedDeviceClass {
420 Dummy,
421 PhilipsHue,
422 #[serde(rename = "Ws2812SPI")]
423 Ws2812Spi,
424 #[serde(rename = "file")]
425 File,
426}
427
428#[derive(Debug, Serialize)]
429pub struct LedDevicesInfo {
430 pub available: Vec<LedDeviceClass>,
431}
432
433impl Default for LedDevicesInfo {
434 fn default() -> Self {
435 Self::new()
436 }
437}
438
439impl LedDevicesInfo {
440 pub fn new() -> Self {
441 use LedDeviceClass::*;
442
443 Self {
444 available: vec![Dummy, PhilipsHue, Ws2812Spi, File],
445 }
446 }
447}
448
449#[derive(Debug, Clone)]
450pub enum GrabberClass {
451 AmLogic,
452 DirectX,
453 Dispmanx,
454 Framebuffer,
455 OSX,
456 Qt,
457 V4L2 { device: PathBuf },
458 X11,
459 Xcb,
460}
461
462impl std::fmt::Display for GrabberClass {
463 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464 match self {
465 GrabberClass::AmLogic => write!(f, "AmLogic"),
466 GrabberClass::DirectX => write!(f, "DirectX"),
467 GrabberClass::Dispmanx => write!(f, "Dispmanx"),
468 GrabberClass::Framebuffer => write!(f, "FrameBuffer"),
469 GrabberClass::OSX => write!(f, "OSX FrameGrabber"),
470 GrabberClass::Qt => write!(f, "Qt"),
471 GrabberClass::V4L2 { device } => write!(f, "V4L2:{}", device.display()),
472 GrabberClass::X11 => write!(f, "X11"),
473 GrabberClass::Xcb => write!(f, "Xcb"),
474 }
475 }
476}
477
478impl serde::ser::Serialize for GrabberClass {
479 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
480 where
481 S: serde::Serializer,
482 {
483 serializer.serialize_str(&self.to_string())
484 }
485}
486
487#[derive(Debug, Serialize)]
488pub struct GrabbersInfo {
489 #[serde(skip_serializing_if = "Option::is_none")]
490 pub active: Option<GrabberClass>,
491 pub available: Vec<GrabberClass>,
492}
493
494impl Default for GrabbersInfo {
495 fn default() -> Self {
496 Self::new()
497 }
498}
499
500impl GrabbersInfo {
501 pub fn new() -> Self {
502 Self {
503 active: None,
505 available: vec![], }
508 }
509}
510
511#[derive(Debug, Serialize)]
513pub struct ServerInfo {
514 pub priorities: Vec<PriorityInfo>,
516 pub priorities_autoselect: bool,
517 pub adjustment: Vec<ChannelAdjustment>,
519 pub effects: Vec<EffectDefinition>,
521 #[serde(rename = "ledDevices")]
523 pub led_devices: LedDevicesInfo,
524 pub grabbers: GrabbersInfo,
526 #[serde(rename = "videomode")]
528 pub video_mode: VideoMode,
529 #[serde(rename = "instance")]
533 pub instances: Vec<InstanceInfo>,
534 pub hostname: String,
536 }
540
541#[derive(Default, Debug, Serialize)]
543pub struct BuildInfo {
544 pub version: String,
546 pub time: String,
548}
549
550impl BuildInfo {
551 pub fn new() -> Self {
552 Self {
553 version: version(),
554 ..Default::default()
555 }
556 }
557}
558
559#[derive(Debug, Serialize)]
560pub struct InstanceInfo {
561 pub friendly_name: String,
562 pub instance: i32,
563 pub running: bool,
564}
565
566impl From<&crate::models::Instance> for InstanceInfo {
567 fn from(config: &crate::models::Instance) -> Self {
568 Self {
569 friendly_name: config.friendly_name.clone(),
570 instance: config.id,
571 running: config.enabled,
573 }
574 }
575}
576
577#[derive(Debug, Serialize)]
578pub struct HyperionResponse {
579 success: bool,
580 #[serde(skip_serializing_if = "Option::is_none")]
581 tan: Option<i32>,
582 #[serde(skip_serializing_if = "Option::is_none")]
583 error: Option<String>,
584 #[serde(skip_serializing_if = "Option::is_none", flatten)]
585 info: Option<HyperionResponseInfo>,
586}
587
588#[derive(Default, Debug, Serialize)]
589#[serde(rename_all = "camelCase")]
590pub struct SystemInfo {
591 pub kernel_type: String,
592 pub kernel_version: String,
593 pub architecture: String,
594 pub cpu_model_name: String,
595 pub cpu_model_type: String,
596 pub cpu_hardware: String,
597 pub cpu_revision: String,
598 pub word_size: String,
599 pub product_type: String,
600 pub product_version: String,
601 pub pretty_name: String,
602 pub host_name: String,
603 pub domain_name: String,
604 pub qt_version: String,
605 pub py_version: String,
606}
607
608impl SystemInfo {
609 pub fn new() -> Self {
610 Self {
612 kernel_type: if cfg!(target_os = "windows") {
613 "winnt".to_owned()
614 } else if cfg!(target_os = "linux") {
615 Command::new("uname")
616 .args(["-s"])
617 .stdout(Stdio::piped())
618 .output()
619 .ok()
620 .and_then(|output| String::from_utf8(output.stdout).ok())
621 .map(|output| output.trim().to_ascii_lowercase())
622 .unwrap_or_default()
623 } else {
624 String::new()
625 },
626 kernel_version: if cfg!(target_os = "linux") {
627 Command::new("uname")
628 .args(["-r"])
629 .stdout(Stdio::piped())
630 .output()
631 .ok()
632 .and_then(|output| String::from_utf8(output.stdout).ok())
633 .map(|output| output.trim().to_ascii_lowercase())
634 .unwrap_or_default()
635 } else {
636 String::new()
637 },
638 host_name: hostname(),
639 ..Default::default()
640 }
641 }
642}
643
644#[derive(Default, Debug, Serialize)]
645#[serde(rename_all = "camelCase")]
646pub struct HyperionInfo {
647 pub version: String,
648 pub build: String,
649 pub gitremote: String,
650 pub time: String,
651 pub id: uuid::Uuid,
652 pub read_only_mode: bool,
653}
654
655impl HyperionInfo {
656 pub fn new(id: uuid::Uuid) -> Self {
657 Self {
659 version: "2.0.0-alpha.8".to_owned(),
661 build: version(),
662 id,
663 read_only_mode: false,
664 ..Default::default()
665 }
666 }
667}
668
669#[derive(Debug, Serialize)]
670pub struct SysInfo {
671 pub system: SystemInfo,
672 pub hyperion: HyperionInfo,
673}
674
675impl SysInfo {
676 pub fn new(id: uuid::Uuid) -> Self {
677 Self {
678 system: SystemInfo::new(),
679 hyperion: HyperionInfo::new(id),
680 }
681 }
682}
683
684#[derive(Debug, Serialize)]
686#[serde(tag = "command", content = "info")]
687#[allow(clippy::large_enum_variant)]
688pub enum HyperionResponseInfo {
689 #[serde(rename = "serverinfo")]
691 ServerInfo(ServerInfo),
692 #[serde(rename = "authorize-adminRequired")]
694 AdminRequired {
695 #[serde(rename = "adminRequired")]
697 admin_required: bool,
698 },
699 #[serde(rename = "authorize-tokenRequired")]
701 TokenRequired {
702 required: bool,
704 },
705 #[serde(rename = "sysinfo")]
707 SysInfo(SysInfo),
708 #[serde(rename = "instance-switchTo")]
710 SwitchTo {
711 #[serde(skip_serializing_if = "Option::is_none")]
712 instance: Option<i32>,
713 },
714}
715
716impl HyperionResponse {
717 pub fn with_tan(mut self, tan: Option<i32>) -> Self {
718 self.tan = tan;
719 self
720 }
721
722 fn success_info(info: HyperionResponseInfo) -> Self {
723 Self {
724 success: true,
725 tan: None,
726 error: None,
727 info: Some(info),
728 }
729 }
730
731 pub fn success() -> Self {
733 Self {
734 success: true,
735 tan: None,
736 error: None,
737 info: None,
738 }
739 }
740
741 pub fn error(error: impl std::fmt::Display) -> Self {
743 Self {
744 success: false,
745 tan: None,
746 error: Some(error.to_string()),
747 info: None,
748 }
749 }
750
751 pub fn error_info(error: impl std::fmt::Display, info: HyperionResponseInfo) -> Self {
753 Self {
754 success: false,
755 tan: None,
756 error: Some(error.to_string()),
757 info: Some(info),
758 }
759 }
760
761 pub fn server_info(
763 priorities: Vec<PriorityInfo>,
764 adjustment: Vec<ChannelAdjustment>,
765 effects: Vec<EffectDefinition>,
766 instances: Vec<InstanceInfo>,
767 ) -> Self {
768 Self::success_info(HyperionResponseInfo::ServerInfo(ServerInfo {
769 priorities,
770 priorities_autoselect: true,
772 adjustment,
773 effects,
774 led_devices: LedDevicesInfo::new(),
775 grabbers: GrabbersInfo::new(),
776 video_mode: VideoMode::Mode2D,
778 instances,
779 hostname: hostname(),
780 }))
781 }
782
783 pub fn admin_required(admin_required: bool) -> Self {
784 Self::success_info(HyperionResponseInfo::AdminRequired { admin_required })
785 }
786
787 pub fn token_required(required: bool) -> Self {
788 Self::success_info(HyperionResponseInfo::TokenRequired { required })
789 }
790
791 pub fn sys_info(id: uuid::Uuid) -> Self {
792 Self::success_info(HyperionResponseInfo::SysInfo(SysInfo::new(id)))
794 }
795
796 pub fn switch_to(id: Option<i32>) -> Self {
797 if let Some(id) = id {
798 Self::success_info(HyperionResponseInfo::SwitchTo { instance: Some(id) })
800 } else {
801 Self::error_info(
802 "selected hyperion instance not found",
803 HyperionResponseInfo::SwitchTo { instance: None },
804 )
805 }
806 }
807}
808
809fn hostname() -> String {
810 hostname::get()
811 .map(|s| s.to_string_lossy().to_string())
812 .unwrap_or_else(|_| "<unknown hostname>".to_owned())
813}
814
815fn version() -> String {
816 git_version::git_version!(
817 prefix = "hyperion.rs-",
818 args = ["--always", "--tags"],
819 fallback = env!("HYPERION_RS_GIT_VERSION")
820 )
821 .to_owned()
822}