/*
* Abstract 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 static com.willwinder.universalgcodesender.Utils.formatter;
import com.willwinder.universalgcodesender.gcode.GcodeCommandCreator;
import com.willwinder.universalgcodesender.gcode.GcodeUtils;
import com.willwinder.universalgcodesender.i18n.Localization;
import com.willwinder.universalgcodesender.listeners.ControllerListener;
import com.willwinder.universalgcodesender.listeners.ControllerListener.MessageType;
import com.willwinder.universalgcodesender.listeners.ControllerStatus;
import com.willwinder.universalgcodesender.listeners.SerialCommunicatorListener;
import com.willwinder.universalgcodesender.model.Position;
import com.willwinder.universalgcodesender.model.UGSEvent.ControlState;
import static com.willwinder.universalgcodesender.model.UGSEvent.ControlState.COMM_DISCONNECTED;
import static com.willwinder.universalgcodesender.model.UGSEvent.ControlState.COMM_IDLE;
import com.willwinder.universalgcodesender.model.UnitUtils;
import static com.willwinder.universalgcodesender.model.UnitUtils.Units.MM;
import static com.willwinder.universalgcodesender.model.UnitUtils.scaleUnits;
import com.willwinder.universalgcodesender.types.GcodeCommand;
import com.willwinder.universalgcodesender.utils.GcodeStreamReader;
import java.io.*;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
* @author wwinder
*/
public abstract class AbstractController implements SerialCommunicatorListener, IController {;
private static final Logger logger = Logger.getLogger(AbstractController.class.getName());
public class UnexpectedCommand extends Exception {
public UnexpectedCommand(String message) {
super(message);
}
}
/** API Interface. */
/**
* Called to ask controller if it is idle.
*/
protected abstract Boolean isIdleEvent();
/**
* Called before and after comm shutdown allowing device specific behavior.
*/
abstract protected void closeCommBeforeEvent();
abstract protected void closeCommAfterEvent();
/**
* Called after comm opening allowing device specific behavior.
* @throws IOException
*/
protected void openCommAfterEvent() throws Exception {
// Empty default implementation.
}
/**
* Called before and after a send cancel allowing device specific behavior.
*/
abstract protected void cancelSendBeforeEvent() throws Exception;
abstract protected void cancelSendAfterEvent() throws Exception;
/**
* Called before the comm is paused and before it is resumed.
*/
abstract protected void pauseStreamingEvent() throws Exception;
abstract protected void resumeStreamingEvent() throws Exception;
/**
* Called prior to sending commands, throw an exception if not ready.
*/
abstract protected void isReadyToSendCommandsEvent() throws Exception;
/**
* Called prior to streaming commands, separate in case you need to be more
* restrictive about streaming a file vs. sending a command.
* throws an exception if not ready.
*/
abstract protected void isReadyToStreamCommandsEvent() throws Exception;
/**
* Raw responses from the serial communicator.
*/
abstract protected void rawResponseHandler(String response);
/**
* Performs homing cycle, throw an exception if not supported.
*/
@Override
public void performHomingCycle() throws Exception {
throw new Exception(Localization.getString("controller.exception.homing"));
}
/**
* Returns machine to home location, throw an exception if not supported.
*/
@Override
public void returnToHome() throws Exception {
throw new Exception(Localization.getString("controller.exception.gohome"));
}
/**
* Reset machine coordinates to zero at the current location.
*/
@Override
public void resetCoordinatesToZero() throws Exception {
throw new Exception(Localization.getString("controller.exception.reset"));
}
/**
* Reset given machine coordinate to zero at the current location.
*/
@Override
public void resetCoordinateToZero(final char coord) throws Exception {
throw new Exception(Localization.getString("controller.exception.reset"));
}
/**
* Disable alarm mode and put device into idle state, throw an exception
* if not supported.
*/
@Override
public void killAlarmLock() throws Exception {
throw new Exception(Localization.getString("controller.exception.killalarm"));
}
/**
* Toggles check mode on or off, throw an exception if not supported.
*/
@Override
public void toggleCheckMode() throws Exception {
throw new Exception(Localization.getString("controller.exception.checkmode"));
}
/**
* Request parser state, either print it here or expect it in the response
* handler. Throw an exception if not supported.
*/
@Override
public void viewParserState() throws Exception {
throw new Exception(Localization.getString("controller.exception.parserstate"));
}
/**
* Execute a soft reset, throw an exception if not supported.
*/
@Override
public void issueSoftReset() throws Exception {
flushSendQueues();
softReset();
}
protected void softReset() throws Exception {
throw new Exception(Localization.getString("controller.exception.softreset"));
}
@Override
public void jogMachine(int dirX, int dirY, int dirZ, double stepSize,
double feedRate, UnitUtils.Units units) throws Exception {
logger.log(Level.INFO, "Adjusting manual location.");
// Format step size from spinner.
String formattedStepSize = Utils.formatter.format(stepSize);
String formattedFeedRate = Utils.formatter.format(feedRate);
String commandString = GcodeUtils.generateXYZ("G91G0", units,
formattedStepSize, formattedFeedRate, dirX, dirY, dirZ);
GcodeCommand command = createCommand(commandString);
command.setTemporaryParserModalChange(true);
sendCommandImmediately(command);
restoreParserModalState();
}
@Override
public void probe(String axis, double feedRate, double distance, UnitUtils.Units units) throws Exception {
logger.log(Level.INFO, "Probing.");
String probePattern = "G38.2 %s%s F%s";
double unitScale = scaleUnits(units, MM);
String probeCommand = String.format(probePattern, axis,
formatter.format(distance * unitScale),
formatter.format(feedRate * unitScale));
GcodeCommand state = createCommand("G21 G91 G49");
state.setTemporaryParserModalChange(true);
GcodeCommand probe = createCommand(probeCommand);
probe.setTemporaryParserModalChange(true);
this.sendCommandImmediately(state);
this.sendCommandImmediately(probe);
restoreParserModalState();
}
@Override
public void offsetTool(String axis, double offset, UnitUtils.Units units) throws Exception {
logger.log(Level.INFO, "Probe offset.");
String offsetPattern = "G43.1 %s%s";
String offsetCommand = String.format(offsetPattern,
axis,
formatter.format(offset * scaleUnits(units, MM)));
GcodeCommand state = createCommand("G21 G90");
state.setTemporaryParserModalChange(true);
this.sendCommandImmediately(state);
this.sendCommandImmediately(createCommand(offsetCommand));
restoreParserModalState();
}
/**
* Listener event for status update values;
*/
abstract protected void statusUpdatesEnabledValueChanged(boolean enabled);
abstract protected void statusUpdatesRateValueChanged(int rate);
// These abstract objects are initialized in concrete class.
protected final AbstractCommunicator comm;
protected GcodeCommandCreator commandCreator;
/**
* Accessible so that it can be configured.
* @return
*/
public GcodeCommandCreator getCommandCreator() {
return commandCreator;
}
// Outside influence
private boolean statusUpdatesEnabled = true;
private int statusUpdateRate = 200;
private UnitUtils.Units reportingUnits = UnitUtils.Units.UNKNOWN;
// Added value
private Boolean isStreaming = false;
private Boolean paused = false;
private long streamStart = 0;
private long streamStop = 0;
private File gcodeFile;
// This metadata needs to be cached instead of looked up from queues and
// streams, because those sources may be compromised during a cancel.
private int numCommands = 0;
private int numCommandsSent = 0;
private int numCommandsSkipped = 0;
private int numCommandsCompleted = 0;
// Commands become active after the Communicator notifies us that they have
// been sent.
//
// Algorithm:
// 1) Send all manually queued commands to the Communicator.
// 2) Queue file stream(s).
// 3) As commands are sent by the Communicator create a GCodeCommand
// (with command number) object and add it to the activeCommands list.
// 4) As commands are completed remove them from the activeCommand list.
private ArrayList<GcodeCommand> queuedCommands; // The list of specially queued commands to be sent.
private ArrayList<GcodeCommand> activeCommands; // The list of active commands.
private Reader rawStreamCommands; // A stream of commands from a newline separated gcode file.
private GcodeStreamReader streamCommands; // The stream of commands to send.
private int errorCount; // Number of 'error' responses.
// Listeners
private ArrayList<ControllerListener> listeners;
//Track current mode to restore after jogging
private String distanceModeCode = null;
private String unitsCode = null;
protected ControlState currentState = COMM_DISCONNECTED;
/**
* Dependency injection constructor to allow a mock communicator.
*/
protected AbstractController(AbstractCommunicator comm) {
this.comm = comm;
this.comm.setListenAll(this);
activeCommands = new ArrayList<>();
queuedCommands = new ArrayList<>();
this.listeners = new ArrayList<>();
}
@Deprecated
public AbstractController() {
this(new GrblCommunicator()); //f4grx: connection created at opencomm() time
}
@Override
public void setSingleStepMode(boolean enabled) {
if (this.comm != null) {
this.comm.setSingleStepMode(enabled);
}
}
@Override
public boolean getSingleStepMode() {
if (this.comm != null) {
return this.comm.getSingleStepMode();
}
return false;
}
@Override
public void setStatusUpdatesEnabled(boolean enabled) {
if (this.statusUpdatesEnabled != enabled) {
this.statusUpdatesEnabled = enabled;
statusUpdatesEnabledValueChanged(enabled);
}
}
@Override
public boolean getStatusUpdatesEnabled() {
return this.statusUpdatesEnabled;
}
@Override
public void setStatusUpdateRate(int rate) {
if (this.statusUpdateRate != rate) {
this.statusUpdateRate = rate;
statusUpdatesRateValueChanged(rate);
}
}
@Override
public int getStatusUpdateRate() {
return this.statusUpdateRate;
}
@Override
public Boolean openCommPort(String port, int portRate) throws Exception {
if (isCommOpen()) {
throw new Exception("Comm port is already open.");
}
// No point in checking response, it throws an exception on errors.
this.comm.openCommPort(port, portRate);
this.currentState = COMM_IDLE;
if (isCommOpen()) {
this.openCommAfterEvent();
this.messageForConsole(
"**** Connected to " + port + " @ " + portRate + " baud ****\n");
}
return isCommOpen();
}
@Override
public Boolean closeCommPort() throws Exception {
// Already closed.
if (isCommOpen() == false) {
return true;
}
this.closeCommBeforeEvent();
this.messageForConsole("**** Connection closed ****\n");
// I was noticing odd behavior, such as continuing to send 'ok's after
// closing and reopening the comm port.
// Note: The "Configuring-Grbl-v0.8" documentation recommends frequent
// soft resets, but also warns that the "startup" block will run
// on a reset and startup blocks may include motion commands.
//this.issueSoftReset();
this.flushSendQueues();
this.commandCreator.resetNum();
this.comm.closeCommPort();
this.closeCommAfterEvent();
return true;
}
@Override
public Boolean isCommOpen() {
return comm != null && comm.isCommOpen();
}
//// File send metadata ////
@Override
public Boolean isStreaming() {
return this.isStreaming;
}
/**
* Send duration can be one of 3 things:
* 1. the current running time of a send.
* 2. the entire duration of the most recent send.
* 3. 0 if there has never been a send.
*/
@Override
public long getSendDuration() {
// Last send duration.
if (this.isStreaming() == false) {
return this.streamStop - this.streamStart;
}
// No send duration data available.
else if (this.streamStart == 0L) {
return 0L;
}
// Current send duration.
else {
return System.currentTimeMillis() - this.streamStart;
}
}
private enum RowStat {
TOTAL_ROWS,
ROWS_SENT,
ROWS_REMAINING
}
/**
* Get one of the row statistics, returns -1 if stat is unavailable.
* @param stat
* @return
*/
public int getRowStat(RowStat stat) {
if (this.rawStreamCommands != null) {
return -1;
}
switch (stat) {
case TOTAL_ROWS:
return this.numCommands;
//return streamCommands.getNumRows();
case ROWS_SENT:
return this.numCommandsSent;
//return streamCommands.getNumRows() - streamCommands.getNumRowsRemaining();
case ROWS_REMAINING:
return this.numCommands - this.numCommandsCompleted - this.numCommandsSkipped;
//return streamCommands.getNumRowsRemaining();
default:
throw new IllegalStateException("This should be impossible - RowStat default case.");
}
}
@Override
public int rowsInSend() {
return getRowStat(RowStat.TOTAL_ROWS);
}
@Override
public int rowsSent() {
return getRowStat(RowStat.ROWS_SENT);
}
@Override
public int rowsRemaining() {
return getRowStat(RowStat.ROWS_REMAINING);
}
@Override
public GcodeCommand getActiveCommand() {
return activeCommands.get(0);
}
/**
* Creates a gcode command and queues it for send immediately.
* Note: this is the only place where a string is sent to the comm.
*/
@Override
public void sendCommandImmediately(GcodeCommand command) throws Exception {
isReadyToSendCommandsEvent();
if (!isCommOpen()) {
throw new Exception("Cannot send command(s), comm port is not open.");
}
this.dispatchStateChange(ControlState.COMM_SENDING);
this.sendStringToComm(command.getCommandString());
this.comm.streamCommands();
}
/**
* This is the only place where commands with an expected 'ok'/'error'
* response are sent to the comm - with the exception of command streams.
*/
private void sendStringToComm(String command) {
this.comm.queueStringForComm(command + "\n");
}
@Override
public Boolean isReadyToReceiveCommands() throws Exception {
if (!isCommOpen()) {
throw new Exception("Comm port is not open.");
}
if (this.isStreaming()) {
throw new Exception("Already streaming.");
}
return true;
}
@Override
public Boolean isReadyToStreamFile() throws Exception {
isReadyToStreamCommandsEvent();
isReadyToReceiveCommands();
if (this.comm.areActiveCommands()) {
throw new Exception("Cannot stream while there are active commands: "
+ comm.activeCommandSummary());
}
return true;
}
@Override
public void queueRawStream(Reader r) {
this.rawStreamCommands = r;
updateNumCommands();
}
@Override
public void queueStream(GcodeStreamReader r) {
this.streamCommands = r;
updateNumCommands();
}
@Override
public GcodeCommand createCommand(String gcode) throws Exception {
return this.commandCreator.createCommand(gcode);
}
@Override
public void queueCommand(GcodeCommand command) throws Exception {
this.queuedCommands.add(command);
updateNumCommands();
}
/**
* Send all queued commands to comm port.
* @throws java.lang.Exception
*/
@Override
public void beginStreaming() throws Exception {
this.isReadyToStreamFile();
// Throw if there's nothing queued.
if (this.queuedCommands.size() == 0 &&
this.rawStreamCommands == null &&
this.streamCommands == null) {
throw new Exception("There are no commands queued for streaming.");
}
// Grbl's "Configuring-Grbl-v0.8" documentation recommends a soft reset
// prior to starting a job. But will this cause GRBL to reset all the
// way to reporting version info? Need to double check that before
// enabling.
//this.issueSoftReset();
this.isStreaming = true;
this.streamStop = 0;
this.streamStart = System.currentTimeMillis();
this.numCommandsSent = 0;
this.numCommandsSkipped = 0;
this.numCommandsCompleted = 0;
updateNumCommands();
// Send all queued commands and streams then kick off the stream.
try {
while (this.queuedCommands.size() > 0) {
this.sendStringToComm(this.queuedCommands.remove(0).getCommandString());
}
if (this.rawStreamCommands != null) {
comm.queueRawStreamForComm(this.rawStreamCommands);
}
if (this.streamCommands != null) {
comm.queueStreamForComm(this.streamCommands);
}
comm.streamCommands();
} catch(Exception e) {
this.isStreaming = false;
this.streamStart = 0;
this.comm.cancelSend();
throw e;
}
}
@Override
public void pauseStreaming() throws Exception {
this.messageForConsole("\n**** Pausing file transfer. ****\n\n");
pauseStreamingEvent();
this.paused = true;
this.comm.pauseSend();
}
@Override
public void resumeStreaming() throws Exception {
this.messageForConsole("\n**** Resuming file transfer. ****\n\n");
resumeStreamingEvent();
this.paused = false;
this.comm.resumeSend();
}
@Override
public Boolean isPaused() {
return paused;
}
@Override
public Boolean isIdle() {
try {
return !isPaused() && !isStreaming && isIdleEvent();
} catch (Exception e) {
return false;
}
}
@Override
public void cancelSend() throws Exception {
this.messageForConsole("\n**** Canceling file transfer. ****\n\n");
cancelSendBeforeEvent();
// Don't clear the command queue, there might be a situation where a
// send is in progress while the next queue is being built. In which
// case a cancel would only be expected to cancel the current action
// to make way for the queued commands.
//this.prepQueue.clear();
//flushSendQueues();
flushQueuedCommands();
this.comm.cancelSend();
// If there are no active commands, done streaming. Otherwise wait for
// them to finish.
if (!comm.areActiveCommands()) {
this.isStreaming = false;
}
cancelSendAfterEvent();
}
@Override
public void resetBuffers() {
this.activeCommands.clear();
this.comm.resetBuffers();
paused = false;
}
private synchronized void flushQueuedCommands() {
// TODO: Special handling for stream necessary?
this.queuedCommands.clear();
}
// Reset send queue and idx's.
private void flushSendQueues() {
this.errorCount = 0;
this.numCommands -= this.getRowStat(RowStat.ROWS_REMAINING);
}
private void updateNumCommands() {
numCommands = queuedCommands.size();
if (streamCommands != null) {
numCommands += streamCommands.getNumRows();
}
}
// No longer a listener event
protected void fileStreamComplete(String filename, boolean success) {
String duration =
com.willwinder.universalgcodesender.Utils.
formattedMillis(this.getSendDuration());
this.messageForConsole("\n**** Finished sending file in "+duration+" ****\n\n");
this.streamStop = System.currentTimeMillis();
this.isStreaming = false;
this.flushSendQueues();
dispatchStreamComplete(filename, success);
}
@Override
public void commandSent(GcodeCommand command) {
if (this.isStreaming()) {
this.numCommandsSent++;
}
command.setSent(true);
this.activeCommands.add(command);
if (command.hasComment()) {
dispatchCommandCommment(command.getComment());
}
dispatchCommandSent(command);
}
public void checkStreamFinished() {
if (this.isStreaming() && !this.comm.areActiveCommands() && (this.activeCommands.size() == 0)) {
String streamName = "queued commands";
if (this.gcodeFile != null) {
streamName = this.gcodeFile.getName();
}
boolean isSuccess = (this.errorCount == 0);
this.fileStreamComplete(streamName, isSuccess);
}
}
@Override
public void commandSkipped(GcodeCommand command) {
if (this.isStreaming()) {
this.numCommandsSkipped++;
}
this.messageForConsole("Skipping command #" + command.getCommandNumber() + "\n");
command.setResponse("<skipped by application>");
command.setSkipped(true);
dispatchCommandSkipped(command);
if (command.hasComment()) {
dispatchCommandCommment(command.getComment());
}
checkStreamFinished();
}
/**
* Notify controller that the next command has completed with response and
* that the stream is complete once the last command has finished.
*/
public void commandComplete(String response) throws UnexpectedCommand {
// Auto-paused while we weren't streaming, resume the comm.
if (!isStreaming() && !isPaused() && comm.isPaused()) {
comm.resumeSend();
}
if (this.activeCommands.isEmpty()) {
throw new UnexpectedCommand(
Localization.getString("controller.exception.unexpectedCommand"));
}
GcodeCommand command = this.activeCommands.remove(0);
command.setResponse(response);
//updateParserModalState(command);
this.numCommandsCompleted++;
dispatchCommandComplete(command);
checkStreamFinished();
}
@Override
public void messageForConsole(String msg) {
dispatchConsoleMessage(MessageType.INFO, msg);
}
@Override
public void verboseMessageForConsole(String msg) {
dispatchConsoleMessage(MessageType.VERBOSE, msg);
}
@Override
public void errorMessageForConsole(String msg) {
dispatchConsoleMessage(MessageType.ERROR, msg);
}
@Override
public void rawResponseListener(String response) {
rawResponseHandler(response);
}
/**
* Listener management.
*/
@Override
public void addListener(ControllerListener cl) {
this.listeners.add(cl);
}
protected void dispatchStatusString(ControllerStatus status) {
if (listeners != null) {
for (ControllerListener c : listeners) {
c.statusStringListener(status);
}
}
}
protected void dispatchConsoleMessage(MessageType type, String message) {
if (listeners != null) {
for (ControllerListener c : listeners) {
c.messageForConsole(type, message);
}
}
}
protected void dispatchStateChange(ControlState state) {
currentState = state;
if (listeners != null) {
for (ControllerListener c : listeners) {
c.controlStateChange(state);
}
}
}
protected void dispatchStreamComplete(String filename, Boolean success) {
if (listeners != null) {
for (ControllerListener c : listeners) {
c.fileStreamComplete(filename, success);
}
}
}
protected void dispatchCommandSkipped(GcodeCommand command) {
if (listeners != null) {
for (ControllerListener c : listeners) {
c.commandSkipped(command);
}
}
}
protected void dispatchCommandSent(GcodeCommand command) {
if (listeners != null) {
for (ControllerListener c : listeners) {
c.commandSent(command);
}
}
}
protected void dispatchCommandComplete(GcodeCommand command) {
if (listeners != null) {
for (ControllerListener c : listeners) {
c.commandComplete(command);
}
}
}
protected void dispatchCommandCommment(String comment) {
if (listeners != null) {
for (ControllerListener c : listeners) {
c.commandComment(comment);
}
}
}
protected void dispatchPostProcessData(int numRows) {
if (listeners != null) {
for (ControllerListener c : listeners) {
c.postProcessData(numRows);
}
}
}
protected void dispatchProbeCoordinates(Position p) {
if (listeners != null) {
for (ControllerListener c : listeners) {
c.probeCoordinates(p);
}
}
}
/**
* Get current machine reporting units
*/
protected UnitUtils.Units getReportingUnits() {
return reportingUnits;
}
/**
* Set current machine reporting units
* @param units
*/
protected void setReportingUnits(UnitUtils.Units units) {
this.reportingUnits = units;
}
protected String getUnitsCode() {
return unitsCode;
}
protected void setUnitsCode(String unitsCode) {
if (unitsCode != null) {
this.unitsCode = unitsCode;
}
}
protected String getDistanceModeCode() {
return distanceModeCode;
}
protected void setDistanceModeCode(String distanceModeCode) {
if (distanceModeCode != null) {
this.distanceModeCode = distanceModeCode;
}
}
@Override
public void updateParserModalState(GcodeCommand command) {
if (command.isTemporaryParserModalChange()) {
return;
}
String gcode = command.getCommandString().toUpperCase();
if (gcode.contains("G90")) {
distanceModeCode = "G90";
}
if (gcode.contains("G91")) {
distanceModeCode = "G91";
}
if (gcode.contains("G20")) {
unitsCode = "G20";
}
if (gcode.contains("G21")) {
unitsCode = "G21";
}
}
@Override
public void restoreParserModalState() {
StringBuilder cmd = new StringBuilder();
if (getDistanceModeCode() != null) {
cmd.append(getDistanceModeCode()).append(" ");
}
if (getUnitsCode() != null) {
cmd.append(getUnitsCode()).append(" ");
}
try {
GcodeCommand command = createCommand(cmd.toString());
command.setTemporaryParserModalChange(true);
sendCommandImmediately(command);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}