megatec_ups_control/
lib.rs

1use rusb::{Context, DeviceHandle, Error as UsbError, UsbContext};
2use std::time::Duration;
3use thiserror::Error;
4
5#[derive(Debug, Error)]
6pub enum UpsError {
7    #[error("USB error: {0}")]
8    Usb(#[from] UsbError),
9    #[error("Invalid response")]
10    InvalidResponse,
11    #[error("Invalid time value")]
12    InvalidTime,
13}
14
15pub type Result<T> = std::result::Result<T, UpsError>;
16
17const ASCII_MIN: u8 = 32;
18const ASCII_MAX: u8 = 126;
19const CHAR_QUOTE: u8 = 34;
20const CHAR_BACKTICK: u8 = 96;
21const CHAR_PAREN: u8 = 40;
22
23/// Main structure for interacting with a Megatec UPS device
24pub struct MegatecUps {
25    handle: DeviceHandle<Context>,
26    context: Context,
27}
28
29impl MegatecUps {
30    /// Create a new UPS connection using vendor_id and product_id
31    pub fn new(vendor_id: u16, product_id: u16) -> Result<Self> {
32        let context = Context::new()?;
33        let handle = context
34            .open_device_with_vid_pid(vendor_id, product_id)
35            .ok_or(UpsError::InvalidResponse)?;
36
37        Ok(Self { handle, context })
38    }
39
40    /// Get a string descriptor from the device
41    fn get_string_descriptor(&self, index: u8, length: u16) -> Result<String> {
42        let mut data = vec![0u8; length as usize];
43        let result = self.handle.read_control(
44            rusb::request_type(
45                rusb::Direction::In,
46                rusb::RequestType::Standard,
47                rusb::Recipient::Device,
48            ),
49            rusb::constants::LIBUSB_REQUEST_GET_DESCRIPTOR,
50            (rusb::constants::LIBUSB_DT_STRING as u16) << 8 | index as u16,
51            0,
52            &mut data,
53            Duration::from_secs(1),
54        )?;
55
56        if result >= 3 {
57            let filtered: String = data
58                .into_iter()
59                .filter(|&c| Self::is_valid_char(c))
60                .map(|c| c as char)
61                .collect();
62            Ok(filtered)
63        } else {
64            Err(UpsError::InvalidResponse)
65        }
66    }
67
68    /// Check if a character is valid according to protocol rules
69    fn is_valid_char(c: u8) -> bool {
70        c >= ASCII_MIN && c <= ASCII_MAX && c != CHAR_QUOTE && c != CHAR_BACKTICK && c != CHAR_PAREN
71    }
72
73    /// Get the UPS name
74    pub fn get_name(&self) -> Result<String> {
75        self.get_string_descriptor(2, 256)
76    }
77
78    /// Get the UPS status with acknowledgment
79    pub fn get_status(&self) -> Result<UpsStatus> {
80        // First request for acknowledgment
81        let _ = self.get_string_descriptor(3, 256)?;
82        std::thread::sleep(Duration::from_secs(1));
83
84        // Second request for actual status
85        let status_str = self.get_string_descriptor(3, 256)?;
86        UpsStatus::from_str(&status_str)
87    }
88
89    /// Get the UPS status without acknowledgment
90    pub fn get_status_no_ack(&self) -> Result<UpsStatus> {
91        let status_str = self.get_string_descriptor(3, 256)?;
92        UpsStatus::from_str(&status_str)
93    }
94
95    /// Test UPS for 10 seconds
96    pub fn test(&self) -> Result<()> {
97        self.get_string_descriptor(4, 256)?;
98        Ok(())
99    }
100
101    /// Test UPS until battery is low
102    pub fn test_until_battery_low(&self) -> Result<()> {
103        self.get_string_descriptor(5, 256)?;
104        Ok(())
105    }
106
107    /// Test UPS for specified minutes
108    pub fn test_with_time(&self, minutes: u8) -> Result<()> {
109        let calculated_time = Self::calculate_time(minutes)?;
110        self.get_string_descriptor(6, calculated_time)?;
111        Ok(())
112    }
113
114    /// Toggle UPS beep
115    pub fn switch_beep(&self) -> Result<()> {
116        self.get_string_descriptor(7, 256)?;
117        Ok(())
118    }
119
120    /// Abort current UPS test
121    pub fn abort_test(&self) -> Result<()> {
122        self.get_string_descriptor(11, 256)?;
123        Ok(())
124    }
125
126    /// Get UPS rating information
127    pub fn get_rating(&self) -> Result<String> {
128        self.get_string_descriptor(13, 256)
129    }
130
131    /// Shutdown UPS after 1 minute
132    pub fn shutdown(&self) -> Result<()> {
133        self.get_string_descriptor(105, 2460)?;
134        Ok(())
135    }
136
137    /// Calculate the protocol-specific time value for the test duration
138    fn calculate_time(minutes: u8) -> Result<u16> {
139        if minutes == 0 || minutes > 99 {
140            return Err(UpsError::InvalidTime);
141        }
142
143        let value = match minutes {
144            1..=9 => 100 + minutes,
145            10..=19 => 125 + (minutes - 19),
146            20..=99 => {
147                let range_start = ((minutes - 20) / 10) * 10 + 20;
148                132 + ((minutes - range_start) * 7)
149            }
150            _ => return Err(UpsError::InvalidTime),
151        };
152
153        Ok(value as u16)
154    }
155}
156
157/// Structure representing the UPS status values
158#[derive(Debug, Clone)]
159pub struct UpsStatus {
160    pub input_voltage: f64,
161    pub input_fault_voltage: f64,
162    pub output_voltage: f64,
163    pub output_current: f64,
164    pub input_frequency: f64,
165    pub battery_voltage: f64,
166    pub temperature: f64,
167}
168
169impl UpsStatus {
170    /// Parse status string into UpsStatus struct
171    fn from_str(status: &str) -> Result<Self> {
172        let values: Vec<f64> = status
173            .split_whitespace()
174            .take(7)
175            .map(|s| s.parse::<f64>())
176            .collect::<std::result::Result<Vec<f64>, _>>()
177            .map_err(|_| UpsError::InvalidResponse)?;
178
179        if values.len() != 7 {
180            return Err(UpsError::InvalidResponse);
181        }
182
183        Ok(Self {
184            input_voltage: values[0],
185            input_fault_voltage: values[1],
186            output_voltage: values[2],
187            output_current: values[3],
188            input_frequency: values[4],
189            battery_voltage: values[5],
190            temperature: values[6],
191        })
192    }
193}
194
195impl Drop for MegatecUps {
196    fn drop(&mut self) {
197        if let Ok(new_context) = Context::new() {
198            let _old_context = std::mem::replace(&mut self.context, new_context);
199        }
200    }
201}