1326 lines
41 KiB
Python
1326 lines
41 KiB
Python
# coding=utf-8
|
|
from __future__ import absolute_import
|
|
__author__ = "Florian Becker <florian@mr-beam.org> based on work by Gina Häußge and David Braam"
|
|
__license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agpl.html"
|
|
__copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
|
|
|
|
import os
|
|
import threading
|
|
import logging
|
|
import glob
|
|
import time
|
|
import serial
|
|
import re
|
|
import Queue
|
|
|
|
from yaml import load as yamlload
|
|
from yaml import dump as yamldump
|
|
from subprocess import call as subprocesscall
|
|
|
|
import octoprint.plugin
|
|
|
|
from octoprint.settings import settings, default_settings
|
|
from octoprint.events import eventManager, Events
|
|
from octoprint.filemanager.destinations import FileDestinations
|
|
from octoprint.util import get_exception_string, RepeatedTimer, CountedEvent, sanitize_ascii
|
|
|
|
### MachineCom #########################################################################################################
|
|
class MachineCom(object):
|
|
STATE_NONE = 0
|
|
STATE_OPEN_SERIAL = 1
|
|
STATE_DETECT_SERIAL = 2
|
|
STATE_CONNECTING = 3
|
|
STATE_OPERATIONAL = 4
|
|
STATE_PRINTING = 5
|
|
STATE_PAUSED = 6
|
|
STATE_CLOSED = 7
|
|
STATE_ERROR = 8
|
|
STATE_CLOSED_WITH_ERROR = 9
|
|
STATE_LOCKED = 10
|
|
STATE_HOMING = 11
|
|
STATE_FLASHING = 12
|
|
|
|
def __init__(self, port=None, baudrate=None, callbackObject=None, printerProfileManager=None):
|
|
self._logger = logging.getLogger(__name__)
|
|
self._serialLogger = logging.getLogger("SERIAL")
|
|
|
|
if port is None:
|
|
port = settings().get(["serial", "port"])
|
|
elif isinstance(port, list):
|
|
port = port[0]
|
|
if baudrate is None:
|
|
settingsBaudrate = settings().getInt(["serial", "baudrate"])
|
|
if settingsBaudrate is None:
|
|
baudrate = 0
|
|
else:
|
|
baudrate = settingsBaudrate
|
|
if callbackObject is None:
|
|
callbackObject = MachineComPrintCallback()
|
|
|
|
self._port = port
|
|
self._baudrate = baudrate
|
|
self._callback = callbackObject
|
|
self._printerProfileManager = printerProfileManager
|
|
|
|
self.RX_BUFFER_SIZE = 127
|
|
|
|
self._state = self.STATE_NONE
|
|
self._grbl_state = None
|
|
self._errorValue = "Unknown Error"
|
|
self._serial = None
|
|
self._currentFile = None
|
|
self._status_timer = None
|
|
self._acc_line_buffer = []
|
|
self._cmd = None
|
|
self._pauseWaitStartTime = None
|
|
self._pauseWaitTimeLost = 0.0
|
|
self._commandQueue = Queue.Queue()
|
|
self._send_event = CountedEvent(max=50)
|
|
self._finished_currentFile = False
|
|
self._pause_delay_time = 0
|
|
self._feedrate_factor = 1
|
|
self._actual_feedrate = None
|
|
self._intensity_factor = 1
|
|
self._actual_intensity = None
|
|
self._feedrate_dict = {}
|
|
self._intensity_dict = {}
|
|
self._passes = 1
|
|
self._finished_passes = 0
|
|
|
|
# regular expressions
|
|
self._regex_command = re.compile("^\s*\$?([GM]\d+|[THX])")
|
|
self._regex_feedrate = re.compile("F\d+", re.IGNORECASE)
|
|
self._regex_intensity = re.compile("S\d+", re.IGNORECASE)
|
|
|
|
self._real_time_commands={'poll_status':False,
|
|
'feed_hold':False,
|
|
'cycle_start':False,
|
|
'soft_reset':False}
|
|
|
|
# metric stuff
|
|
#self._metric_chars = 0
|
|
#self._metric_time = None
|
|
#self._metric_active = True
|
|
#self._metric_thread = threading.Thread(target=self._metric_loop, name="comm._metric_thread")
|
|
#self._metric_thread.daemon = True
|
|
#self._metric_thread.start()
|
|
|
|
# hooks
|
|
self._pluginManager = octoprint.plugin.plugin_manager()
|
|
self._serial_factory_hooks = self._pluginManager.get_hooks("octoprint.comm.transport.serial.factory")
|
|
|
|
# monitoring thread
|
|
self._monitoring_active = True
|
|
self.monitoring_thread = threading.Thread(target=self._monitor_loop, name="comm._monitoring_thread")
|
|
self.monitoring_thread.daemon = True
|
|
self.monitoring_thread.start()
|
|
|
|
# sending thread
|
|
self._sending_active = True
|
|
self.sending_thread = threading.Thread(target=self._send_loop, name="comm.sending_thread")
|
|
self.sending_thread.daemon = True
|
|
|
|
def _monitor_loop(self):
|
|
#Open the serial port.
|
|
if not self._openSerial():
|
|
return
|
|
|
|
self._log("Connected to: %s, starting monitor" % self._serial)
|
|
self._changeState(self.STATE_CONNECTING)
|
|
self._timeout = get_new_timeout("communication")
|
|
|
|
while self._monitoring_active:
|
|
try:
|
|
line = self._readline()
|
|
if line is None:
|
|
break
|
|
if line.strip() is not "":
|
|
self._timeout = get_new_timeout("communication")
|
|
if line.startswith('<'): # status report
|
|
self._handle_status_report(line)
|
|
elif line.startswith('ok'): # ok message :)
|
|
self._handle_ok_message()
|
|
elif line.startswith('err'): # error message
|
|
self._handle_error_message(line)
|
|
elif line.startswith('ALA'): # ALARM message
|
|
self._handle_alarm_message(line)
|
|
elif line.startswith('['): # feedback message
|
|
self._handle_feedback_message(line)
|
|
elif line.startswith('Grb'): # Grbl startup message
|
|
self._handle_startup_message(line)
|
|
except:
|
|
self._logger.exception("Something crashed inside the monitoring loop, please report this to Mr. Beam")
|
|
errorMsg = "See octoprint.log for details"
|
|
self._log(errorMsg)
|
|
self._errorValue = errorMsg
|
|
self._changeState(self.STATE_ERROR)
|
|
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
|
|
self._log("Connection closed, closing down monitor")
|
|
|
|
def _send_loop(self):
|
|
while self._sending_active:
|
|
try:
|
|
self._process_rt_commands()
|
|
if self.isPrinting() and self._commandQueue.empty():
|
|
cmd = self._getNext()
|
|
if cmd is not None:
|
|
self.sendCommand(cmd)
|
|
self._callback.on_comm_progress()
|
|
else:
|
|
if self._finished_passes >= self._passes:
|
|
if len(self._acc_line_buffer) == 0:
|
|
self._set_print_finished()
|
|
self._currentFile.resetToBeginning()
|
|
cmd = self._getNext()
|
|
if cmd is not None:
|
|
self.sendCommand(cmd)
|
|
self._callback.on_comm_progress()
|
|
|
|
self._sendCommand()
|
|
self._send_event.wait(1)
|
|
self._send_event.clear()
|
|
except:
|
|
self._logger.exception("Something crashed inside the sending loop, please report this to Mr. Beam")
|
|
errorMsg = "See octoprint.log for details"
|
|
self._log(errorMsg)
|
|
self._errorValue = errorMsg
|
|
self._changeState(self.STATE_ERROR)
|
|
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
|
|
|
|
def _metric_loop(self):
|
|
self._metricf = open('metric.tmp','w')
|
|
self._metricf.write("1 sec interval")
|
|
while self._metric_active:
|
|
time.sleep(1)
|
|
if self._metric_time is not None:
|
|
t = time.time()
|
|
#s = "Metric: %f [chars/sec]" % (self._metric_chars / (t - self._metric_time))
|
|
s = "%.2f" % (self._metric_chars / (t - self._metric_time))
|
|
self._metric_time = None
|
|
self._metric_chars = 0
|
|
self._metricf.write(s)
|
|
#self._log(s)
|
|
self._metricf.close()
|
|
|
|
def _sendCommand(self, cmd=None):
|
|
if cmd is None:
|
|
if self._cmd is None and self._commandQueue.empty():
|
|
return
|
|
elif self._cmd is None:
|
|
self._cmd = self._commandQueue.get()
|
|
if sum([len(x) for x in self._acc_line_buffer]) + len(self._cmd) +1 < self.RX_BUFFER_SIZE-5:
|
|
self._cmd, _, _ = self._process_command_phase("sending", self._cmd)
|
|
self._log("Send: %s" % self._cmd)
|
|
self._acc_line_buffer.append(self._cmd + '\n')
|
|
try:
|
|
self._serial.write(self._cmd + '\n')
|
|
self._process_command_phase("sent", self._cmd)
|
|
self._cmd = None
|
|
self._send_event.set()
|
|
except serial.SerialException:
|
|
self._log("Unexpected error while writing serial port: %s" % (get_exception_string()))
|
|
self._errorValue = get_exception_string()
|
|
self.close(True)
|
|
else:
|
|
cmd, _, _ = self._process_command_phase("sending", cmd)
|
|
self._log("Send: %s" % cmd)
|
|
try:
|
|
|
|
self._serial.write(cmd)
|
|
self._process_command_phase("sent", cmd)
|
|
except serial.SerialException:
|
|
self._log("Unexpected error while writing serial port: %s" % (get_exception_string()))
|
|
self._errorValue = get_exception_string()
|
|
self.close(True)
|
|
|
|
def _process_rt_commands(self):
|
|
if self._real_time_commands['poll_status']:
|
|
self._sendCommand('?')
|
|
self._real_time_commands['poll_status']=False
|
|
elif self._real_time_commands['feed_hold']:
|
|
self._sendCommand('!')
|
|
self._real_time_commands['feed_hold']=False
|
|
elif self._real_time_commands['cycle_start']:
|
|
self._sendCommand('~')
|
|
self._real_time_commands['cycle_start']=False
|
|
elif self._real_time_commands['soft_reset']:
|
|
self._sendCommand(b'\x18')
|
|
self._real_time_commands['soft_reset']=False
|
|
|
|
def _openSerial(self):
|
|
def default(_, port, baudrate, read_timeout):
|
|
if port is None or port == 'AUTO':
|
|
# no known port, try auto detection
|
|
self._changeState(self.STATE_DETECT_SERIAL)
|
|
ser = self._detectPort(True)
|
|
if ser is None:
|
|
self._errorValue = 'Failed to autodetect serial port, please set it manually.'
|
|
self._changeState(self.STATE_ERROR)
|
|
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
|
|
self._log("Failed to autodetect serial port, please set it manually.")
|
|
return None
|
|
port = ser.port
|
|
|
|
# connect to regular serial port
|
|
self._log("Connecting to: %s" % port)
|
|
if baudrate == 0:
|
|
baudrates = baudrateList()
|
|
ser = serial.Serial(str(port), 115200 if 115200 in baudrates else baudrates[0], timeout=read_timeout, writeTimeout=10000, parity=serial.PARITY_ODD)
|
|
else:
|
|
ser = serial.Serial(str(port), baudrate, timeout=read_timeout, writeTimeout=10000, parity=serial.PARITY_ODD)
|
|
ser.close()
|
|
ser.parity = serial.PARITY_NONE
|
|
ser.open()
|
|
return ser
|
|
|
|
serial_factories = self._serial_factory_hooks.items() + [("default", default)]
|
|
for name, factory in serial_factories:
|
|
try:
|
|
serial_obj = factory(self, self._port, self._baudrate, settings().getFloat(["serial", "timeout", "connection"]))
|
|
except (OSError, serial.SerialException):
|
|
exception_string = get_exception_string()
|
|
self._errorValue = "Connection error, see Terminal tab"
|
|
self._changeState(self.STATE_ERROR)
|
|
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
|
|
self._log("Unexpected error while connecting to serial port: %s %s (hook %s)" % (self._port, exception_string, name))
|
|
if "failed to set custom baud rate" in exception_string.lower():
|
|
self._log("Your installation does not support custom baudrates (e.g. 250000) for connecting to your printer. This is a problem of the pyserial library that OctoPrint depends on. Please update to a pyserial version that supports your baudrate or switch your printer's firmware to a standard baudrate (e.g. 115200). See https://github.com/foosel/OctoPrint/wiki/OctoPrint-support-for-250000-baud-rate-on-Raspbian")
|
|
return False
|
|
if serial_obj is not None:
|
|
# first hook to succeed wins, but any can pass on to the next
|
|
self._log(repr(self._serial))
|
|
self._changeState(self.STATE_OPEN_SERIAL)
|
|
self._serial = serial_obj
|
|
return True
|
|
return False
|
|
|
|
def _readline(self):
|
|
if self._serial is None:
|
|
return None
|
|
try:
|
|
ret = self._serial.readline()
|
|
self._send_event.set()
|
|
if('ok' in ret or 'error' in ret):
|
|
if(len(self._acc_line_buffer) > 0):
|
|
del self._acc_line_buffer[0] # Delete the commands character count corresponding to the last 'ok'
|
|
except serial.SerialException:
|
|
self._log("Unexpected error while reading serial port: %s" % (get_exception_string()))
|
|
self._errorValue = get_exception_string()
|
|
self.close(True)
|
|
return None
|
|
if ret == '': return ''
|
|
try:
|
|
self._log("Recv: %s" % sanitize_ascii(ret))
|
|
except ValueError as e:
|
|
self._log("WARN: While reading last line: %s" % e)
|
|
self._log("Recv: %r" % ret)
|
|
return ret
|
|
|
|
def _getNext(self):
|
|
if self._finished_currentFile is False:
|
|
line = self._currentFile.getNext()
|
|
if line is None:
|
|
self._finished_passes += 1
|
|
if self._finished_passes >= self._passes:
|
|
self._finished_currentFile = True
|
|
return line
|
|
else:
|
|
return None
|
|
|
|
def _set_print_finished(self):
|
|
self._callback.on_comm_print_job_done()
|
|
self._changeState(self.STATE_OPERATIONAL)
|
|
payload = {
|
|
"file": self._currentFile.getFilename(),
|
|
"filename": os.path.basename(self._currentFile.getFilename()),
|
|
"origin": self._currentFile.getFileLocation(),
|
|
"time": self.getPrintTime()
|
|
}
|
|
eventManager().fire(Events.PRINT_DONE, payload)
|
|
self.sendCommand("M5")
|
|
self.sendCommand("G0X0Y0")
|
|
self.sendCommand("M9")
|
|
|
|
def _handle_status_report(self, line):
|
|
self._grbl_state = line[1:].split(',')[0]
|
|
if self._grbl_state == 'Queue':
|
|
if time.time() - self._pause_delay_time > 0.3:
|
|
if not self.isPaused():
|
|
self.setPause(True, False)
|
|
elif self._grbl_state == 'Run' or self._grbl_state == 'Idle':
|
|
if time.time() - self._pause_delay_time > 0.3:
|
|
if self.isPaused():
|
|
self.setPause(False, False)
|
|
self._update_grbl_pos(line)
|
|
#if self._metricf is not None:
|
|
# self._metricf.write(line)
|
|
|
|
def _handle_ok_message(self):
|
|
if self._state == self.STATE_HOMING:
|
|
self._changeState(self.STATE_OPERATIONAL)
|
|
|
|
def _handle_error_message(self, line):
|
|
self._errorValue = line
|
|
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
|
|
self._changeState(self.STATE_LOCKED)
|
|
|
|
def _handle_alarm_message(self, line):
|
|
if "Hard/soft limit" in line:
|
|
errorMsg = "Machine Limit Hit. Please reset the machine and do a homing cycle"
|
|
self._log(errorMsg)
|
|
self._errorValue = errorMsg
|
|
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
|
|
eventManager().fire(Events.LIMITS_HIT, {"error": self.getErrorString()})
|
|
elif "Abort during cycle" in line:
|
|
errorMsg = "Soft-reset detected. Please do a homing cycle"
|
|
self._log(errorMsg)
|
|
self._errorValue = errorMsg
|
|
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
|
|
eventManager().fire(Events.SOFT_RESET, {"error": self.getErrorString()})
|
|
elif "Probe fail" in line:
|
|
errorMsg = "Probing has failed. Please reset the machine and do a homing cycle"
|
|
self._log(errorMsg)
|
|
self._errorValue = errorMsg
|
|
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
|
|
|
|
with self._commandQueue.mutex:
|
|
self._commandQueue.queue.clear()
|
|
self._acc_line_buffer = []
|
|
self._send_event.clear(completely=True)
|
|
self._changeState(self.STATE_LOCKED)
|
|
|
|
# close and open serial port to reset arduino
|
|
self._serial.close()
|
|
self._openSerial()
|
|
|
|
def _handle_feedback_message(self, line):
|
|
if line[1:].startswith('Res'): # [Reset to continue]
|
|
pass
|
|
elif line[1:].startswith('\'$H'): # ['$H'|'$X' to unlock]
|
|
pass
|
|
elif line[1:].startswith('Cau'): # [Caution: Unlocked]
|
|
pass
|
|
elif line[1:].startswith('Ena'): # [Enabled]
|
|
pass
|
|
elif line[1:].startswith('Dis'): # [Disabled]
|
|
pass
|
|
|
|
def _handle_startup_message(self, line):
|
|
if self.isOperational():
|
|
errorMsg = "Machine reset."
|
|
self._cmd = None
|
|
self._acc_line_buffer = []
|
|
self._pauseWaitStartTime = None
|
|
self._pauseWaitTimeLost = 0.0
|
|
self._send_event.clear(completely=True)
|
|
with self._commandQueue.mutex:
|
|
self._commandQueue.queue.clear()
|
|
self._log(errorMsg)
|
|
self._errorValue = errorMsg
|
|
self._changeState(self.STATE_LOCKED)
|
|
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
|
|
else:
|
|
versionMatch = re.search("Grbl (?P<grbl>.+?)(_(?P<git>[0-9a-f]{7})(?P<dirty>-dirty)?)? \[.+\]", line)
|
|
if versionMatch:
|
|
versionDict = versionMatch.groupdict()
|
|
self._writeGrblVersionToFile(versionDict)
|
|
if self._compareGrblVersion(versionDict) is False:
|
|
self._flashGrbl()
|
|
else:
|
|
self._onConnected(self.STATE_LOCKED)
|
|
|
|
def _update_grbl_pos(self, line):
|
|
# line example:
|
|
# <Idle,MPos:-434.000,-596.000,0.000,WPos:0.000,0.000,0.000,S:0,laser off:0>
|
|
try:
|
|
idx_mx_begin = line.index('MPos:') + 5
|
|
idx_mx_end = line.index('.', idx_mx_begin) + 2
|
|
idx_my_begin = line.index(',', idx_mx_end) + 1
|
|
idx_my_end = line.index('.', idx_my_begin) + 2
|
|
#idx_mz_begin = line.index(',', idx_my_end) + 1
|
|
#idx_mz_end = line.index('.', idx_mz_begin) + 2
|
|
|
|
idx_wx_begin = line.index('WPos:') + 5
|
|
idx_wx_end = line.index('.', idx_wx_begin) + 2
|
|
idx_wy_begin = line.index(',', idx_wx_end) + 1
|
|
idx_wy_end = line.index('.', idx_wy_begin) + 2
|
|
#idx_wz_begin = line.index(',', idx_wy_end) + 1
|
|
#idx_wz_end = line.index('.', idx_wz_begin) + 2
|
|
|
|
#idx_intensity_begin = line.index('S:', idx_wz_end) + 2
|
|
#idx_intensity_end = line.index(',', idx_intensity_begin)
|
|
|
|
#idx_laserstate_begin = line.index('laser ', idx_intensity_end) + 6
|
|
#idx_laserstate_end = line.index(':', idx_laserstate_begin)
|
|
|
|
#payload = {
|
|
#"mx": line[idx_mx_begin:idx_mx_end],
|
|
#"my": line[idx_my_begin:idx_my_end],
|
|
#"mz": line[idx_mz_begin:idx_mz_end],
|
|
#"wx": line[idx_wx_begin:idx_wx_end],
|
|
#"wy": line[idx_wy_begin:idx_wy_end],
|
|
#"wz": line[idx_wz_begin:idx_wz_end],
|
|
#"laser": line[idx_laserstate_begin:idx_laserstate_end],
|
|
#"intensity": line[idx_intensity_begin:idx_intensity_end]
|
|
#}
|
|
mx = float(line[idx_mx_begin:idx_mx_end])
|
|
my = float(line[idx_my_begin:idx_my_end])
|
|
wx = float(line[idx_wx_begin:idx_wx_end])
|
|
wy = float(line[idx_wy_begin:idx_wy_end])
|
|
self._callback.on_comm_pos_update([mx, my, 0], [wx, wy, 0])
|
|
#eventManager().fire(Events.RT_STATE, payload)
|
|
except ValueError:
|
|
pass
|
|
|
|
def _process_command_phase(self, phase, command, command_type=None, gcode=None):
|
|
if phase not in ("queuing", "queued", "sending", "sent"):
|
|
return command, command_type, gcode
|
|
|
|
if gcode is None:
|
|
gcode = self._gcode_command_for_cmd(command)
|
|
|
|
# if it's a gcode command send it through the specific handler if it exists
|
|
if gcode is not None:
|
|
gcodeHandler = "_gcode_" + gcode + "_" + phase
|
|
if hasattr(self, gcodeHandler):
|
|
handler_result = getattr(self, gcodeHandler)(command, cmd_type=command_type)
|
|
command, command_type, gcode = self._handle_command_handler_result(command, command_type, gcode, handler_result)
|
|
|
|
# finally return whatever we resulted on
|
|
return command, command_type, gcode
|
|
|
|
def _gcode_command_for_cmd(self, cmd):
|
|
"""
|
|
Tries to parse the provided ``cmd`` and extract the GCODE command identifier from it (e.g. "G0" for "G0 X10.0").
|
|
|
|
Arguments:
|
|
cmd (str): The command to try to parse.
|
|
|
|
Returns:
|
|
str or None: The GCODE command identifier if it could be parsed, or None if not.
|
|
"""
|
|
if not cmd:
|
|
return None
|
|
|
|
if cmd == '!': return 'Hold'
|
|
if cmd == '~': return 'Resume'
|
|
|
|
gcode = self._regex_command.search(cmd)
|
|
if not gcode:
|
|
return None
|
|
|
|
return gcode.group(1)
|
|
|
|
# internal state management
|
|
def _changeState(self, newState):
|
|
if self._state == newState:
|
|
return
|
|
|
|
if newState == self.STATE_PRINTING:
|
|
if self._status_timer is not None:
|
|
self._status_timer.cancel()
|
|
self._status_timer = RepeatedTimer(1, self._poll_status)
|
|
self._status_timer.start()
|
|
elif newState == self.STATE_OPERATIONAL:
|
|
if self._status_timer is not None:
|
|
self._status_timer.cancel()
|
|
self._status_timer = RepeatedTimer(2, self._poll_status)
|
|
self._status_timer.start()
|
|
elif newState == self.STATE_PAUSED:
|
|
if self._status_timer is not None:
|
|
self._status_timer.cancel()
|
|
self._status_timer = RepeatedTimer(0.2, self._poll_status)
|
|
self._status_timer.start()
|
|
|
|
if newState == self.STATE_CLOSED or newState == self.STATE_CLOSED_WITH_ERROR:
|
|
if self._currentFile is not None:
|
|
self._currentFile.close()
|
|
self._log("entered state closed / closed with error. reseting character counter.")
|
|
self.acc_line_lengths = []
|
|
|
|
oldState = self.getStateString()
|
|
self._state = newState
|
|
self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString()))
|
|
self._callback.on_comm_state_change(newState)
|
|
|
|
def _onConnected(self, nextState):
|
|
self._serial.timeout = settings().getFloat(["serial", "timeout", "communication"])
|
|
|
|
if(nextState is None):
|
|
self._changeState(self.STATE_LOCKED)
|
|
else:
|
|
self._changeState(nextState)
|
|
|
|
if not self.sending_thread.isAlive():
|
|
self.sending_thread.start()
|
|
|
|
payload = dict(port=self._port, baudrate=self._baudrate)
|
|
eventManager().fire(Events.CONNECTED, payload)
|
|
|
|
def _detectPort(self, close):
|
|
self._log("Serial port list: %s" % (str(serialList())))
|
|
for p in serialList():
|
|
try:
|
|
self._log("Connecting to: %s" % (p))
|
|
serial_obj = serial.Serial(p)
|
|
if close:
|
|
serial_obj.close()
|
|
return serial_obj
|
|
except (OSError, serial.SerialException) as e:
|
|
self._log("Error while connecting to %s: %s" % (p, str(e)))
|
|
return None
|
|
|
|
def _poll_status(self):
|
|
if self.isOperational():
|
|
self._real_time_commands['poll_status']=True
|
|
self._send_event.set()
|
|
|
|
def _soft_reset(self):
|
|
if self.isOperational():
|
|
self._real_time_commands['soft_reset']=True
|
|
self._send_event.set()
|
|
|
|
def _log(self, message):
|
|
self._callback.on_comm_log(message)
|
|
self._serialLogger.debug(message)
|
|
|
|
def _compareGrblVersion(self, versionDict):
|
|
cwd = os.path.dirname(__file__)
|
|
with open(cwd + "/../grbl/grblVersionRequirement.yml", 'r') as infile:
|
|
grblReqDict = yamlload(infile)
|
|
requiredGrblVer = str(grblReqDict['grbl']) + '_' + str(grblReqDict['git'])
|
|
if grblReqDict['dirty'] is True:
|
|
requiredGrblVer += '-dirty'
|
|
actualGrblVer = str(versionDict['grbl']) + '_' + str(versionDict['git'])
|
|
if versionDict['dirty'] is not(None):
|
|
actualGrblVer += '-dirty'
|
|
# compare actual and required grbl version
|
|
self._requiredGrblVer = requiredGrblVer
|
|
self._actualGrblVer = actualGrblVer
|
|
print repr(requiredGrblVer)
|
|
print repr(actualGrblVer)
|
|
if requiredGrblVer != actualGrblVer:
|
|
self._log("unsupported grbl version detected...")
|
|
self._log("required: " + requiredGrblVer)
|
|
self._log("detected: " + actualGrblVer)
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
def _flashGrbl(self):
|
|
self._changeState(self.STATE_FLASHING)
|
|
self._serial.close()
|
|
cwd = os.path.dirname(__file__)
|
|
pathToGrblHex = cwd + "/../grbl/grbl.hex"
|
|
|
|
# TODO check if avrdude is installed.
|
|
# TODO log in logfile as well, not only to the serial monitor (use self._logger.info()... )
|
|
params = ["avrdude", "-patmega328p", "-carduino", "-b" + str(self._baudrate), "-P" + str(self._port), "-D", "-Uflash:w:" + pathToGrblHex]
|
|
rc = subprocesscall(params)
|
|
|
|
if rc == 0:
|
|
self._log("successfully flashed new grbl version")
|
|
self._openSerial()
|
|
self._changeState(self.STATE_CONNECTING)
|
|
else:
|
|
self._log("error during flashing of new grbl version")
|
|
self._errorValue = "avrdude returncode: %s" % rc
|
|
self._changeState(self.STATE_CLOSED_WITH_ERROR)
|
|
|
|
@staticmethod
|
|
def _writeGrblVersionToFile(versionDict):
|
|
if versionDict['dirty'] == '-dirty':
|
|
versionDict['dirty'] = True
|
|
versionDict['lastConnect'] = time.time()
|
|
versionFile = os.path.join(settings().getBaseFolder("logs"), 'grbl_Version.yml')
|
|
with open(versionFile, 'w') as outfile:
|
|
outfile.write(yamldump(versionDict, default_flow_style=True))
|
|
|
|
def _handle_command_handler_result(self, command, command_type, gcode, handler_result):
|
|
original_tuple = (command, command_type, gcode)
|
|
|
|
if handler_result is None:
|
|
# handler didn't return anything, we'll just continue
|
|
return original_tuple
|
|
|
|
if isinstance(handler_result, basestring):
|
|
# handler did return just a string, we'll turn that into a 1-tuple now
|
|
handler_result = (handler_result,)
|
|
elif not isinstance(handler_result, (tuple, list)):
|
|
# handler didn't return an expected result format, we'll just ignore it and continue
|
|
return original_tuple
|
|
|
|
hook_result_length = len(handler_result)
|
|
if hook_result_length == 1:
|
|
# handler returned just the command
|
|
command, = handler_result
|
|
elif hook_result_length == 2:
|
|
# handler returned command and command_type
|
|
command, command_type = handler_result
|
|
else:
|
|
# handler returned a tuple of an unexpected length
|
|
return original_tuple
|
|
|
|
gcode = self._gcode_command_for_cmd(command)
|
|
return command, command_type, gcode
|
|
|
|
def _set_feedrate_override(self, value):
|
|
temp = value / 100.0
|
|
if temp > 0:
|
|
self._feedrate_factor = temp
|
|
self._feedrate_dict = {}
|
|
if self._actual_feedrate is not None:
|
|
temp = round(self._actual_feedrate * self._feedrate_factor)
|
|
# TODO replace with value from printer profile
|
|
if temp > 5000:
|
|
temp = 5000
|
|
self.sendCommand('F%d' % round(temp))
|
|
|
|
def _set_intensity_override(self, value):
|
|
temp = value / 100.0
|
|
if temp >= 0:
|
|
self._intensity_factor = temp
|
|
self._intensity_dict = {}
|
|
if self._actual_intensity is not None:
|
|
temp = round(self._actual_intensity * self._intensity_factor)
|
|
if temp > 1000:
|
|
temp = 1000
|
|
self.sendCommand('S%d' % round(temp))
|
|
|
|
def _replace_feedrate(self, cmd):
|
|
if self._feedrate_factor != 1:
|
|
obj = self._regex_feedrate.search(cmd)
|
|
if obj is not None:
|
|
feedrate_cmd = cmd[obj.start():obj.end()]
|
|
if feedrate_cmd in self._feedrate_dict:
|
|
new_feedrate = self._feedrate_dict[feedrate_cmd]
|
|
else:
|
|
self._actual_feedrate = int(feedrate_cmd[1:])
|
|
new_feedrate = round(self._actual_feedrate * self._feedrate_factor)
|
|
# TODO replace with value from printer profile
|
|
if new_feedrate > 5000:
|
|
new_feedrate = 5000
|
|
elif new_feedrate < 30:
|
|
new_feedrate = 30
|
|
self._feedrate_dict[feedrate_cmd] = new_feedrate
|
|
else:
|
|
return cmd
|
|
return cmd.replace(feedrate_cmd, 'F%d' % round(new_feedrate))
|
|
return cmd
|
|
|
|
def _replace_intensity(self, cmd):
|
|
if self._intensity_factor != 1:
|
|
obj = self._regex_intensity.search(cmd)
|
|
if obj is not None:
|
|
intensity_cmd = cmd[obj.start():obj.end()]
|
|
if intensity_cmd in self._intensity_dict:
|
|
new_intensity = self._intensity_dict[intensity_cmd]
|
|
else:
|
|
self._actual_intensity = int(intensity_cmd[1:])
|
|
new_intensity = round(self._actual_intensity * self._intensity_factor)
|
|
if new_intensity > 1000:
|
|
new_intensity = 1000
|
|
self._intensity_dict[intensity_cmd] = new_intensity
|
|
else:
|
|
return cmd
|
|
return cmd.replace(intensity_cmd, 'S%d' % round(new_intensity))
|
|
return cmd
|
|
|
|
##~~ command handlers
|
|
def _gcode_G1_sending(self, cmd, cmd_type=None):
|
|
cmd = self._replace_feedrate(cmd)
|
|
cmd = self._replace_intensity(cmd)
|
|
return cmd
|
|
|
|
def _gcode_G2_sending(self, cmd, cmd_type=None):
|
|
cmd = self._replace_feedrate(cmd)
|
|
cmd = self._replace_intensity(cmd)
|
|
return cmd
|
|
|
|
def _gcode_G3_sending(self, cmd, cmd_type=None):
|
|
cmd = self._replace_feedrate(cmd)
|
|
cmd = self._replace_intensity(cmd)
|
|
return cmd
|
|
|
|
def _gcode_G01_sending(self, cmd, cmd_type=None):
|
|
return self._gcode_G1_sending(cmd, cmd_type)
|
|
|
|
def _gcode_G02_sending(self, cmd, cmd_type=None):
|
|
return self._gcode_G2_sending(cmd, cmd_type)
|
|
|
|
def _gcode_G03_sending(self, cmd, cmd_type=None):
|
|
return self._gcode_G3_sending(cmd, cmd_type)
|
|
|
|
def _gcode_H_sent(self, cmd, cmd_type=None):
|
|
self._changeState(self.STATE_HOMING)
|
|
return cmd
|
|
|
|
def _gcode_X_sent(self, cmd, cmd_type=None):
|
|
self._changeState(self.STATE_HOMING) # TODO: maybe change to seperate $X mode
|
|
return cmd
|
|
|
|
def _gcode_Hold_sent(self, cmd, cmd_type=None):
|
|
self._changeState(self.STATE_PAUSED)
|
|
return cmd
|
|
|
|
def _gcode_Resume_sent(self, cmd, cmd_type=None):
|
|
self._changeState(self.STATE_PRINTING)
|
|
return cmd
|
|
|
|
def sendCommand(self, cmd, cmd_type=None, processed=False):
|
|
cmd = cmd.encode('ascii', 'replace')
|
|
if not processed:
|
|
cmd = process_gcode_line(cmd)
|
|
if not cmd:
|
|
return
|
|
|
|
if cmd[0] == "/":
|
|
specialcmd = cmd[1:].lower()
|
|
if "togglestatusreport" in specialcmd:
|
|
if self._status_timer is None:
|
|
self._status_timer = RepeatedTimer(1, self._poll_status)
|
|
self._status_timer.start()
|
|
else:
|
|
self._status_timer.cancel()
|
|
self._status_timer = None
|
|
elif "setstatusfrequency" in specialcmd:
|
|
data = specialcmd[18:]
|
|
try:
|
|
frequency = float(data)
|
|
except ValueError:
|
|
self._log("No frequency setting found! Using 1 sec.")
|
|
frequency = 1
|
|
if self._status_timer is not None:
|
|
self._status_timer.cancel()
|
|
self._status_timer = RepeatedTimer(frequency, self._poll_status)
|
|
self._status_timer.start()
|
|
elif "disconnect" in specialcmd:
|
|
self.close()
|
|
elif "feedrate" in specialcmd:
|
|
data = specialcmd[8:]
|
|
self._set_feedrate_override(int(data))
|
|
elif "intensity" in specialcmd:
|
|
data = specialcmd[9:]
|
|
self._set_intensity_override(int(data))
|
|
else:
|
|
self._log("Command not Found! %s" % cmd)
|
|
self._log("available commands are:")
|
|
self._log(" /togglestatusreport")
|
|
self._log(" /setstatusfrequency <Inteval Sec>")
|
|
self._log(" /feedrate <%>")
|
|
self._log(" /intensity <%>")
|
|
self._log(" /disconnect")
|
|
return
|
|
|
|
eepromCmd = re.search("^\$[0-9]+=.+$", cmd)
|
|
if(eepromCmd and self.isPrinting()):
|
|
self._log("Warning: Configuration changes during print are not allowed!")
|
|
|
|
self._commandQueue.put(cmd)
|
|
self._send_event.set()
|
|
|
|
def selectFile(self, filename, sd):
|
|
if self.isBusy():
|
|
return
|
|
|
|
self._currentFile = PrintingGcodeFileInformation(filename)
|
|
eventManager().fire(Events.FILE_SELECTED, {
|
|
"file": self._currentFile.getFilename(),
|
|
"filename": os.path.basename(self._currentFile.getFilename()),
|
|
"origin": self._currentFile.getFileLocation()
|
|
})
|
|
self._callback.on_comm_file_selected(filename, self._currentFile.getFilesize(), False)
|
|
|
|
def unselectFile(self):
|
|
if self.isBusy():
|
|
return
|
|
|
|
self._currentFile = None
|
|
eventManager().fire(Events.FILE_DESELECTED)
|
|
self._callback.on_comm_file_selected(None, None, False)
|
|
|
|
def startPrint(self):
|
|
if not self.isOperational():
|
|
return
|
|
|
|
if self._currentFile is None:
|
|
raise ValueError("No file selected for printing")
|
|
|
|
# reset feedrate and intesity factor in case they where changed in a previous run
|
|
self._feedrate_factor = 1
|
|
self._intensity_factor = 1
|
|
self._passes = 1
|
|
self._finished_passes = 0
|
|
|
|
try:
|
|
# ensure fan is on whatever gcode follows.
|
|
self.sendCommand("M08")
|
|
|
|
self._currentFile.start()
|
|
self._finished_currentFile = False
|
|
|
|
payload = {
|
|
"file": self._currentFile.getFilename(),
|
|
"filename": os.path.basename(self._currentFile.getFilename()),
|
|
"origin": self._currentFile.getFileLocation()
|
|
}
|
|
eventManager().fire(Events.PRINT_STARTED, payload)
|
|
#self.sendGcodeScript("beforePrintStarted", replacements=dict(event=payload))
|
|
|
|
self._changeState(self.STATE_PRINTING)
|
|
except:
|
|
self._logger.exception("Error while trying to start printing")
|
|
self._errorValue = get_exception_string()
|
|
self._changeState(self.STATE_ERROR)
|
|
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
|
|
|
|
def cancelPrint(self):
|
|
if not self.isOperational():
|
|
return
|
|
|
|
with self._commandQueue.mutex:
|
|
self._commandQueue.queue.clear()
|
|
self._soft_reset()
|
|
self._acc_line_buffer = []
|
|
self._send_event.clear(completely=True)
|
|
self._changeState(self.STATE_LOCKED)
|
|
|
|
payload = {
|
|
"file": self._currentFile.getFilename(),
|
|
"filename": os.path.basename(self._currentFile.getFilename()),
|
|
"origin": self._currentFile.getFileLocation()
|
|
}
|
|
|
|
eventManager().fire(Events.PRINT_CANCELLED, payload)
|
|
|
|
def setPause(self, pause, send_cmd=True):
|
|
if not self._currentFile:
|
|
return
|
|
|
|
payload = {
|
|
"file": self._currentFile.getFilename(),
|
|
"filename": os.path.basename(self._currentFile.getFilename()),
|
|
"origin": self._currentFile.getFileLocation()
|
|
}
|
|
|
|
if not pause and self.isPaused():
|
|
if self._pauseWaitStartTime:
|
|
self._pauseWaitTimeLost = self._pauseWaitTimeLost + (time.time() - self._pauseWaitStartTime)
|
|
self._pauseWaitStartTime = None
|
|
self._pause_delay_time = time.time()
|
|
if send_cmd is True:
|
|
self._real_time_commands['cycle_start']=True
|
|
self._send_event.set()
|
|
eventManager().fire(Events.PRINT_RESUMED, payload)
|
|
elif pause and self.isPrinting():
|
|
if not self._pauseWaitStartTime:
|
|
self._pauseWaitStartTime = time.time()
|
|
self._pause_delay_time = time.time()
|
|
if send_cmd is True:
|
|
self._real_time_commands['feed_hold']=True
|
|
self._send_event.set()
|
|
eventManager().fire(Events.PRINT_PAUSED, payload)
|
|
|
|
def increasePasses(self):
|
|
self._passes += 1
|
|
self._log("increased Passes to %d" % self._passes)
|
|
|
|
def degreasePasses(self):
|
|
self._passes -= 1
|
|
self._log("degrease Passes to %d" % self._passes)
|
|
|
|
def getStateString(self):
|
|
if self._state == self.STATE_NONE:
|
|
return "Offline"
|
|
if self._state == self.STATE_OPEN_SERIAL:
|
|
return "Opening serial port"
|
|
if self._state == self.STATE_DETECT_SERIAL:
|
|
return "Detecting serial port"
|
|
if self._state == self.STATE_CONNECTING:
|
|
return "Connecting"
|
|
if self._state == self.STATE_OPERATIONAL:
|
|
return "Operational"
|
|
if self._state == self.STATE_PRINTING:
|
|
return "Printing"
|
|
if self._state == self.STATE_PAUSED:
|
|
return "Paused"
|
|
if self._state == self.STATE_CLOSED:
|
|
return "Closed"
|
|
if self._state == self.STATE_ERROR:
|
|
return "Error: %s" % (self.getErrorString())
|
|
if self._state == self.STATE_CLOSED_WITH_ERROR:
|
|
return "Error: %s" % (self.getErrorString())
|
|
if self._state == self.STATE_LOCKED:
|
|
return "Locked"
|
|
if self._state == self.STATE_HOMING:
|
|
return "Homing"
|
|
if self._state == self.STATE_FLASHING:
|
|
return "Flashing"
|
|
return "?%d?" % (self._state)
|
|
|
|
def getPrintProgress(self):
|
|
if self._currentFile is None:
|
|
return None
|
|
return self._currentFile.getProgress()
|
|
|
|
def getPrintFilepos(self):
|
|
if self._currentFile is None:
|
|
return None
|
|
return self._currentFile.getFilepos()
|
|
|
|
def getCleanedPrintTime(self):
|
|
printTime = self.getPrintTime()
|
|
if printTime is None:
|
|
return None
|
|
return printTime
|
|
|
|
def getConnection(self):
|
|
return self._port, self._baudrate
|
|
|
|
def isOperational(self):
|
|
return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED
|
|
|
|
def isPrinting(self):
|
|
return self._state == self.STATE_PRINTING
|
|
|
|
def isPaused(self):
|
|
return self._state == self.STATE_PAUSED
|
|
|
|
def isLocked(self):
|
|
return self._state == self.STATE_LOCKED
|
|
|
|
def isHoming(self):
|
|
return self._state == self.STATE_HOMING
|
|
|
|
def isFlashing(self):
|
|
return self._state == self.STATE_FLASHING
|
|
|
|
def isBusy(self):
|
|
return self.isPrinting() or self.isPaused()
|
|
|
|
def isError(self):
|
|
return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
|
|
|
|
def isClosedOrError(self):
|
|
return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
|
|
|
|
def isSdReady(self):
|
|
return False
|
|
|
|
def isStreaming(self):
|
|
return False
|
|
|
|
def getErrorString(self):
|
|
return self._errorValue
|
|
|
|
def getPrintTime(self):
|
|
if self._currentFile is None or self._currentFile.getStartTime() is None:
|
|
return None
|
|
else:
|
|
return time.time() - self._currentFile.getStartTime() - self._pauseWaitTimeLost
|
|
|
|
def close(self, isError = False):
|
|
if self._status_timer is not None:
|
|
try:
|
|
self._status_timer.cancel()
|
|
self._status_timer = None
|
|
except AttributeError:
|
|
pass
|
|
|
|
self._monitoring_active = False
|
|
self._sending_active = False
|
|
|
|
printing = self.isPrinting() or self.isPaused()
|
|
if self._serial is not None:
|
|
if isError:
|
|
self._changeState(self.STATE_CLOSED_WITH_ERROR)
|
|
else:
|
|
self._changeState(self.STATE_CLOSED)
|
|
self._serial.close()
|
|
self._serial = None
|
|
|
|
if printing:
|
|
payload = None
|
|
if self._currentFile is not None:
|
|
payload = {
|
|
"file": self._currentFile.getFilename(),
|
|
"filename": os.path.basename(self._currentFile.getFilename()),
|
|
"origin": self._currentFile.getFileLocation()
|
|
}
|
|
eventManager().fire(Events.PRINT_FAILED, payload)
|
|
eventManager().fire(Events.DISCONNECTED)
|
|
|
|
### MachineCom callback ################################################################################################
|
|
class MachineComPrintCallback(object):
|
|
def on_comm_log(self, message):
|
|
pass
|
|
|
|
def on_comm_temperature_update(self, temp, bedTemp):
|
|
pass
|
|
|
|
def on_comm_state_change(self, state):
|
|
pass
|
|
|
|
def on_comm_message(self, message):
|
|
pass
|
|
|
|
def on_comm_progress(self):
|
|
pass
|
|
|
|
def on_comm_print_job_done(self):
|
|
pass
|
|
|
|
def on_comm_z_change(self, newZ):
|
|
pass
|
|
|
|
def on_comm_file_selected(self, filename, filesize, sd):
|
|
pass
|
|
|
|
def on_comm_sd_state_change(self, sdReady):
|
|
pass
|
|
|
|
def on_comm_sd_files(self, files):
|
|
pass
|
|
|
|
def on_comm_file_transfer_started(self, filename, filesize):
|
|
pass
|
|
|
|
def on_comm_file_transfer_done(self, filename):
|
|
pass
|
|
|
|
def on_comm_force_disconnect(self):
|
|
pass
|
|
|
|
def on_comm_pos_update(self, MPos, WPos):
|
|
pass
|
|
|
|
class PrintingFileInformation(object):
|
|
"""
|
|
Encapsulates information regarding the current file being printed: file name, current position, total size and
|
|
time the print started.
|
|
Allows to reset the current file position to 0 and to calculate the current progress as a floating point
|
|
value between 0 and 1.
|
|
"""
|
|
|
|
def __init__(self, filename):
|
|
self._logger = logging.getLogger(__name__)
|
|
self._filename = filename
|
|
self._pos = 0
|
|
self._size = None
|
|
self._start_time = None
|
|
|
|
def getStartTime(self):
|
|
return self._start_time
|
|
|
|
def getFilename(self):
|
|
return self._filename
|
|
|
|
def getFilesize(self):
|
|
return self._size
|
|
|
|
def getFilepos(self):
|
|
return self._pos
|
|
|
|
def getFileLocation(self):
|
|
return FileDestinations.LOCAL
|
|
|
|
def getProgress(self):
|
|
"""
|
|
The current progress of the file, calculated as relation between file position and absolute size. Returns -1
|
|
if file size is None or < 1.
|
|
"""
|
|
if self._size is None or not self._size > 0:
|
|
return -1
|
|
return float(self._pos) / float(self._size)
|
|
|
|
def reset(self):
|
|
"""
|
|
Resets the current file position to 0.
|
|
"""
|
|
self._pos = 0
|
|
|
|
def start(self):
|
|
"""
|
|
Marks the print job as started and remembers the start time.
|
|
"""
|
|
self._start_time = time.time()
|
|
|
|
def close(self):
|
|
"""
|
|
Closes the print job.
|
|
"""
|
|
pass
|
|
|
|
class PrintingGcodeFileInformation(PrintingFileInformation):
|
|
"""
|
|
Encapsulates information regarding an ongoing direct print. Takes care of the needed file handle and ensures
|
|
that the file is closed in case of an error.
|
|
"""
|
|
|
|
def __init__(self, filename, offsets_callback=None, current_tool_callback=None):
|
|
PrintingFileInformation.__init__(self, filename)
|
|
|
|
self._handle = None
|
|
|
|
self._first_line = None
|
|
|
|
self._offsets_callback = offsets_callback
|
|
self._current_tool_callback = current_tool_callback
|
|
|
|
if not os.path.exists(self._filename) or not os.path.isfile(self._filename):
|
|
raise IOError("File %s does not exist" % self._filename)
|
|
|
|
self._stripCommments()
|
|
|
|
self._size = os.stat(self._filename).st_size
|
|
self._pos = 0
|
|
|
|
def start(self):
|
|
"""
|
|
Opens the file for reading and determines the file size.
|
|
"""
|
|
PrintingFileInformation.start(self)
|
|
self._handle = open(self._filename, "r")
|
|
|
|
def close(self):
|
|
"""
|
|
Closes the file if it's still open.
|
|
"""
|
|
PrintingFileInformation.close(self)
|
|
if self._handle is not None:
|
|
try:
|
|
self._handle.close()
|
|
except:
|
|
pass
|
|
self._handle = None
|
|
|
|
def resetToBeginning(self):
|
|
"""
|
|
resets the file handle so you can read from the beginning again.
|
|
"""
|
|
self._handle = open(self._filename, "r")
|
|
|
|
def getNext(self):
|
|
"""
|
|
Retrieves the next line for printing.
|
|
"""
|
|
if self._handle is None:
|
|
raise ValueError("File %s is not open for reading" % self._filename)
|
|
|
|
try:
|
|
processed = None
|
|
while processed is None:
|
|
if self._handle is None:
|
|
# file got closed just now
|
|
return None
|
|
line = self._handle.readline()
|
|
if not line:
|
|
self.close()
|
|
processed = process_gcode_line(line)
|
|
self._pos = self._handle.tell()
|
|
|
|
return processed
|
|
except Exception as e:
|
|
self.close()
|
|
self._logger.exception("Exception while processing line")
|
|
raise e
|
|
|
|
def _stripCommments(self):
|
|
dir = os.path.dirname(os.path.abspath(self._filename))
|
|
tmpfile = open(dir + '/gcode.tmp', 'w')
|
|
with open(self._filename, "r") as fileobject:
|
|
for line in fileobject:
|
|
if process_gcode_line(line) is not None:
|
|
tmpfile.write(line)
|
|
tmpfile.close()
|
|
self._filename = dir + '/gcode.tmp'
|
|
|
|
def convert_pause_triggers(configured_triggers):
|
|
triggers = {
|
|
"enable": [],
|
|
"disable": [],
|
|
"toggle": []
|
|
}
|
|
for trigger in configured_triggers:
|
|
if not "regex" in trigger or not "type" in trigger:
|
|
continue
|
|
|
|
try:
|
|
regex = trigger["regex"]
|
|
t = trigger["type"]
|
|
if t in triggers:
|
|
# make sure regex is valid
|
|
re.compile(regex)
|
|
# add to type list
|
|
triggers[t].append(regex)
|
|
except re.error:
|
|
# invalid regex or something like this, we'll just skip this entry
|
|
pass
|
|
|
|
result = dict()
|
|
for t in triggers.keys():
|
|
if len(triggers[t]) > 0:
|
|
result[t] = re.compile("|".join(map(lambda pattern: "({pattern})".format(pattern=pattern), triggers[t])))
|
|
return result
|
|
|
|
def process_gcode_line(line):
|
|
line = strip_comment(line).strip()
|
|
line = line.replace(" ", "")
|
|
if not len(line):
|
|
return None
|
|
return line
|
|
|
|
def strip_comment(line):
|
|
if not ";" in line:
|
|
# shortcut
|
|
return line
|
|
|
|
escaped = False
|
|
result = []
|
|
for c in line:
|
|
if c == ";" and not escaped:
|
|
break
|
|
result += c
|
|
escaped = (c == "\\") and not escaped
|
|
return "".join(result)
|
|
|
|
def get_new_timeout(t):
|
|
now = time.time()
|
|
return now + get_interval(t)
|
|
|
|
def get_interval(t):
|
|
if t not in default_settings["serial"]["timeout"]:
|
|
return 0
|
|
else:
|
|
return settings().getFloat(["serial", "timeout", t])
|
|
|
|
def serialList():
|
|
baselist = []
|
|
baselist = baselist \
|
|
+ glob.glob("/dev/ttyUSB*") \
|
|
+ glob.glob("/dev/ttyACM*") \
|
|
+ glob.glob("/dev/ttyAMA*") \
|
|
+ glob.glob("/dev/tty.usb*") \
|
|
+ glob.glob("/dev/cu.*") \
|
|
+ glob.glob("/dev/cuaU*") \
|
|
+ glob.glob("/dev/rfcomm*")
|
|
|
|
additionalPorts = settings().get(["serial", "additionalPorts"])
|
|
for additional in additionalPorts:
|
|
baselist += glob.glob(additional)
|
|
|
|
prev = settings().get(["serial", "port"])
|
|
if prev in baselist:
|
|
baselist.remove(prev)
|
|
baselist.insert(0, prev)
|
|
if settings().getBoolean(["devel", "virtualPrinter", "enabled"]):
|
|
baselist.append("VIRTUAL")
|
|
return filter(None, baselist)
|
|
|
|
def baudrateList():
|
|
ret = [250000, 230400, 115200, 57600, 38400, 19200, 9600]
|
|
prev = settings().getInt(["serial", "baudrate"])
|
|
if prev in ret:
|
|
ret.remove(prev)
|
|
ret.insert(0, prev)
|
|
return ret
|