paneldue: execute commands synchronously

This prevents gcode requests from blocking temperature updates, accessing the file list, etc.

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
Arksine 2020-12-10 11:28:43 -05:00
parent 70501772c1
commit eea2abd60e
1 changed files with 65 additions and 72 deletions

View File

@ -13,7 +13,6 @@ from collections import deque
from utils import ServerError from utils import ServerError
from tornado import gen from tornado import gen
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
from tornado.locks import Lock
MIN_EST_TIME = 10. MIN_EST_TIME = 10.
INITIALIZE_TIMEOUT = 10. INITIALIZE_TIMEOUT = 10.
@ -30,12 +29,11 @@ class SerialConnection:
self.paneldue = paneldue self.paneldue = paneldue
self.port = config.get('serial') self.port = config.get('serial')
self.baud = config.getint('baud', 57600) self.baud = config.getint('baud', 57600)
self.sendlock = Lock()
self.partial_input = b"" self.partial_input = b""
self.ser = self.fd = None self.ser = self.fd = None
self.connected = False self.connected = False
self.busy = False self.send_busy = False
self.pending_lines = [] self.send_buffer = b""
self.attempting_connect = True self.attempting_connect = True
self.ioloop.spawn_callback(self._connect) self.ioloop.spawn_callback(self._connect)
@ -48,6 +46,7 @@ class SerialConnection:
self.ser.close() self.ser.close()
self.ser = None self.ser = None
self.partial_input = b"" self.partial_input = b""
self.send_buffer = b""
self.paneldue.initialized = False self.paneldue.initialized = False
logging.info("PanelDue Disconnected") logging.info("PanelDue Disconnected")
if reconnect and not self.attempting_connect: if reconnect and not self.attempting_connect:
@ -98,22 +97,14 @@ class SerialConnection:
return return
# Remove null bytes, separate into lines # Remove null bytes, separate into lines
data = data.strip(b'\x00\xFF') data = data.strip(b'\x00')
lines = data.split(b'\n') lines = data.split(b'\n')
lines[0] = self.partial_input + lines[0] lines[0] = self.partial_input + lines[0]
self.partial_input = lines.pop() self.partial_input = lines.pop()
self.pending_lines.extend(lines) for line in lines:
if self.busy:
return
self.busy = True
self.ioloop.spawn_callback(self._process_commands)
async def _process_commands(self):
while self.pending_lines:
line = self.pending_lines.pop(0)
line = line.strip().decode()
try: try:
await self.paneldue.process_line(line) line = line.strip().decode('utf-8', 'ignore')
self.paneldue.process_line(line)
except ServerError: except ServerError:
logging.exception( logging.exception(
f"GCode Processing Error: {line}") f"GCode Processing Error: {line}")
@ -121,28 +112,31 @@ class SerialConnection:
f"!! GCode Processing Error: {line}") f"!! GCode Processing Error: {line}")
except Exception: except Exception:
logging.exception("Error during gcode processing") logging.exception("Error during gcode processing")
self.busy = False
async def send(self, data): def send(self, data):
if self.connected: self.send_buffer += data
async with self.sendlock: if not self.send_busy:
while data: self.send_busy = True
try: self.ioloop.spawn_callback(self._do_send)
sent = os.write(self.fd, data)
except os.error as e:
if e.errno == errno.EBADF or e.errno == errno.EPIPE:
sent = 0
else:
await gen.sleep(.001)
continue
if sent:
data = data[sent:]
else:
logging.exception(
"Error writing data, closing serial connection")
self.disconnect(reconnect=True)
return
async def _do_send(self):
while self.send_buffer:
try:
sent = os.write(self.fd, self.send_buffer)
except os.error as e:
if e.errno == errno.EBADF or e.errno == errno.EPIPE:
sent = 0
else:
await gen.sleep(.001)
continue
if sent:
self.send_buffer = self.send_buffer[sent:]
else:
logging.exception(
"Error writing data, closing serial connection")
self.disconnect(reconnect=True)
return
self.send_busy = False
class PanelDue: class PanelDue:
def __init__(self, config): def __init__(self, config):
@ -171,7 +165,7 @@ class PanelDue:
self.is_ready = False self.is_ready = False
self.is_shutdown = False self.is_shutdown = False
self.initialized = False self.initialized = False
self.last_printer_state = 'C' self.last_printer_state = 'O'
self.last_update_time = 0. self.last_update_time = 0.
# Set up macros # Set up macros
@ -296,31 +290,32 @@ class PanelDue:
self.is_shutdown = False self.is_shutdown = False
self.is_ready = True self.is_ready = True
async def _process_klippy_shutdown(self): def _process_klippy_shutdown(self):
self.is_shutdown = True self.is_shutdown = True
async def _process_klippy_disconnect(self): def _process_klippy_disconnect(self):
# Tell the PD that we are shutting down # Tell the PD that the printer is "off"
await self.write_response({'status': 'S'}) self.write_response({'status': 'O'})
self.is_ready = False self.last_printer_state = 'O'
self.is_shutdown = self.is_shutdown = False
async def handle_status_update(self, status): def handle_status_update(self, status):
for obj, items in status.items(): for obj, items in status.items():
if obj in self.printer_state: if obj in self.printer_state:
self.printer_state[obj].update(items) self.printer_state[obj].update(items)
else: else:
self.printer_state[obj] = items self.printer_state[obj] = items
async def paneldue_beep(self, frequency, duration): def paneldue_beep(self, frequency, duration):
duration = int(duration * 1000.) duration = int(duration * 1000.)
await self.write_response( self.write_response(
{'beep_freq': frequency, 'beep_length': duration}) {'beep_freq': frequency, 'beep_length': duration})
async def process_line(self, line): def process_line(self, line):
self.debug_queue.append(line) self.debug_queue.append(line)
# If we find M112 in the line then skip verification # If we find M112 in the line then skip verification
if "M112" in line.upper(): if "M112" in line.upper():
await self.klippy_apis.emergency_stop() self.ioloop.spawn_callback(self.klippy_apis.emergency_stop)
return return
if self.enable_checksum: if self.enable_checksum:
@ -356,11 +351,11 @@ class PanelDue:
logging.info("PanelDue: " + msg) logging.info("PanelDue: " + msg)
raise PanelDueError(msg) raise PanelDueError(msg)
await self._run_gcode(line[line_index+1:cs_index]) self._run_gcode(line[line_index+1:cs_index])
else: else:
await self._run_gcode(line) self._run_gcode(line)
async def _run_gcode(self, script): def _run_gcode(self, script):
# Execute the gcode. Check for special RRF gcodes that # Execute the gcode. Check for special RRF gcodes that
# require special handling # require special handling
parts = script.split() parts = script.split()
@ -381,7 +376,7 @@ class PanelDue:
return return
params["arg_" + arg] = val params["arg_" + arg] = val
func = self.direct_gcodes[cmd] func = self.direct_gcodes[cmd]
await func(**params) func(**params)
return return
# Prepare GCodes that require special handling # Prepare GCodes that require special handling
@ -392,9 +387,11 @@ class PanelDue:
if not script: if not script:
return return
elif script in RESTART_GCODES: elif script in RESTART_GCODES:
await self.klippy_apis.do_restart(script) self.ioloop.spawn_callback(self.klippy_apis.do_restart, script)
return return
self.ioloop.spawn_callback(self._send_klippy_gcode, script)
async def _send_klippy_gcode(self, script):
try: try:
await self.klippy_apis.run_gcode(script) await self.klippy_apis.run_gcode(script)
except self.server.error: except self.server.error:
@ -466,7 +463,7 @@ class PanelDue:
mbox['msgBox.title'] = title mbox['msgBox.title'] = title
mbox['msgBox.controls'] = 0 mbox['msgBox.controls'] = 0
mbox['msgBox.timeout'] = 0 mbox['msgBox.timeout'] = 0
self.ioloop.spawn_callback(self.write_response, mbox) self.write_response(mbox)
def handle_gcode_response(self, response): def handle_gcode_response(self, response):
# Only queue up "non-trivial" gcode responses. At the # Only queue up "non-trivial" gcode responses. At the
@ -480,9 +477,9 @@ class PanelDue:
self.last_gcode_response = response self.last_gcode_response = response
return return
async def write_response(self, response): def write_response(self, response):
byte_resp = json.dumps(response) + "\r\n" byte_resp = json.dumps(response) + "\r\n"
await self.ser_conn.send(byte_resp.encode()) self.ser_conn.send(byte_resp.encode())
def _get_printer_status(self): def _get_printer_status(self):
# PanelDue States applicable to Klipper: # PanelDue States applicable to Klipper:
@ -516,7 +513,7 @@ class PanelDue:
return 'I' return 'I'
async def _run_paneldue_M408(self, arg_r=None, arg_s=1): def _run_paneldue_M408(self, arg_r=None, arg_s=1):
response = {} response = {}
sequence = arg_r sequence = arg_r
response_type = arg_s response_type = arg_s
@ -530,19 +527,16 @@ class PanelDue:
response['dir'] = "/macros" response['dir'] = "/macros"
response['files'] = list(self.available_macros.keys()) response['files'] = list(self.available_macros.keys())
self.initialized = True self.initialized = True
if not self.is_ready: if not self.is_ready:
self.last_printer_state = 'S' if self.is_shutdown else 'C' self.last_printer_state = 'O'
response['status'] = self.last_printer_state response['status'] = self.last_printer_state
await self.write_response(response) self.write_response(response)
return return
# Send gcode responses
if sequence is not None and self.last_gcode_response: if sequence is not None and self.last_gcode_response:
# Send gcode responses
response['seq'] = sequence + 1 response['seq'] = sequence + 1
response['resp'] = self.last_gcode_response response['resp'] = self.last_gcode_response
self.last_gcode_response = None self.last_gcode_response = None
if response_type == 1: if response_type == 1:
# Extended response Request # Extended response Request
response['myName'] = self.machine_name response['myName'] = self.machine_name
@ -642,9 +636,9 @@ class PanelDue:
# is strange about this, and displays it as a full screen # is strange about this, and displays it as a full screen
# notification # notification
self.last_message = msg self.last_message = msg
await self.write_response(response) self.write_response(response)
async def _run_paneldue_M20(self, arg_p, arg_s=0): def _run_paneldue_M20(self, arg_p, arg_s=0):
response_type = arg_s response_type = arg_s
if response_type != 2: if response_type != 2:
logging.info( logging.info(
@ -682,9 +676,9 @@ class PanelDue:
flist = self.file_manager.list_dir(path, simple_format=True) flist = self.file_manager.list_dir(path, simple_format=True)
if flist: if flist:
response['files'] = flist response['files'] = flist
await self.write_response(response) self.write_response(response)
async def _run_paneldue_M30(self, arg_p=None): def _run_paneldue_M30(self, arg_p=None):
# Delete a file. Clean up the file name and make sure # Delete a file. Clean up the file name and make sure
# it is relative to the "gcodes" root. # it is relative to the "gcodes" root.
path = arg_p path = arg_p
@ -696,9 +690,9 @@ class PanelDue:
if not path.startswith("gcodes/"): if not path.startswith("gcodes/"):
path = "gcodes/" + path path = "gcodes/" + path
await self.file_manager.delete_file(path) self.ioloop.spawn_callback(self.file_manager.delete_file, path)
async def _run_paneldue_M36(self, arg_p=None): def _run_paneldue_M36(self, arg_p=None):
response = {} response = {}
filename = arg_p filename = arg_p
sd_status = self.printer_state.get('virtual_sdcard', {}) sd_status = self.printer_state.get('virtual_sdcard', {})
@ -713,12 +707,11 @@ class PanelDue:
if not filename or not active: if not filename or not active:
# Either no file printing or no virtual_sdcard # Either no file printing or no virtual_sdcard
response['err'] = 1 response['err'] = 1
await self.write_response(response) self.write_response(response)
return return
else: else:
response['fileName'] = filename.split("/")[-1] response['fileName'] = filename.split("/")[-1]
# For consistency make sure that the filename begins with the # For consistency make sure that the filename begins with the
# "gcodes/" root. The M20 HACK should add this in some cases. # "gcodes/" root. The M20 HACK should add this in some cases.
# Ideally we would add support to the PanelDue firmware that # Ideally we would add support to the PanelDue firmware that
@ -749,9 +742,9 @@ class PanelDue:
response['printTime'] = int(est_time + .5) response['printTime'] = int(est_time + .5)
else: else:
response['err'] = 1 response['err'] = 1
await self.write_response(response) self.write_response(response)
async def close(self): def close(self):
self.ser_conn.disconnect() self.ser_conn.disconnect()
msg = "\nPanelDue GCode Dump:" msg = "\nPanelDue GCode Dump:"
for i, gc in enumerate(self.debug_queue): for i, gc in enumerate(self.debug_queue):