/* Copywrite 2015-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.model; import com.google.common.io.Files; import com.willwinder.universalgcodesender.AbstractController; import com.willwinder.universalgcodesender.listeners.ControllerListener; import com.willwinder.universalgcodesender.IController; import com.willwinder.universalgcodesender.utils.*; import com.willwinder.universalgcodesender.Utils; import com.willwinder.universalgcodesender.gcode.GcodeParser; import com.willwinder.universalgcodesender.gcode.GcodePreprocessorUtils; import com.willwinder.universalgcodesender.gcode.processors.CommandLengthProcessor; import com.willwinder.universalgcodesender.gcode.processors.CommandSplitter; import com.willwinder.universalgcodesender.gcode.processors.CommentProcessor; import com.willwinder.universalgcodesender.gcode.processors.DecimalProcessor; import com.willwinder.universalgcodesender.gcode.processors.ICommandProcessor; import com.willwinder.universalgcodesender.gcode.processors.M30Processor; import com.willwinder.universalgcodesender.gcode.processors.WhitespaceProcessor; import com.willwinder.universalgcodesender.model.UnitUtils.Units; import com.willwinder.universalgcodesender.i18n.Localization; import com.willwinder.universalgcodesender.listeners.ControllerStatus; import com.willwinder.universalgcodesender.model.UGSEvent.ControlState; import com.willwinder.universalgcodesender.model.UGSEvent.FileState; import com.willwinder.universalgcodesender.pendantui.SystemStateBean; import com.willwinder.universalgcodesender.types.GcodeCommand; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.nio.charset.Charset; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; import com.willwinder.universalgcodesender.listeners.UGSEventListener; import com.willwinder.universalgcodesender.model.UGSEvent.EventType; import java.awt.AWTException; import java.awt.MouseInfo; import java.awt.Point; import java.awt.Robot; import java.util.regex.Matcher; import javax.swing.JFrame; import javax.swing.JOptionPane; /** * * @author wwinder */ public class GUIBackend implements BackendAPI, ControllerListener, SettingChangeListener { private static final Logger logger = Logger.getLogger(GUIBackend.class.getName()); private static final String NEW_LINE = "\n "; private static final int AUTO_DISCONNECT_THRESHOLD = 5000; private AbstractController controller = null; private Settings settings = null; private Position machineCoord = null; private Position workCoord = null; private Units reportUnits = Units.UNKNOWN; private String state; private final Collection<ControllerListener> controllerListeners = new ArrayList<>(); private final Collection<UGSEventListener> controlStateListeners = new ArrayList<>(); // GUI State private File gcodeFile = null; private File processedGcodeFile = null; private File tempDir = null; private String lastComment; private String activeState; private ControlState controlState = ControlState.COMM_DISCONNECTED; private long estimatedSendDuration = -1L; private boolean sendingFile = false; //private long estimatedSendTimeRemaining = 0; //private long rowsInFile = 0; private String openCloseButtonText; private boolean openCloseButtonEnabled; private String pauseButtonText; private String cancelButtonText; private String firmware = null; private boolean reprocessFileAfterStreamComplete = false; private long lastResponse = Long.MIN_VALUE; private long lastConnectAttempt = Long.MIN_VALUE; private boolean streamFailed = false; private boolean autoconnect = false; private final java.util.Timer autoConnectTimer = new Timer("AutoConnectTimer", true); public GcodeParser gcp = new GcodeParser(); public GUIBackend() { scheduleTimers(); } protected final void scheduleTimers() { autoConnectTimer.scheduleAtFixedRate(new TimerTask() { private int count = 0; @Override public void run() { autoconnect(); // Move the mouse every 30 seconds to prevent sleeping. if (isPaused() || isActive()) { count++; if (count % 10 == 0) { keepAwake(); count = 0; } } } }, 1000, 1000); } @Override public void addUGSEventListener(UGSEventListener listener) { logger.log(Level.INFO, "Adding control state listener."); controlStateListeners.add(listener); } @Override public void addControllerListener(ControllerListener listener) { logger.log(Level.INFO, "Adding controller listener."); controllerListeners.add(listener); if (this.controller != null) { this.controller.addListener(listener); } } ////////////////// // GUI API ////////////////// @Override public void preprocessAndExportToFile(File f) throws Exception { gcp.reset(); try(BufferedReader br = new BufferedReader(new FileReader(this.getGcodeFile()))) { try (GcodeStreamWriter gsw = new GcodeStreamWriter(f)) { int i = 0; for(String line; (line = br.readLine()) != null; ) { i++; if (i % 1000000 == 0) { logger.log(Level.FINE, "i: " + i); } String comment = GcodePreprocessorUtils.parseComment(line); // Parse the gcode for the buffer. Collection<String> lines = gcp.preprocessCommand(line); // If it is a comment-only line, add the comment, if (!comment.isEmpty() && lines.isEmpty()) { gsw.addLine(line, "", comment, i); } // Otherwise add each processed line (often just one line). else { for(String processedLine : lines) { gsw.addLine(line, processedLine, comment, i); gcp.addCommand(processedLine); } } } } } } private void initGcodeParser() { // Configure gcode parser. gcp.resetCommandProcessors(); List<ICommandProcessor> processors = FirmwareUtils.getParserFor(firmware, settings).orElse(null); if (processors != null) { for (ICommandProcessor p : processors) { gcp.addCommandProcessor(p); } } else { initializeWithFallbackProcessors(gcp); } } private void updateWithFirmware(String firmware) throws Exception { this.firmware = firmware; // Load command processors for this firmware. Optional<List<ICommandProcessor>> processor_ret = FirmwareUtils.getParserFor(firmware, settings); if (!processor_ret.isPresent()) { disconnect(); throw new Exception("Bad configuration file for: " + firmware); } // Reload gcode file to use the controllers processors. if (this.gcodeFile != null) { setGcodeFile(this.gcodeFile); } } @Override public void connect(String firmware, String port, int baudRate) throws Exception { logger.log(Level.INFO, "Connecting to {0} on port {1}", new Object[]{firmware, port}); lastConnectAttempt = System.currentTimeMillis(); updateWithFirmware(firmware); Optional<AbstractController> c = FirmwareUtils.getControllerFor(firmware); if (!c.isPresent()) { throw new Exception("Unable to create handler for: " + firmware); } this.controller = c.get(); applySettings(settings); this.controller.addListener(this); for (ControllerListener l : controllerListeners) { this.controller.addListener(l); } if (openCommConnection(port, baudRate)) { this.sendControlStateEvent(new UGSEvent(ControlState.COMM_IDLE), false); streamFailed = false; //reset } } @Override public boolean isConnected() { boolean isConnected = this.controlState != ControlState.COMM_DISCONNECTED; logger.log(Level.FINEST, "Is connected: {0}", isConnected); return isConnected; } @Override public void disconnect() throws Exception { autoconnect = false; disconnectInternal(); } private void disconnectInternal() throws Exception { logger.log(Level.INFO, "Disconnecting."); if (this.controller != null) { this.controller.closeCommPort(); this.controller = null; this.sendControlStateEvent(new UGSEvent(ControlState.COMM_DISCONNECTED), false); } } public void autoconnect() { if (!autoconnect) { return; } // This breaks when a machine is homing. GRBL at least will stop sending // status during a homing operation. /* // Check if a timeout has occurred. if (controller.getStatusUpdatesEnabled() && settings.isAutoReconnect()) { long now = System.currentTimeMillis(); if (now - lastResponse > AUTO_DISCONNECT_THRESHOLD && now - lastConnectAttempt > AUTO_DISCONNECT_THRESHOLD ) { logger.log(Level.INFO, "No Response in " + (now - lastResponse)+"ms."); if (controller != null && controller.isStreamingFile()) { streamFailed = true; } try { disconnectInternal(); } catch (Exception e) { logger.log(Level.INFO, "Disconnect failed ", e); } } } */ if (!isConnected()) { if (settings == null || streamFailed) { return; } if (lastResponse == Long.MIN_VALUE && autoconnect) { logger.log(Level.INFO, "Attempting auto connect."); } else if (lastResponse > Long.MIN_VALUE && settings.isAutoReconnect()) { logger.log(Level.INFO, "Attempting auto reconnect."); } else { return; } try { String[] portList = CommUtils.getSerialPortList(); boolean portMatch = false; for (String port : portList) { if (port.equals(settings.getPort())) { portMatch = true; break; } } if (portMatch) { connect(settings.getFirmwareVersion(), settings.getPort(), Integer.parseInt(settings.getPortRate())); } } catch (Exception e) { logger.log(Level.WARNING, "Auto connect failed",e); } } } public void keepAwake() { logger.log(Level.INFO, "Moving the mouse location slightly to keep the computer awake."); try { Robot hal = new Robot(); Point pObj = MouseInfo.getPointerInfo().getLocation(); hal.mouseMove(pObj.x + 1, pObj.y + 1); hal.mouseMove(pObj.x - 1, pObj.y - 1); pObj = MouseInfo.getPointerInfo().getLocation(); System.out.println(pObj.toString() + "x>>" + pObj.x + " y>>" + pObj.y); } catch (AWTException | NullPointerException ex) { Logger.getLogger(GUIBackend.class.getName()).log(Level.SEVERE, null, ex); } } @Override public void applySettings(Settings settings) throws Exception { logger.log(Level.INFO, "Applying settings."); this.settings = settings; this.settings.setSettingChangeListener(this); if (this.controller != null) { applySettingsToController(this.settings, this.controller); } // Reload gcode file to use the controllers processors. if (this.gcodeFile != null) { setGcodeFile(this.gcodeFile); } } /** * This allows us to visualize a file without loading a controller profile. */ private static void initializeWithFallbackProcessors(GcodeParser parser) { parser.addCommandProcessor(new WhitespaceProcessor()); parser.addCommandProcessor(new CommentProcessor()); parser.addCommandProcessor(new M30Processor()); parser.addCommandProcessor(new CommandSplitter()); parser.addCommandProcessor(new DecimalProcessor(4)); parser.addCommandProcessor(new CommandLengthProcessor(50)); } @Override public void updateSystemState(SystemStateBean systemStateBean) { logger.log(Level.FINE, "Getting system state 'updateSystemState'"); if (gcodeFile != null) systemStateBean.setFileName(gcodeFile.getAbsolutePath()); systemStateBean.setLatestComment(lastComment); systemStateBean.setActiveState(activeState); systemStateBean.setControlState(controlState); if (this.machineCoord != null) { systemStateBean.setMachineX(Utils.formatter.format(this.machineCoord.getX())); systemStateBean.setMachineY(Utils.formatter.format(this.machineCoord.getY())); systemStateBean.setMachineZ(Utils.formatter.format(this.machineCoord.getZ())); } if (this.controller != null) { systemStateBean.setRemainingRows(String.valueOf(this.getNumRemainingRows())); systemStateBean.setRowsInFile(String.valueOf(this.getNumRows())); systemStateBean.setSentRows(String.valueOf(this.getNumSentRows())); systemStateBean.setDuration(String.valueOf(this.getSendDuration())); systemStateBean.setEstimatedTimeRemaining(String.valueOf(this.getSendRemainingDuration())); } if (this.workCoord != null) { systemStateBean.setWorkX(Utils.formatter.format(this.workCoord.getX())); systemStateBean.setWorkY(Utils.formatter.format(this.workCoord.getY())); systemStateBean.setWorkZ(Utils.formatter.format(this.workCoord.getZ())); } systemStateBean.setSendButtonText(openCloseButtonText); systemStateBean.setSendButtonEnabled(openCloseButtonEnabled); systemStateBean.setPauseResumeButtonText(pauseButtonText); systemStateBean.setPauseResumeButtonEnabled(this.canPause()); systemStateBean.setCancelButtonText(cancelButtonText); systemStateBean.setCancelButtonEnabled(this.canCancel()); } @Override public void sendGcodeCommand(String commandText) throws Exception { sendGcodeCommand(false, commandText); } @Override public void sendGcodeCommand(boolean restoreParserState, String commandText) throws Exception { if (this.isConnected()) { GcodeCommand command = controller.createCommand(commandText); command.setTemporaryParserModalChange(restoreParserState); sendGcodeCommand(command); if (restoreParserState && this.isConnected()) { controller.restoreParserModalState(); } } else { throw new Exception(Localization.getString("controller.log.notconnected")); } } @Override public void sendGcodeCommand(GcodeCommand command) throws Exception { if (this.isConnected()) { logger.log(Level.INFO, "Sending gcode command: {0}", command.getCommandString()); this.sendControlStateEvent(new UGSEvent(ControlState.COMM_SENDING), false); controller.sendCommandImmediately(command); } } /** * Sends a G91 command in some combination of x, y, and z directions with a * step size of stepDirection. * * Direction is specified by the direction param being positive or negative. */ @Override public void adjustManualLocation(int dirX, int dirY, int dirZ, double stepSize, double feedRate, Units units) throws Exception { // Don't send empty commands. if ((dirX == 0) && (dirY == 0) && (dirZ == 0)) { return; } controller.jogMachine(dirX, dirY, dirZ, stepSize, feedRate, units); } @Override public void probe(String axis, double feedRate, double distance, UnitUtils.Units units) throws Exception { controller.probe(axis, feedRate, distance, units); } @Override public void offsetTool(String axis, double offset, UnitUtils.Units units) throws Exception { controller.offsetTool(axis, offset, units); } @Override public Settings getSettings() { logger.log(Level.FINEST, "Getting settings."); return this.settings; } @Override public ControlState getControlState() { logger.log(Level.FINEST, "Getting control state."); return this.controlState; } @Override public IController getController() { logger.log(Level.FINEST, "Getting controller"); return this.controller; } @Override public void setTempDir(File file) throws IOException { if (file.isDirectory()) this.tempDir = file; else throw new IOException("Temp dir " + file.toString() + " is not a directory."); } private File getTempDir() { if (tempDir == null) { tempDir = Files.createTempDir(); } return tempDir; } @Override public void setGcodeFile(File file) throws Exception { logger.log(Level.INFO, "Setting gcode file."); initGcodeParser(); this.gcodeFile = file; this.processedGcodeFile = null; this.sendControlStateEvent(new UGSEvent(FileState.FILE_LOADING, file.getAbsolutePath()), false); initializeProcessedLines(true); this.sendControlStateEvent(new UGSEvent(FileState.FILE_LOADED, processedGcodeFile.getAbsolutePath()), false); } @Override public File getGcodeFile() { logger.log(Level.INFO, "Getting gcode file."); return this.gcodeFile; } @Override public File getProcessedGcodeFile() { logger.log(Level.INFO, "Getting processed gcode file."); return this.processedGcodeFile; } @Override public void send() throws Exception { logger.log(Level.INFO, "Sending gcode file."); // Note: there is a divide by zero error in the timer because it uses // the rowsValueLabel that was just reset. try { this.sendingFile = true; // This will throw an exception and prevent that other stuff from // happening (clearing the table before its ready for clearing. this.controller.isReadyToStreamFile(); this.sendControlStateEvent(new UGSEvent(ControlState.COMM_SENDING), false); //this.controller.queueCommands(processedCommandLines); //this.controller.queueStream(new BufferedReader(new FileReader(this.processedGcodeFile))); this.controller.queueStream(new GcodeStreamReader(this.processedGcodeFile)); this.controller.beginStreaming(); } catch (Exception e) { this.sendControlStateEvent(new UGSEvent(ControlState.COMM_IDLE), false); e.printStackTrace(); throw new Exception(Localization.getString("mainWindow.error.startingStream") + ": "+e.getMessage()); } } @Override public long getNumRows() { logger.log(Level.FINEST, "Getting number of rows."); return this.controller.rowsInSend(); } @Override public long getNumSentRows() { logger.log(Level.FINEST, "Getting number of sent rows."); return controller == null ? 0 : controller.rowsSent(); } @Override public long getNumRemainingRows() { return controller == null ? 0 : controller.rowsRemaining(); } @Override public long getSendDuration() { return controller == null ? 0 : controller.getSendDuration(); } @Override public long getSendRemainingDuration() { long sent = this.getNumSentRows(); // Early exit condition. Can't make an estimate if we haven't started. if (sent == 0) { return -1L; } long estimate = this.estimatedSendDuration; long elapsedTime = this.getSendDuration(); // If we don't have an actual duration estimate, make a crude estimate. if (estimate <= 0) { long timePerCode = elapsedTime / sent; estimate = timePerCode * this.getNumRows(); } return estimate - elapsedTime; } @Override public void pauseResume() throws Exception { logger.log(Level.INFO, "Pause/Resume"); try { switch(controlState) { case COMM_IDLE: default: if (!sendingFile) { throw new Exception("Cannot pause while '" + controlState + "'."); } // Fall through if we're really sending a file. // This can happen at the beginning of a stream when GRBL // reports an error before we send it a status request. case COMM_SENDING: this.controller.pauseStreaming(); this.sendControlStateEvent(new UGSEvent(ControlState.COMM_SENDING_PAUSED), false); return; case COMM_SENDING_PAUSED: this.controller.resumeStreaming(); this.sendControlStateEvent(new UGSEvent(ControlState.COMM_SENDING), false); return; } } catch (Exception e) { logger.log(Level.SEVERE, "Exception in pauseResume", e); throw new Exception(Localization.getString("mainWindow.error.pauseResume")); } } @Override public String getPauseResumeText() { if (isPaused()) return Localization.getString("mainWindow.ui.resumeButton"); else return Localization.getString("mainWindow.ui.pauseButton"); } @Override public boolean isActive() { return this.controller != null && !isIdle(); } @Override public boolean isSendingFile() { return sendingFile; } @Override public boolean isIdle() { return this.controller != null && controller.isIdle(); } @Override public boolean isPaused() { return this.controller != null && this.controller.isPaused(); } @Override public boolean canPause() { return this.controller != null && !isIdle() && !isPaused(); } @Override public boolean canCancel() { return canPause() || isPaused(); } @Override public boolean canSend() { return isIdle() && (this.gcodeFile != null); } @Override public void cancel() throws Exception { if (this.canCancel()) { this.controller.cancelSend(); this.sendControlStateEvent(new UGSEvent(ControlState.COMM_IDLE), false); } } @Override public void returnToZero() throws Exception { this.controller.returnToHome(); } @Override public void resetCoordinatesToZero() throws Exception { this.controller.resetCoordinatesToZero(); } @Override public void restoreParserState() throws Exception { this.controller.restoreParserModalState(); } @Override public void resetCoordinateToZero(char coordinate) throws Exception { this.controller.resetCoordinateToZero(coordinate); } @Override public void killAlarmLock() throws Exception { this.controller.killAlarmLock(); } @Override public void performHomingCycle() throws Exception { this.controller.performHomingCycle(); } @Override public void toggleCheckMode() throws Exception { this.controller.toggleCheckMode(); } @Override public void issueSoftReset() throws Exception { this.controller.issueSoftReset(); } @Override public void requestParserState() throws Exception { this.controller.viewParserState(); } @Override public void performAction(ACTIONS action) throws Exception { switch (action) { case RETURN_TO_ZERO: returnToZero(); break; case RESET_COORDINATES_TO_ZERO: resetCoordinatesToZero(); break; case KILL_ALARM_LOCK: killAlarmLock(); break; case HOMING_CYCLE: performHomingCycle(); break; case TOGGLE_CHECK_MODE: toggleCheckMode(); break; case ISSUE_SOFT_RESET: issueSoftReset(); break; case REQUEST_PARSER_STATE: requestParserState(); break; default: break; } } ////////////////// // Controller Listener ////////////////// @Override public void controlStateChange(ControlState state) { // This comes from the boss, force the event change. this.sendControlStateEvent(new UGSEvent(state), true); } @Override public void fileStreamComplete(String filename, boolean success) { // If we were sending a file, we aren't anymore. this.sendingFile = false; this.sendControlStateEvent(new UGSEvent(ControlState.COMM_IDLE), false); // Reprocess file if a custom pattern remover was added while streaming. if (this.reprocessFileAfterStreamComplete) { try { updateWithFirmware(firmware); } catch (Exception ex) { Logger.getLogger(GUIBackend.class.getName()).log(Level.SEVERE, null, ex); } } } @Override public void commandSkipped(GcodeCommand command) { } @Override public void commandSent(GcodeCommand command) { } @Override public void commandComplete(GcodeCommand command) { if (isIdle()) { this.sendControlStateEvent(new UGSEvent(ControlState.COMM_IDLE), false); } if (command.isError()) { if (this.sendingFile && !this.isPaused()) { try { this.pauseResume(); } catch (Exception e) { GUIHelpers.displayErrorDialog(e.getLocalizedMessage()); } String error = String.format(Localization.getString("controller.exception.sendError"), command.getCommandString(), command.getResponse()).replaceAll("\\.\\.", "\\."); messageForConsole(MessageType.INFO, error); String checkboxQuestion = Localization.getString("controller.exception.ignoreFutureErrors"); Object[] params = {String.format(NarrowOptionPane.pattern, 300, error), checkboxQuestion}; int n = JOptionPane.showConfirmDialog(new JFrame(), params, Localization.getString("error"), JOptionPane.YES_NO_OPTION); if (n == JOptionPane.YES_OPTION) { try { FirmwareUtils.addPatternRemoverForFirmware(firmware, Matcher.quoteReplacement(command.getCommandString())); this.reprocessFileAfterStreamComplete = true; } catch (IOException ex) { GUIHelpers.displayErrorDialog(ex.getLocalizedMessage()); } } } } } @Override public void commandComment(String comment) { this.lastComment = comment; } @Override public void probeCoordinates(Position p) { this.sendControlStateEvent(new UGSEvent(p), false); } @Override public void messageForConsole(MessageType type, String msg) { if (type == MessageType.ERROR) { GUIHelpers.displayErrorDialog(msg); } } @Override public void statusStringListener(ControllerStatus status) { this.activeState = status.getState(); this.machineCoord = status.getMachineCoord(); this.workCoord = status.getWorkCoord(); this.reportUnits = machineCoord.getUnits(); this.lastResponse = System.currentTimeMillis(); } @Override public void postProcessData(int numRows) { } //////////////////// // Utility functions //////////////////// /** * This would be static but I want to define it in the interface. * @param settings Settings to apply to the controller. * @param controller Controller to receive settings. * @throws java.lang.Exception Exception thrown if controller doesn't support some settings. */ @Override public void applySettingsToController(Settings settings, IController controller) throws Exception { if (settings == null) { throw new Exception("Programmer error."); } autoconnect = settings.isAutoConnectEnabled(); // Apply settings settings to controller. try { controller.getCommandCreator(); controller.setSingleStepMode(settings.isSingleStepMode()); controller.setStatusUpdatesEnabled(settings.isStatusUpdatesEnabled()); controller.setStatusUpdateRate(settings.getStatusUpdateRate()); } catch (Exception ex) { StringBuilder message = new StringBuilder() .append(Localization.getString("mainWindow.error.firmwareSetting")) .append(": \n ") .append(Localization.getString("firmware.feature.maxCommandLength")).append(NEW_LINE) .append(Localization.getString("firmware.feature.truncateDecimal")).append(NEW_LINE) .append(Localization.getString("firmware.feature.singleStep")).append(NEW_LINE) .append(Localization.getString("firmware.feature.removeWhitespace")).append(NEW_LINE) .append(Localization.getString("firmware.feature.linesToArc")).append(NEW_LINE) .append(Localization.getString("firmware.feature.statusUpdates")).append(NEW_LINE) .append(Localization.getString("firmware.feature.statusUpdateRate")); throw new Exception(message.toString()); } } @Override public void sendMessageForConsole(String msg) { if (controller != null) { controller.messageForConsole(msg); } else { //should still send! Controller probably shouldn't ever be null. } } ///////////////////// // Private functions. ///////////////////// private boolean openCommConnection(String port, int baudRate) throws Exception { boolean connected = false; try { connected = controller.openCommPort(port, baudRate); this.initializeProcessedLines(false); } catch (Exception e) { logger.log(Level.INFO, "Exception in openCommConnection.", e); throw new Exception(Localization.getString("mainWindow.error.connection") + " ("+ e.getClass().getName() + "): "+e.getMessage()); } return connected; } private void initializeProcessedLines(boolean forceReprocess) throws FileNotFoundException, Exception { if (this.gcodeFile != null) { Charset cs; try (FileReader fr = new FileReader(this.gcodeFile)) { cs = Charset.forName(fr.getEncoding()); } logger.info("Start preprocessing"); long start = System.currentTimeMillis(); if (this.processedGcodeFile == null || forceReprocess) { this.processedGcodeFile = new File(this.getTempDir(), this.gcodeFile.getName()); this.preprocessAndExportToFile(this.processedGcodeFile); } long end = System.currentTimeMillis(); logger.info("Took " + (end - start) + "ms to preprocess"); if (this.isConnected()) { this.estimatedSendDuration = -1L; Thread estimateThread = new Thread(new Runnable() { @Override public void run() { estimatedSendDuration = controller.getJobLengthEstimate(processedGcodeFile); } }); estimateThread.start(); } } } private void sendControlStateEvent(UGSEvent event, boolean force) { logger.log(Level.FINE, "Sending control state event {0}.", event.evt); if (event.isStateChangeEvent()) { if (this.controller != null && this.controller.handlesAllStateChangeEvents() && !force){ return; } this.controlState = event.getControlState(); } for (UGSEventListener l : controlStateListeners) { l.UGSEvent(event); } } @Override public void sendOverrideCommand(Overrides override) throws Exception { this.controller.sendOverrideCommand(override); } @Override public void settingChanged() { this.sendControlStateEvent(new UGSEvent(EventType.SETTING_EVENT), false); } }