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}