/* * GRBL serial port interface class. */ /* Copywrite 2012-2016 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 static com.willwinder.universalgcodesender.AbstractCommunicator.SerialCommunicatorEvent.*; import com.willwinder.universalgcodesender.types.GcodeCommand; import com.willwinder.universalgcodesender.utils.CommUtils; import com.willwinder.universalgcodesender.utils.GcodeStreamReader; import java.io.BufferedReader; import java.io.IOException; import java.io.Reader; import java.util.concurrent.LinkedBlockingDeque; /** * * @author wwinder */ public abstract class BufferedCommunicator extends AbstractCommunicator { // Command streaming variables private Boolean sendPaused = false; private GcodeCommand nextCommand; // Cached command. private BufferedReader rawCommandStream; // Arbitrary number of commands private GcodeStreamReader commandStream; // Arbitrary number of commands private LinkedBlockingDeque<String> commandBuffer; // Manually specified commands private LinkedBlockingDeque<GcodeCommand> activeCommandList; // Currently running commands private int sentBufferSize = 0; private Boolean singleStepModeEnabled = false; abstract public int getBufferSize(); protected void setQueuesForTesting(LinkedBlockingDeque<String> cb, LinkedBlockingDeque<GcodeCommand> asl) { this.commandBuffer = cb; this.activeCommandList = asl; } public BufferedCommunicator() { } @Override public void setSingleStepMode(boolean enable) { this.singleStepModeEnabled = enable; } @Override public boolean getSingleStepMode() { return this.singleStepModeEnabled; } /** * Add command to the command queue outside file mode. This is the only way * to send a command to the comm port without being in file mode. * These commands will be sent prior to any queued stream, they should * typically be control commands calculated by the application. */ @Override public void queueStringForComm(final String input) { String commandString = input; // Add command to queue this.commandBuffer.add(commandString); } /** * Arbitrary length of commands to send to the communicator. * @param input */ @Override public void queueRawStreamForComm(final Reader input) { rawCommandStream = new BufferedReader(input); } /** * Arbitrary length of commands to send to the communicator. * @param input */ @Override public void queueStreamForComm(final GcodeStreamReader input) { commandStream = input; } /* // TODO: Figure out why this isn't working ... boolean isCommPortOpen() throws NoSuchPortException { CommPortIdentifier portIdentifier = CommPortIdentifier.getPortIdentifier(this.commPort.getName()); String owner = portIdentifier.getCurrentOwner(); String thisClass = this.getClass().getName(); return portIdentifier.isCurrentlyOwned() && owner.equals(thisClass); } */ /** File Stream Methods. **/ @Override public void resetBuffersInternal() { if (activeCommandList != null) { activeCommandList.clear(); } } @Override public String activeCommandSummary() { StringBuilder sb = new StringBuilder(); String comma = ""; for (GcodeCommand gc : activeCommandList) { sb.append(comma).append(gc.getCommandString()); comma = ", "; } if (commandStream != null) { sb.append(comma) .append(commandStream.getNumRowsRemaining()) .append(" streaming commands."); } return sb.toString(); } @Override public boolean areActiveCommands() { return (this.activeCommandList.size() > 0) || (this.commandStream != null && this.commandStream.getNumRowsRemaining() > 0); } @Override public int numActiveCommands() { int streamingCount = commandStream == null ? 0 : commandStream.getNumRowsRemaining(); return this.activeCommandList.size() + streamingCount; } // Helper for determining if commands should be throttled. private boolean allowMoreCommands() { if (this.singleStepModeEnabled) { return this.activeCommandList.isEmpty(); } return true; } /** * THIS COMMAND CAN ONLY BE CALLED FROM streamCommands UNLESS * THE nextCommand OBJECT IS SYNCHRONIZED. * * Returns the next command with the following priority: * 1. nextCommand object if set. * 2. Front of the commandBuffer collection. * 3. Next line in the commandStream. * @return */ private GcodeCommand getNextCommand() { if (nextCommand != null) { return nextCommand; } else if (!this.commandBuffer.isEmpty()) { nextCommand = new GcodeCommand(commandBuffer.pop()); } else try { if (rawCommandStream != null && rawCommandStream.ready()) { nextCommand = new GcodeCommand(rawCommandStream.readLine()); } else if (commandStream != null && commandStream.ready()) { nextCommand = commandStream.getNextCommand(); } } catch (IOException ex) { // Fall through to null handling. } if (nextCommand != null) { nextCommand.setCommandNumber(getNextCommandId()); if (nextCommand.getCommandString().endsWith("\n")) { nextCommand.setCommand(nextCommand.getCommandString().trim()); } return nextCommand; } return null; } /** * Streams anything in the command buffer to the comm port. * Synchronized to prevent commands from sending out of order. */ @Override synchronized public void streamCommands() { // If there are no commands to send, exit. if (this.getNextCommand() == null) { return; } // If streaming is paused, exit. if (this.sendPaused) { // Another NO-OP return; } // Send command if: // There is room in the buffer. // AND We are NOT in single step mode. // OR We are in single command mode and there are no active commands. while (this.getNextCommand() != null && CommUtils.checkRoomInBuffer( this.sentBufferSize, this.getNextCommand().getCommandString(), this.getBufferSize()) && allowMoreCommands()) { GcodeCommand command = this.getNextCommand(); if (command.getCommandString().isEmpty()) { dispatchListenerEvents(COMMAND_SKIPPED, command); nextCommand = null; continue; } String commandString = command.getCommandString().trim(); this.activeCommandList.add(command); this.sentBufferSize += (commandString.length() + 1); // Command already has a newline attached. this.sendMessageToConsoleListener(">>> " + commandString + "\n"); try { this.sendingCommand(commandString); conn.sendStringToComm(commandString + "\n"); dispatchListenerEvents(COMMAND_SENT, command); nextCommand = null; } catch (Exception e) { e.printStackTrace(); System.exit(-1); } } } @Override public void pauseSend() { this.sendPaused = true; } @Override public void resumeSend() { this.sendPaused = false; this.streamCommands(); } @Override public boolean isPaused() { return sendPaused; } @Override public void cancelSend() { this.nextCommand = null; this.commandBuffer.clear(); this.commandStream = null; this.sendPaused = false; } /** * This is to allow the GRBL Ctrl-C soft reset command. */ @Override public void softReset() { this.commandBuffer.clear(); this.activeCommandList.clear(); this.sentBufferSize = 0; } /** * Notifies the subclass that a command has been sent. * @param command The command being sent. */ abstract protected void sendingCommand(String command); /** * Returns whether or not a command has been completed based on a response * from the controller. * @param response * @return true if a command has completed. */ abstract protected boolean processedCommand(String response); /** * Returns whether or not a completed command had an error based on a * response from the controller. * @param response * @return true if a command has completed. */ abstract protected boolean processedCommandIsError(String response); /** * Processes message from GRBL. This should only be called from the * connection object. * @param response */ @Override public void responseMessage(String response) { // Send this information back up to the Controller. dispatchListenerEvents(SerialCommunicatorEvent.RAW_RESPONSE, response); // Pause if there is an error. // FIXME: This prevents more commands from being sent, but does not // issue a FEED HOLD event. The GUI will appear to hang if the // controller does not detect the error and update the GUI. // // This is handled in two ways: // 1) In GUIBackend.java if we were streaming a feed hold command // is sent to pause whatever command is currently running. // 2) In AbstractController.java commandComplete we reset // sendPaused if this command wasn't part of a file stream. if (processedCommandIsError(response)) { this.sendPaused = true; } // Keep the data flow going in case of an "ok". if (processedCommand(response)) { // Pop the front of the active list. if (this.activeCommandList != null && this.activeCommandList.size() > 0) { GcodeCommand command = this.activeCommandList.pop(); this.sentBufferSize -= (command.getCommandString().length() + 1); if (this.sendPaused == false) { this.streamCommands(); } } } } @Override public boolean openCommPort(String name, int baud) throws Exception { boolean ret = super.openCommPort(name, baud); if (ret) { this.commandBuffer = new LinkedBlockingDeque<>(); this.activeCommandList = new LinkedBlockingDeque<>(); this.sentBufferSize = 0; } return ret; } @Override public void closeCommPort() throws Exception { this.cancelSend(); super.closeCommPort(); this.sendPaused = false; this.commandBuffer = null; this.activeCommandList = null; } @Override public void sendByteImmediately(byte b) throws Exception { conn.sendByteImmediately(b); } }