com.willwinder.universalgcodesender.GrblUtils.java Source code

Java tutorial

Introduction

Here is the source code for com.willwinder.universalgcodesender.GrblUtils.java

Source

/*
Copyright 2012-2018 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.i18n.Localization;
import com.willwinder.universalgcodesender.listeners.ControllerState;
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.Alarm;
import com.willwinder.universalgcodesender.model.Axis;
import com.willwinder.universalgcodesender.model.Overrides;
import com.willwinder.universalgcodesender.model.Position;
import com.willwinder.universalgcodesender.model.UnitUtils.Units;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;

/**
 * Collection of useful Grbl related utilities.
 *
 * @author wwinder
 */
public class GrblUtils {
    private static final DecimalFormat decimalFormatter = new DecimalFormat("0.0000", Localization.dfs);

    // 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";

    /**
     * For setting a the coordinate to a specific position on an axis.
     * First string parameter should be either X, Y or Z. The second parameter should be a floating point number in
     * the format 0.000
     */
    private static final String GCODE_SET_COORDINATE_V9 = "G10 P0 L20 %s%s";
    private static final String GCODE_SET_COORDINATE_V8 = "G92 %s%s";

    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";

    /**
     * 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 "";
        }
    }

    /**
     * Generate a command to set the work coordinate for a specific axis to zero.
     *
     * @param axis the axis to reset
     * @param grblVersion the GRBL version
     * @param grblVersionLetter the GRBL build version
     * @return a string with the gcode command
     */
    protected static String getResetCoordToZeroCommand(final Axis axis, final double grblVersion,
            final Character grblVersionLetter) {
        return getSetCoordCommand(axis, 0, grblVersion, grblVersionLetter);
    }

    /**
     * Generate a command to set the work coordinate position for the given axis.
     *
     * @param axis the axis change
     * @param position the new work position to use
     * @param grblVersion the GRBL version
     * @param grblVersionLetter the GRBL build version
     * @return a string with the gcode command
     */
    protected static String getSetCoordCommand(final Axis axis, final double position, final double grblVersion,
            final Character grblVersionLetter) {
        if (grblVersion >= 0.9) {
            return String.format(GrblUtils.GCODE_SET_COORDINATE_V9, axis.toString(),
                    decimalFormatter.format(position));
        } else if (grblVersion >= 0.8 && (grblVersionLetter != null) && (grblVersionLetter >= 'c')) {
            // TODO: Is G10 available in 0.8c?
            // No it is not -> error: Unsupported statement
            return String.format(GrblUtils.GCODE_SET_COORDINATE_V8, axis.toString(),
                    decimalFormatter.format(position));
        } else if (grblVersion >= 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();
        ret.addCapability(CapabilitiesConstants.JOGGING);
        ret.addCapability(CapabilitiesConstants.CHECK_MODE);
        ret.addCapability(CapabilitiesConstants.FIRMWARE_SETTINGS);

        if (version >= 0.8) {
            ret.addCapability(CapabilitiesConstants.HOMING);
            ret.addCapability(CapabilitiesConstants.HARD_LIMITS);
        }

        if (version == 0.8 && (letter != null) && (letter >= 'c')) {
            ret.addCapability(GrblCapabilitiesConstants.REAL_TIME);
        }

        if (version >= 0.9) {
            ret.addCapability(GrblCapabilitiesConstants.REAL_TIME);
            ret.addCapability(CapabilitiesConstants.SOFT_LIMITS);
            ret.addCapability(CapabilitiesConstants.SETUP_WIZARD);

        }

        if (version >= 1.1) {
            ret.addCapability(GrblCapabilitiesConstants.V1_FORMAT);
            ret.addCapability(GrblCapabilitiesConstants.HARDWARE_JOGGING);
            ret.addCapability(CapabilitiesConstants.OVERRIDES);
            ret.addCapability(CapabilitiesConstants.CONTINUOUS_JOGGING);
        }

        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.hasCapability(GrblCapabilitiesConstants.V1_FORMAT)) {
            return response.startsWith("[GC:");
        } else {
            return FEEDBACK_PATTERN.matcher(response).find();
        }
    }

