airtouch5 0.2.0

A library for communicating with AirTouch 5 air conditioning system control consoles
Documentation
//! Utilities for implementing control and status message type
//!
//! Use the `control_message` and `status_message` macros to define the
//! control request and status response structs and theie required
//! `ControlStatusMessage` trait functions, respectively, and automatically
//! implement the `TryFrom` conversions to and from a `Frame`.
//!
//! # Unsolicited status messages
//!
//! Status messages are sent both as a response to a contol request, and also
//! independantly of any request. They may occur, for axmaple, in response to
//! some exterally or internally induced change in status, or periodically
//! during the lifetime of the connection.
//!
//! # Message ID
//!
//! Each message struct has a `message_id: u8` field. This is added
//! automatically by the `control_message` and `status_message` macros. The
//! `message_id` for a control request should be a unique-ish number;
//! typically use `next_msg_id()` to generate the ID for a request. The
//! `messge_id` for a status message in response to a control request is taken
//! from the control request message it is a response to; the `message_id` for
//! an unsolicited status message is generated by the AitTouch 5 console.
//!
//! # Examples
//!
//! ```ignore
//! # use airtouch5::message::next_msg_id;
//! # use airtouch5::message::control_status::{control_status_message, ControlStatusMessage, ControlStatusMessageSubtype};
//! const SUBTYPE_EXAMPLE: ControlStatusMessageSubtype = 0x01;
//! control_status_message!(SUBTYPE_EXAMPLE,
//!     pub struct ExampleMessage {
//!         message: String,
//!         numbers: Vec<u32>,
//!     }
//!     {
//!         fn impl_frame_normal_len(&self) -> usize {
//!             self.message.len()
//!         }
//!         fn impl_frame_normal_data<W: std::io::Write>(
//!             &self,
//!             dst: &mut W,
//!         ) -> Result<(), MessageError> {
//!             dst.write_all(self.message.as_bytes())?;
//!             Ok(())
//!         }
//!
//!         fn impl_frame_repeat_len(&self) -> usize {
//!             size_of::<u32>()
//!         }
//!         fn impl_frame_repeat_count(&self) -> u16 {
//!             self.numbers.len().try_into().unwrap_or(u16::MAX)
//!         }
//!         fn impl_frame_repeat_data<W: std::io::Write>(
//!             &self,
//!             index: u16,
//!             dst: &mut W,
//!         ) -> Result<(), MessageError> {
//!             dst.write_all(&self.numbers[index].to_be_bytes())?;
//!             Ok(())
//!         }
//!
//!         fn from_frame_data(
//!             message_id: u8,
//!             is_request: bool,
//!             normal_data: Vec<u8>,
//!             repeat_data: Vec<Vec<u8>>,
//!         ) -> Result<Self, MessageError> {
//!             let message = str::from_utf8(normal_data)?.to_string();
//!             if repeat_data.len() % size_of::<u32>() != 0 {
//!                 return Err(MessageError::InvalidData);
//!             }
//!             let numbers = repeat_data.into_iter()
//!                 .map(|v| u32::from_be_bytes(v.try_into().unwrap()))
//!                 .collect();
//!             Ok(Self { message_id, is_request, message, numbers })
//!         }
//!     }
//! );
//! ```

use super::MessageError;
use crate::conn::{frame::Frame, MessageKind};

pub(super) type ControlStatusMessageSubtype = u8;
const MSG_SUBTYPE_SIZE: usize = std::mem::size_of::<ControlStatusMessageSubtype>();

/// Generates a message that implements `ControlStatusMessage`. See module-
/// level docs for details.
macro_rules! control_status_message {
    ( $subtype:expr,
      $vis:vis struct $name:ident { $($fields:tt)* }
    $(,)?
    { $($impl:tt)* } ) => {
        use crate::conn::frame::Frame;
        use super::MessageError;
        #[derive(Clone, Debug, PartialEq)]
        $vis struct $name {
            message_id: u8,
            is_request: bool,
            $($fields)*
        }
        impl ControlStatusMessage for $name {
            const MSG_SUBTYPE: ControlStatusMessageSubtype = ($subtype);
            fn message_id(&self) -> u8 { self.message_id }
            fn is_request(&self) -> bool { self.is_request }
            $($impl)*
        }
        impl TryFrom<Frame> for $name {
            type Error = MessageError;
            fn try_from(value: Frame) -> Result<Self, Self::Error> { Self::from_frame(value) }
        }
        impl TryFrom<$name> for Frame {
            type Error = MessageError;
            fn try_from(value: $name) -> Result<Self, Self::Error> { value.into_frame() }
        }
    };
}
pub(super) use control_status_message;

/// The frame message type for extended messages. See ยง3.d.
const MSG_TYPE_CONTOL_STATUS: u8 = 0xc0;

/// Size of the "header" containing the subtype, repeat count and lengths
pub(super) const MSG_HEADER_SIZE: usize = 8;

/// Trait implemented by control and status messages.
pub(super) trait ControlStatusMessage: Sized {
    /// Each control and status message has a subtype. A subtype may be shared
    /// between a control message and a related status message.
    const MSG_SUBTYPE: ControlStatusMessageSubtype;

