a lot of changes
This commit is contained in:
parent
0c9ddbfd85
commit
796fbeacdd
1 changed files with 268 additions and 72 deletions
|
|
@ -11,6 +11,7 @@ import glob
|
|||
import time
|
||||
import serial
|
||||
import re
|
||||
import Queue
|
||||
|
||||
from yaml import load as yamlload
|
||||
from yaml import dump as yamldump
|
||||
|
|
@ -18,8 +19,8 @@ from subprocess import call as subprocesscall
|
|||
|
||||
import octoprint.plugin
|
||||
|
||||
from octoprint.events import eventManager, Events
|
||||
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
|
||||
|
||||
|
|
@ -45,6 +46,8 @@ class MachineCom(object):
|
|||
|
||||
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:
|
||||
|
|
@ -67,9 +70,13 @@ class MachineCom(object):
|
|||
self._currentFile = None
|
||||
self._status_timer = None
|
||||
self._acc_line_buffer = []
|
||||
self._cmd = None
|
||||
self._pauseWaitTimeLost = 0.0
|
||||
self._send_event = threading.Event()
|
||||
self._send_event.clear()
|
||||
self._commandQueue = Queue.Queue()
|
||||
self._send_event = CountedEvent(max=20)
|
||||
|
||||
# regular expressions
|
||||
self._regex_command = re.compile("^\s*\$?([GM]\d+|[TH])")
|
||||
|
||||
self._real_time_commands={'poll_status':False,
|
||||
'feed_hold':False,
|
||||
|
|
@ -81,10 +88,18 @@ class MachineCom(object):
|
|||
self._serial_factory_hooks = self._pluginManager.get_hooks("octoprint.comm.transport.serial.factory")
|
||||
|
||||
self._state_parse_dict = {self.STATE_NONE:self._state_none_handle,
|
||||
self.STATE_CONNECTING:self._state_connecting_handle,
|
||||
self.STATE_LOCKED:self._state_locked_handle,
|
||||
self.STATE_HOMING:self._state_homing_handle,
|
||||
self.STATE_OPERATIONAL:self._state_operational_handle}
|
||||
self.STATE_OPEN_SERIAL:self._state_none_handle,
|
||||
self.STATE_DETECT_SERIAL:self._state_none_handle,
|
||||
self.STATE_CONNECTING:self._state_connecting_handle,
|
||||
self.STATE_OPERATIONAL:self._state_operational_handle,
|
||||
self.STATE_PRINTING:self._state_printing_handle,
|
||||
self.STATE_PAUSED:self._state_none_handle,
|
||||
self.STATE_CLOSED:self._state_none_handle,
|
||||
self.STATE_ERROR:self._state_none_handle,
|
||||
self.STATE_CLOSED_WITH_ERROR:self._state_none_handle,
|
||||
self.STATE_LOCKED:self._state_locked_handle,
|
||||
self.STATE_HOMING:self._state_homing_handle,
|
||||
self.STATE_FLASHING:self._state_none_handle}
|
||||
|
||||
# monitoring thread
|
||||
self._monitoring_active = True
|
||||
|
|
@ -99,8 +114,6 @@ class MachineCom(object):
|
|||
self.sending_thread.start()
|
||||
|
||||
def _monitor_loop(self):
|
||||
pause_triggers = convert_pause_triggers(settings().get(["printerParameters", "pauseTriggers"]))
|
||||
|
||||
#Open the serial port.
|
||||
if not self._openSerial():
|
||||
return
|
||||
|
|
@ -109,8 +122,6 @@ class MachineCom(object):
|
|||
self._changeState(self.STATE_CONNECTING)
|
||||
self._timeout = get_new_timeout("communication")
|
||||
|
||||
supportWait = settings().getBoolean(["feature", "supportWait"])
|
||||
|
||||
while self._monitoring_active:
|
||||
try:
|
||||
line = self._readline()
|
||||
|
|
@ -119,8 +130,7 @@ class MachineCom(object):
|
|||
if line.strip() is not "":
|
||||
self._timeout = get_new_timeout("communication")
|
||||
# parse line depending on state
|
||||
self._state_parse_dict[self._state](self, line)
|
||||
return
|
||||
self._state_parse_dict[self._state](line)
|
||||
except:
|
||||
self._logger.exception("Something crashed inside the monitoring loop, please report this to Mr. Beam")
|
||||
errorMsg = "See octoprint.log for details"
|
||||
|
|
@ -138,21 +148,28 @@ class MachineCom(object):
|
|||
while self._sending_active:
|
||||
try:
|
||||
if self._real_time_commands['poll_status']:
|
||||
self._sendCommand('?')
|
||||
self.sendCommand('?')
|
||||
self._real_time_commands['poll_status']=False
|
||||
elif self._real_time_commands['feed_hold']:
|
||||
self._sendCommand('!')
|
||||
self.sendCommand('!')
|
||||
self._real_time_commands['feed_hold']=False
|
||||
elif self._real_time_commands['cycle_start']:
|
||||
self._sendCommand('~')
|
||||
self.sendCommand('~')
|
||||
self._real_time_commands['cycle_start']=False
|
||||
elif self._real_time_commands['soft_reset']:
|
||||
self._sendCommand(b'\x18')
|
||||
self.sendCommand(b'\x18')
|
||||
self._real_time_commands['soft_reset']=False
|
||||
elif self.isOperational() or self.isPaused():
|
||||
pass # TODO send buffered command
|
||||
elif self.isPrinting():
|
||||
self._sendCommand(self._getNext())
|
||||
#else:
|
||||
#if not self._commandQueue.empty() and not self.isHoming():
|
||||
# self._sendCommand(self._commandQueue.get())
|
||||
#elif self.isPrinting():
|
||||
# self._sendCommand(self._getNext())
|
||||
if self.isPrinting() and self._commandQueue.empty():
|
||||
cmd = self._getNext()
|
||||
if cmd is not None:
|
||||
self.sendCommand(cmd)
|
||||
|
||||
self._sendCommand()
|
||||
self._send_event.wait(1)
|
||||
self._send_event.clear()
|
||||
except:
|
||||
|
|
@ -163,12 +180,19 @@ class MachineCom(object):
|
|||
self._changeState(self.STATE_ERROR)
|
||||
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
|
||||
|
||||
def _sendCommand(self, cmd):
|
||||
if sum([len(x) for x in self._acc_line_buffer]) + len(cmd) +1 < self.RX_BUFFER_SIZE:
|
||||
self._log("Send: %s" % cmd)
|
||||
self._acc_line_buffer.append(cmd)
|
||||
def _sendCommand(self):
|
||||
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-10:
|
||||
self._log("Send: %s" % self._cmd)
|
||||
self._acc_line_buffer.append(self._cmd)
|
||||
try:
|
||||
self._serial.write(cmd + '\n')
|
||||
self._serial.write(self._cmd + '\n')
|
||||
# trigger "sent" phase and use up one "ok"
|
||||
self._process_command_phase("sent", self._cmd)
|
||||
self._cmd = None
|
||||
except serial.SerialException:
|
||||
self._log("Unexpected error while writing serial port: %s" % (get_exception_string()))
|
||||
self._errorValue = get_exception_string()
|
||||
|
|
@ -215,8 +239,8 @@ class MachineCom(object):
|
|||
return False
|
||||
if serial_obj is not None:
|
||||
# first hook to succeed wins, but any can pass on to the next
|
||||
self._changeState(self.STATE_OPEN_SERIAL)
|
||||
self._log(repr(self._serial))
|
||||
self._changeState(self.STATE_OPEN_SERIAL)
|
||||
self._serial = serial_obj
|
||||
return True
|
||||
return False
|
||||
|
|
@ -227,9 +251,9 @@ class MachineCom(object):
|
|||
try:
|
||||
ret = self._serial.readline()
|
||||
if('ok' in ret or 'error' in ret):
|
||||
if(len(self.acc_line_lengths) > 0):
|
||||
if(len(self._acc_line_buffer) > 0):
|
||||
#print('buffer',sum(self.acc_line_lengths), 'deleting after ok', self.acc_line_lengths[0])
|
||||
del self.acc_line_lengths[0] # Delete the commands character count corresponding to the last 'ok'
|
||||
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()
|
||||
|
|
@ -266,12 +290,12 @@ class MachineCom(object):
|
|||
|
||||
def _state_connecting_handle(self, line):
|
||||
if line.startswith("Grbl"):
|
||||
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()
|
||||
#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()
|
||||
self._send_event.set()
|
||||
self._onConnected(self.STATE_LOCKED)
|
||||
|
||||
|
|
@ -283,7 +307,96 @@ class MachineCom(object):
|
|||
self._changeState(self.STATE_OPERATIONAL)
|
||||
|
||||
def _state_operational_handle(self, line):
|
||||
pass
|
||||
if line.startswith("ok"):
|
||||
self._send_event.set()
|
||||
elif line.startswith("<"):
|
||||
self._update_grbl_pos(line)
|
||||
|
||||
def _state_printing_handle(self, line):
|
||||
if line.startswith("ok"):
|
||||
self._send_event.set()
|
||||
|
||||
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):
|
||||
|
|
@ -339,6 +452,7 @@ class MachineCom(object):
|
|||
def _poll_status(self):
|
||||
if self.isOperational():
|
||||
self._real_time_commands['poll_status']=True
|
||||
self._send_event.set()
|
||||
|
||||
def _log(self, message):
|
||||
self._callback.on_comm_log(message)
|
||||
|
|
@ -396,6 +510,47 @@ class MachineCom(object):
|
|||
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
|
||||
|
||||
##~~ command handlers
|
||||
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
|
||||
|
||||
def sendCommand(self, cmd, cmd_type=None, processed=False):
|
||||
cmd = cmd.encode('ascii', 'replace')
|
||||
if not processed:
|
||||
|
|
@ -403,45 +558,42 @@ class MachineCom(object):
|
|||
if not cmd:
|
||||
return
|
||||
|
||||
# if cmd[0] == "/":
|
||||
# specialcmd = cmd[1:].lower()
|
||||
# if "togglestatusreport" in specialcmd:
|
||||
# if self._temperature_timer is None:
|
||||
# self._temperature_timer = RepeatedTimer(1, self._poll_temperature, run_first=True)
|
||||
# self._temperature_timer.start()
|
||||
# else:
|
||||
# self._temperature_timer.cancel()
|
||||
# self._temperature_timer = None
|
||||
# elif "setstatusfrequency" in specialcmd:
|
||||
# data = specialcmd.split(' ')
|
||||
# try:
|
||||
# frequency = float(data[1])
|
||||
# except ValueError:
|
||||
# self._log("No frequency setting found! Using 1 sec.")
|
||||
# frequency = 1
|
||||
# if self._temperature_timer is not None:
|
||||
# self._temperature_timer.cancel()
|
||||
#
|
||||
# self._temperature_timer = RepeatedTimer(frequency, self._poll_temperature, run_first=True)
|
||||
# self._temperature_timer.start()
|
||||
# elif "disconnect" in specialcmd:
|
||||
# self.close()
|
||||
# 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
|
||||
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.split(' ')
|
||||
try:
|
||||
frequency = float(data[1])
|
||||
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()
|
||||
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
|
||||
|
||||
eepromCmd = re.search("^\$[0-9]+=.+$", cmd)
|
||||
if(eepromCmd and self.isPrinting()):
|
||||
self._log("Warning: Configuration changes during print are not allowed!")
|
||||
|
||||
if self.isPrinting():
|
||||
self._commandQueue.put((cmd, cmd_type))
|
||||
elif self.isOperational() or self.isLocked() or self.isHoming():
|
||||
self._sendCommand(cmd, cmd_type=cmd_type)
|
||||
self._commandQueue.put(cmd)
|
||||
self._send_event.set()
|
||||
|
||||
def selectFile(self, filename, sd):
|
||||
if self.isBusy():
|
||||
|
|
@ -455,6 +607,34 @@ class MachineCom(object):
|
|||
})
|
||||
self._callback.on_comm_file_selected(filename, self._currentFile.getFilesize(), False)
|
||||
|
||||
def startPrint(self):
|
||||
if not self.isOperational() or self.isPrinting():
|
||||
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()
|
||||
|
||||
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 getStateString(self):
|
||||
if self._state == self.STATE_NONE:
|
||||
return "Offline"
|
||||
|
|
@ -502,9 +682,24 @@ class MachineCom(object):
|
|||
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
|
||||
|
||||
|
|
@ -744,7 +939,8 @@ def convert_pause_triggers(configured_triggers):
|
|||
|
||||
def process_gcode_line(line):
|
||||
line = strip_comment(line).strip()
|
||||
if len(line):
|
||||
line = line.replace(" ", "")
|
||||
if not len(line):
|
||||
return None
|
||||
return line
|
||||
|
||||
|
|
@ -770,7 +966,7 @@ def get_interval(t):
|
|||
if t not in default_settings["serial"]["timeout"]:
|
||||
return 0
|
||||
else:
|
||||
return settings().getFloat(["serial", "timeout", type])
|
||||
return settings().getFloat(["serial", "timeout", t])
|
||||
|
||||
def serialList():
|
||||
baselist = [glob.glob("/dev/ttyUSB*"),
|
||||
|
|
|
|||
Loading…
Reference in a new issue