kenwood_thd75/radio/
service.rs

1//! Factory service mode commands for calibration, testing, and diagnostics.
2//!
3//! # Safety
4//!
5//! These commands are intended for factory use only. Incorrect use can:
6//! - **Corrupt factory calibration** (0R, 1A-1W) — may require professional recalibration
7//! - **Brick the radio** (1F) — raw flash write can overwrite boot code
8//! - **Change the serial number** (1I) — may void warranty
9//!
10//! Service mode is entered by sending `0G KENWOOD` and exited with bare `0G`.
11//! While in service mode, the standard CAT command table is replaced with the
12//! service mode table — normal commands will not work until service mode is exited.
13//!
14//! All 20 service mode commands were discovered via Ghidra decompilation of the
15//! TH-D75 V1.03 firmware. The service mode table is at address 0xC006F288 with
16//! 34 entries (20 service + 14 remapped standard commands).
17
18use crate::error::{Error, ProtocolError};
19use crate::protocol::{Command, Response};
20use crate::transport::Transport;
21
22use super::Radio;
23
24impl<T: Transport> Radio<T> {
25    /// Enter factory service mode (0G KENWOOD).
26    ///
27    /// Switches the radio from the standard 53-command CAT table to the
28    /// 34-entry service mode table. Normal CAT commands will not work until
29    /// service mode is exited with [`exit_service_mode`](Self::exit_service_mode).
30    ///
31    /// # Errors
32    ///
33    /// Returns an error if the command fails or the response is unexpected.
34    pub async fn enter_service_mode(&mut self) -> Result<(), Error> {
35        tracing::info!("entering factory service mode (0G KENWOOD)");
36        let response = self.execute(Command::EnterServiceMode).await?;
37        match response {
38            Response::ServiceMode { .. } => Ok(()),
39            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
40                expected: "ServiceMode".into(),
41                actual: format!("{other:?}").into_bytes(),
42            })),
43        }
44    }
45
46    /// Exit factory service mode (0G bare).
47    ///
48    /// Restores the standard CAT command table. Normal CAT commands will
49    /// work again after this call.
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if the command fails or the response is unexpected.
54    pub async fn exit_service_mode(&mut self) -> Result<(), Error> {
55        tracing::info!("exiting factory service mode (bare 0G)");
56        let response = self.execute(Command::ExitServiceMode).await?;
57        match response {
58            Response::ServiceMode { .. } => Ok(()),
59            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
60                expected: "ServiceMode".into(),
61                actual: format!("{other:?}").into_bytes(),
62            })),
63        }
64    }
65
66    /// Read factory calibration data (0S).
67    ///
68    /// Returns 200 bytes of hex-encoded factory calibration data (118 bytes
69    /// from flash 0x4E000 + 82 bytes from a second address).
70    ///
71    /// Requires service mode. Call [`enter_service_mode`](Self::enter_service_mode) first.
72    ///
73    /// # Errors
74    ///
75    /// Returns an error if the command fails or the response is unexpected.
76    pub async fn read_calibration_data(&mut self) -> Result<String, Error> {
77        tracing::debug!("reading factory calibration data (0S)");
78        let response = self.execute(Command::ReadCalibrationData).await?;
79        match response {
80            Response::ServiceCalibrationData { data } => Ok(data),
81            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
82                expected: "ServiceCalibrationData".into(),
83                actual: format!("{other:?}").into_bytes(),
84            })),
85        }
86    }
87
88    /// Write factory calibration data (0R).
89    ///
90    /// Writes 200 bytes of calibration data. The `data` parameter must be
91    /// exactly 400 hex characters encoding 200 bytes.
92    ///
93    /// # Safety
94    ///
95    /// **CRITICAL: Can corrupt factory calibration.** Always read calibration
96    /// first with [`read_calibration_data`](Self::read_calibration_data) and
97    /// keep a backup before writing.
98    ///
99    /// Requires service mode. Call [`enter_service_mode`](Self::enter_service_mode) first.
100    ///
101    /// # Errors
102    ///
103    /// Returns an error if the command fails or the response is unexpected.
104    pub async fn write_calibration_data(&mut self, data: &str) -> Result<(), Error> {
105        tracing::warn!("writing factory calibration data (0R) — CRITICAL OPERATION");
106        let response = self
107            .execute(Command::WriteCalibrationData {
108                data: data.to_owned(),
109            })
110            .await?;
111        match response {
112            Response::ServiceCalibrationWrite { .. } => Ok(()),
113            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
114                expected: "ServiceCalibrationWrite".into(),
115                actual: format!("{other:?}").into_bytes(),
116            })),
117        }
118    }
119
120    /// Get service/MCP status (0E in service mode).
121    ///
122    /// Reads 3 bytes from hardware status register at address 0x110.
123    /// In normal mode, 0E returns `N` (not available). In service mode,
124    /// it returns actual status data.
125    ///
126    /// Requires service mode. Call [`enter_service_mode`](Self::enter_service_mode) first.
127    ///
128    /// # Errors
129    ///
130    /// Returns an error if the command fails or the response is unexpected.
131    pub async fn get_service_status(&mut self) -> Result<String, Error> {
132        tracing::debug!("reading service status (0E)");
133        let response = self.execute(Command::GetServiceStatus).await?;
134        match response {
135            // In service mode, the user parser handles 0E and returns McpStatus.
136            Response::McpStatus { value } => Ok(value),
137            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
138                expected: "McpStatus".into(),
139                actual: format!("{other:?}").into_bytes(),
140            })),
141        }
142    }
143
144    /// Read/write calibration parameter 1A.
145    ///
146    /// Delegates to the firmware's command executor for calibration access.
147    ///
148    /// Requires service mode. Call [`enter_service_mode`](Self::enter_service_mode) first.
149    ///
150    /// # Errors
151    ///
152    /// Returns an error if the command fails or the response is unexpected.
153    pub async fn service_calibrate_1a(&mut self) -> Result<String, Error> {
154        tracing::debug!("service calibration 1A");
155        let response = self.execute(Command::ServiceCalibrate1A).await?;
156        match response {
157            Response::ServiceCalibrationParam { data, .. } => Ok(data),
158            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
159                expected: "ServiceCalibrationParam".into(),
160                actual: format!("{other:?}").into_bytes(),
161            })),
162        }
163    }
164
165    /// Read/write calibration parameter 1D.
166    ///
167    /// Same executor-based pattern as 1A.
168    ///
169    /// Requires service mode. Call [`enter_service_mode`](Self::enter_service_mode) first.
170    ///
171    /// # Errors
172    ///
173    /// Returns an error if the command fails or the response is unexpected.
174    pub async fn service_calibrate_1d(&mut self) -> Result<String, Error> {
175        tracing::debug!("service calibration 1D");
176        let response = self.execute(Command::ServiceCalibrate1D).await?;
177        match response {
178            Response::ServiceCalibrationParam { data, .. } => Ok(data),
179            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
180                expected: "ServiceCalibrationParam".into(),
181                actual: format!("{other:?}").into_bytes(),
182            })),
183        }
184    }
185
186    /// Read calibration parameter 1E, or write with a 3-character value.
187    ///
188    /// When `value` is `None`, sends a bare read (`1E\r`).
189    /// When `value` is `Some`, sends `1E XXX\r` (write form).
190    ///
191    /// Requires service mode. Call [`enter_service_mode`](Self::enter_service_mode) first.
192    ///
193    /// # Errors
194    ///
195    /// Returns an error if the command fails or the response is unexpected.
196    pub async fn service_calibrate_1e(&mut self, value: Option<&str>) -> Result<String, Error> {
197        tracing::debug!(?value, "service calibration 1E");
198        let response = self
199            .execute(Command::ServiceCalibrate1E {
200                value: value.map(str::to_owned),
201            })
202            .await?;
203        match response {
204            Response::ServiceCalibrationParam { data, .. } => Ok(data),
205            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
206                expected: "ServiceCalibrationParam".into(),
207                actual: format!("{other:?}").into_bytes(),
208            })),
209        }
210    }
211
212    /// Read/write calibration parameter 1N.
213    ///
214    /// Same executor-based pattern as 1A.
215    ///
216    /// Requires service mode. Call [`enter_service_mode`](Self::enter_service_mode) first.
217    ///
218    /// # Errors
219    ///
220    /// Returns an error if the command fails or the response is unexpected.
221    pub async fn service_calibrate_1n(&mut self) -> Result<String, Error> {
222        tracing::debug!("service calibration 1N");
223        let response = self.execute(Command::ServiceCalibrate1N).await?;
224        match response {
225            Response::ServiceCalibrationParam { data, .. } => Ok(data),
226            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
227                expected: "ServiceCalibrationParam".into(),
228                actual: format!("{other:?}").into_bytes(),
229            })),
230        }
231    }
232
233    /// Read calibration parameter 1V, or write with a 3-character value.
234    ///
235    /// Same dual-mode pattern as 1E.
236    ///
237    /// Requires service mode. Call [`enter_service_mode`](Self::enter_service_mode) first.
238    ///
239    /// # Errors
240    ///
241    /// Returns an error if the command fails or the response is unexpected.
242    pub async fn service_calibrate_1v(&mut self, value: Option<&str>) -> Result<String, Error> {
243        tracing::debug!(?value, "service calibration 1V");
244        let response = self
245            .execute(Command::ServiceCalibrate1V {
246                value: value.map(str::to_owned),
247            })
248            .await?;
249        match response {
250            Response::ServiceCalibrationParam { data, .. } => Ok(data),
251            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
252                expected: "ServiceCalibrationParam".into(),
253                actual: format!("{other:?}").into_bytes(),
254            })),
255        }
256    }
257
258    /// Write calibration parameter 1W (write only).
259    ///
260    /// Single-character parameter, likely a mode or flag toggle.
261    ///
262    /// Requires service mode. Call [`enter_service_mode`](Self::enter_service_mode) first.
263    ///
264    /// # Errors
265    ///
266    /// Returns an error if the command fails or the response is unexpected.
267    pub async fn service_calibrate_1w(&mut self, value: &str) -> Result<String, Error> {
268        tracing::debug!(value, "service calibration 1W");
269        let response = self
270            .execute(Command::ServiceCalibrate1W {
271                value: value.to_owned(),
272            })
273            .await?;
274        match response {
275            Response::ServiceCalibrationParam { data, .. } => Ok(data),
276            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
277                expected: "ServiceCalibrationParam".into(),
278                actual: format!("{other:?}").into_bytes(),
279            })),
280        }
281    }
282
283    /// Write factory callsign/serial number (1I).
284    ///
285    /// The `id` parameter must be exactly 8 alphanumeric characters and
286    /// `code` must be exactly 3 alphanumeric characters.
287    ///
288    /// # Safety
289    ///
290    /// **HIGH RISK: Changes the radio's factory serial number / callsign.**
291    /// This may void the warranty and could cause regulatory issues.
292    ///
293    /// Requires service mode. Call [`enter_service_mode`](Self::enter_service_mode) first.
294    ///
295    /// # Errors
296    ///
297    /// Returns an error if the command fails or the response is unexpected.
298    pub async fn service_write_id(&mut self, id: &str, code: &str) -> Result<(), Error> {
299        tracing::warn!(id, code, "writing factory ID (1I) — HIGH RISK OPERATION");
300        let response = self
301            .execute(Command::ServiceWriteId {
302                id: id.to_owned(),
303                code: code.to_owned(),
304            })
305            .await?;
306        match response {
307            Response::ServiceWriteId { .. } => Ok(()),
308            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
309                expected: "ServiceWriteId".into(),
310                actual: format!("{other:?}").into_bytes(),
311            })),
312        }
313    }
314
315    /// Read flash memory (1F bare read).
316    ///
317    /// The read behavior depends on the firmware executor's internal state.
318    ///
319    /// # Safety
320    ///
321    /// While reading is generally safe, this accesses raw flash memory.
322    ///
323    /// Requires service mode. Call [`enter_service_mode`](Self::enter_service_mode) first.
324    ///
325    /// # Errors
326    ///
327    /// Returns an error if the command fails or the response is unexpected.
328    pub async fn service_flash_read(&mut self) -> Result<String, Error> {
329        tracing::debug!("reading flash memory (1F)");
330        let response = self.execute(Command::ServiceFlashRead).await?;
331        match response {
332            Response::ServiceFlash { data } => Ok(data),
333            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
334                expected: "ServiceFlash".into(),
335                actual: format!("{other:?}").into_bytes(),
336            })),
337        }
338    }
339
340    /// Write raw flash memory (1F write).
341    ///
342    /// The `address` must be a 6-digit hex string (max 0x04FFFF) and `data`
343    /// must be hex-encoded bytes. Address + data length must not exceed 0x50000.
344    ///
345    /// # Safety
346    ///
347    /// **CRITICAL: Can brick the radio.** Raw flash writes can overwrite
348    /// boot code, calibration data, or firmware. There is no recovery
349    /// mechanism short of JTAG or factory repair.
350    ///
351    /// Requires service mode. Call [`enter_service_mode`](Self::enter_service_mode) first.
352    ///
353    /// # Errors
354    ///
355    /// Returns an error if the command fails or the response is unexpected.
356    pub async fn service_flash_write(&mut self, address: &str, data: &str) -> Result<(), Error> {
357        tracing::warn!(
358            address,
359            data_len = data.len(),
360            "writing raw flash memory (1F) — CRITICAL OPERATION"
361        );
362        let response = self
363            .execute(Command::ServiceFlashWrite {
364                address: address.to_owned(),
365                data: data.to_owned(),
366            })
367            .await?;
368        match response {
369            Response::ServiceFlash { .. } => Ok(()),
370            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
371                expected: "ServiceFlash".into(),
372                actual: format!("{other:?}").into_bytes(),
373            })),
374        }
375    }
376
377    /// Generic write via executor (0W).
378    ///
379    /// The exact operation depends on the firmware executor's internal state.
380    ///
381    /// Requires service mode. Call [`enter_service_mode`](Self::enter_service_mode) first.
382    ///
383    /// # Errors
384    ///
385    /// Returns an error if the command fails or the response is unexpected.
386    pub async fn service_write_config(&mut self) -> Result<String, Error> {
387        tracing::debug!("service write config (0W)");
388        let response = self.execute(Command::ServiceWriteConfig).await?;
389        match response {
390            Response::ServiceWriteConfig { data } => Ok(data),
391            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
392                expected: "ServiceWriteConfig".into(),
393                actual: format!("{other:?}").into_bytes(),
394            })),
395        }
396    }
397
398    /// Select service mode band (0Y).
399    ///
400    /// Band 0 and band 1 activate different receiver chain code paths
401    /// in the firmware.
402    ///
403    /// Requires service mode. Call [`enter_service_mode`](Self::enter_service_mode) first.
404    ///
405    /// # Errors
406    ///
407    /// Returns an error if the command fails or the response is unexpected.
408    pub async fn service_band_select(&mut self, band: u8) -> Result<(), Error> {
409        tracing::debug!(band, "service band select (0Y)");
410        let response = self.execute(Command::ServiceBandSelect { band }).await?;
411        match response {
412            Response::ServiceBandSelect { .. } => Ok(()),
413            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
414                expected: "ServiceBandSelect".into(),
415                actual: format!("{other:?}").into_bytes(),
416            })),
417        }
418    }
419
420    /// Read bulk EEPROM/calibration data (9E).
421    ///
422    /// Reads up to 256 bytes from the specified address. The `address`
423    /// parameter is a 6-digit hex string and `length` is a 2-digit hex
424    /// string (00 = 256 bytes). Address + length must not exceed 0x50000.
425    ///
426    /// Requires service mode. Call [`enter_service_mode`](Self::enter_service_mode) first.
427    ///
428    /// # Errors
429    ///
430    /// Returns an error if the command fails or the response is unexpected.
431    pub async fn service_read_eeprom(
432        &mut self,
433        address: &str,
434        length: &str,
435    ) -> Result<String, Error> {
436        tracing::debug!(address, length, "reading EEPROM data (9E)");
437        let response = self
438            .execute(Command::ServiceReadEeprom {
439                address: address.to_owned(),
440                length: length.to_owned(),
441            })
442            .await?;
443        match response {
444            Response::ServiceEepromData { data } => Ok(data),
445            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
446                expected: "ServiceEepromData".into(),
447                actual: format!("{other:?}").into_bytes(),
448            })),
449        }
450    }
451
452    /// Read calibration data at current offset (9R).
453    ///
454    /// Returns 4 bytes of formatted calibration data. The offset is
455    /// determined by firmware internal state.
456    ///
457    /// Requires service mode. Call [`enter_service_mode`](Self::enter_service_mode) first.
458    ///
459    /// # Errors
460    ///
461    /// Returns an error if the command fails or the response is unexpected.
462    pub async fn service_read_eeprom_addr(&mut self) -> Result<String, Error> {
463        tracing::debug!("reading EEPROM at current offset (9R)");
464        let response = self.execute(Command::ServiceReadEepromAddr).await?;
465        match response {
466            Response::ServiceEepromAddr { data } => Ok(data),
467            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
468                expected: "ServiceEepromAddr".into(),
469                actual: format!("{other:?}").into_bytes(),
470            })),
471        }
472    }
473
474    /// Get internal version/variant information (2V).
475    ///
476    /// Returns model code (e.g., EX-5210), build date, hardware revision,
477    /// and calibration date. The `param1` is a 2-digit hex parameter and
478    /// `param2` is a 3-digit hex parameter.
479    ///
480    /// Requires service mode. Call [`enter_service_mode`](Self::enter_service_mode) first.
481    ///
482    /// # Errors
483    ///
484    /// Returns an error if the command fails or the response is unexpected.
485    pub async fn service_get_version(
486        &mut self,
487        param1: &str,
488        param2: &str,
489    ) -> Result<String, Error> {
490        tracing::debug!(param1, param2, "reading service version info (2V)");
491        let response = self
492            .execute(Command::ServiceGetVersion {
493                param1: param1.to_owned(),
494                param2: param2.to_owned(),
495            })
496            .await?;
497        match response {
498            Response::ServiceVersion { data } => Ok(data),
499            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
500                expected: "ServiceVersion".into(),
501                actual: format!("{other:?}").into_bytes(),
502            })),
503        }
504    }
505
506    /// Get hardware register / GPIO status (1G).
507    ///
508    /// Returns hex-encoded hardware register values. Used for factory
509    /// testing of GPIO and peripheral status.
510    ///
511    /// Requires service mode. Call [`enter_service_mode`](Self::enter_service_mode) first.
512    ///
513    /// # Errors
514    ///
515    /// Returns an error if the command fails or the response is unexpected.
516    pub async fn service_get_hardware(&mut self) -> Result<String, Error> {
517        tracing::debug!("reading hardware register status (1G)");
518        let response = self.execute(Command::ServiceGetHardware).await?;
519        match response {
520            Response::ServiceHardware { data } => Ok(data),
521            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
522                expected: "ServiceHardware".into(),
523                actual: format!("{other:?}").into_bytes(),
524            })),
525        }
526    }
527
528    /// Write new D75-specific calibration parameter (1C).
529    ///
530    /// The `value` must be a 3-digit hex string (max 0xFF). This command
531    /// is new in the D75 (not present in D74) and may be related to the
532    /// 220 MHz band or enhanced DSP.
533    ///
534    /// Requires service mode. Call [`enter_service_mode`](Self::enter_service_mode) first.
535    ///
536    /// # Errors
537    ///
538    /// Returns an error if the command fails or the response is unexpected.
539    pub async fn service_calibrate_new(&mut self, value: &str) -> Result<(), Error> {
540        tracing::debug!(value, "service calibration 1C (D75-specific)");
541        let response = self
542            .execute(Command::ServiceCalibrateNew {
543                value: value.to_owned(),
544            })
545            .await?;
546        match response {
547            Response::ServiceCalibrationParam { .. } => Ok(()),
548            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
549                expected: "ServiceCalibrationParam".into(),
550                actual: format!("{other:?}").into_bytes(),
551            })),
552        }
553    }
554
555    /// Read or write dynamic-length hardware configuration (1U).
556    ///
557    /// When `data` is `None`, sends a bare read. When `data` is `Some`,
558    /// sends a write with the provided data. The expected wire length
559    /// is determined dynamically by the firmware reading a hardware register.
560    ///
561    /// Requires service mode. Call [`enter_service_mode`](Self::enter_service_mode) first.
562    ///
563    /// # Errors
564    ///
565    /// Returns an error if the command fails or the response is unexpected.
566    pub async fn service_dynamic_param(&mut self, data: Option<&str>) -> Result<String, Error> {
567        tracing::debug!(?data, "service dynamic parameter (1U)");
568        let response = self
569            .execute(Command::ServiceDynamicParam {
570                data: data.map(str::to_owned),
571            })
572            .await?;
573        match response {
574            Response::ServiceCalibrationParam { data: resp, .. } => Ok(resp),
575            other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
576                expected: "ServiceCalibrationParam".into(),
577                actual: format!("{other:?}").into_bytes(),
578            })),
579        }
580    }
581}
582
583#[cfg(test)]
584mod tests {
585    use crate::radio::Radio;
586    use crate::transport::MockTransport;
587
588    #[tokio::test]
589    async fn service_enter_and_exit() {
590        let mut mock = MockTransport::new();
591        mock.expect(b"0G KENWOOD\r", b"0G KENWOOD\r");
592        mock.expect(b"0G\r", b"0G \r");
593        let mut radio = Radio::connect(mock).await.unwrap();
594        radio.enter_service_mode().await.unwrap();
595        radio.exit_service_mode().await.unwrap();
596    }
597
598    #[tokio::test]
599    async fn service_band_select() {
600        let mut mock = MockTransport::new();
601        mock.expect(b"0Y 0\r", b"0Y 0\r");
602        let mut radio = Radio::connect(mock).await.unwrap();
603        radio.service_band_select(0).await.unwrap();
604    }
605
606    #[tokio::test]
607    async fn service_band_select_band_1() {
608        let mut mock = MockTransport::new();
609        mock.expect(b"0Y 1\r", b"0Y 1\r");
610        let mut radio = Radio::connect(mock).await.unwrap();
611        radio.service_band_select(1).await.unwrap();
612    }
613
614    #[tokio::test]
615    async fn service_get_hardware() {
616        let mut mock = MockTransport::new();
617        mock.expect(b"1G\r", b"1G AA,BB,CC\r");
618        let mut radio = Radio::connect(mock).await.unwrap();
619        let data = radio.service_get_hardware().await.unwrap();
620        assert_eq!(data, "AA,BB,CC");
621    }
622
623    #[tokio::test]
624    async fn service_get_version() {
625        let mut mock = MockTransport::new();
626        mock.expect(b"2V 00,000\r", b"2V EX-5210\r");
627        let mut radio = Radio::connect(mock).await.unwrap();
628        let data = radio.service_get_version("00", "000").await.unwrap();
629        assert_eq!(data, "EX-5210");
630    }
631
632    #[tokio::test]
633    async fn service_read_calibration_data() {
634        let mut mock = MockTransport::new();
635        mock.expect(b"0S\r", b"0S AABBCCDD\r");
636        let mut radio = Radio::connect(mock).await.unwrap();
637        let data = radio.read_calibration_data().await.unwrap();
638        assert_eq!(data, "AABBCCDD");
639    }
640
641    #[tokio::test]
642    async fn service_read_eeprom() {
643        let mut mock = MockTransport::new();
644        mock.expect(b"9E 04E000,10\r", b"9E DEADBEEF\r");
645        let mut radio = Radio::connect(mock).await.unwrap();
646        let data = radio.service_read_eeprom("04E000", "10").await.unwrap();
647        assert_eq!(data, "DEADBEEF");
648    }
649}