1use std::cmp::Ordering;
2use std::collections::HashSet;
3use std::fmt;
4use std::io::{BufRead, BufReader, Read};
5use std::path::Path;
6use std::str::FromStr;
7use std::time::Duration;
8
9use anyhow::{anyhow, bail, Context};
10use base64::engine::general_purpose::STANDARD;
11use base64::Engine;
12use bytes::{BufMut, BytesMut};
13use chrono::{NaiveTime, Timelike};
14use clap::ValueEnum;
15use derivative::Derivative;
16use glow_effects::effects::shine::Shine;
17use glow_effects::util::color_point::{ColorPointContainer, RgbPoint};
18use glow_effects::util::effect::Effect;
19use glow_effects::util::point::Point;
20use log::debug;
21use palette::{FromColor, Hsl, IntoColor, Srgb};
22
23use reqwest::{Client, StatusCode};
24use serde::{Deserialize, Deserializer, Serialize};
25use serde_json::json;
26use tokio::net::UdpSocket;
27use tokio::time::{sleep, Instant};
28use uuid::Uuid;
29
30use crate::util::auth::Auth;
31use crate::util::discovery::DeviceIdentifier;
32use crate::util::movie::Movie;
33use crate::util::traits;
34use crate::util::traits::{ResponseCode, ResponseCodeTrait};
35
36pub enum HardwareVersion {
38 Version1,
39 Version2,
40 Version3,
41}
42
43#[derive(Debug, Clone)]
44pub struct ControlInterface {
45 pub host: String,
46 hw_address: String,
47 pub(crate) auth_token: String,
48 client: Client,
49 device_info: DeviceInfoResponse,
50}
51
52impl PartialEq for ControlInterface {
58 fn eq(&self, other: &ControlInterface) -> bool {
59 self.device_info == other.device_info
60 && self.host == other.host
61 && self.hw_address == other.hw_address
62 }
63}
64
65impl PartialOrd for ControlInterface {
72 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
73 let mut ord: Ordering = self.device_info.partial_cmp(&other.device_info).unwrap();
74 if ord.is_eq() {
75 ord = self.host.cmp(&other.host);
76 if ord.is_eq() {
77 ord = self.hw_address.cmp(&other.hw_address);
78 }
79 }
80 Some(ord)
81 }
82}
83
84#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
85pub enum CliDeviceMode {
86 Movie,
87 Playlist,
88 RealTime,
89 Demo,
90 Effect,
91 Color,
92 Off,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum DeviceMode {
97 Movie,
98 Playlist,
99 RealTime,
100 Demo,
101 Effect,
102 Color,
103 Off,
104}
105
106#[derive(Debug, Clone, Deserialize)]
108pub struct BrightnessResponse {
109 pub code: u32,
111 pub mode: String,
113 pub value: i32,
115}
116
117impl BrightnessResponse {
118 pub fn is_enabled(&self) -> bool {
120 self.mode == "enabled"
121 }
122}
123
124impl ResponseCodeTrait for BrightnessResponse {
125 fn response_code(&self) -> ResponseCode {
126 Self::map_response_code(self.code)
127 }
128}
129
130impl FromStr for DeviceMode {
131 type Err = anyhow::Error;
132
133 fn from_str(s: &str) -> Result<Self, Self::Err> {
134 match s.to_lowercase().as_str() {
135 "movie" => Ok(DeviceMode::Movie),
136 "playlist" => Ok(DeviceMode::Playlist),
137 "rt" => Ok(DeviceMode::RealTime),
138 "demo" => Ok(DeviceMode::Demo),
139 "effect" => Ok(DeviceMode::Effect),
140 "color" => Ok(DeviceMode::Color),
141 "off" => Ok(DeviceMode::Off),
142 _ => Err(anyhow!("Invalid mode")),
143 }
144 }
145}
146
147impl fmt::Display for DeviceMode {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 let mode_str = match self {
150 DeviceMode::Movie => "movie",
151 DeviceMode::Playlist => "playlist",
152 DeviceMode::RealTime => "rt",
153 DeviceMode::Demo => "demo",
154 DeviceMode::Effect => "effect",
155 DeviceMode::Color => "color",
156 DeviceMode::Off => "off",
157 };
158 write!(f, "{}", mode_str)
159 }
160}
161
162impl ControlInterface {
163 pub async fn new(
164 host: &str,
165 hw_address: &str,
166 existing_auth_token: Option<String>,
167 ) -> anyhow::Result<Self> {
168 let client = Client::new();
169
170 let auth_token: String = if let Some(given_auth_token) = existing_auth_token {
171 given_auth_token
172 } else {
173 ControlInterface::authenticate(&client, host, hw_address).await?
174 };
175
176 let device_info = ControlInterface::fetch_device_info(&client, host, &auth_token).await?;
178
179 Ok(ControlInterface {
180 host: host.to_string(),
181 hw_address: hw_address.to_string(),
182 auth_token,
183 client,
184 device_info,
185 })
186 }
187
188 pub async fn reauthenticate(&mut self) -> bool {
189 if let Ok(result) =
190 ControlInterface::authenticate(&self.client, &self.host, &self.hw_address).await
191 {
192 self.auth_token = result;
193 true
194 } else {
195 false
196 }
197 }
198
199 pub fn with_auth_token(mut self, auth_token: String) -> Self {
203 self.auth_token = auth_token;
204 self
205 }
206
207 pub fn new_mock_device_info_response(
212 id: String,
213 device_name: String,
214 mac: String,
215 number_of_led: usize,
216 ) -> DeviceInfoResponse {
217 DeviceInfoResponse {
218 product_name: "Twinkly".to_string(),
219 hardware_version: "500".to_string(),
220 bytes_per_led: 3,
221 hw_id: id,
222 flash_size: None,
223 led_type: 36,
224 product_code: "TWQ012STW".to_string(),
225 fw_family: "T".to_string(),
226 device_name,
227 uptime: Default::default(),
228 mac,
229 uuid: Uuid::new_v4().to_string(),
230 max_supported_led: 3904.max(number_of_led),
231 number_of_led,
232 pwr: Some(DevicePower {
233 mA: 3250,
234 mV: 20000,
235 }),
236 led_profile: LedProfile::RGB,
237 frame_rate: 40.0,
238 measured_frame_rate: 12.0,
239 movie_capacity: 2722,
240 max_movies: 55,
241 wire_type: 0,
242 copyright: "Fake Copyright".to_string(),
243 code: crate::util::traits::OK.code as usize,
244 }
245 }
246
247 pub fn new_mock_control_interface(
253 host: String,
254 hw_address: String,
255 auth_token: String,
256 device_info: DeviceInfoResponse,
257 ) -> Self {
258 ControlInterface {
259 host,
260 hw_address,
261 auth_token,
262 client: Client::new(),
263 device_info,
264 }
265 }
266
267 pub async fn from_device_identifier(
271 device_identifier: DeviceIdentifier,
272 ) -> anyhow::Result<Self> {
273 ControlInterface::new(
274 device_identifier.ip_address.to_string().as_str(),
275 device_identifier.mac_address.to_string().as_str(),
276 device_identifier.auth_token,
277 )
278 .await
279 }
280
281 pub fn get_hw_address(&self) -> String {
282 self.hw_address.clone()
283 }
284
285 pub async fn shine_leds(
286 &self,
287 time_between_glow_start: Duration,
288 time_to_max_glow: Duration,
289 time_to_fade: Duration,
290 colors: HashSet<RGB>,
291 frame_rate: f64,
292 num_start_simultaneous: usize,
293 ) -> anyhow::Result<()> {
294 let socket = UdpSocket::bind("0.0.0.0:0").await?;
295 socket.connect((self.host.as_str(), 7777)).await?;
296 self.set_mode(DeviceMode::RealTime).await?;
297
298 let num_leds = self.device_info.number_of_led;
299 let leds = vec![
300 RgbPoint::new(
301 Point {
302 x: 0.0,
303 y: 0.0,
304 z: 0.0,
305 },
306 glow_effects::util::color::RGB {
307 red: 0,
308 green: 0,
309 blue: 0,
310 }
311 );
312 num_leds
313 ];
314 let colors = colors
316 .into_iter()
317 .map(|color| glow_effects::util::color::RGB {
318 red: color.red,
319 green: color.green,
320 blue: color.blue,
321 });
322
323 let colors: HashSet<glow_effects::util::color::RGB> = colors.into_iter().collect();
325 let milliseconds_between_frames = (1.0 / frame_rate * 1000.0) as u128;
326 let frames_between_glow_start =
327 (time_between_glow_start.as_millis() / milliseconds_between_frames) as u32;
328 let frames_to_max_glow =
329 (time_to_max_glow.as_millis() / milliseconds_between_frames) as u32;
330 let frames_to_fade = (time_to_fade.as_millis() / milliseconds_between_frames) as u32;
331 let mut effect = Shine::new(
332 leds,
333 colors,
334 frames_between_glow_start,
335 frames_to_max_glow,
336 frames_to_fade,
337 num_start_simultaneous,
338 )?;
339
340 for frame in effect.iter() {
341 let frame = frame
343 .iter()
344 .map(|point| {
345 let color = point.get_color_value();
346 (color.red, color.green, color.blue)
347 })
348 .collect();
349 let flattened_frame = ControlInterface::flatten_rgb_vec(frame);
350 self.set_rt_frame_socket(&socket, &flattened_frame, HardwareVersion::Version3)
351 .await?;
352 sleep(Duration::from_secs_f64(1.0 / frame_rate)).await;
353 }
354 Ok(())
355 }
356
357 pub async fn show_solid_color(&self, rgb: RGB) -> anyhow::Result<()> {
358 let frame = vec![(rgb.red, rgb.green, rgb.blue); self.device_info.number_of_led];
359 let flattened_frame = ControlInterface::flatten_rgb_vec(frame);
360 self.set_mode(DeviceMode::RealTime).await?;
361 let socket = UdpSocket::bind("0.0.0.0:0").await?;
362 socket.connect((self.host.as_str(), 7777)).await?;
363 loop {
364 self.set_rt_frame_socket(&socket, &flattened_frame, HardwareVersion::Version3)
365 .await?;
366 sleep(Duration::from_millis(100)).await;
367 }
368 }
369
370 pub async fn show_real_time_stdin_stream(
371 &self,
372 format: RtStdinFormat,
373 error_mode: RtStdinErrorMode,
374 leds_per_frame: u16,
375 min_frame_time: Duration,
376 ) -> anyhow::Result<()> {
377 let stream = std::io::stdin();
378 let mut reader = BufReader::new(stream);
379 let mut current_frame = vec![(0, 0, 0); self.device_info.number_of_led];
381 self.set_mode(DeviceMode::RealTime).await?;
382 loop {
383 let mut leds_read: Vec<AddressableLed> = Vec::new();
384 let time_at_last_frame = Instant::now();
385 loop {
386 let mut led = match format {
387 RtStdinFormat::Binary => {
388 self.show_real_time_stdin_stream_binary(&mut reader).await?
389 } RtStdinFormat::JsonLines => {
391 self.show_real_time_stdin_stream_jsonl(&mut reader).await?
392 }
393 };
394 match error_mode {
395 RtStdinErrorMode::IgnoreInvalidAddress => {}
396 RtStdinErrorMode::ModInvalidAddress => {
397 led.address %= self.device_info.number_of_led as u16;
398 }
399 RtStdinErrorMode::StopInvalidAddress => {
400 if led.address >= self.device_info.number_of_led as u16 {
401 bail!("Invalid LED address: {:?}", led);
402 }
403 }
404 }
405 println!("LED: {:?}", led);
406 leds_read.push(led);
407
408 AddressableLed::merge_frame_array(&leds_read, &mut current_frame);
409 if leds_read.len() == leds_per_frame as usize {
410 break;
411 }
412 }
413 let current_time = Instant::now();
414 let time_since_last_frame = current_time - time_at_last_frame;
415 if time_since_last_frame < min_frame_time {
417 sleep(min_frame_time - time_since_last_frame).await;
418 }
419
420 let network_frame = ControlInterface::flatten_rgb_vec(current_frame.clone().to_vec());
421 let socket = UdpSocket::bind("0.0.0.0:0").await?;
422 socket.connect((self.host.as_str(), 7777)).await?;
423 self.set_rt_frame_socket(&socket, &network_frame, HardwareVersion::Version3)
424 .await?;
425 }
426 }
427
428 async fn show_real_time_stdin_stream_binary(
429 &self,
430 reader: &mut BufReader<impl Read>,
431 ) -> anyhow::Result<AddressableLed> {
432 let mut buffer = [0; 5];
433 reader.read_exact(&mut buffer)?;
434
435 let led_address = u16::from_be_bytes([buffer[0], buffer[1]]);
436 let red = buffer[2];
437 let green = buffer[3];
438 let blue = buffer[4];
439 let data = BinaryStreamFormat {
440 led_address,
441 red,
442 green,
443 blue,
444 };
445 let led: AddressableLed = data.into();
446
447 Ok(led)
448 }
449
450 async fn show_real_time_stdin_stream_jsonl(
451 &self,
452 reader: &mut BufReader<impl Read>,
453 ) -> anyhow::Result<AddressableLed> {
454 let mut line = String::new();
456 reader.read_line(&mut line)?;
457 let led: AddressableLedJsonLFormat = serde_json::from_str(&line)?;
459 let led: AddressableLed = led.into();
461
462 Ok(led)
463 }
464
465 pub async fn show_real_time_test_color_wheel(
466 &self,
467 step: f64,
468 frame_rate: f64,
469 ) -> anyhow::Result<()> {
470 let interval = Duration::from_secs_f64(1.0 / frame_rate);
471 let mut offset = 0_f64;
472 self.set_mode(DeviceMode::RealTime).await?;
473 let layout = self.fetch_layout().await?;
474 loop {
475 let gradient_frame =
477 generate_color_gradient_along_axis(&layout.coordinates, Axis::Z, offset);
478 let gradient_frame = ControlInterface::flatten_rgb_vec(gradient_frame);
479 let socket = UdpSocket::bind("0.0.0.0:0").await?;
480 socket.connect((self.host.as_str(), 7777)).await?;
481 self.set_rt_frame_socket(&socket, &gradient_frame, HardwareVersion::Version3)
482 .await?;
483
484 offset = (offset + step) % 1.0;
486
487 sleep(interval).await;
489
490 println!("Offset: {}", offset);
491 }
492 }
493
494 pub fn flatten_rgb_vec(rgb_vec: Vec<(u8, u8, u8)>) -> Vec<u8> {
495 rgb_vec
496 .into_iter()
497 .flat_map(|(r, g, b)| vec![r, g, b])
498 .collect()
499 }
500
501 pub async fn set_rt_frame_socket(
508 &self,
509 socket: &UdpSocket,
510 frame: &[u8],
511 version: HardwareVersion,
512 ) -> anyhow::Result<usize> {
513 let access_token = STANDARD
518 .decode(&self.auth_token)
519 .context("Failed to decode access token")?;
520
521 let mut packet = BytesMut::new();
523 match version {
524 HardwareVersion::Version1 => {
525 packet.put_u8(1); packet.extend_from_slice(&access_token);
527 packet.put_u8(self.device_info.number_of_led as u8); packet.extend_from_slice(frame);
529 }
530 HardwareVersion::Version2 => {
531 packet.put_u8(2); packet.extend_from_slice(&access_token);
533 packet.put_u8(0); packet.extend_from_slice(frame);
535 }
536 _ => {
537 let packet_size = 900;
539 let mut written_bytes = 0;
540 for (i, chunk) in frame.chunks(packet_size).enumerate() {
541 packet.clear();
542 packet.put_u8(3); packet.extend_from_slice(&access_token);
544 packet.put_u16(0); packet.put_u8(i as u8); packet.extend_from_slice(chunk);
547 let send_result: std::io::Result<usize> = socket.send(&packet).await;
548
549 if let Ok(send_result) = send_result {
550 written_bytes += send_result;
551 } else if let Some(err) = send_result.err() {
552 let err_string = format!("Failed to send frame {}: {:?}", i, err);
553 debug!("{}", err_string);
554 return Err(anyhow!(err_string));
555 }
556 }
557 return Ok(written_bytes); }
559 }
560
561 socket.send(&packet).await.map_err(|err| anyhow!(err))
563 }
564
565 pub async fn show_rt_frame(&self, frame: &[u8]) -> anyhow::Result<usize> {
574 let mode_response = self.get_mode().await?;
576 let current_mode = mode_response;
577
578 if current_mode != DeviceMode::RealTime {
580 self.set_mode(DeviceMode::RealTime).await?;
581 }
582
583 let socket = UdpSocket::bind("0.0.0.0:0").await?;
584 socket.connect((self.host.as_str(), 7777)).await?;
585 self.set_rt_frame_socket(&socket, frame, HardwareVersion::Version3)
587 .await
588 .map_err(|err| anyhow!(err))
589 }
590
591 pub fn get_device_info(&self) -> &DeviceInfoResponse {
592 &self.device_info
593 }
594 async fn fetch_device_info(
595 client: &Client,
596 host: &str,
597 auth_token: &str,
598 ) -> anyhow::Result<DeviceInfoResponse> {
599 let url = format!("http://{}/xled/v1/gestalt", host);
600 let response = client
601 .get(&url)
602 .header("X-Auth-Token", auth_token)
603 .send()
604 .await
605 .map_err(|e| anyhow!("Failed to fetch layout: {}", e))?;
606
607 if response.status() != reqwest::StatusCode::OK {
608 return Err(anyhow!(
609 "Failed to fetch device info with status: {}",
610 response.status()
611 ));
612 }
613 let response = response.text().await?;
614 let device_info: DeviceInfoResponse = serde_json::from_str(&response)?;
616 Ok(device_info)
622 }
623
624 pub async fn upload_movie<P: AsRef<Path>>(
626 &self,
627 path: P,
628 led_profile: LedProfile,
629 _fps: f64,
630 force: bool,
631 ) -> anyhow::Result<u32> {
632 let movie = Movie::load_movie(path, led_profile)?;
633 let num_frames = movie.frames.len();
634 let _num_leds = self.device_info.number_of_led;
635 let _bytes_per_led = match led_profile {
636 LedProfile::RGB => 3,
637 LedProfile::RGBW => 4,
638 };
639
640 let capacity = self.get_device_capacity().await?;
642 if num_frames > capacity && !force {
643 return Err(anyhow!("Not enough capacity for the movie"));
644 }
645
646 if force {
648 self.clear_movies().await?;
649 }
650
651 let movie_data = Movie::to_movie(movie.frames, led_profile);
653
654 let url = format!("http://{}/xled/v1/led/movie/full", self.host);
656 let response = self
657 .client
658 .post(&url)
659 .header("X-Auth-Token", &self.auth_token)
660 .body(movie_data)
661 .send()
662 .await?;
663
664 match response.status() {
665 StatusCode::OK => {
666 let response_text = response.text().await?;
667 let response_json: serde_json::Value = serde_json::from_str(&response_text)?;
668 if let Some(id) = response_json["id"].as_u64() {
669 Ok(id as u32)
670 } else {
671 Err(anyhow!("Failed to get movie ID from response"))
672 }
673 }
674 _ => Err(anyhow!(
675 "Failed to upload movie with status: {}",
676 response.status()
677 )),
678 }
679 }
680
681 pub async fn turn_on(&self) -> anyhow::Result<VerifyResponse> {
683 let mode_response: DeviceMode = self.get_mode().await?;
685 let current_mode = mode_response;
686
687 if current_mode != DeviceMode::Off {
689 return Ok(VerifyResponse {
690 code: traits::OK.code,
691 });
692 }
693
694 self.set_mode(DeviceMode::Movie).await
697 }
698
699 pub async fn turn_off(&self) -> anyhow::Result<VerifyResponse> {
701 self.set_mode(DeviceMode::Off).await
703 }
704
705 pub async fn set_mode(&self, mode: DeviceMode) -> anyhow::Result<VerifyResponse> {
707 let url = format!("http://{}/xled/v1/led/mode", self.host);
708 let response = self
709 .client
710 .post(&url)
711 .header("X-Auth-Token", &self.auth_token)
712 .json(&json!({ "mode": mode.to_string() }))
713 .send()
714 .await
715 .context("Failed to set mode")?;
716
717 if response.status() == StatusCode::OK {
718 let mode_response = response.json::<VerifyResponse>().await?;
719 Ok(mode_response)
720 } else {
721 Err(anyhow::anyhow!(
722 "Failed to set mode with status: {}",
723 response.status()
724 ))
725 }
726 }
727
728 pub async fn set_brightness(&self, brightness: i32) -> anyhow::Result<()> {
734 let url = format!("http://{}/xled/v1/led/out/brightness", self.host);
735 let response = self
736 .client
737 .post(&url)
738 .header("X-Auth-Token", &self.auth_token)
739 .json(&json!({ "mode": "enabled", "type": "A", "value": brightness }))
740 .send()
741 .await
742 .context("Failed to set brightness")?;
743
744 if response.status() == StatusCode::OK {
745 Ok(())
746 } else {
747 Err(anyhow::anyhow!(
748 "Failed to set the brightness with status: {}",
749 response.status()
750 ))
751 }
752 }
753
754 async fn authenticate(client: &Client, host: &str, hw_address: &str) -> anyhow::Result<String> {
755 let challenge = Auth::generate_challenge();
757
758 let challenge_response = send_challenge(client, host, &challenge).await?;
760
761 let response = Auth::make_challenge_response(&challenge, hw_address)?;
763
764 send_verify(
766 client,
767 host,
768 &challenge_response.authentication_token,
769 &response,
770 )
771 .await?;
772
773 Ok(challenge_response.authentication_token)
774 }
775
776 pub async fn get_mode(&self) -> anyhow::Result<DeviceMode> {
777 let url = format!("http://{}/xled/v1/led/mode", self.host);
778 let response = self
779 .client
780 .get(&url)
781 .header("X-Auth-Token", &self.auth_token)
782 .send()
783 .await
784 .context("Failed to get mode")?;
785
786 match response.status() {
787 StatusCode::OK => {
788 let mode_response = response.json::<ModeResponse>().await?;
789 println!("Mode response: {:#?}", mode_response);
790 println!("Mode: {}", mode_response.mode);
791 let mode = DeviceMode::from_str(&mode_response.mode)
792 .map_err(|e| anyhow!("Failed to parse mode: {}", e))?;
793 Ok(mode)
794 }
795 _ => Err(anyhow::anyhow!(
796 "Failed to get mode with status: {}",
797 response.status()
798 )),
799 }
800 }
801
802 pub async fn get_brightness(&self) -> anyhow::Result<BrightnessResponse> {
803 let url = format!("http://{}/xled/v1/led/out/brightness", self.host);
804 let response = self
805 .client
806 .get(&url)
807 .header("X-Auth-Token", &self.auth_token)
808 .send()
809 .await
810 .context("Failed to get brightness")?;
811
812 match response.status() {
813 StatusCode::OK => {
814 let mode_response = response.json::<BrightnessResponse>().await?;
815 println!("Brightness response: {:#?}", mode_response);
816 println!("Brightness: {}", mode_response.value);
817 Ok(mode_response)
818 }
819 _ => Err(anyhow::anyhow!(
820 "Failed to get brightness with status: {}",
821 response.status()
822 )),
823 }
824 }
825
826 pub async fn get_timer(&self) -> anyhow::Result<TimerResponse> {
827 let url = format!("http://{}/xled/v1/timer", self.host);
828 let response = self
829 .client
830 .get(&url)
831 .header("X-Auth-Token", &self.auth_token)
832 .send()
833 .await
834 .context("Failed to get timer")?;
835
836 match response.status() {
837 StatusCode::OK => {
838 let timer_response = response.json::<TimerResponse>().await?;
839 Ok(timer_response)
840 }
841 _ => Err(anyhow::anyhow!(
842 "Failed to get timer with status: {}",
843 response.status()
844 )),
845 }
846 }
847
848 pub async fn set_formatted_timer(
849 &self,
850 time_on_str: &str,
851 time_off_str: &str,
852 ) -> anyhow::Result<()> {
853 let time_on = NaiveTime::parse_from_str(time_on_str, "%H:%M:%S")
855 .or_else(|_| NaiveTime::parse_from_str(time_on_str, "%H:%M"))
856 .context("Failed to parse time_on string")?;
857 let time_off = NaiveTime::parse_from_str(time_off_str, "%H:%M:%S")
858 .or_else(|_| NaiveTime::parse_from_str(time_off_str, "%H:%M"))
859 .context("Failed to parse time_off string")?;
860
861 let time_on_seconds = time_on.num_seconds_from_midnight() as i32;
863 let time_off_seconds = time_off.num_seconds_from_midnight() as i32;
864
865 let url = format!("http://{}/xled/v1/timer", self.host);
867
868 let response = self
870 .client
871 .post(&url)
872 .header("X-Auth-Token", &self.auth_token)
873 .json(&json!({
874 "time_on": time_on_seconds,
875 "time_off": time_off_seconds,
876 }))
877 .send()
878 .await
879 .context("Failed to set timer")?;
880
881 if response.status() == StatusCode::OK {
883 Ok(())
884 } else {
885 Err(anyhow::anyhow!(
886 "Failed to set timer with status: {}",
887 response.status()
888 ))
889 }
890 }
891
892 pub async fn get_playlist(&self) -> anyhow::Result<PlaylistResponse> {
893 let url = format!("http://{}/xled/v1/playlist", self.host);
894 let response = self
895 .client
896 .get(&url)
897 .header("X-Auth-Token", &self.auth_token)
898 .send()
899 .await?;
900
901 match response.status() {
902 StatusCode::OK => {
903 let response = response.text().await?;
904 println!("Response: {}", response);
905 let playlist_response: PlaylistResponse = serde_json::from_str(&response)?;
906 Ok(playlist_response)
908 }
909 _ => Err(response.error_for_status().unwrap_err().into()),
910 }
911 }
912
913 pub async fn fetch_layout(&self) -> anyhow::Result<LayoutResponse> {
915 let url = format!("http://{}/xled/v1/led/layout/full", self.host);
916 let response = self
917 .client
918 .get(&url)
919 .header("X-Auth-Token", &self.auth_token)
920 .send()
921 .await
922 .context("Failed to fetch layout")?;
923
924 if response.status() == StatusCode::OK {
925 let layout_response = response
926 .json::<LayoutResponse>()
927 .await
928 .context("Failed to deserialize layout response")?;
929 Ok(layout_response)
930 } else {
931 Err(anyhow::anyhow!(
932 "Failed to fetch layout with status: {}",
933 response.status()
934 ))
935 }
936 }
937
938 pub async fn get_device_capacity(&self) -> anyhow::Result<usize> {
939 let url = format!("http://{}/xled/v1/led/movies", self.host);
940 let response = self
941 .client
942 .get(&url)
943 .header("X-Auth-Token", &self.auth_token)
944 .send()
945 .await?;
946
947 match response.status() {
948 StatusCode::OK => {
949 let response_json = response.json::<serde_json::Value>().await?;
950 if let Some(available_frames) = response_json["available_frames"].as_u64() {
951 Ok(available_frames as usize)
952 } else {
953 Err(anyhow!("Failed to get available frames from response"))
954 }
955 }
956 _ => Err(anyhow!(
957 "Failed to get device capacity with status: {}",
958 response.status()
959 )),
960 }
961 }
962
963 pub async fn clear_movies(&self) -> anyhow::Result<()> {
965 let url = format!("http://{}/xled/v1/led/movies", self.host);
966 let response = self
967 .client
968 .delete(&url)
969 .header("X-Auth-Token", &self.auth_token)
970 .send()
971 .await?;
972
973 match response.status() {
974 StatusCode::NO_CONTENT => Ok(()),
975 _ => Err(anyhow!(
976 "Failed to clear movies with status: {}",
977 response.status()
978 )),
979 }
980 }
981
982 pub fn to_movie(frames: Vec<Vec<(u8, u8, u8)>>, led_profile: LedProfile) -> Vec<u8> {
985 let mut movie_data = Vec::new();
986 for frame in frames {
987 for &(r, g, b) in &frame {
988 match led_profile {
989 LedProfile::RGB => {
990 movie_data.push(r);
991 movie_data.push(g);
992 movie_data.push(b);
993 }
994 LedProfile::RGBW => {
995 let w = r.min(g).min(b);
997 movie_data.push(r - w);
998 movie_data.push(g - w);
999 movie_data.push(b - w);
1000 movie_data.push(w);
1001 }
1002 }
1003 }
1004 }
1005 movie_data
1006 }
1007
1008 }
1010
1011#[derive(Derivative)]
1013#[derivative(PartialEq)]
1014#[derive(Deserialize, Debug, Clone, Copy)]
1015#[allow(non_snake_case)]
1016pub struct DevicePower {
1017 pub mA: i64,
1019
1020 pub mV: i64,
1022}
1023
1024#[allow(non_snake_case)]
1025impl DevicePower {
1026 pub fn mW(&self) -> i64 {
1028 (self.mA * self.mV) / 1_000
1029 }
1030}
1031
1032#[derive(Derivative)]
1034#[derivative(PartialEq)]
1035#[derive(Deserialize, Debug, Clone)]
1036pub struct DeviceInfoResponse {
1037 pub product_name: String,
1038 pub hardware_version: String,
1039 pub bytes_per_led: usize,
1040 pub hw_id: String,
1041 pub flash_size: Option<usize>,
1043 pub led_type: usize,
1044 pub product_code: String,
1045 pub fw_family: String,
1046 pub device_name: String,
1047
1048 #[derivative(PartialEq = "ignore")]
1050 #[serde(deserialize_with = "deserialize_duration_millis")]
1052 pub uptime: Duration,
1053
1054 pub mac: String,
1055 pub uuid: String,
1056 pub max_supported_led: usize,
1057 pub number_of_led: usize,
1058
1059 #[derivative(PartialEq = "ignore")]
1062 pub pwr: Option<DevicePower>,
1063
1064 pub led_profile: LedProfile,
1066 pub frame_rate: f64,
1067
1068 #[derivative(PartialEq = "ignore")]
1070 pub measured_frame_rate: f64,
1071
1072 pub movie_capacity: usize,
1073 pub max_movies: usize,
1074 pub wire_type: usize,
1075 pub copyright: String,
1076 pub code: usize,
1077}
1078
1079impl PartialOrd for DeviceInfoResponse {
1080 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1082 Some(self.uuid.cmp(&other.uuid))
1083 }
1084}
1085
1086impl ResponseCodeTrait for DeviceInfoResponse {
1087 fn response_code(&self) -> ResponseCode {
1088 Self::map_response_code(self.code as u32)
1089 }
1090}
1091
1092fn deserialize_duration_millis<'de, D>(deserializer: D) -> anyhow::Result<Duration, D::Error>
1093where
1094 D: Deserializer<'de>,
1095{
1096 let millis_str: String = Deserialize::deserialize(deserializer)?;
1097 millis_str
1098 .parse::<u64>()
1099 .map(Duration::from_millis)
1100 .map_err(serde::de::Error::custom)
1101}
1102
1103#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
1104#[serde(rename_all = "UPPERCASE")]
1105pub enum LedProfile {
1106 RGB,
1107 RGBW,
1108 }
1110
1111#[derive(Serialize, Deserialize, Debug)]
1112pub struct PlaylistEntry {
1113 pub id: u32,
1114 pub unique_id: String,
1115 pub name: String,
1116 pub duration: u32,
1117 pub handle: u32,
1118}
1119
1120#[derive(Serialize, Deserialize, Debug)]
1121pub struct PlaylistResponse {
1122 pub entries: Vec<PlaylistEntry>,
1123 pub unique_id: String,
1124 pub name: String,
1125 pub code: u32,
1126}
1127
1128impl ResponseCodeTrait for PlaylistResponse {
1129 fn response_code(&self) -> ResponseCode {
1130 Self::map_response_code(self.code)
1131 }
1132}
1133
1134#[derive(Serialize, Deserialize, Debug)]
1135pub struct ModeResponse {
1136 pub mode: String,
1137 pub code: u32,
1138}
1139
1140impl ResponseCodeTrait for ModeResponse {
1141 fn response_code(&self) -> ResponseCode {
1142 Self::map_response_code(self.code)
1143 }
1144}
1145
1146#[derive(Serialize, Deserialize, Debug)]
1147pub struct TimerResponse {
1148 pub time_now: i32,
1149 pub time_off: i32,
1150 pub time_on: i32,
1151 pub code: u32,
1152}
1153
1154impl ResponseCodeTrait for TimerResponse {
1155 fn response_code(&self) -> ResponseCode {
1156 Self::map_response_code(self.code)
1157 }
1158}
1159
1160#[derive(Deserialize, Debug, PartialEq, Copy, Clone)]
1161pub struct LedCoordinate {
1162 pub x: f64,
1163 pub y: f64,
1164 pub z: f64,
1165}
1166
1167#[derive(Deserialize, Debug, PartialEq, Clone)]
1169pub struct LayoutResponse {
1170 pub source: String,
1171 pub synthesized: bool,
1172 pub uuid: String,
1173 pub coordinates: Vec<LedCoordinate>,
1174 pub code: u32,
1175}
1176
1177impl ResponseCodeTrait for LayoutResponse {
1178 fn response_code(&self) -> ResponseCode {
1179 Self::map_response_code(self.code)
1180 }
1181}
1182
1183pub enum Axis {
1184 X,
1185 Y,
1186 Z,
1187}
1188
1189#[derive(Serialize, Deserialize, Debug)]
1190pub struct Challenge {
1191 challenge: String,
1192}
1193
1194#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1195pub struct RGB {
1196 pub red: u8,
1197 pub green: u8,
1198 pub blue: u8,
1199}
1200
1201#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1202pub struct RgbJsonLFormat {
1203 pub red: u8,
1204 pub green: u8,
1205 pub blue: u8,
1206}
1207
1208impl From<(u8, u8, u8)> for RGB {
1210 fn from(tuple: (u8, u8, u8)) -> Self {
1211 RGB {
1212 red: tuple.0,
1213 green: tuple.1,
1214 blue: tuple.2,
1215 }
1216 }
1217}
1218
1219impl From<RGB> for (u8, u8, u8) {
1220 fn from(rgb: RGB) -> Self {
1221 (rgb.red, rgb.green, rgb.blue)
1222 }
1223}
1224
1225#[derive(Debug, Clone, Copy, ValueEnum)]
1226pub enum CliColors {
1227 Red,
1228 Green,
1229 Blue,
1230 Yellow,
1231 Orange,
1232 Purple,
1233 Cyan,
1234 Magenta,
1235 Lime,
1236 Pink,
1237 Teal,
1238 Lavender,
1239 Brown,
1240 Beige,
1241 Maroon,
1242 Mint,
1243}
1244
1245impl FromStr for CliColors {
1246 type Err = anyhow::Error;
1247
1248 fn from_str(s: &str) -> Result<Self, Self::Err> {
1249 match s.to_lowercase().as_str() {
1250 "red" => Ok(CliColors::Red),
1251 "green" => Ok(CliColors::Green),
1252 "blue" => Ok(CliColors::Blue),
1253 "yellow" => Ok(CliColors::Yellow),
1254 "orange" => Ok(CliColors::Orange),
1255 "purple" => Ok(CliColors::Purple),
1256 "cyan" => Ok(CliColors::Cyan),
1257 "magenta" => Ok(CliColors::Magenta),
1258 "lime" => Ok(CliColors::Lime),
1259 "pink" => Ok(CliColors::Pink),
1260 "teal" => Ok(CliColors::Teal),
1261 "lavender" => Ok(CliColors::Lavender),
1262 "brown" => Ok(CliColors::Brown),
1263 "beige" => Ok(CliColors::Beige),
1264 "maroon" => Ok(CliColors::Maroon),
1265 "mint" => Ok(CliColors::Mint),
1266 _ => Err(anyhow!("Invalid color")),
1267 }
1268 }
1269}
1270
1271impl From<CliDeviceMode> for DeviceMode {
1272 fn from(mode: CliDeviceMode) -> Self {
1273 match mode {
1274 CliDeviceMode::Movie => DeviceMode::Movie,
1275 CliDeviceMode::Playlist => DeviceMode::Playlist,
1276 CliDeviceMode::RealTime => DeviceMode::RealTime,
1277 CliDeviceMode::Demo => DeviceMode::Demo,
1278 CliDeviceMode::Effect => DeviceMode::Effect,
1279 CliDeviceMode::Color => DeviceMode::Color,
1280 CliDeviceMode::Off => DeviceMode::Off,
1281 }
1282 }
1283}
1284
1285impl From<CliColors> for RGB {
1286 fn from(color: CliColors) -> Self {
1287 match color {
1288 CliColors::Red => RGB {
1289 red: 255,
1290 green: 0,
1291 blue: 0,
1292 },
1293 CliColors::Green => RGB {
1294 red: 0,
1295 green: 255,
1296 blue: 0,
1297 },
1298 CliColors::Blue => RGB {
1299 red: 0,
1300 green: 0,
1301 blue: 255,
1302 },
1303 CliColors::Yellow => RGB {
1304 red: 255,
1305 green: 255,
1306 blue: 0,
1307 },
1308 CliColors::Orange => RGB {
1309 red: 255,
1310 green: 165,
1311 blue: 0,
1312 },
1313 CliColors::Purple => RGB {
1314 red: 128,
1315 green: 0,
1316 blue: 128,
1317 },
1318 CliColors::Cyan => RGB {
1319 red: 0,
1320 green: 255,
1321 blue: 255,
1322 },
1323 CliColors::Magenta => RGB {
1324 red: 255,
1325 green: 0,
1326 blue: 255,
1327 },
1328 CliColors::Lime => RGB {
1329 red: 50,
1330 green: 205,
1331 blue: 50,
1332 },
1333 CliColors::Pink => RGB {
1334 red: 255,
1335 green: 192,
1336 blue: 203,
1337 },
1338 CliColors::Teal => RGB {
1339 red: 0,
1340 green: 128,
1341 blue: 128,
1342 },
1343 CliColors::Lavender => RGB {
1344 red: 230,
1345 green: 230,
1346 blue: 250,
1347 },
1348 CliColors::Brown => RGB {
1349 red: 165,
1350 green: 42,
1351 blue: 42,
1352 },
1353 CliColors::Beige => RGB {
1354 red: 245,
1355 green: 245,
1356 blue: 220,
1357 },
1358 CliColors::Maroon => RGB {
1359 red: 128,
1360 green: 0,
1361 blue: 0,
1362 },
1363 CliColors::Mint => RGB {
1364 red: 189,
1365 green: 252,
1366 blue: 201,
1367 },
1368 }
1369 }
1370}
1371
1372async fn send_verify(
1373 client: &Client,
1374 ip: &str,
1375 auth_token: &str,
1376 challenge_response: &str,
1377) -> anyhow::Result<()> {
1378 let verify_url = format!("http://{}/xled/v1/verify", ip);
1379
1380 let response = client
1381 .post(&verify_url)
1382 .header("X-Auth-Token", auth_token)
1383 .json(&json!({ "challenge-response": challenge_response }))
1384 .send()
1385 .await
1386 .context("Failed to send verification")?;
1387
1388 match response.status() {
1389 StatusCode::OK => {
1390 let verify_response = response
1391 .json::<VerifyResponse>()
1392 .await
1393 .context("Failed to deserialize verify response")?;
1394 if verify_response.response_code().is_ok() {
1395 Ok(())
1397 } else {
1398 Err(anyhow::anyhow!(
1399 "Verification failed with code: {}",
1400 verify_response.code
1401 ))
1402 }
1403 }
1404 _ => {
1405 let error_msg = format!("Verification failed with status: {}", response.status());
1406 Err(anyhow::anyhow!(error_msg))
1407 }
1408 }
1409}
1410
1411#[derive(Serialize, Deserialize, Debug)]
1412struct LoginResponse {
1413 authentication_token: String,
1414 #[serde(rename = "challenge-response")]
1415 challenge_response: String,
1416 code: u32,
1417}
1418
1419impl ResponseCodeTrait for LoginResponse {
1420 fn response_code(&self) -> ResponseCode {
1421 Self::map_response_code(self.code)
1422 }
1423}
1424
1425#[derive(Serialize, Deserialize, Debug)]
1427pub struct VerifyResponse {
1428 code: u32,
1429}
1430
1431impl ResponseCodeTrait for VerifyResponse {
1432 fn response_code(&self) -> ResponseCode {
1433 Self::map_response_code(self.code)
1434 }
1435}
1436
1437#[derive(Serialize, Deserialize, Debug)]
1438struct ChallengeResponse {
1439 #[serde(rename = "challenge-response")]
1440 challenge_response: String,
1441 authentication_token: String,
1442 authentication_token_expires_in: Option<i32>,
1444}
1445
1446#[derive(Serialize, Deserialize, Debug)]
1447struct Mode {
1448 mode: String,
1449}
1450
1451pub fn generate_color_wheel_gradient(num_leds: usize, offset: usize) -> Vec<(u8, u8, u8)> {
1452 (0..num_leds)
1453 .map(|i| {
1454 let offset_index = (i + offset) % num_leds;
1456 let hue = offset_index as f32 / num_leds as f32 * 360.0;
1458 let hsl_color = Hsl::new(hue, 1.0, 0.5);
1460 let rgb_color = Srgb::from_color(hsl_color);
1462 let (r, g, b) = rgb_color.into_components();
1464 ((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
1465 })
1466 .collect()
1467}
1468
1469fn generate_color_gradient_along_axis(
1470 leds: &[LedCoordinate],
1471 axis: Axis,
1472 offset: f64,
1473) -> Vec<(u8, u8, u8)> {
1474 assert!(
1475 (0.0..1.0).contains(&offset),
1476 "Offset must be in the range [0.0, 1.0)"
1477 );
1478
1479 let (min_value, max_value) = match axis {
1481 Axis::X => leds
1482 .iter()
1483 .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), led| {
1484 (min.min(led.x), max.max(led.x))
1485 }),
1486 Axis::Y => leds
1487 .iter()
1488 .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), led| {
1489 (min.min(led.y), max.max(led.y))
1490 }),
1491 Axis::Z => leds
1492 .iter()
1493 .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), led| {
1494 (min.min(led.z), max.max(led.z))
1495 }),
1496 };
1497
1498 let total_range = max_value - min_value;
1500
1501 let offset_value = total_range * offset;
1503
1504 leds.iter()
1506 .map(|led| {
1507 let position = match axis {
1509 Axis::X => led.x,
1510 Axis::Y => led.y,
1511 Axis::Z => led.z,
1512 };
1513
1514 let adjusted_position = (position - min_value + offset_value) % total_range;
1516 let hue = (adjusted_position / total_range) * 360.0;
1517
1518 let hsl_color = Hsl::new(hue as f32, 1.0, 0.5);
1520
1521 let rgb_color: Srgb = hsl_color.into_color();
1523
1524 let (r, g, b) = rgb_color.into_components();
1526 ((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
1527 })
1528 .collect()
1529}
1530
1531async fn send_challenge(
1532 client: &Client,
1533 ip: &str,
1534 challenge: &[u8],
1535) -> anyhow::Result<ChallengeResponse> {
1536 let login_url = format!("http://{}/xled/v1/login", ip);
1537 let challenge_b64 = STANDARD.encode(challenge);
1538
1539 let response = client
1540 .post(&login_url)
1541 .json(&Challenge {
1542 challenge: challenge_b64,
1543 })
1544 .send()
1545 .await
1546 .context("Failed to send authentication challenge")?;
1547
1548 if response.status() != 200 {
1549 anyhow::bail!(
1550 "Authentication challenge failed with status: {}",
1551 response.status()
1552 );
1553 }
1554
1555 let content = response.text().await?;
1557 let challenge_response: ChallengeResponse =
1559 serde_json::from_str(&content).context("Failed to deserialize challenge response")?;
1560
1561 Ok(challenge_response)
1562}
1563
1564#[derive(Clone, Copy, PartialEq, Eq, ValueEnum)]
1565pub enum RtStdinFormat {
1566 Binary,
1567 JsonLines,
1569}
1570
1571#[derive(Clone, Copy, PartialEq, Eq, ValueEnum)]
1572pub enum RtStdinErrorMode {
1573 IgnoreInvalidAddress,
1574 ModInvalidAddress,
1575 StopInvalidAddress,
1576}
1577
1578#[derive(Clone, Copy, Debug)]
1579pub struct BinaryStreamFormat {
1580 pub led_address: u16,
1581 pub red: u8,
1582 pub green: u8,
1583 pub blue: u8,
1584}
1585
1586#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
1587pub struct AddressableLed {
1588 pub address: u16,
1589 pub color: RGB,
1590}
1591
1592#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
1593pub struct AddressableLedJsonLFormat {
1594 pub address: u16,
1595 pub color: RgbJsonLFormat,
1596}
1597
1598impl From<AddressableLedJsonLFormat> for AddressableLed {
1599 fn from(data: AddressableLedJsonLFormat) -> Self {
1600 AddressableLed {
1601 address: data.address,
1602 color: RGB {
1603 red: data.color.red,
1604 green: data.color.green,
1605 blue: data.color.blue,
1606 },
1607 }
1608 }
1609}
1610
1611impl AddressableLed {
1612 pub fn merge_frame_array(new_values: &Vec<AddressableLed>, old_frame: &mut [(u8, u8, u8)]) {
1613 for led in new_values {
1614 let (r, g, b) = led.color.into();
1615 old_frame[led.address as usize] = (r, g, b);
1616 }
1617 }
1618}
1619
1620impl From<BinaryStreamFormat> for AddressableLed {
1622 fn from(data: BinaryStreamFormat) -> Self {
1623 AddressableLed {
1624 address: data.led_address,
1625 color: RGB {
1626 red: data.red,
1627 green: data.green,
1628 blue: data.blue,
1629 },
1630 }
1631 }
1632}