Skip to main content

pokeys_lib/
oem_parameters.rs

1//! OEM parameter read/write support (command 0xFD)
2//!
3//! PoKeys57-series devices provide 62 non-volatile 32-bit OEM parameters (indices 0–61).
4//! This module exposes a general-purpose interface for reading and writing those parameters,
5//! and a dedicated helper pair for a device *location* value stored at
6//! [`LOCATION_PARAMETER_INDEX`].
7
8use crate::device::PoKeysDevice;
9use crate::error::{PoKeysError, Result};
10
11/// Maximum valid OEM parameter index (the device stores 62 parameters, 0–61).
12pub const OEM_PARAMETER_MAX_INDEX: u8 = 61;
13
14/// OEM parameter index used to store the device location.
15pub const LOCATION_PARAMETER_INDEX: u8 = 0;
16
17// Protocol constants for command 0xFD
18const OEM_PARAM_CMD: u8 = 0xFD;
19const OEM_PARAM_READ: u8 = 0x00;
20const OEM_PARAM_WRITE: u8 = 0x01;
21const OEM_PARAM_CLEAR: u8 = 0x02;
22
23// Response byte offsets (0-based)
24const RESP_SUBCMD: usize = 2; // echoed sub-command (0x00 or 0x01)
25const RESP_STATUS: usize = 5; // read response: bit-mapped set-status
26const RESP_VALUE_START: usize = 8; // first 32-bit parameter value (LE)
27
28impl PoKeysDevice {
29    /// Read a single OEM parameter from non-volatile storage.
30    ///
31    /// Returns `None` if the parameter slot has never been written (the device
32    /// clears the corresponding status bit on a factory reset).
33    ///
34    /// # Errors
35    ///
36    /// Returns [`PoKeysError::Parameter`] when `index` > [`OEM_PARAMETER_MAX_INDEX`],
37    /// or a communication error if the exchange fails.
38    pub fn read_oem_parameter(&mut self, index: u8) -> Result<Option<i32>> {
39        validate_index(index)?;
40
41        // 0xFD / 0x00 – read parameters
42        // byte layout: CMD=0xFD, sub-cmd=0x00, param-index, count=1, reserved=0
43        let response = self.send_request(OEM_PARAM_CMD, OEM_PARAM_READ, index, 1, 0)?;
44
45        if response[RESP_SUBCMD] != OEM_PARAM_READ {
46            return Err(PoKeysError::Protocol(format!(
47                "OEM read: unexpected response sub-command 0x{:02X}",
48                response[RESP_SUBCMD]
49            )));
50        }
51
52        // Bit 0 of the status byte indicates whether the first returned parameter has been set.
53        if response[RESP_STATUS] & 0x01 == 0 {
54            return Ok(None);
55        }
56
57        let value = i32::from_le_bytes([
58            response[RESP_VALUE_START],
59            response[RESP_VALUE_START + 1],
60            response[RESP_VALUE_START + 2],
61            response[RESP_VALUE_START + 3],
62        ]);
63        Ok(Some(value))
64    }
65
66    /// Write a single OEM parameter to non-volatile storage.
67    ///
68    /// # Errors
69    ///
70    /// Returns [`PoKeysError::Parameter`] when `index` > [`OEM_PARAMETER_MAX_INDEX`],
71    /// or a communication error if the exchange fails.
72    pub fn write_oem_parameter(&mut self, index: u8, value: i32) -> Result<()> {
73        validate_index(index)?;
74
75        // 0xFD / 0x01 – set parameter
76        // byte layout: CMD=0xFD, sub-cmd=0x01, param-index, reserved, reserved
77        // data payload (bytes 9-12 / 0-based 8-11): parameter value as little-endian u32
78        let value_bytes = value.to_le_bytes();
79        let response =
80            self.send_request_with_data(OEM_PARAM_CMD, OEM_PARAM_WRITE, index, 0, 0, &value_bytes)?;
81
82        if response[RESP_SUBCMD] != OEM_PARAM_WRITE {
83            return Err(PoKeysError::Protocol(format!(
84                "OEM write: unexpected response sub-command 0x{:02X}",
85                response[RESP_SUBCMD]
86            )));
87        }
88
89        // The device echoes the written value back at bytes 9-12; verify it matches.
90        let echoed = i32::from_le_bytes([
91            response[RESP_VALUE_START],
92            response[RESP_VALUE_START + 1],
93            response[RESP_VALUE_START + 2],
94            response[RESP_VALUE_START + 3],
95        ]);
96        if echoed != value {
97            return Err(PoKeysError::Protocol(format!(
98                "OEM write: echoed value {} does not match written value {}",
99                echoed, value
100            )));
101        }
102
103        Ok(())
104    }
105
106    /// Read the device location from OEM parameter storage.
107    ///
108    /// The location is persisted at OEM parameter index [`LOCATION_PARAMETER_INDEX`].
109    /// Returns `None` if the location has never been set.
110    pub fn get_location(&mut self) -> Result<Option<i32>> {
111        self.read_oem_parameter(LOCATION_PARAMETER_INDEX)
112    }
113
114    /// Write the device location to OEM parameter storage.
115    ///
116    /// The location is a user-defined integer stored in the device's non-volatile
117    /// OEM parameter slot at index [`LOCATION_PARAMETER_INDEX`].
118    pub fn set_location(&mut self, location: i32) -> Result<()> {
119        self.write_oem_parameter(LOCATION_PARAMETER_INDEX, location)
120    }
121
122    /// Clear the device location from OEM parameter storage.
123    ///
124    /// After this call [`get_location`](PoKeysDevice::get_location) will return `None`.
125    pub fn clear_location(&mut self) -> Result<()> {
126        self.clear_oem_parameter(LOCATION_PARAMETER_INDEX)
127    }
128
129    /// Clear a single OEM parameter, marking it as unset in non-volatile storage.
130    ///
131    /// After clearing, [`read_oem_parameter`](PoKeysDevice::read_oem_parameter) returns `None`
132    /// for this index until a new value is written.
133    ///
134    /// # Errors
135    ///
136    /// Returns [`PoKeysError::Parameter`] when `index` > [`OEM_PARAMETER_MAX_INDEX`],
137    /// or a communication error if the exchange fails.
138    pub fn clear_oem_parameter(&mut self, index: u8) -> Result<()> {
139        validate_index(index)?;
140
141        // 0xFD / 0x02 – clear parameter
142        // byte layout: CMD=0xFD, sub-cmd=0x02, param-index, reserved, reserved
143        let response = self.send_request(OEM_PARAM_CMD, OEM_PARAM_CLEAR, index, 0, 0)?;
144
145        if response[RESP_SUBCMD] != OEM_PARAM_CLEAR {
146            return Err(PoKeysError::Protocol(format!(
147                "OEM clear: unexpected response sub-command 0x{:02X}",
148                response[RESP_SUBCMD]
149            )));
150        }
151
152        Ok(())
153    }
154}
155
156fn validate_index(index: u8) -> Result<()> {
157    if index > OEM_PARAMETER_MAX_INDEX {
158        return Err(PoKeysError::Parameter(format!(
159            "OEM parameter index {} out of range (valid range 0–{})",
160            index, OEM_PARAMETER_MAX_INDEX
161        )));
162    }
163    Ok(())
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use crate::communication::Protocol;
170    use crate::types::*;
171
172    // ── index validation ────────────────────────────────────────────────────
173
174    #[test]
175    fn validate_index_accepts_boundary_values() {
176        assert!(validate_index(0).is_ok());
177        assert!(validate_index(61).is_ok());
178    }
179
180    #[test]
181    fn validate_index_rejects_out_of_range() {
182        let err = validate_index(62).unwrap_err();
183        assert!(matches!(err, PoKeysError::Parameter(_)));
184        assert!(err.to_string().contains("62"));
185    }
186
187    // ── read packet construction ────────────────────────────────────────────
188
189    #[test]
190    fn read_request_packet_layout() {
191        let mut protocol = Protocol::new();
192        // CMD=0xFD, sub-cmd=0x00, index=5, count=1, reserved=0
193        let pkt = protocol.prepare_request(OEM_PARAM_CMD, OEM_PARAM_READ, 5, 1, 0, None);
194
195        assert_eq!(pkt[0], REQUEST_HEADER); // 0xBB
196        assert_eq!(pkt[1], OEM_PARAM_CMD); // 0xFD
197        assert_eq!(pkt[2], OEM_PARAM_READ); // 0x00
198        assert_eq!(pkt[3], 5); // parameter index
199        assert_eq!(pkt[4], 1); // count
200        assert_eq!(pkt[5], 0); // reserved
201        // checksum covers bytes 0-6
202        assert_eq!(pkt[7], Protocol::calculate_checksum(&pkt));
203    }
204
205    // ── write packet construction ───────────────────────────────────────────
206
207    #[test]
208    fn write_request_packet_layout() {
209        let mut protocol = Protocol::new();
210        // CMD=0xFD, sub-cmd=0x01, index=3, reserved, reserved
211        let mut pkt = protocol.prepare_request(OEM_PARAM_CMD, OEM_PARAM_WRITE, 3, 0, 0, None);
212
213        // Embed value 0xDEAD_BEEF at bytes 8-11
214        let value: i32 = 0x0102_0304;
215        let value_bytes = value.to_le_bytes();
216        pkt[8..12].copy_from_slice(&value_bytes);
217        pkt[7] = Protocol::calculate_checksum(&pkt);
218
219        assert_eq!(pkt[1], OEM_PARAM_CMD);
220        assert_eq!(pkt[2], OEM_PARAM_WRITE);
221        assert_eq!(pkt[3], 3); // parameter index
222        assert_eq!(&pkt[8..12], &value_bytes);
223        // checksum is still over bytes 0-6 only
224        assert_eq!(pkt[7], Protocol::calculate_checksum(&pkt));
225    }
226
227    // ── response parsing ────────────────────────────────────────────────────
228
229    #[test]
230    fn read_response_parses_set_parameter() {
231        // Craft a synthetic read response
232        let mut response = [0u8; RESPONSE_BUFFER_SIZE];
233        response[0] = RESPONSE_HEADER; // 0xAA
234        response[1] = OEM_PARAM_CMD; // 0xFD
235        response[2] = OEM_PARAM_READ; // 0x00
236        response[3] = LOCATION_PARAMETER_INDEX; // index
237        response[4] = 1; // count
238        response[5] = 0x01; // status: bit 0 set → parameter is set
239        response[6] = 1; // request ID
240        let value: i32 = 42;
241        response[8..12].copy_from_slice(&value.to_le_bytes());
242        response[7] = Protocol::calculate_checksum(&response);
243
244        // Verify parsing logic directly
245        assert_eq!(response[RESP_SUBCMD], OEM_PARAM_READ);
246        assert_ne!(response[RESP_STATUS] & 0x01, 0);
247        let parsed = i32::from_le_bytes([
248            response[RESP_VALUE_START],
249            response[RESP_VALUE_START + 1],
250            response[RESP_VALUE_START + 2],
251            response[RESP_VALUE_START + 3],
252        ]);
253        assert_eq!(parsed, 42);
254    }
255
256    #[test]
257    fn read_response_detects_unset_parameter() {
258        let mut response = [0u8; RESPONSE_BUFFER_SIZE];
259        response[0] = RESPONSE_HEADER;
260        response[1] = OEM_PARAM_CMD;
261        response[2] = OEM_PARAM_READ;
262        response[5] = 0x00; // status: bit 0 clear → parameter not set
263        response[6] = 1;
264        response[7] = Protocol::calculate_checksum(&response);
265
266        assert_eq!(response[RESP_STATUS] & 0x01, 0);
267    }
268
269    #[test]
270    fn write_response_echo_check() {
271        let value: i32 = -99;
272        let mut response = [0u8; RESPONSE_BUFFER_SIZE];
273        response[0] = RESPONSE_HEADER;
274        response[1] = OEM_PARAM_CMD;
275        response[2] = OEM_PARAM_WRITE;
276        response[3] = LOCATION_PARAMETER_INDEX;
277        response[6] = 1;
278        response[8..12].copy_from_slice(&value.to_le_bytes());
279        response[7] = Protocol::calculate_checksum(&response);
280
281        let echoed = i32::from_le_bytes([
282            response[RESP_VALUE_START],
283            response[RESP_VALUE_START + 1],
284            response[RESP_VALUE_START + 2],
285            response[RESP_VALUE_START + 3],
286        ]);
287        assert_eq!(echoed, value);
288    }
289
290    // ── clear packet construction ───────────────────────────────────────────
291
292    #[test]
293    fn clear_request_packet_layout() {
294        let mut protocol = Protocol::new();
295        let pkt = protocol.prepare_request(OEM_PARAM_CMD, OEM_PARAM_CLEAR, 7, 0, 0, None);
296
297        assert_eq!(pkt[0], REQUEST_HEADER);
298        assert_eq!(pkt[1], OEM_PARAM_CMD);
299        assert_eq!(pkt[2], OEM_PARAM_CLEAR);
300        assert_eq!(pkt[3], 7); // parameter index
301        assert_eq!(pkt[4], 0); // reserved
302        assert_eq!(pkt[5], 0); // reserved
303        assert_eq!(pkt[7], Protocol::calculate_checksum(&pkt));
304    }
305
306    #[test]
307    fn clear_response_subcmd_check() {
308        let mut response = [0u8; RESPONSE_BUFFER_SIZE];
309        response[0] = RESPONSE_HEADER;
310        response[1] = OEM_PARAM_CMD;
311        response[2] = OEM_PARAM_CLEAR;
312        response[3] = 7; // parameter index echoed
313        response[6] = 1;
314        response[7] = Protocol::calculate_checksum(&response);
315
316        assert_eq!(response[RESP_SUBCMD], OEM_PARAM_CLEAR);
317    }
318}