    /// Return the length of the encoded message normal data. This does not include
    /// the subtype or length fields. Must be implemented by each message struct.
    fn impl_frame_normal_len(&self) -> usize;

    /// Return the length of each encoded message repeat data. Must be implemented
    /// by each message struct.
    fn impl_frame_repeat_len(&self) -> usize;

    /// Return the count of repeat data in this message. Must be implemented by
    /// each message struct.
    fn impl_frame_repeat_count(&self) -> u16;

    /// Encode the message normal data (not including the subtype or lengths) to
    /// `dst`. Must be implemented by each message struct.
    fn impl_frame_normal_data<W: std::io::Write>(&self, dst: &mut W) -> Result<(), MessageError>;

    /// Encode a single message repear data to `dst`. Guaranteed to be called after
    /// `impl_frame_normal_data()`, `impl_frame_repeat_count()` times, with the
    /// indicies `(0..impl_frame_repeat_count())` in order. Implementor may use
    /// this guarantee to optimise their implementation. Must be implemented by
    /// each message struct.
    fn impl_frame_repeat_data<W: std::io::Write>(
        &self,
        index: u16,
        dst: &mut W,
    ) -> Result<(), MessageError>;

    /// Decode the control or status message from `normal_data` and zero or more
    /// `repeat_data`. Must be implemented by each mmessage struct.
    fn from_frame_data(
        message_id: u8,
        is_request: bool,
        normal_data: Vec<u8>,
        repeat_data: Vec<Vec<u8>>,
    ) -> Result<Self, MessageError>;

    /// The message ID of this message. This is typically automatically
    /// implemented by the `control_status_message` macro.
    fn message_id(&self) -> u8;

    /// Whether this message is a request from the client to the console. This is
    /// typically automatically implemented by the `control_status_message` macro.
    ///
    /// Control messages are always requests; status messages may be requests for
    /// a status update.
    fn is_request(&self) -> bool;

    fn from_frame(mut frame: Frame) -> Result<Self, MessageError> {
        if peek_subtype(&frame)? != Self::MSG_SUBTYPE {
            return Err(MessageError::IncorrectSubtype(super::Frame {
                inner: frame,
            }));
        }
        if frame.data.len() < MSG_HEADER_SIZE {
            return Err(MessageError::InvalidData);
        }

        // parse the header lengths and count
        let head: Vec<u8> = frame.data.drain(..MSG_HEADER_SIZE).collect();
        let mut i = head
            .chunks_exact(2)
            .map(|x| u16::from_be_bytes(x.try_into().unwrap()) as usize);
        let _ = i.next();
        let normal_len = i.next().unwrap();
        let repeat_len = i.next().unwrap();
        let repeat_count = i.next().unwrap();

        if frame.data.len() != normal_len + repeat_count * repeat_len {
            return Err(MessageError::InvalidData);
        }

        // split frame into normal data + repeat count * repeat data
        let normal_data = frame.data.drain(..normal_len).collect();
        let mut repeat_data = Vec::new();
        for _ in 0..repeat_count {
            repeat_data.push(frame.data.drain(..repeat_len).collect());
        }

        Self::from_frame_data(
            frame.msg_id,
            frame.kind == MessageKind::ControlRequest,
            normal_data,
            repeat_data,
        )
    }

    fn into_frame(self) -> Result<Frame, MessageError> {
        use std::io::Write;
        let kind = if self.is_request() {
            MessageKind::ControlRequest
        } else {
            MessageKind::StatusResponse
        };
        let (msg_type, address) = kind.into();
        let normal_len = self.impl_frame_normal_len();
        let repeat_len = self.impl_frame_repeat_len();
        let repeat_count = self.impl_frame_repeat_count();
        let mut data = std::io::Cursor::new(Vec::with_capacity(
            MSG_HEADER_SIZE + normal_len + repeat_len * repeat_count as usize,
        ));

        // write header
        data.write_all(&Self::MSG_SUBTYPE.to_be_bytes())?;
        data.write_all(&0u8.to_be_bytes())?;
        data.write_all(&(normal_len as u16).to_be_bytes())?;
        data.write_all(&(repeat_len as u16).to_be_bytes())?;
        data.write_all(&repeat_count.to_be_bytes())?;

        // write data
        self.impl_frame_normal_data(&mut data)?;
        for i in 0..repeat_count {
            self.impl_frame_repeat_data(i, &mut data)?
        }

        Ok(Frame {
            address,
            msg_id: self.message_id(),
            msg_type,
            kind,
            data: data.into_inner(),
        })
    }
}

pub(super) fn peek_subtype(frame: &Frame) -> Result<ControlStatusMessageSubtype, MessageError> {
    if frame.msg_type != MSG_TYPE_CONTOL_STATUS || frame.data.len() < MSG_SUBTYPE_SIZE {
        return Err(MessageError::InvalidData);
    }
    Ok(ControlStatusMessageSubtype::from_be_bytes(
        frame.data[..MSG_SUBTYPE_SIZE].try_into().unwrap(),
    ))
}