From 7360c437a9687dc0c851f846ece468e6be53067d Mon Sep 17 00:00:00 2001 From: Henrik Muehe Date: Sun, 3 May 2026 17:58:31 +0200 Subject: [PATCH] Preplanner: surface plan failures so the Processing dialog exits When a plan fails (e.g. AuxPreprocessor Z-A coupling rejection at planner-load time, or any other gplan error in the plan.py subprocess), the Plan future was never resolved. PathHandler then 1-second-times-out forever returning {progress:0}, and the JS poll loop in load_toolpath kept the 'Processing New File' dialog up indefinitely. - Preplanner.Plan now records .error and always resolves the future. - PathHandler returns {progress:1, error:...} when the plan failed. - load_toolpath closes the dialog and alerts the operator on error, and breaks out of the poll loop on api errors instead of looping. - FileHandler upload-time AuxPreprocessor coupling errors now post a visible state message instead of being silently swallowed. --- src/js/program-mixin.js | 15 +++++++++++++++ src/py/bbctrl/FileHandler.py | 22 +++++++++++++++++----- src/py/bbctrl/Preplanner.py | 16 ++++++++++++++-- src/py/bbctrl/Web.py | 15 +++++++++++++-- 4 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/js/program-mixin.js b/src/js/program-mixin.js index e8c2aad..34be1c8 100644 --- a/src/js/program-mixin.js +++ b/src/js/program-mixin.js @@ -232,6 +232,17 @@ module.exports = { const toolpath = await api.get(`path/${file}`); this.toolpath_progress = toolpath.progress; + // Planner failure (e.g. AuxPreprocessor Z-A coupling + // rejection). Close the dialog and surface the message + // instead of polling the same broken plan forever. + if (toolpath.error) { + this.showGcodeMessage = false; + this.toolpath_progress = 0; + console.error("Plan failed:", toolpath.error); + alert("Could not plan G-code:\n\n" + toolpath.error); + return; + } + if (toolpath.progress === 1 || typeof toolpath.progress == "undefined") { this.showGcodeMessage = false; @@ -248,7 +259,11 @@ module.exports = { } } } catch (error) { + // api.get throws on non-2xx; log and break the loop so the + // dialog doesn't stay up forever. console.error(error); + this.showGcodeMessage = false; + return; } } }, diff --git a/src/py/bbctrl/FileHandler.py b/src/py/bbctrl/FileHandler.py index 61a492e..2181575 100644 --- a/src/py/bbctrl/FileHandler.py +++ b/src/py/bbctrl/FileHandler.py @@ -107,15 +107,27 @@ class FileHandler(bbctrl.APIHandler): # auxcnc stepper is exposed as a virtual A axis (see # ExternalAxis). try: - from bbctrl.AuxPreprocessor import preprocess_file + from bbctrl.AuxPreprocessor import ( + preprocess_file, AuxPreprocessorError) log = self.get_log('AuxPreprocessor') ext = getattr(self.get_ctrl(), 'ext_axis', None) coupling = (ext.coupling_for_preprocessor() if ext is not None else None) - if preprocess_file(filename.decode('utf8'), - log=log, coupling=coupling): - log.info('Rewrote upload (ATC / Z-A coupling) in %s' - % self.uploadFilename) + try: + if preprocess_file(filename.decode('utf8'), + log=log, coupling=coupling): + log.info('Rewrote upload (ATC / Z-A coupling) in %s' + % self.uploadFilename) + except AuxPreprocessorError as e: + # Surface coupling-violation errors to the operator + # via the message stream so the upload doesn't go + # silently un-rewritten and then trip the runtime + # check (which can hang the planner dialog). + log.warning('Aux preprocess refused upload: %s' % e) + try: + self.get_ctrl().state.add_message( + 'Z-A coupling: ' + str(e)) + except Exception: pass except Exception: self.get_log('AuxPreprocessor').exception( 'Aux preprocess failed; uploading unchanged') diff --git a/src/py/bbctrl/Preplanner.py b/src/py/bbctrl/Preplanner.py index 094fe9a..045a45b 100644 --- a/src/py/bbctrl/Preplanner.py +++ b/src/py/bbctrl/Preplanner.py @@ -74,6 +74,7 @@ class Plan(object): self.progress = 0 self.cancel = False self.pid = None + self.error = None root = ctrl.get_path() self.gcode = '%s/upload/%s' % (root, filename) @@ -202,8 +203,16 @@ class Plan(object): if not self._exists(): yield self._exec() self.future.set_result(self._read()) - except: - self.preplanner.log.exception("Failed to load file - doesn't appear to be GCode.") + except Exception as e: + # Record the error and ALWAYS resolve the future, otherwise + # PathHandler.get keeps timing out at 1s forever and the UI + # gets stuck on the "Processing New File" dialog. + self.preplanner.log.exception( + "Failed to plan file: " + str(e)) + self.error = str(e) or 'Plan failed' + self.progress = 1 + if not self.future.done(): + self.future.set_result(None) class Preplanner(object): @@ -268,3 +277,6 @@ class Preplanner(object): def get_plan_progress(self, filename): return self.plans[filename].progress if filename in self.plans else 0 + + def get_plan_error(self, filename): + return self.plans[filename].error if filename in self.plans else None diff --git a/src/py/bbctrl/Web.py b/src/py/bbctrl/Web.py index a1d37cf..aa33142 100644 --- a/src/py/bbctrl/Web.py +++ b/src/py/bbctrl/Web.py @@ -411,11 +411,22 @@ class PathHandler(bbctrl.APIHandler): except gen.TimeoutError: progress = preplanner.get_plan_progress(filename) - self.write_json(dict(progress = progress)) + err = preplanner.get_plan_error(filename) + resp = dict(progress = progress) + if err: resp['error'] = err + self.write_json(resp) return try: - if data is None: return + # Plan finished but produced no data (planner subprocess + # failed, e.g. AuxPreprocessor coupling rejection at + # planner-load time). Surface the error so the UI can + # close the "Processing New File" dialog instead of + # polling forever. + if data is None: + err = preplanner.get_plan_error(filename) or 'Plan failed' + self.write_json(dict(progress = 1, error = err)) + return meta, positions, speeds = data if dataType == '/positions': data = positions