diff --git a/ugs-core/src/com/willwinder/universalgcodesender/AbstractController.java b/ugs-core/src/com/willwinder/universalgcodesender/AbstractController.java index 83528663ff..a9203085b0 100644 --- a/ugs-core/src/com/willwinder/universalgcodesender/AbstractController.java +++ b/ugs-core/src/com/willwinder/universalgcodesender/AbstractController.java @@ -148,7 +148,7 @@ protected void openCommAfterEvent() throws Exception { /** * Called prior to sending commands, throw an exception if not ready. */ - abstract protected void isReadyToSendCommandsEvent() throws Exception; + abstract protected void isReadyToSendCommandsEvent() throws ControllerException; /** * Called prior to streaming commands, separate in case you need to be more * restrictive about streaming a file vs. sending a command. @@ -490,11 +490,11 @@ public GcodeState getCurrentGcodeState() { * Note: this is the only place where a string is sent to the comm. */ @Override - public void sendCommandImmediately(GcodeCommand command) throws Exception { + public void sendCommandImmediately(GcodeCommand command) throws ControllerException { isReadyToSendCommandsEvent(); if (!isCommOpen()) { - throw new Exception("Cannot send command(s), comm port is not open."); + throw new ControllerException("Cannot send command(s), comm port is not open."); } this.setCurrentState(CommunicatorState.COMM_SENDING); @@ -503,13 +503,13 @@ public void sendCommandImmediately(GcodeCommand command) throws Exception { } @Override - public Boolean isReadyToReceiveCommands() throws Exception { + public Boolean isReadyToReceiveCommands() throws ControllerException { if (!isCommOpen()) { - throw new Exception("Comm port is not open."); + throw new ControllerException("Comm port is not open."); } if (this.isStreaming()) { - throw new Exception("Already streaming."); + throw new ControllerException("Already streaming."); } return true; diff --git a/ugs-core/src/com/willwinder/universalgcodesender/GrblController.java b/ugs-core/src/com/willwinder/universalgcodesender/GrblController.java index 68a0ec0b3a..edc4a8dc97 100644 --- a/ugs-core/src/com/willwinder/universalgcodesender/GrblController.java +++ b/ugs-core/src/com/willwinder/universalgcodesender/GrblController.java @@ -317,9 +317,9 @@ protected void isReadyToStreamCommandsEvent() throws Exception { } @Override - protected void isReadyToSendCommandsEvent() throws Exception { + protected void isReadyToSendCommandsEvent() throws ControllerException { if (!isCommOpen()) { - throw new Exception(Localization.getString("controller.exception.booting")); + throw new ControllerException(Localization.getString("controller.exception.booting")); } } diff --git a/ugs-core/src/com/willwinder/universalgcodesender/IController.java b/ugs-core/src/com/willwinder/universalgcodesender/IController.java index d773f6b166..2bbff53f41 100644 --- a/ugs-core/src/com/willwinder/universalgcodesender/IController.java +++ b/ugs-core/src/com/willwinder/universalgcodesender/IController.java @@ -151,7 +151,7 @@ public interface IController { /* Stream information */ - Boolean isReadyToReceiveCommands() throws Exception; + Boolean isReadyToReceiveCommands() throws ControllerException; Boolean isReadyToStreamFile() throws Exception; Boolean isStreaming(); long getSendDuration(); @@ -183,7 +183,7 @@ public interface IController { Stream content */ GcodeCommand createCommand(String gcode) throws Exception; - void sendCommandImmediately(GcodeCommand cmd) throws Exception; + void sendCommandImmediately(GcodeCommand cmd) throws ControllerException; void queueStream(IGcodeStreamReader r); /** diff --git a/ugs-core/src/com/willwinder/universalgcodesender/firmware/fluidnc/FluidNCController.java b/ugs-core/src/com/willwinder/universalgcodesender/firmware/fluidnc/FluidNCController.java index 1088db0e5d..be0077a683 100644 --- a/ugs-core/src/com/willwinder/universalgcodesender/firmware/fluidnc/FluidNCController.java +++ b/ugs-core/src/com/willwinder/universalgcodesender/firmware/fluidnc/FluidNCController.java @@ -1,5 +1,5 @@ /* - Copyright 2022-2023 Will Winder + Copyright 2022-2024 Will Winder This file is part of Universal Gcode Sender (UGS). @@ -20,6 +20,7 @@ This file is part of Universal Gcode Sender (UGS). import com.willwinder.universalgcodesender.Capabilities; import com.willwinder.universalgcodesender.ConnectionWatchTimer; +import com.willwinder.universalgcodesender.ControllerException; import com.willwinder.universalgcodesender.GrblCapabilitiesConstants; import com.willwinder.universalgcodesender.GrblUtils; import com.willwinder.universalgcodesender.IController; @@ -64,6 +65,7 @@ This file is part of Universal Gcode Sender (UGS). import static com.willwinder.universalgcodesender.model.UnitUtils.Units.MM; import static com.willwinder.universalgcodesender.model.UnitUtils.scaleUnits; import com.willwinder.universalgcodesender.services.MessageService; +import com.willwinder.universalgcodesender.types.CommandException; import com.willwinder.universalgcodesender.types.GcodeCommand; import com.willwinder.universalgcodesender.utils.ControllerUtils; import static com.willwinder.universalgcodesender.utils.ControllerUtils.sendAndWaitForCompletion; @@ -372,7 +374,7 @@ public Boolean isCommOpen() { } @Override - public Boolean isReadyToReceiveCommands() throws Exception { + public Boolean isReadyToReceiveCommands() { return isCommOpen() && !this.isStreaming(); } @@ -612,7 +614,17 @@ private void queryControllerInformation() throws Exception { private void refreshFirmwareSettings() throws FirmwareSettingsException { messageService.dispatchMessage(MessageType.INFO, "*** Fetching device settings\n"); - firmwareSettings.refresh(); + try { + firmwareSettings.refresh(); + } catch (FirmwareSettingsException e) { + messageService.dispatchMessage(MessageType.ERROR, "*** There was an error while fetching the configuration from the controller:\n"); + messageService.dispatchMessage(MessageType.ERROR, e.getMessage() + "\""); + throw e; + } catch (CommandException e) { + messageService.dispatchMessage(MessageType.ERROR, "*** There was an error while reading the configuration, see detailed error message below:\n"); + messageService.dispatchMessage(MessageType.ERROR, e.getMessage() + "\""); + throw e; + } } @Override @@ -627,7 +639,7 @@ public GcodeCommand createCommand(String command) throws Exception { } @Override - public void sendCommandImmediately(GcodeCommand cmd) throws Exception { + public void sendCommandImmediately(GcodeCommand cmd) throws ControllerException { communicator.queueCommand(cmd); communicator.streamCommands(); } diff --git a/ugs-core/src/com/willwinder/universalgcodesender/firmware/fluidnc/FluidNCSettings.java b/ugs-core/src/com/willwinder/universalgcodesender/firmware/fluidnc/FluidNCSettings.java index ae25969700..c8d28b0160 100644 --- a/ugs-core/src/com/willwinder/universalgcodesender/firmware/fluidnc/FluidNCSettings.java +++ b/ugs-core/src/com/willwinder/universalgcodesender/firmware/fluidnc/FluidNCSettings.java @@ -28,6 +28,7 @@ This file is part of Universal Gcode Sender (UGS). import com.willwinder.universalgcodesender.firmware.fluidnc.commands.GetFirmwareSettingsCommand; import com.willwinder.universalgcodesender.model.Axis; import com.willwinder.universalgcodesender.model.UnitUtils; +import com.willwinder.universalgcodesender.types.CommandException; import com.willwinder.universalgcodesender.utils.ControllerUtils; import org.apache.commons.lang3.StringUtils; @@ -57,21 +58,24 @@ public FluidNCSettings(IController controller) { this.controller = controller; } - public void refresh() throws FirmwareSettingsException { + public void refresh() throws FirmwareSettingsException, CommandException { + GetFirmwareSettingsCommand firmwareSettingsCommand = new GetFirmwareSettingsCommand(); + try { - GetFirmwareSettingsCommand firmwareSettingsCommand = new GetFirmwareSettingsCommand(); ControllerUtils.sendAndWaitForCompletion(controller, firmwareSettingsCommand); + } catch (InterruptedException e) { + throw new FirmwareSettingsException("Timed out waiting for the controller settings", e); + } - if (firmwareSettingsCommand.isOk()) { - firmwareSettingsCommand.getSettings().keySet().forEach(key -> { - String value = firmwareSettingsCommand.getSettings().get(key); - FirmwareSetting firmwareSetting = new FirmwareSetting(key, value, "", "", ""); - settings.put(key.toLowerCase(), firmwareSetting); - listeners.forEach(l -> l.onUpdatedFirmwareSetting(firmwareSetting)); - }); - } - } catch (Exception e) { - throw new FirmwareSettingsException("Couldn't fetch settings", e); + + if (firmwareSettingsCommand.isOk()) { + Map responseSettings = firmwareSettingsCommand.getSettings(); + responseSettings.keySet().forEach(key -> { + String value = responseSettings.get(key); + FirmwareSetting firmwareSetting = new FirmwareSetting(key, value, "", "", ""); + settings.put(key.toLowerCase(), firmwareSetting); + listeners.forEach(l -> l.onUpdatedFirmwareSetting(firmwareSetting)); + }); } } diff --git a/ugs-core/src/com/willwinder/universalgcodesender/firmware/fluidnc/commands/GetFirmwareSettingsCommand.java b/ugs-core/src/com/willwinder/universalgcodesender/firmware/fluidnc/commands/GetFirmwareSettingsCommand.java index 3c6f6eb351..fda9378011 100644 --- a/ugs-core/src/com/willwinder/universalgcodesender/firmware/fluidnc/commands/GetFirmwareSettingsCommand.java +++ b/ugs-core/src/com/willwinder/universalgcodesender/firmware/fluidnc/commands/GetFirmwareSettingsCommand.java @@ -18,8 +18,10 @@ This file is part of Universal Gcode Sender (UGS). */ package com.willwinder.universalgcodesender.firmware.fluidnc.commands; +import com.willwinder.universalgcodesender.types.CommandException; import org.apache.commons.lang3.StringUtils; import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.error.YAMLException; import java.util.AbstractMap; import java.util.HashMap; @@ -40,9 +42,13 @@ public Map getSettings() { } String response = StringUtils.removeEnd(getResponse(), "ok"); - Yaml yaml = new Yaml(); - Map settingsTree = yaml.load(response); - return flatten(settingsTree); + try { + Yaml yaml = new Yaml(); + Map settingsTree = yaml.load(response); + return flatten(settingsTree); + } catch (YAMLException e) { + throw new CommandException(e); + } } private Map flatten(Map mapToFlatten) { @@ -60,8 +66,7 @@ private Stream> flatten(Map.Entry entr } Object value = entry.getValue(); - if (value instanceof Map) { - Map properties = (Map) value; + if (value instanceof Map properties) { return properties.entrySet().stream() .flatMap(e -> flatten(new AbstractMap.SimpleEntry<>(entry.getKey().toLowerCase() + "/" + e.getKey().toString().toLowerCase(), e.getValue()))); } diff --git a/ugs-core/src/com/willwinder/universalgcodesender/firmware/smoothie/SmoothieController.java b/ugs-core/src/com/willwinder/universalgcodesender/firmware/smoothie/SmoothieController.java index dbde1defdd..36e61fbbe0 100644 --- a/ugs-core/src/com/willwinder/universalgcodesender/firmware/smoothie/SmoothieController.java +++ b/ugs-core/src/com/willwinder/universalgcodesender/firmware/smoothie/SmoothieController.java @@ -21,6 +21,7 @@ This file is part of Universal Gcode Sender (UGS). import com.willwinder.universalgcodesender.AbstractController; import com.willwinder.universalgcodesender.Capabilities; import com.willwinder.universalgcodesender.CapabilitiesConstants; +import com.willwinder.universalgcodesender.ControllerException; import com.willwinder.universalgcodesender.GrblUtils; import com.willwinder.universalgcodesender.StatusPollTimer; import com.willwinder.universalgcodesender.communicator.ICommunicator; @@ -134,9 +135,9 @@ protected void isReadyToStreamCommandsEvent() throws Exception { } @Override - protected void isReadyToSendCommandsEvent() throws Exception { + protected void isReadyToSendCommandsEvent() throws ControllerException { if (!this.isReady && !isSmoothieReady) { - throw new Exception(Localization.getString("controller.exception.booting")); + throw new ControllerException(Localization.getString("controller.exception.booting")); } } diff --git a/ugs-core/src/com/willwinder/universalgcodesender/types/CommandException.java b/ugs-core/src/com/willwinder/universalgcodesender/types/CommandException.java new file mode 100644 index 0000000000..36c6c3453e --- /dev/null +++ b/ugs-core/src/com/willwinder/universalgcodesender/types/CommandException.java @@ -0,0 +1,39 @@ +/* + Copyright 2024 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 . + */ +package com.willwinder.universalgcodesender.types; + +/** + * Exception related to commands and the response parsing + * + * @author Joacim Breiler + */ +public class CommandException extends RuntimeException { + public CommandException(String message) { + super(message); + } + + public CommandException(String message, Throwable throwable) { + super(message, throwable); + } + + public CommandException(Throwable throwable) { + super(throwable); + } +} + diff --git a/ugs-core/src/com/willwinder/universalgcodesender/utils/ControllerUtils.java b/ugs-core/src/com/willwinder/universalgcodesender/utils/ControllerUtils.java index b0ac6c81b1..8cb19f9b0e 100644 --- a/ugs-core/src/com/willwinder/universalgcodesender/utils/ControllerUtils.java +++ b/ugs-core/src/com/willwinder/universalgcodesender/utils/ControllerUtils.java @@ -1,5 +1,5 @@ /* - Copyright 2022 Will Winder + Copyright 2022-2024 Will Winder This file is part of Universal Gcode Sender (UGS). @@ -46,9 +46,9 @@ private ControllerUtils() { * @param controller the controller to send the command through * @param command a command to send * @param maxExecutionTime the max number of milliseconds to wait before throwing a timeout error - * @throws Exception if the command could not be sent or a timeout occurred + * @throws InterruptedException if the command could not be sent or a timeout occurred */ - public static T sendAndWaitForCompletion(IController controller, T command, long maxExecutionTime) throws Exception { + public static T sendAndWaitForCompletion(IController controller, T command, long maxExecutionTime) throws InterruptedException { final AtomicBoolean isDone = new AtomicBoolean(false); CommandListener listener = c -> isDone.set(c.isDone()); command.addListener(listener); @@ -57,7 +57,7 @@ public static T sendAndWaitForCompletion(IController co long startTime = System.currentTimeMillis(); while (!isDone.get()) { if (System.currentTimeMillis() > startTime + maxExecutionTime) { - throw new RuntimeException("The command \"" + command.getCommandString() + "\" has timed out as it wasn't finished within " + maxExecutionTime + "ms"); + throw new InterruptedException("The command \"" + command.getCommandString() + "\" has timed out as it wasn't finished within " + maxExecutionTime + "ms"); } Thread.sleep(10); } @@ -72,10 +72,10 @@ public static T sendAndWaitForCompletion(IController co * @param command a command * @param a class extending from {@link GcodeCommand} * @return the executed command with the response - * @throws Exception if the command could not be sent or a timeout occurred + * @throws InterruptedException if the command could not be sent or a timeout occurred */ - public static T sendAndWaitForCompletion(IController controller, T command) throws Exception { + public static T sendAndWaitForCompletion(IController controller, T command) throws InterruptedException { return sendAndWaitForCompletion(controller, command, MAX_EXECUTION_TIME); } @@ -89,7 +89,7 @@ public static void waitOnActiveCommands(IController controller) throws Interrupt long startTime = System.currentTimeMillis(); while (controller.getActiveCommand().isPresent()) { if (startTime + maxExecutionTime < System.currentTimeMillis()) { - throw new RuntimeException("The command \"" + controller.getActiveCommand().get().getCommandString() + "\" has timed out as it wasn't finished within " + maxExecutionTime + "ms"); + throw new InterruptedException("The command \"" + controller.getActiveCommand().get().getCommandString() + "\" has timed out as it wasn't finished within " + maxExecutionTime + "ms"); } Thread.sleep(10); diff --git a/ugs-core/test/com/willwinder/universalgcodesender/firmware/fluidnc/commands/GetFirmwareSettingsCommandTest.java b/ugs-core/test/com/willwinder/universalgcodesender/firmware/fluidnc/commands/GetFirmwareSettingsCommandTest.java index 9cfe14ca29..e820207641 100644 --- a/ugs-core/test/com/willwinder/universalgcodesender/firmware/fluidnc/commands/GetFirmwareSettingsCommandTest.java +++ b/ugs-core/test/com/willwinder/universalgcodesender/firmware/fluidnc/commands/GetFirmwareSettingsCommandTest.java @@ -1,11 +1,13 @@ package com.willwinder.universalgcodesender.firmware.fluidnc.commands; +import com.willwinder.universalgcodesender.types.CommandException; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import org.junit.Test; import java.util.Map; -import static org.junit.Assert.assertEquals; - public class GetFirmwareSettingsCommandTest { @Test public void appendResponseWithYamlConfigShouldParseSettingsAsYaml() { @@ -26,4 +28,14 @@ public void appendResponseWithYamlConfigShouldParseSettingsAsYaml() { assertEquals("800", settings.get("axis/x/steps_per_mm")); assertEquals("800", settings.get("axis/y/steps_per_mm")); } + + @Test + public void getSettingsShouldThrowExceptionOnFaultyYaml() { + GetFirmwareSettingsCommand command = new GetFirmwareSettingsCommand(); + command.appendResponse("meta: a command with nested colons : should not be allowed"); + command.appendResponse("ok"); + + CommandException exception = assertThrows(CommandException.class, command::getSettings); + assertTrue(exception.getMessage().startsWith("mapping values are not allowed here")); + } } diff --git a/ugs-core/test/com/willwinder/universalgcodesender/utils/ControllerUtilsTest.java b/ugs-core/test/com/willwinder/universalgcodesender/utils/ControllerUtilsTest.java index 7b680d1811..c2aa377dd7 100644 --- a/ugs-core/test/com/willwinder/universalgcodesender/utils/ControllerUtilsTest.java +++ b/ugs-core/test/com/willwinder/universalgcodesender/utils/ControllerUtilsTest.java @@ -1,5 +1,5 @@ /* - Copyright 2022 Will Winder + Copyright 2022-2024 Will Winder This file is part of Universal Gcode Sender (UGS). @@ -23,24 +23,22 @@ This file is part of Universal Gcode Sender (UGS). import com.willwinder.universalgcodesender.listeners.ControllerState; import com.willwinder.universalgcodesender.model.CommunicatorState; import com.willwinder.universalgcodesender.types.GcodeCommand; -import org.junit.Test; - -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Supplier; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import org.junit.Test; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + public class ControllerUtilsTest { @Test @@ -75,7 +73,7 @@ public void sendAndWaitForCompletionShouldBlockAndReturnWhenCommandIsDone() thro public void sendAndWaitForCompletionShouldTimeOut() throws Exception { IController controller = mock(IController.class); GcodeCommand command = new GcodeCommand("blah"); - assertThrows("The command \"blah\" has timed out as it wasn't finished within 100ms", RuntimeException.class, () -> ControllerUtils.sendAndWaitForCompletion(controller, command, 100)); + assertThrows("The command \"blah\" has timed out as it wasn't finished within 100ms", InterruptedException.class, () -> ControllerUtils.sendAndWaitForCompletion(controller, command, 100)); } @Test