/* * Collection of useful Grbl related utilities. */ /* Copywrite 2012-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.listeners.ControllerStatus; import com.willwinder.universalgcodesender.listeners.ControllerStatus.OverridePercents; import com.willwinder.universalgcodesender.listeners.ControllerStatus.AccessoryStates; import com.willwinder.universalgcodesender.listeners.ControllerStatus.EnabledPins; import com.willwinder.universalgcodesender.model.Overrides; import com.willwinder.universalgcodesender.model.Position; import com.willwinder.universalgcodesender.model.UnitUtils.Units; import java.util.ArrayList; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * * @author wwinder */ public class GrblUtils { // Note: 5 characters of this buffer reserved for real time commands. public static final int GRBL_RX_BUFFER_SIZE= 123; /** * Grbl commands */ // Real time public static final byte GRBL_PAUSE_COMMAND = '!'; public static final byte GRBL_RESUME_COMMAND = '~'; public static final byte GRBL_STATUS_COMMAND = '?'; public static final byte GRBL_DOOR_COMMAND = (byte)0x84; public static final byte GRBL_JOG_CANCEL_COMMAND = (byte)0x85; public static final byte GRBL_RESET_COMMAND = 0x18; // Non real time public static final String GRBL_KILL_ALARM_LOCK_COMMAND = "$X"; public static final String GRBL_TOGGLE_CHECK_MODE_COMMAND = "$C"; public static final String GRBL_VIEW_PARSER_STATE_COMMAND = "$G"; public static final String GRBL_VIEW_SETTINGS_COMMAND = "$$"; /** * Gcode Commands */ public static final String GCODE_RESET_COORDINATES_TO_ZERO_V9 = "G10 P0 L20 X0 Y0 Z0"; public static final String GCODE_RESET_COORDINATES_TO_ZERO_V8 = "G92 X0 Y0 Z0"; public static final String GCODE_RESET_COORDINATE_TO_ZERO_V9 = "G10 P0 L20 %c0"; public static final String GCODE_RESET_COORDINATE_TO_ZERO_V8 = "G92 %c0"; public static final String GCODE_RETURN_TO_ZERO_LOCATION_V8 = "G90 G0 X0 Y0"; public static final String GCODE_RETURN_TO_ZERO_LOCATION_Z0_V8 = "G90 G0 Z0"; public static final String GCODE_RETURN_TO_MAX_Z_LOCATION_V8 = "G90 G0 Z"; public static final String GCODE_PERFORM_HOMING_CYCLE_V8 = "G28 X0 Y0 Z0"; public static final String GCODE_PERFORM_HOMING_CYCLE_V8C = "$H"; public static class Capabilities { public boolean REAL_TIME = false; public boolean OVERRIDES = false; public boolean V1_FORMAT = false; public boolean JOG_MODE = false; } /** * Checks if the string contains the GRBL version. */ static Boolean isGrblVersionString(final String response) { Boolean version = response.startsWith("Grbl ") || response.startsWith("CarbideMotion "); return version && (getVersionDouble(response) != -1); } /** * Parses the version double out of the version response string. */ final static String VERSION_DOUBLE_REGEX = "[0-9]*\\.[0-9]*"; final static Pattern VERSION_DOUBLE_PATTERN = Pattern.compile(VERSION_DOUBLE_REGEX); static protected double getVersionDouble(final String response) { double retValue = -1; // Search for a version. Matcher matcher = VERSION_DOUBLE_PATTERN.matcher(response); if (matcher.find()) { retValue = Double.parseDouble(matcher.group(0)); } return retValue; } final static String VERSION_LETTER_REGEX = "(?<=[0-9]\\.[0-9])[a-zA-Z]"; final static Pattern VERSION_LETTER_PATTERN = Pattern.compile(VERSION_LETTER_REGEX); static protected Character getVersionLetter(final String response) { Character retValue = null; // Search for a version. Matcher matcher = VERSION_LETTER_PATTERN.matcher(response); if (matcher.find()) { retValue = matcher.group(0).charAt(0); //retValue = Double.parseDouble(matcher.group(0)); } return retValue; } static protected String getHomingCommand(final double version, final Character letter) { if ((version >= 0.8 && (letter != null) && (letter >= 'c')) || version >= 0.9) { return GrblUtils.GCODE_PERFORM_HOMING_CYCLE_V8C; } else if (version >= 0.8) { return GrblUtils.GCODE_PERFORM_HOMING_CYCLE_V8; } else { return ""; } } static protected String getResetCoordsToZeroCommand(final double version, final Character letter) { if (version >= 0.9) { return GrblUtils.GCODE_RESET_COORDINATES_TO_ZERO_V9; } else if (version >= 0.8 && (letter != null) && (letter >= 'c')) { // TODO: Is G10 available in 0.8c? // No it is not -> error: Unsupported statement return GrblUtils.GCODE_RESET_COORDINATES_TO_ZERO_V8; } else if (version >= 0.8) { return GrblUtils.GCODE_RESET_COORDINATES_TO_ZERO_V8; } else { return ""; } } static protected String getResetCoordToZeroCommand(final char coord, final double version, final Character letter) { if (version >= 0.9) { return String.format(GrblUtils.GCODE_RESET_COORDINATE_TO_ZERO_V9, coord); } else if (version >= 0.8 && (letter != null) && (letter >= 'c')) { // TODO: Is G10 available in 0.8c? // No it is not -> error: Unsupported statement return String.format(GrblUtils.GCODE_RESET_COORDINATE_TO_ZERO_V8, coord); } else if (version >= 0.8) { return ""; } else { return ""; } } static protected ArrayList<String> getReturnToHomeCommands(final double version, final Character letter, final double zHeight) { ArrayList<String> commands = new ArrayList<>(); // If Z is less than zero, raise it before further movement. if (zHeight < 0) { commands.add(GrblUtils.GCODE_RETURN_TO_ZERO_LOCATION_Z0_V8); } commands.add(GrblUtils.GCODE_RETURN_TO_ZERO_LOCATION_V8); commands.add(GrblUtils.GCODE_RETURN_TO_ZERO_LOCATION_Z0_V8); return commands; } static protected String getKillAlarmLockCommand(final double version, final Character letter) { if ((version >= 0.8 && (letter != null) && letter >= 'c') || version >= 0.9) { return GrblUtils.GRBL_KILL_ALARM_LOCK_COMMAND; } else { return ""; } } static protected String getToggleCheckModeCommand(final double version, final Character letter) { if ((version >= 0.8 && (letter != null) && letter >= 'c') || version >= 0.9) { return GrblUtils.GRBL_TOGGLE_CHECK_MODE_COMMAND; } else { return ""; } } static protected String getViewParserStateCommand(final double version, final Character letter) { if ((version >= 0.8 && (letter != null) && letter >= 'c') || version >= 0.9) { return GrblUtils.GRBL_VIEW_PARSER_STATE_COMMAND; } else { return ""; } } /** * Determines version of GRBL position capability. */ static protected Capabilities getGrblStatusCapabilities(final double version, final Character letter) { Capabilities ret = new Capabilities(); // Check if real time commands are enabled. if (version==0.8 && (letter != null) && (letter >= 'c')) { ret.REAL_TIME = true; } else if (version >= 0.9) { ret.REAL_TIME = true; } // Check for V1.x features if (version >= 1.1) { ret.REAL_TIME = true; // GRBL 1.1 ret.V1_FORMAT = true; ret.OVERRIDES = true; ret.JOG_MODE = true; } return ret; } static String PROBE_POSITION_REGEX = "\\[PRB:(-?\\d*\\.\\d*),(-?\\d*\\.\\d*),(-?\\d*\\.\\d*)(?::(\\d))?]"; static Pattern PROBE_POSITION_PATTERN = Pattern.compile(PROBE_POSITION_REGEX); static protected Position parseProbePosition(final String response, final Units units) { // Don't parse failed probe response. if (response.contains(":0]")) { return null; } return GrblUtils.getPositionFromStatusString(response, PROBE_POSITION_PATTERN, units); } /** * Check if a string contains a GRBL position string. */ private static final String STATUS_REGEX = "\\<.*\\>"; private static final Pattern STATUS_PATTERN = Pattern.compile(STATUS_REGEX); static protected Boolean isGrblStatusString(final String response) { return STATUS_PATTERN.matcher(response).find(); } private static final String PROBE_REGEX = "\\[PRB:.*\\]"; private static final Pattern PROBE_PATTERN = Pattern.compile(PROBE_REGEX); static protected Boolean isGrblProbeMessage(final String response) { return PROBE_PATTERN.matcher(response).find(); } private static final String FEEDBACK_REGEX = "\\[.*\\]"; private static final Pattern FEEDBACK_PATTERN = Pattern.compile(FEEDBACK_REGEX); static protected Boolean isGrblFeedbackMessage(final String response, Capabilities c) { if (c.V1_FORMAT) { return response.startsWith("[GC:"); } else { return FEEDBACK_PATTERN.matcher(response).find(); } } private static final String SETTING_REGEX = "\\$\\d+=.+"; private static final Pattern SETTING_PATTERN = Pattern.compile(SETTING_REGEX); static protected Boolean isGrblSettingMessage(final String response) { return SETTING_PATTERN.matcher(response).find(); } /** * Parses a GRBL status string in the legacy format or v1.x format: * legacy: <status,WPos:1,2,3,MPos:1,2,3> * 1.x: <status|WPos:1,2,3|Bf:0,0|WCO:0,0,0> * @param lastStatus required for the 1.x version which requires WCO coords * and override status from previous status updates. * @param status the raw status string * @param version capabilities flags * @param units units * @return */ static protected ControllerStatus getStatusFromStatusString( ControllerStatus lastStatus, final String status, final Capabilities version, Units reportingUnits) { // Legacy status. if (!version.V1_FORMAT) { return new ControllerStatus( getStateFromStatusString(status, version), getMachinePositionFromStatusString(status, version, reportingUnits), getWorkPositionFromStatusString(status, version, reportingUnits)); } else { String state = ""; Position MPos = null; Position WPos = null; Position WCO = null; OverridePercents overrides = null; EnabledPins pins = null; AccessoryStates accessoryStates = null; Double feedSpeed = null; Double spindleSpeed = null; boolean isOverrideReport = false; // Parse out the status messages. for (String part : status.substring(0, status.length()-1).split("\\|")) { if (part.startsWith("<")) { int idx = part.indexOf(':'); if (idx == -1) state = part.substring(1); else state = part.substring(1, idx); } else if (part.startsWith("MPos:")) { MPos = GrblUtils.getPositionFromStatusString(status, machinePattern, reportingUnits); } else if (part.startsWith("WPos:")) { WPos = GrblUtils.getPositionFromStatusString(status, workPattern, reportingUnits); } else if (part.startsWith("WCO:")) { WCO = GrblUtils.getPositionFromStatusString(status, wcoPattern, reportingUnits); } else if (part.startsWith("Ov:")) { isOverrideReport = true; String[] overrideParts = part.substring(3).trim().split(","); if (overrideParts.length == 3) { overrides = new OverridePercents( Integer.parseInt(overrideParts[0]), Integer.parseInt(overrideParts[1]), Integer.parseInt(overrideParts[2])); } } else if (part.startsWith("F:")) { feedSpeed = Double.parseDouble(part.substring(2)); } else if (part.startsWith("FS:")) { String[] parts = part.substring(3).split(","); feedSpeed = Double.parseDouble(parts[0]); spindleSpeed = Double.parseDouble(parts[1]); } else if (part.startsWith("Pn:")) { String value = part.substring(part.indexOf(':')+1); pins = new EnabledPins(value); } else if (part.startsWith("A:")) { String value = part.substring(part.indexOf(':')+1); accessoryStates = new AccessoryStates(value); } } // Grab WCO from state information if necessary. if (WCO == null) { // Grab the work coordinate offset. if (lastStatus != null && lastStatus.getWorkCoordinateOffset() != null) { WCO = lastStatus.getWorkCoordinateOffset(); } else { WCO = new Position(0,0,0, reportingUnits); } } // Calculate missing coordinate with WCO if (WPos == null) { WPos = new Position(MPos.x-WCO.x, MPos.y-WCO.y, MPos.z-WCO.z, reportingUnits); } if (MPos == null) { MPos = new Position(WPos.x+WCO.x, WPos.y+WCO.y, WPos.z+WCO.z, reportingUnits); } if (!isOverrideReport && lastStatus != null) { overrides = lastStatus.getOverrides(); pins = lastStatus.getEnabledPins(); accessoryStates = lastStatus.getAccessoryStates(); } else if (isOverrideReport) { // If this is an override report and the 'Pn:' field wasn't sent // set all pins to a disabled state. if (pins == null) { pins = new EnabledPins(""); } // Likewise for accessory states. if (accessoryStates == null) { accessoryStates = new AccessoryStates(""); } } return new ControllerStatus(state, MPos, WPos, feedSpeed, spindleSpeed, overrides, WCO, pins, accessoryStates); } } /** * Parse state out of position string. */ final static String STATUS_STATE_REGEX = "(?<=\\<)[a-zA-z]*(?=[,])"; final static Pattern STATUS_STATE_PATTERN = Pattern.compile(STATUS_STATE_REGEX); static protected String getStateFromStatusString(final String status, final Capabilities version) { String retValue = null; if (!version.REAL_TIME) { return null; } // Search for a version. Matcher matcher = STATUS_STATE_PATTERN.matcher(status); if (matcher.find()) { retValue = matcher.group(0);; } return retValue; } static Pattern mmPattern = Pattern.compile(".*:\\d+\\.\\d\\d\\d,.*"); static protected Units getUnitsFromStatusString(final String status, final Capabilities version) { if (version.REAL_TIME) { if (mmPattern.matcher(status).find()) { return Units.MM; } else { return Units.INCH; } } return Units.UNKNOWN; } static Pattern machinePattern = Pattern.compile("(?<=MPos:)(-?\\d*\\..\\d*),(-?\\d*\\..\\d*),(-?\\d*\\..\\d*)"); static Pattern workPattern = Pattern.compile("(?<=WPos:)(\\-?\\d*\\..\\d*),(\\-?\\d*\\..\\d*),(\\-?\\d*\\..\\d*)"); static Pattern wcoPattern = Pattern.compile("(?<=WCO:)(\\-?\\d*\\..\\d*),(\\-?\\d*\\..\\d*),(\\-?\\d*\\..\\d*)"); static protected Position getMachinePositionFromStatusString(final String status, final Capabilities version, Units reportingUnits) { if (version.REAL_TIME) { return GrblUtils.getPositionFromStatusString(status, machinePattern, reportingUnits); } else { return null; } } static protected Position getWorkPositionFromStatusString(final String status, final Capabilities version, Units reportingUnits) { if (version.REAL_TIME) { return GrblUtils.getPositionFromStatusString(status, workPattern, reportingUnits); } else { return null; } } static private Position getPositionFromStatusString(final String status, final Pattern pattern, Units reportingUnits) { Matcher matcher = pattern.matcher(status); if (matcher.find()) { return new Position(Double.parseDouble(matcher.group(1)), Double.parseDouble(matcher.group(2)), Double.parseDouble(matcher.group(3)), reportingUnits); } return null; } /** * Map version enum to GRBL real time command byte. */ static public Byte getOverrideForEnum(final Overrides command, final Capabilities version) { if (version != null && version.OVERRIDES) { switch (command) { //CMD_DEBUG_REPORT, // 0x85 // Only when DEBUG enabled, sends debug report in '{}' braces. case CMD_FEED_OVR_RESET: return (byte)0x90; // Restores feed override value to 100%. case CMD_FEED_OVR_COARSE_PLUS: return (byte)0x91; case CMD_FEED_OVR_COARSE_MINUS: return (byte)0x92; case CMD_FEED_OVR_FINE_PLUS : return (byte)0x93; case CMD_FEED_OVR_FINE_MINUS : return (byte)0x94; case CMD_RAPID_OVR_RESET: return (byte)0x95; case CMD_RAPID_OVR_MEDIUM: return (byte)0x96; case CMD_RAPID_OVR_LOW: return (byte)0x97; case CMD_SPINDLE_OVR_RESET: return (byte)0x99; // Restores spindle override value to 100%. case CMD_SPINDLE_OVR_COARSE_PLUS: return (byte)0x9A; case CMD_SPINDLE_OVR_COARSE_MINUS: return (byte)0x9B; case CMD_SPINDLE_OVR_FINE_PLUS: return (byte)0x9C; case CMD_SPINDLE_OVR_FINE_MINUS: return (byte)0x9D; case CMD_TOGGLE_SPINDLE: return (byte)0x9E; case CMD_TOGGLE_FLOOD_COOLANT: return (byte)0xA0; case CMD_TOGGLE_MIST_COOLANT: return (byte)0xA1; } } return null; } }