New probe dialog working

This commit is contained in:
David Carley
2022-07-10 02:00:45 -07:00
parent bc161fcd3d
commit de665cac4d
11 changed files with 320 additions and 330 deletions

View File

@@ -289,6 +289,15 @@ module.exports = new Vue({
update_object(this.config, config, true); update_object(this.config, config, true);
this.parse_hash(); this.parse_hash();
if (!this.devModChecked) {
this.devModChecked = true;
if (this.config.devmode) {
SvelteComponents.createComponent("Devmode",
document.getElementById("svelte-devmode-host")
);
}
}
if (!this.checkedUpgrade) { if (!this.checkedUpgrade) {
this.checkedUpgrade = true; this.checkedUpgrade = true;

View File

@@ -254,6 +254,7 @@ module.exports = {
this.load(); this.load();
SvelteComponents.registerControllerMethods({ SvelteComponents.registerControllerMethods({
stop: (...args) => this.stop(...args),
send: (...args) => this.send(...args), send: (...args) => this.send(...args),
goto_zero: (...args) => this.goto_zero(...args) goto_zero: (...args) => this.goto_zero(...args)
}); });

View File

@@ -21,6 +21,7 @@ html(lang="en")
body(v-cloak) body(v-cloak)
#svelte-dialog-host #svelte-dialog-host
#svelte-devmode-host
#overlay(v-if="status != 'connected'") #overlay(v-if="status != 'connected'")
span {{status}} span {{status}}

View File

@@ -1,18 +1,22 @@
import os import os
import bbctrl import bbctrl
class Ctrl(object): class Ctrl(object):
def __init__(self, args, ioloop, id): def __init__(self, args, ioloop, id):
self.args = args self.args = args
self.ioloop = bbctrl.IOLoop(ioloop) self.ioloop = bbctrl.IOLoop(ioloop)
self.id = id self.id = id
self.timeout = None # Used in demo mode self.timeout = None # Used in demo mode
if id and not os.path.exists(id): os.mkdir(id) if id and not os.path.exists(id):
os.mkdir(id)
# Start log # Start log
if args.demo: log_path = self.get_path(filename = 'bbctrl.log') if args.demo:
else: log_path = args.log log_path = self.get_path(filename='bbctrl.log')
else:
log_path = args.log
self.log = bbctrl.log.Log(args, self.ioloop, log_path) self.log = bbctrl.log.Log(args, self.ioloop, log_path)
self.state = bbctrl.State(self) self.state = bbctrl.State(self)
@@ -21,14 +25,17 @@ class Ctrl(object):
self.log.get('Ctrl').info('Starting %s' % self.id) self.log.get('Ctrl').info('Starting %s' % self.id)
try: try:
if args.demo: self.avr = bbctrl.AVREmu(self) if args.demo:
else: self.avr = bbctrl.AVR(self) self.avr = bbctrl.AVREmu(self)
else:
self.avr = bbctrl.AVR(self)
self.i2c = bbctrl.I2C(args.i2c_port, args.demo) self.i2c = bbctrl.I2C(args.i2c_port, args.demo)
self.lcd = bbctrl.LCD(self) self.lcd = bbctrl.LCD(self)
self.mach = bbctrl.Mach(self, self.avr) self.mach = bbctrl.Mach(self, self.avr)
self.preplanner = bbctrl.Preplanner(self) self.preplanner = bbctrl.Preplanner(self)
if not args.demo: self.jog = bbctrl.Jog(self) if not args.demo:
self.jog = bbctrl.Jog(self)
self.pwr = bbctrl.Pwr(self) self.pwr = bbctrl.Pwr(self)
self.mach.connect() self.mach.connect()
@@ -38,48 +45,41 @@ class Ctrl(object):
os.environ['GCODE_SCRIPT_PATH'] = self.get_upload() os.environ['GCODE_SCRIPT_PATH'] = self.get_upload()
except Exception: self.log.get('Ctrl').exception('Internal error: Control initialization failed') except Exception:
self.log.get('Ctrl').exception(
'Internal error: Control initialization failed')
def __del__(self): print('Ctrl deleted') def __del__(self): print('Ctrl deleted')
def clear_timeout(self): def clear_timeout(self):
if self.timeout is not None: self.ioloop.remove_timeout(self.timeout) if self.timeout is not None:
self.ioloop.remove_timeout(self.timeout)
self.timeout = None self.timeout = None
def set_timeout(self, cb, *args, **kwargs): def set_timeout(self, cb, *args, **kwargs):
self.clear_timeout() self.clear_timeout()
t = self.args.client_timeout t = self.args.client_timeout
self.timeout = self.ioloop.call_later(t, cb, *args, **kwargs) self.timeout = self.ioloop.call_later(t, cb, *args, **kwargs)
def get_path(self, dir=None, filename=None):
def get_path(self, dir = None, filename = None):
path = './' + self.id if self.id else '.' path = './' + self.id if self.id else '.'
path = path if dir is None else (path + '/' + dir) path = path if dir is None else (path + '/' + dir)
return path if filename is None else (path + '/' + filename) return path if filename is None else (path + '/' + filename)
def get_upload(self, filename=None):
def get_upload(self, filename = None):
return self.get_path('upload', filename) return self.get_path('upload', filename)
def get_plan(self, filename=None):
def get_plan(self, filename = None):
return self.get_path('plans', filename) return self.get_path('plans', filename)
def configure(self): def configure(self):
# Indirectly configures state via calls to config() and the AVR # Indirectly configures state via calls to config() and the AVR
self.config.reload() self.config.reload()
self.state.init()
def ready(self): def ready(self):
# This is used to synchronize the start of the preplanner # This is used to synchronize the start of the preplanner
self.preplanner.start() self.preplanner.start()
def close(self): def close(self):
self.log.get('Ctrl').info('Closing %s' % self.id) self.log.get('Ctrl').info('Closing %s' % self.id)
self.ioloop.close() self.ioloop.close()

View File

@@ -1,30 +1,3 @@
################################################################################
# #
# This file is part of the Buildbotics firmware. #
# #
# Copyright (c) 2015 - 2018, Buildbotics LLC #
# All rights reserved. #
# #
# This file ("the software") is free software: you can redistribute it #
# and/or modify it under the terms of the GNU General Public License, #
# version 2 as published by the Free Software Foundation. You should #
# have received a copy of the GNU General Public License, version 2 #
# along with the software. If not, see <http://www.gnu.org/licenses/>. #
# #
# The software is distributed in the hope that it will be useful, but #
# WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU #
# Lesser General Public License for more details. #
# #
# You should have received a copy of the GNU Lesser General Public #
# License along with the software. If not, see #
# <http://www.gnu.org/licenses/>. #
# #
# For information regarding this software email: #
# "Joseph Coffland" <joseph@buildbotics.com> #
# #
################################################################################
import bbctrl import bbctrl
from bbctrl.Comm import Comm from bbctrl.Comm import Comm
import bbctrl.Cmd as Cmd import bbctrl.Cmd as Cmd
@@ -74,6 +47,7 @@ and check your motor cabling. See the "Motor Faults" table on the "Indicators" \
for more information.\ for more information.\
''' '''
def overrides(interface_class): def overrides(interface_class):
def overrider(method): def overrider(method):
if not method.__name__ in dir(interface_class): if not method.__name__ in dir(interface_class):
@@ -102,7 +76,6 @@ class Mach(Comm):
super().reboot() super().reboot()
def _get_state(self): return self.ctrl.state.get('xx', '') def _get_state(self): return self.ctrl.state.get('xx', '')
def _is_estopped(self): return self._get_state() == 'ESTOPPED' def _is_estopped(self): return self._get_state() == 'ESTOPPED'
def _is_holding(self): return self._get_state() == 'HOLDING' def _is_holding(self): return self._get_state() == 'HOLDING'
@@ -110,19 +83,18 @@ class Mach(Comm):
def _get_pause_reason(self): return self.ctrl.state.get('pr', '') def _get_pause_reason(self): return self.ctrl.state.get('pr', '')
def _get_cycle(self): return self.ctrl.state.get('cycle', 'idle') def _get_cycle(self): return self.ctrl.state.get('cycle', 'idle')
def _is_paused(self): def _is_paused(self):
if not self._is_holding() or self.unpausing: return False if not self._is_holding() or self.unpausing:
return False
return self._get_pause_reason() in ( return self._get_pause_reason() in (
'User pause', 'Program pause', 'Optional pause') 'User pause', 'Program pause', 'Optional pause')
def _set_cycle(self, cycle): self.ctrl.state.set('cycle', cycle) def _set_cycle(self, cycle): self.ctrl.state.set('cycle', cycle)
def _begin_cycle(self, cycle): def _begin_cycle(self, cycle):
current = self._get_cycle() current = self._get_cycle()
if current == cycle: return # No change if current == cycle:
return # No change
if current != 'idle': if current != 'idle':
raise Exception('Cannot enter %s cycle while in %s cycle' % raise Exception('Cannot enter %s cycle while in %s cycle' %
@@ -132,14 +104,12 @@ class Mach(Comm):
# if current == 'idle' or (cycle == 'jogging' and self._is_paused()): # if current == 'idle' or (cycle == 'jogging' and self._is_paused()):
self._set_cycle(cycle) self._set_cycle(cycle)
def process_log(self, log): def process_log(self, log):
# When a probe has failed, we have to e-stop or things # When a probe has failed, we have to e-stop or things
# end up in a bad state, where positions and offsets are incorrect # end up in a bad state, where positions and offsets are incorrect
if log['msg'] == 'Switch not found': if log['msg'] == 'Switch not found':
self.estop() self.estop()
def _update(self, update): def _update(self, update):
# Detect motor faults # Detect motor faults
for motor in range(4): for motor in range(4):
@@ -153,12 +123,12 @@ class Mach(Comm):
# Handle EStop # Handle EStop
if state_changed and state == 'ESTOPPED': if state_changed and state == 'ESTOPPED':
self.planner.reset(stop = False) self.planner.reset(stop=False)
# Exit cycle if state changed to READY # Exit cycle if state changed to READY
if (state_changed and self._get_cycle() != 'idle' and if (state_changed and self._get_cycle() != 'idle' and
self._is_ready() and not self.planner.is_busy() and self._is_ready() and not self.planner.is_busy() and
not super().is_active()): not super().is_active()):
self.planner.position_change() self.planner.position_change()
self._set_cycle('idle') self._set_cycle('idle')
@@ -187,7 +157,6 @@ class Mach(Comm):
(pr == 'Optional pause' and not op))): (pr == 'Optional pause' and not op))):
self._unpause() self._unpause()
def _unpause(self): def _unpause(self):
pause_reason = self._get_pause_reason() pause_reason = self._get_pause_reason()
self.mlog.info('Unpause: ' + pause_reason) self.mlog.info('Unpause: ' + pause_reason)
@@ -196,36 +165,31 @@ class Mach(Comm):
self.planner.stop() self.planner.stop()
self.ctrl.state.set('line', 0) self.ctrl.state.set('line', 0)
else: self.planner.restart() else:
self.planner.restart()
super().i2c_command(Cmd.UNPAUSE) super().i2c_command(Cmd.UNPAUSE)
self.unpausing = True self.unpausing = True
def _i2c_block(self, block): def _i2c_block(self, block):
super().i2c_command(block[0], block = block[1:]) super().i2c_command(block[0], block=block[1:])
def _i2c_set(self, name, value): self._i2c_block(Cmd.set(name, value)) def _i2c_set(self, name, value): self._i2c_block(Cmd.set(name, value))
@overrides(Comm) @overrides(Comm)
def comm_next(self): def comm_next(self):
if self.planner.is_running() and not self._is_holding(): if self.planner.is_running() and not self._is_holding():
return self.planner.next() return self.planner.next()
@overrides(Comm) @overrides(Comm)
def comm_error(self): def comm_error(self):
self.planner.reset() self.planner.reset()
@overrides(Comm) @overrides(Comm)
def connect(self): def connect(self):
self.planner.reset() self.planner.reset()
super().connect() super().connect()
def _query_var(self, cmd): def _query_var(self, cmd):
equal = cmd.find('=') equal = cmd.find('=')
if equal == -1: if equal == -1:
@@ -234,21 +198,26 @@ class Mach(Comm):
else: else:
name, value = cmd[1:equal], cmd[equal + 1:] name, value = cmd[1:equal], cmd[equal + 1:]
if value.lower() == 'true': value = True if value.lower() == 'true':
elif value.lower() == 'false': value = False value = True
elif value.lower() == 'false':
value = False
else: else:
try: try:
value = float(value) value = float(value)
except: pass except:
pass
self.ctrl.state.config(name, value) self.ctrl.state.config(name, value)
def mdi(self, cmd, with_limits=True):
def mdi(self, cmd, with_limits = True):
try: try:
if not len(cmd): return if not len(cmd):
if cmd[0] == '$': self._query_var(cmd) return
elif cmd[0] == '\\': super().queue_command(cmd[1:]) if cmd[0] == '$':
self._query_var(cmd)
elif cmd[0] == '\\':
super().queue_command(cmd[1:])
else: else:
self._begin_cycle('mdi') self._begin_cycle('mdi')
self.planner.mdi(cmd, with_limits) self.planner.mdi(cmd, with_limits)
@@ -260,18 +229,18 @@ class Mach(Comm):
def set(self, code, value): def set(self, code, value):
super().queue_command('${}={}'.format(code, value)) super().queue_command('${}={}'.format(code, value))
def jog(self, axes): def jog(self, axes):
self._begin_cycle('jogging') self._begin_cycle('jogging')
self.planner.position_change() self.planner.position_change()
super().queue_command(Cmd.jog(axes)) super().queue_command(Cmd.jog(axes))
def home(self, axis, position=None):
def home(self, axis, position = None):
state = self.ctrl.state state = self.ctrl.state
if axis is None: axes = 'zxyabc' # TODO This should be configurable if axis is None:
else: axes = '%c' % axis axes = 'zxyabc' # TODO This should be configurable
else:
axes = '%c' % axis
for axis in axes: for axis in axes:
enabled = state.is_axis_enabled(axis) enabled = state.is_axis_enabled(axis)
@@ -291,7 +260,8 @@ class Mach(Comm):
continue continue
if mode == 'manual': if mode == 'manual':
if position is None: raise Exception('Position not set') if position is None:
raise Exception('Position not set')
self.mdi('G28.3 %c%f' % (axis, position)) self.mdi('G28.3 %c%f' % (axis, position))
continue continue
@@ -299,56 +269,63 @@ class Mach(Comm):
self.mlog.info('Homing %s axis' % axis) self.mlog.info('Homing %s axis' % axis)
self._begin_cycle('homing') self._begin_cycle('homing')
if mode.startswith('stall-'): procedure = stall_homing_procedure if mode.startswith('stall-'):
else: procedure = axis_homing_procedure procedure = stall_homing_procedure
else:
procedure = axis_homing_procedure
gcode = procedure % {'axis': axis} gcode = procedure % {'axis': axis}
self.planner.mdi(gcode, False) self.planner.mdi(gcode, False)
super().resume() super().resume()
def unhome(self, axis): self.mdi('G28.2 %c0' % axis) def unhome(self, axis): self.mdi('G28.2 %c0' % axis)
def estop(self): super().estop() def estop(self): super().estop()
def clear(self): def clear(self):
if self._is_estopped(): if self._is_estopped():
self.planner.reset() self.planner.reset()
super().clear() super().clear()
def fake_probe_contact(self):
self._i2c_set('pt', 2)
self.ctrl.state.set('pw', 0)
self.timer = self.ctrl.ioloop.call_later(0.5, self.clear_fake_probe_contact)
def clear_fake_probe_contact(self):
self._i2c_set('pt', 1)
self.ctrl.state.set('pw', 1)
def start(self): def start(self):
filename = self.ctrl.state.get('selected', '') filename = self.ctrl.state.get('selected', '')
if not filename: return if not filename:
return
self._begin_cycle('running') self._begin_cycle('running')
self.planner.load(filename) self.planner.load(filename)
super().resume() super().resume()
def step(self): def step(self):
raise Exception('NYI') # TODO raise Exception('NYI') # TODO
if self._get_cycle() != 'running': self.start() if self._get_cycle() != 'running':
else: super().i2c_command(Cmd.UNPAUSE) self.start()
else:
super().i2c_command(Cmd.UNPAUSE)
def stop(self): def stop(self):
if self._get_state() != 'jogging': self.stopping = True if self._get_state() != 'jogging':
self.stopping = True
super().i2c_command(Cmd.STOP) super().i2c_command(Cmd.STOP)
def pause(self): super().pause() def pause(self): super().pause()
def unpause(self): def unpause(self):
if self._is_paused(): if self._is_paused():
self.ctrl.state.set('optional_pause', False) self.ctrl.state.set('optional_pause', False)
self._unpause() self._unpause()
def optional_pause(self, enable=True):
def optional_pause(self, enable = True):
self.ctrl.state.set('optional_pause', enable) self.ctrl.state.set('optional_pause', enable)
def set_position(self, axis, position): def set_position(self, axis, position):
axis = axis.lower() axis = axis.lower()
state = self.ctrl.state state = self.ctrl.state
@@ -367,17 +344,13 @@ class Mach(Comm):
state.set(axis + 'p', target) state.set(axis + 'p', target)
super().queue_command(Cmd.set_axis(axis, target)) super().queue_command(Cmd.set_axis(axis, target))
def override_feed(self, override): def override_feed(self, override):
self._i2c_set('fo', int(1000 * override)) self._i2c_set('fo', int(1000 * override))
def override_speed(self, override): def override_speed(self, override):
self._i2c_set('so', int(1000 * override)) self._i2c_set('so', int(1000 * override))
def modbus_read(self, addr): self._i2c_block(Cmd.modbus_read(addr)) def modbus_read(self, addr): self._i2c_block(Cmd.modbus_read(addr))
def modbus_write(self, addr, value): def modbus_write(self, addr, value):
self._i2c_block(Cmd.modbus_write(addr, value)) self._i2c_block(Cmd.modbus_write(addr, value))

View File

@@ -1,30 +1,3 @@
################################################################################
# #
# This file is part of the Buildbotics firmware. #
# #
# Copyright (c) 2015 - 2018, Buildbotics LLC #
# All rights reserved. #
# #
# This file ("the software") is free software: you can redistribute it #
# and/or modify it under the terms of the GNU General Public License, #
# version 2 as published by the Free Software Foundation. You should #
# have received a copy of the GNU General Public License, version 2 #
# along with the software. If not, see <http://www.gnu.org/licenses/>. #
# #
# The software is distributed in the hope that it will be useful, but #
# WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU #
# Lesser General Public License for more details. #
# #
# You should have received a copy of the GNU Lesser General Public #
# License along with the software. If not, see #
# <http://www.gnu.org/licenses/>. #
# #
# For information regarding this software email: #
# "Joseph Coffland" <joseph@buildbotics.com> #
# #
################################################################################
import traceback import traceback
import copy import copy
import uuid import uuid
@@ -33,6 +6,7 @@ import bbctrl
from watchdog.observers import Observer from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler from watchdog.events import FileSystemEventHandler
class UploadChangeHandler(FileSystemEventHandler): class UploadChangeHandler(FileSystemEventHandler):
def __init__(self, state): def __init__(self, state):
self.state = state self.state = state
@@ -70,44 +44,36 @@ class State(object):
# the planner will scale returned values when in imperial mode. # the planner will scale returned values when in imperial mode.
for i in range(4): for i in range(4):
self.set_callback(str(i) + 'home_position', self.set_callback(str(i) + 'home_position',
lambda name, i = i: self.motor_home_position(i)) lambda name, i=i: self.motor_home_position(i))
self.set_callback(str(i) + 'home_travel', self.set_callback(str(i) + 'home_travel',
lambda name, i = i: self.motor_home_travel(i)) lambda name, i=i: self.motor_home_travel(i))
self.set_callback(str(i) + 'latch_backoff', self.set_callback(str(i) + 'latch_backoff',
lambda name, i = i: self.motor_latch_backoff(i)) lambda name, i=i: self.motor_latch_backoff(i))
self.set_callback(str(i) + 'zero_backoff', self.set_callback(str(i) + 'zero_backoff',
lambda name, i = i: self.motor_zero_backoff(i)) lambda name, i=i: self.motor_zero_backoff(i))
self.set_callback(str(i) + 'search_velocity', self.set_callback(str(i) + 'search_velocity',
lambda name, i = i: self.motor_search_velocity(i)) lambda name, i=i: self.motor_search_velocity(i))
self.set_callback(str(i) + 'latch_velocity', self.set_callback(str(i) + 'latch_velocity',
lambda name, i = i: self.motor_latch_velocity(i)) lambda name, i=i: self.motor_latch_velocity(i))
self.reset() self.reset()
self.load_files() self.load_files()
observer = Observer() observer = Observer()
observer.schedule(UploadChangeHandler(self), self.ctrl.get_upload(), recursive=True) observer.schedule(UploadChangeHandler(
self), self.ctrl.get_upload(), recursive=True)
observer.start() observer.start()
def init(self):
# Init machine units
metric = self.ctrl.config.get('units', 'METRIC').upper() == 'METRIC'
self.log.info('INIT Metric %d' % metric)
if not 'metric' in self.vars: self.set('metric', metric)
if not 'imperial' in self.vars: self.set('imperial', not metric)
def reset(self): def reset(self):
# Unhome all motors # Unhome all motors
for i in range(4): self.set('%dhomed' % i, False) for i in range(4):
self.set('%dhomed' % i, False)
# Zero offsets and positions # Zero offsets and positions
for axis in 'xyzabc': for axis in 'xyzabc':
self.set(axis + 'p', 0) self.set(axis + 'p', 0)
self.set('offset_' + axis, 0) self.set('offset_' + axis, 0)
def load_files(self): def load_files(self):
files = [] files = []
@@ -125,15 +91,15 @@ class State(object):
files.sort() files.sort()
self.set('files', files) self.set('files', files)
if len(files): self.select_file(files[0]) if len(files):
else: self.select_file('') self.select_file(files[0])
else:
self.select_file('')
def clear_files(self): def clear_files(self):
self.select_file('') self.select_file('')
self.set('files', []) self.set('files', [])
def add_file(self, filename): def add_file(self, filename):
files = copy.deepcopy(self.get('files')) files = copy.deepcopy(self.get('files'))
if not filename in files: if not filename in files:
@@ -143,7 +109,6 @@ class State(object):
self.select_file(filename) self.select_file(filename)
def remove_file(self, filename): def remove_file(self, filename):
files = copy.deepcopy(self.get('files')) files = copy.deepcopy(self.get('files'))
if filename in files: if filename in files:
@@ -151,16 +116,16 @@ class State(object):
self.set('files', files) self.set('files', files)
if self.get('selected', filename) == filename: if self.get('selected', filename) == filename:
if len(files): self.select_file(files[0]) if len(files):
else: self.select_file('') self.select_file(files[0])
else:
self.select_file('')
def select_file(self, filename): def select_file(self, filename):
self.set('selected', filename) self.set('selected', filename)
time = os.path.getmtime(self.ctrl.get_upload(filename)) time = os.path.getmtime(self.ctrl.get_upload(filename))
self.set('selected_time', time) self.set('selected_time', time)
def set_bounds(self, bounds): def set_bounds(self, bounds):
for axis in 'xyzabc': for axis in 'xyzabc':
for name in ('min', 'max'): for name in ('min', 'max'):
@@ -168,24 +133,22 @@ class State(object):
value = bounds[name][axis] if axis in bounds[name] else 0 value = bounds[name][axis] if axis in bounds[name] else 0
self.set(var, value) self.set(var, value)
def ack_message(self, id): def ack_message(self, id):
self.log.info('Message %d acknowledged' % id) self.log.info('Message %d acknowledged' % id)
msgs = self.vars['messages'] msgs = self.vars['messages']
msgs = list(filter(lambda m: id < m['id'], msgs)) msgs = list(filter(lambda m: id < m['id'], msgs))
self.set('messages', msgs) self.set('messages', msgs)
def add_message(self, text): def add_message(self, text):
msg = dict(text = text, id = self.message_id) msg = dict(text=text, id=self.message_id)
self.message_id += 1 self.message_id += 1
msgs = self.vars['messages'] msgs = self.vars['messages']
msgs = msgs + [msg] # It's important we make a new list here msgs = msgs + [msg] # It's important we make a new list here
self.set('messages', msgs) self.set('messages', msgs)
def _notify(self): def _notify(self):
if not self.changes: return if not self.changes:
return
for listener in self.listeners: for listener in self.listeners:
try: try:
@@ -193,25 +156,23 @@ class State(object):
except Exception as e: except Exception as e:
self.log.warning('Updating state listener: %s', self.log.warning('Updating state listener: %s',
traceback.format_exc()) traceback.format_exc())
self.changes = {} self.changes = {}
self.timeout = None self.timeout = None
def resolve(self, name): def resolve(self, name):
# Resolve axis prefixes to motor numbers # Resolve axis prefixes to motor numbers
if 2 < len(name) and name[1] == '_' and name[0] in 'xyzabc': if 2 < len(name) and name[1] == '_' and name[0] in 'xyzabc':
motor = self.find_motor(name[0]) motor = self.find_motor(name[0])
if motor is not None: return str(motor) + name[2:] if motor is not None:
return str(motor) + name[2:]
return name return name
def has(self, name): return self.resolve(name) in self.vars def has(self, name): return self.resolve(name) in self.vars
def set_callback(self, name, cb): self.callbacks[self.resolve(name)] = cb def set_callback(self, name, cb): self.callbacks[self.resolve(name)] = cb
def set(self, name, value): def set(self, name, value):
name = self.resolve(name) name = self.resolve(name)
@@ -223,23 +184,22 @@ class State(object):
if self.timeout is None: if self.timeout is None:
self.timeout = self.ctrl.ioloop.call_later(0.25, self._notify) self.timeout = self.ctrl.ioloop.call_later(0.25, self._notify)
def update(self, update): def update(self, update):
for name, value in update.items(): for name, value in update.items():
self.set(name, value) self.set(name, value)
def get(self, name, default=None):
def get(self, name, default = None):
name = self.resolve(name) name = self.resolve(name)
if name in self.vars: return self.vars[name] if name in self.vars:
if name in self.callbacks: return self.callbacks[name](name) return self.vars[name]
if name in self.callbacks:
return self.callbacks[name](name)
if default is None: if default is None:
self.log.warning('State variable "%s" not found' % name) self.log.warning('State variable "%s" not found' % name)
return default return default
def snapshot(self): def snapshot(self):
vars = copy.deepcopy(self.vars) vars = copy.deepcopy(self.vars)
@@ -262,21 +222,19 @@ class State(object):
return vars return vars
def config(self, code, value): def config(self, code, value):
# Set machine variables via mach, others directly # Set machine variables via mach, others directly
if code in self.machine_var_set: self.ctrl.mach.set(code, value) if code in self.machine_var_set:
else: self.set(code, value) self.ctrl.mach.set(code, value)
else:
self.set(code, value)
def add_listener(self, listener): def add_listener(self, listener):
self.listeners.append(listener) self.listeners.append(listener)
listener(self.vars) listener(self.vars)
def remove_listener(self, listener): self.listeners.remove(listener) def remove_listener(self, listener): self.listeners.remove(listener)
def set_machine_vars(self, vars): def set_machine_vars(self, vars):
# Record all machine vars, indexed or otherwise # Record all machine vars, indexed or otherwise
self.machine_var_set = set() self.machine_var_set = set()
@@ -284,8 +242,8 @@ class State(object):
if 'index' in spec: if 'index' in spec:
for index in spec['index']: for index in spec['index']:
self.machine_var_set.add(index + code) self.machine_var_set.add(index + code)
else: self.machine_var_set.add(code) else:
self.machine_var_set.add(code)
def get_position(self): def get_position(self):
position = {} position = {}
@@ -296,8 +254,7 @@ class State(object):
return position return position
def get_axis_vector(self, name, scale=1):
def get_axis_vector(self, name, scale = 1):
v = {} v = {}
for axis in 'xyzabc': for axis in 'xyzabc':
@@ -305,11 +262,11 @@ class State(object):
if motor is not None and self.motor_enabled(motor): if motor is not None and self.motor_enabled(motor):
value = self.get(str(motor) + name, None) value = self.get(str(motor) + name, None)
if value is not None: v[axis] = value * scale if value is not None:
v[axis] = value * scale
return v return v
def get_soft_limit_vector(self, var, default): def get_soft_limit_vector(self, var, default):
limit = self.get_axis_vector(var, 1) limit = self.get_axis_vector(var, 1)
@@ -319,23 +276,20 @@ class State(object):
return limit return limit
def find_motor(self, axis): def find_motor(self, axis):
for motor in range(4): for motor in range(4):
if not ('%dan' % motor) in self.vars: continue if not ('%dan' % motor) in self.vars:
continue
motor_axis = 'xyzabc'[self.vars['%dan' % motor]] motor_axis = 'xyzabc'[self.vars['%dan' % motor]]
if motor_axis == axis.lower() and self.vars.get('%dme' % motor, 0): if motor_axis == axis.lower() and self.vars.get('%dme' % motor, 0):
return motor return motor
def is_axis_homed(self, axis): return self.get('%s_homed' % axis, False) def is_axis_homed(self, axis): return self.get('%s_homed' % axis, False)
def is_axis_enabled(self, axis): def is_axis_enabled(self, axis):
motor = self.find_motor(axis) motor = self.find_motor(axis)
return motor is not None and self.motor_enabled(motor) return motor is not None and self.motor_enabled(motor)
def get_enabled_axes(self): def get_enabled_axes(self):
axes = [] axes = []
@@ -345,26 +299,25 @@ class State(object):
return axes return axes
def is_motor_faulted(self, motor): def is_motor_faulted(self, motor):
return self.get('%ddf' % motor, 0) & 0x1f return self.get('%ddf' % motor, 0) & 0x1f
def is_axis_faulted(self, axis): def is_axis_faulted(self, axis):
motor = self.find_motor(axis) motor = self.find_motor(axis)
return motor is not None and self.is_motor_faulted(motor) return motor is not None and self.is_motor_faulted(motor)
def axis_homing_mode(self, axis): def axis_homing_mode(self, axis):
motor = self.find_motor(axis) motor = self.find_motor(axis)
if motor is None: return 'disabled' if motor is None:
return 'disabled'
return self.motor_homing_mode(motor) return self.motor_homing_mode(motor)
def axis_home_fail_reason(self, axis): def axis_home_fail_reason(self, axis):
motor = self.find_motor(axis) motor = self.find_motor(axis)
if motor is None: return 'Not mapped to motor' if motor is None:
if not self.motor_enabled(motor): return 'Motor disabled' return 'Not mapped to motor'
if not self.motor_enabled(motor):
return 'Motor disabled'
mode = self.motor_homing_mode(motor) mode = self.motor_homing_mode(motor)
@@ -381,35 +334,39 @@ class State(object):
return 'max-soft-limit must be at least 1mm greater ' \ return 'max-soft-limit must be at least 1mm greater ' \
'than min-soft-limit' 'than min-soft-limit'
def motor_enabled(self, motor): def motor_enabled(self, motor):
return bool(int(self.vars.get('%dme' % motor, 0))) return bool(int(self.vars.get('%dme' % motor, 0)))
def motor_homing_mode(self, motor): def motor_homing_mode(self, motor):
mode = str(self.vars.get('%dho' % motor, 0)) mode = str(self.vars.get('%dho' % motor, 0))
if mode == '0': return 'manual' if mode == '0':
if mode == '1': return 'switch-min' return 'manual'
if mode == '2': return 'switch-max' if mode == '1':
if mode == '3': return 'stall-min' return 'switch-min'
if mode == '4': return 'stall-max' if mode == '2':
return 'switch-max'
if mode == '3':
return 'stall-min'
if mode == '4':
return 'stall-max'
raise Exception('Unrecognized homing mode "%s"' % mode) raise Exception('Unrecognized homing mode "%s"' % mode)
def motor_home_direction(self, motor): def motor_home_direction(self, motor):
mode = self.motor_homing_mode(motor) mode = self.motor_homing_mode(motor)
if mode.endswith('-min'): return -1 if mode.endswith('-min'):
if mode.endswith('-max'): return 1 return -1
return 0 # Disabled if mode.endswith('-max'):
return 1
return 0 # Disabled
def motor_home_position(self, motor): def motor_home_position(self, motor):
mode = self.motor_homing_mode(motor) mode = self.motor_homing_mode(motor)
# Return soft limit positions # Return soft limit positions
if mode.endswith('-min'): return self.vars['%dtn' % motor] if mode.endswith('-min'):
if mode.endswith('-max'): return self.vars['%dtm' % motor] return self.vars['%dtn' % motor]
return 0 # Disabled if mode.endswith('-max'):
return self.vars['%dtm' % motor]
return 0 # Disabled
def motor_home_travel(self, motor): def motor_home_travel(self, motor):
tmin = self.get(str(motor) + 'tm', 0) tmin = self.get(str(motor) + 'tm', 0)
@@ -419,27 +376,22 @@ class State(object):
# (travel_max - travel_min) * 1.5 * home_dir # (travel_max - travel_min) * 1.5 * home_dir
return (tmin - tmax) * 1.5 * hdir return (tmin - tmax) * 1.5 * hdir
def motor_latch_backoff(self, motor): def motor_latch_backoff(self, motor):
lb = self.get(str(motor) + 'lb', 0) lb = self.get(str(motor) + 'lb', 0)
hdir = self.motor_home_direction(motor) hdir = self.motor_home_direction(motor)
return -(lb * hdir) # -latch_backoff * home_dir return -(lb * hdir) # -latch_backoff * home_dir
def motor_zero_backoff(self, motor): def motor_zero_backoff(self, motor):
zb = self.get(str(motor) + 'zb', 0) zb = self.get(str(motor) + 'zb', 0)
hdir = self.motor_home_direction(motor) hdir = self.motor_home_direction(motor)
return -(zb * hdir) # -zero_backoff * home_dir return -(zb * hdir) # -zero_backoff * home_dir
def motor_search_velocity(self, motor): def motor_search_velocity(self, motor):
return 1000 * self.get(str(motor) + 'sv', 0) return 1000 * self.get(str(motor) + 'sv', 0)
def motor_latch_velocity(self, motor): def motor_latch_velocity(self, motor):
return 1000 * self.get(str(motor) + 'lv', 0) return 1000 * self.get(str(motor) + 'lv', 0)
def get_axis_switch(self, axis, side): def get_axis_switch(self, axis, side):
axis = axis.lower() axis = axis.lower()
@@ -453,14 +405,17 @@ class State(object):
# This must match the switch ID enum in avr/src/switch.h # This must match the switch ID enum in avr/src/switch.h
hmode = self.motor_homing_mode(motor) hmode = self.motor_homing_mode(motor)
if hmode.startswith('stall-'): return motor + 10 if hmode.startswith('stall-'):
return motor + 10
return 2 * motor + 2 + (0 if side.lower() == 'min' else 1) return 2 * motor + 2 + (0 if side.lower() == 'min' else 1)
def get_switch_id(self, switch): def get_switch_id(self, switch):
# TODO Support other input switches in CAMotics gcode/machine/PortType.h # TODO Support other input switches in CAMotics gcode/machine/PortType.h
switch = switch.lower() switch = switch.lower()
if switch == 'probe': return 1 if switch == 'probe':
if switch[1:] == '-min': return self.get_axis_switch(switch[0], 'min') return 1
if switch[1:] == '-max': return self.get_axis_switch(switch[0], 'max') if switch[1:] == '-min':
return self.get_axis_switch(switch[0], 'min')
if switch[1:] == '-max':
return self.get_axis_switch(switch[0], 'max')
raise Exception('Unsupported switch "%s"' % switch) raise Exception('Unsupported switch "%s"' % switch)

View File

@@ -269,6 +269,14 @@ class HomeHandler(bbctrl.APIHandler):
self.get_ctrl().mach.home(axis) self.get_ctrl().mach.home(axis)
class DevmodeHandler(bbctrl.APIHandler):
def put_ok(self, command, *args):
if command == "/probe":
self.get_ctrl().mach.fake_probe_contact()
else:
raise HTTPError(400, 'Not implemented')
class StartHandler(bbctrl.APIHandler): class StartHandler(bbctrl.APIHandler):
def put_ok(self): self.get_ctrl().mach.start() def put_ok(self): self.get_ctrl().mach.start()
@@ -463,6 +471,7 @@ class Web(tornado.web.Application):
(r'/api/file(/[^/]+)?', bbctrl.FileHandler), (r'/api/file(/[^/]+)?', bbctrl.FileHandler),
(r'/api/path/([^/]+)((/positions)|(/speeds))?', PathHandler), (r'/api/path/([^/]+)((/positions)|(/speeds))?', PathHandler),
(r'/api/home(/[xyzabcXYZABC]((/set)|(/clear))?)?', HomeHandler), (r'/api/home(/[xyzabcXYZABC]((/set)|(/clear))?)?', HomeHandler),
(r'/api/devmode((/probe))?', DevmodeHandler),
(r'/api/start', StartHandler), (r'/api/start', StartHandler),
(r'/api/estop', EStopHandler), (r'/api/estop', EStopHandler),
(r'/api/clear', ClearHandler), (r'/api/clear', ClearHandler),

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import * as api from "$lib/api";
</script>
<div class="devmode">
<button on:click={() => api.PUT("devmode/probe")}> Probe Contact </button>
</div>
<style lang="scss">
.devmode {
background-color: greenyellow;
z-index: 20000000;
position: absolute;
top: 10px;
left: 400px;
width: 500px;
height: 100px;
padding: 6px;
display: flex;
align-items: flex-start;
justify-items: space-between;
}
</style>

View File

@@ -5,12 +5,10 @@
import { Config } from "$lib/ConfigStore"; import { Config } from "$lib/ConfigStore";
import { waitForChange } from "$lib/StoreHelpers"; import { waitForChange } from "$lib/StoreHelpers";
import { ControllerMethods } from "$lib/RegisterControllerMethods"; import { ControllerMethods } from "$lib/RegisterControllerMethods";
import { tick } from "svelte";
type Stage = type Step =
| "None" | "None"
| "TestingProbe" | "TestingProbe"
| "TestingProbeComplete"
| "GetToolDiameter" | "GetToolDiameter"
| "Probing" | "Probing"
| "ProbingFailed" | "ProbingFailed"
@@ -55,77 +53,48 @@
<script type="ts"> <script type="ts">
export let open; export let open;
export let probeType: "xyz" | "z"; export let probeType: "xyz" | "z";
let stage: Stage = "None"; let step: Step = "None";
let toolDiameter; let toolDiameter;
let confirmButton = { let showCancelButton = true;
label: "Continue", let nextButton = {
label: "Next",
disabled: false, disabled: false,
allowClose: false, allowClose: false,
}; };
$: showPrompts = $Config.settings?.["probing-prompts"]; $: showPrompts = $Config.settings?.["probing-prompts"];
$: clearFlags(stage);
$: updateConfirmButton();
$: if (open) { $: if (open) {
toolDiameter = "";
// Svelte appears not to like it when you invoke // Svelte appears not to like it when you invoke
// an async function from a reactive statement // an async function from a reactive statement, so we
// use requestAnimationFrame to call begin at a later moment.
requestAnimationFrame(begin); requestAnimationFrame(begin);
} }
function updateConfirmButton() {
confirmButton.label = "Continue";
confirmButton.disabled = false;
confirmButton.allowClose = false;
switch (stage) {
case "TestingProbe":
case "Probing":
confirmButton.disabled = true;
break;
case "ProbingComplete":
confirmButton.disabled = true;
confirmButton.label = "Done";
confirmButton.allowClose = true;
break;
}
}
async function begin() { async function begin() {
try { try {
$probingActive = true; $probingActive = true;
assertValidProbeType(); assertValidProbeType();
if (showPrompts) { if (showPrompts) {
stage = "TestingProbe"; await stepCompleted("TestingProbe", probeContacted);
await cancellableSignal(probeContacted);
stage = "TestingProbeComplete";
await cancellableSignal(userAcknowledged);
} }
if (probeType === "xyz") { if (probeType === "xyz") {
stage = "GetToolDiameter"; await stepCompleted("GetToolDiameter", userAcknowledged);
await cancellableSignal(userAcknowledged);
} }
do { do {
stage = "Probing"; await stepCompleted("Probing", probingComplete, probingFailed);
executeProbe(probeType, toolDiameter);
await cancellableSignal(probingComplete, probingFailed);
if ($probingFailed) { if ($probingFailed) {
stage = "ProbingFailed"; await stepCompleted("ProbingFailed", userAcknowledged);
await cancellableSignal(userAcknowledged);
} }
} while (!$probingComplete); } while (!$probingComplete);
stage = "ProbingComplete"; await stepCompleted("ProbingComplete", userAcknowledged);
await cancellableSignal(userAcknowledged);
if (probeType === "xyz") { if (probeType === "xyz") {
ControllerMethods.goto_zero(1, 1, 0, 0); ControllerMethods.goto_zero(1, 1, 0, 0);
@@ -136,12 +105,40 @@
} }
} finally { } finally {
$probingActive = false; $probingActive = false;
stage = "None"; step = "None";
if ($probingStarted) {
ControllerMethods.stop();
}
clearFlags(); clearFlags();
} }
} }
async function cancellableSignal<T>(...writables: Array<Writable<T>>) { function assertValidProbeType() {
switch (probeType) {
case "xyz":
case "z":
break;
default:
throw new Error(`Invalid probe type: ${probeType}`);
}
}
async function stepCompleted(
nextStep: Step,
...writables: Array<Writable<any>>
) {
step = nextStep;
clearFlags();
updateButtons();
if (step === "Probing") {
executeProbe();
}
await Promise.race([ await Promise.race([
...writables.map((writable) => waitForChange(writable)), ...writables.map((writable) => waitForChange(writable)),
waitForChange(cancelled), waitForChange(cancelled),
@@ -161,18 +158,33 @@
$userAcknowledged = false; $userAcknowledged = false;
} }
function assertValidProbeType() { function updateButtons() {
switch (probeType) { showCancelButton = true;
case "xyz":
case "z": nextButton = {
label: "Next",
disabled: false,
allowClose: false,
};
switch (step) {
case "TestingProbe":
case "Probing":
nextButton.disabled = true;
break; break;
default: case "ProbingComplete":
throw new Error(`Invalid probe type: ${probeType}`); showCancelButton = false;
nextButton = {
disabled: false,
label: "Done",
allowClose: true,
};
break;
} }
} }
function executeProbe(probeType: "xyz" | "z", toolDiameter: number) { function executeProbe() {
const probeBlockWidth = $Config.probe["probe-xdim"]; const probeBlockWidth = $Config.probe["probe-xdim"];
const probeBlockLength = $Config.probe["probe-ydim"]; const probeBlockLength = $Config.probe["probe-ydim"];
const probeBlockHeight = $Config.probe["probe-zdim"]; const probeBlockHeight = $Config.probe["probe-zdim"];
@@ -186,18 +198,18 @@
if (probeType === "z") { if (probeType === "z") {
ControllerMethods.send(` ControllerMethods.send(`
G21 G21
G92 Z0 G92 Z0
G38.2 Z -25.4 F${fastSeek} G38.2 Z -25.4 F${fastSeek}
G91 G1 Z 1 G91 G1 Z 1
G38.2 Z -2 F${slowSeek} G38.2 Z -2 F${slowSeek}
G92 Z ${zOffset} G92 Z ${zOffset}
G91 G0 Z 3 G91 G0 Z 3
M2 M2
`); `);
} else { } else {
// After probing Z, we want to drop the bit down: // After probing Z, we want to drop the bit down:
// Ideally, 12.7mm/0.5in // Ideally, 12.7mm/0.5in
@@ -206,35 +218,35 @@
const plunge = Math.min(12.7, zOffset * 0.75) + zLift; const plunge = Math.min(12.7, zOffset * 0.75) + zLift;
ControllerMethods.send(` ControllerMethods.send(`
G21 G21
G92 X0 Y0 Z0 G92 X0 Y0 Z0
G38.2 Z -25 F${fastSeek} G38.2 Z -25 F${fastSeek}
G91 G1 Z 1 G91 G1 Z 1
G38.2 Z -2 F${slowSeek} G38.2 Z -2 F${slowSeek}
G92 Z ${zOffset} G92 Z ${zOffset}
G91 G0 Z ${zLift} G91 G0 Z ${zLift}
G91 G0 X 20 G91 G0 X 20
G91 G0 Z ${-plunge} G91 G0 Z ${-plunge}
G38.2 X -20 F${fastSeek} G38.2 X -20 F${fastSeek}
G91 G1 X 1 G91 G1 X 1
G38.2 X -2 F${slowSeek} G38.2 X -2 F${slowSeek}
G92 X ${xOffset} G92 X ${xOffset}
G91 G0 X 1 G91 G0 X 1
G91 G0 Y 20 G91 G0 Y 20
G91 G0 X -20 G91 G0 X -20
G38.2 Y -20 F${fastSeek} G38.2 Y -20 F${fastSeek}
G91 G1 Y 1 G91 G1 Y 1
G38.2 Y -2 F${slowSeek} G38.2 Y -2 F${slowSeek}
G92 Y ${yOffset} G92 Y ${yOffset}
G91 G0 Y 3 G91 G0 Y 3
G91 G0 Z 25 G91 G0 Z 25
M2 M2
`); `);
} }
} }
</script> </script>
@@ -248,27 +260,27 @@
<Title id="simple-title">Probe {probeType}</Title> <Title id="simple-title">Probe {probeType}</Title>
<Content id="simple-content"> <Content id="simple-content">
{#if stage === "TestingProbe" || stage === "TestingProbeComplete"} {#if step === "TestingProbe"}
<p>Attach the probe magnet to the collet.</p> <p>Attach the probe magnet to the collet.</p>
<p>Touch the probe block to the bit.</p> <p>Touch the probe block to the bit.</p>
{#if stage === "TestingProbe"} {#if $probeContacted}
<p>Waiting for probe contact...</p>
{:else}
<p>Probe contact detected!</p> <p>Probe contact detected!</p>
{:else}
<p>Waiting for probe contact...</p>
{/if} {/if}
{:else if stage === "GetToolDiameter"} {:else if step === "GetToolDiameter"}
<label for="tool-diameter">Tool Diameter (mm)</label> <label for="tool-diameter">Tool Diameter (mm)</label>
<input id="tool-diameter" bind:value={toolDiameter} /> <input id="tool-diameter" bind:value={toolDiameter} />
{:else if stage === "Probing"} {:else if step === "Probing"}
<p>Probing in progress...</p> <p>Probing in progress...</p>
{:else if stage === "ProbingFailed"} {:else if step === "ProbingFailed"}
<p>Could not find the probe block during probing!</p> <p>Could not find the probe block during probing!</p>
<p> <p>
Make sure the tip of the bit is about 1/4" (6mm) above the probe block, Make sure the tip of the bit is about 1/4" (6mm) above the probe block,
and try again. and try again.
</p> </p>
{:else if stage === "ProbingComplete"} {:else if step === "ProbingComplete"}
<p>Don't forget to put away the probe!</p> <p>Don't forget to put away the probe!</p>
<p>The machine will now move to the XY origin.</p> <p>The machine will now move to the XY origin.</p>
<p>Watch your hands!</p> <p>Watch your hands!</p>
@@ -276,17 +288,19 @@
</Content> </Content>
<Actions> <Actions>
<Button on:click={() => ($cancelled = true)}> {#if showCancelButton}
<Label>Cancel</Label> <Button on:click={() => ($cancelled = true)}>
</Button> <Label>Cancel</Label>
</Button>
{/if}
<Button <Button
defaultAction defaultAction
data-mdc-dialog-action={confirmButton.allowClose ? "close" : ""} data-mdc-dialog-action={nextButton.allowClose ? "close" : ""}
disabled={confirmButton.disabled} disabled={nextButton.disabled}
on:click={() => ($userAcknowledged = true)} on:click={() => ($userAcknowledged = true)}
> >
<Label> <Label>
{confirmButton.label} {nextButton.label}
</Label> </Label>
</Button> </Button>
</Actions> </Actions>

View File

@@ -1,4 +1,5 @@
type ControllerMethods = { type ControllerMethods = {
stop: () => void;
send: (gcode: string) => void; send: (gcode: string) => void;
goto_zero: (x: number, y: number, z: number, a: number) => void; goto_zero: (x: number, y: number, z: number, a: number) => void;
} }

View File

@@ -2,6 +2,7 @@ import 'polyfill-object.fromentries';
import AdminNetworkView from '$components/AdminNetworkView.svelte'; import AdminNetworkView from '$components/AdminNetworkView.svelte';
import DialogHost, { showDialog } from "$dialogs/DialogHost.svelte"; import DialogHost, { showDialog } from "$dialogs/DialogHost.svelte";
import Devmode from "$components/Devmode.svelte";
import { handleConfigUpdate } from '$lib/ConfigStore'; import { handleConfigUpdate } from '$lib/ConfigStore';
import { init as initNetworkInfo } from '$lib/NetworkInfo'; import { init as initNetworkInfo } from '$lib/NetworkInfo';
import { handleControllerStateUpdate } from "$dialogs/ProbeDialog.svelte"; import { handleControllerStateUpdate } from "$dialogs/ProbeDialog.svelte";
@@ -15,6 +16,9 @@ export function createComponent(component: string, target: HTMLElement, props: R
case "DialogHost": case "DialogHost":
return new DialogHost({ target, props }); return new DialogHost({ target, props });
case "Devmode":
return new Devmode({target, props});
default: default:
throw new Error("Unknown component"); throw new Error("Unknown component");
} }