g-code 0.6.0

g-code parsing and emission
Documentation
//! Parsing for meatpacked g-code
//!
//! <https://github.com/scottmudge/OctoPrint-MeatPack/>

use std::{cell::RefCell, rc::Rc};

use nom::{
    Compare, IResult, Input, Parser,
    bytes::complete::tag,
    combinator::{cond, flat_map, iterator},
    error::{ErrorKind, FromExternalError},
    number::complete::le_u8,
};

/// Present when two characters will not be found by [unpack_character]
pub(crate) const MP_BOTH_UNPACKABLE_HEADER: [u8; 1] = [0xFF];

/// Bitmask that indicates that a particular character in a pair cannot be unpacked
pub(crate) const MP_SINGLE_UNPACKABLE_MASK: u8 = 0xF;

/// Byte sequence preceding a command
pub(crate) const MP_COMMAND_HEADER: [u8; 2] = [0xFF, 0xFF];

/// Enables packing for size reduction
pub(crate) const MP_COMMAND_ENABLE_PACKING: u8 = 251;

/// Disables packing
pub(crate) const MP_COMMAND_DISABLE_PACKING: u8 = 250;

/// Reset configuration to defaults
///
/// Usually sent at the end of packed g-code
pub(crate) const MP_COMMAND_RESET_ALL: u8 = 249;

/// Ignored
pub(crate) const MP_COMMAND_QUERY_CONFIG: u8 = 248;

/// Special mode where packed g-code contains no spaces
///
/// `E` replaces ` ` in the lookup table
pub(crate) const MP_COMMAND_ENABLE_NO_SPACES: u8 = 247;
pub(crate) const MP_COMMAND_DISABLE_NO_SPACES: u8 = 246;

/// Unpack meatpacked g-code to a string using [nom]
///
/// Once unpacked, call [crate::parse::file_parser] or [crate::parse::snippet_parser] as appropriate
pub fn meatpacked_to_string<I>(input: I) -> IResult<I, String, nom::error::Error<I>>
where
    I: Input<Item = u8> + for<'a> Compare<&'a [u8]>,
{
    let state = Rc::new(RefCell::new(MeatpackState::default()));
    let mut parser = iterator(input, decode_next(state.clone()));
    let it = &mut parser;
    let bytes = it.fold(vec![], |mut acc, yielded| {
        match yielded {
            YieldedChars::None => {}
            YieldedChars::One(one) => acc.push(one),
            YieldedChars::Two(two) => acc.extend_from_slice(&two),
        };
        acc
    });
    let (remaining, ()) = parser.finish()?;

    match String::from_utf8(bytes) {
        Ok(string) => Ok((remaining, string)),
        Err(err) => Err(nom::Err::Error(nom::error::Error::from_external_error(
            remaining,
            ErrorKind::Char,
            err,
        ))),
    }
}

/// Used to make the [nom] parser stateful
#[derive(Debug, Default)]
struct MeatpackState {
    packing: bool,
    no_spaces: bool,
}

enum YieldedChars {
    None,
    One(u8),
    Two([u8; 2]),
}

/// Decode the next command or character pair
fn decode_next<I>(
    state: Rc<RefCell<MeatpackState>>,
) -> impl Parser<I, Output = YieldedChars, Error = nom::error::Error<I>>
where
    I: Input<Item = u8> + for<'a> Compare<&'a [u8]>,
{
    let state_clone = state.clone();
    flat_map(tag(MP_COMMAND_HEADER.as_slice()), |_tag| le_u8)
        .map(move |command| {
            let mut state = state.borrow_mut();
            match command {
                MP_COMMAND_ENABLE_PACKING => state.packing = true,
                MP_COMMAND_DISABLE_PACKING => state.packing = false,
                MP_COMMAND_ENABLE_NO_SPACES => state.no_spaces = true,
                MP_COMMAND_DISABLE_NO_SPACES => state.no_spaces = false,
                MP_COMMAND_RESET_ALL => state.packing = false,
                MP_COMMAND_QUERY_CONFIG => {}
                // Not a known command? Swallow bytes and do nothing
                _other => {}
            }
            YieldedChars::None
        })
        .or(decode_character_pair(state_clone))
}

/// Decode the next pair of characters
fn decode_character_pair<I>(
    state: Rc<RefCell<MeatpackState>>,
) -> impl Parser<I, Output = YieldedChars, Error = nom::error::Error<I>>
where
    I: Input<Item = u8> + for<'a> Compare<&'a [u8]>,
{
    let both_unpacked_parser = tag(MP_BOTH_UNPACKABLE_HEADER.as_slice())
        .and(le_u8)
        .and(le_u8)
        .map(|((_tag, first), second)| YieldedChars::Two([first, second]));

    let packed_parser = flat_map(le_u8, move |byte: u8| {
        let state = state.borrow();
        let first_unpacked = if state.packing {
            unpack_character(byte & MP_SINGLE_UNPACKABLE_MASK, state.no_spaces)
        } else {
            None
        };
        let second_unpacked = if state.packing {
            unpack_character((byte >> 4) & MP_SINGLE_UNPACKABLE_MASK, state.no_spaces)
        } else {
            None
        };
        cond(
            state.packing && (first_unpacked.is_none() || second_unpacked.is_none()),
            le_u8,
        )
        .map(move |next_byte| match (first_unpacked, second_unpacked) {
            (None, None) => YieldedChars::One(byte),
            (None, Some(second)) => YieldedChars::Two([next_byte.unwrap(), second]),
            (Some(first), None) => YieldedChars::Two([first, next_byte.unwrap()]),
            (Some(first), Some(second)) => YieldedChars::Two([first, second]),
        })
    });

    both_unpacked_parser.or(packed_parser)
}

/// Lookup table for a 4-bit packed character
const fn unpack_character(x: u8, no_spaces: bool) -> Option<u8> {
    Some(match x {
        0 => b'0',
        1 => b'1',
        2 => b'2',
        3 => b'3',
        4 => b'4',
        5 => b'5',
        6 => b'6',
        7 => b'7',
        8 => b'8',
        9 => b'9',
        10 => b'.',
        11 if !no_spaces => b' ',
        11 if no_spaces => b'E',
        12 => b'\n',
        13 => b'G',
        14 => b'X',
        _other => return None,
    })
}