ebds 0.4.2

Messages and related types for implementing the EBDS serial communication protocol
Documentation
use crate::std;
use std::fmt;

use crate::{
    banknote::*, impl_extended_ops, impl_message_ops, impl_omnibus_extended_reply,
    len::EXTENDED_NOTE_REPLY, status::*, ExtendedCommand, ExtendedCommandOps, MessageOps,
    MessageType, OmnibusReplyOps,
};

impl From<&ExtendedNoteReply> for Banknote {
    fn from(reply: &ExtendedNoteReply) -> Self {
        let base_value: f32 = reply.base_value().into();
        let exponent: f32 = reply.exponent().into();

        let value = match reply.sign() {
            Sign::Positive => base_value * 10f32.powf(exponent),
            Sign::Negative => base_value * 10f32.powf(-exponent),
        };

        Self::new(
            value,
            reply.iso_code(),
            reply.note_type(),
            reply.note_series(),
            reply.note_compatibility(),
            reply.note_version(),
            reply.banknote_classification(),
        )
    }
}

impl From<ExtendedNoteReply> for Banknote {
    fn from(reply: ExtendedNoteReply) -> Self {
        Self::from(&reply)
    }
}

impl From<&ExtendedNoteReply> for NoteTableItem {
    fn from(e: &ExtendedNoteReply) -> NoteTableItem {
        Self::new(e.note_index(), e.into())
    }
}

impl From<ExtendedNoteReply> for NoteTableItem {
    fn from(e: ExtendedNoteReply) -> NoteTableItem {
        (&e).into()
    }
}

impl From<&ExtendedNoteReply> for DocumentStatus {
    fn from(reply: &ExtendedNoteReply) -> Self {
        let status = DocumentStatus::default().with_standard_denomination(reply.note_value());

        match reply.banknote_classification() {
            BanknoteClassification::Genuine | BanknoteClassification::DisabledOrNotSupported => {
                status.with_accepted_note_table_item(AcceptedNoteTableItem::new(
                    reply.into(),
                    reply.orientation(),
                ))
            }
            _ => status,
        }
    }
}

pub mod index {
    use super::{BaseValue, Exponent, ISOCode};

    pub const EXTENDED: usize = 10;
    pub const NOTE_INDEX: usize = EXTENDED;
    pub const ISO_CODE: usize = EXTENDED + 1;
    pub const ISO_CODE_END: usize = ISO_CODE + ISOCode::LEN;
    pub const BASE_VALUE: usize = EXTENDED + 4;
    pub const BASE_VALUE_END: usize = BASE_VALUE + BaseValue::LEN;
    pub const SIGN: usize = EXTENDED + 7;
    pub const EXPONENT: usize = EXTENDED + 8;
    pub const EXPONENT_END: usize = EXPONENT + Exponent::LEN;
    pub const ORIENTATION: usize = EXTENDED + 10;
    pub const NOTE_TYPE: usize = EXTENDED + 11;
    pub const NOTE_SERIES: usize = EXTENDED + 12;
    pub const NOTE_COMPATIBILITY: usize = EXTENDED + 13;
    pub const NOTE_VERSION: usize = EXTENDED + 14;
    pub const BANKNOTE_CLASSIFICATION: usize = EXTENDED + 15;
}

