Skip to main content

osdp/command/
text.rs

1//! `osdp_TEXT` (`0x6B`) — reader text output.
2//!
3//! # Spec: §6.12, Tables 21–22
4//!
5//! Body layout:
6//!
7//! ```text
8//! +--------+----------+--------+--------+--------+--------+-- ... --+
9//! | reader | command  | temp_t | row    | column | length | text…   |
10//! +--------+----------+--------+--------+--------+--------+-- ... --+
11//! ```
12
13use crate::error::Error;
14use alloc::vec::Vec;
15
16/// Text command codes (Table 21).
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18#[repr(u8)]
19#[allow(missing_docs)]
20pub enum TextCommand {
21    PermanentNoWrap = 0x01,
22    PermanentWrap = 0x02,
23    TemporaryNoWrap = 0x03,
24    TemporaryWrap = 0x04,
25}
26
27impl TextCommand {
28    /// Parse from byte.
29    pub const fn from_byte(b: u8) -> Result<Self, Error> {
30        Ok(match b {
31            0x01 => Self::PermanentNoWrap,
32            0x02 => Self::PermanentWrap,
33            0x03 => Self::TemporaryNoWrap,
34            0x04 => Self::TemporaryWrap,
35            _ => {
36                return Err(Error::MalformedPayload {
37                    code: 0x6B,
38                    reason: "unknown TEXT command code",
39                });
40            }
41        })
42    }
43
44    /// Raw byte.
45    pub const fn as_byte(self) -> u8 {
46        self as u8
47    }
48}
49
50/// `osdp_TEXT` body.
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct Text {
53    /// Reader number.
54    pub reader: u8,
55    /// Text command code.
56    pub command: TextCommand,
57    /// Temporary on-time in seconds (1..=255). Set 0 for permanent commands.
58    pub temp_time_s: u8,
59    /// Display row (1..).
60    pub row: u8,
61    /// Display column (1..).
62    pub column: u8,
63    /// Printable ASCII text (`0x20..=0x7E`).
64    pub text: Vec<u8>,
65}
66
67impl Text {
68    /// Spec-compliant ASCII bound.
69    fn validate_ascii(s: &[u8]) -> Result<(), Error> {
70        for &b in s {
71            if !(0x20..=0x7E).contains(&b) {
72                return Err(Error::MalformedPayload {
73                    code: 0x6B,
74                    reason: "TEXT must be printable ASCII",
75                });
76            }
77        }
78        Ok(())
79    }
80
81    /// Encode.
82    pub fn encode(&self) -> Result<Vec<u8>, Error> {
83        Self::validate_ascii(&self.text)?;
84        if self.text.len() > u8::MAX as usize {
85            return Err(Error::MalformedPayload {
86                code: 0x6B,
87                reason: "TEXT length exceeds 255 bytes",
88            });
89        }
90        let mut out = Vec::with_capacity(6 + self.text.len());
91        out.push(self.reader);
92        out.push(self.command.as_byte());
93        out.push(self.temp_time_s);
94        out.push(self.row);
95        out.push(self.column);
96        out.push(self.text.len() as u8);
97        out.extend_from_slice(&self.text);
98        Ok(out)
99    }
100
101    /// Decode.
102    pub fn decode(data: &[u8]) -> Result<Self, Error> {
103        if data.len() < 6 {
104            return Err(Error::MalformedPayload {
105                code: 0x6B,
106                reason: "TEXT requires at least 6 bytes",
107            });
108        }
109        let length = data[5] as usize;
110        if data.len() != 6 + length {
111            return Err(Error::MalformedPayload {
112                code: 0x6B,
113                reason: "TEXT length field disagrees with payload",
114            });
115        }
116        let text = data[6..6 + length].to_vec();
117        Self::validate_ascii(&text)?;
118        Ok(Self {
119            reader: data[0],
120            command: TextCommand::from_byte(data[1])?,
121            temp_time_s: data[2],
122            row: data[3],
123            column: data[4],
124            text,
125        })
126    }
127}