    static protected String parseFeedbackMessage(final String response, Capabilities c) {
        if (c.hasCapability(GrblCapabilitiesConstants.V1_FORMAT)) {
            return response.substring(4, response.length() - 1);
        } else {
            return response.substring(1, response.length() - 1);
        }
    }

    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 reportingUnits units
     * @return 
     */
    static protected ControllerStatus getStatusFromStatusString(ControllerStatus lastStatus, final String status,
            final Capabilities version, Units reportingUnits) {
        // Legacy status.
        if (!version.hasCapability(GrblCapabilitiesConstants.V1_FORMAT)) {
            String stateString = getStateFromStatusString(status, version);
            ControllerState state = getControllerStateFromStateString(stateString);
            return new ControllerStatus(stateString, state,
                    getMachinePositionFromStatusString(status, version, reportingUnits),
                    getWorkPositionFromStatusString(status, version, reportingUnits));
        } else {
            String stateString = "";
            Position MPos = null;
            Position WPos = null;
            Position WCO = null;

            OverridePercents overrides = null;
            EnabledPins pins = null;
            AccessoryStates accessoryStates = null;

            double feedSpeed = 0;
            double spindleSpeed = 0;
            if (lastStatus != null) {
                feedSpeed = lastStatus.getFeedSpeed();
                spindleSpeed = lastStatus.getSpindleSpeed();
            }
            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)
                        stateString = part.substring(1);
                    else
                        stateString = 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("");
                }
            }

            ControllerState state = getControllerStateFromStateString(stateString);
            return new ControllerStatus(stateString, state, MPos, WPos, feedSpeed, reportingUnits, 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.hasCapability(GrblCapabilitiesConstants.REAL_TIME)) {
            return null;
        }

        // Search for a version.
        Matcher matcher = STATUS_STATE_PATTERN.matcher(status);
        if (matcher.find()) {
            retValue = matcher.group(0);
        }

        return retValue;
    }

    public static ControllerState getControllerStateFromStateString(String stateString) {
        switch (stateString.toLowerCase()) {
        case "jog":
            return ControllerState.JOG;
        case "run":
            return ControllerState.RUN;
        case "hold":
            return ControllerState.HOLD;
        case "door":
            return ControllerState.DOOR;
        case "home":
            return ControllerState.HOME;
        case "idle":
            return ControllerState.IDLE;
        case "alarm":
            return ControllerState.ALARM;
        case "check":
            return ControllerState.CHECK;
        case "sleep":
            return ControllerState.SLEEP;
        default:
            return ControllerState.UNKNOWN;
        }
    }

    static Pattern mmPattern = Pattern.compile(".*:\\d+\\.\\d\\d\\d,.*");

    static protected Units getUnitsFromStatusString(final String status, final Capabilities version) {
        if (version.hasCapability(GrblCapabilitiesConstants.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.hasCapability(GrblCapabilitiesConstants.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.hasCapability(GrblCapabilitiesConstants.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.hasOverrides()) {
            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;
    }

    public static boolean isOkErrorAlarmResponse(String response) {
        return isOkResponse(response) || isErrorResponse(response) || isAlarmResponse(response);
    }

    public static boolean isOkResponse(String response) {
        return StringUtils.equalsIgnoreCase(response, "ok");
    }

    public static boolean isErrorResponse(String response) {
        return StringUtils.containsIgnoreCase(response, "error");
    }

    public static boolean isAlarmResponse(String response) {
        return StringUtils.startsWith(response, "ALARM");
    }

    public static Alarm parseAlarmResponse(String response) {
        String alarmCode = StringUtils.substringAfter(response.toLowerCase(), "alarm:");
        switch (alarmCode) {
        case "1":
            return Alarm.HARD_LIMIT;
        default:
            return Alarm.UNKONWN;
        }
    }
}