/// Extended Note Specification - Reply (Subtype 0x02)
///
/// ExtendedNoteReply represents a message sent from the device back to the host
///
/// The reply contains 18 additional bytes of data that describe the bank note in great detail.
///
/// This message can be sent from the device for two reasons:
///
/// * Response to a host’s query extended note command
/// * Device is running in extended note mode and a valid banknote has either reached escrow or been stacked.
///
/// The Extended Note Reply is formatted as follows:
///
/// | Name  | STX  | LEN  | CTRL | Subtype | Data 0 | Data 1 | Data 2 | Data 3 | Data 4 | Data 5 | ExtData 0 | ... | ExtData 17 | ETX  | CHK |
/// |:------|:----:|:----:|:----:|:-------:|:------:|:------:|:------:|:------:|:------:|:------:|:---------:|:---:|:----------:|:----:|:---:|
/// | Byte  | 0    | 1    | 2    | 3       | 4      | 5      | 6      | 7      | 8      | 9      | 10        | ... | 27         | 28   | 29  |
/// | Value | 0x02 | 0x1E | 0x7n | 0x03    | nn     | nn     | nn     | nn     | nn     | nn     | nn        | nn  | nn         | 0x03 | zz  |
///
/// If this is a reply to the Host Query Command, the index will match the same value sent from the host.
/// Otherwise, the index value is not used and set to 0x00 for any escrowed or stacked notes.
///
/// | Field                   | Byte Offset | Field Description                                    | Sample Value <br> (2000 Yen Note)  |
/// |:------------------------|:-----------:|:-----------------------------------------------------|:----------------------------------:|
/// | Index                   | 0           | Not used for escrow or stacked notes                 | 0x00                               |
/// | ISO Code                | 1..3        | A three character ASCII currecny code,<br> see <https://en.wikipedia.org/wiki/ISO_4217> | "JPY" |
/// | Base Value              | 4..6        | A three character ASCII coded decimal value          | "002"                              |
/// | Sign                    | 7           | An ASCII coded sign value for the Exponent ("+"/"-") | "+"                                |
/// | Exponent                | 8..9        | An ASCII coded decimal power of ten to [multiply "+", divide "-"] the Base Value | "03"   |
/// | Orientation             | 10          | A single character binary field that encodes the orientation of the bank note.<br><br>0x00 = Right Edge, Face Up<br>0x01 = Right Edge, Face Down<br>0x02 = Left Edge, Face Up<br>0x03 = Left Edge, Face Down<br><br>Note: in general this field is only correct if the Extended Orientation bit is set in the device capabilities map.| 0x00 |
/// | Type                    | 11          | An ASCII letter that documents the note type.<br>This corresponds to the data in the variant identity card. | "A" |
/// | Series                  | 12          | An ASCII letter that documents the note series.<br>This corresponds to the data in the variant identity card. | "A" |
/// | Compatibility           | 13          | An ASCII letter that documents the revision of the compatibility core used.<br>This corresponds to the data in the variant identity card. | "B" |
/// | Version                 | 14          | An ASCII letter that documents the version of the note's recognition criteria.<br>This corresponds to the data in the variant identity card. | "A" |
/// | Banknote Classification | 15          | 0x00 = Sent for any of the following:<ul><li>In response to a Host Query Extended Note Specification Command (i.e. host requests a note table element).</li><li>In response to a note escrowed or stacked event while device is in extended note mode and classification is:<ul><li>Supported by the device but disabled.</li><li>NOT supported by the device.</li></ul></li></ul><br>**SC Adv Classification**<br>**SCR Classification**<br>0x01 = Class 1 (unidentified banknote)<br>0x02 = Class 2 (suspected counterfeit)<br>0x03 = Class 3 (suspected zero value note)<br>0x04 = Class 4 (genuine banknote) | 0x00 |
/// | Reserved                | 16..17      | Bytes reserved for future use                         | N/A                             |
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ExtendedNoteReply {
    buf: [u8; EXTENDED_NOTE_REPLY],
}

impl ExtendedNoteReply {
    /// Create a new ExtendedNoteReply message
    pub fn new() -> Self {
        let mut message = Self {
            buf: [0u8; EXTENDED_NOTE_REPLY],
        };

        message.init();
        message.set_message_type(MessageType::Extended);
        message.set_extended_command(ExtendedCommand::ExtendedNoteSpecification);

        message
    }

    /// Get the note index (invalid for escrowed and stacked notes).
    pub fn note_index(&self) -> usize {
        self.buf[index::NOTE_INDEX] as usize
    }

    /// Get the ISO 4217 code
    pub fn iso_code(&self) -> ISOCode {
        self.buf[index::ISO_CODE..index::ISO_CODE_END]
            .as_ref()
            .into()
    }

    /// Get the note's base value
    pub fn base_value(&self) -> BaseValue {
        self.buf[index::BASE_VALUE..index::BASE_VALUE_END]
            .as_ref()
            .into()
    }

    /// Get the note's sign
    pub fn sign(&self) -> Sign {
        self.buf[index::SIGN].into()
    }

    /// Get the note's exponent
    pub fn exponent(&self) -> Exponent {
        self.buf[index::EXPONENT..index::EXPONENT_END]
            .as_ref()
            .into()
    }

    /// Get the note's orientation
    pub fn orientation(&self) -> BanknoteOrientation {
        self.buf[index::ORIENTATION].into()
    }

    /// Get the note's type
    pub fn note_type(&self) -> NoteType {
        self.buf[index::NOTE_TYPE].into()
    }

    /// Get the note's series
    pub fn note_series(&self) -> NoteSeries {
        self.buf[index::NOTE_SERIES].into()
    }

