mks979b 0.1.0

no_std driver for the MKS 979B Atmosphere to Vacuum Transducer. Based on the embedded-hal traits.
Documentation
use embedded_hal::serial;

use crate::cmd::{Message, Response, Value};
use crate::errors::{
    try_code_from_bytes, DriverError, Mks979bError, ResponseError,
};
use crate::scinumber::SciNumber979b;
use crate::*;
use core::fmt::Write;
use core::str::from_utf8;
use heapless::{String, Vec};
use numtoa::NumToA;

/// Used to represent the status of the I/O operations in the device
#[derive(PartialEq, Debug)]
pub enum State {
    Idle,
    SendingMessage(Vec<u8, MAX_STR_LEN>),
    Flush,
    ReadFirst,
    ReadID(usize),
    ReadResult(Vec<u8, MAX_STR_LEN>),
    ReadTermination(usize, Vec<u8, MAX_STR_LEN>),
    Failure,
}

/**
Representation of the device.

- `port`: Serial port used for communication. Must implement the traits from embedded_hal::serial

- `addr`: Configured RS-485 address in the device. By default, this should be 253.
    Even in RS-232 operation mode it must be configured. At driver startup, a message
    is automatically sent to assure the configured address in the device is the specified one.

- `addrnum`: same as addr but as int to avoid formating-realted complexity

- `response_length`: Indicates the length of the message in the response_buffer

- `last_message`: Keeps metadata information of the last-sent message for troubleshooting and other tasks

- Timeout logic implementation is left for the user to implement.
*/
#[derive(Debug)]
pub struct Mks979b<S>
where
    S: Serial,
{
    port: S,
    addr: [u8; ADDR_LEN],
    addrnum: usize,
    state: State,
    last_message: Message,
}
pub type NBResult<T, S> = nb::Result<
    T,
    DriverError<
        <S as serial::Read<u8>>::Error,
        <S as serial::Write<u8>>::Error,
    >,
>;

/// Utility struct for storing query response data
pub struct ResponseData {
    query: Message,
    response: Option<Vec<u8, MAX_STR_LEN>>,
}
/// Wrapper function for embedded-hal serial read
fn read<S: Serial>(serial: &mut S) -> NBResult<u8, S> {
    serial
        .read()
        .map_err(|error| error.map(DriverError::SerialRead))
}

/// Wrapper function for embedded-hal serial write
fn write<S: Serial>(serial: &mut S, byte: u8) -> NBResult<(), S> {
    serial
        .write(byte)
        .map_err(|error| error.map(DriverError::SerialWrite))
}

/// Wrapper function for embedded-hal serial flush
fn flush<S: Serial>(serial: &mut S) -> NBResult<(), S> {
    serial
        .flush()
        .map_err(|error| error.map(DriverError::SerialWrite))
}

impl<S> Mks979b<S>
where
    S: Serial,
{
    // Makes sure the ID of the device is in a valid range. If that is the case,
    // creates a new instance of the driver.
    pub fn new(port: S, address: u8) -> Mks979b<S> {
        let mut addr = [0u8; ADDR_LEN];
        address.numtoa(10, &mut addr);
        Mks979b {
            port,
            addr,
            addrnum: address as usize,
            last_message: Message::None,
            state: State::Idle,
        }
    }

    /// Returns the underlying serial port.
    pub fn take_port(self) -> S {
        self.port
    }

    /// Provide a configuration function to be applied to the underlying serial port.
    pub fn reconfig_port(&mut self, config: fn(p: &mut S) -> ()) {
        config(&mut self.port);
    }

    /// If a response is being processed, this will try to read as much info as possible from
    /// the serial bus in a non-blocking way. If the current state is not a response-reading one,
    /// it will return a wrapped `None` result.  The received response is not parsed inmediatly in
    /// case the information about the sent message is relevant for the user. If you just want the
    /// parsed data, the `parse_response` utility function is provided.
    pub fn poll(&mut self) -> NBResult<Option<ResponseData>, S> {
        let response: Option<ResponseData> = loop {
            match &mut self.state {
                State::ReadFirst => {
                    if read(&mut self.port)? == b'@' {
                        self.state = State::ReadID(0);
                    } else {
                        self.state = State::Failure;
                    }
                }
                //Read first 2 digits of ID
                State::ReadID(i) => {
                    if read(&mut self.port)? == self.addr[*i] {
                        self.state = if *i < ADDR_LEN - 1 {
                            State::ReadID(*i + 1)
                        } else {
                            State::ReadResult(Vec::new())
                        };
                    } else {
                        self.state = State::Failure;
                    }
                }
                //Read the message content
                State::ReadResult(buffer) => {
                    let byte = read(&mut self.port)?;
                    match byte {
                        //assumes we never see ; unless for termination of the mesage
                        b';' => {
                            self.state = State::ReadTermination(
                                0,
                                core::mem::take(buffer),
                            );
                        }
                        ch => {
                            if buffer.push(ch).is_err() {
                                self.state = State::Failure
                            };
                        }
                    }
                }
                //Make sure the termination is correct
                State::ReadTermination(i, buffer) => {
                    match read(&mut self.port)? {
                        b'F' => {
                            if *i < 1 {
                                *i += 1;
                            } else {
                                break Some(ResponseData {
                                    query: self.last_message.clone(),
                                    response: Some(core::mem::take(buffer)),
                                });
                            }
                        }
                        _ => self.state = State::Failure,
                    }
                }
                State::Failure => {
                    while read(&mut self.port).is_ok() {}
                    self.state = State::Idle;
                    break Some(ResponseData {
                        query: self.last_message.clone(),
                        response: None,
                    });
                }
                State::Idle | State::SendingMessage(_) | State::Flush => {
                    let _ = read(&mut self.port)?;
                    break None;
                }
            }
        };
        self.state = State::Idle;
        Ok(response)
    }

    /// Produces and schedules the query text to be sent to the instrument.
    pub fn schedule_message(&mut self, msg: Message) -> NBResult<(), S> {
        match self.state {
            State::Idle => {
                let mut buffer = Vec::<u8, MAX_STR_LEN>::new();
                self.last_message = msg;
                write!(
                    &mut buffer,
                    "@{}{};FF",
                    self.addrnum, self.last_message
                )
                .map_err(|_| DriverError::BadMessage)?;
                buffer.reverse();
                self.state = State::SendingMessage(buffer);
                Ok(())
            }
            _ => Err(nb::Error::WouldBlock),
        }
    }

    /// Calls `schedule_message` with the last message sent as the argument.
    /// Intended for error correction or repeated queries.
    pub fn reschedule_last_message(&mut self) -> NBResult<(), S> {
        self.schedule_message(self.last_message.clone())
    }

    /// Sends as much of the scheduled message in a non-blocking way.
    pub fn send_message(&mut self) -> NBResult<(), S> {
        loop {
            match &mut self.state {
                State::SendingMessage(buffer) => {
                    if let Some(byte) = buffer.last() {
                        write(&mut self.port, *byte)?;
                        buffer.pop();
                    } else {
                        self.state = State::Flush;
                    }
                }
                State::Flush => {
                    flush(&mut self.port)?;
                    self.state = State::ReadFirst
                }
                _ => break,
            }
        }
        Ok(())
    }
}

