dvb-ci 0.9.1

DVB Common Interface (EN 50221) — APDU/resource objects, session/transport PDUs, and a CA_PMT builder from a dvb-si PMT.
Documentation
//! Power Manager objects — ETSI TS 101 699 V1.1.1 §6.3, Tables 52-55
//! (PDF pp. 51-52). See `docs/ci_plus/power-manager.md`.
//!
//! Resource ID `0x00220041`. Lets a module tell the host it is busy with a task
//! that should complete before the host powers down.
//!
//! - `activation_state_change_request` (`9F 80 00`, Table 52) — host asks the
//!   module to change activation state.
//! - `activation_state_change_ack` (`9F 80 01`, Table 54) — module's reply.

use crate::error::{Error, Result};
use crate::objects;
use broadcast_common::{Parse, Serialize};

/// Resource-scoped `apdu_tag`s for the Power Manager (Tables 52, 54).
pub mod tag {
    use crate::tag::ApduTag;
    /// `activation_status_change_request_tag` = `9F 80 00`.
    pub const ACTIVATION_STATE_CHANGE_REQUEST: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x00);
    /// `activation_status_change_ack_tag` = `9F 80 01`.
    pub const ACTIVATION_STATE_CHANGE_ACK: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x01);
}

/// `activation_state` — the requested power mode (Table 53).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum ActivationState {
    /// `0` — Standby-passive (the EACEM-defined power mode).
    StandbyPassive,
    /// `1`-`15` — reserved for future use.
    Reserved(u8),
}

impl ActivationState {
    /// Decode the 4-bit `activation_state`.
    #[must_use]
    pub fn from_u8(v: u8) -> Self {
        match v & 0x0F {
            0 => Self::StandbyPassive,
            other => Self::Reserved(other),
        }
    }
    /// 4-bit wire value.
    #[must_use]
    pub const fn to_u8(self) -> u8 {
        match self {
            Self::StandbyPassive => 0,
            Self::Reserved(v) => v & 0x0F,
        }
    }
    /// Spec token, or `"reserved"`.
    #[must_use]
    pub fn name(&self) -> &'static str {
        match self {
            Self::StandbyPassive => "Standby-passive",
            Self::Reserved(_) => "reserved",
        }
    }
}
broadcast_common::impl_spec_display!(ActivationState, Reserved);

/// `reply_code` — the module's response to a state-change request (Table 55).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum ReplyCode {
    /// `0` — OK to change state.
    Ok,
    /// `1` — Module busy, don't change state.
    Busy,
    /// `2`-`255` — reserved for future use.
    Reserved(u8),
}

impl ReplyCode {
    /// Decode the `reply_code` byte.
    #[must_use]
    pub fn from_u8(v: u8) -> Self {
        match v {
            0 => Self::Ok,
            1 => Self::Busy,
            other => Self::Reserved(other),
        }
    }
    /// Wire byte.
    #[must_use]
    pub const fn to_u8(self) -> u8 {
        match self {
            Self::Ok => 0,
            Self::Busy => 1,
            Self::Reserved(v) => v,
        }
    }
    /// Spec token, or `"reserved"`.
    #[must_use]
    pub fn name(&self) -> &'static str {
        match self {
            Self::Ok => "OK to change state",
            Self::Busy => "Module busy, don't change state",
            Self::Reserved(_) => "reserved",
        }
    }
}
broadcast_common::impl_spec_display!(ReplyCode, Reserved);

/// `activation_state_change_request()` (Table 52): host → module.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ActivationStateChangeRequest {
    /// The requested new activation state (low 4 bits; top 4 bits reserved).
    pub activation_state: ActivationState,
}

/// `activation_state_change_ack()` (Table 54): module → host.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ActivationStateChangeAck {
    /// The module's response.
    pub reply_code: ReplyCode,
}

// reserved(4) + activation_state(4).
const REQUEST_BODY: usize = 1;
// reply_code(8).
const ACK_BODY: usize = 1;

impl<'a> Parse<'a> for ActivationStateChangeRequest {
    type Error = Error;
    fn parse(bytes: &'a [u8]) -> Result<Self> {
        let body = objects::parse_apdu_header(
            bytes,
            tag::ACTIVATION_STATE_CHANGE_REQUEST,
            "activation_state_change_request",
        )?;
        if body.len() < REQUEST_BODY {
            return Err(Error::BufferTooShort {
                need: REQUEST_BODY,
                have: body.len(),
                what: "activation_state_change_request",
            });
        }
        crate::objects::reject_trailing_body(
            body,
            REQUEST_BODY,
            "activation_state_change_request",
        )?;
        Ok(Self {
            activation_state: ActivationState::from_u8(body[0] & 0x0F),
        })
    }
}
impl Serialize for ActivationStateChangeRequest {
    type Error = Error;
    fn serialized_len(&self) -> usize {
        objects::apdu_len(REQUEST_BODY)
    }
    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
        let pos =
            objects::write_apdu_header(tag::ACTIVATION_STATE_CHANGE_REQUEST, REQUEST_BODY, buf)?;
        // reserved(4)='0000', activation_state(4).
        buf[pos] = self.activation_state.to_u8() & 0x0F;
        Ok(pos + REQUEST_BODY)
    }
}

