draw_bridge 0.1.1

A driver for AxiDraw Pen Plotters
Documentation
//! Instructions
//!
//! An instruction maps 1:1 with commands supported by the
//! [EiBotBoard](https://evil-mad.github.io/EggBot/ebb.html). Instructions are
//! modeled as an enum, `EBBInstruction`, which provides a common interface for
//! e.g. getting the bytes of an instruction. Every instruction also maps 1:1
//! with an expected type of response, and the enum knows how to parse those
//! responses as well.
use std::convert::{From, TryFrom};
use std::fmt;

use regex::Regex;

// Regexen for parsing various plotter responses.
lazy_static! {
    static ref VERSION_RE: Regex =
        Regex::new(r"^EBBv13_and_above EB Firmware Version (\d+)\.(\d+)(?:\.(\d+))?$").unwrap();
    static ref MOTOR_STATUS_RE: Regex = Regex::new(r"^QM,(\d+),(\d+),(\d+),(\d+)$").unwrap();
}

/// The plotter always responds with at least one String; frequently, it
/// responds with more than one.
type PlotterResponse = Vec<String>;

/// EBB responses become a little idiosyncratic in the context of Rust Results,
/// as the generic "all is well" return value is the string "OK". We model this
/// as a Result all the same, as it's the most idiomatic. To avoid the very
/// strange feeling pattern of `Ok(EBBResponse::Ok)`, we model the EBB "Ok"
/// response as `Unit`, on the theory that "Ok" amounts to, "it worked and I
/// have nothing to say about it".
#[derive(Debug, Eq, PartialEq, Serialize)]
pub enum EBBResponse {
    Unit,
    Version {
        major: u8,
        minor: u8,
        patch: u8,
    },
    Steps {
        x_pos: i32,
        y_pos: i32,
    },
    MotorStatus {
        exec_ing: bool,
        x_moving: bool,
        y_moving: bool,
        motion_queue_empty: bool,
    },
    PenPosition(PenPosition),
}

impl fmt::Display for EBBResponse {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            EBBResponse::MotorStatus {
                exec_ing,
                x_moving,
                y_moving,
                motion_queue_empty,
            } => write!(
                f,
                "Executing: {}; X Moving: {}; Y Moving: {}; Queue Empty: {}",
                exec_ing, x_moving, y_moving, motion_queue_empty
            ),
            EBBResponse::PenPosition(pos) => {
                write!(f, "Pen is currently {}", pos)
            }
            EBBResponse::Steps { x_pos, y_pos } => {
                write!(f, "Motor has X position {}, Y Position {}", x_pos, y_pos)
            }
            EBBResponse::Unit => write!(f, "Ok"),
            EBBResponse::Version {
                major,
                minor,
                patch,
            } => write!(f, "Firmware is at version: {}.{}.{}", major, minor, patch),



        }
    }
}

type Result = std::result::Result<EBBResponse, String>;

#[derive(Debug, Eq, PartialEq, Serialize)]
pub enum PenPosition {
    Up,
    Down,
}

impl fmt::Display for PenPosition {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            PenPosition::Up => write!(f, "{}", "Up"),
            PenPosition::Down => write!(f, "{}", "Down"),
        }
    }
}


impl TryFrom<&str> for PenPosition {
    type Error = String;

    fn try_from(s: &str) -> std::result::Result<PenPosition, String> {
        match s {
            "0" => Ok(PenPosition::Down),
            "1" => Ok(PenPosition::Up),
            _ => {
                let msg = format!("Invalid pen position: {}", s);
                Err(msg)
            }
        }
    }
}

/// EBBInstruction describes every EBB command `draw-bridge` currently knows how
/// to handle.
pub enum EBBInstruction {
    HomeMove,
    MixedGeometryMove {
        x: i32,
        y: i32,
        duration: Option<i32>,
    },
    QueryMotors,
    QueryPen,
    QuerySteps,
    SetPen(PenPosition),
    TogglePen,
    Version,
}

impl EBBInstruction {
    pub fn as_bytes(&self) -> Vec<u8> {
        let cmd_str = match self {
            EBBInstruction::HomeMove => home_move(),
            EBBInstruction::MixedGeometryMove { x, y, duration } => move_pen(*x, *y, *duration),
            EBBInstruction::QueryMotors => query_motors(),
            EBBInstruction::QueryPen => query_pen(),
            EBBInstruction::QuerySteps => query_steps(),
            EBBInstruction::SetPen(set_to) => set_pen(set_to),
            EBBInstruction::TogglePen => toggle_pen(),
            EBBInstruction::Version => version(),
        };

        cmd_str.into_bytes()
    }

    pub fn parse_response(&self, resp: PlotterResponse) -> Result {
        match self {
            EBBInstruction::HomeMove => parse_ok(resp),
            EBBInstruction::MixedGeometryMove { .. } => parse_ok(resp),
            EBBInstruction::QueryMotors => parse_query_motor_response(resp),
            EBBInstruction::QueryPen => parse_query_pen_response(resp),
            EBBInstruction::QuerySteps => parse_query_steps_response(resp),
            EBBInstruction::SetPen(_) => parse_ok(resp),
            EBBInstruction::TogglePen => parse_ok(resp),
            EBBInstruction::Version => parse_version_response(resp),
        }
    }

    /// The plotter responds to a command with one-or-more responses. This
    /// allows a command to override the default as needed.
    pub fn responses(&self) -> usize {
        match self {
            _ => 1,
        }
    }
}

fn to_err_string(not_ok: PlotterResponse) -> String {
    not_ok.join(",")
}

