MrDraw/src/octoprint/util/comm_acc2.py

1239 lines
38 KiB
Python
Raw Normal View History

# 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
2015-10-20 14:21:01 +00:00
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
2015-10-20 14:21:01 +00:00
from octoprint.events import eventManager, Events
2015-10-19 13:05:56 +00:00
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"])
2015-10-20 14:21:01 +00:00
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
2015-10-19 12:00:52 +00:00
self.RX_BUFFER_SIZE = 127
self._state = self.STATE_NONE
2015-10-22 13:21:52 +00:00
self._grbl_state = None
self._errorValue = "Unknown Error"
self._serial = None
self._currentFile = None
self._status_timer = None
2015-10-19 12:00:52 +00:00
self._acc_line_buffer = []
2015-10-20 14:21:01 +00:00
self._cmd = None
2015-10-20 16:12:30 +00:00
self._pauseWaitStartTime = None
2015-10-19 12:00:52 +00:00
self._pauseWaitTimeLost = 0.0
2015-10-20 14:21:01 +00:00
self._commandQueue = Queue.Queue()
2015-10-27 12:33:39 +00:00
self._send_event = CountedEvent(max=50)
2015-10-22 13:21:52 +00:00
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
2015-10-20 14:21:01 +00:00
# regular expressions
self._regex_command = re.compile("^\s*\$?([GM]\d+|[TH])")
self._regex_feedrate = re.compile("F\d+", re.IGNORECASE)
self._regex_intensity = re.compile("S\d+", re.IGNORECASE)
2015-10-19 12:00:52 +00:00
self._real_time_commands={'poll_status':False,
'feed_hold':False,
'cycle_start':False,
'soft_reset':False}
2015-10-27 12:33:39 +00:00
# metric stuff
2015-10-27 15:07:36 +00:00
#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()
2015-10-27 12:33:39 +00:00
# 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")
2015-10-22 13:21:52 +00:00
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()
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):
2015-10-19 12:00:52 +00:00
while self._sending_active:
try:
2015-10-22 13:21:52 +00:00
self._process_rt_commands()
2015-10-20 14:21:01 +00:00
if self.isPrinting() and self._commandQueue.empty():
cmd = self._getNext()
if cmd is not None:
self.sendCommand(cmd)
2015-10-22 13:21:52 +00:00
self._callback.on_comm_progress()
elif len(self._acc_line_buffer) == 0:
self._set_print_finished()
2015-10-20 14:21:01 +00:00
self._sendCommand()
2015-10-28 14:21:47 +00:00
self._send_event.wait(1)
2015-10-27 12:33:39 +00:00
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()})
2015-10-19 12:00:52 +00:00
2015-10-27 12:33:39 +00:00
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()
2015-10-21 13:57:34 +00:00
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)
2015-10-21 13:57:34 +00:00
self._log("Send: %s" % self._cmd)
2015-10-26 15:37:04 +00:00
self._acc_line_buffer.append(self._cmd + '\n')
2015-10-21 13:57:34 +00:00
try:
2015-10-26 15:37:04 +00:00
self._serial.write(self._cmd + '\n')
2015-10-21 13:57:34 +00:00
self._process_command_phase("sent", self._cmd)
self._cmd = None
2015-10-27 12:33:39 +00:00
self._send_event.set()
2015-10-21 13:57:34 +00:00
except serial.SerialException:
self._log("Unexpected error while writing serial port: %s" % (get_exception_string()))
self._errorValue = get_exception_string()
self.close(True)
else:
self._log("Send: %s" % cmd)
2015-10-19 12:00:52 +00:00
try:
self._cmd, _, _ = self._process_command_phase("sending", self._cmd)
self._serial.write(cmd)
2015-10-21 13:57:34 +00:00
self._process_command_phase("sent", cmd)
2015-10-19 12:00:52 +00:00
except serial.SerialException:
self._log("Unexpected error while writing serial port: %s" % (get_exception_string()))
self._errorValue = get_exception_string()
self.close(True)
2015-10-22 13:21:52 +00:00
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
2015-10-19 13:05:56 +00:00
self._log(repr(self._serial))
2015-10-20 14:21:01 +00:00
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()
2015-10-27 12:33:39 +00:00
self._send_event.set()
if('ok' in ret or 'error' in ret):
2015-10-20 14:21:01 +00:00
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
2015-10-19 12:00:52 +00:00
def _getNext(self):
2015-10-22 13:21:52 +00:00
if self._finished_currentFile is False:
line = self._currentFile.getNext()
if line is None:
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)
2015-10-28 10:18:28 +00:00
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)
2015-10-22 13:21:52 +00:00
self._update_grbl_pos(line)
2015-10-27 15:07:36 +00:00
#if self._metricf is not None:
# self._metricf.write(line)
2015-10-22 13:21:52 +00:00
def _handle_ok_message(self):
if self._state == self.STATE_HOMING:
2015-10-19 12:00:52 +00:00
self._changeState(self.STATE_OPERATIONAL)
2015-10-22 13:21:52 +00:00
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:
2015-10-28 10:18:28 +00:00
errorMsg = "Soft-reset detected. Please do a homing cycle"
2015-10-22 13:21:52 +00:00
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()})
2015-10-28 10:18:28 +00:00
with self._commandQueue.mutex:
self._commandQueue.queue.clear()
self._acc_line_buffer = []
self._send_event.clear(completely=True)
self._changeState(self.STATE_LOCKED)
2015-10-22 13:21:52 +00:00
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):
if self.isOperational():
errorMsg = "Machine reset."
self._cmd = None
self._acc_line_buffer = []
self._pauseWaitStartTime = None
self._pauseWaitTimeLost = 0.0
2015-10-27 12:33:39 +00:00
self._send_event.clear(completely=True)
2015-10-22 13:21:52 +00:00
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:
self._onConnected(self.STATE_LOCKED)
2015-10-19 12:00:52 +00:00
2015-10-20 14:21:01 +00:00
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)
2015-10-19 12:00:52 +00:00
self._status_timer.start()
2015-10-28 10:18:28 +00:00
elif newState == self.STATE_OPERATIONAL:
if self._status_timer is not None:
self._status_timer.cancel()
2015-10-19 12:00:52 +00:00
self._status_timer = RepeatedTimer(2, self._poll_status)
self._status_timer.start()
2015-10-28 10:18:28 +00:00
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)
2015-10-22 13:21:52 +00:00
if not self.sending_thread.isAlive():
self.sending_thread.start()
2015-10-21 13:57:34 +00:00
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):
2015-10-19 12:00:52 +00:00
if self.isOperational():
self._real_time_commands['poll_status']=True
2015-10-27 12:33:39 +00:00
self._send_event.set()
2015-10-22 13:21:52 +00:00
def _soft_reset(self):
if self.isOperational():
self._real_time_commands['soft_reset']=True
2015-10-27 12:33:39 +00:00
self._send_event.set()
2015-10-22 13:21:52 +00:00
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 is False:
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))
2015-10-20 14:21:01 +00:00
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
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' % temp)
def _set_intensity_override(self, value):
temp = value / 100.0
if temp >= 0:
self._intensity_factor = temp
if self._actual_intensity is not None:
temp = round(self._actual_intensity * self._intensity_factor)
if temp > 1000:
temp = 1000
self.sendCommand('S%d' % 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()]
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
2015-11-13 14:23:45 +00:00
elif new_feedrate < 30:
new_feedrate = 30
else:
return cmd
return cmd.replace(feedrate_cmd, 'F%d' % 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()]
self._actual_intensity = int(intensity_cmd[1:])
new_intensity = round(self._actual_intensity * self._intensity_factor)
if new_intensity > 1000:
new_intensity = 1000
else:
return cmd
return cmd.replace(intensity_cmd, 'S%d' % 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
2015-10-20 14:21:01 +00:00
def _gcode_H_sent(self, cmd, cmd_type=None):
self._changeState(self.STATE_HOMING)
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
2015-10-19 12:00:52 +00:00
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
2015-10-20 14:21:01 +00:00
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:
2015-10-27 12:33:39 +00:00
data = specialcmd[18:]
2015-10-20 14:21:01 +00:00
try:
2015-10-27 12:33:39 +00:00
frequency = float(data)
2015-10-20 14:21:01 +00:00
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))
2015-10-20 14:21:01 +00:00
else:
self._log("Command not Found! %s" % cmd)
self._log("available commands are:")
self._log(" /togglestatusreport")
self._log(" /setstatusfrequency <Inteval Sec>")
self._log(" /disconnect")
return
2015-10-19 12:00:52 +00:00
eepromCmd = re.search("^\$[0-9]+=.+$", cmd)
if(eepromCmd and self.isPrinting()):
self._log("Warning: Configuration changes during print are not allowed!")
2015-10-20 14:21:01 +00:00
self._commandQueue.put(cmd)
2015-10-27 12:33:39 +00:00
self._send_event.set()
2015-10-19 12:00:52 +00:00
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)
2015-10-20 14:21:01 +00:00
def startPrint(self):
2015-10-22 13:21:52 +00:00
if not self.isOperational():
2015-10-20 14:21:01 +00:00
return
if self._currentFile is None:
raise ValueError("No file selected for printing")
try:
# ensure fan is on whatever gcode follows.
self.sendCommand("M08")
self._currentFile.start()
2015-10-22 13:21:52 +00:00
self._finished_currentFile = False
2015-10-20 14:21:01 +00:00
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()})
2015-10-20 16:03:47 +00:00
def cancelPrint(self):
if not self.isOperational():
return
2015-10-21 12:55:54 +00:00
with self._commandQueue.mutex:
self._commandQueue.queue.clear()
2015-10-22 13:21:52 +00:00
self._soft_reset()
2015-10-23 08:36:27 +00:00
self._acc_line_buffer = []
2015-10-27 12:33:39 +00:00
self._send_event.clear(completely=True)
2015-10-22 13:21:52 +00:00
self._changeState(self.STATE_LOCKED)
2015-10-20 16:03:47 +00:00
payload = {
"file": self._currentFile.getFilename(),
"filename": os.path.basename(self._currentFile.getFilename()),
"origin": self._currentFile.getFileLocation()
}
eventManager().fire(Events.PRINT_CANCELLED, payload)
2015-10-20 16:12:30 +00:00
def setPause(self, pause):
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()
2015-10-21 13:57:34 +00:00
self._real_time_commands['cycle_start']=True
2015-10-27 12:33:39 +00:00
self._send_event.set()
2015-10-20 16:12:30 +00:00
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()
2015-10-21 13:57:34 +00:00
self._real_time_commands['feed_hold']=True
2015-10-27 12:33:39 +00:00
self._send_event.set()
2015-10-20 16:12:30 +00:00
eventManager().fire(Events.PRINT_PAUSED, payload)
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)
2015-10-22 13:21:52 +00:00
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
2015-10-19 12:00:52 +00:00
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
2015-10-19 12:00:52 +00:00
def isLocked(self):
return self._state == self.STATE_LOCKED
def isHoming(self):
return self._state == self.STATE_HOMING
2015-10-20 14:21:01 +00:00
def isFlashing(self):
return self._state == self.STATE_FLASHING
2015-10-19 12:00:52 +00:00
def isBusy(self):
return self.isPrinting() or self.isPaused()
2015-10-20 14:21:01 +00:00
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
2015-10-19 12:00:52 +00:00
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
2015-10-19 12:36:03 +00:00
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
2015-10-19 12:00:52 +00:00
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._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 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 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
2015-10-19 12:00:52 +00:00
def process_gcode_line(line):
line = strip_comment(line).strip()
2015-10-20 14:21:01 +00:00
line = line.replace(" ", "")
if not len(line):
2015-10-19 12:00:52 +00:00
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:
2015-10-20 14:21:01 +00:00
return settings().getFloat(["serial", "timeout", t])
def serialList():
baselist = [glob.glob("/dev/ttyUSB*"),
2015-10-17 14:14:55 +00:00
glob.glob("/dev/ttyACM*"),
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