Skip to main content

g_code/emit/compact/
meatpack.rs

1//! Compact g-code representation using meatpack
2//!
3//! <https://github.com/scottmudge/OctoPrint-MeatPack/>
4
5use std::{borrow::Borrow, fmt::Arguments, io::Write as IoWrite};
6
7use crate::{
8    emit::{Field, FormatOptions, Token, Value, token::Flag},
9    parse::compact::meatpack::*,
10};
11
12#[derive(Clone)]
13pub struct MeatpackOptions {
14    pub no_spaces: bool,
15}
16
17struct MeatpackEncodingWriter<W> {
18    downstream: W,
19    meatpack_opts: MeatpackOptions,
20    checksum_acc: u8,
21    enabled: bool,
22    pending: Option<(u8, bool)>,
23}
24
25impl<W> MeatpackEncodingWriter<W>
26where
27    W: IoWrite,
28{
29    fn new(downstream: W, meatpack_opts: MeatpackOptions) -> Self {
30        Self {
31            downstream,
32            meatpack_opts,
33            checksum_acc: 0,
34            enabled: false,
35            pending: None,
36        }
37    }
38
39    fn start_meatpack(&mut self) -> std::io::Result<()> {
40        if self.meatpack_opts.no_spaces {
41            self.downstream.write_all(&MP_COMMAND_HEADER)?;
42            self.downstream.write_all(&[MP_COMMAND_ENABLE_NO_SPACES])?;
43        }
44        Ok(())
45    }
46
47    fn stop_meatpack(&mut self) -> std::io::Result<()> {
48        if self.pending.is_some() {
49            self.enable_packing()?;
50            self.write_pending()?;
51        }
52        self.downstream.write_all(&MP_COMMAND_HEADER)?;
53        self.downstream.write_all(&[MP_COMMAND_RESET_ALL])?;
54        self.enabled = false;
55        Ok(())
56    }
57
58    fn enable_packing(&mut self) -> std::io::Result<()> {
59        if !self.enabled {
60            self.downstream.write_all(&MP_COMMAND_HEADER)?;
61            self.downstream.write_all(&[MP_COMMAND_ENABLE_PACKING])?;
62        }
63        self.enabled = true;
64        Ok(())
65    }
66
67    fn disable_packing(&mut self) -> std::io::Result<()> {
68        if self.enabled {
69            self.write_pending()?;
70            self.downstream.write_all(&MP_COMMAND_HEADER)?;
71            self.downstream.write_all(&[MP_COMMAND_DISABLE_PACKING])?;
72        }
73        self.enabled = false;
74        Ok(())
75    }
76
77    fn will_write_newline(&mut self) -> bool {
78        self.enabled && self.meatpack_opts.no_spaces && self.pending.is_some()
79    }
80
81    fn write_pending(&mut self) -> std::io::Result<()> {
82        if let Some((pending_byte, _)) = self.pending.take() {
83            self.write_slice(
84                [
85                    pending_byte,
86                    if self.meatpack_opts.no_spaces {
87                        b'\n'
88                    } else {
89                        b' '
90                    },
91                ]
92                .as_slice(),
93            )?;
94        }
95        Ok(())
96    }
97
98    fn checksum(&self) -> u8 {
99        let mut checksum = self.checksum_acc;
100        if let Some((pending_byte, true)) = self.pending {
101            if self.enabled {
102                checksum ^= packable_to_uppercase(pending_byte, self.meatpack_opts.no_spaces);
103            } else {
104                checksum ^= pending_byte;
105            }
106        }
107        checksum
108    }
109
110    fn reset_checksum(&mut self) {
111        self.checksum_acc = 0;
112        if let Some((_, ref mut include_pending_in_checksum)) = self.pending {
113            *include_pending_in_checksum = false;
114        }
115    }
116
117    fn write_fmt(&mut self, arguments: Arguments<'_>) -> std::io::Result<()> {
118        let input = arguments.to_string();
119        if !self.enabled {
120            self.checksum_acc = input.bytes().fold(self.checksum_acc, |acc, b| acc ^ b);
121            self.downstream.write_all(input.as_bytes())?;
122        } else if !input.is_empty() {
123            assert!(input.is_ascii(), "Meatpack can only encode ASCII");
124            if self.meatpack_opts.no_spaces {
125                for substr in input.split(' ') {
126                    self.write_slice(substr.as_bytes())?;
127                }
128            } else {
129                self.write_slice(input.as_bytes())?;
130            }
131        }
132
133        Ok(())
134    }
135
136    fn write_slice(&mut self, mut slice: &[u8]) -> std::io::Result<()> {
137        if slice.is_empty() {
138            return Ok(());
139        }
140        let mut pending_slice_opt = None;
141        let mut _pending_array_opt = None;
142        if let Some((pending, include_pending_in_checksum)) = self.pending.take() {
143            _pending_array_opt = Some([pending, slice[0]]);
144            pending_slice_opt = _pending_array_opt
145                .as_ref()
146                .map(|array| (array.as_slice(), include_pending_in_checksum));
147            slice = &slice[1..];
148        }
149
150        for (chunk, include_first_in_checksum) in pending_slice_opt
151            .into_iter()
152            .chain(slice.chunks(2).map(|c| (c, true)))
153        {
154            match chunk {
155                [first, second] => {
156                    let first = packable_to_uppercase(*first, self.meatpack_opts.no_spaces);
157                    let second = packable_to_uppercase(*second, self.meatpack_opts.no_spaces);
158                    if include_first_in_checksum {
159                        self.checksum_acc ^= first;
160                    }
161                    self.checksum_acc ^= second;
162                    write_packed_characters(
163                        [first, second],
164                        self.meatpack_opts.no_spaces,
165                        &mut self.downstream,
166                    )?;
167                }
168                [odd] => self.pending = Some((*odd, true)),
169                _ => unreachable!(),
170            }
171        }
172        Ok(())
173    }
174}
175
176/// Write g-code to a [std::io::Write] in a meatpacked representation
177pub fn format_gcode_meatpack<'a: 'b, 'b, W, I, T>(
178    program: I,
179    opts: FormatOptions,
180    meatpack_opts: MeatpackOptions,
181    w: W,
182) -> std::io::Result<()>
183where
184    W: IoWrite,
185    I: IntoIterator<Item = T>,
186    T: Borrow<Token<'a>> + 'b,
187{
188    let mut preceded_by_newline = true;
189    let mut line_number = 0usize;
190
191    let mut w = MeatpackEncodingWriter::new(w, meatpack_opts);
192    w.start_meatpack()?;
193
194    if opts.delimit_with_percent {
195        writeln!(w, "%")?;
196        w.reset_checksum();
197    }
198
199    for token in program {
200        let token = token.borrow();
201        if let Token::Field(f) = token {
202            // Can't handle user-provided line numbers
203            if preceded_by_newline && f.letters == "N" {
204                continue;
205            }
206        }
207
208        // Disable meatpack if there are non-ASCII characters,
209        // it doesn't handle multi-byte chars.
210        let disable_meatpack = match token {
211            Token::Field(Field { letters, value }) => {
212                !letters.is_ascii()
213                    || match value {
214                        Value::String(_) => true,
215                        Value::Rational(_) | Value::Float(_) | Value::Integer(_) => false,
216                    }
217            }
218            Token::Flag(Flag { letter }) => !letter.is_ascii(),
219            Token::Comment { .. } => true,
220        };
221
222        if disable_meatpack {
223            let will_write_newline = w.will_write_newline();
224            if will_write_newline {
225                if opts.line_numbers && preceded_by_newline {
226                    write!(w, "N{line_number} ")?;
227                }
228
229                if opts.checksums {
230                    write!(w, "*{}", w.checksum())?;
231                }
232                line_number += 1;
233            }
234            let still_going_to_write_newline = w.will_write_newline();
235            w.disable_packing()?;
236            if will_write_newline {
237                // Degenerate case: odd number of chars became even, oops
238                if !still_going_to_write_newline {
239                    writeln!(w)?;
240                }
241                w.reset_checksum();
242            }
243            preceded_by_newline = will_write_newline;
244        } else {
245            w.enable_packing()?;
246        }
247
248        if opts.line_numbers && preceded_by_newline {
249            write!(w, "N{line_number} ")?;
250        }
251
252        match token {
253            Token::Field(f) => {
254                if !preceded_by_newline {
255                    if matches!(f.letters.as_ref(), "G" | "g" | "M" | "m" | "D" | "d") {
256                        if opts.checksums {
257                            write!(w, "*{}", w.checksum())?;
258                        }
259                        line_number += 1;
260                        writeln!(w)?;
261                        w.reset_checksum();
262                        if opts.line_numbers {
263                            write!(w, "N{line_number} ")?;
264                        }
265                    } else {
266                        write!(w, " ")?;
267                    }
268                }
269
270                write!(w, "{f}")?;
271                preceded_by_newline = false;
272            }
273            Token::Flag(f) => {
274                if !preceded_by_newline {
275                    write!(w, " ")?;
276                }
277                write!(w, "{f}")?;
278            }
279            Token::Comment {
280                is_inline: true,
281                inner,
282            } => {
283                write!(w, "({inner})")?;
284                preceded_by_newline = false;
285            }
286            Token::Comment {
287                is_inline: false,
288                inner,
289            } => {
290                if opts.checksums {
291                    write!(w, "*{}", w.checksum())?;
292                }
293                if !preceded_by_newline && opts.newline_before_comment {
294                    line_number += 1;
295                    writeln!(w)?;
296                    w.reset_checksum();
297                    if opts.line_numbers {
298                        write!(w, "N{line_number} ")?;
299                    }
300                    if opts.checksums {
301                        write!(w, "*{}", w.checksum())?;
302                    }
303                }
304                line_number += 1;
305                writeln!(w, ";{inner}")?;
306                w.reset_checksum();
307                preceded_by_newline = true;
308            }
309        }
310    }
311    // Ensure presence of trailing newline
312    if !preceded_by_newline {
313        if opts.checksums {
314            write!(w, "*{}", w.checksum())?;
315            w.reset_checksum();
316        }
317        writeln!(w)?;
318    }
319
320    w.stop_meatpack()?;
321
322    if opts.delimit_with_percent {
323        write!(w, "%")?;
324    }
325    Ok(())
326}
327
328/// Pack a pair of characters into an [IoWrite]
329fn write_packed_characters<W>(
330    [first, second]: [u8; 2],
331    no_spaces: bool,
332    dest: &mut W,
333) -> std::io::Result<()>
334where
335    W: IoWrite,
336{
337    match (pack_char(first, no_spaces), pack_char(second, no_spaces)) {
338        (None, None) => {
339            dest.write_all(&MP_BOTH_UNPACKABLE_HEADER)?;
340            dest.write_all(&[first, second])
341        }
342        (None, Some(second)) => dest.write_all(&[(second << 4) | MP_SINGLE_UNPACKABLE_MASK, first]),
343        (Some(first), None) => dest.write_all(&[first | (MP_SINGLE_UNPACKABLE_MASK << 4), second]),
344        (Some(first), Some(second)) => dest.write_all(&[first | (second << 4)]),
345    }
346}
347
348const fn pack_char(c: u8, no_spaces: bool) -> Option<u8> {
349    Some(match c {
350        b'0' => 0,
351        b'1' => 1,
352        b'2' => 2,
353        b'3' => 3,
354        b'4' => 4,
355        b'5' => 5,
356        b'6' => 6,
357        b'7' => 7,
358        b'8' => 8,
359        b'9' => 9,
360        b'.' => 10,
361        b' ' if !no_spaces => 11,
362        b'E' if no_spaces => 11,
363        b'\n' => 12,
364        b'G' => 13,
365        b'X' => 14,
366        _other => return None,
367    })
368}
369
370const fn packable_to_uppercase(c: u8, no_spaces: bool) -> u8 {
371    match c {
372        b'e' if !no_spaces => b'E',
373        b'g' => b'G',
374        b'x' => b'X',
375        other => other,
376    }
377}