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.
This commit is contained in:
2026-05-03 17:58:31 +02:00
parent 01e39722d3
commit 7360c437a9
4 changed files with 59 additions and 9 deletions

View File

@@ -232,6 +232,17 @@ module.exports = {
const toolpath = await api.get(`path/${file}`); const toolpath = await api.get(`path/${file}`);
this.toolpath_progress = toolpath.progress; 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") { if (toolpath.progress === 1 || typeof toolpath.progress == "undefined") {
this.showGcodeMessage = false; this.showGcodeMessage = false;
@@ -248,7 +259,11 @@ module.exports = {
} }
} }
} catch (error) { } catch (error) {
// api.get throws on non-2xx; log and break the loop so the
// dialog doesn't stay up forever.
console.error(error); console.error(error);
this.showGcodeMessage = false;
return;
} }
} }
}, },

View File

@@ -107,15 +107,27 @@ class FileHandler(bbctrl.APIHandler):
# auxcnc stepper is exposed as a virtual A axis (see # auxcnc stepper is exposed as a virtual A axis (see
# ExternalAxis). # ExternalAxis).
try: try:
from bbctrl.AuxPreprocessor import preprocess_file from bbctrl.AuxPreprocessor import (
preprocess_file, AuxPreprocessorError)
log = self.get_log('AuxPreprocessor') log = self.get_log('AuxPreprocessor')
ext = getattr(self.get_ctrl(), 'ext_axis', None) ext = getattr(self.get_ctrl(), 'ext_axis', None)
coupling = (ext.coupling_for_preprocessor() coupling = (ext.coupling_for_preprocessor()
if ext is not None else None) if ext is not None else None)
if preprocess_file(filename.decode('utf8'), try:
log=log, coupling=coupling): if preprocess_file(filename.decode('utf8'),
log.info('Rewrote upload (ATC / Z-A coupling) in %s' log=log, coupling=coupling):
% self.uploadFilename) 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: except Exception:
self.get_log('AuxPreprocessor').exception( self.get_log('AuxPreprocessor').exception(
'Aux preprocess failed; uploading unchanged') 'Aux preprocess failed; uploading unchanged')

View File

@@ -74,6 +74,7 @@ class Plan(object):
self.progress = 0 self.progress = 0
self.cancel = False self.cancel = False
self.pid = None self.pid = None
self.error = None
root = ctrl.get_path() root = ctrl.get_path()
self.gcode = '%s/upload/%s' % (root, filename) self.gcode = '%s/upload/%s' % (root, filename)
@@ -202,8 +203,16 @@ class Plan(object):
if not self._exists(): yield self._exec() if not self._exists(): yield self._exec()
self.future.set_result(self._read()) self.future.set_result(self._read())
except: except Exception as e:
self.preplanner.log.exception("Failed to load file - doesn't appear to be GCode.") # 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): class Preplanner(object):
@@ -268,3 +277,6 @@ class Preplanner(object):
def get_plan_progress(self, filename): def get_plan_progress(self, filename):
return self.plans[filename].progress if filename in self.plans else 0 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

View File

@@ -411,11 +411,22 @@ class PathHandler(bbctrl.APIHandler):
except gen.TimeoutError: except gen.TimeoutError:
progress = preplanner.get_plan_progress(filename) 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 return
try: 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 meta, positions, speeds = data
if dataType == '/positions': data = positions if dataType == '/positions': data = positions