diff --git a/bCNC/Sender.py b/bCNC/Sender.py index f54f0d04d..55d6ce31e 100644 --- a/bCNC/Sender.py +++ b/bCNC/Sender.py @@ -4,6 +4,8 @@ # Email: vvlachoudis@gmail.com # Date: 17-Jun-2015 +import asyncio +import functools import glob import os import re @@ -67,7 +69,6 @@ NOT_CONNECTED: "OrangeRed", } - # ============================================================================= # bCNC Sender class # ============================================================================= @@ -81,6 +82,8 @@ class Sender: MSG_ERROR = 4 # error message or exception MSG_RUNEND = 5 # run ended MSG_CLEAR = 6 # clear buffer + SERIAL_IO_THREAD_NAME = 'serialIO' + MAIN_THREAD_NAME = 'main' def __init__(self): # Global variables @@ -98,6 +101,8 @@ def __init__(self): self.log = Queue() # Log queue returned from GRBL self.queue = Queue() # Command queue to be send to GRBL self.pendant = Queue() # Command queue to be executed from Pendant + self.context = threading.local() + self.context.name = self.MAIN_THREAD_NAME self.serial = None self.thread = None @@ -121,6 +126,12 @@ def __init__(self): self._onStart = "" self._onStop = "" + + self.loop = asyncio.new_event_loop() + self.resetCondition = None + self.bufferSyncEvent = None + self.resetLock =None + self.idleFunction = None # ---------------------------------------------------------------------- def controllerLoad(self): @@ -515,7 +526,7 @@ def open(self, device, baudrate): self.mcontrol.initController() self._gcount = 0 self._alarm = True - self.thread = threading.Thread(target=self.serialIO) + self.thread = threading.Thread(target=self.serialIOWrapper) self.thread.start() return True @@ -530,8 +541,9 @@ def close(self): except Exception: pass self._runLines = 0 + self.thread = None - time.sleep(1) + try: self.serial.close() except Exception: @@ -565,7 +577,9 @@ def hardReset(self): self.mcontrol.hardReset() def softReset(self, clearAlarm=True): - self.mcontrol.softReset(clearAlarm) + if not self.loop.is_running(): + return + self.scheduleCoroutine(self.mcontrol.softReset(clearAlarm)) def unlock(self, clearAlarm=True): self.mcontrol.unlock(clearAlarm) @@ -619,7 +633,9 @@ def pause(self, event=None): self.mcontrol.pause(event) def purgeController(self): - self.mcontrol.purgeController() + if not self.loop.is_running(): + return + self.scheduleCoroutine(self.mcontrol.purgeController()) def g28Command(self): self.sendGCode("G28.1") # FIXME: ??? @@ -689,16 +705,16 @@ def stopRun(self, event=None): # So we can purge the controller for the next job # See https://github.com/vlachoudis/bCNC/issues/1035 # ---------------------------------------------------------------------- - def jobDone(self): + async def jobDone(self): print(f"Job done. Purging the controller. (Running: {self.running})") - self.purgeController() + asyncio.create_task(self.mcontrol.purgeController()) # ---------------------------------------------------------------------- # This is called everytime that motion controller changes the state # YOU SHOULD PASS ONLY REAL HW STATE TO THIS, NOT BCNC STATE # Right now the primary idea of this is to detect when job stopped running # ---------------------------------------------------------------------- - def controllerStateChange(self, state): + async def controllerStateChange(self, state): print( f"Controller state changed to: {state} (Running: {self.running})") if state in ("Idle"): @@ -709,26 +725,60 @@ def controllerStateChange(self, state): and self.running is False and state in ("Idle")): self.cleanAfter = False - self.jobDone() + await self.jobDone() # ---------------------------------------------------------------------- # thread performing I/O on serial line # ---------------------------------------------------------------------- - def serialIO(self): + def scheduleCoroutine(self, coro): + if self.context.name in self.SERIAL_IO_THREAD_NAME: + asyncio.create_task(coro) + else: + task = asyncio.run_coroutine_threadsafe(coro, self.loop) + while not task.done(): + if self.idleFunction: + self.idleFunction() + else: + time.sleep(0.1) + + def serialIOWrapper(self): + self.context.name = self.SERIAL_IO_THREAD_NAME + asyncio.set_event_loop(self.loop) + self.loop.run_until_complete(self.serialIO()) + for task in asyncio.all_tasks(self.loop): + task.cancel() + + async def triggerBufferSync(self): + if self.bufferSyncEvent: + self.bufferSyncEvent.set() + + async def serialIO(self): # wait for commands to complete (status change to Idle) self.sio_wait = False self.sio_status = False # waiting for status <...> report - cline = [] # length of pipeline commands - sline = [] # pipeline commands - tosend = None # next string to send - tr = tg = time.time() # last time a ? or $G was send to grbl - + self.context.cline = [] # length of pipeline commands + self.context.sline = [] # pipeline commands + self.context.tosend = None # next string to send + self.context.tr = self.context.tg = time.time() # last time a ? or $G was send to grbl + self.resetCondition = asyncio.Condition() + self.resetLock = asyncio.Lock() while self.thread: + await self.writeSerialI0() + if not self.thread: + return + await self.readSerialI0() + await asyncio.sleep(0.01) + + self.resetCondition = None + self.resetLock = None + + async def writeSerialI0(self): + try: t = time.time() # refresh machine position? - if t - tr > SERIAL_POLL: + if t - self.context.tr > SERIAL_POLL: self.mcontrol.viewStatusReport() - tr = t + self.context.tr = t # If Override change, attach feed if CNC.vars["_OvChanged"]: @@ -736,38 +786,38 @@ def serialIO(self): # Fetch new command to send if... if ( - tosend is None + self.context.tosend is None and not self.sio_wait and not self._pause and self.queue.qsize() > 0 ): try: - tosend = self.queue.get_nowait() - if isinstance(tosend, tuple): + self.context.tosend = self.queue.get_nowait() + if isinstance(self.context.tosend, tuple): # wait to empty the grbl buffer and status is Idle - if tosend[0] == WAIT: + if self.context.tosend[0] == WAIT: # Don't count WAIT until we are idle! self.sio_wait = True - elif tosend[0] == MSG: + elif self.context.tosend[0] == MSG: # Count executed commands as well self._gcount += 1 - if tosend[1] is not None: + if self.context.tosend[1] is not None: # show our message on machine status - self._msg = tosend[1] - elif tosend[0] == UPDATE: + self._msg = self.context.tosend[1] + elif self.context.tosend[0] == UPDATE: # Count executed commands as well self._gcount += 1 - self._update = tosend[1] + self._update = self.context.tosend[1] else: # Count executed commands as well self._gcount += 1 - tosend = None + self.context.tosend = None - elif not isinstance(tosend, str): + elif not isinstance(self.context.tosend, str): try: - tosend = self.gcode.evaluate(tosend, self) - if isinstance(tosend, str): - tosend += "\n" + self.context.tosend = self.gcode.evaluate(self.context.tosend, self) + if isinstance(self.context.tosend, str): + self.context.tosend += "\n" else: # Count executed commands as well self._gcount += 1 @@ -775,16 +825,16 @@ def serialIO(self): for s in str(sys.exc_info()[1]).splitlines(): self.log.put((Sender.MSG_ERROR, s)) self._gcount += 1 - tosend = None + self.context.tosend = None except Empty: - break + return - if tosend is not None: - # All modification in tosend should be + if self.context.tosend is not None: + # All modification in self.context.tosend should be # done before adding it to cline # Keep track of last feed - pat = FEEDPAT.match(tosend) + pat = FEEDPAT.match(self.context.tosend) if pat is not None: self._lastFeed = pat.group(2) @@ -799,16 +849,16 @@ def serialIO(self): if ( pat is None and self._newFeed != 0 - and not tosend.startswith("$") + and not self.context.tosend.startswith("$") ): - tosend = f"f{self._newFeed:g}{tosend}" + self.context.tosend = f"f{self._newFeed:g}{self.context.tosend}" # Apply override Feed if CNC.vars["_OvFeed"] != 100 and self._newFeed != 0: - pat = FEEDPAT.match(tosend) + pat = FEEDPAT.match(self.context.tosend) if pat is not None: try: - tosend = "{}f{:g}{}\n".format( + self.context.tosend = "{}f{:g}{}\n".format( pat.group(1), self._newFeed, pat.group(3), @@ -817,49 +867,68 @@ def serialIO(self): pass # Bookkeeping of the buffers - sline.append(tosend) - cline.append(len(tosend)) - - # Anything to receive? - if self.serial.inWaiting() or tosend is None: - try: - line = str(self.serial.readline().decode("ascii", "ignore")).strip() - except Exception: - self.log.put((Sender.MSG_RECEIVE, str(sys.exc_info()[1]))) - self.emptyQueue() - self.close() - return - - if not line: - pass - elif self.mcontrol.parseLine(line, cline, sline): - pass - else: - self.log.put((Sender.MSG_RECEIVE, line)) + self.context.sline.append(self.context.tosend) + self.context.cline.append(len(self.context.tosend)) # Received external message to stop if self._stop: self.emptyQueue() - tosend = None + self.context.tosend = None + self.bufferSyncEvent = asyncio.Event() self.log.put((Sender.MSG_CLEAR, "")) + await self.bufferSyncEvent.wait() + self.bufferSyncEvent = None # WARNING if runLines==maxint then it means we are # still preparing/sending lines from from bCNC.run(), # so don't stop if self._runLines != sys.maxsize: self._stop = False + async with self.resetCondition: + self.resetCondition.notify_all() - if tosend is not None and sum(cline) < RX_BUFFER_SIZE: - self._sumcline = sum(cline) + if self.context.tosend is not None and sum(self.context.cline) < RX_BUFFER_SIZE: + self._sumcline = sum(self.context.cline) if self.mcontrol.gcode_case > 0: - tosend = tosend.upper() + self.context.tosend = self.context.tosend.upper() if self.mcontrol.gcode_case < 0: - tosend = tosend.lower() - - self.serial_write(tosend) + self.context.tosend = self.context.tosend.lower() + + self.serial_write(self.context.tosend) - self.log.put((Sender.MSG_BUFFER, tosend)) + self.log.put((Sender.MSG_BUFFER, self.context.tosend)) - tosend = None - if not self.running and t - tg > G_POLL: + self.context.tosend = None + if not self.running and t - self.context.tg > G_POLL: self.mcontrol.viewState() - tg = t + self.context.tg = t + + except serial.SerialException: + self.log.put((Sender.MSG_RECEIVE, str(sys.exc_info()[1]))) + self.emptyQueue() + if self.thread: + self.event_generate("<>") + return + except Exception: + return + + + async def readSerialI0(self): + # Anything to receive? + try: + if self.serial.inWaiting() or self.context.tosend is None: + line = str(self.serial.readline().decode("ascii", "ignore")).strip() + + if not line: + pass + elif await self.mcontrol.parseLine(line, self.context.cline, self.context.sline): + pass + else: + self.log.put((Sender.MSG_RECEIVE, line)) + except serial.SerialException: + self.log.put((Sender.MSG_RECEIVE, str(sys.exc_info()[1]))) + self.emptyQueue() + if self.thread: + self.event_generate("<>") + return + except Exception: + return \ No newline at end of file diff --git a/bCNC/bmain.py b/bCNC/bmain.py index d83183773..0ec772d56 100755 --- a/bCNC/bmain.py +++ b/bCNC/bmain.py @@ -4,6 +4,7 @@ # Author: vvlachoudis@gmail.com # Date: 24-Aug-2014 +import asyncio import os import socket import sys @@ -146,6 +147,7 @@ def __init__(self, **kw): Tk.__init__(self, **kw) Sender.__init__(self) + self.idleFunction = self._monitorSerial Utils.loadIcons() tkinter.CallWrapper = Utils.CallWrapper tkExtra.bindClasses(self) @@ -2581,10 +2583,12 @@ def run(self, lines=None): self.statusbar.setProgress(0, 0) self._paths = self.gcode.compile(self.queue, self.checkStop) if self._paths is None: + self._runLines = 0 self.emptyQueue() self.purgeController() return elif not self._paths: + self._runLines = 0 self.runEnded() messagebox.showerror( _("Empty gcode"), @@ -2731,6 +2735,7 @@ def _monitorSerial(self): elif msg == Sender.MSG_CLEAR: self.buffer.delete(0, END) + asyncio.run_coroutine_threadsafe(self.triggerBufferSync(), self.loop) else: # Unknown? diff --git a/bCNC/controllers/G2Core.py b/bCNC/controllers/G2Core.py index bd6419a54..0db923996 100644 --- a/bCNC/controllers/G2Core.py +++ b/bCNC/controllers/G2Core.py @@ -38,7 +38,7 @@ def executeCommand(self, oline, line, cmd): print("ec",oline,line,cmd) return False - def softReset(self, clearAlarm=True): + async def softReset(self, clearAlarm=True): # Don't do this, it resets all the config values in firmware. # if self.master.serial: # self.master.serial_write(b"\030") @@ -83,10 +83,10 @@ def mapState(self,state): "Run", "Run", "Home", "Jog", "Door", "Alarm", "Alarm" ] return states[state] - def setState(self, stat): + async def setState(self, stat): state = self.mapState(stat) if CNC.vars["state"] != state or self.master.runningPrev != self.master.running: - self.master.controllerStateChange(state) + await self.master.controllerStateChange(state) self.master.runningPrev = self.master.running self.displayState(state) @@ -108,9 +108,9 @@ def setCNCints(self, sr, maps ): if key1 in sr: CNC.vars[key2] = int(sr[key1]) - def processStatusReport(self, sr): + async def processStatusReport(self, sr): if "stat" in sr: - self.setState(sr["stat"]) + await self.setState(sr["stat"]) self.setCNCfloats(sr, { "feed" : "curfeed", "vel":"curvel", "posx" : "wx", @@ -154,9 +154,9 @@ def processFooter(self,f): revision, status, lines_available = f # self.setState(status) NO, THIS IS A DIFFERENT STATUS. - def parseValues(self, values): + async def parseValues(self, values): if "sr" in values: - self.processStatusReport(values["sr"]) + await self.processStatusReport(values["sr"]) if "err" in values: # JSON Syntax Errors self.processErrorReport(values["err"]) if "er" in values: # Lower level errors @@ -164,7 +164,7 @@ def parseValues(self, values): if "f" in values: self.processFooter(values["f"]) if "r" in values: - self.parseValues(values["r"]) + await self.parseValues(values["r"]) else: # print(values) k = list(values.keys()) @@ -182,7 +182,7 @@ def parseValues(self, values): # CNC.vars[gcode+"B"] = values[k[0]]['b'] # CNC.vars[gcode+"C"] = values[k[0]]['c'] - def parseLine(self, line, cline, sline): + async def parseLine(self, line, cline, sline): if not line: return True @@ -196,7 +196,7 @@ def parseLine(self, line, cline, sline): if cline: del cline[0] if sline: del sline[0] self.master.sio_status = False - self.parseValues(values) + await self.parseValues(values) else: #We return false in order to tell that we can't parse this line @@ -215,14 +215,14 @@ def parseLine(self, line, cline, sline): #Parsing succesful return True - def purgeController(self): + async def purgeController(self): self.master.serial_write(b"!\004\n") self.master.serial.flush() time.sleep(1) # remember and send all G commands G = " ".join([x for x in CNC.vars["G"] if x[0] == "G"] and x != "G43.1") # remember $G TLO = CNC.vars["TLO"] - self.softReset(False) # reset controller + await self.softReset(False) # reset controller self.purgeControllerExtra() self.master.runEnded() self.master.stopProbe() diff --git a/bCNC/controllers/GRBL0.py b/bCNC/controllers/GRBL0.py index ced0b4237..8d5a4c0b3 100644 --- a/bCNC/controllers/GRBL0.py +++ b/bCNC/controllers/GRBL0.py @@ -11,7 +11,7 @@ def __init__(self, master): self.has_override = False self.master = master - def parseBracketAngle(self, line, cline): + async def parseBracketAngle(self, line, cline): self.master.sio_status = False pat = STATUSPAT.match(line) if pat: diff --git a/bCNC/controllers/GRBL1.py b/bCNC/controllers/GRBL1.py index 76b063259..d4df20bf0 100644 --- a/bCNC/controllers/GRBL1.py +++ b/bCNC/controllers/GRBL1.py @@ -91,7 +91,7 @@ def overrideSet(self): self.master.serial_write(OV_SPINDLE_d1) CNC.vars["_OvChanged"] = diff < -1 - def parseBracketAngle(self, line, cline): + async def parseBracketAngle(self, line, cline): self.master.sio_status = False fields = line[1:-1].split("|") CNC.vars["pins"] = "" @@ -101,7 +101,7 @@ def parseBracketAngle(self, line, cline): CNC.vars["state"] != fields[0] or self.master.runningPrev != self.master.running ): - self.master.controllerStateChange(fields[0]) + await self.master.controllerStateChange(fields[0]) self.master.runningPrev = self.master.running self.displayState(fields[0]) diff --git a/bCNC/controllers/SMOOTHIE.py b/bCNC/controllers/SMOOTHIE.py index 5230611a3..36827b364 100644 --- a/bCNC/controllers/SMOOTHIE.py +++ b/bCNC/controllers/SMOOTHIE.py @@ -64,7 +64,7 @@ def viewBuild(self): def grblHelp(self): self.master.serial_write(b"help\n") - def parseBracketAngle(self, line, cline): + async def parseBracketAngle(self, line, cline): # ln = line[1:-1] # strip off < .. > diff --git a/bCNC/controllers/_GenericController.py b/bCNC/controllers/_GenericController.py index e20540866..c10ecf38e 100644 --- a/bCNC/controllers/_GenericController.py +++ b/bCNC/controllers/_GenericController.py @@ -7,6 +7,7 @@ from CNC import CNC, WCS import Utils +import asyncio # GRBLv1 SPLITPAT = re.compile(r"[:,]") @@ -89,13 +90,16 @@ def hardReset(self): self.master.notBusy() # ---------------------------------------------------------------------- - def softReset(self, clearAlarm=True): - if self.master.serial: - self.master.serial_write(b"\030") - self.master.stopProbe() - if clearAlarm: - self.master._alarm = False - CNC.vars["_OvChanged"] = True # force a feed change if any + async def softReset(self, clearAlarm=True): + async with self.master.resetLock: + async with self.master.resetCondition: + if self.master.serial: + self.master.serial_write(b"\030") + self.master.stopProbe() + if clearAlarm: + self.master._alarm = False + CNC.vars["_OvChanged"] = True # force a feed change if any + await self.master.resetCondition.wait() # ---------------------------------------------------------------------- def unlock(self, clearAlarm=True): @@ -209,14 +213,14 @@ def pause(self, event=None): # Purge the buffer of the controller. Unfortunately we have to perform # a reset to clear the buffer of the controller # --------------------------------------------------------------------- - def purgeController(self): + async def purgeController(self): self.master.serial_write(b"!") self.master.serial.flush() time.sleep(1) # remember and send all G commands G = " ".join([x for x in CNC.vars["G"] if x[0] == "G"]) # remember $G TLO = CNC.vars["TLO"] - self.softReset(False) # reset controller + await self.softReset(False) # reset controller self.purgeControllerExtra() self.master.runEnded() self.master.stopProbe() @@ -244,7 +248,7 @@ def displayState(self, state): CNC.vars["state"] = state # ---------------------------------------------------------------------- - def parseLine(self, line, cline, sline): + async def parseLine(self, line, cline, sline): if not line: return True @@ -252,7 +256,7 @@ def parseLine(self, line, cline, sline): if not self.master.sio_status: self.master.log.put((self.master.MSG_RECEIVE, line)) else: - self.parseBracketAngle(line, cline) + await self.parseBracketAngle(line, cline) elif line[0] == "[": self.master.log.put((self.master.MSG_RECEIVE, line))