/// Parser for strings following the MKS979B response message format.
/// It uses `data.query` to infer the datatype to be used in the parsing of the
/// contents.
pub fn parse_response(data: &ResponseData) -> Result<Response, ResponseError> {
    if let Some(response) = &data.response {
        match &response[0..ACKNAK_LEN] {
            b"ACK" => match data.query.clone() {
                Message::Value(Value::RSDelayON)
                | Message::Value(Value::TestLedON)
                | Message::Value(Value::ActiveFilamentPowerON)
                | Message::Value(Value::SetPointEnabled(_))
                | Message::Value(Value::ActiveFilamentON)
                | Message::Value(Value::DegasON)
                | Message::Setting(cmd::Setting::RSDelayON(_))
                | Message::Setting(cmd::Setting::TestLedON(_))
                | Message::Setting(cmd::Setting::ActiveFilamentPowerON(_))
                | Message::Setting(cmd::Setting::DegasON(_))
                | Message::Setting(cmd::Setting::EnableSetPoint(_, _))=> Ok(match &response[ACKNAK_LEN..response.len()] {
                    b"ON" => Response::Boolean(true),
                    _ => Response::Boolean(false),
                }),
                Message::Value(Value::DeviceStatus) => Ok(match &response[ACKNAK_LEN..response.len()] {
                    b"F" => {
                        Response::String(String::from("Filament fault, filament cannot turn on"))
                    }
                    b"G" => Response::String(String::from("Hot cathode on ")),
                    b"P" => Response::String(String::from(
                        "Pressure fault, system pressure above protect pressure",
                    )),
                    b"W" => Response::String(String::from(
                        "Hot Cathode is turning on; pressure reading not valid",
                    )),
                    b"D" => Response::String(String::from("Degas ON")),
                    _ => Response::String(String::from("OK, no errors to report")),
                }),
                Message::Value(Value::OnChipSensorTemp)
                | Message::Value(Value::HotCathodeTemp)
                | Message::Value(Value::PressureReadingMicroPirani)
                | Message::Value(Value::PressureReadingHotCathode)
                | Message::Value(Value::PressureReadingCombined)
                | Message::Value(Value::SetPointValue(_))
                | Message::Value(Value::SetPointHysteresis(_))
                | Message::Setting(cmd::Setting::SetPointValue(_, _))
                | Message::Setting(cmd::Setting::SetPointHysteresis(_, _))
                | Message::Setting(cmd::Setting::AtmosphericPressure(_))
                | Message::Setting(cmd::Setting::VacuumReadoutZero) => {
                    if let Ok(res) = SciNumber979b::from_ascii(&response[ACKNAK_LEN..response.len()]){
                        Ok(Response::SciNumber(res))
                    }else{
                        Err(ResponseError::FailedToParseResponse)
                    }

                },
                _ => {
                    if let Ok(string) = from_utf8(&response[ACKNAK_LEN..response.len()]) {
                        Ok(Response::String(String::from(string)))
                    } else {
                        Err(ResponseError::FailedToParseResponse)
                    }
                }
            },
            b"NAK" => {
                if let Ok(code) = try_code_from_bytes(&response[ACKNAK_LEN..response.len()]) {
                    Err(ResponseError::Mks979bError(Mks979bError::get_error(code)))
                } else {
                    Err(ResponseError::FailedToParseResponse)
                }
            }
            _ =>Err(ResponseError::FailedToParseResponse) }
    } else {
        Err(ResponseError::FailedToParseResponse)
    }
}