Skip to main content

hidpp/feature/
device_friendly_name.rs

1//! Implements the `DeviceFriendlyName` feature (ID `0x0007`) that provides
2//! functionality to set and retrieve a custom device name.
3
4use openlogi_hidpp_derive::Feature;
5
6use crate::{feature::FeatureEndpoint, protocol::v20::Hidpp20Error};
7
8/// Implements the `DeviceFriendlyName` / `0x0007` feature.
9#[derive(Clone, Feature)]
10#[creatable(id = 0x0007, version = 0)]
11pub struct DeviceFriendlyNameFeature {
12    /// The endpoint this feature talks to.
13    endpoint: FeatureEndpoint,
14}
15
16impl DeviceFriendlyNameFeature {
17    /// Retrieves the length data of the friendly device name feature.
18    pub async fn get_friendly_name_length(&self) -> Result<DeviceFriendlyNameLength, Hidpp20Error> {
19        let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
20
21        Ok(DeviceFriendlyNameLength {
22            name_length: payload[0],
23            name_max_length: payload[1],
24            default_name_length: payload[2],
25        })
26    }
27
28    /// Retrieves a chunk of characters of the friendly name of the device,
29    /// starting at a specific index (inclusive).
30    ///
31    /// This function will always retrieve 15 bytes, filling up the rest with
32    /// zeroes if the chunk is shorter than that.
33    ///
34    /// Use this function in conjunction with [`Self::get_friendly_name_length`]
35    /// to retrieve the whole friendly name of the device.\
36    /// A convenience wrapper implementing this functionality is provided as
37    /// [`Self::get_whole_friendly_name`].
38    pub async fn get_friendly_name(&self, index: u8) -> Result<[u8; 15], Hidpp20Error> {
39        let payload = self
40            .endpoint
41            .call(1, [index, 0x00, 0x00])
42            .await?
43            .extend_payload();
44
45        Ok([
46            payload[1],
47            payload[2],
48            payload[3],
49            payload[4],
50            payload[5],
51            payload[6],
52            payload[7],
53            payload[8],
54            payload[9],
55            payload[10],
56            payload[11],
57            payload[12],
58            payload[13],
59            payload[14],
60            payload[15],
61        ])
62    }
63
64    /// Retrieves the whole friendly name of the device by first calling
65    /// [`Self::get_friendly_name_length`] once and then repeatedly calling
66    /// [`Self::get_friendly_name`] until all characters were received.
67    pub async fn get_whole_friendly_name(&self) -> Result<String, Hidpp20Error> {
68        let count = self.get_friendly_name_length().await?.name_length;
69        let mut string = String::with_capacity(count as usize);
70
71        let mut len = 0;
72        while len < count as usize {
73            #[expect(
74                clippy::cast_possible_truncation,
75                reason = "len < count as usize and count is a u8, so len always fits in u8"
76            )]
77            let index = len as u8;
78            let part = self.get_friendly_name(index).await?;
79            string.push_str(str::from_utf8(&part).map_err(|_| Hidpp20Error::UnsupportedResponse)?);
80            len = string.len();
81        }
82
83        Ok(string.trim_end_matches(char::from(0)).to_string())
84    }
85
86    /// Retrieves a chunk of characters of the default friendly name of the
87    /// device, starting at a specific index (inclusive).
88    ///
89    /// This function will always retrieve 15 bytes, filling up the rest with
90    /// zeroes if the chunk is shorter than that.
91    ///
92    /// Use this function in conjunction with [`Self::get_friendly_name_length`]
93    /// to retrieve the whole default friendly name of the device.\
94    /// A convenience wrapper implementing this functionality is provided as
95    /// [`Self::get_whole_default_friendly_name`].
96    pub async fn get_default_friendly_name(&self, index: u8) -> Result<[u8; 15], Hidpp20Error> {
97        let payload = self
98            .endpoint
99            .call(2, [index, 0x00, 0x00])
100            .await?
101            .extend_payload();
102
103        Ok([
104            payload[1],
105            payload[2],
106            payload[3],
107            payload[4],
108            payload[5],
109            payload[6],
110            payload[7],
111            payload[8],
112            payload[9],
113            payload[10],
114            payload[11],
115            payload[12],
116            payload[13],
117            payload[14],
118            payload[15],
119        ])
120    }
121
122    /// Retrieves the whole default friendly name of the device by first calling
123    /// [`Self::get_friendly_name_length`] once and then repeatedly calling
124    /// [`Self::get_default_friendly_name`] until all characters were received.
125    pub async fn get_whole_default_friendly_name(&self) -> Result<String, Hidpp20Error> {
126        let count = self.get_friendly_name_length().await?.default_name_length;
127        let mut string = String::with_capacity(count as usize);
128
129        let mut len = 0;
130        while len < count as usize {
131            #[expect(
132                clippy::cast_possible_truncation,
133                reason = "len < count as usize and count is a u8, so len always fits in u8"
134            )]
135            let index = len as u8;
136            let part = self.get_default_friendly_name(index).await?;
137            string.push_str(str::from_utf8(&part).map_err(|_| Hidpp20Error::UnsupportedResponse)?);
138            len = string.len();
139        }
140
141        Ok(string.trim_end_matches(char::from(0)).to_string())
142    }
143
144    /// Sets a chunk of the friendly device name, starting at a specific index
145    /// (inclusive).
146    ///
147    /// If the index and chunk combination would exceed the
148    /// [`DeviceFriendlyNameLength::name_max_length`], the name is automatically
149    /// truncated by the device.
150    ///
151    /// Returns the new total length of the friendly device name.
152    ///
153    /// A convenience wrapper setting the whole friendly device name at once is
154    /// provided as [`Self::set_whole_device_name`].
155    pub async fn set_friendly_name(&self, index: u8, chunk: [u8; 15]) -> Result<u8, Hidpp20Error> {
156        let mut data = [0u8; 16];
157        data[0] = index;
158        data[1..].copy_from_slice(&chunk);
159
160        let payload = self.endpoint.call_long(3, data).await?.extend_payload();
161
162        Ok(payload[0])
163    }
164
165    /// Sets the whole friendly device name, truncating the value to a maximum
166    /// of [`DeviceFriendlyNameLength::name_max_length`] bytes.
167    ///
168    /// This method calls [`Self::get_friendly_name_length`] first to retrieve
169    /// the maximum length and then repeatedly calls [`Self::set_friendly_name`]
170    /// until the whole name is set.
171    ///
172    /// Returns the total length of the name after setting it,
173    pub async fn set_whole_device_name(&self, name: String) -> Result<u8, Hidpp20Error> {
174        let max_len = self.get_friendly_name_length().await?.name_max_length;
175        let mut bytes = name.into_bytes();
176        bytes.truncate(max_len as usize);
177        let chunks = bytes.chunks_exact(15);
178        let remainder = chunks.remainder();
179
180        let mut index = 0;
181        for chunk in chunks {
182            // `chunks_exact(15)` guarantees every yielded chunk is exactly 15
183            // bytes long.
184            let chunk: [u8; 15] = std::array::from_fn(|i| chunk[i]);
185            index += self.set_friendly_name(index, chunk).await?;
186        }
187
188        if !remainder.is_empty() {
189            let mut chunk = [0u8; 15];
190            chunk[..remainder.len()].copy_from_slice(remainder);
191            index += self.set_friendly_name(index, chunk).await?;
192        }
193
194        Ok(index)
195    }
196
197    /// Resets the friendly device name to the default one.
198    ///
199    /// Returns the total length of the name after resetting it,
200    pub async fn reset_friendly_name(&self) -> Result<u8, Hidpp20Error> {
201        Ok(self.endpoint.call(4, [0; 3]).await?.extend_payload()[0])
202    }
203}
204
205/// Represents the length data as returned by
206/// [`DeviceFriendlyNameFeature::get_friendly_name_length`].
207#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
208#[cfg_attr(feature = "serde", derive(serde::Serialize))]
209#[non_exhaustive]
210pub struct DeviceFriendlyNameLength {
211    /// The current length of the friendly device name.
212    pub name_length: u8,
213
214    /// The maximum length of the friendly device name.
215    pub name_max_length: u8,
216
217    /// The length of the default friendly device name.
218    pub default_name_length: u8,
219}