g_code/parse/compact/
meatpack.rs

1//! Parsing for meatpacked g-code
2//!
3//! <https://github.com/scottmudge/OctoPrint-MeatPack/>
4
5use std::{cell::RefCell, ops::RangeFrom, rc::Rc};
6
7use nom::{
8    bytes::complete::tag,
9    combinator::{cond, flat_map, iterator},
10    number::complete::le_u8,
11    Compare, IResult, InputIter, InputLength, InputTake, Parser, Slice,
12};
13
14/// Present when two characters will not be found by [unpack_character]
15pub(crate) const MP_BOTH_UNPACKABLE_HEADER: [u8; 1] = [0xFF];
16
17/// Bitmask that indicates that a particular character in a pair cannot be unpacked
18pub(crate) const MP_SINGLE_UNPACKABLE_MASK: u8 = 0xF;
19
20/// Byte sequence preceding a command
21pub(crate) const MP_COMMAND_HEADER: [u8; 2] = [0xFF, 0xFF];
22
23/// Enables packing for size reduction
24pub(crate) const MP_COMMAND_ENABLE_PACKING: u8 = 251;
25
26/// Disables packing
27pub(crate) const MP_COMMAND_DISABLE_PACKING: u8 = 250;
28
29/// Reset configuration to defaults
30///
31/// Usually sent at the end of packed g-code
32pub(crate) const MP_COMMAND_RESET_ALL: u8 = 249;
33
34/// Ignored
35pub(crate) const MP_COMMAND_QUERY_CONFIG: u8 = 248;
36
37/// Special mode where packed g-code contains no spaces
38///
39/// `E` replaces ` ` in the lookup table
40pub(crate) const MP_COMMAND_ENABLE_NO_SPACES: u8 = 247;
41pub(crate) const MP_COMMAND_DISABLE_NO_SPACES: u8 = 246;
42
43/// Unpack meatpacked g-code to a string using [nom]
44///
45/// Once unpacked, call [crate::parse::file_parser] or [crate::parse::snippet_parser] as appropriate
46pub fn meatpacked_to_string<I>(input: I) -> IResult<I, String, nom::error::Error<I>>
47where
48    I: Clone
49        + Slice<RangeFrom<usize>>
50        + InputIter<Item = u8>
51        + InputTake
52        + InputLength
53        + Compare<&'static [u8]>,
54{
55    let state = Rc::new(RefCell::new(MeatpackState::default()));
56    let mut parser = iterator(input, |input| decode_next(state.clone()).parse(input));
57    let it = &mut parser;
58    let acc = String::from_utf8(it.flatten().collect::<Vec<u8>>()).unwrap();
59    parser.finish().map(|(input, ())| (input, acc))
60}
61
62/// Used to make the [nom] parser stateful
63#[derive(Debug, Default)]
64struct MeatpackState {
65    packing: bool,
66    no_spaces: bool,
67}
68
69/// Decode the next command or character pair
70fn decode_next<I>(
71    state: Rc<RefCell<MeatpackState>>,
72) -> impl Parser<I, Vec<u8>, nom::error::Error<I>>
73where
74    I: Clone
75        + Slice<RangeFrom<usize>>
76        + InputIter<Item = u8>
77        + InputTake
78        + InputLength
79        + Compare<&'static [u8]>,
80{
81    let state_clone = state.clone();
82    flat_map(tag(MP_COMMAND_HEADER.as_slice()), |_tag| le_u8)
83        .map(move |command| {
84            let mut state = state.borrow_mut();
85            match command {
86                MP_COMMAND_ENABLE_PACKING => state.packing = true,
87                MP_COMMAND_DISABLE_PACKING => state.packing = false,
88                MP_COMMAND_ENABLE_NO_SPACES => state.no_spaces = true,
89                MP_COMMAND_DISABLE_NO_SPACES => state.no_spaces = false,
90                MP_COMMAND_RESET_ALL => state.packing = false,
91                MP_COMMAND_QUERY_CONFIG => {}
92                // Not a known command? Swallow bytes and do nothing
93                _other => {}
94            }
95            vec![]
96        })
97        .or(decode_character_pair(state_clone).map(|pair| pair.to_vec()))
98}
99
100/// Decode the next pair of characters
101fn decode_character_pair<I>(
102    state: Rc<RefCell<MeatpackState>>,
103) -> impl Parser<I, Vec<u8>, nom::error::Error<I>>
104where
105    I: Clone
106        + Slice<RangeFrom<usize>>
107        + InputIter<Item = u8>
108        + InputTake
109        + InputLength
110        + Compare<&'static [u8]>,
111{
112    let both_unpacked_parser = tag(MP_BOTH_UNPACKABLE_HEADER.as_slice())
113        .and(le_u8)
114        .and(le_u8)
115        .map(|((_tag, first), second)| [first, second].to_vec());
116
117    let packed_parser = flat_map(le_u8, move |byte: u8| {
118        let state = state.borrow();
119        let first_unpacked = if state.packing {
120            unpack_character(byte & MP_SINGLE_UNPACKABLE_MASK, state.no_spaces)
121        } else {
122            None
123        };
124        let second_unpacked = if state.packing {
125            unpack_character((byte >> 4) & MP_SINGLE_UNPACKABLE_MASK, state.no_spaces)
126        } else {
127            None
128        };
129        cond(
130            state.packing && (first_unpacked.is_none() || second_unpacked.is_none()),
131            le_u8,
132        )
133        .map(move |next_byte| {
134            let next_char = next_byte.map(|b| b);
135            match (first_unpacked, second_unpacked) {
136                (None, None) => [byte].to_vec(),
137                (None, Some(second)) => [next_char.unwrap(), second].to_vec(),
138                (Some(first), None) => [first, next_char.unwrap()].to_vec(),
139                (Some(first), Some(second)) => [first, second].to_vec(),
140            }
141        })
142    });
143
144    both_unpacked_parser.or(packed_parser)
145}
146
147/// Lookup table for a 4-bit packed character
148const fn unpack_character(x: u8, no_spaces: bool) -> Option<u8> {
149    Some(match x {
150        0 => b'0',
151        1 => b'1',
152        2 => b'2',
153        3 => b'3',
154        4 => b'4',
155        5 => b'5',
156        6 => b'6',
157        7 => b'7',
158        8 => b'8',
159        9 => b'9',
160        10 => b'.',
161        11 if !no_spaces => b' ',
162        11 if no_spaces => b'E',
163        12 => b'\n',
164        13 => b'G',
165        14 => b'X',
166        _other => return None,
167    })
168}