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}