Skip to main content

libcdio_rs/mmc/
get_event_status.rs

1// Copyright (C) 2026 Shiva Kiran Koninty <shiva@skran.xyz>
2//
3// This file is part of libcdio-rs.
4//
5// libcdio-rs is free software: you can redistribute it and/or
6// modify it under the terms of the GNU General Public License as
7// published by the Free Software Foundation, either version 3 of the
8// License, or (at your option) any later version.
9//
10// libcdio-rs is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13// General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with libcdio-rs. If not, see <https://www.gnu.org/licenses/>.
17
18//! Routines based on MMC `GET EVENT STATUS NOTIFICATION`.
19
20use std::time::Duration;
21
22use bitflags::bitflags;
23use displaydoc::Display;
24use num_enum::{TryFromPrimitive, TryFromPrimitiveError};
25use thiserror::Error;
26use tracing::debug;
27use winnow::{
28    Parser,
29    binary::{
30        be_u16,
31        bits::{bits, bool, take as bits_take},
32        length_and_then, u8,
33    },
34    combinator::{preceded, separated_pair},
35    error::{ContextError, StrContext},
36    token::{rest, take},
37};
38
39use crate::{
40    Mmc,
41    mmc::{Cdb, MmcDirection, MmcError},
42};
43
44/// Routines based on MMC `GET EVENT STATUS NOTIFICATION`.
45impl Mmc {
46    /// Get all the event classes supported by the device.
47    pub fn supported_events(&self) -> Result<EventClass, MmcStatusError> {
48        let data = self.get_event_status_notification(EventMode::Polled, EventClass::empty())?;
49        let supported_events = parse_header(EventClass::empty(), &mut data.as_slice())?;
50
51        Ok(supported_events)
52    }
53
54    /// Perform an MMC `GET EVENT STATUS NOTIFICATION`.
55    fn get_event_status_notification(
56        &self,
57        mode: EventMode,
58        class: EventClass,
59    ) -> Result<EventData, MmcError> {
60        let mut data = EventData::default();
61        let mut cdb = Cdb::default();
62        cdb[0] = OPCODE;
63        cdb[1] = if mode == EventMode::Polled { 1 } else { 0 };
64        cdb[4] = class.bits();
65        cdb[7..9].copy_from_slice(&(data.len() as u16).to_be_bytes());
66
67        self.run_command(Some(MmcDirection::Read), &mut data, cdb)?;
68
69        return Ok(data);
70
71        const OPCODE: u8 = 0x4A;
72    }
73
74    /// Is the device tray open.
75    ///
76    /// If the device does not have a tray, this should still return `false`.
77    pub fn is_tray_open(&self) -> Result<bool, MmcStatusError> {
78        self.media_status()
79            .map(|status| status.state.contains(MediaState::DoorOrTrayOpen))
80    }
81
82    /// Get operational change status from the device.
83    pub fn operational_event_status(&self) -> Result<OperationalStatus, MmcStatusError> {
84        let data =
85            self.get_event_status_notification(EventMode::Polled, EventClass::OperationalChange)?;
86        let input = &mut data.as_slice();
87        parse_header(EventClass::OperationalChange, input)?;
88
89        // skip the legacy 'operational status' field
90        let prevent_bit_and_op_status = bits(separated_pair(
91            bool,
92            take(3_usize),
93            bits_take::<_, u8, _, ContextError>(4_usize),
94        ));
95        // skip the 'event code' field as the 'operational change' field
96        // provides the same
97        let (_, (prevent_bit, _op_status), op_change) =
98            (parse_event_code, prevent_bit_and_op_status, be_u16)
99                .context(StrContext::Label("operational change event descriptor"))
100                .parse_next(input)?;
101        let change = (op_change != 0)
102            .then(|| OperationalEvent::try_from(op_change))
103            .transpose()?;
104
105        Ok(OperationalStatus {
106            event: change,
107            persistent_prevent: prevent_bit,
108        })
109    }
110
111    /// Get power management status from the device.
112    pub fn power_status(&self) -> Result<PowerStatus, MmcStatusError> {
113        let data =
114            self.get_event_status_notification(EventMode::Polled, EventClass::PowerManagement)?;
115        let input = &mut data.as_slice();
116        parse_header(EventClass::PowerManagement, input)?;
117
118        let (event_code, status) = (parse_event_code, u8)
119            .context(StrContext::Label("power management event descriptor"))
120            .parse_next(input)?;
121        let event = (event_code != 0)
122            .then(|| PowerEvent::try_from(event_code))
123            .transpose()?;
124        let state = PowerState::try_from(status)?;
125
126        Ok(PowerStatus { event, state })
127    }
128
129    /// Get external request status from the device.
130    pub fn external_status(&self) -> Result<ExternalRequestStatus, MmcStatusError> {
131        let data =
132            self.get_event_status_notification(EventMode::Polled, EventClass::ExternalRequest)?;
133        let input = &mut data.as_slice();
134        parse_header(EventClass::ExternalRequest, input)?;
135
136        // skip the 'persistent prevented' bit as the 'external request status'
137        // field provides the same
138        let status = bits(preceded(
139            bits_take::<_, u8, _, ContextError>(4_usize),
140            bits_take::<_, u8, _, _>(4_usize),
141        ));
142        let (event_code, status, request) = (parse_event_code, status, be_u16)
143            .context(StrContext::Label("external request event descriptor"))
144            .parse_next(input)?;
145        let event = (event_code != 0)
146            .then(|| ExternalRequestEvent::try_from(event_code))
147            .transpose()?;
148        let state = ExternalRequestState::try_from(status)?;
149        let request = (request != 0)
150            .then(|| ExternalRequest::try_from(request))
151            .transpose()?;
152
153        Ok(ExternalRequestStatus {
154            event,
155            state,
156            request,
157        })
158    }
159
160    /// Get media status from the device.
161    pub fn media_status(&self) -> Result<MediaStatus, MmcStatusError> {
162        let data = self.get_event_status_notification(EventMode::Polled, EventClass::Media)?;
163        let input = &mut data.as_slice();
164        parse_header(EventClass::Media, input)?;
165
166        let (event_code, status, start_slot, end_slot) = (parse_event_code, u8, u8, u8)
167            .context(StrContext::Label("media event descriptor"))
168            .parse_next(input)?;
169        let event = (event_code != 0)
170            .then(|| MediaEvent::try_from(event_code))
171            .transpose()?;
172        let state = MediaState::from_bits_truncate(status);
173
174        Ok(MediaStatus {
175            event,
176            state,
177            start_slot,
178            end_slot,
179        })
180    }
181
182    /// Get multiple host event status from the device.
183    pub fn multihost_status(&self) -> Result<MultiHostStatus, MmcStatusError> {
184        let data = self.get_event_status_notification(EventMode::Polled, EventClass::MultiHost)?;
185        let input = &mut data.as_slice();
186        parse_header(EventClass::MultiHost, input)?;
187
188        // skip the 'persistent prevented' bit as the 'multiple host status'
189        // field provides the same
190        let status = bits(preceded(
191            bits_take::<_, u8, _, ContextError>(4_usize),
192            bits_take::<_, u8, _, _>(4_usize),
193        ));
194        let (event_code, status, priority) = (parse_event_code, status, be_u16)
195            .context(StrContext::Label("multiple host event descriptor"))
196            .parse_next(input)?;
197        let event = (event_code != 0)
198            .then(|| MultiHostEvent::try_from(event_code))
199            .transpose()?;
200        let state = MultiHostState::try_from(status)?;
201        let priority = (priority != 0)
202            .then(|| MultiHostPriority::try_from(priority))
203            .transpose()?;
204
205        Ok(MultiHostStatus {
206            event,
207            state,
208            priority,
209        })
210    }
211
212    /// Get busy status from the device.
213    pub fn busy_status(&self) -> Result<BusyStatus, MmcStatusError> {
214        let data = self.get_event_status_notification(EventMode::Polled, EventClass::DeviceBusy)?;
215        let input = &mut data.as_slice();
216        parse_header(EventClass::DeviceBusy, input)?;
217
218        let (event_code, status, time) = (parse_event_code, u8, be_u16)
219            .context(StrContext::Label("device busy event descriptor"))
220            .parse_next(input)?;
221        let event = (event_code != 0)
222            .then(|| BusyEvent::try_from(event_code))
223            .transpose()?;
224        let state = BusyState::try_from(status)?;
225        let time = (state != BusyState::NotBusy).then(|| Duration::from_millis(u64::from(time)));
226
227        Ok(BusyStatus {
228            event,
229            state,
230            busy_time: time,
231        })
232    }
233}
234type EventData = [u8; 12];
235
236bitflags! {
237    /// Notification class of `GET EVENT STATUS NOTIFICATION` command
238    #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
239    pub struct EventClass: u8 {
240        /// Change of operational capabilities or parameters for the drive.
241        const OperationalChange = 1 << 1;
242        /// Changes to power status.
243        const PowerManagement = 1 << 2;
244        /// External requests such as a remote or a button.
245        const ExternalRequest = 1 << 3;
246        /// Media related changes
247        const Media = 1 << 4;
248        /// Requests for control by other hosts.
249        const MultiHost = 1 << 5;
250        /// Commands that are executing but require a long time to complete.
251        const DeviceBusy = 1 << 6;
252    }
253}
254
255/// error from a `GET EVENT STATUS NOTIFICATION` command
256#[non_exhaustive]
257#[derive(Debug, Display, Error)]
258pub enum MmcStatusError {
259    /// error performing MMC command
260    Cmd(#[from] MmcError),
261
262    /// invalid response from mmc command: {0}
263    InvalidResponse(String),
264
265    /// device does not support: {0:?}
266    EventNotSupported(EventClass),
267}
268
269impl From<ContextError> for MmcStatusError {
270    fn from(err: ContextError) -> Self {
271        Self::InvalidResponse(err.to_string())
272    }
273}
274
275impl<T: TryFromPrimitive> From<TryFromPrimitiveError<T>> for MmcStatusError {
276    fn from(err: TryFromPrimitiveError<T>) -> Self {
277        Self::InvalidResponse(err.to_string())
278    }
279}
280
281/// Operation mode of `GET EVENT STATUS NOTIFICATION` command
282#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
283enum EventMode {
284    /// Asynchronous events
285    #[allow(unused)]
286    Async,
287
288    /// Polled events
289    #[default]
290    Polled,
291}
292
293/// Validate the header with the expected class and return the supported events
294fn parse_header(
295    expected_class: EventClass,
296    input: &mut &[u8],
297) -> Result<EventClass, MmcStatusError> {
298    debug!(header = ?input, "parse_header()");
299    let nea_and_notif_class = bits(separated_pair::<_, _, u8, u8, ContextError, _, _, _>(
300        bool,
301        bits_take(4_usize),
302        bits_take(3_usize),
303    ));
304    let ((nea, notif_class), supported_events, remaining) =
305        length_and_then(be_u16, (nea_and_notif_class, u8, rest))
306            .context(StrContext::Label(
307                "GET EVENT STATUS NOTIFICATION response header",
308            ))
309            .parse_next(input)?;
310    *input = remaining;
311    if !expected_class.is_empty() && nea {
312        return Err(MmcStatusError::EventNotSupported(expected_class));
313    }
314    let event_class = 1 << notif_class;
315    if !expected_class.is_empty() && event_class != expected_class.bits() {
316        return Err(MmcStatusError::InvalidResponse(format!(
317            "invalid event code, expected 0b{:b} got 0b{:b}",
318            expected_class.bits(),
319            event_class
320        )));
321    }
322
323    Ok(EventClass::from_bits_truncate(supported_events))
324}
325
326/// Status of operational changes to the device
327#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
328pub struct OperationalStatus {
329    /// Most recent operational change event on the device
330    pub event: Option<OperationalEvent>,
331
332    /// Persistent prevent state is active.
333    ///
334    /// Upon entering the Persistent Prevent state, the Drive shall disable any eject mechanisms, and all media after
335    /// initial media spin up shall remain locked in the Drive until the Host issues an eject request, or the Persistent
336    /// Prevent status is reset and the hardware eject mechanism again becomes available.
337    pub persistent_prevent: bool,
338}
339/// Source of operational changes to the device
340#[repr(u16)]
341#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, TryFromPrimitive)]
342pub enum OperationalEvent {
343    /// An unspecified event may have changed feature currency
344    #[default]
345    FeatureChange = 0x1,
346
347    /// The feature list may have added current features
348    NewFeatures = 0x2,
349
350    /// The logical unit has been reset
351    Reset = 0x3,
352
353    /// The logical unit's microcode may have changed
354    FirmwareChanged = 0x4,
355
356    /// The logical unit's identification information may have changed
357    InquiryChange = 0x5,
358}
359
360/// Take a byte and interpret the lowest four bits
361fn parse_event_code(input: &mut &[u8]) -> winnow::Result<u8> {
362    bits(preceded(
363        take::<_, _, ContextError>(4_usize),
364        bits_take(4_usize),
365    ))
366    .context(StrContext::Label("event code"))
367    .parse_next(input)
368}
369
370/// Changes to power status
371#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
372pub struct PowerStatus {
373    /// Most recent power management event on the device
374    pub event: Option<PowerEvent>,
375
376    /// Current power state of the drive
377    pub state: PowerState,
378}
379/// The power change event
380#[repr(u8)]
381#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, TryFromPrimitive)]
382pub enum PowerEvent {
383    /// The drive successfully changed to the specified power state
384    #[default]
385    PwrChgSuccessful = 0x1,
386
387    /// The drive failed to enter the last requested state and is still
388    /// operating at the power state specified in the `status` field
389    PwrChgFail = 0x2,
390}
391/// The current power state of the drive
392#[repr(u8)]
393#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, TryFromPrimitive)]
394pub enum PowerState {
395    /// Active
396    #[default]
397    Active = 0x1,
398
399    /// Idle
400    Idle = 0x2,
401
402    /// Standby
403    Standby = 0x3,
404
405    /// The drive is about to enter Sleep
406    Sleep = 0x4,
407}
408
409/// External requests such as a remote or a button
410#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
411pub struct ExternalRequestStatus {
412    /// Most recent external request event on the device
413    pub event: Option<ExternalRequestEvent>,
414
415    /// Device's ability to respond to the host
416    pub state: ExternalRequestState,
417
418    /// Operation requested or performed via an external request to the device
419    pub request: Option<ExternalRequest>,
420}
421/// External request events to the device
422#[repr(u8)]
423#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, TryFromPrimitive)]
424pub enum ExternalRequestEvent {
425    /// A front, back, or remote button has been depressed
426    DriveKeyDown = 0x1,
427
428    /// A front, back, or remote button has been released
429    DriveKeyUp = 0x2,
430
431    /// The drive has received a command from another host that requires an
432    /// action that may interfere with the persistent prevent owner's
433    /// operation
434    #[default]
435    ExternalRequestNotification = 0x3,
436}
437/// The device's ability to respond to the host
438#[repr(u8)]
439#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, TryFromPrimitive)]
440pub enum ExternalRequestState {
441    /// The drive is ready for operation
442    #[default]
443    Ready = 0x0,
444
445    /// Another host has an active persistent prevent
446    OtherPrevent = 0x1,
447}
448/// Operation requested or performed via an external request to the device
449#[repr(u16)]
450#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, TryFromPrimitive)]
451pub enum ExternalRequest {
452    /// The request queue has overflowed. External Request events may be lost.
453    Overrun = 0x1,
454
455    /// The play button was pressed or was requested by another host.
456    Play = 0x101,
457
458    /// The rewind/back button was pressed or was requested by another host.
459    RewindOrBack = 0x102,
460
461    /// The fast forward button was pressed or was requested by another host.
462    FastForward = 0x103,
463
464    /// The pause button was pressed or was requested by another host.
465    Pause = 0x104,
466
467    /// The stop button was pressed or was requested by another host.
468    Stop = 0x106,
469
470    /// A front panel button was pressed or was requested by another host.
471    Ascii = 0x107,
472
473    /// A vendor unique request
474    #[default]
475    #[num_enum(alternatives = [0xF001..=0xFFFF])]
476    VendorUnique = 0xF000,
477}
478
479/// Media related changes
480#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
481pub struct MediaStatus {
482    /// Most recent media event on the device
483    pub event: Option<MediaEvent>,
484
485    /// Current media state of the device
486    pub state: MediaState,
487
488    /// The first slot of a multiple slot drive the media status
489    /// notification applies to.
490    /// Only applies to drives that support multiple slots.
491    pub start_slot: u8,
492
493    /// The last slot of a multiple slot drive the media status
494    /// notification applies to.
495    /// Only applies to drives that support
496    pub end_slot: u8,
497}
498/// Media event
499#[repr(u8)]
500#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, TryFromPrimitive)]
501pub enum MediaEvent {
502    /// The drive has received a request from the user to eject the specified
503    /// slot or media
504    #[default]
505    EjectRequest = 0x1,
506
507    /// The specified slot has received new media and is ready to access it
508    NewMedia = 0x2,
509
510    /// The media has been removed from the specified slot and the drive is
511    /// unable to access the media without user intervention.
512    /// This applies to media changers only.
513    MediaRemoval = 0x3,
514
515    /// The user has requested that the media in the specified slot be loaded.
516    /// This applies to media changers only.
517    MediaChanged = 0x4,
518
519    /// A DVD+RW background format has completed.
520    BackgroundFormatCompleted = 0x5,
521
522    /// A DVD+RW background format has been automatically restarted by the
523    /// drive.
524    BackgroundFormatRestarted = 0x6,
525}
526bitflags! {
527    /// Media state
528    #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
529    pub struct MediaState: u8 {
530        /// The tray or door mechanism is in the open state.
531        const DoorOrTrayOpen = 1;
532
533        /// Media is present in the drive.
534        const MediaPresent = 1 << 1;
535    }
536}
537
538/// Requests for control by other hosts
539#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
540pub struct MultiHostStatus {
541    /// Most recent multiple host event on the device
542    pub event: Option<MultiHostEvent>,
543
544    /// The drive's ability to respond to the host
545    pub state: MultiHostState,
546
547    /// Priority of tasks relative to the other host
548    pub priority: Option<MultiHostPriority>,
549}
550/// Requests for drive control and state changes from other hosts
551#[repr(u8)]
552#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, TryFromPrimitive)]
553pub enum MultiHostEvent {
554    /// Another host has requested drive control
555    #[default]
556    ControlRequest = 0x1,
557
558    /// Another host has received drive control
559    ControlGrant = 0x2,
560
561    /// Another host has released drive control
562    ControlRelease = 0x3,
563}
564/// Ability of the drive to respond to the host
565#[repr(u8)]
566#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, TryFromPrimitive)]
567pub enum MultiHostState {
568    /// The drive is ready for operation
569    #[default]
570    Ready = 0x0,
571
572    /// Another host has an active persistent prevent.
573    OtherPrevent = 0x1,
574}
575/// Priority of tasks relative to the other host
576#[repr(u16)]
577#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, TryFromPrimitive)]
578pub enum MultiHostPriority {
579    /// No tasks pending on the host
580    #[default]
581    Low = 0x1,
582
583    /// No critical tasks pending on the host
584    Medium = 0x2,
585
586    /// There are critical tasks pending on the host
587    High = 0x3,
588}
589
590/// Used to notify the host of commands that are executing but require an
591/// abnormally long time to complete.
592#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
593pub struct BusyStatus {
594    /// Most recent device busy event on the device
595    pub event: Option<BusyEvent>,
596
597    /// Current busy state of the device
598    pub state: BusyState,
599
600    /// Predicted amount of time remaining for the device to become not busy
601    /// if the drive is currently busy.
602    pub busy_time: Option<Duration>,
603}
604/// Device busy events
605#[repr(u8)]
606#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, TryFromPrimitive)]
607pub enum BusyEvent {
608    /// The drive busy state has changed
609    #[default]
610    Change = 0x1,
611
612    /// The drive busy condition has been changed by a loading/unloading
613    /// operation that is not caused by command execution
614    LoChange = 0x2,
615}
616/// Busy state of the device
617#[repr(u8)]
618#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, TryFromPrimitive)]
619pub enum BusyState {
620    /// The drive is not busy
621    #[default]
622    NotBusy = 0x0,
623
624    /// The drive is busy
625    Busy = 0x1,
626}
627
628#[cfg(test)]
629mod tests {
630    use super::*;
631
632    use tracing::info;
633
634    #[test_log::test(test)]
635    #[ignore = "requires a disc drive with mmc"]
636    fn supported_events() {
637        Mmc::new().unwrap().supported_events().unwrap();
638    }
639
640    #[test_log::test(test)]
641    #[ignore = "requires a disc drive with mmc"]
642    fn is_tray_open() {
643        let is_tray_open = Mmc::new().unwrap().is_tray_open();
644        info!(?is_tray_open);
645        assert!(matches!(
646            is_tray_open,
647            Ok(_) | Err(MmcStatusError::EventNotSupported(_))
648        ));
649    }
650
651    #[test_log::test(test)]
652    #[ignore = "requires a disc drive with mmc"]
653    fn operational_status() {
654        let status = Mmc::new().unwrap().operational_event_status();
655        info!(?status);
656        assert!(matches!(
657            status,
658            Ok(_) | Err(MmcStatusError::EventNotSupported(_))
659        ));
660    }
661
662    #[test_log::test(test)]
663    #[ignore = "requires a disc drive with mmc"]
664    fn power_status() {
665        let status = Mmc::new().unwrap().power_status();
666        info!(?status);
667        assert!(matches!(
668            status,
669            Ok(_) | Err(MmcStatusError::EventNotSupported(_))
670        ));
671    }
672
673    #[test_log::test(test)]
674    #[ignore = "requires a disc drive with mmc"]
675    fn external_status() {
676        let status = Mmc::new().unwrap().external_status();
677        info!(?status);
678        assert!(matches!(
679            status,
680            Ok(_) | Err(MmcStatusError::EventNotSupported(_))
681        ));
682    }
683
684    #[test_log::test(test)]
685    #[ignore = "requires a disc drive with mmc"]
686    fn media_status() {
687        let status = Mmc::new().unwrap().media_status();
688        info!(?status);
689        assert!(matches!(
690            status,
691            Ok(_) | Err(MmcStatusError::EventNotSupported(_))
692        ));
693    }
694
695    #[test_log::test(test)]
696    #[ignore = "requires a disc drive with mmc"]
697    fn multihost_status() {
698        let status = Mmc::new().unwrap().multihost_status();
699        info!(?status);
700        assert!(matches!(
701            status,
702            Ok(_) | Err(MmcStatusError::EventNotSupported(_))
703        ));
704    }
705
706    #[test_log::test(test)]
707    #[ignore = "requires a disc drive with mmc"]
708    fn busy_status() {
709        let status = Mmc::new().unwrap().busy_status();
710        info!(?status);
711        assert!(matches!(
712            status,
713            Ok(_) | Err(MmcStatusError::EventNotSupported(_))
714        ));
715    }
716}