draw_bridge 0.1.1

A driver for AxiDraw Pen Plotters
Documentation
mod messages {

    use std::collections::HashMap;
    use std::convert::From;
    use std::fmt;

    use crate::commands as cmds;
    use crate::plotter::RegisteredPlotters;

    type Result = std::result::Result<Box<dyn cmds::Command>, MessageParsingError>;

    pub trait Response {
        fn respond(&self) -> String;
    }

    #[derive(Debug)]
    pub enum MessageParsingError {
        TypeKeyMissing,
        APINotImplemented(String),
        CommandNotImplemented(String),
    }

    impl fmt::Display for MessageParsingError {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            let s = match self {
                Self::APINotImplemented(api) => format!("API named '{}' is not implemented", api),
                Self::CommandNotImplemented(cmd) => format!("Command '{}' is not implemented", cmd),
                Self::TypeKeyMissing => String::from("Key 'type' was missing from the command map"),
            };
            write!(f, "Failed to parse message: {}", s)
        }
    }

    #[derive(Debug, Serialize, Deserialize, Copy, Clone)]
    pub enum API {
        #[serde(rename = "config")]
        Config,
        #[serde(rename = "core")]
        Core,
        #[serde(alias = "plot")]
        Plot,
    }

    /// A message sent from the user to the API.
    #[derive(Serialize, Deserialize, Debug, Clone)]
    pub struct ApiMessage {
        pub api: API,
        pub plotter: Option<RegisteredPlotters>,
        pub command: HashMap<String, String>,
    }

    impl ApiMessage {
        pub fn render_commands(&self) -> Result {
            match self.api {
                API::Config => Err(MessageParsingError::APINotImplemented(String::from(
                    "config",
                ))),
                API::Core => core::parse_command(&self.command),
                API::Plot => Err(MessageParsingError::APINotImplemented(String::from("plot"))),
            }
        }
    }

    /// A response sent back *to* the user *from* the API.
    #[derive(Debug, Serialize)]
    pub struct ApiResponse {
        pub api: API,
        pub plotter: Option<RegisteredPlotters>,
        pub command: HashMap<String, String>,
        pub result: cmds::Result,
    }

    pub mod config {

        use std::convert::TryFrom;

        use super::MessageParsingError;

        #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
        pub enum Action {
            #[serde(rename = "get_config")]
            GetConfig,
            #[serde(rename = "set_config")]
            SetConfig,
        }

        impl TryFrom<&str> for Action {
            type Error = MessageParsingError;

            fn try_from(s: &str) -> std::result::Result<Action, Self::Error> {
                match s {
                    "get_config" => Ok(Action::GetConfig),
                    "set_conig" => Ok(Action::SetConfig),
                    _ => Err(MessageParsingError::CommandNotImplemented(s.to_string())),
                }
            }
        }
    }

    pub mod core {

        use std::collections::HashMap;
        use std::convert::{From, TryFrom};

        use crate::commands as cmds;
        use crate::commands::Command;

        use super::{MessageParsingError, Result};

        #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
        pub enum Action {
            #[serde(rename = "raise_pen")]
            PlotterRaisePen,
            #[serde(rename = "lower_pen")]
            PlotterLowerPen,
            #[serde(rename = "toggle_pen")]
            PlotterTogglePen,
            #[serde(rename = "move_pen")]
            PlotterMovePen,
            #[serde(rename = "firmware_version")]
            QueryFirmwareVersion,
        }

        impl TryFrom<&str> for Action {
            type Error = MessageParsingError;

            fn try_from(s: &str) -> std::result::Result<Action, Self::Error> {
                match s {
                    "raise_pen" => Ok(Action::PlotterRaisePen),
                    "lower_pen" => Ok(Action::PlotterLowerPen),
                    "toggle_pen" => Ok(Action::PlotterTogglePen),
                    "move_pen" => Ok(Action::PlotterMovePen),
                    "firmware_version" => Ok(Action::QueryFirmwareVersion),
                    _ => Err(MessageParsingError::CommandNotImplemented(s.to_string())),
                }
            }
        }

        pub fn parse_command(input: &HashMap<String, String>) -> Result {
            input
                .get("type")
                .map_or(Err(MessageParsingError::TypeKeyMissing), |t| -> Result {
                    Action::try_from(t.as_ref()).map(|action| -> Box<dyn Command> {
                        match action {
                            Action::PlotterLowerPen => Box::new(cmds::TogglePen {}),
                            Action::PlotterRaisePen => Box::new(cmds::TogglePen {}),
                            Action::PlotterTogglePen => Box::new(cmds::TogglePen {}),
                            Action::PlotterMovePen => Box::new(cmds::GoTo::from(input)),
                            Action::QueryFirmwareVersion => Box::new(cmds::Version {}),
                        }
                    })
                })
        }
    }
}

use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixStream;

use serial_core::SerialDevice;
use serial_unix::TTYPort;

use log::{debug, info};

use crate::plotter::{Device, Plotter, ReferenceDevice};
use crate::runtime::Context;

// The Box needs to own the device reference to ensure it lives the correct span.
fn load_device(ctx: Context, reference: bool) -> Option<Box<dyn Device>> {
    match (ctx.socket, reference) {
        (_, true) => Some(Box::new(ReferenceDevice::default())), // reference plotter,
        (Some(socket), false) => {
            let mut tty =
                TTYPort::open(&socket.as_path()).expect("Failed to open reader on TTY to plotter");

            let timeout = std::time::Duration::from_millis(50);
            tty.set_timeout(timeout)
                .expect("Failed to set timeout on TTY");
            let p = Plotter::new(tty);
            Some(Box::new(p))
        }
        (None, false) => None,
    }
}

/// Runs the API as a Unix Socket handler, reading messages, generating
/// responses, and writing them back out.
pub fn make_api_handler(mut stream: UnixStream, reference: bool) -> impl FnMut() -> () + Sync {
    move || {
        let mut buf = Vec::new();
        let mut reader = BufReader::new(&stream);

        let ctx = Context::new();

        let bytes_read = reader.read_until(b'\r', &mut buf);
        debug!("Read {} bytes", bytes_read.expect("Read no bytes"));

        // TODO: this is fine for dev, but we seriously need better error handling.
        let msg: messages::ApiMessage =
            serde_json::from_slice(&buf).expect("Failed to parse message");

        info!("Received message:\n{:#?}", msg);
        let cmd = msg.render_commands().expect("Failed to parse commands");

        let device = load_device(ctx, reference);

        match msg.api {
            messages::API::Config => {}
            messages::API::Core => {
                // TODO: Around here, we need to square the configs of the
                // plotter we're trying to run against. A specified Plotter
                // comes from the *user*; all we know is that we've got an EBB
                // device on a socket.
                match device {
                    Some(dev) => {
                        info!("Running command...");
                        let res = cmd.run(dev);
                        info!("Got result: {:#?}", res);

                        let response = messages::ApiResponse {
                            api: msg.api,
                            command: msg.command,
                            plotter: msg.plotter,
                            result: res,
                        };

                        serde_json::to_writer(&stream, &response)
                            .expect("Failed to write response to stream");
                        stream.flush().expect("Failed to flush intermediate buffer");
                    }
                    _ => debug!("No plotter found!"),
                }
            }
            messages::API::Plot => (),
        }
    }
}

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

    fn build_test_json(cmd: &str, body: &str) -> String {
        format!(
            r#"
{{
    "api_version": 1,
    "action": "{}",
    "params": [{}]
}}
"#,
            cmd, body
        )
    }
}