/* * GRBL Control layer, coordinates all aspects of control. */ /* Copywrite 2013-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; import com.willwinder.universalgcodesender.gcode.GcodeCommandCreator; import com.willwinder.universalgcodesender.gcode.GcodeUtils; import com.willwinder.universalgcodesender.i18n.Localization; import com.willwinder.universalgcodesender.listeners.ControllerStatus; import com.willwinder.universalgcodesender.listeners.GrblSettingsListener; import com.willwinder.universalgcodesender.model.Overrides; import com.willwinder.universalgcodesender.model.Position; import com.willwinder.universalgcodesender.model.UGSEvent.ControlState; import static com.willwinder.universalgcodesender.model.UGSEvent.ControlState.COMM_IDLE; import com.willwinder.universalgcodesender.model.UnitUtils.Units; import com.willwinder.universalgcodesender.types.GcodeCommand; import com.willwinder.universalgcodesender.types.GrblFeedbackMessage; import com.willwinder.universalgcodesender.types.GrblSettingMessage; import com.willwinder.universalgcodesender.utils.GrblLookups; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.Timer; import org.apache.commons.lang3.StringUtils; /** * * @author wwinder */ public class GrblController extends AbstractController { private static final GrblLookups ALARMS = new GrblLookups("alarm_codes"); private static final GrblLookups ERRORS = new GrblLookups("error_codes"); // Grbl state private double grblVersion = 0.0; // The 0.8 in 'Grbl 0.8c' private Character grblVersionLetter = null; // The c in 'Grbl 0.8c' protected Boolean isReady = false; // Not ready until version is received. private GrblSettingsListener settings; // Grbl status members. private GrblUtils.Capabilities capabilities = new GrblUtils.Capabilities(); private String grblState = ""; private Position machineLocation; private Position workLocation; private double maxZLocationMM; // Polling state private int outstandingPolls = 0; private Timer positionPollTimer = null; private ControllerStatus controllerStatus = null; // Canceling state private Boolean isCanceling = false; // Set for the position polling thread. private int attemptsRemaining; private Position lastLocation; public GrblController(AbstractCommunicator comm) { super(comm); this.commandCreator = new GcodeCommandCreator(); this.positionPollTimer = createPositionPollTimer(); this.maxZLocationMM = -1; // Listen for any setting changes. this.settings = new GrblSettingsListener(); this.comm.setListenAll(settings); this.addListener(settings); } public GrblController() { this(new GrblCommunicator()); } @Override public Boolean handlesAllStateChangeEvents() { return capabilities.REAL_TIME; } @Override public long getJobLengthEstimate(File gcodeFile) { // Pending update to support cross-platform and multiple GRBL versions. return 0; //GrblSimulator simulator = new GrblSimulator(settings.getSettings()); //return simulator.estimateRunLength(jobLines); } /*********************** * API Implementation. * ***********************/ @Override protected void rawResponseHandler(String response) { if (GcodeCommand.isOkErrorResponse(response)) { String processed = response; if (response.startsWith("error:")) { String parts[] = response.split(":"); if (parts.length == 2) { String code = parts[1].trim(); if (StringUtils.isNumeric(code)) { String[] errorParts = ERRORS.lookup(code); if (errorParts != null && errorParts.length >= 3) { processed = "error: " + errorParts[1] + ": " + errorParts[2]; } } } } try { this.commandComplete(processed); this.messageForConsole(processed + "\n"); } catch (Exception e) { String message = ""; if (e.getMessage() != null) { message = ": " + e.getMessage(); } this.errorMessageForConsole(Localization.getString("controller.error.response") + " <" + processed + ">" + message); } } else if (GrblUtils.isGrblVersionString(response)) { this.stopPollingPosition(); positionPollTimer = createPositionPollTimer(); this.beginPollingPosition(); this.isReady = true; resetBuffers(); // In case a reset occurred while streaming. if (this.isStreaming()) { checkStreamFinished(); } // Version string goes to console this.messageForConsole(response + "\n"); this.grblVersion = GrblUtils.getVersionDouble(response); this.grblVersionLetter = GrblUtils.getVersionLetter(response); this.capabilities = GrblUtils.getGrblStatusCapabilities(this.grblVersion, this.grblVersionLetter); try { this.sendCommandImmediately(createCommand(GrblUtils.GRBL_VIEW_SETTINGS_COMMAND)); this.sendCommandImmediately(createCommand(GrblUtils.GRBL_VIEW_PARSER_STATE_COMMAND)); } catch (Exception e) { throw new RuntimeException(e); } this.beginPollingPosition(); Logger.getLogger(GrblController.class.getName()).log(Level.CONFIG, "{0} = {1}{2}", new Object[]{Localization.getString("controller.log.version"), this.grblVersion, this.grblVersionLetter}); Logger.getLogger(GrblController.class.getName()).log(Level.CONFIG, "{0} = {1}", new Object[]{Localization.getString("controller.log.realtime"), this.capabilities.REAL_TIME}); } else if (GrblUtils.isGrblProbeMessage(response)) { this.messageForConsole(response + "\n"); Position p = GrblUtils.parseProbePosition(response, getReportingUnits()); if (p != null) { dispatchProbeCoordinates(p); } } else if (GrblUtils.isGrblStatusString(response)) { // Only 1 poll is sent at a time so don't decrement, reset to zero. this.outstandingPolls = 0; // Status string goes to verbose console verboseMessageForConsole(response + "\n"); this.handleStatusString(response); } else if (GrblUtils.isGrblFeedbackMessage(response, capabilities)) { GrblFeedbackMessage grblFeedbackMessage = new GrblFeedbackMessage(response); this.verboseMessageForConsole(grblFeedbackMessage.toString() + "\n"); this.messageForConsole(response + "\n"); setDistanceModeCode(grblFeedbackMessage.getDistanceMode()); setUnitsCode(grblFeedbackMessage.getUnits()); } else if (GrblUtils.isGrblSettingMessage(response)) { GrblSettingMessage message = new GrblSettingMessage(response); this.messageForConsole(message + "\n"); if (message.isReportingUnits()) { setReportingUnits(message.getReportingUnits()); } } else { // Display any unhandled messages this.messageForConsole(response + "\n"); } } @Override protected void pauseStreamingEvent() throws Exception { if (this.capabilities.REAL_TIME) { this.comm.sendByteImmediately(GrblUtils.GRBL_PAUSE_COMMAND); } } @Override protected void resumeStreamingEvent() throws Exception { if (this.capabilities.REAL_TIME) { this.comm.sendByteImmediately(GrblUtils.GRBL_RESUME_COMMAND); } } @Override protected void closeCommBeforeEvent() { this.stopPollingPosition(); } @Override protected void closeCommAfterEvent() { this.grblVersion = 0.0; this.grblVersionLetter = null; } @Override protected void openCommAfterEvent() throws Exception { this.comm.sendByteImmediately(GrblUtils.GRBL_RESET_COMMAND); } @Override protected void isReadyToStreamCommandsEvent() throws Exception { isReadyToSendCommandsEvent(); if (grblState != null && grblState.equals("Alarm")) { throw new Exception(Localization.getString("grbl.exception.Alarm")); } } @Override protected void isReadyToSendCommandsEvent() throws Exception { if (this.isReady == false) { throw new Exception(Localization.getString("controller.exception.booting")); } } @Override protected void cancelSendBeforeEvent() throws Exception { boolean paused = isPaused(); // The cancel button is left enabled at all times now, but can only be // used for some versions of GRBL. if (paused && !this.capabilities.REAL_TIME) { throw new Exception("Cannot cancel while paused with this version of GRBL. Reconnect to reset GRBL."); } // If we're canceling a "jog" just send the door hold command. if (this.capabilities.JOG_MODE && controllerStatus != null && "jog".equalsIgnoreCase(controllerStatus.getState())) { this.comm.sendByteImmediately(GrblUtils.GRBL_JOG_CANCEL_COMMAND); } // Otherwise, check if we can get fancy with a soft reset. else if (!paused && this.capabilities.REAL_TIME) { try { this.pauseStreaming(); this.dispatchStateChange(ControlState.COMM_SENDING_PAUSED); } catch (Exception e) { // Oh well, was worth a shot. System.out.println("Exception while trying to issue a soft reset: " + e.getMessage()); } } } @Override protected void cancelSendAfterEvent() throws Exception { if (this.capabilities.REAL_TIME && this.getStatusUpdatesEnabled()) { // Trigger the position listener to watch for the machine to stop. this.attemptsRemaining = 50; this.isCanceling = true; this.lastLocation = null; } else { } } @Override protected Boolean isIdleEvent() { if (this.capabilities.REAL_TIME) { return this.currentState == COMM_IDLE; } // Otherwise let the abstract controller decide. return true; } /** * Sends the version specific homing cycle to the machine. */ @Override public void performHomingCycle() throws Exception { if (this.isCommOpen()) { String gcode = GrblUtils.getHomingCommand(this.grblVersion, this.grblVersionLetter); if (!"".equals(gcode)) { GcodeCommand command = createCommand(gcode); this.sendCommandImmediately(command); return; } } // Throw exception super.performHomingCycle(); } @Override public void resetCoordinatesToZero() throws Exception { if (this.isCommOpen()) { String gcode = GrblUtils.getResetCoordsToZeroCommand(this.grblVersion, this.grblVersionLetter); if (!"".equals(gcode)) { GcodeCommand command = createCommand(gcode); this.sendCommandImmediately(command); return; } } // Throw exception super.resetCoordinatesToZero(); } @Override public void resetCoordinateToZero(final char coord) throws Exception { if (this.isCommOpen()) { String gcode = GrblUtils.getResetCoordToZeroCommand(coord, this.grblVersion, this.grblVersionLetter); if (!"".equals(gcode)) { GcodeCommand command = createCommand(gcode); this.sendCommandImmediately(command); return; } } // Throw exception super.resetCoordinatesToZero(); } @Override public void returnToHome() throws Exception { if (this.isCommOpen()) { // Not using max for now, it was causing issue for many people. double max = 0; if (this.maxZLocationMM != -1) { max = this.maxZLocationMM; } ArrayList<String> commands = GrblUtils.getReturnToHomeCommands(this.grblVersion, this.grblVersionLetter, this.workLocation.z); if (!commands.isEmpty()) { Iterator<String> iter = commands.iterator(); // Perform the homing commands while(iter.hasNext()){ String gcode = iter.next(); GcodeCommand command = createCommand(gcode); this.sendCommandImmediately(command); } return; } restoreParserModalState(); } // Throw exception super.returnToHome(); } @Override public void killAlarmLock() throws Exception { if (this.isCommOpen()) { String gcode = GrblUtils.getKillAlarmLockCommand(this.grblVersion, this.grblVersionLetter); if (!"".equals(gcode)) { GcodeCommand command = createCommand(gcode); this.sendCommandImmediately(command); return; } } // Throw exception super.killAlarmLock(); } @Override public void toggleCheckMode() throws Exception { if (this.isCommOpen()) { String gcode = GrblUtils.getToggleCheckModeCommand(this.grblVersion, this.grblVersionLetter); if (!"".equals(gcode)) { GcodeCommand command = createCommand(gcode); this.sendCommandImmediately(command); return; } } // Throw exception super.toggleCheckMode(); } @Override public void viewParserState() throws Exception { if (this.isCommOpen()) { String gcode = GrblUtils.getViewParserStateCommand(this.grblVersion, this.grblVersionLetter); if (!"".equals(gcode)) { GcodeCommand command = createCommand(gcode); this.sendCommandImmediately(command); return; } } // Throw exception super.viewParserState(); } /** * If it is supported, a soft reset real-time command will be issued. */ @Override public void softReset() throws Exception { if (this.isCommOpen() && this.capabilities.REAL_TIME) { this.comm.sendByteImmediately(GrblUtils.GRBL_RESET_COMMAND); //Does GRBL need more time to handle the reset? this.comm.softReset(); } } @Override public void jogMachine(int dirX, int dirY, int dirZ, double stepSize, double feedRate, Units units) throws Exception { if (capabilities.JOG_MODE) { // Format step size from spinner. String formattedStepSize = Utils.formatter.format(stepSize); String formattedFeedRate = Utils.formatter.format(feedRate); String commandString = GcodeUtils.generateXYZ("G91", units, formattedStepSize, formattedFeedRate, dirX, dirY, dirZ); GcodeCommand command = createCommand("$J=" + commandString); sendCommandImmediately(command); } else { super.jogMachine(dirX, dirY, dirZ, stepSize, feedRate, units); } } /************ * Helpers. ************/ public String getGrblVersion() { if (this.isCommOpen()) { StringBuilder str = new StringBuilder(); str.append("Grbl "); if (this.grblVersion > 0.0) { str.append(this.grblVersion); } if (this.grblVersionLetter != null) { str.append(this.grblVersionLetter); } if (this.grblVersion <= 0.0 && this.grblVersionLetter == null) { str.append("<").append(Localization.getString("unknown")).append(">"); } return str.toString(); } return "<" + Localization.getString("controller.log.notconnected") + ">"; } /** * Create a timer which will execute GRBL's position polling mechanism. */ private Timer createPositionPollTimer() { // Action Listener for GRBL's polling mechanism. ActionListener actionListener = new ActionListener() { @Override public void actionPerformed(ActionEvent actionEvent) { java.awt.EventQueue.invokeLater(new Runnable() { @Override public void run() { try { if (outstandingPolls == 0) { outstandingPolls++; comm.sendByteImmediately(GrblUtils.GRBL_STATUS_COMMAND); } else { // If a poll is somehow lost after 20 intervals, // reset for sending another. outstandingPolls++; if (outstandingPolls >= 20) { outstandingPolls = 0; } } } catch (Exception ex) { messageForConsole(Localization.getString("controller.exception.sendingstatus") + ": " + ex.getMessage() + "\n"); ex.printStackTrace(); } } }); } }; return new Timer(this.getStatusUpdateRate(), actionListener); } /** * Begin issuing GRBL status request commands. */ private void beginPollingPosition() { // Start sending '?' commands if supported and enabled. if (this.capabilities != null && this.getStatusUpdatesEnabled()) { if (this.positionPollTimer.isRunning() == false) { this.outstandingPolls = 0; this.positionPollTimer.start(); } } } /** * Stop issuing GRBL status request commands. */ private void stopPollingPosition() { if (this.positionPollTimer.isRunning()) { this.positionPollTimer.stop(); } } private void sendStateMessageIfChanged(String beforeState, ControlState current) { ControlState state = ControlState.COMM_IDLE; switch (controllerStatus.getState().toLowerCase()) { case "jog": case "run": state = ControlState.COMM_SENDING; break; case "hold": case "door": state = ControlState.COMM_SENDING_PAUSED; break; case "check": case "alarm": case "idle": if (isStreaming()){ state = ControlState.COMM_SENDING_PAUSED; } else { // GRBL 1.1: cancel the send when from jog -> idle. if (beforeState != null && beforeState.toLowerCase().equals("jog")) { this.comm.cancelSend(); } state = ControlState.COMM_IDLE; } break; } if (current != state) { this.dispatchStateChange(state); } } // No longer a listener event private void handleStatusString(final String string) { if (this.capabilities == null) { return; } String beforeState = (controllerStatus != null) ? controllerStatus.getState() : ""; controllerStatus = GrblUtils.getStatusFromStatusString( controllerStatus, string, capabilities, getReportingUnits()); // Make UGS more responsive to the state being reported by GRBL. sendStateMessageIfChanged(beforeState, this.currentState); grblState = controllerStatus.getState(); machineLocation = controllerStatus.getMachineCoord(); workLocation = controllerStatus.getWorkCoord(); // Prior to GRBL v1.1 the GUI is required to keep checking locations // to verify that the machine has come to a complete stop after // pausing. if (isCanceling) { if (attemptsRemaining > 0 && lastLocation != null) { attemptsRemaining--; // If the machine goes into idle, we no longer need to cancel. if (grblState.equals("Idle")) { isCanceling = false; } // Otherwise check if the machine is Hold and stopped. else if (grblState.equals("Hold") && lastLocation.equals(machineLocation)) { try { this.issueSoftReset(); } catch(Exception e) { this.errorMessageForConsole(e.getMessage()); } isCanceling = false; } if (isCanceling && attemptsRemaining == 0) { this.errorMessageForConsole(Localization.getString("grbl.exception.cancelReset")); } } lastLocation = new Position(machineLocation); } // Save max Z location if (machineLocation != null && this.getUnitsCode() != null) { Units u = this.getUnitsCode().toUpperCase().equals("G21") ? Units.MM : Units.INCH; double zLocationMM = machineLocation.z; if (u == Units.INCH) zLocationMM *= 26.4; if (zLocationMM > this.maxZLocationMM) { maxZLocationMM = zLocationMM; } } dispatchStatusString(controllerStatus); } @Override protected void statusUpdatesEnabledValueChanged(boolean enabled) { if (enabled) { beginPollingPosition(); } else { stopPollingPosition(); } } @Override protected void statusUpdatesRateValueChanged(int rate) { this.stopPollingPosition(); this.positionPollTimer = this.createPositionPollTimer(); // This will start the timer up again if it is supported and enabled. this.beginPollingPosition(); } @Override public void sendOverrideCommand(Overrides command) throws Exception { Byte realTimeCommand = GrblUtils.getOverrideForEnum(command, capabilities); if (realTimeCommand != null) { this.messageForConsole(String.format(">>> 0x%02x\n", realTimeCommand)); this.comm.sendByteImmediately(realTimeCommand); } } }