g_code/emit/compact/
meatpack.rs

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