pub fn parse_ok(maybe_ok: PlotterResponse) -> Result {
    if maybe_ok.len() == 1 && maybe_ok[0] == "OK" {
        Ok(EBBResponse::Unit)
    } else {
        Err(to_err_string(maybe_ok))
    }
}

/// NOTE: We use `XM` (that's "mixed-axis geometries move"), *not* `SM`,
/// or "stepper move". Stepper move _moves both motors in sync_. XM does
/// the math to only move each motor the correct number of steps.
fn move_pen(x: i32, y: i32, maybe_duration: Option<i32>) -> String {
    let default_duration = 500;
    let duration = maybe_duration.unwrap_or(default_duration);
    format!("XM,{},{},{}\r", duration, x, y)
}

fn query_motors() -> String {
    String::from("QM\r")
}

pub fn version() -> String {
    String::from("V\r")
}

#[derive(Debug, Eq, PartialEq)]
pub struct Version {
    pub major: u8,
    pub minor: u8,
    pub patch: u8,
}

fn match_to_u8(m: Option<regex::Match>) -> u8 {
    match m {
        Some(res) => {
            let parsed = res.as_str().parse();
            parsed.expect("Failed to parse string")
        }
        None => 0,
    }
}

fn match_to_i32(m: Option<regex::Match>) -> i32 {
    match m {
        Some(res) => {
            let parsed = res.as_str().parse();
            parsed.expect("Failed to parse string")
        }
        None => 0,
    }
}

fn match_to_bool(m: Option<regex::Match>) -> bool {
    match m {
        Some(res) => "0" == res.as_str(),
        None => false,
    }
}

pub struct MotorStatus {
    pub command_executing: bool,
    pub x_moving: bool,
    pub y_moving: bool,
    pub motion_queue_empty: bool,
}

pub fn parse_query_motor_response(resp: PlotterResponse) -> Result {
    if resp.len() == 1 {
        MOTOR_STATUS_RE.captures(&resp[0]).map_or(
            Err(format!("Couldn't parse EBB response: {}", resp.join(","))),
            |cap| {
                let exec_ing = match_to_bool(cap.get(1));
                let x_moving = match_to_bool(cap.get(2));
                let y_moving = match_to_bool(cap.get(3));
                let motion_queue_empty = match_to_bool(cap.get(4));

                Ok(EBBResponse::MotorStatus {
                    exec_ing,
                    x_moving,
                    y_moving,
                    motion_queue_empty,
                })
            },
        )
    } else {
        Err(to_err_string(resp))
    }
}

fn parse_version_response(resp: PlotterResponse) -> Result {
    if resp.len() == 1 {
        VERSION_RE.captures(&resp[0]).map_or(
            Err(String::from("Could't parse EBB Response")),
            |cap| {
                let major = match_to_u8(cap.get(1));
                let minor = match_to_u8(cap.get(2));
                let patch = match_to_u8(cap.get(3));

                Ok(EBBResponse::Version {
                    major,
                    minor,
                    patch,
                })
            },
        )
    } else {
        Err(to_err_string(resp))
    }
}

pub fn toggle_pen() -> String {
    String::from("TP\r")
}

pub fn query_steps() -> String {
    String::from("QS\r")
}

// Command:QS<CR>
// Response: GlobalMotor1StepPosition,GlobalMotor2StepPosition<NL><CR>OK<CR><NL>
#[derive(Debug, PartialEq, Eq)]
pub struct Steps {
    pub x_pos: i32,
    pub y_pos: i32,
}

pub fn parse_query_steps_response(resp: PlotterResponse) -> Result {
    match resp.len() {
        2 => {
            let maybe_steps: Vec<&str> = resp[0].split(',').collect();
            let x_pos: i32 = maybe_steps[0].parse().expect("Failed to parse");
            let y_pos: i32 = maybe_steps[1].parse().expect("Failed to parse");
            Ok(EBBResponse::Steps { x_pos, y_pos })
        }
        _ => Err(to_err_string(resp)),
    }
}

pub fn query_pen() -> String {
    String::from("QP\r")
}

pub fn parse_query_pen_response(resp: PlotterResponse) -> Result {
    match resp.len() {
        2 => {
            let maybe_pos = &resp[0];
            PenPosition::try_from(maybe_pos.as_ref())
                .and_then(|position| Ok(EBBResponse::PenPosition(position)))
        }
        _ => Err(to_err_string(resp)),
    }
}

/// Technically, the "Home Move" instruction is parameterized by an unsigned
/// integer between 2 and 25k, representing the steps-per-second speed at which
/// home should be returned to. This seems... very silly to me, so far; will
/// consider adding a parameter in the future.
pub fn home_move() -> String {
    return String::from("HM,1000\r");
}

pub fn set_pen(set_to: &PenPosition) -> String {
    match set_to {
        PenPosition::Up => String::from("SP,1\r"),
        PenPosition::Down => String::from("SP,0\r"),
    }
}

#[cfg(test)]
mod instructions_test {

    use super::*;

    #[test]
    pub fn test_parse_query_steps_response() {
        let test_response = vec![String::from("1,2"), String::from("OK")];

        let expected = EBBResponse::Steps { x_pos: 1, y_pos: 2 };

        let got = parse_query_steps_response(test_response);

        assert_eq!(Ok(expected), got);
    }

    #[test]
    pub fn test_version_parse_result() {
        let test_str = String::from("EBBv13_and_above EB Firmware Version 2.4.2");

        let expected = EBBResponse::Version {
            major: 2,
            minor: 4,
            patch: 2,
        };

        let result = parse_version_response(vec![test_str]);

        assert_eq!(Ok(expected), result);
    }
}