Java tutorial
/* Copyright 2016-2017 Will Winder This file is part of Universal Gcode Sender (UGS). UGS is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. UGS is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with UGS. If not, see <http://www.gnu.org/licenses/>. */ package com.willwinder.universalgcodesender.gcode; import com.google.common.collect.Iterables; import com.willwinder.universalgcodesender.gcode.GcodeParser.GcodeMeta; import com.willwinder.universalgcodesender.gcode.processors.ArcExpander; import com.willwinder.universalgcodesender.gcode.util.GcodeParserException; import com.willwinder.universalgcodesender.gcode.util.Plane; import com.willwinder.universalgcodesender.gcode.processors.CommandLengthProcessor; import com.willwinder.universalgcodesender.gcode.processors.CommentProcessor; import com.willwinder.universalgcodesender.gcode.processors.DecimalProcessor; import com.willwinder.universalgcodesender.gcode.processors.FeedOverrideProcessor; import com.willwinder.universalgcodesender.gcode.processors.LineSplitter; import com.willwinder.universalgcodesender.gcode.processors.M30Processor; import com.willwinder.universalgcodesender.gcode.processors.MeshLeveler; import com.willwinder.universalgcodesender.gcode.processors.WhitespaceProcessor; import com.willwinder.universalgcodesender.gcode.util.Code; import static com.willwinder.universalgcodesender.gcode.util.Code.G0; import static com.willwinder.universalgcodesender.gcode.util.Code.G1; import static com.willwinder.universalgcodesender.gcode.util.Code.G3; import com.willwinder.universalgcodesender.gcode.util.GcodeParserUtils; import com.willwinder.universalgcodesender.i18n.Localization; import com.willwinder.universalgcodesender.model.Position; import com.willwinder.universalgcodesender.model.UnitUtils.Units; import static com.willwinder.universalgcodesender.model.UnitUtils.Units.MM; import com.willwinder.universalgcodesender.types.GcodeCommand; import com.willwinder.universalgcodesender.types.PointSegment; import com.willwinder.universalgcodesender.utils.GcodeStreamReader; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.assertj.core.api.Assertions; import static org.assertj.core.api.Assertions.*; import org.junit.Assert; import org.junit.Test; import static org.junit.Assert.*; /** * * @author wwinder */ public class GcodeParserTest { private void testCommand(List<GcodeMeta> segments, int numResults, double speed, double x, double y, double z, boolean fastTraversal, boolean zMovement, boolean arc, boolean clockwise, boolean isMetric, int num) { int points = 0; for (GcodeMeta meta : segments) { if (meta.point != null) { points++; PointSegment ps = meta.point; assertEquals(ps.getSpeed(), speed, 0); assertEquals(x, ps.point().x, 0); assertEquals(y, ps.point().y, 0); assertEquals(z, ps.point().z, 0); assertEquals(fastTraversal, ps.isFastTraverse()); assertEquals(zMovement, ps.isZMovement()); assertEquals(arc, ps.isArc()); if (arc) { assertEquals(clockwise, ps.isClockwise()); } assertEquals(num, ps.getLineNumber()); assertEquals(isMetric, ps.isMetric()); } } assertEquals(numResults, points); } /** * Test of addCommand method, of class GcodeParser. */ @Test public void testAddCommand_String() throws Exception { System.out.println("addCommand"); List<GcodeMeta> results; GcodeParser instance = new GcodeParser(); // X movement with speed results = instance.addCommand("G20 G0X1F150"); testCommand(results, 1, 150, 1., 0., 0., true, false, false, false, false, 0); results = instance.addCommand("G1Y1F250"); testCommand(results, 1, 250, 1., 1., 0., false, false, false, false, false, 1); // Use same speed from before results = instance.addCommand("G1Z1"); testCommand(results, 1, 250, 1., 1., 1., false, true, false, false, false, 2); // Use same G command from before results = instance.addCommand("X2Y2Z2"); testCommand(results, 1, 250, 2., 2., 2., false, false, false, false, false, 3); results = instance.addCommand("X-0.5Y0Z0"); testCommand(results, 1, 250, -0.5, 0., 0., false, false, false, false, false, 4); // Clockwise arc! results = instance.addCommand("G2 X0. Y0.5 I0.5 J0. F2.5"); testCommand(results, 1, 2.5, 0., 0.5, 0., false, false, true, true, false, 5); results = instance.addCommand("X0.5 Y0. I0. J-0.5"); testCommand(results, 1, 2.5, 0.5, 0., 0., false, false, true, true, false, 6); results = instance.addCommand("X0. Y-0.5 I-0.5 J0."); testCommand(results, 1, 2.5, 0., -0.5, 0., false, false, true, true, false, 7); results = instance.addCommand("X-0.5 Y0. I0. J0.5"); testCommand(results, 1, 2.5, -0.5, 0., 0., false, false, true, true, false, 8); // Move up a bit. results = instance.addCommand("G0 Z2"); testCommand(results, 1, 2.5, -0.5, 0., 2., true, true, false, false, false, 9); // Counter-clockwise arc! results = instance.addCommand("G3 X-0.5 Y0. I0. J0.5"); testCommand(results, 1, 2.5, -0.5, 0., 2., false, false, true, false, false, 10); results = instance.addCommand("X0. Y-0.5 I-0.5 J0."); testCommand(results, 1, 2.5, 0., -0.5, 2., false, false, true, false, false, 11); results = instance.addCommand("X0.5 Y0. I0. J-0.5"); testCommand(results, 1, 2.5, 0.5, 0., 2., false, false, true, false, false, 12); results = instance.addCommand("X0. Y0.5 I0.5 J0. F2.5"); testCommand(results, 1, 2.5, 0., 0.5, 2., false, false, true, false, false, 13); } /** * Test of addCommand method, of class GcodeParser. */ @Test public void testAddCommand_String_int() throws Exception { System.out.println("addCommand"); GcodeParser instance = new GcodeParser(); // More or less the same thing as the above test, so just make sure the // line number is applied. List<GcodeMeta> results = instance.addCommand("G20 G0X1F150", 123); testCommand(results, 1, 150, 1., 0., 0., true, false, false, false, false, 123); } /** * Test of getCurrentPoint method, of class GcodeParser. */ @Test public void testGetCurrentState() throws Exception { System.out.println("getCurrentPoint"); GcodeParser instance = new GcodeParser(); instance.addCommand("G17 G21 G90 G94 G54 M0 M5 M9"); GcodeState state = instance.getCurrentState(); assertEquals(Plane.XY, state.plane); assertEquals(true, state.isMetric); assertEquals(true, state.inAbsoluteMode); } /** * Test of addCommandProcessor method, of class GcodeParser. */ @Test public void testAddCommandProcessor() { System.out.println("addCommandProcessor"); GcodeParser instance = new GcodeParser(); instance.addCommandProcessor(new CommentProcessor()); assertEquals(1, instance.numCommandProcessors()); } /** * Test of resetCommandProcessors method, of class GcodeParser. */ @Test public void testResetCommandProcessors() { System.out.println("resetCommandProcessors"); GcodeParser instance = new GcodeParser(); instance.addCommandProcessor(new CommentProcessor()); assertEquals(1, instance.numCommandProcessors()); instance.resetCommandProcessors(); assertEquals(0, instance.numCommandProcessors()); } /** * Test of preprocessCommand method, of class GcodeParser. */ @Test public void testPreprocessCommandGood() throws Exception { System.out.println("preprocessCommandGood"); // Tests: // '(comment)' is removed // '; Comment!' is removed // 'M30' is removed // Decimal truncated to 0.88889 // Remove spaces String command = "(comment) G01 X0.888888888888888888 M30; Comment!"; GcodeParser instance = new GcodeParser(); instance.addCommandProcessor(new CommentProcessor()); instance.addCommandProcessor(new DecimalProcessor(5)); instance.addCommandProcessor(new M30Processor()); instance.addCommandProcessor(new WhitespaceProcessor()); instance.addCommandProcessor(new CommandLengthProcessor(50)); List<String> result = instance.preprocessCommand(command, instance.getCurrentState()); assertEquals(1, result.size()); assertEquals("G01X0.88889", result.get(0)); } @Test public void testPreprocessCommandFeedOverride() throws Exception { System.out.println("preprocessCommandFeedOverride"); // Tests: // '(comment)' is removed // '; Comment!' is removed // 'M30' is removed // Decimal truncated to 0.88889 // Remove spaces String command = "(comment) G01 X0.888888888888888888 M30 F100; Comment!"; GcodeParser instance = new GcodeParser(); instance.addCommandProcessor(new CommentProcessor()); instance.addCommandProcessor(new FeedOverrideProcessor(0.)); instance.addCommandProcessor(new DecimalProcessor(5)); instance.addCommandProcessor(new M30Processor()); instance.addCommandProcessor(new WhitespaceProcessor()); instance.addCommandProcessor(new CommandLengthProcessor(50)); List<String> result = instance.preprocessCommand(command, instance.getCurrentState()); assertEquals(1, result.size()); assertEquals("G01X0.88889F100", result.get(0)); instance.resetCommandProcessors(); instance.addCommandProcessor(new CommentProcessor()); instance.addCommandProcessor(new FeedOverrideProcessor(200.)); instance.addCommandProcessor(new DecimalProcessor(5)); instance.addCommandProcessor(new M30Processor()); instance.addCommandProcessor(new WhitespaceProcessor()); instance.addCommandProcessor(new CommandLengthProcessor(50)); result = instance.preprocessCommand(command, instance.getCurrentState()); assertEquals(1, result.size()); assertEquals("G01X0.88889F200.0", result.get(0)); } @Test public void testPreprocessCommandException() throws Exception { System.out.println("preprocessCommandException?!"); GcodeParser instance = new GcodeParser(); instance.addCommandProcessor(new CommentProcessor()); // Don't process decimals to make this test easier to create. instance.addCommandProcessor(new DecimalProcessor(0)); instance.addCommandProcessor(new M30Processor()); instance.addCommandProcessor(new WhitespaceProcessor()); instance.addCommandProcessor(new CommandLengthProcessor(50)); // Shouldn't throw if exactly 50 characters long. final String command = "G01X0.88888888888888888888888888888888888888888888"; instance.preprocessCommand(command, instance.getCurrentState()); // Should throw an exception when it is 51 characters long. Assertions.assertThatThrownBy(() -> instance.preprocessCommand(command + "8", instance.getCurrentState())) .isInstanceOf(GcodeParserException.class); } @Test public void autoLevelerProcessorSet() throws Exception { System.out.println("autoLevelerProcessorSet"); GcodeParser gcp = new GcodeParser(); gcp.addCommandProcessor(new CommentProcessor()); gcp.addCommandProcessor(new ArcExpander(true, 0.1)); gcp.addCommandProcessor(new LineSplitter(1)); Position grid[][] = { { new Position(-5, -5, 0, MM), new Position(-5, 35, 0, MM) }, { new Position(35, -5, 0, MM), new Position(35, 35, 0, MM) } }; gcp.addCommandProcessor(new MeshLeveler(0, grid, Units.MM)); Path output = Files.createTempFile("autoleveler_processor_set_test.nc", ""); // Copy resource to temp file since my parser methods need it that way. URL file = this.getClass().getClassLoader().getResource("./gcode/circle_test.nc"); File tempFile = File.createTempFile("temp", "file"); IOUtils.copy(file.openStream(), FileUtils.openOutputStream(tempFile)); GcodeParserUtils.processAndExport(gcp, tempFile, output.toFile()); GcodeStreamReader reader = new GcodeStreamReader(output.toFile()); file = this.getClass().getClassLoader().getResource("./gcode/circle_test.nc.processed"); Files.lines(Paths.get(file.toURI())).forEach((t) -> { try { GcodeCommand c = reader.getNextCommand(); if (c == null) { Assert.fail("Reached end of gcode reader before end of expected commands."); } Assert.assertEquals(c.getCommandString(), t); } catch (IOException ex) { Assert.fail("Unexpected exception."); } }); assertEquals(1027, reader.getNumRows()); output.toFile().delete(); } @Test public void nonGcodeIgnoresImplicitGcode() throws Exception { GcodeParser gcp = new GcodeParser(); gcp.addCommandProcessor(new CommentProcessor()); GcodeState initialState = new GcodeState(); initialState.currentPoint = new Position(0, 0, 1, MM); initialState.currentMotionMode = Code.G0; List<String> result = gcp.preprocessCommand("M05", initialState); assertEquals(1, result.size()); assertEquals("M05", result.get(0)); } @Test public void doubleParenCommentTest() throws Exception { GcodeParser gcp = new GcodeParser(); gcp.addCommandProcessor(new CommentProcessor()); String command = "(comment (with subcomment) still in the comment) G01 X10"; GcodeParser instance = new GcodeParser(); instance.addCommandProcessor(new CommentProcessor()); List<String> result = instance.preprocessCommand(command, instance.getCurrentState()); assertEquals(1, result.size()); assertEquals(" G01 X10", result.get(0)); } @Test public void stateInitialized() throws Exception { GcodeState state = new GcodeState(); Assert.assertEquals(G0, state.currentMotionMode); List<GcodeMeta> metaList = GcodeParser.processCommand("X1", 0, new GcodeState()); Assert.assertEquals(1, metaList.size()); GcodeMeta meta = Iterables.getOnlyElement(metaList); Assert.assertEquals(G0, meta.code); } @Test public void multipleAxisWordCommands() throws Exception { assertThatThrownBy(() -> GcodeParser.processCommand("G0G1X1X2", 0, new GcodeState())) .isInstanceOf(GcodeParserException.class) .hasMessageStartingWith(Localization.getString("parser.gcode.multiple-axis-commands")); } @Test public void missingAxisWords() throws Exception { assertThatThrownBy(() -> GcodeParser.processCommand("G38.2", 0, new GcodeState())) .isInstanceOf(GcodeParserException.class) .hasMessage(Localization.getString("parser.gcode.missing-axis-commands") + ": G38.2"); } @Test public void duplicateFeedException() throws Exception { assertThatThrownBy(() -> GcodeParser.processCommand("F1F1", 0, new GcodeState())) .isInstanceOf(GcodeParserException.class).hasMessage("Multiple F-codes on one line."); } @Test public void duplicateSpindleException() throws Exception { assertThatThrownBy(() -> GcodeParser.processCommand("S1S1", 0, new GcodeState())) .isInstanceOf(GcodeParserException.class).hasMessage("Multiple S-codes on one line."); } @Test public void g28WithAxes() throws Exception { // No exception GcodeParser.processCommand("G28 X1 Y2 Z3", 0, new GcodeState()); } @Test public void g28NoAxes() throws Exception { // No exception GcodeParser.processCommand("G28", 0, new GcodeState()); } @Test public void motionNoAxes() throws Exception { List<GcodeMeta> metaList = GcodeParser.processCommand("G3", 0, new GcodeState()); GcodeMeta meta = Iterables.getOnlyElement(metaList); assertThat(meta.code).isEqualTo(G3); assertThat(meta.state.currentPoint).isEqualTo(new Position(0, 0, 0, MM)); } @Test public void spaceInAxisWord() throws Exception { List<GcodeMeta> metaList = GcodeParser.processCommand("G \t1 X-1Y - 0.\t5Z\n1 .0", 0, new GcodeState()); GcodeMeta meta = Iterables.getOnlyElement(metaList); assertThat(meta.code).isEqualTo(G1); assertThat(meta.state.currentPoint).isEqualTo(new Position(-1, -0.5, 1, MM)); } @Test public void fWordOnly() throws Exception { List<GcodeMeta> metaList = GcodeParser.processCommand("F100", 0, new GcodeState(), true); GcodeMeta meta = Iterables.getOnlyElement(metaList); assertThat(meta.state.speed).isEqualTo(100.0); } @Test public void sWordOnly() throws Exception { List<GcodeMeta> metaList = GcodeParser.processCommand("S100", 0, new GcodeState(), true); GcodeMeta meta = Iterables.getOnlyElement(metaList); assertThat(meta.state.spindleSpeed).isEqualTo(100.0); } }