impl<'a> Parse<'a> for ActivationStateChangeAck {
    type Error = Error;
    fn parse(bytes: &'a [u8]) -> Result<Self> {
        let body = objects::parse_apdu_header(
            bytes,
            tag::ACTIVATION_STATE_CHANGE_ACK,
            "activation_state_change_ack",
        )?;
        if body.len() < ACK_BODY {
            return Err(Error::BufferTooShort {
                need: ACK_BODY,
                have: body.len(),
                what: "activation_state_change_ack",
            });
        }
        crate::objects::reject_trailing_body(body, ACK_BODY, "activation_state_change_ack")?;
        Ok(Self {
            reply_code: ReplyCode::from_u8(body[0]),
        })
    }
}
impl Serialize for ActivationStateChangeAck {
    type Error = Error;
    fn serialized_len(&self) -> usize {
        objects::apdu_len(ACK_BODY)
    }
    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
        let pos = objects::write_apdu_header(tag::ACTIVATION_STATE_CHANGE_ACK, ACK_BODY, buf)?;
        buf[pos] = self.reply_code.to_u8();
        Ok(pos + ACK_BODY)
    }
}

crate::dispatch::declare_resource_apdus! {
    /// Resource-scoped dispatch over the Power Manager objects.
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
    #[non_exhaustive]
    pub enum PowerManagerApdu ("power_manager") {
        /// `activation_state_change_request` (`9F 80 00`).
        Request(ActivationStateChangeRequest) = tag::ACTIVATION_STATE_CHANGE_REQUEST,
        /// `activation_state_change_ack` (`9F 80 01`).
        Ack(ActivationStateChangeAck) = tag::ACTIVATION_STATE_CHANGE_ACK,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn request_round_trips_and_bites() {
        let r = ActivationStateChangeRequest {
            activation_state: ActivationState::StandbyPassive,
        };
        let bytes = r.to_bytes();
        assert_eq!(bytes, [0x9F, 0x80, 0x00, 0x01, 0x00]);
        assert_eq!(ActivationStateChangeRequest::parse(&bytes).unwrap(), r);
        assert_eq!(r.activation_state.name(), "Standby-passive");
        let other = ActivationStateChangeRequest {
            activation_state: ActivationState::Reserved(5),
        };
        assert_ne!(bytes, other.to_bytes());
        assert_eq!(other.to_bytes()[4], 0x05);
    }

    #[test]
    fn ack_round_trips_and_bites() {
        let a = ActivationStateChangeAck {
            reply_code: ReplyCode::Busy,
        };
        let bytes = a.to_bytes();
        assert_eq!(bytes, [0x9F, 0x80, 0x01, 0x01, 0x01]);
        assert_eq!(ActivationStateChangeAck::parse(&bytes).unwrap(), a);
        assert_eq!(a.reply_code.name(), "Module busy, don't change state");
        let other = ActivationStateChangeAck {
            reply_code: ReplyCode::Ok,
        };
        assert_ne!(bytes, other.to_bytes());
    }

    /// `declare_resource_apdus!` surface: the declared tag list, and the
    /// error a foreign tag produces (names the first declared tag).
    #[test]
    fn declared_dispatch_lists_tags_and_rejects_foreign_ones() {
        let tags: alloc::vec::Vec<u32> =
            PowerManagerApdu::TAGS.iter().map(|t| t.as_u24()).collect();
        assert_eq!(tags, [0x9F_8000, 0x9F_8001]);
        // 9F8002 is not a Power Manager object.
        match PowerManagerApdu::parse(&[0x9F, 0x80, 0x02, 0x00]) {
            Err(Error::UnexpectedApduTag {
                got,
                expected,
                what,
            }) => {
                assert_eq!(got, 0x9F_8002);
                assert_eq!(expected, 0x9F_8000);
                assert_eq!(what, "power_manager");
            }
            other => panic!("expected UnexpectedApduTag, got {other:?}"),
        }
        assert!(matches!(
            PowerManagerApdu::parse(&[0x9F, 0x80]),
            Err(Error::BufferTooShort {
                need: 3,
                have: 2,
                ..
            })
        ));
    }

    #[test]
    fn dispatch_routes_both_tags() {
        let req = ActivationStateChangeRequest {
            activation_state: ActivationState::StandbyPassive,
        }
        .to_bytes();
        assert!(matches!(
            PowerManagerApdu::parse(&req).unwrap(),
            PowerManagerApdu::Request(_)
        ));
        let ack = ActivationStateChangeAck {
            reply_code: ReplyCode::Ok,
        }
        .to_bytes();
        let parsed = PowerManagerApdu::parse(&ack).unwrap();
        assert!(matches!(parsed, PowerManagerApdu::Ack(_)));
        assert_eq!(parsed.to_bytes(), ack);
    }
}