    /// Get the note's compatibility
    pub fn note_compatibility(&self) -> NoteCompatibility {
        self.buf[index::NOTE_COMPATIBILITY].into()
    }

    /// Get the note's version
    pub fn note_version(&self) -> NoteVersion {
        self.buf[index::NOTE_VERSION].into()
    }

    /// Get the note's banknote classification
    pub fn banknote_classification(&self) -> BanknoteClassification {
        self.buf[index::BANKNOTE_CLASSIFICATION].into()
    }

    /// Check if the reply is null
    pub fn is_null(&self) -> bool {
        let mut res = true;
        self.buf[index::EXTENDED..self.etx_index()]
            .iter()
            .for_each(|&b| {
                if res && b != 0 {
                    res = false
                }
            });
        res
    }
}

impl_message_ops!(ExtendedNoteReply);
impl_extended_ops!(ExtendedNoteReply);
impl_omnibus_extended_reply!(ExtendedNoteReply);

impl fmt::Display for ExtendedNoteReply {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "AckNak: {}, DeviceType: {}, MessageType: {}, DeviceState: {}, DeviceStatus: {}, ExceptionStatus: {}, MiscDeviceState: {}, ModelNumber: {}, CodeRevision: {}, Banknote: {}",
            self.acknak(),
            self.device_type(),
            self.message_type(),
            self.device_state(),
            self.device_status(),
            self.exception_status(),
            self.misc_device_state(),
            self.model_number(),
            self.code_revision(),
            Banknote::from(self),
        )
    }
}

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

    #[test]
    #[rustfmt::skip]
    fn test_extended_note_reply_from_bytes() -> Result<()> {
        let mut msg_bytes = [
            // STX | LEN | Message type | Subtype
            0x02, 0x1e, 0x70, 0x02,
            // Data
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            // Index
            0x00,
            // ISO code
            0x00, 0x00, 0x00,
            // Base value
            0x00, 0x00, 0x00,
            // Sign
            0x00,
            // Exponent
            0x00, 0x00,
            // Orientation
            0x00,
            // Note type
            0x00,
            // Note series
            0x00,
            // Note compatibility
            0x00,
            // Note version
            0x00,
            // Banknote classification
            0x00,
            // Reserved
            0x00, 0x00,
            // ETX | Checksum
            0x03, 0x36,
        ];

        msg_bytes[index::ISO_CODE..index::ISO_CODE_END].copy_from_slice(b"JPY".as_ref());
        msg_bytes[index::BASE_VALUE..index::BASE_VALUE_END].copy_from_slice(b"002".as_ref());
        msg_bytes[index::SIGN] = b'+';
        msg_bytes[index::EXPONENT..index::EXPONENT_END].copy_from_slice(b"03".as_ref());
        msg_bytes[index::ORIENTATION] = BanknoteOrientation::RightEdgeFaceUp as u8;
        msg_bytes[index::NOTE_TYPE] = b'A';
        msg_bytes[index::NOTE_SERIES] = b'A';
        msg_bytes[index::NOTE_COMPATIBILITY] = b'B';
        msg_bytes[index::NOTE_VERSION] = b'A';
        msg_bytes[index::BANKNOTE_CLASSIFICATION] = BanknoteClassification::DisabledOrNotSupported as u8;

        let mut msg = ExtendedNoteReply::new();
        msg.from_buf(msg_bytes.as_ref())?;

        assert_eq!(msg.message_type(), MessageType::Extended);
        assert_eq!(msg.extended_command(), ExtendedCommand::ExtendedNoteSpecification);
        assert_eq!(msg.note_index(), 0);
        assert_eq!(msg.iso_code(), ISOCode::JPY);
        assert_eq!(msg.base_value(), BaseValue::from(b"002"));
        assert_eq!(msg.sign(), Sign::Positive);
        assert_eq!(msg.exponent(), Exponent::from(b"03"));
        assert_eq!(msg.orientation(), BanknoteOrientation::RightEdgeFaceUp);
        assert_eq!(msg.note_type(), NoteType::from(b'A'));
        assert_eq!(msg.note_series(), NoteSeries::from(b'A'));
        assert_eq!(msg.note_compatibility(), NoteCompatibility::from(b'B'));
        assert_eq!(msg.note_version(), NoteVersion::from(b'A'));
        assert_eq!(msg.banknote_classification(), BanknoteClassification::DisabledOrNotSupported);

        assert_eq!(Banknote::from(msg).value(), 2000.0);

        Ok(())
    }
}