gcode_nom/
command.rs

1use std::collections::HashSet;
2
3use nom::branch::alt;
4use nom::bytes::complete::tag;
5use nom::character::complete::digit1;
6use nom::character::complete::line_ending;
7use nom::character::complete::not_line_ending;
8use nom::character::complete::space0;
9use nom::combinator::map;
10use nom::combinator::map_res;
11use nom::multi::many;
12use nom::sequence::preceded;
13use nom::sequence::terminated;
14use nom::IResult;
15use nom::Parser;
16
17use crate::arc::parse_arc_a;
18use crate::arc::parse_arc_b;
19use crate::arc::parse_arc_c;
20use crate::arc::parse_arc_e;
21use crate::arc::parse_arc_f;
22use crate::arc::parse_arc_i;
23use crate::arc::parse_arc_j;
24use crate::arc::parse_arc_s;
25use crate::arc::parse_arc_u;
26use crate::arc::parse_arc_v;
27use crate::arc::parse_arc_w;
28use crate::arc::parse_arc_x;
29use crate::arc::parse_arc_y;
30use crate::arc::parse_arc_z;
31use crate::arc::ArcVal;
32use crate::arc::Form as ArcForm;
33
34use crate::params::head::parse_a;
35use crate::params::head::parse_b;
36use crate::params::head::parse_c;
37use crate::params::head::parse_e;
38use crate::params::head::parse_f;
39use crate::params::head::parse_s;
40use crate::params::head::parse_u;
41use crate::params::head::parse_v;
42use crate::params::head::parse_w;
43use crate::params::head::parse_x;
44use crate::params::head::parse_y;
45use crate::params::head::parse_z;
46use crate::params::head::PosVal;
47
48use crate::params::mp::parse_mp_c;
49use crate::params::mp::parse_mp_p;
50use crate::params::mp::parse_mp_s;
51use crate::params::mp::parse_mp_t;
52use crate::params::mp::parse_mp_u;
53use crate::params::mp::MultiPartVal;
54
55/// Commands: -
56///
57/// "The G0 and G1 commands add a linear move to the queue to be performed after all previous moves are completed."
58/// [GCODE doc](<https://marlinfw.org/docs/gcode/G000-G001.html>)
59///
60/// Missing Commands :-
61///  "bezier"
62///  ... TODO maybe more.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum Command {
65    ///  "G0 for non-print moves. It makes G-code more adaptable to lasers, engravers, etc."
66    G0(HashSet<PosVal>),
67    /// Printable move
68    G1(HashSet<PosVal>),
69
70    /// G2 – Clockwise Arc
71    G2(ArcForm),
72    /// G3 – Counter-clockwise Arc
73    G3(ArcForm),
74
75    /// TODO Must implement.
76    // G5 - Bézier Cubic Spline
77
78    /// Change unit to imperial
79    G20,
80    /// Change units to metric
81    G21,
82
83    ///G90 – Set Positioning Mode Absolute
84    ///
85    /// "G90 ; Set all axes to absolute"
86    G90,
87    /// G91 – Set Positioning Mode Relative
88    ///
89    /// "G91 ; Set all axes to relative"
90    G91,
91    /// Set the current position
92    ///
93    /// eg. "G92 E0"
94    ///
95    /// "The G92 command is used to set the current position of the
96    /// machine to specified coordinates without any physical movement.
97    /// This command is particularly useful for adjusting offsets and
98    /// setting the origin of the coordinate system.
99    ///
100    /// For example,
101    ///
102    /// If the current position is at X=4 and G92 X7 is programmed,
103    ///  the current position is redefined as X=7, effectively
104    /// moving the origin of the coordinate system -3 units in X.""
105    ///
106    /// TODO:  F and S are not permitted here.
107    G92(HashSet<PosVal>),
108    /// Multipart: Cancel, Un-cancel parts listed by index
109    ///
110    /// M486 T12               ; Total of 12 objects (otherwise the firmware must count)
111    /// M486 S3                ; Indicate that the 4th object is starting now
112    /// M486 S3 A"cube copy 3" ; Indicate that the 4th object is starting now and name it
113    /// M486 S-1               ; Indicate a non-object, purge tower, or other global feature
114    /// M486 P10               ; Cancel object with index 10 (the 11th object)
115    /// M486 U2                ; Un-cancel object with index 2 (the 3rd object)
116    /// M486 C                 ; Cancel the current object (use with care!)
117    /// M486                   ; List the objects on the build plate
118    ///
119    /// source <https://docs.duet3d.com/User_manual/Reference/Gcodes>
120    M486(MultiPartVal),
121    /// Drop G - no further action.
122    GDrop(u16),
123    /// Drop M - no further action.
124    MDrop(u16),
125    /// ; This is a comment
126    Comment(String),
127    /// No Operation eg a blank line "".
128    Nop,
129}
130
131impl Command {
132    /// Decodes a `GCode` command.
133    ///
134    /// # Errors
135    ///   When match fails.
136    pub fn parse_line(line: &str) -> IResult<&str, Self> {
137        // Most common first.
138        alt((
139            parse_g1,
140            parse_g0,
141            parse_g2,
142            parse_g3,
143            // TODO add G5 - Bézier Cubic Spline
144            map(tag("G20"), |_| Self::G20),
145            map(tag("G21"), |_| Self::G21),
146            map(tag("G90"), |_| Self::G90),
147            map(tag("G91"), |_| Self::G91),
148            parse_g92,
149            parse_comment,
150            parse_486,
151            // Dropping "bed leveling", "dock sled", "Retract", "Stepper motor", "Mechanical Gantry Calibration"
152            map(g_drop, Self::GDrop),
153            map(m_drop, Self::MDrop),
154            map(tag(""), |_| Self::Nop),
155        ))
156        .parse(line)
157    }
158}
159
160/// G commands that require no further action
161///
162/// # Errors
163///   When match fails.
164pub fn g_drop(i: &str) -> IResult<&str, u16> {
165    map_res(preceded(tag("G"), digit1), str::parse).parse(i)
166}
167
168// Collect everything after the semicolon until the end of the line.
169// as a comment string
170fn parse_comment(i: &str) -> IResult<&str, Command> {
171    preceded(
172        (space0, tag(";")),
173        terminated(
174            map(not_line_ending, |v: &str| Command::Comment(v.to_string())),
175            line_ending,
176        ),
177    )
178    .parse(i)
179}
180
181/// # Errors
182///   When match fails.
183fn parse_g0(i: &str) -> IResult<&str, Command> {
184    preceded(
185        (alt((tag("G00"), tag("G0"))), space0),
186        map(pos_many, |vals: Vec<PosVal>| {
187            // Paranoid: deduplication.
188            // eg. There can be only one E<f64>.
189            let hs = HashSet::from_iter(vals);
190            Command::G0(hs)
191        }),
192    )
193    .parse(i)
194}
195
196/// Linear move
197///
198/// May or may not include whitespace separators.
199///
200/// G1X94.838Y81.705F9000
201/// G1 X94.838Y81.705 F9000 ; comment text
202///
203/// NB - The command is dropped and cannot be recovered.
204///
205/// # Errors
206///   When match fails.
207fn parse_g1(i: &str) -> IResult<&str, Command> {
208    preceded(
209        (alt((tag("G01"), tag("G1"))), space0),
210        map(pos_many, |vals: Vec<PosVal>| {
211            // Paranoid: deduplication.
212            // eg. There can be only one E<f64>.
213            let hs = HashSet::from_iter(vals);
214            Command::G1(hs)
215        }),
216    )
217    .parse(i)
218}
219
220/// G2 Clockwise arc
221///
222/// May or may not include whitespace separators.
223///
224/// G2X94.838Y81.705F9000
225/// G2 X94.838Y81.705 F9000 ; comment text
226///
227/// NB - The command is dropped and cannot be recovered.
228///
229/// # Errors
230///   When match fails.
231fn parse_g2(i: &str) -> IResult<&str, Command> {
232    preceded(
233        (alt((tag("G02"), tag("G2"))), space0),
234        map_res(arc_many, |vals: Vec<ArcVal>| {
235            // Paranoid: deduplication.
236            // eg. There can be only one E<f64>.
237            let hs = HashSet::from_iter(vals);
238            let mut has_ij = false;
239            let mut has_r = false;
240            for val in &hs {
241                match val {
242                    ArcVal::I(_) | ArcVal::J(_) => {
243                        // If I or J is present, then we have a "IJ" form.
244                        has_ij = true;
245                    }
246                    ArcVal::R(_) => {
247                        // If R is present, then we have a "R" form.
248                        has_r = true;
249                    }
250                    _ => {}
251                }
252            }
253            // Checks (I,J) and R are mutually exclusive.
254            // If both are present then the command is invalid.
255            // If neither is present then the command is invalid.
256            match (has_ij, has_r) {
257                (true, false) => Ok(Command::G2(ArcForm::IJ(hs))),
258                (false, true) => Ok(Command::G2(ArcForm::R(hs))),
259                _ => {
260                    // Invalid G2 command: must have either I,J or R but not both,
261                    Err("Invalid G2 command: must have either I,J or R but not both")
262                }
263            }
264        }),
265    )
266    .parse(i)
267}
268
269/// G2 Clockwise arc
270///
271/// May or may not include whitespace separators.
272///
273/// G2X94.838Y81.705F9000
274/// G2 X94.838Y81.705 F9000 ; comment text
275///
276/// NB - The command is dropped and cannot be recovered.
277///
278/// # Errors
279///   When match fails.
280fn parse_g3(i: &str) -> IResult<&str, Command> {
281    preceded(
282        (alt((tag("G3"), tag("G03"))), space0),
283        map_res(arc_many, |vals: Vec<ArcVal>| {
284            // Paranoid: deduplication.
285            // eg. There can be only one E<f64>.
286            let hs = HashSet::from_iter(vals);
287            let mut has_ij = false;
288            let mut has_r = false;
289            for val in &hs {
290                match val {
291                    ArcVal::I(_) | ArcVal::J(_) => {
292                        // If I or J is present, then we have a "IJ" form.
293                        has_ij = true;
294                    }
295                    ArcVal::R(_) => {
296                        // If R is present, then we have a "R" form.
297                        has_r = true;
298                    }
299                    _ => {}
300                }
301            }
302            // Checks (I,J) and R are mutually exclusive.
303            // If both are present then the command is invalid.
304            // If neither is present then the command is invalid.
305            match (has_ij, has_r) {
306                (true, false) => Ok(Command::G3(ArcForm::IJ(hs))),
307                (false, true) => Ok(Command::G3(ArcForm::R(hs))),
308                _ => {
309                    // Invalid G2 command: must have either I,J or R but not both,
310                    Err("Invalid G2 command: must have either I,J or R but not both")
311                }
312            }
313        }),
314    )
315    .parse(i)
316}
317
318/// G92 Set current position.
319///
320/// # Errors
321///   When match fails.
322fn parse_g92(i: &str) -> IResult<&str, Command> {
323    preceded(
324        (tag("G92"), space0),
325        map(pos_many, |vals: Vec<PosVal>| {
326            // Paranoid: deduplication.
327            // eg. There can be only one E<f63> value.
328            let hs = HashSet::from_iter(vals);
329            Command::G92(hs)
330        }),
331    )
332    .parse(i)
333}
334
335/// M486 Start/Cancel objects
336///
337/// This command supports multipart rendering.
338///
339/// M486 T12 ; Total of 12 objects (otherwise the firmware must count)
340/// M486 U2  ; Un-cancel object with index 2 (the 3rd object)
341fn parse_486(i: &str) -> IResult<&str, Command> {
342    preceded(
343        (tag("M486"), space0),
344        map(multipart_val, |val: MultiPartVal| {
345            // Paranoid: deduplication.
346            // eg. There can be only one E<f63> value.
347
348            Command::M486(val)
349        }),
350    )
351    .parse(i)
352}
353
354/// Extracts from 1 to 12 values from the set of `PosVal`s.
355///
356/// ( A, B, C, E, F, S, U, V, W, X, Y, Z )
357///
358/// # Errors
359///   When match fails.
360fn pos_many(i: &str) -> IResult<&str, Vec<PosVal>> {
361    many(1..12, pos_val).parse(i)
362}
363
364/// Extracts from 1 to 12 values from the set of `PosVal`s.
365///
366/// ( A, B, C, E, F, S, U, V, W, X, Y, Z )
367///
368/// # Errors
369///   When match fails.
370fn arc_many(i: &str) -> IResult<&str, Vec<ArcVal>> {
371    many(1..16, arc_val).parse(i)
372}
373
374///
375/// # Errors
376///   When match fails.
377fn pos_val(i: &str) -> IResult<&str, PosVal> {
378    alt((
379        parse_a, parse_b, parse_c, parse_e, parse_f, parse_s, parse_u, parse_v, parse_w, parse_x,
380        parse_y, parse_z,
381    ))
382    .parse(i)
383}
384
385///
386/// # Errors
387///   When match fails.
388fn multipart_val(i: &str) -> IResult<&str, MultiPartVal> {
389    alt((parse_mp_c, parse_mp_p, parse_mp_s, parse_mp_t, parse_mp_u)).parse(i)
390}
391
392///
393/// # Errors
394///   When match fails.
395fn arc_val(i: &str) -> IResult<&str, ArcVal> {
396    alt((
397        parse_arc_a,
398        parse_arc_b,
399        parse_arc_c,
400        parse_arc_e,
401        parse_arc_f,
402        parse_arc_i,
403        parse_arc_j,
404        parse_arc_s,
405        parse_arc_u,
406        parse_arc_v,
407        parse_arc_w,
408        parse_arc_x,
409        parse_arc_y,
410        parse_arc_z,
411    ))
412    .parse(i)
413}
414
415/// Drop M code - no further action
416///
417/// # Errors
418///   When match fails.
419pub fn m_drop(i: &str) -> IResult<&str, u16> {
420    map_res(preceded((tag("M"), space0), digit1), str::parse).parse(i)
421}
422
423#[cfg(test)]
424mod test {
425
426    use super::*;
427
428    #[test]
429    fn comments() {
430        let text_commands = [
431            (
432                "; perimeters extrusion width = 0.67mm\n",
433                Ok((
434                    "",
435                    Command::Comment(String::from(" perimeters extrusion width = 0.67mm")),
436                )),
437            ),
438            (
439                // a sample of a comment with a base-64 encoded thumbnail.
440                "; 7K6Ho8Q5vPBT4ZkdDGAk/t/wOw4rChXwlVJwAAAABJRU5ErkJggg==\n",
441                Ok((
442                    "",
443                    Command::Comment(String::from(
444                        " 7K6Ho8Q5vPBT4ZkdDGAk/t/wOw4rChXwlVJwAAAABJRU5ErkJggg==",
445                    )),
446                )),
447            ),
448            (
449                // Header:- "assets/3dBenchy.gcode"
450                // Paranoid: This test asserts "No greedy grabbing over a blank line!"
451                // Input string should be interpreted a (Command::comment, Command::blank, CommandComment)
452                "; generated by Slic3r 1.2.9 on 2015-10-01 at 20:51:53
453
454; external perimeters extrusion width = 0.40mm",
455                Ok((
456                    "
457; external perimeters extrusion width = 0.40mm",
458                    Command::Comment(" generated by Slic3r 1.2.9 on 2015-10-01 at 20:51:53".into()),
459                )),
460            ),
461        ];
462
463        for (line, expected) in text_commands {
464            let actual = Command::parse_line(line);
465            assert_eq!(actual, expected, "line: {line}");
466        }
467    }
468
469    #[test]
470    fn g0() {
471        let text_commands = [
472            (
473                // Troublesome pattern found in "both \parts.gcode".
474                "G0E-2.7F4200",
475                Ok((
476                    "",
477                    Command::G0([PosVal::E(-2.7), PosVal::F(4200_f64)].into()),
478                )),
479            ),
480            (
481                // Leading zero check.
482                "G00 E20",
483                Ok(("", Command::G0([PosVal::E(20_f64)].into()))),
484            ),
485            (
486                // Compact form ( Missing trailing space ).
487                "G00E20",
488                Ok(("", Command::G0([PosVal::E(20_f64)].into()))),
489            ),
490        ];
491
492        for (line, expected) in text_commands {
493            let actual = Command::parse_line(line);
494            assert_eq!(actual, expected, "line: {line}");
495        }
496    }
497
498    #[test]
499    fn g1() {
500        // let default = PosPayload::<f64>::default();
501
502        let text_commands = [
503            ("G1 Z5", Ok(("", Command::G1([PosVal::Z(5_f64)].into())))),
504            (
505                "G1 Z5 F5000 ; lift nozzle",
506                Ok((
507                    " ; lift nozzle",
508                    Command::G1([PosVal::Z(5_f64), PosVal::F(5000_f64)].into()),
509                )),
510            ),
511            (
512                "G1 E1.00000 F1800.00000 ; text",
513                Ok((
514                    " ; text",
515                    Command::G1([PosVal::E(1.0_f64), PosVal::F(1800_f64)].into()),
516                )),
517            ),
518            (
519                "G1 Z0.350 F7800.000",
520                Ok((
521                    "",
522                    Command::G1([PosVal::Z(0.350_f64), PosVal::F(7800_f64)].into()),
523                )),
524            ),
525            (
526                // Must tolerate compact form without whitespace.
527                "G1Z0.350F7800.000",
528                Ok((
529                    "",
530                    Command::G1([PosVal::Z(0.350_f64), PosVal::F(7800_f64)].into()),
531                )),
532            ),
533            (
534                // Paranoid: - Initial tags has whitespace, but parameters are expressed in a compact form.
535                "G1 Z0.350F7800.000",
536                Ok((
537                    "",
538                    Command::G1([PosVal::Z(0.350_f64), PosVal::F(7800_f64)].into()),
539                )),
540            ),
541            (
542                // Paranoid: - Initial tags has whitespace, but parameters are expressed in a compact form.
543                "G1X888F1000",
544                Ok((
545                    "",
546                    Command::G1([PosVal::X(888_f64), PosVal::F(1000_f64)].into()),
547                )),
548            ),
549            (
550                // Leading zero check
551                "G01X100E20",
552                Ok((
553                    "",
554                    Command::G1([PosVal::X(100_f64), PosVal::E(20_f64)].into()),
555                )),
556            ),
557            // Fails : -
558            (
559                // Invalid  missing parameters
560                // G1 Fails falls back to a generic GDrop(1)
561                "G1 ",
562                Ok((" ", Command::GDrop(1))),
563            ),
564            // (
565            //     // Supplied 13 parameters when only 12 are permitted.
566            //     "G1A0A1B2C3E4F5S6U7V8W9X0Y1Z1Z2",
567            //     Ok((
568            //         "",
569            //         Command::G1(
570            //             [
571            //                 PosVal::A(0_f64),
572            //                 PosVal::A(1_f64),
573            //                 PosVal::B(2_f64),
574            //                 PosVal::C(3_f64),
575            //                 PosVal::E(4_f64),
576            //                 PosVal::F(5_f64),
577            //                 PosVal::S(6_f64),
578            //                 PosVal::U(7_f64),
579            //                 PosVal::V(8_f64),
580            //                 PosVal::W(9_f64),
581            //                 PosVal::X(0_f64),
582            //                 PosVal::Y(1_f64),
583            //                 PosVal::Z(2_f64),
584            //             ]
585            //             .into(),
586            //         ),
587            //     )),
588            // ),
589        ];
590
591        for (line, expected) in text_commands {
592            let actual = Command::parse_line(line);
593            assert_eq!(actual, expected, "line: {line}");
594        }
595    }
596
597    // G2 Clockwise arc
598    //
599    // ARC command come in two forms:
600    //
601    // "IJ" Form
602    // "R" Form
603    //
604    // TODO add this test
605    //
606    // IJ Form
607    // At least one of the I J parameters is required.
608    // X and Y can be omitted to do a complete circle.
609    // Mixing I or J with R will throw an error.
610    //
611    // R Form
612    // R specifies the radius. X or Y is required.
613    // Omitting both X and Y will throw an error.
614    // Mixing R with I or J will throw an error.
615    //
616    // source https://marlinfw.org/docs/gcode/G002-G003.html
617    #[test]
618    fn g2() {
619        let text_commands = [
620            (
621                "G2 X125 Y32 I10.5 J10.5; arc",
622                Ok((
623                    "; arc",
624                    Command::G2(ArcForm::IJ(
625                        [
626                            ArcVal::X(125_f64),
627                            ArcVal::Y(32_f64),
628                            ArcVal::I(10.5),
629                            ArcVal::J(10.5),
630                        ]
631                        .into(),
632                    )),
633                )),
634            ),
635            (
636                "G2 I20 J20; X and Y can be omitted to do a complete circle.",
637                Ok((
638                    "; X and Y can be omitted to do a complete circle.",
639                    Command::G2(ArcForm::IJ([ArcVal::I(20_f64), ArcVal::J(20_f64)].into())),
640                )),
641            ),
642            (
643                // Leading zero check
644                "G02X100J20",
645                Ok((
646                    "",
647                    Command::G2(ArcForm::IJ([ArcVal::X(100_f64), ArcVal::J(20_f64)].into())),
648                )),
649            ),
650        ];
651
652        for (line, expected) in text_commands {
653            let actual = Command::parse_line(line);
654            assert_eq!(actual, expected, "line: {line}");
655        }
656    }
657    // // G3 X2 Y7 R5
658
659    // G3 Clockwise arc
660    //
661    // ARC command come in two forms:
662    //
663    // "IJ" Form
664    // "R" Form
665    //
666    // TODO add this test
667    //
668    // IJ Form
669    // At least one of the I J parameters is required.
670    // X and Y can be omitted to do a complete circle.
671    // Mixing I or J with R will throw an error.
672    //
673    // R Form
674    // R specifies the radius. X or Y is required.
675    // Omitting both X and Y will throw an error.
676    // Mixing R with I or J will throw an error.
677    //
678    // source https://marlinfw.org/docs/gcode/G002-G003.html
679    #[test]
680    fn g3() {
681        let text_commands = [
682            (
683                "G3 X125 Y32 I10.5 J10.5; arc",
684                Ok((
685                    "; arc",
686                    Command::G3(ArcForm::IJ(
687                        [
688                            ArcVal::X(125_f64),
689                            ArcVal::Y(32_f64),
690                            ArcVal::I(10.5),
691                            ArcVal::J(10.5),
692                        ]
693                        .into(),
694                    )),
695                )),
696            ),
697            (
698                "G3 I20 J20; X and Y can be omitted to do a complete circle.",
699                Ok((
700                    "; X and Y can be omitted to do a complete circle.",
701                    Command::G3(ArcForm::IJ([ArcVal::I(20_f64), ArcVal::J(20_f64)].into())),
702                )),
703            ),
704            (
705                // Leading zero check
706                "G03X100J20",
707                Ok((
708                    "",
709                    Command::G3(ArcForm::IJ([ArcVal::X(100_f64), ArcVal::J(20_f64)].into())),
710                )),
711            ),
712        ];
713        for (line, expected) in text_commands {
714            let actual = Command::parse_line(line);
715            assert_eq!(actual, expected, "line: {line}");
716        }
717    }
718
719    // G486 Multipart support.
720    //
721    // Start, Un-cancel,
722    #[test]
723    fn m486() {
724        let text_commands = [
725            (
726                "M486 C; cancel the current object (use with care)",
727                Ok((
728                    "; cancel the current object (use with care)",
729                    Command::M486(MultiPartVal::C),
730                )),
731            ),
732            // This is broken...
733            // (
734            //     "M486 S3 A\"cube copy 3\" xx",
735            //     Ok((
736            //         "",
737            //         Command::M486(MultiPartVal::S(3, Some("cube copy 3".to_string()))),
738            //     )),
739            // ),
740            (
741                "M486 S3; Indicate that the 4th object is starting now",
742                Ok((
743                    "; Indicate that the 4th object is starting now",
744                    Command::M486(MultiPartVal::S(3, None)),
745                )),
746            ),
747            (
748                "M486 P10; Cancel object with index 10 (the 11th object)",
749                Ok((
750                    "; Cancel object with index 10 (the 11th object)",
751                    Command::M486(MultiPartVal::P(10)),
752                )),
753            ),
754            (
755                "M486 U2; Un-cancel object with index 2 (the 3rd object)",
756                Ok((
757                    "; Un-cancel object with index 2 (the 3rd object)",
758                    Command::M486(MultiPartVal::U(2)),
759                )),
760            ),
761            (
762                "M486 T12; Total of 12 objects (otherwise the firmware must count)",
763                Ok((
764                    "; Total of 12 objects (otherwise the firmware must count)",
765                    Command::M486(MultiPartVal::T(12)),
766                )),
767            ),
768            (
769                "M486 S-1",
770                Ok(("", Command::M486(MultiPartVal::S(-1, None)))),
771            ),
772            ("M486 T12", Ok(("", Command::M486(MultiPartVal::T(12))))),
773            ("M486 U2", Ok(("", Command::M486(MultiPartVal::U(2))))),
774            ("M486 P1", Ok(("", Command::M486(MultiPartVal::P(1))))),
775            ("M486 S2", Ok(("", Command::M486(MultiPartVal::S(2, None))))),
776            ("M486 T3", Ok(("", Command::M486(MultiPartVal::T(3))))),
777            ("M486 U-1", Ok(("", Command::M486(MultiPartVal::U(-1))))),
778        ];
779
780        for (line, expected) in text_commands {
781            let actual = Command::parse_line(line);
782            assert_eq!(actual, expected, "line: {line}");
783        }
784    }
785
786    #[test]
787    const fn parse_g_drop() {}
788}