glow_control_lib/control_interface/
mod.rs

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
36/// Twinkly hardware version.
37pub 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
52/**
53Compare everything, except the client, since this that is a utility, and not a device describing
54identifier. Also ignoring the token, since this changes on every new connection,
55while the device stays the same.
56 */
57impl 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
65/**
66Sort in that order:
671. device_info
682. host
693. hw_address
70 */
71impl 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/// Brightness response when getting brightness.
107#[derive(Debug, Clone, Deserialize)]
108pub struct BrightnessResponse {
109    /// Something like `1000`.
110    pub code: u32,
111    /// Either "enabled" or "disabled".
112    pub mode: String,
113    /// Range inside 0..100.
114    pub value: i32,
115}
116
117impl BrightnessResponse {
118    /// If the mode signals that the devices is enabled.
119    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        // Fetch the device information
177        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    /**
200    Updates the authentication token, after a device re-authenticated.
201     */
202    pub fn with_auth_token(mut self, auth_token: String) -> Self {
203        self.auth_token = auth_token;
204        self
205    }
206
207    /**
208    Creates a mock / demo [DeviceInfoResponse].
209    A utility function for [Self::new_mock_control_interface].
210     */
211    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    /**
248    Creates a mock / demo [ControlInterface] with a demo [DeviceInfoResponse].
249    A utility function [Self::new_mock_device_info_response] exists,
250    to easily create a valid [DeviceInfoResponse].
251    */
252    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    /**
268    Creates a [ControlInterface] by a [DeviceIdentifier].
269    */
270    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        // convert colors to glow_effects::util::color::RGB
315        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        // colors should be HashSet
324        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            // convert frame to Vec<(u8, u8, u8)>
342            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        // number of LEDs
380        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::Ascii => process_ascii_stream(reader)?,
390                    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            // sleep for the remaining time
416            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        // Read a line from the input stream
455        let mut line = String::new();
456        reader.read_line(&mut line)?;
457        // Deserialize the JSON line
458        let led: AddressableLedJsonLFormat = serde_json::from_str(&line)?;
459        // Convert the JSON format into the standard format
460        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 = generate_color_wheel_gradient(self.device_info.number_of_led, offset as usize);
476            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            // Increment the offset for the next frame
485            offset = (offset + step) % 1.0;
486
487            // Sleep for the interval duration to maintain the frame rate
488            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    /**
502    Set realtime frame with a provided socket.
503
504    # Return
505    Returns either the written bytes or an error.
506     */
507    pub async fn set_rt_frame_socket(
508        &self,
509        socket: &UdpSocket,
510        frame: &[u8],
511        version: HardwareVersion,
512    ) -> anyhow::Result<usize> {
513        // Determine the protocol version from the device configuration
514        // let version = self.device_info.fw_version; // Assuming fw_version is a field in DeviceInfoResponse
515
516        // Decode the access token
517        let access_token = STANDARD
518            .decode(&self.auth_token)
519            .context("Failed to decode access token")?;
520
521        // Prepare the packet based on the protocol version
522        let mut packet = BytesMut::new();
523        match version {
524            HardwareVersion::Version1 => {
525                packet.put_u8(1); // Protocol version 1
526                packet.extend_from_slice(&access_token);
527                packet.put_u8(self.device_info.number_of_led as u8); // Number of LEDs
528                packet.extend_from_slice(frame);
529            }
530            HardwareVersion::Version2 => {
531                packet.put_u8(2); // Protocol version 2
532                packet.extend_from_slice(&access_token);
533                packet.put_u8(0); // Placeholder byte
534                packet.extend_from_slice(frame);
535            }
536            _ => {
537                // Protocol version 3 or higher
538                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); // Protocol version 3
543                    packet.extend_from_slice(&access_token);
544                    packet.put_u16(0); // Placeholder bytes
545                    packet.put_u8(i as u8); // Frame index
546                    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); // Early return for version 3
558            }
559        }
560
561        // Send the packet for versions 1 and 2
562        socket.send(&packet).await.map_err(|err| anyhow!(err))
563    }
564
565    /**
566    Ensures the mode is [`DeviceMode::RealTime`], creates a UDP socket, connect it and send a frame to it.
567    For a continuous animation, without constant socket recreation and rebinding,
568    use [`Self::set_rt_frame_socket`] instead.
569
570    # Return
571    Returns either the written bytes or an error.
572     */
573    pub async fn show_rt_frame(&self, frame: &[u8]) -> anyhow::Result<usize> {
574        // Fetch the current mode from the device
575        let mode_response = self.get_mode().await?;
576        let current_mode = mode_response;
577
578        // Check if we need to switch to real-time mode
579        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        // Call the set_rt_frame_socket method to send the frame
586        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        // println!("Response: {}", response);
615        let device_info: DeviceInfoResponse = serde_json::from_str(&response)?;
616        // let device_info = response
617        //     .json::<DeviceInfoResponse>()
618        //     .await
619        //     .map_err(|e| anyhow!("Failed to deserialize device info: {}", e))?;
620
621        Ok(device_info)
622    }
623
624    /// Uploads a new movie to the device.
625    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        // Check if the movie fits in the remaining capacity
641        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        // Clear existing movies if necessary
647        if force {
648            self.clear_movies().await?;
649        }
650
651        // Convert the movie to the binary format expected by the device
652        let movie_data = Movie::to_movie(movie.frames, led_profile);
653
654        // Upload the movie to the device
655        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    /// Turns on the device by setting it to the last known mode or a default mode.
682    pub async fn turn_on(&self) -> anyhow::Result<VerifyResponse> {
683        // Fetch the current mode from the device
684        let mode_response: DeviceMode = self.get_mode().await?;
685        let current_mode = mode_response;
686
687        // If the device is already on, we don't need to change the mode
688        if current_mode != DeviceMode::Off {
689            return Ok(VerifyResponse {
690                code: traits::OK.code,
691            });
692        }
693
694        // If the device is off, set it to a default mode
695        // Here we choose "movie" as the default mode, but you can adjust as needed
696        self.set_mode(DeviceMode::Movie).await
697    }
698
699    /// Turns off the device and remembers the last non-real-time mode.
700    pub async fn turn_off(&self) -> anyhow::Result<VerifyResponse> {
701        // Set the device mode to "off"
702        self.set_mode(DeviceMode::Off).await
703    }
704
705    /// Helper method to set the device mode.
706    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    /// Helper method to set the device brightness.
729    ///
730    /// # Arguments
731    /// - `brightness`: The brightness value to set.
732    ///                 Range is 0..100.
733    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        // Generate a random challenge
756        let challenge = Auth::generate_challenge();
757
758        // Send the challenge to the device and get the response
759        let challenge_response = send_challenge(client, host, &challenge).await?;
760
761        // Create a challenge response based on the device's response and the MAC address
762        let response = Auth::make_challenge_response(&challenge, hw_address)?;
763
764        // Send the verification to the device
765        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        // Parse the time strings into NaiveTime objects
854        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        // Convert NaiveTime objects to seconds after midnight
862        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        // Construct the URL for setting the timer
866        let url = format!("http://{}/xled/v1/timer", self.host);
867
868        // Send the request to set the timer
869        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        // Check the response status
882        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                // let playlist_response = response.json::<PlaylistResponse>().await?;
907                Ok(playlist_response)
908            }
909            _ => Err(response.error_for_status().unwrap_err().into()),
910        }
911    }
912
913    /// Fetches the LED layout from the device.
914    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    /// Clears all uploaded movies from the device.
964    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    /// Converts a vector of frames into a binary movie format.
983    /// This function handles both RGB and RGBW LED profiles.
984    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                        // Calculate the white component as the minimum of r, g, b
996                        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    // ... other methods ...
1009}
1010
1011/// Define a struct to deserialize information about the power usage of the device.
1012#[derive(Derivative)]
1013#[derivative(PartialEq)]
1014#[derive(Deserialize, Debug, Clone, Copy)]
1015#[allow(non_snake_case)]
1016pub struct DevicePower {
1017    /// Power usage in Milliampere.
1018    pub mA: i64,
1019
1020    /// Power usage in Millivolt.
1021    pub mV: i64,
1022}
1023
1024#[allow(non_snake_case)]
1025impl DevicePower {
1026    /// Power usage in Milliwatt.
1027    pub fn mW(&self) -> i64 {
1028        (self.mA * self.mV) / 1_000
1029    }
1030}
1031
1032// Define a struct to deserialize the device information response
1033#[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    /// This only attribute which doesn't appear in Twinkly Squares, 2nd generation.
1042    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    // Ignore uptime for partial-equal, it changes over time, while the device stays the same.
1049    #[derivative(PartialEq = "ignore")]
1050    // Uptime is now an unsigned 64-bit integer
1051    #[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    /** Ignore power consumption for partial-equal,
1060    it may change over time, while the device stays the same. */
1061    #[derivative(PartialEq = "ignore")]
1062    pub pwr: Option<DevicePower>,
1063
1064    // LedProfile is now an enum
1065    pub led_profile: LedProfile,
1066    pub frame_rate: f64,
1067
1068    // The measured frame rate will change on the same device.
1069    #[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    /// Do a partial comparison by comparing their UUIDs.
1081    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    // Add other LED profiles as needed
1109}
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// Define a struct to deserialize the layout response
1168#[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
1208// implement From for (u8,u8,u8) to RGB and vice verse
1209impl 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                //   println!("Verify response code is 1000");
1396                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/// The response code in the response JSON, returned additionally to the returned HTTP Status code.
1426#[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    /// Seems to be always 1440. Since a Day has 1440 Minutes, this may mean: "10 Days"?
1443    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            // Calculate the index with offset, wrapping around using modulo if the offset is larger than num_leds
1455            let offset_index = (i + offset) % num_leds;
1456            // Calculate the hue for this LED, spreading the hues evenly across the color wheel
1457            let hue = offset_index as f32 / num_leds as f32 * 360.0;
1458            // Create an HSL color with full saturation and lightness for a fully saturated color
1459            let hsl_color = Hsl::new(hue, 1.0, 0.5);
1460            // Convert the HSL color to RGB
1461            let rgb_color = Srgb::from_color(hsl_color);
1462            // Convert the RGB color to 8-bit color components
1463            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    // Determine the range of the specified axis
1480    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    // Calculate the total range
1499    let total_range = max_value - min_value;
1500
1501    // Apply the offset to the range
1502    let offset_value = total_range * offset;
1503
1504    // Map each LED's position to a hue value and convert to RGB
1505    leds.iter()
1506        .map(|led| {
1507            // Determine the position of the LED on the specified axis
1508            let position = match axis {
1509                Axis::X => led.x,
1510                Axis::Y => led.y,
1511                Axis::Z => led.z,
1512            };
1513
1514            // Apply the offset and wrap around using modulo to ensure the gradient is continuous
1515            let adjusted_position = (position - min_value + offset_value) % total_range;
1516            let hue = (adjusted_position / total_range) * 360.0;
1517
1518            // Create an HSL color with full saturation and lightness for a fully saturated color
1519            let hsl_color = Hsl::new(hue as f32, 1.0, 0.5);
1520
1521            // Convert the HSL color to RGB
1522            let rgb_color: Srgb = hsl_color.into_color();
1523
1524            // Convert the RGB color to 8-bit color components
1525            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    // println!("Challenge response: {:?}", response);
1556    let content = response.text().await?;
1557    // println!("Challenge response content: {:?}", content);
1558    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    //  Ascii,
1568    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
1620// device AddressableLed from BinaryStreamFormat
1621impl 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}