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}