Skip to main content

ht32_panel_client/
lib.rs

1//! D-Bus client library for communicating with the HT32 Panel Daemon.
2//!
3//! This crate provides a unified client for both CLI and applet use cases.
4
5use anyhow::{Context, Result};
6use tracing::debug;
7use zbus::{proxy, Connection};
8
9/// D-Bus bus type selection.
10#[derive(Debug, Clone, Copy, Default)]
11pub enum BusType {
12    /// Session bus (user session).
13    Session,
14    /// System bus (system-wide).
15    System,
16    /// Try session first, fall back to system.
17    #[default]
18    Auto,
19}
20
21/// D-Bus proxy for the HT32 Panel Daemon.
22#[proxy(
23    interface = "org.ht32panel.Daemon1",
24    default_service = "org.ht32panel.Daemon",
25    default_path = "/org/ht32panel/Daemon"
26)]
27trait Daemon1 {
28    /// Sets the display orientation.
29    fn set_orientation(&self, orientation: &str) -> zbus::Result<()>;
30
31    /// Gets the current orientation.
32    fn get_orientation(&self) -> zbus::Result<String>;
33
34    /// Clears the display to a solid color.
35    fn clear_display(&self, color: &str) -> zbus::Result<()>;
36
37    /// Sets the display face.
38    fn set_face(&self, face: &str) -> zbus::Result<()>;
39
40    /// Gets the current face name.
41    fn get_face(&self) -> zbus::Result<String>;
42
43    /// Sets LED parameters.
44    fn set_led(&self, theme: u8, intensity: u8, speed: u8) -> zbus::Result<()>;
45
46    /// Turns off LEDs.
47    fn led_off(&self) -> zbus::Result<()>;
48
49    /// Gets current LED settings as (theme, intensity, speed).
50    fn get_led_settings(&self) -> zbus::Result<(u8, u8, u8)>;
51
52    /// Gets the current color theme name.
53    fn get_theme(&self) -> zbus::Result<String>;
54
55    /// Sets the color theme by name.
56    fn set_theme(&self, name: &str) -> zbus::Result<()>;
57
58    /// Lists available color themes (IDs only).
59    fn list_themes(&self) -> zbus::Result<Vec<String>>;
60
61    /// Lists available color themes with display names (JSON-encoded).
62    fn list_themes_detailed(&self) -> zbus::Result<Vec<String>>;
63
64    /// Lists available faces (IDs only).
65    fn list_face_ids(&self) -> zbus::Result<Vec<String>>;
66
67    /// Lists available faces with display names (JSON-encoded).
68    fn list_faces(&self) -> zbus::Result<Vec<String>>;
69
70    /// Lists all available network interfaces.
71    fn list_network_interfaces(&self) -> zbus::Result<Vec<String>>;
72
73    /// Lists available complications for the current face.
74    /// Returns (id, name, description, enabled) tuples.
75    fn list_complications(&self) -> zbus::Result<Vec<(String, String, String, bool)>>;
76
77    /// Lists available complications with full details including options.
78    /// Returns JSON-encoded complication data.
79    fn list_complications_detailed(&self) -> zbus::Result<Vec<String>>;
80
81    /// Gets enabled complications for the current face.
82    fn get_enabled_complications(&self) -> zbus::Result<Vec<String>>;
83
84    /// Enables a complication for the current face.
85    fn enable_complication(&self, complication_id: &str) -> zbus::Result<()>;
86
87    /// Disables a complication for the current face.
88    fn disable_complication(&self, complication_id: &str) -> zbus::Result<()>;
89
90    /// Gets a complication option value.
91    fn get_complication_option(
92        &self,
93        complication_id: &str,
94        option_id: &str,
95    ) -> zbus::Result<String>;
96
97    /// Sets a complication option value.
98    fn set_complication_option(
99        &self,
100        complication_id: &str,
101        option_id: &str,
102        value: &str,
103    ) -> zbus::Result<()>;
104
105    /// Returns the current framebuffer as PNG data.
106    fn get_screen_png(&self) -> zbus::Result<Vec<u8>>;
107
108    /// Shuts down the daemon.
109    fn quit(&self) -> zbus::Result<()>;
110
111    /// Whether the LCD device is connected.
112    #[zbus(property)]
113    fn connected(&self) -> zbus::Result<bool>;
114
115    /// Whether the web UI is enabled.
116    #[zbus(property)]
117    fn web_enabled(&self) -> zbus::Result<bool>;
118
119    /// Current display orientation.
120    #[zbus(property)]
121    fn orientation(&self) -> zbus::Result<String>;
122
123    /// Current LED theme (1-5).
124    #[zbus(property)]
125    fn led_theme(&self) -> zbus::Result<u8>;
126
127    /// Current LED intensity (1-5).
128    #[zbus(property)]
129    fn led_intensity(&self) -> zbus::Result<u8>;
130
131    /// Current LED speed (1-5).
132    #[zbus(property)]
133    fn led_speed(&self) -> zbus::Result<u8>;
134
135    /// Current display face name.
136    #[zbus(property)]
137    fn face(&self) -> zbus::Result<String>;
138}
139
140/// D-Bus client wrapper for the daemon.
141pub struct DaemonClient {
142    proxy: Daemon1Proxy<'static>,
143}
144
145impl DaemonClient {
146    /// Attempts to connect to the daemon via D-Bus with auto bus detection.
147    ///
148    /// Tries session bus first, falls back to system bus.
149    pub async fn connect() -> Result<Self> {
150        Self::connect_with_bus(BusType::Auto).await
151    }
152
153    /// Attempts to connect to the daemon via D-Bus with specified bus type.
154    pub async fn connect_with_bus(bus_type: BusType) -> Result<Self> {
155        let connection = match bus_type {
156            BusType::Session => {
157                debug!("Connecting to session bus");
158                Connection::session()
159                    .await
160                    .context("Failed to connect to session bus")?
161            }
162            BusType::System => {
163                debug!("Connecting to system bus");
164                Connection::system()
165                    .await
166                    .context("Failed to connect to system bus")?
167            }
168            BusType::Auto => {
169                // Try session bus first, but verify the service exists
170                if let Ok(conn) = Connection::session().await {
171                    debug!("Connected to session bus, checking for daemon service");
172                    if Self::service_exists(&conn).await {
173                        debug!("Found daemon on session bus");
174                        conn
175                    } else {
176                        debug!("Daemon not on session bus, trying system bus");
177                        let sys_conn = Connection::system()
178                            .await
179                            .context("Failed to connect to system bus")?;
180                        if Self::service_exists(&sys_conn).await {
181                            debug!("Found daemon on system bus");
182                            sys_conn
183                        } else {
184                            // Neither bus has the service, return session for better error
185                            anyhow::bail!("Daemon service not found on session or system bus. Is ht32paneld running?")
186                        }
187                    }
188                } else {
189                    debug!("Session bus unavailable, trying system bus");
190                    Connection::system()
191                        .await
192                        .context("Failed to connect to any D-Bus")?
193                }
194            }
195        };
196
197        let proxy = Daemon1Proxy::new(&connection)
198            .await
199            .context("Failed to create D-Bus proxy")?;
200
201        Ok(Self { proxy })
202    }
203
204    /// Checks if the daemon service exists on the given connection.
205    async fn service_exists(conn: &Connection) -> bool {
206        use zbus::fdo::DBusProxy;
207        if let Ok(dbus_proxy) = DBusProxy::new(conn).await {
208            dbus_proxy
209                .name_has_owner("org.ht32panel.Daemon".try_into().unwrap())
210                .await
211                .unwrap_or(false)
212        } else {
213            false
214        }
215    }
216
217    /// Sets the display orientation.
218    pub async fn set_orientation(&self, orientation: &str) -> Result<()> {
219        self.proxy
220            .set_orientation(orientation)
221            .await
222            .context("Failed to set orientation via D-Bus")
223    }
224
225    /// Gets the current orientation.
226    pub async fn get_orientation(&self) -> Result<String> {
227        self.proxy
228            .get_orientation()
229            .await
230            .context("Failed to get orientation via D-Bus")
231    }
232
233    /// Clears the display to a solid color.
234    pub async fn clear_display(&self, color: &str) -> Result<()> {
235        self.proxy
236            .clear_display(color)
237            .await
238            .context("Failed to clear display via D-Bus")
239    }
240
241    /// Sets the display face.
242    pub async fn set_face(&self, face: &str) -> Result<()> {
243        self.proxy
244            .set_face(face)
245            .await
246            .context("Failed to set face via D-Bus")
247    }
248
249    /// Gets the current face name.
250    pub async fn get_face(&self) -> Result<String> {
251        self.proxy
252            .get_face()
253            .await
254            .context("Failed to get face via D-Bus")
255    }
256
257    /// Sets LED parameters.
258    pub async fn set_led(&self, theme: u8, intensity: u8, speed: u8) -> Result<()> {
259        self.proxy
260            .set_led(theme, intensity, speed)
261            .await
262            .context("Failed to set LED via D-Bus")
263    }
264
265    /// Turns off LEDs.
266    pub async fn led_off(&self) -> Result<()> {
267        self.proxy
268            .led_off()
269            .await
270            .context("Failed to turn off LED via D-Bus")
271    }
272
273    /// Gets current LED settings.
274    pub async fn get_led_settings(&self) -> Result<(u8, u8, u8)> {
275        self.proxy
276            .get_led_settings()
277            .await
278            .context("Failed to get LED settings via D-Bus")
279    }
280
281    /// Gets the current color theme name.
282    pub async fn get_theme(&self) -> Result<String> {
283        self.proxy
284            .get_theme()
285            .await
286            .context("Failed to get theme via D-Bus")
287    }
288
289    /// Sets the color theme by name.
290    pub async fn set_theme(&self, name: &str) -> Result<()> {
291        self.proxy
292            .set_theme(name)
293            .await
294            .context("Failed to set theme via D-Bus")
295    }
296
297    /// Lists available color themes (IDs only).
298    pub async fn list_themes(&self) -> Result<Vec<String>> {
299        self.proxy
300            .list_themes()
301            .await
302            .context("Failed to list themes via D-Bus")
303    }
304
305    /// Lists available color themes with display names (JSON-encoded).
306    pub async fn list_themes_detailed(&self) -> Result<Vec<String>> {
307        self.proxy
308            .list_themes_detailed()
309            .await
310            .context("Failed to list themes detailed via D-Bus")
311    }
312
313    /// Lists available faces (IDs only).
314    pub async fn list_face_ids(&self) -> Result<Vec<String>> {
315        self.proxy
316            .list_face_ids()
317            .await
318            .context("Failed to list face IDs via D-Bus")
319    }
320
321    /// Lists available faces with display names (JSON-encoded).
322    pub async fn list_faces(&self) -> Result<Vec<String>> {
323        self.proxy
324            .list_faces()
325            .await
326            .context("Failed to list faces via D-Bus")
327    }
328
329    /// Lists available network interfaces.
330    pub async fn list_network_interfaces(&self) -> Result<Vec<String>> {
331        self.proxy
332            .list_network_interfaces()
333            .await
334            .context("Failed to list network interfaces via D-Bus")
335    }
336
337    /// Gets the screen as PNG data.
338    pub async fn get_screen_png(&self) -> Result<Vec<u8>> {
339        self.proxy
340            .get_screen_png()
341            .await
342            .context("Failed to get screen PNG via D-Bus")
343    }
344
345    /// Shuts down the daemon.
346    pub async fn quit(&self) -> Result<()> {
347        self.proxy
348            .quit()
349            .await
350            .context("Failed to quit daemon via D-Bus")
351    }
352
353    /// Checks if the LCD is connected.
354    pub async fn is_connected(&self) -> Result<bool> {
355        self.proxy
356            .connected()
357            .await
358            .context("Failed to get connection status via D-Bus")
359    }
360
361    /// Checks if the web UI is enabled.
362    pub async fn is_web_enabled(&self) -> Result<bool> {
363        self.proxy
364            .web_enabled()
365            .await
366            .context("Failed to get web enabled status via D-Bus")
367    }
368
369    /// Lists complications for the current face.
370    /// Returns (id, name, description, enabled) tuples.
371    pub async fn list_complications(&self) -> Result<Vec<(String, String, String, bool)>> {
372        self.proxy
373            .list_complications()
374            .await
375            .context("Failed to list complications via D-Bus")
376    }
377
378    /// Lists complications with full details including options.
379    /// Returns JSON-encoded complication data.
380    pub async fn list_complications_detailed(&self) -> Result<Vec<String>> {
381        self.proxy
382            .list_complications_detailed()
383            .await
384            .context("Failed to list complications detailed via D-Bus")
385    }
386
387    /// Gets enabled complications for the current face.
388    pub async fn get_enabled_complications(&self) -> Result<Vec<String>> {
389        self.proxy
390            .get_enabled_complications()
391            .await
392            .context("Failed to get enabled complications via D-Bus")
393    }
394
395    /// Enables a complication for the current face.
396    pub async fn enable_complication(&self, complication_id: &str) -> Result<()> {
397        self.proxy
398            .enable_complication(complication_id)
399            .await
400            .context("Failed to enable complication via D-Bus")
401    }
402
403    /// Disables a complication for the current face.
404    pub async fn disable_complication(&self, complication_id: &str) -> Result<()> {
405        self.proxy
406            .disable_complication(complication_id)
407            .await
408            .context("Failed to disable complication via D-Bus")
409    }
410
411    /// Gets a complication option value.
412    pub async fn get_complication_option(
413        &self,
414        complication_id: &str,
415        option_id: &str,
416    ) -> Result<String> {
417        self.proxy
418            .get_complication_option(complication_id, option_id)
419            .await
420            .context("Failed to get complication option via D-Bus")
421    }
422
423    /// Sets a complication option value.
424    pub async fn set_complication_option(
425        &self,
426        complication_id: &str,
427        option_id: &str,
428        value: &str,
429    ) -> Result<()> {
430        self.proxy
431            .set_complication_option(complication_id, option_id, value)
432            .await
433            .context("Failed to set complication option via D-Bus")
434    }
435}