From 4747c4988c11849062fea9b1680c287b82350afc Mon Sep 17 00:00:00 2001 From: "Eloise Y." <98988862+eloise-nebula@users.noreply.github.com> Date: Fri, 22 Aug 2025 12:57:53 -0700 Subject: [PATCH 01/29] Replaced old device.screen with new event driven device.screen in sliclet.screen. --- .gitignore | 1 + slicops/device/screen.py | 6 + slicops/package_data/device_db.sqlite3 | Bin 1101824 -> 1101824 bytes slicops/pkcli/device_db.py | 1 + slicops/sliclet/screen.py | 159 +++++++++++-------------- slicops/unit_util.py | 13 -- tests/sliclet/screen_data/ioc.yaml | 16 +++ tests/sliclet/screen_test.py | 75 ++++++------ 8 files changed, 131 insertions(+), 140 deletions(-) create mode 100644 tests/sliclet/screen_data/ioc.yaml diff --git a/.gitignore b/.gitignore index 7ac465e..c648774 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ tox.ini \#* .idea/ .vscode/ +*~ # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/slicops/device/screen.py b/slicops/device/screen.py index cde846b..4b31df7 100644 --- a/slicops/device/screen.py +++ b/slicops/device/screen.py @@ -389,7 +389,13 @@ def __handle_monitor(self, change): def _start(self, *args, **kwargs): for a in "acquire", "image", "target_status": + # Needs better handling here if the accessor doesn't exit. + if not self.device.has_accessor(a): + pkdlog(f"Device={self.device} has no accessor={a}") + self.destroy() + return self.device.accessor(a).monitor(self.__handle_monitor) + pkdlog(f"Started monitor={self.device.accessor(a)}") super()._start(*args, **kwargs) def _repr(self): diff --git a/slicops/package_data/device_db.sqlite3 b/slicops/package_data/device_db.sqlite3 index 21564c9afe66c3a92a184178b500d993dcb77ef5..bb2d4eb884a9fb930baefd46d27dee7ee7c38b39 100644 GIT binary patch delta 3612 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 U95jB^_`IC)Io9HnAx}&H199sU`~Uy| delta 3898 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 Date: Sat, 23 Aug 2025 17:44:37 +0000 Subject: [PATCH 02/29] Fix #151 use fconf to generate image for sliclet/screen_test --- slicops/pkcli/ioc.py | 17 +++++++++----- tests/sliclet/screen_data/ioc.py | 37 ++++++++++++++++++++++++++++++ tests/sliclet/screen_data/ioc.yaml | 12 +++++----- tests/sliclet/screen_test.py | 17 +++++--------- 4 files changed, 60 insertions(+), 23 deletions(-) create mode 100644 tests/sliclet/screen_data/ioc.py diff --git a/slicops/pkcli/ioc.py b/slicops/pkcli/ioc.py index 4b9dd48..de01786 100644 --- a/slicops/pkcli/ioc.py +++ b/slicops/pkcli/ioc.py @@ -8,12 +8,21 @@ from pykern.pkdebug import pkdc, pkdlog, pkdp, pkdexc import asyncio import caproto.server +import numpy +import pykern.fconf import pykern.pkio import pykern.pkyaml -import numpy def run(init_yaml, db_yaml=None): + def _fconf(): + p = pykern.pkio.py_path(init_yaml) + if p.check(dir=True): + return pykern.fconf.parse_all(p) + if p.check() and p.ext: + return pykern.fconf.Parser([p]).result + return pykern.fconf.parse_all(path=p.dirpath(), glob=p.basename + "*") + def _normalize(raw): for k, v in raw.items(): if not isinstance(v, dict): @@ -31,11 +40,7 @@ def _pvgroup(config): log_pv_names=False, ) - caproto.server.run( - **_pvgroup( - PKDict(_normalize(pykern.pkyaml.load_file(init_yaml))), - ), - ) + caproto.server.run(**_pvgroup(PKDict(_normalize(_fconf())))) class _PVGroup(caproto.server.PVGroup): diff --git a/tests/sliclet/screen_data/ioc.py b/tests/sliclet/screen_data/ioc.py new file mode 100644 index 0000000..2900eb1 --- /dev/null +++ b/tests/sliclet/screen_data/ioc.py @@ -0,0 +1,37 @@ +import numpy + +_X = 50 +_Y_FACTOR = 1.3 + + +def empty_image(self): + return [0] * 50 * 65 + + +def gaussian_image(self): + sigma = _X // 5 + + def _dist(vec, is_y): + s = _y_adjust(sigma) if is_y else sigma + return (vec - vec.shape[0] // 2) ** 2 / (2 * (s**2)) + + def _norm(mat): + return ((mat - mat.min()) / (mat.max() - mat.min())) * 255 + + def _vec(size): + return numpy.linspace(0, size - 1, size) + + x, y = numpy.meshgrid(_vec(_X), _vec(_y_adjust(_X))) + return _norm(numpy.exp(-(_dist(x, False) + _dist(y, True)))).flatten() + + +def size_x(self): + return _X + + +def size_y(self): + return _y_adjust(_X) + + +def _y_adjust(value): + return int(value * _Y_FACTOR) diff --git a/tests/sliclet/screen_data/ioc.yaml b/tests/sliclet/screen_data/ioc.yaml index 0e3e0db..f5bbc92 100644 --- a/tests/sliclet/screen_data/ioc.yaml +++ b/tests/sliclet/screen_data/ioc.yaml @@ -3,14 +3,14 @@ value: 0 dispatch: 13SIM1:image1:ArrayData: - 0: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - 1: [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3] -13SIM1:image1:ArrayData: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] -13SIM1:cam1:SizeY: 4 -13SIM1:cam1:SizeX: 4 + 0: empty_image() + 1: gaussian_image() +13SIM1:image1:ArrayData: empty_image() +13SIM1:cam1:SizeY: size_y() +13SIM1:cam1:SizeX: size_x() 13SIM1:cam1:PNEUMATIC: dispatch: 13SIM1:cam1:TGT_STS: 0: 1 1: 2 -13SIM1:cam1:TGT_STS: 1 \ No newline at end of file +13SIM1:cam1:TGT_STS: 1 diff --git a/tests/sliclet/screen_test.py b/tests/sliclet/screen_test.py index 4cf7676..dead3b3 100644 --- a/tests/sliclet/screen_test.py +++ b/tests/sliclet/screen_test.py @@ -30,7 +30,7 @@ async def _buttons(s, expect, msg): break return rv - with unit_util.start_ioc("ioc.yaml"): + with unit_util.start_ioc("ioc"): async with unit_util.SlicletSetup("screen") as s: from pykern import pkunit, pkdebug from pykern.pkdebug import pkdc, pkdexc, pkdlog, pkdp @@ -44,17 +44,12 @@ async def _buttons(s, expect, msg): await s.ctx_field_set(start_button=None) await _buttons(s, (False, False, False), "all disabled after start") await _buttons(s, (False, True, False), "acquire should fire") - # plot comes back - r = await s.ctx_update() - # TODO(robnagler) need to test acquire is set around when the plot comes back - # mock epics should handle this - p = r.fields.plot.value - # TODO Need to find a way to get actual data here again. - # pkunit.pkeq(65, len(p.raw_pixels)) - # pkunit.pkeq(50, len(p.raw_pixels[0])) + p = (await s.ctx_update()).fields.plot.value + pkunit.pkeq(65, len(p.raw_pixels)) + pkunit.pkeq(50, len(p.raw_pixels[0])) # x fit should be 10 - # pkunit.pkeq(10.00, round(p.x.fit.results.sig, 2)) - # pkunit.pkeq(13.00, round(p.y.fit.results.sig, 2)) + pkunit.pkeq(10.00, round(p.x.fit.results.sig, 2)) + pkunit.pkeq(13.00, round(p.y.fit.results.sig, 2)) await s.ctx_field_set( beam_path="CU_SPEC", curve_fit_method="super_gaussian", From c5bf978f0889d374306b5fe716b1d35ad7bb3b92 Mon Sep 17 00:00:00 2001 From: "Eloise Y." <98988862+eloise-nebula@users.noreply.github.com> Date: Mon, 25 Aug 2025 12:02:48 -0700 Subject: [PATCH 03/29] Better error handling in screen._ActionLoop --- slicops/device/screen.py | 52 +++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/slicops/device/screen.py b/slicops/device/screen.py index 4b31df7..2d1c450 100644 --- a/slicops/device/screen.py +++ b/slicops/device/screen.py @@ -81,7 +81,7 @@ def __init__(self): self.destroyed = False self.__lock = threading.Lock() self.__actions = queue.Queue() - self.__thread = threading.Thread(target=self._start, daemon=True) + self.__thread = threading.Thread(target=self.__target, daemon=True) if self._loop_timeout_secs > 0 and not hasattr(self, "action_loop_timeout"): raise AssertionError( f"_loop_timeout_secs={self._loop_timeout_secs} and not action_loop_timeout" @@ -137,29 +137,32 @@ def _start(self): timeout_kwarg = PKDict() if self._loop_timeout_secs: timeout_kwarg.timeout = self._loop_timeout_secs + while True: + with self.__lock: + if self.destroyed: + return + try: + m, a = self.__actions.get(**timeout_kwarg) + except queue.Empty: + m, a = self.action_loop_timeout(), None + with self.__lock: + if self.destroyed: + return + # Do not need to check m, because only invalid when destroyed is True + if (m := m(a)) is self._LOOP_END: + return + # Will be true if destroy called inside action (m) + if self.destroyed: + return + # Action returned an external callback, which must occur outside lock + if m: + m() + + def __target(self): try: - while True: - with self.__lock: - if self.destroyed: - return - try: - m, a = self.__actions.get(**timeout_kwarg) - except queue.Empty: - m, a = self.action_loop_timeout(), None - with self.__lock: - if self.destroyed: - return - # Do not need to check m, because only invalid when destroyed is True - if (m := m(a)) is self._LOOP_END: - return - # Will be true if destroy called inside action (m) - if self.destroyed: - return - # Action returned an external callback, which must occur outside lock - if m: - m() + self._start() except Exception as e: - pkdlog("error={} {} stack={}", e, self, pkdexc(simplify=True)) + pkdlog("clyde error={} {} stack={}", e, self, pkdexc(simplify=True)) finally: self.destroy() @@ -389,11 +392,6 @@ def __handle_monitor(self, change): def _start(self, *args, **kwargs): for a in "acquire", "image", "target_status": - # Needs better handling here if the accessor doesn't exit. - if not self.device.has_accessor(a): - pkdlog(f"Device={self.device} has no accessor={a}") - self.destroy() - return self.device.accessor(a).monitor(self.__handle_monitor) pkdlog(f"Started monitor={self.device.accessor(a)}") super()._start(*args, **kwargs) From ad36b290bbb273a06e090010ecd6dd93c9d18582 Mon Sep 17 00:00:00 2001 From: "Eloise Y." <98988862+eloise-nebula@users.noreply.github.com> Date: Mon, 25 Aug 2025 13:46:45 -0700 Subject: [PATCH 04/29] Replaced slicops.device.screen._ActionLoop with pykern.pkasyncio.ActionLoop --- slicops/device/screen.py | 123 +++++---------------------------------- 1 file changed, 15 insertions(+), 108 deletions(-) diff --git a/slicops/device/screen.py b/slicops/device/screen.py index 2d1c450..36bdaeb 100644 --- a/slicops/device/screen.py +++ b/slicops/device/screen.py @@ -6,6 +6,7 @@ from pykern.pkcollections import PKDict from pykern.pkdebug import pkdc, pkdexc, pkdlog, pkdp +from pykern.pkasyncio import ActionLoop from slicops.device import DeviceError import abc import enum @@ -48,7 +49,8 @@ def move_target(self, want_in): want_in (bool): True to insert, and False to remove """ self.__worker.req_action( - self.__worker.action_req_move_target, PKDict(want_in=want_in) + "req_move_target", + PKDict(want_in=want_in) ) @@ -72,101 +74,6 @@ def on_screen_device_update(self, accessor_name, value): pass -class _ActionLoop: - """Generic thread that processes actions in a loop on request""" - - _LOOP_END = object() - - def __init__(self): - self.destroyed = False - self.__lock = threading.Lock() - self.__actions = queue.Queue() - self.__thread = threading.Thread(target=self.__target, daemon=True) - if self._loop_timeout_secs > 0 and not hasattr(self, "action_loop_timeout"): - raise AssertionError( - f"_loop_timeout_secs={self._loop_timeout_secs} and not action_loop_timeout" - ) - self.__thread.start() - - def action(self, method, arg): - """Queue ``method`` to be called in loop thread. - - Actions are methods that (by convention) begin with - ``action_`` and are called sequentially inside `_start`. A - lock is used to prevent `destroy` being called during the action. - - Actions return ``None`` to continue on to the next - action. `_LOOP_END` should be returned to terminate `_start` - (the loop) in which case no further actions are - performed. Actions can return a callable that will be called - inside the loop and outside the lock. These returned callables - are known as external callbacks, that is, functions that may - do anything so holding the lock could be problematic. - - Args: - method (callable): see above - arg (object): passed verbatim to ``method`` - - """ - self.__actions.put_nowait((method, arg)) - - def destroy(self): - """Stops thread and calls subclass `_destroy` - - THREADING: subclasses should not call destroy directly. They should - return `_LOOP_END` instead. External callbacks may call destroy, because - _ActionLoop does not hold lock during external callbacks. - """ - try: - with self.__lock: - if self.destroyed: - return - self.destroyed = True - self.__actions.put_nowait((None, None)) - self._destroy() - except Exception as e: - pkdlog("error={} {} stack={}", e, self, pkdexc(simplify=True)) - - def __repr__(self): - def _destroyed(): - return " DESTROYED" if self.destroyed else "" - - return f"<{self.__class__.__name__}{_destroyed()} self._repr()>" - - def _start(self): - timeout_kwarg = PKDict() - if self._loop_timeout_secs: - timeout_kwarg.timeout = self._loop_timeout_secs - while True: - with self.__lock: - if self.destroyed: - return - try: - m, a = self.__actions.get(**timeout_kwarg) - except queue.Empty: - m, a = self.action_loop_timeout(), None - with self.__lock: - if self.destroyed: - return - # Do not need to check m, because only invalid when destroyed is True - if (m := m(a)) is self._LOOP_END: - return - # Will be true if destroy called inside action (m) - if self.destroyed: - return - # Action returned an external callback, which must occur outside lock - if m: - m() - - def __target(self): - try: - self._start() - except Exception as e: - pkdlog("clyde error={} {} stack={}", e, self, pkdexc(simplify=True)) - finally: - self.destroy() - - class _FSM: """Finite State Machine called by `_Worker` exclusively @@ -193,7 +100,7 @@ def _event_handle_monitor(self, arg, **kwargs): n = arg.accessor.accessor_name if "error" in arg: self.worker.action( - self.worker.action_call_handler, + "call_handler", PKDict( error_kind=ErrorKind.monitor, accessor_name=n, error_msg=arg.error ), @@ -216,7 +123,7 @@ def _event_handle_monitor(self, arg, **kwargs): else: raise AssertionError(f"unsupported accessor={n} {self}") self.worker.action( - self.worker.action_call_handler, PKDict(accessor_name=n, value=v) + "call_handler", PKDict(accessor_name=n, value=v) ) return rv @@ -231,7 +138,7 @@ def _event_move_target( ): if move_target_arg: self.worker.action( - self.worker.action_call_handler, + "call_handler", PKDict(error_kind=ErrorKind.fsm, error_msg="target already moving"), ) return @@ -243,21 +150,21 @@ def _event_move_target( rv = PKDict(move_target_arg=arg) if arg.want_in and upstream_problems is None or upstream_problems: # Recheck the upstream - self.worker.action(self.worker.action_check_upstream, None) + self.worker.action("check_upstream", None) rv.check_upstream = True else: - self.worker.action(self.worker.action_move_target, arg) + self.worker.action("move_target", arg) return rv def _event_upstream_status(self, arg, move_target_arg, **kwargs): rv = PKDict(check_upstream=False, upstream_problems=arg.problems) if arg.problems: self.worker.action( - self.worker.action_call_handler, + "call_handler", PKDict(error_kind=ErrorKind.upstream, error_msg=arg.problems), ) return rv.pkupdate(move_target_arg=None) - self.worker.action(self.worker.action_move_target, move_target_arg) + self.worker.action("move_target", move_target_arg) return rv def __repr__(self): @@ -267,7 +174,7 @@ def _states(curr): return f"<_FSM {self.worker.device.device_name} {_states(self.curr)}>" -class _Upstream(_ActionLoop): +class _Upstream(ActionLoop): """Action loop to check targets of upstream screens""" def __init__(self, worker): @@ -306,14 +213,14 @@ def _destroy(self): def __done(self): self.__worker.action( - self.__worker.action_upstream_status, PKDict(problems=self.__problems) + "upstream_status", PKDict(problems=self.__problems) ) return self._LOOP_END def __handle_status(self, kwargs): if "connected" in kwargs: return - self.action(self.action_handle_status, kwargs) + self.action("handle_status", kwargs) def _start(self, *args, **kwargs): for d in self.__devices.values(): @@ -324,7 +231,7 @@ def _repr(self): return f"pending={sorted(self.__devices)} problems={sorted(self.__problems)}" -class _Worker(_ActionLoop): +class _Worker(ActionLoop): """Action loop for Screen _Worker uses `_FSM` to translate events to actions. Monitor calls @@ -388,7 +295,7 @@ def _destroy(self): u.destroy() def __handle_monitor(self, change): - self.action(self.action_handle_monitor, change) + self.action("handle_monitor", change) def _start(self, *args, **kwargs): for a in "acquire", "image", "target_status": From 82e52d53c8500874502d65c4c46888a3c486edae Mon Sep 17 00:00:00 2001 From: "Eloise Y." <98988862+eloise-nebula@users.noreply.github.com> Date: Wed, 27 Aug 2025 16:26:10 -0700 Subject: [PATCH 05/29] Added ADSimDetector available PVs as dev target_status and target_control. --- slicops/device/__init__.py | 2 ++ slicops/package_data/device_db.sqlite3 | Bin 1101824 -> 1101824 bytes slicops/pkcli/device_db.py | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/slicops/device/__init__.py b/slicops/device/__init__.py index 2912f5c..2cec9a3 100644 --- a/slicops/device/__init__.py +++ b/slicops/device/__init__.py @@ -216,6 +216,8 @@ def _reshape(image): return bool(raw) if self.accessor_name == "image": return _reshape(raw) + elif self.accessor_name == "target_status": + return(raw + 1) return raw def _on_connection(self, **kwargs): diff --git a/slicops/package_data/device_db.sqlite3 b/slicops/package_data/device_db.sqlite3 index bb2d4eb884a9fb930baefd46d27dee7ee7c38b39..1a30ddd154bb17cd7ab1f1d515351b8e9a7fa09d 100644 GIT binary patch delta 219 zcmZoz;M@R2EsQNpEzB(}EvzkUE$l5EEu1Y}E!-_UTXYsScUa=T3< zZ#Scg9E+kRlZ$IuytAXPYmj3}Vo`c(NqlmCUP)1Yj_zbx7DaWY>3&BA#M@b#d4ZS@ zi20YZGz-`=+p}^`XFniN17d6zG-zhAHNGtjG|n2AafZghp1y`w$%(m!R>2viB_*jv fzWFJsX19fawm5@qQRYVyj}LMR+g@>0po0qluQWuR delta 121 zcmZoz;M@R2EsQNpEzB(}EvzkUE$l5EEu1Y}E!-_UTX Date: Wed, 27 Aug 2025 17:00:22 -0700 Subject: [PATCH 06/29] Added buttons to screen UI that change the target status. --- slicops/package_data/sliclet/screen.yaml | 13 +++++++++ slicops/sliclet/screen.py | 35 +++++++++++++++++++----- tests/sliclet/screen_data/ioc.yaml | 6 ---- 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/slicops/package_data/sliclet/screen.yaml b/slicops/package_data/sliclet/screen.yaml index 7c07650..e4f4e9f 100644 --- a/slicops/package_data/sliclet/screen.yaml +++ b/slicops/package_data/sliclet/screen.yaml @@ -50,6 +50,16 @@ fields: ui: css_kind: danger label: Stop + target_in_button: + prototype: Button + ui: + css_kind: primary + label: In + target_out_button: + prototype: Button + ui: + css_kind: primary + label: Out ui_layout: - cols: @@ -62,6 +72,9 @@ ui_layout: - start_button - stop_button - single_button + - cell_group: + - target_in_button + - target_out_button - css: col-sm-9 col-xxl-7 rows: - plot diff --git a/slicops/sliclet/screen.py b/slicops/sliclet/screen.py index 216b2b1..a438e35 100644 --- a/slicops/sliclet/screen.py +++ b/slicops/sliclet/screen.py @@ -59,6 +59,11 @@ ("plot.ui.visible", True), ) +# Target Controls +_CTL_OUT = 0 +_CTL_IN = 1 +_STS_OUT = 1 +_STS_IN = 2 class Screen(slicops.sliclet.Base): def handle_destroy(self): @@ -83,6 +88,13 @@ def on_click_start_button(self, txn, **kwargs): def on_click_stop_button(self, txn, **kwargs): self.__set_acquire(txn, False) + def on_click_target_in_button(self, txn, **kwargs): + pkdlog("in") + self.__set_target(txn, _CTL_IN) + + def on_click_target_out_button(self, txn, **kwargs): + self.__set_target(txn, _CTL_OUT) + def handle_init(self, txn): self.__device = None self.__handler = None @@ -192,7 +204,10 @@ def __handle_acquire(self, acquire): self.__single_button = False def __handle_error(self, accessor_name, error_kind, error_msg): - pkdlog("error={} accessor={} msg={}", error_kind, accessor_name, error_msg) + m = f"{error_kind} accessor={accessor_name} msg={error_msg}" + pkdlog("error=" + m) + e = pykern.util.APIError(m) + self._put_work(slicops.sliclet._Work.error, f"error={e}") def __handle_image(self, image): with self.lock_for_update() as txn: @@ -223,6 +238,16 @@ def __set_acquire(self, txn, acquire): ) raise pykern.util.APIError(e) + def __set_target(self, txn, target): + try: + self.__device.put("target_control", target) + except slicops.device.DeviceError as e: + pkdlog( + "error={} on {}, clearing camera; stack={}", e, self.__device, pkdexc() + ) + raise pykern.util.APIError(e) + + # def __target_moved(self, status): # if status is failed: # display error @@ -283,16 +308,12 @@ def on_screen_device_error(self, accessor_name, error_kind, error_msg): self.__device_error(accessor_name, error_kind, error_msg) def on_screen_device_update(self, accessor_name, value): - # TODO move business logic here # TODO move prev value to sliclet within txn - def _update(d): - self.__value.update(d) - if accessor_name == "image": - _update(PKDict(image=value)) + self.__value.update(PKDict(image=value)) self.__handle_image(value) elif accessor_name == "acquire": - _update(PKDict(acquire=value)) + self.__value.update(PKDict(acquire=value)) self.__handle_acquire(value) elif accessor_name == "target_status": pass diff --git a/tests/sliclet/screen_data/ioc.yaml b/tests/sliclet/screen_data/ioc.yaml index f5bbc92..4d47095 100644 --- a/tests/sliclet/screen_data/ioc.yaml +++ b/tests/sliclet/screen_data/ioc.yaml @@ -8,9 +8,3 @@ 13SIM1:image1:ArrayData: empty_image() 13SIM1:cam1:SizeY: size_y() 13SIM1:cam1:SizeX: size_x() -13SIM1:cam1:PNEUMATIC: - dispatch: - 13SIM1:cam1:TGT_STS: - 0: 1 - 1: 2 -13SIM1:cam1:TGT_STS: 1 From acb376af14d82d04a6f39bdcac5615021ed6be12 Mon Sep 17 00:00:00 2001 From: "Eloise Y." <98988862+eloise-nebula@users.noreply.github.com> Date: Thu, 28 Aug 2025 17:36:05 -0700 Subject: [PATCH 07/29] Added In/Out Label in Screen UI. sliclet.screen now uses device.Screen.move_target to move the target. Fixed Bug Where upstream_status Would Never Fire With No Upstream Devices. --- slicops/device/screen.py | 11 +++- slicops/package_data/sliclet/screen.yaml | 7 +++ slicops/sliclet/screen.py | 67 +++++++++++------------- 3 files changed, 46 insertions(+), 39 deletions(-) diff --git a/slicops/device/screen.py b/slicops/device/screen.py index 36bdaeb..a4293b9 100644 --- a/slicops/device/screen.py +++ b/slicops/device/screen.py @@ -139,7 +139,11 @@ def _event_move_target( if move_target_arg: self.worker.action( "call_handler", - PKDict(error_kind=ErrorKind.fsm, error_msg="target already moving"), + PKDict( + accessor_name="", + error_kind=ErrorKind.fsm, + error_msg="target already moving" + ), ) return if target_status is not None and arg.want_in == target_status: @@ -186,6 +190,10 @@ def _names(): self.__worker = worker self.__problems = PKDict() self.__devices = PKDict({u: slicops.device.Device(u) for u in _names()}) + if len(self.__devices) == 0: + self.__done() + self._destroy() + return self._loop_timeout_secs = _cfg.upstream_timeout_secs super().__init__() @@ -300,7 +308,6 @@ def __handle_monitor(self, change): def _start(self, *args, **kwargs): for a in "acquire", "image", "target_status": self.device.accessor(a).monitor(self.__handle_monitor) - pkdlog(f"Started monitor={self.device.accessor(a)}") super()._start(*args, **kwargs) def _repr(self): diff --git a/slicops/package_data/sliclet/screen.yaml b/slicops/package_data/sliclet/screen.yaml index e4f4e9f..0c7e516 100644 --- a/slicops/package_data/sliclet/screen.yaml +++ b/slicops/package_data/sliclet/screen.yaml @@ -35,6 +35,12 @@ fields: label: PV writable: false widget: static + target_status: + prototype: String + ui: + label: Target Status + writable: false + widget: static single_button: prototype: Button ui: @@ -75,6 +81,7 @@ ui_layout: - cell_group: - target_in_button - target_out_button + - target_status - css: col-sm-9 col-xxl-7 rows: - plot diff --git a/slicops/sliclet/screen.py b/slicops/sliclet/screen.py index a438e35..812c01d 100644 --- a/slicops/sliclet/screen.py +++ b/slicops/sliclet/screen.py @@ -66,6 +66,10 @@ _STS_IN = 2 class Screen(slicops.sliclet.Base): + def __init__(self, *args): + self.__prev_value = PKDict(acquire=None, image=None) + super().__init__(*args) + def handle_destroy(self): self.__device_destroy() @@ -89,7 +93,6 @@ def on_click_stop_button(self, txn, **kwargs): self.__set_acquire(txn, False) def on_click_target_in_button(self, txn, **kwargs): - pkdlog("in") self.__set_target(txn, _CTL_IN) def on_click_target_out_button(self, txn, **kwargs): @@ -171,8 +174,11 @@ def __device_destroy(self, txn=None): def __device_setup(self, txn, beam_path, camera): self.__handler = _Handler( self.__handle_error, - self.__handle_image, - self.__handle_acquire + PKDict( + image=self.__handle_image, + acquire=self.__handle_acquire, + target_status=self.__handle_target + ) ) try: # If there's an epics issues, we have to clear the device @@ -190,6 +196,7 @@ def __device_setup(self, txn, beam_path, camera): def __handle_acquire(self, acquire): with self.lock_for_update() as txn: + self.__prev_value["acquire"] = acquire n = not acquire # Leave plot alone txn.multi_set( @@ -204,13 +211,11 @@ def __handle_acquire(self, acquire): self.__single_button = False def __handle_error(self, accessor_name, error_kind, error_msg): - m = f"{error_kind} accessor={accessor_name} msg={error_msg}" - pkdlog("error=" + m) - e = pykern.util.APIError(m) - self._put_work(slicops.sliclet._Work.error, f"error={e}") + pkdlog(f"error={error_kind} accessor={accessor_name} msg={error_msg}") def __handle_image(self, image): with self.lock_for_update() as txn: + self.__prev_value["image"] = image if self.__update_plot(txn) and self.__single_button: # self.__single_button = False self.__set_acquire(txn, False) @@ -219,11 +224,16 @@ def __handle_image(self, image): ("start_button.ui.enabled", True), ) + def __handle_target(self, status): + with self.lock_for_update() as txn: + msg = "In" if status else "Out" + txn.field_set("target_status", msg) + def __set_acquire(self, txn, acquire): if not self.__device or not self.__handler: # buttons already disabled return - v = self.__handler.prev_value("acquire") + v = self.__prev_value["acquire"] if v is not None and v == acquire: # No button disable since nothing changed return @@ -240,7 +250,7 @@ def __set_acquire(self, txn, acquire): def __set_target(self, txn, target): try: - self.__device.put("target_control", target) + self.__device.move_target(want_in=bool(target)) except slicops.device.DeviceError as e: pkdlog( "error={} on {}, clearing camera; stack={}", e, self.__device, pkdexc() @@ -260,7 +270,7 @@ def __update_plot(self, txn): if not self.__device or not self.__handler: return False # TOOD Change previous value to current value (nominally). - if (i := self.__handler.prev_value("image")) is None or not i.size: + if (i := self.__prev_value["image"]) is None or not i.size: return False if not txn.group_get("plot", "ui", "visible"): txn.multi_set(_PLOT_ENABLE) @@ -278,47 +288,30 @@ def __user_alert(self, txn, fmt, *args): class _Handler(slicops.device.screen.EventHandler): - def __init__(self, device_error, handle_image, handle_acquire): + def __init__(self, handle_error, handle_device): self.__destroyed = False - self.__value = PKDict( - acquire=None, - image=None - ) self.__lock = threading.Lock() - self.__device_error = device_error - self.__handle_image = handle_image - self.__handle_acquire = handle_acquire + self.__handle_error = handle_error + self.__handle_device = handle_device.copy() def destroy(self): with self.__lock: if self.__destroyed: return self.__destroyed = True - self.__value = PKDict() - self.__device_error = None - self.__device_update = None - - def prev_value(self, accessor): - with self.__lock: - if self.__destroyed: - return - return self.__value[accessor] + self.__handle_error = None + self.__handle_device = None def on_screen_device_error(self, accessor_name, error_kind, error_msg): - self.__device_error(accessor_name, error_kind, error_msg) + self.__handle_error(accessor_name, error_kind, error_msg) def on_screen_device_update(self, accessor_name, value): # TODO move prev value to sliclet within txn - if accessor_name == "image": - self.__value.update(PKDict(image=value)) - self.__handle_image(value) - elif accessor_name == "acquire": - self.__value.update(PKDict(acquire=value)) - self.__handle_acquire(value) - elif accessor_name == "target_status": - pass - else: + if not accessor_name in self.__handle_device: raise AssertionError(f"unsupported accessor={n} {self}") + h = self.__handle_device[accessor_name] + h(value) + def _init(): global _cfg From 075204068022d72bbbd5e87cdaa946445a64ae9c Mon Sep 17 00:00:00 2001 From: "Eloise Y." <98988862+eloise-nebula@users.noreply.github.com> Date: Fri, 29 Aug 2025 12:34:54 -0700 Subject: [PATCH 08/29] fmt --- slicops/device/__init__.py | 2 +- slicops/device/screen.py | 15 ++++----------- slicops/sliclet/screen.py | 15 ++++++--------- 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/slicops/device/__init__.py b/slicops/device/__init__.py index 2cec9a3..a60a858 100644 --- a/slicops/device/__init__.py +++ b/slicops/device/__init__.py @@ -217,7 +217,7 @@ def _reshape(image): if self.accessor_name == "image": return _reshape(raw) elif self.accessor_name == "target_status": - return(raw + 1) + return raw + 1 return raw def _on_connection(self, **kwargs): diff --git a/slicops/device/screen.py b/slicops/device/screen.py index a4293b9..399b4da 100644 --- a/slicops/device/screen.py +++ b/slicops/device/screen.py @@ -48,10 +48,7 @@ def move_target(self, want_in): Args: want_in (bool): True to insert, and False to remove """ - self.__worker.req_action( - "req_move_target", - PKDict(want_in=want_in) - ) + self.__worker.req_action("req_move_target", PKDict(want_in=want_in)) class ErrorKind(enum.Enum): @@ -122,9 +119,7 @@ def _event_handle_monitor(self, arg, **kwargs): rv = PKDict(move_target_arg=None, target_status=v) else: raise AssertionError(f"unsupported accessor={n} {self}") - self.worker.action( - "call_handler", PKDict(accessor_name=n, value=v) - ) + self.worker.action("call_handler", PKDict(accessor_name=n, value=v)) return rv def _event_move_target( @@ -142,7 +137,7 @@ def _event_move_target( PKDict( accessor_name="", error_kind=ErrorKind.fsm, - error_msg="target already moving" + error_msg="target already moving", ), ) return @@ -220,9 +215,7 @@ def _destroy(self): x.destroy() def __done(self): - self.__worker.action( - "upstream_status", PKDict(problems=self.__problems) - ) + self.__worker.action("upstream_status", PKDict(problems=self.__problems)) return self._LOOP_END def __handle_status(self, kwargs): diff --git a/slicops/sliclet/screen.py b/slicops/sliclet/screen.py index 812c01d..d19173e 100644 --- a/slicops/sliclet/screen.py +++ b/slicops/sliclet/screen.py @@ -65,11 +65,12 @@ _STS_OUT = 1 _STS_IN = 2 + class Screen(slicops.sliclet.Base): def __init__(self, *args): self.__prev_value = PKDict(acquire=None, image=None) super().__init__(*args) - + def handle_destroy(self): self.__device_destroy() @@ -116,10 +117,7 @@ def handle_init(self, txn): txn.field_set("camera", c) def handle_start(self, txn): - self.__device_setup(txn, - txn.field_get("beam_path"), - txn.field_get("camera") - ) + self.__device_setup(txn, txn.field_get("beam_path"), txn.field_get("camera")) def __beam_path_change(self, txn, value): def _choices(): @@ -177,8 +175,8 @@ def __device_setup(self, txn, beam_path, camera): PKDict( image=self.__handle_image, acquire=self.__handle_acquire, - target_status=self.__handle_target - ) + target_status=self.__handle_target, + ), ) try: # If there's an epics issues, we have to clear the device @@ -257,7 +255,6 @@ def __set_target(self, txn, target): ) raise pykern.util.APIError(e) - # def __target_moved(self, status): # if status is failed: # display error @@ -321,7 +318,7 @@ def _init(): beam_path=("DEV_BEAM_PATH", str, "dev beam path name"), camera=("DEV_CAMERA", str, "dev camera name"), ), - ) + ) _init() From daff5115500aacb025eec5d61e3dddb1e9f58eac Mon Sep 17 00:00:00 2001 From: "Eloise Y." <98988862+eloise-nebula@users.noreply.github.com> Date: Tue, 2 Sep 2025 16:03:21 -0700 Subject: [PATCH 09/29] target_status fixup only applies to dev camera as intended --- slicops/device/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/slicops/device/__init__.py b/slicops/device/__init__.py index a60a858..b8685f4 100644 --- a/slicops/device/__init__.py +++ b/slicops/device/__init__.py @@ -216,7 +216,10 @@ def _reshape(image): return bool(raw) if self.accessor_name == "image": return _reshape(raw) - elif self.accessor_name == "target_status": + if ( + self.accessor_name == "target_status" + and self.device.device_name == "DEV_CAMERA" + ): return raw + 1 return raw From 184eddda47bf9c6542d6a9359d9064f95e611d87 Mon Sep 17 00:00:00 2001 From: "Eloise Y." <98988862+eloise-nebula@users.noreply.github.com> Date: Wed, 3 Sep 2025 09:53:17 -0700 Subject: [PATCH 10/29] Addressed 150 to propogate action loop errors to the UI. --- slicops/device/screen.py | 15 ++++++++++++--- slicops/sliclet/__init__.py | 3 +++ slicops/sliclet/screen.py | 33 +++++++++++++++++++++++---------- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/slicops/device/screen.py b/slicops/device/screen.py index 399b4da..b504433 100644 --- a/slicops/device/screen.py +++ b/slicops/device/screen.py @@ -62,6 +62,10 @@ class ErrorKind(enum.Enum): class EventHandler: """Clients of DeviceScreen must implement this""" + @abc.abstractmethod + def on_screen_worker_error(self, exception): + pass + @abc.abstractmethod def on_screen_device_error(self, accessor_name, error_kind, error_msg): pass @@ -299,9 +303,14 @@ def __handle_monitor(self, change): self.action("handle_monitor", change) def _start(self, *args, **kwargs): - for a in "acquire", "image", "target_status": - self.device.accessor(a).monitor(self.__handle_monitor) - super()._start(*args, **kwargs) + try: + for a in "acquire", "image", "target_status": + self.device.accessor(a).monitor(self.__handle_monitor) + super()._start(*args, **kwargs) + except Exception as e: + msg = f"error='{e}' in worker loop. See log for stack trace." + self.__handler.on_screen_worker_error(msg) + raise def _repr(self): return f"device={self.device.device_name}" diff --git a/slicops/sliclet/__init__.py b/slicops/sliclet/__init__.py index 2a6028e..afdc5d6 100644 --- a/slicops/sliclet/__init__.py +++ b/slicops/sliclet/__init__.py @@ -75,6 +75,9 @@ def handle_init(self, txn): def handle_start(self, txn): pass + def put_error(self, exception): + self.__put_work(_Work.error, exception) + @contextlib.contextmanager def lock_for_update(self, log_op=None): ok = True diff --git a/slicops/sliclet/screen.py b/slicops/sliclet/screen.py index d19173e..62735bb 100644 --- a/slicops/sliclet/screen.py +++ b/slicops/sliclet/screen.py @@ -171,7 +171,8 @@ def __device_destroy(self, txn=None): def __device_setup(self, txn, beam_path, camera): self.__handler = _Handler( - self.__handle_error, + self.__handle_worker_error, + self.__handle_device_error, PKDict( image=self.__handle_image, acquire=self.__handle_acquire, @@ -208,7 +209,7 @@ def __handle_acquire(self, acquire): if not acquire: self.__single_button = False - def __handle_error(self, accessor_name, error_kind, error_msg): + def __handle_device_error(self, accessor_name, error_kind, error_msg): pkdlog(f"error={error_kind} accessor={accessor_name} msg={error_msg}") def __handle_image(self, image): @@ -227,6 +228,9 @@ def __handle_target(self, status): msg = "In" if status else "Out" txn.field_set("target_status", msg) + def __handle_worker_error(self, exception): + self.put_error(exception) + def __set_acquire(self, txn, acquire): if not self.__device or not self.__handler: # buttons already disabled @@ -285,28 +289,37 @@ def __user_alert(self, txn, fmt, *args): class _Handler(slicops.device.screen.EventHandler): - def __init__(self, handle_error, handle_device): + def __init__( + self, + handle_worker_error, + handle_device_error, + handle_device_update, + ): self.__destroyed = False self.__lock = threading.Lock() - self.__handle_error = handle_error - self.__handle_device = handle_device.copy() + self.__handle_worker_error = handle_worker_error + self.__handle_device_error = handle_device_error + self.__handle_device_update = handle_device_update def destroy(self): with self.__lock: if self.__destroyed: return self.__destroyed = True - self.__handle_error = None - self.__handle_device = None + self.__handle_device_error = None + self.__handle_device_update = None + + def on_screen_worker_error(self, exception): + self.__handle_worker_error(exception) def on_screen_device_error(self, accessor_name, error_kind, error_msg): - self.__handle_error(accessor_name, error_kind, error_msg) + self.__handle_device_error(accessor_name, error_kind, error_msg) def on_screen_device_update(self, accessor_name, value): # TODO move prev value to sliclet within txn - if not accessor_name in self.__handle_device: + if not accessor_name in self.__handle_device_update: raise AssertionError(f"unsupported accessor={n} {self}") - h = self.__handle_device[accessor_name] + h = self.__handle_device_update[accessor_name] h(value) From cb5ee219fd0a1e1a5346a4e158d46f1f1c2653c9 Mon Sep 17 00:00:00 2001 From: "Eloise Y." <98988862+eloise-nebula@users.noreply.github.com> Date: Thu, 4 Sep 2025 16:32:04 -0700 Subject: [PATCH 11/29] Added Exception Handling to Sliclet --- slicops/device/screen.py | 56 ++++++++++++++++++++++------------- slicops/sliclet/__init__.py | 10 +++---- slicops/sliclet/screen.py | 58 +++++++++++++++---------------------- 3 files changed, 64 insertions(+), 60 deletions(-) diff --git a/slicops/device/screen.py b/slicops/device/screen.py index b504433..28d31b0 100644 --- a/slicops/device/screen.py +++ b/slicops/device/screen.py @@ -5,7 +5,7 @@ """ from pykern.pkcollections import PKDict -from pykern.pkdebug import pkdc, pkdexc, pkdlog, pkdp +from pykern.pkdebug import pkdc, pkdexc, pkdlog, pkdp, pkdformat from pykern.pkasyncio import ActionLoop from slicops.device import DeviceError import abc @@ -63,11 +63,7 @@ class EventHandler: """Clients of DeviceScreen must implement this""" @abc.abstractmethod - def on_screen_worker_error(self, exception): - pass - - @abc.abstractmethod - def on_screen_device_error(self, accessor_name, error_kind, error_msg): + def on_screen_device_error(self, exc): pass @abc.abstractmethod @@ -102,8 +98,11 @@ def _event_handle_monitor(self, arg, **kwargs): if "error" in arg: self.worker.action( "call_handler", - PKDict( - error_kind=ErrorKind.monitor, accessor_name=n, error_msg=arg.error + ScreenError( + device=self.worker.device.device_name, + error_kind=ErrorKind.monitor, + accessor_name=n, + error_msg=arg.error ), ) if n == "target_status": @@ -138,8 +137,8 @@ def _event_move_target( if move_target_arg: self.worker.action( "call_handler", - PKDict( - accessor_name="", + ScreenError( + device=self.worker.device.device_name, error_kind=ErrorKind.fsm, error_msg="target already moving", ), @@ -164,7 +163,11 @@ def _event_upstream_status(self, arg, move_target_arg, **kwargs): if arg.problems: self.worker.action( "call_handler", - PKDict(error_kind=ErrorKind.upstream, error_msg=arg.problems), + ScreenError( + device=self.worker.device.device_name, + error_kind=ErrorKind.upstream, + error_msg=arg.problems, + ), ) return rv.pkupdate(move_target_arg=None) self.worker.action("move_target", move_target_arg) @@ -176,6 +179,15 @@ def _states(curr): return f"<_FSM {self.worker.device.device_name} {_states(self.curr)}>" +class ScreenError(Exception): + def __init__(self, **kwargs): + def _arg_str(): + return pkdformat( + " ".join(k + "={" + k + "}" for k in sorted(kwargs)), + **kwargs, + ) + super().__init__(_arg_str()) + class _Upstream(ActionLoop): """Action loop to check targets of upstream screens""" @@ -259,7 +271,7 @@ def __init__(self, beam_path, handler, device): def action_call_handler(self, arg): m = ( self.__handler.on_screen_device_error - if "error_kind" in arg + if isinstance(arg, Exception) else self.__handler.on_screen_device_update ) # Denormalized state so no need for lock during call @@ -280,6 +292,7 @@ def action_move_target(self, arg): return None def action_req_move_target(self, arg): + raise AssertionError("broken") self.__fsm.event("move_target", arg) return None @@ -299,18 +312,21 @@ def _destroy(self): (u, self.__upstream) = (self.__upstream, None) u.destroy() + def _handle_exception(self, exc): + self.__handler.on_screen_device_error( + ScreenError( + device=self.device.device_name, + error=exc, + ) + ) + def __handle_monitor(self, change): self.action("handle_monitor", change) def _start(self, *args, **kwargs): - try: - for a in "acquire", "image", "target_status": - self.device.accessor(a).monitor(self.__handle_monitor) - super()._start(*args, **kwargs) - except Exception as e: - msg = f"error='{e}' in worker loop. See log for stack trace." - self.__handler.on_screen_worker_error(msg) - raise + for a in "acquire", "image", "target_status": + self.device.accessor(a).monitor(self.__handle_monitor) + super()._start(*args, **kwargs) def _repr(self): return f"device={self.device.device_name}" diff --git a/slicops/sliclet/__init__.py b/slicops/sliclet/__init__.py index afdc5d6..06d20f0 100644 --- a/slicops/sliclet/__init__.py +++ b/slicops/sliclet/__init__.py @@ -75,9 +75,6 @@ def handle_init(self, txn): def handle_start(self, txn): pass - def put_error(self, exception): - self.__put_work(_Work.error, exception) - @contextlib.contextmanager def lock_for_update(self, log_op=None): ok = True @@ -109,12 +106,15 @@ def lock_for_update(self, log_op=None): if log_op: d += f" op={log_op}" except Exception as e2: - pkdlog("error={} during exception stack={}", e2, pkdexc()) + pkdlog("error={} during exception stack={}", e2, pkdexc(simplify=True)) if not isinstance(e, pykern.util.APIError): - pkdlog("stack={}", pkdexc()) + pkdlog("stack={}", pkdexc(simplify=True)) pkdlog("ERROR {}", d) self.__put_work(_Work.error, PKDict(desc=d)) + def put_exception(self, exc): + self.__put_work(_Work.error, exc) + def session_end(self): self.__put_work(_Work.session_end, None) diff --git a/slicops/sliclet/screen.py b/slicops/sliclet/screen.py index 62735bb..8aa3d01 100644 --- a/slicops/sliclet/screen.py +++ b/slicops/sliclet/screen.py @@ -59,16 +59,14 @@ ("plot.ui.visible", True), ) -# Target Controls -_CTL_OUT = 0 -_CTL_IN = 1 -_STS_OUT = 1 -_STS_IN = 2 - class Screen(slicops.sliclet.Base): def __init__(self, *args): - self.__prev_value = PKDict(acquire=None, image=None) + self.__prev_value = PKDict( + acquire=None, + image=None, + target_status=None + ) super().__init__(*args) def handle_destroy(self): @@ -94,10 +92,10 @@ def on_click_stop_button(self, txn, **kwargs): self.__set_acquire(txn, False) def on_click_target_in_button(self, txn, **kwargs): - self.__set_target(txn, _CTL_IN) + self.__set_target(txn, True) def on_click_target_out_button(self, txn, **kwargs): - self.__set_target(txn, _CTL_OUT) + self.__set_target(txn, False) def handle_init(self, txn): self.__device = None @@ -171,7 +169,6 @@ def __device_destroy(self, txn=None): def __device_setup(self, txn, beam_path, camera): self.__handler = _Handler( - self.__handle_worker_error, self.__handle_device_error, PKDict( image=self.__handle_image, @@ -209,8 +206,8 @@ def __handle_acquire(self, acquire): if not acquire: self.__single_button = False - def __handle_device_error(self, accessor_name, error_kind, error_msg): - pkdlog(f"error={error_kind} accessor={accessor_name} msg={error_msg}") + def __handle_device_error(self, exc): + self.put_exception(exc) def __handle_image(self, image): with self.lock_for_update() as txn: @@ -224,12 +221,9 @@ def __handle_image(self, image): ) def __handle_target(self, status): + self.__prev_value["target_status"] = status with self.lock_for_update() as txn: - msg = "In" if status else "Out" - txn.field_set("target_status", msg) - - def __handle_worker_error(self, exception): - self.put_error(exception) + txn.field_set("target_status", "In" if status else "Out") def __set_acquire(self, txn, acquire): if not self.__device or not self.__handler: @@ -239,9 +233,8 @@ def __set_acquire(self, txn, acquire): if v is not None and v == acquire: # No button disable since nothing changed return - if txn: - # No presses until we get a response from device - txn.multi_set(_BUTTONS_DISABLE) + # No presses until we get a response from device + txn.multi_set(_BUTTONS_DISABLE) try: self.__device.put("acquire", acquire) except slicops.device.DeviceError as e: @@ -250,14 +243,14 @@ def __set_acquire(self, txn, acquire): ) raise pykern.util.APIError(e) - def __set_target(self, txn, target): - try: - self.__device.move_target(want_in=bool(target)) - except slicops.device.DeviceError as e: - pkdlog( - "error={} on {}, clearing camera; stack={}", e, self.__device, pkdexc() - ) - raise pykern.util.APIError(e) + def __set_target(self, txn, want_in): + if not self.__device or not self.__handler: + # buttons already disabled + return + v = self.__prev_value["target_status"] + if v is not None and v == want_in: + return + self.__device.move_target(want_in=want_in) # def __target_moved(self, status): # if status is failed: @@ -291,13 +284,11 @@ def __user_alert(self, txn, fmt, *args): class _Handler(slicops.device.screen.EventHandler): def __init__( self, - handle_worker_error, handle_device_error, handle_device_update, ): self.__destroyed = False self.__lock = threading.Lock() - self.__handle_worker_error = handle_worker_error self.__handle_device_error = handle_device_error self.__handle_device_update = handle_device_update @@ -309,11 +300,8 @@ def destroy(self): self.__handle_device_error = None self.__handle_device_update = None - def on_screen_worker_error(self, exception): - self.__handle_worker_error(exception) - - def on_screen_device_error(self, accessor_name, error_kind, error_msg): - self.__handle_device_error(accessor_name, error_kind, error_msg) + def on_screen_device_error(self, exc): + self.__handle_device_error(exc) def on_screen_device_update(self, accessor_name, value): # TODO move prev value to sliclet within txn From 9b075e1685deaa162f84d0f3e79691dffeba085c Mon Sep 17 00:00:00 2001 From: Rob Nagler <5495179+robnagler@users.noreply.github.com> Date: Wed, 10 Sep 2025 23:04:58 +0000 Subject: [PATCH 12/29] limit images to 2 frames a second --- slicops/device/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/slicops/device/__init__.py b/slicops/device/__init__.py index b8685f4..d0d4345 100644 --- a/slicops/device/__init__.py +++ b/slicops/device/__init__.py @@ -7,6 +7,7 @@ from pykern.pkcollections import PKDict from pykern.pkdebug import pkdc, pkdexc, pkdlog, pkdp import epics +import time import slicops.device_db import threading @@ -244,6 +245,12 @@ def _on_value(self, **kwargs): if self.meta.accessor_name == "image" and not len(v): pkdlog("empty image received {}", self) return + if self.meta.accessor_name == "image": + # 2 frames a second + t = int(time.time() * 2) / 2 + if t == self._last_time: + return + self._last_time = t self._run_callback(value=self._fixup_value(v)) except Exception as e: pkdlog("error={} {} stack={}", e, self, pkdexc()) @@ -269,6 +276,7 @@ def __pv(self): # from within a monitor callback. # TODO(robnagler) need a better way of dealing with this self._image_shape = (self.device.get("n_row"), self.device.get("n_col")) + self._last_time = 0 self._pv = epics.PV( self.meta.pv_name, connection_callback=self._on_connection, From c0606a9344624bd38ecf1b7c1b421de19907cccf Mon Sep 17 00:00:00 2001 From: Eloise Y <98988862+eloise-nebula@users.noreply.github.com> Date: Thu, 11 Sep 2025 13:39:43 -0700 Subject: [PATCH 13/29] fixed device_setup bug in sliclet.screen. --- package-lock.json | 6 ++++++ slicops/sliclet/screen.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..acc91cf --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "slicops", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/slicops/sliclet/screen.py b/slicops/sliclet/screen.py index 90fed39..feb406b 100644 --- a/slicops/sliclet/screen.py +++ b/slicops/sliclet/screen.py @@ -148,7 +148,7 @@ def _choices(): def __device_change(self, txn, beam_path, camera): self.__device_destroy(txn) txn.multi_set(_DEVICE_DISABLE) - self.__device_setup(txn, camera) + self.__device_setup(txn, beam_path, camera) def __device_destroy(self, txn=None): if not self.__device: From eb48b1a3658f046c89461cd4ae7ccbcd2ad8f226 Mon Sep 17 00:00:00 2001 From: Eloise Y <98988862+eloise-nebula@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:34:49 -0700 Subject: [PATCH 14/29] removed assertionerror --- slicops/device/screen.py | 1 - 1 file changed, 1 deletion(-) diff --git a/slicops/device/screen.py b/slicops/device/screen.py index 28d31b0..50976d0 100644 --- a/slicops/device/screen.py +++ b/slicops/device/screen.py @@ -292,7 +292,6 @@ def action_move_target(self, arg): return None def action_req_move_target(self, arg): - raise AssertionError("broken") self.__fsm.event("move_target", arg) return None From c49e03d4021077a3f3757a2e8d6c41408b0abe5e Mon Sep 17 00:00:00 2001 From: Eloise Y <98988862+eloise-nebula@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:57:48 -0700 Subject: [PATCH 15/29] fixed call handler bug --- slicops/device/screen.py | 2 +- slicops/package_data/device_db.sqlite3 | Bin 1101824 -> 1101824 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/slicops/device/screen.py b/slicops/device/screen.py index 50976d0..87610c2 100644 --- a/slicops/device/screen.py +++ b/slicops/device/screen.py @@ -275,7 +275,7 @@ def action_call_handler(self, arg): else self.__handler.on_screen_device_update ) # Denormalized state so no need for lock during call - return lambda: m(**arg) + return lambda: m(**arg if isinstance(arg, dict) else arg) def action_check_upstream(self, arg): self.__upstream = _Upstream(self) diff --git a/slicops/package_data/device_db.sqlite3 b/slicops/package_data/device_db.sqlite3 index 1a30ddd154bb17cd7ab1f1d515351b8e9a7fa09d..fea55a228225128630825359cf9630b56e6f8f70 100644 GIT binary patch delta 100 zcmZoz;M}mld4e<}*F+g-RxSqJN1BZ(ttpJHDNL;?%v)1fjDr{rr_TstF`XvH%-Vh_ nhy{p2G#e1J12G2>a{@6J5OV`D4-oSLF&_}~Z@&~IpjQh3ymTa} delta 100 zcmZoz;M}mld4e<}$3z)tRt^TevhK!|))dCp6sFb`=B+6##zE7>m|3UK31Ts1G;BW? o!~(>uK+Fcj>_E%`#GF9P1;pGy%mc){K+Ffk{M*k32^iG^0I{qjYXATM From f54c7ff1ef9d0a09e1f82c971b0355ba706b8ed0 Mon Sep 17 00:00:00 2001 From: Eloise Y <98988862+eloise-nebula@users.noreply.github.com> Date: Thu, 11 Sep 2025 15:03:44 -0700 Subject: [PATCH 16/29] b --- slicops/device/__init__.py | 3 ++- slicops/device/screen.py | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/slicops/device/__init__.py b/slicops/device/__init__.py index d0d4345..2685c89 100644 --- a/slicops/device/__init__.py +++ b/slicops/device/__init__.py @@ -247,7 +247,8 @@ def _on_value(self, **kwargs): return if self.meta.accessor_name == "image": # 2 frames a second - t = int(time.time() * 2) / 2 + # t = int(time.time() * 2) / 2 + t = int(time.time()) if t == self._last_time: return self._last_time = t diff --git a/slicops/device/screen.py b/slicops/device/screen.py index 87610c2..8696127 100644 --- a/slicops/device/screen.py +++ b/slicops/device/screen.py @@ -275,7 +275,10 @@ def action_call_handler(self, arg): else self.__handler.on_screen_device_update ) # Denormalized state so no need for lock during call - return lambda: m(**arg if isinstance(arg, dict) else arg) + if isinstance(arg, dict): + return lambda: m(**arg) + else: + return lambda: m(arg) def action_check_upstream(self, arg): self.__upstream = _Upstream(self) From df0254fe4826bfe400e69ad668722a97a62180d5 Mon Sep 17 00:00:00 2001 From: "Eloise Y." <98988862+eloise-nebula@users.noreply.github.com> Date: Tue, 16 Sep 2025 13:54:53 -0700 Subject: [PATCH 17/29] fmt --- slicops/device/screen.py | 4 +++- slicops/sliclet/screen.py | 6 +----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/slicops/device/screen.py b/slicops/device/screen.py index 8696127..152454a 100644 --- a/slicops/device/screen.py +++ b/slicops/device/screen.py @@ -102,7 +102,7 @@ def _event_handle_monitor(self, arg, **kwargs): device=self.worker.device.device_name, error_kind=ErrorKind.monitor, accessor_name=n, - error_msg=arg.error + error_msg=arg.error, ), ) if n == "target_status": @@ -179,6 +179,7 @@ def _states(curr): return f"<_FSM {self.worker.device.device_name} {_states(self.curr)}>" + class ScreenError(Exception): def __init__(self, **kwargs): def _arg_str(): @@ -186,6 +187,7 @@ def _arg_str(): " ".join(k + "={" + k + "}" for k in sorted(kwargs)), **kwargs, ) + super().__init__(_arg_str()) diff --git a/slicops/sliclet/screen.py b/slicops/sliclet/screen.py index feb406b..51feafc 100644 --- a/slicops/sliclet/screen.py +++ b/slicops/sliclet/screen.py @@ -62,11 +62,7 @@ class Screen(slicops.sliclet.Base): def __init__(self, *args): - self.__prev_value = PKDict( - acquire=None, - image=None, - target_status=None - ) + self.__prev_value = PKDict(acquire=None, image=None, target_status=None) super().__init__(*args) def handle_destroy(self): From 6545e41ac3b13e3b9a5a42d0073b610f3504e96e Mon Sep 17 00:00:00 2001 From: "Eloise Y." <98988862+eloise-nebula@users.noreply.github.com> Date: Tue, 16 Sep 2025 13:57:35 -0700 Subject: [PATCH 18/29] Removed package_lock.json --- package-lock.json | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index acc91cf..0000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "slicops", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} From c21d7e0b417a50070a725ac08a90e09f839b6d7f Mon Sep 17 00:00:00 2001 From: "Eloise Y." <98988862+eloise-nebula@users.noreply.github.com> Date: Tue, 16 Sep 2025 14:02:55 -0700 Subject: [PATCH 19/29] ckp --- slicops/ctx.py | 14 ++-- slicops/device/__init__.py | 10 +-- slicops/device/screen.py | 37 +++++++---- slicops/sliclet/screen.py | 120 ++++++++++++++++++++--------------- tests/sliclet/screen_test.py | 2 + 5 files changed, 105 insertions(+), 78 deletions(-) diff --git a/slicops/ctx.py b/slicops/ctx.py index ed2c9e7..5a6e4a6 100644 --- a/slicops/ctx.py +++ b/slicops/ctx.py @@ -168,14 +168,16 @@ def group_get(self, field, group, attr=None): def multi_set(self, *args): def _args(): if len(args) > 1: + # (("a", 1), ("b", 2), ..) return args if len(args) == 0: raise AssetionError("must be at list one update") - if isinstance(args[0][0], str): - # (("a", 1)) - return args - # ((("a", 1), ("b", 2), ..)) or a dict - return args[0] + rv = args[0] + # ({"a": 1, "b": 2, ...}) + if isinstance(rv, dict): + return rv.items() + # else ((("a", 1), ("b", 2), ..)) + return args if isinstance(rv[0], str) else rv def _parse(): rv = PKDict() @@ -184,6 +186,8 @@ def _parse(): return rv for k, v in _parse().items(): + if not isinstance(v, PKDict): + v = PKDict(value=v) self.__field_update(k, self.__field(k), v) def rollback(self): diff --git a/slicops/device/__init__.py b/slicops/device/__init__.py index 2685c89..df53ca9 100644 --- a/slicops/device/__init__.py +++ b/slicops/device/__init__.py @@ -7,7 +7,6 @@ from pykern.pkcollections import PKDict from pykern.pkdebug import pkdc, pkdexc, pkdlog, pkdp import epics -import time import slicops.device_db import threading @@ -217,6 +216,7 @@ def _reshape(image): return bool(raw) if self.accessor_name == "image": return _reshape(raw) + # TODO: This is a hack. Needs to be database-driven. if ( self.accessor_name == "target_status" and self.device.device_name == "DEV_CAMERA" @@ -245,13 +245,6 @@ def _on_value(self, **kwargs): if self.meta.accessor_name == "image" and not len(v): pkdlog("empty image received {}", self) return - if self.meta.accessor_name == "image": - # 2 frames a second - # t = int(time.time() * 2) / 2 - t = int(time.time()) - if t == self._last_time: - return - self._last_time = t self._run_callback(value=self._fixup_value(v)) except Exception as e: pkdlog("error={} {} stack={}", e, self, pkdexc()) @@ -277,7 +270,6 @@ def __pv(self): # from within a monitor callback. # TODO(robnagler) need a better way of dealing with this self._image_shape = (self.device.get("n_row"), self.device.get("n_col")) - self._last_time = 0 self._pv = epics.PV( self.meta.pv_name, connection_callback=self._on_connection, diff --git a/slicops/device/screen.py b/slicops/device/screen.py index 152454a..b62d231 100644 --- a/slicops/device/screen.py +++ b/slicops/device/screen.py @@ -19,10 +19,8 @@ # TODO(robnagler) these should be reused for both cases _MOVE_TARGET_IN = PKDict({False: 0, True: 1}) -_STATUS_IN = 2 -_STATUS_OUT = 1 -_BLOCKING_MSG = "upstream target is in" +_BLOCKING_MSG = "upstream target is {}" _TIMEOUT_MSG = "upstream target status accessor timed out" _ERROR_PREFIX_MSG = "upstream target error: " @@ -118,7 +116,7 @@ def _event_handle_monitor(self, arg, **kwargs): v = arg.value rv = PKDict(acquire=arg.value) elif n == "target_status": - v = _STATUS_IN == arg.value + v = TargetStatus(arg.value) rv = PKDict(move_target_arg=None, target_status=v) else: raise AssertionError(f"unsupported accessor={n} {self}") @@ -134,17 +132,17 @@ def _event_move_target( upstream_problems, **kwargs, ): - if move_target_arg: + if move_target_arg or target_status in (TargetStatus.MOVING, TargetStatus.INCONSISTENT, None): self.worker.action( "call_handler", ScreenError( device=self.worker.device.device_name, error_kind=ErrorKind.fsm, - error_msg="target already moving", + error_msg="target already moving, inconsistent, or not intialized", ), ) return - if target_status is not None and arg.want_in == target_status: + if arg.want_in == (target_status == TargetStatus.IN): # TODO(robnagler) could be a race condition so probably fine to do nothing pkdlog("same target_status={} self.want_in={}", target_status, arg.want_in) return @@ -191,6 +189,15 @@ def _arg_str(): super().__init__(_arg_str()) +class TargetStatus(enum.Enum): + """Errors passed to on_screen_device_error""" + + INCONSISTENT = 3 + IN = 2 + OUT = 1 + MOVING = 0 + + class _Upstream(ActionLoop): """Action loop to check targets of upstream screens""" @@ -210,14 +217,14 @@ def _names(): self._loop_timeout_secs = _cfg.upstream_timeout_secs super().__init__() - def action_handle_status(self, arg): + def action_handle_target_status(self, arg): n = arg.accessor.device.device_name self.__devices.pkdel(n).destroy() if e := arg.get("error"): pkdlog("device={} error={}", n, e) self.__problems[n] = f"{_ERROR_PREFIX_MSG}{e}" - elif arg.value == _STATUS_IN: - self.__problems[n] = _BLOCKING_MSG + elif arg.value != TargetStatus.OUT.value: + self.__problems[n] = _BLOCKING_MSG.format(arg.value) if not self.__devices: return self.__done() return None @@ -236,14 +243,14 @@ def __done(self): self.__worker.action("upstream_status", PKDict(problems=self.__problems)) return self._LOOP_END - def __handle_status(self, kwargs): + def __handle_target_status(self, kwargs): if "connected" in kwargs: return - self.action("handle_status", kwargs) + self.action("handle_target_status", kwargs) def _start(self, *args, **kwargs): for d in self.__devices.values(): - d.accessor("target_status").monitor(self.__handle_status) + d.accessor("target_status").monitor(self.__handle_target_status) super()._start(*args, **kwargs) def _repr(self): @@ -328,8 +335,10 @@ def __handle_monitor(self, change): self.action("handle_monitor", change) def _start(self, *args, **kwargs): - for a in "acquire", "image", "target_status": + for a in "acquire", "image": self.device.accessor(a).monitor(self.__handle_monitor) + if self.device.has_accessor("target_status"): + self.device.accessor("target_status").monitor(self.__handle_monitor) super()._start(*args, **kwargs) def _repr(self): diff --git a/slicops/sliclet/screen.py b/slicops/sliclet/screen.py index 51feafc..7838f1c 100644 --- a/slicops/sliclet/screen.py +++ b/slicops/sliclet/screen.py @@ -6,6 +6,7 @@ from pykern.pkcollections import PKDict from pykern.pkdebug import pkdc, pkdexc, pkdlog, pkdp +from slicops.device.screen import TargetStatus import pykern.pkconfig import pykern.util import queue @@ -23,7 +24,35 @@ _BUTTONS_DISABLE = ( ("single_button.ui.enabled", False), ("stop_button.ui.enabled", False), - ("start_button.ui.enabled", False), +) + +_TARGET_DISABLE = ( + ("target_in_button.ui.enabled", False), + ("target_out_button.ui.enabled", False), +) + +_TARGET_INVISIBLE = ( + ("target_in_button.ui.visible", False), + ("target_out_button.ui.visible", False), + ("target_status.ui.visible", False), +) + +_TARGET_VISIBLE = ( + ("target_in_button.ui.visible", True), + ("target_out_button.ui.visible", True), + ("target_status.ui.visible", True), +) + +_BUTTONS_INVISIBLE = ( + ("single_button.ui.visible", False), + ("start_button.ui.visible", False), + ("stop_button.ui.visible", False), +) + +_BUTTONS_VISIBLE = ( + ("single_button.ui.visible", True), + ("start_button.ui.visible", True), + ("stop_button.ui.visible", True), ) _DEVICE_DISABLE = ( @@ -36,20 +65,11 @@ ("plot.value", None), ("pv.ui.visible", False), ("pv.value", None), - ("single_button.ui.visible", False), - ("start_button.ui.visible", False), - ("stop_button.ui.visible", False), -) + _BUTTONS_DISABLE +) + _BUTTONS_DISABLE + _BUTTONS_INVISIBLE + _TARGET_DISABLE + _TARGET_INVISIBLE _DEVICE_ENABLE = ( ("pv.ui.visible", True), - ("single_button.ui.visible", True), - ("start_button.ui.visible", True), - ("stop_button.ui.visible", True), - ("single_button.ui.enabled", True), - ("stop_button.ui.enabled", False), - ("start_button.ui.enabled", True), -) +) + _BUTTONS_VISIBLE _PLOT_ENABLE = ( ("color_map.ui.enabled", True), @@ -62,7 +82,15 @@ class Screen(slicops.sliclet.Base): def __init__(self, *args): +<<<<<<< Updated upstream self.__prev_value = PKDict(acquire=None, image=None, target_status=None) +======= + self.__current_value = PKDict( + acquire=None, + image=None, + target=None + ) +>>>>>>> Stashed changes super().__init__(*args) def handle_destroy(self): @@ -79,19 +107,19 @@ def on_change_curve_fit_method(self, txn, **kwargs): def on_click_single_button(self, txn, **kwargs): self.__single_button = True - self.__set_acquire(txn, True) + self.__set(txn, "acquire", True, _BUTTONS_DISABLE) def on_click_start_button(self, txn, **kwargs): - self.__set_acquire(txn, True) + self.__set(txn, "acquire", True, _BUTTONS_DISABLE) def on_click_stop_button(self, txn, **kwargs): - self.__set_acquire(txn, False) + self.__set(txn, "acquire", False, _BUTTONS_DISABLE) def on_click_target_in_button(self, txn, **kwargs): - self.__set_target(txn, True) + self.__set(txn, "target", True, _TARGET_DISABLE, method="move_target") def on_click_target_out_button(self, txn, **kwargs): - self.__set_target(txn, False) + self.__set(txn, "target", False, _TARGET_DISABLE, method="move_target") def handle_init(self, txn): self.__device = None @@ -168,7 +196,7 @@ def __device_setup(self, txn, beam_path, camera): PKDict( image=self.__handle_image, acquire=self.__handle_acquire, - target_status=self.__handle_target, + target_status=self.__handle_target_status, ), ) @@ -186,11 +214,14 @@ def __device_setup(self, txn, beam_path, camera): self.__device_destroy(txn) self.__user_alert(txn, "unable to connect to camera={} error={}", camera, e) return - txn.multi_set(_DEVICE_ENABLE + (("pv.value", self.__device.meta.pv_prefix),)) + s = PKDict(_DEVICE_ENABLE + (("pv.value", self.__device.meta.pv_prefix),)) + if self.__device.has_accessor("target_status"): + s.update(_TARGET_VISIBLE) + txn.multi_set(s) def __handle_acquire(self, acquire): with self.lock_for_update() as txn: - self.__prev_value["acquire"] = acquire + self.__current_value["acquire"] = acquire n = not acquire # Leave plot alone txn.multi_set( @@ -209,60 +240,49 @@ def __handle_device_error(self, exc): def __handle_image(self, image): with self.lock_for_update() as txn: - self.__prev_value["image"] = image + self.__current_value["image"] = image if self.__update_plot(txn) and self.__single_button: # self.__single_button = False - self.__set_acquire(txn, False) + self.__set(txn, "acquire", False, _BUTTONS_DISABLE) txn.multi_set( ("single_button.ui.enabled", True), ("start_button.ui.enabled", True), ) - def __handle_target(self, status): - self.__prev_value["target_status"] = status + def __handle_target_status(self, status): with self.lock_for_update() as txn: - txn.field_set("target_status", "In" if status else "Out") + self.__current_value["target"] = status + txn.multi_set( + ("target_status", status.name), + ("target_in_button.ui.enabled", status == TargetStatus.OUT), + ("target_out_button.ui.enabled", status == TargetStatus.IN), + ) - def __set_acquire(self, txn, acquire): + def __set(self, txn, accessor, value, txn_set, method=None): if not self.__device or not self.__handler: # buttons already disabled return - v = self.__prev_value["acquire"] - if v is not None and v == acquire: + v = self.__current_value[accessor] + if v is not None and v == value: # No button disable since nothing changed return - # No presses until we get a response from device - txn.multi_set(_BUTTONS_DISABLE) + txn.multi_set(txn_set) try: - self.__device.put("acquire", acquire) + if method is None: + self.__device.put(accessor, value) + else: + m = getattr(self.__device, method) + m(value) except slicops.device.DeviceError as e: pkdlog( "error={} on {}, clearing camera; stack={}", e, self.__device, pkdexc() ) raise pykern.util.APIError(e) - def __set_target(self, txn, want_in): - if not self.__device or not self.__handler: - # buttons already disabled - return - v = self.__prev_value["target_status"] - if v is not None and v == want_in: - return - self.__device.move_target(want_in=want_in) - - # def __target_moved(self, status): - # if status is failed: - # display error - # if status is out: - # disable buttons - # if status is in: - # enable buttons - # def __update_plot(self, txn): if not self.__device or not self.__handler: return False - # TOOD Change previous value to current value (nominally). - if (i := self.__prev_value["image"]) is None or not i.size: + if (i := self.__current_value["image"]) is None or not i.size: return False if not txn.group_get("plot", "ui", "visible"): txn.multi_set(_PLOT_ENABLE) diff --git a/tests/sliclet/screen_test.py b/tests/sliclet/screen_test.py index dead3b3..0014bef 100644 --- a/tests/sliclet/screen_test.py +++ b/tests/sliclet/screen_test.py @@ -15,6 +15,7 @@ async def test_basic(): async def _buttons(s, expect, msg): from pykern import pkunit, pkdebug + from pykern.pkdebug import pkdlog from asyncio.exceptions import CancelledError # Wait for buttons to "settle" on expect. The updates @@ -25,6 +26,7 @@ async def _buttons(s, expect, msg): except CancelledError: # timed out so now report mismatch via pkunit pkunit.pkeq(expect, v, msg) + pkdlog(rv.fields) v = tuple(rv.fields.pknested_get(k) for k in _BUTTONS) if v == expect: break From 4e6e038793d3d25b893e8035aed9a5f8a09dd7e9 Mon Sep 17 00:00:00 2001 From: "Eloise Y." <98988862+eloise-nebula@users.noreply.github.com> Date: Tue, 16 Sep 2025 14:05:52 -0700 Subject: [PATCH 20/29] chkp --- slicops/sliclet/screen.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/slicops/sliclet/screen.py b/slicops/sliclet/screen.py index 7838f1c..cc6bffb 100644 --- a/slicops/sliclet/screen.py +++ b/slicops/sliclet/screen.py @@ -82,15 +82,11 @@ class Screen(slicops.sliclet.Base): def __init__(self, *args): -<<<<<<< Updated upstream - self.__prev_value = PKDict(acquire=None, image=None, target_status=None) -======= self.__current_value = PKDict( acquire=None, image=None, target=None ) ->>>>>>> Stashed changes super().__init__(*args) def handle_destroy(self): From 4628d1a268edb58422fcd27ac33435f9fe11ac33 Mon Sep 17 00:00:00 2001 From: "Eloise Y." <98988862+eloise-nebula@users.noreply.github.com> Date: Mon, 22 Sep 2025 16:50:27 -0700 Subject: [PATCH 21/29] chkp --- slicops/device/screen.py | 6 +++++- slicops/sliclet/screen.py | 38 +++++++++++++++++++------------------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/slicops/device/screen.py b/slicops/device/screen.py index b62d231..1606f1b 100644 --- a/slicops/device/screen.py +++ b/slicops/device/screen.py @@ -132,7 +132,11 @@ def _event_move_target( upstream_problems, **kwargs, ): - if move_target_arg or target_status in (TargetStatus.MOVING, TargetStatus.INCONSISTENT, None): + if move_target_arg or target_status in ( + TargetStatus.MOVING, + TargetStatus.INCONSISTENT, + None, + ): self.worker.action( "call_handler", ScreenError( diff --git a/slicops/sliclet/screen.py b/slicops/sliclet/screen.py index cc6bffb..6c25db0 100644 --- a/slicops/sliclet/screen.py +++ b/slicops/sliclet/screen.py @@ -56,20 +56,24 @@ ) _DEVICE_DISABLE = ( - ("color_map.ui.enabled", False), - ("color_map.ui.visible", False), - ("curve_fit_method.ui.enabled", False), - ("curve_fit_method.ui.visible", False), - ("plot.ui.visible", False), - # Useful to avoid large ctx sends - ("plot.value", None), - ("pv.ui.visible", False), - ("pv.value", None), -) + _BUTTONS_DISABLE + _BUTTONS_INVISIBLE + _TARGET_DISABLE + _TARGET_INVISIBLE - -_DEVICE_ENABLE = ( - ("pv.ui.visible", True), -) + _BUTTONS_VISIBLE + ( + ("color_map.ui.enabled", False), + ("color_map.ui.visible", False), + ("curve_fit_method.ui.enabled", False), + ("curve_fit_method.ui.visible", False), + ("plot.ui.visible", False), + # Useful to avoid large ctx sends + ("plot.value", None), + ("pv.ui.visible", False), + ("pv.value", None), + ) + + _BUTTONS_DISABLE + + _BUTTONS_INVISIBLE + + _TARGET_DISABLE + + _TARGET_INVISIBLE +) + +_DEVICE_ENABLE = (("pv.ui.visible", True),) + _BUTTONS_VISIBLE _PLOT_ENABLE = ( ("color_map.ui.enabled", True), @@ -82,11 +86,7 @@ class Screen(slicops.sliclet.Base): def __init__(self, *args): - self.__current_value = PKDict( - acquire=None, - image=None, - target=None - ) + self.__current_value = PKDict(acquire=None, image=None, target=None) super().__init__(*args) def handle_destroy(self): From a546fe06309e963d1a139b6e99ffb7fef55f5fd2 Mon Sep 17 00:00:00 2001 From: "Eloise Y." <98988862+eloise-nebula@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:05:30 -0700 Subject: [PATCH 22/29] chkp --- slicops/unit_util.py | 4 ++-- tests/device/screen2_test.py | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/slicops/unit_util.py b/slicops/unit_util.py index 0e5cc09..7b4feb7 100644 --- a/slicops/unit_util.py +++ b/slicops/unit_util.py @@ -185,8 +185,8 @@ def __init__(self, *args, **kwargs): } ) - def on_screen_device_error(self, **kwargs): - self.event_q.error.put_nowait(PKDict(kwargs)) + def on_screen_device_error(self, exc): + self.event_q.error.put_nowait(PKDict(exception=exc)) def on_screen_device_update(self, **kwargs): self.event_q[kwargs["accessor_name"]].put_nowait(PKDict(kwargs)) diff --git a/tests/device/screen2_test.py b/tests/device/screen2_test.py index f2912fa..6181295 100644 --- a/tests/device/screen2_test.py +++ b/tests/device/screen2_test.py @@ -8,9 +8,14 @@ def test_upstream_blocked(): from pykern import pkdebug, pkunit from slicops import unit_util + from slicops.device.screen import ScreenError, ErrorKind with unit_util.setup_screen("CU_HXR", "YAG03") as s: s.device.move_target(want_in=True) e = s.handler.test_get("error") - pkunit.pkeq("upstream", e.error_kind.name) - pkunit.pkeq("upstream target is in", e.error_msg.YAG02) + s = ScreenError( + device="YAG03", + error_kind=ErrorKind.upstream, + error_msg="upstream target is in", + ) + pkunit.pkeq(s, e.exception) From f3d32dcc641ec705eee60fc292a9750467466881 Mon Sep 17 00:00:00 2001 From: "Eloise Y." <98988862+eloise-nebula@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:18:46 -0700 Subject: [PATCH 23/29] Moving target waits until target is initialized and upstream checked --- slicops/device/screen.py | 65 ++++++++++++++++++++++++++++-------- tests/device/screen2_test.py | 15 ++++++--- 2 files changed, 62 insertions(+), 18 deletions(-) diff --git a/slicops/device/screen.py b/slicops/device/screen.py index 1606f1b..ac82f7b 100644 --- a/slicops/device/screen.py +++ b/slicops/device/screen.py @@ -91,7 +91,15 @@ def event(self, name, arg): if u := getattr(self, f"_event_{name}")(arg, **self.curr): self.curr.update(u) - def _event_handle_monitor(self, arg, **kwargs): + def _event_handle_monitor( + self, + arg, + check_upstream, + move_target_arg, + target_status, + upstream_problems, + **kwargs, + ): n = arg.accessor.accessor_name if "error" in arg: self.worker.action( @@ -117,7 +125,16 @@ def _event_handle_monitor(self, arg, **kwargs): rv = PKDict(acquire=arg.value) elif n == "target_status": v = TargetStatus(arg.value) - rv = PKDict(move_target_arg=None, target_status=v) + if target_status is None and move_target_arg: + rv = PKDict(target_status=v) + u = self.__move_with_upstream_check( + move_target_arg, upstream_problems, check_upstream + ) + if u is not None: + rv.check_upstream = u + else: + v = TargetStatus(arg.value) + rv = PKDict(move_target_arg=None, target_status=v) else: raise AssertionError(f"unsupported accessor={n} {self}") self.worker.action("call_handler", PKDict(accessor_name=n, value=v)) @@ -132,10 +149,13 @@ def _event_move_target( upstream_problems, **kwargs, ): + # If target_status hasn't initialized, defer to monitor fire. + if target_status == None: + rv = PKDict(move_target_arg=arg) + return rv if move_target_arg or target_status in ( TargetStatus.MOVING, TargetStatus.INCONSISTENT, - None, ): self.worker.action( "call_handler", @@ -152,12 +172,11 @@ def _event_move_target( return # TODO(robnagler) allow moving without checking upstream rv = PKDict(move_target_arg=arg) - if arg.want_in and upstream_problems is None or upstream_problems: - # Recheck the upstream - self.worker.action("check_upstream", None) - rv.check_upstream = True - else: - self.worker.action("move_target", arg) + v = self.__move_with_upstream_check( + arg.want_in, upstream_problems, check_upstream + ) + if v is not None: + rv.check_upstream = v return rv def _event_upstream_status(self, arg, move_target_arg, **kwargs): @@ -175,6 +194,17 @@ def _event_upstream_status(self, arg, move_target_arg, **kwargs): self.worker.action("move_target", move_target_arg) return rv + def __move_with_upstream_check(self, want_in, upstream_problems, check_upstream): + rv = None + if want_in and upstream_problems is None or upstream_problems: + # Recheck the upstream + if not check_upstream: + self.worker.action("check_upstream", None) + rv = True + else: + self.worker.action("move_target", arg) + return rv + def __repr__(self): def _states(curr): return " ".join(f"{k}={curr[k]}" for k in sorted(curr.keys())) @@ -211,6 +241,7 @@ def _names(): "PROF", "target_control", worker.beam_path, worker.device.device_name ) + self.__is_ready = threading.Event() self.__worker = worker self.__problems = PKDict() self.__devices = PKDict({u: slicops.device.Device(u) for u in _names()}) @@ -228,7 +259,8 @@ def action_handle_target_status(self, arg): pkdlog("device={} error={}", n, e) self.__problems[n] = f"{_ERROR_PREFIX_MSG}{e}" elif arg.value != TargetStatus.OUT.value: - self.__problems[n] = _BLOCKING_MSG.format(arg.value) + s = TargetStatus(arg.value) + self.__problems[n] = _BLOCKING_MSG.format(s.name) if not self.__devices: return self.__done() return None @@ -276,11 +308,16 @@ def __init__(self, beam_path, handler, device): self.__handler = handler self.__upstream = None self.__status = None - self.__fsm = _FSM(self) + # self.monitors = pkdict... + self.__fsm = _FSM(self) # self.monitors ... self.__target_control = None self._loop_timeout_secs = 0 super().__init__() + # get from ready queue with timeout + # except + # exit + def action_call_handler(self, arg): m = ( self.__handler.on_screen_device_error @@ -294,7 +331,8 @@ def action_call_handler(self, arg): return lambda: m(arg) def action_check_upstream(self, arg): - self.__upstream = _Upstream(self) + if self.__upstream is None or self.__upstream.destroyed: + self.__upstream = _Upstream(self) return None def action_handle_monitor(self, arg): @@ -318,6 +356,7 @@ def action_upstream_status(self, arg): def req_action(self, method, arg): """Called by DeviceScreen which has separate life cycle""" + # __fsm.is_ready if self.destroyed: raise AssertionError("object is destroyed") self.action(method, arg) @@ -339,7 +378,7 @@ def __handle_monitor(self, change): self.action("handle_monitor", change) def _start(self, *args, **kwargs): - for a in "acquire", "image": + for a in "acquire", "image": # self.monitors ... self.device.accessor(a).monitor(self.__handle_monitor) if self.device.has_accessor("target_status"): self.device.accessor("target_status").monitor(self.__handle_monitor) diff --git a/tests/device/screen2_test.py b/tests/device/screen2_test.py index 6181295..95ee870 100644 --- a/tests/device/screen2_test.py +++ b/tests/device/screen2_test.py @@ -11,11 +11,16 @@ def test_upstream_blocked(): from slicops.device.screen import ScreenError, ErrorKind with unit_util.setup_screen("CU_HXR", "YAG03") as s: + # hack to make sure target_status fires + # import time + # time.sleep(1.0) + s.device.move_target(want_in=True) e = s.handler.test_get("error") + pkunit.pkeq(e, False) s = ScreenError( - device="YAG03", - error_kind=ErrorKind.upstream, - error_msg="upstream target is in", - ) - pkunit.pkeq(s, e.exception) + device="YAG03", + error_kind=ErrorKind.upstream, + error_msg="{'YAG02': 'upstream target is IN'}", + ) + pkunit.pkeq(repr(s), repr(e.exception)) # pkeq magic? From f61f82e89a1b0f927234d8a594957f189917a7d5 Mon Sep 17 00:00:00 2001 From: "Eloise Y." <98988862+eloise-nebula@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:28:36 -0700 Subject: [PATCH 24/29] Fixed screen1_test.py as well. --- slicops/device/screen.py | 1 + tests/device/screen1_test.py | 7 ++++--- tests/device/screen2_test.py | 7 +------ 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/slicops/device/screen.py b/slicops/device/screen.py index ac82f7b..224a276 100644 --- a/slicops/device/screen.py +++ b/slicops/device/screen.py @@ -202,6 +202,7 @@ def __move_with_upstream_check(self, want_in, upstream_problems, check_upstream) self.worker.action("check_upstream", None) rv = True else: + arg = PKDict(want_in=want_in) self.worker.action("move_target", arg) return rv diff --git a/tests/device/screen1_test.py b/tests/device/screen1_test.py index 0beee91..1cbaf58 100644 --- a/tests/device/screen1_test.py +++ b/tests/device/screen1_test.py @@ -9,12 +9,13 @@ def test_upstream_ok(): from pykern import pkdebug, pkunit + from slicops.device.screen import TargetStatus with unit_util.setup_screen("CU_HXR", "YAG03") as s: s.handler.test_get("image") pkunit.pkeq(False, s.handler.test_get("acquire")) - pkunit.pkeq(False, s.handler.test_get("target_status")) + pkunit.pkeq(TargetStatus.OUT, s.handler.test_get("target_status")) s.device.move_target(want_in=True) - pkunit.pkeq(True, s.handler.test_get("target_status")) + pkunit.pkeq(TargetStatus.IN, s.handler.test_get("target_status")) s.device.move_target(want_in=False) - pkunit.pkeq(False, s.handler.test_get("target_status")) + pkunit.pkeq(TargetStatus.OUT, s.handler.test_get("target_status")) diff --git a/tests/device/screen2_test.py b/tests/device/screen2_test.py index 95ee870..b309208 100644 --- a/tests/device/screen2_test.py +++ b/tests/device/screen2_test.py @@ -11,16 +11,11 @@ def test_upstream_blocked(): from slicops.device.screen import ScreenError, ErrorKind with unit_util.setup_screen("CU_HXR", "YAG03") as s: - # hack to make sure target_status fires - # import time - # time.sleep(1.0) - s.device.move_target(want_in=True) e = s.handler.test_get("error") - pkunit.pkeq(e, False) s = ScreenError( device="YAG03", error_kind=ErrorKind.upstream, error_msg="{'YAG02': 'upstream target is IN'}", ) - pkunit.pkeq(repr(s), repr(e.exception)) # pkeq magic? + pkunit.pkeq(repr(s), repr(e.exception)) # pkunit magic? From 2f88971c25caef6f5fc03c4924f6e93db55caff8 Mon Sep 17 00:00:00 2001 From: "Eloise Y." <98988862+eloise-nebula@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:55:39 -0700 Subject: [PATCH 25/29] Fixed sliclet.screen_test. Why was start_button removed from _BUTTONS_DISABLE? --- slicops/sliclet/screen.py | 3 ++- tests/sliclet/screen_test.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/slicops/sliclet/screen.py b/slicops/sliclet/screen.py index 6c25db0..105e265 100644 --- a/slicops/sliclet/screen.py +++ b/slicops/sliclet/screen.py @@ -22,8 +22,9 @@ _cfg = None _BUTTONS_DISABLE = ( - ("single_button.ui.enabled", False), + ("start_button.ui.enabled", False), ("stop_button.ui.enabled", False), + ("single_button.ui.enabled", False), ) _TARGET_DISABLE = ( diff --git a/tests/sliclet/screen_test.py b/tests/sliclet/screen_test.py index 0014bef..c8ab803 100644 --- a/tests/sliclet/screen_test.py +++ b/tests/sliclet/screen_test.py @@ -26,7 +26,6 @@ async def _buttons(s, expect, msg): except CancelledError: # timed out so now report mismatch via pkunit pkunit.pkeq(expect, v, msg) - pkdlog(rv.fields) v = tuple(rv.fields.pknested_get(k) for k in _BUTTONS) if v == expect: break From bf4e907ddfeae4229f3283c43926a74b0e958855 Mon Sep 17 00:00:00 2001 From: "Eloise Y." <98988862+eloise-nebula@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:04:28 -0700 Subject: [PATCH 26/29] chkp --- slicops/device/screen.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/slicops/device/screen.py b/slicops/device/screen.py index 224a276..6e826c8 100644 --- a/slicops/device/screen.py +++ b/slicops/device/screen.py @@ -6,8 +6,7 @@ from pykern.pkcollections import PKDict from pykern.pkdebug import pkdc, pkdexc, pkdlog, pkdp, pkdformat -from pykern.pkasyncio import ActionLoop -from slicops.device import DeviceError +import pykern.pkasyncio import abc import enum import logging @@ -233,7 +232,7 @@ class TargetStatus(enum.Enum): MOVING = 0 -class _Upstream(ActionLoop): +class _Upstream(pykern.pkasyncio.ActionLoop): """Action loop to check targets of upstream screens""" def __init__(self, worker): @@ -294,7 +293,7 @@ def _repr(self): return f"pending={sorted(self.__devices)} problems={sorted(self.__problems)}" -class _Worker(ActionLoop): +class _Worker(pykern.pkasyncio.ActionLoop): """Action loop for Screen _Worker uses `_FSM` to translate events to actions. Monitor calls From 41cbd13933c680d6d6d318246f6d16bc645fb560 Mon Sep 17 00:00:00 2001 From: Paul Moeller Date: Thu, 9 Oct 2025 20:51:19 +0000 Subject: [PATCH 27/29] Better handling of DEV_CAMERA add/remove target simulation - removed special DEV_CAMERA code in device/__init__.py - updated epics sim-detector to simulate add/remove target - added DEV_CAMERA2 for example camera which has no targets --- slicops/device/__init__.py | 6 ----- slicops/package_data/device_db.sqlite3 | Bin 1101824 -> 1101824 bytes slicops/pkcli/device_db.py | 19 ++++++++++++++-- slicops/pkcli/epics.py | 29 +++++++++++++++++++++++++ slicops/unit_util.py | 2 +- 5 files changed, 47 insertions(+), 9 deletions(-) diff --git a/slicops/device/__init__.py b/slicops/device/__init__.py index df53ca9..2912f5c 100644 --- a/slicops/device/__init__.py +++ b/slicops/device/__init__.py @@ -216,12 +216,6 @@ def _reshape(image): return bool(raw) if self.accessor_name == "image": return _reshape(raw) - # TODO: This is a hack. Needs to be database-driven. - if ( - self.accessor_name == "target_status" - and self.device.device_name == "DEV_CAMERA" - ): - return raw + 1 return raw def _on_connection(self, **kwargs): diff --git a/slicops/package_data/device_db.sqlite3 b/slicops/package_data/device_db.sqlite3 index fea55a228225128630825359cf9630b56e6f8f70..ab85f97a30f04cd8a7855aecdd1df9940179ebd5 100644 GIT binary patch delta 4672 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 delta 3842 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 L1iGNju~PXDZ%$Db diff --git a/slicops/pkcli/device_db.py b/slicops/pkcli/device_db.py index fff09f4..c1c965f 100644 --- a/slicops/pkcli/device_db.py +++ b/slicops/pkcli/device_db.py @@ -33,8 +33,23 @@ n_col: 13SIM1:cam1:SizeX n_row: 13SIM1:cam1:SizeY n_bits: 13SIM1:cam1:N_OF_BITS - target_status: 13SIM1:cam1:ShutterMode_RBV - target_control: 13SIM1:cam1:ShutterMode + target_status: 13SIM1:cam1:ShutterMode + target_control: 13SIM1:cam1:TriggerMode + control_name: 13SIM1 + metadata: + area: DEV_AREA + beam_path: + - DEV_BEAM_PATH + sum_l_meters: 0.614 + type: PROF + DEV_CAMERA2: + controls_information: + PVs: + acquire: 13SIM1:cam1:Acquire + image: 13SIM1:image1:ArrayData + n_col: 13SIM1:cam1:SizeX + n_row: 13SIM1:cam1:SizeY + n_bits: 13SIM1:cam1:N_OF_BITS control_name: 13SIM1 metadata: area: DEV_AREA diff --git a/slicops/pkcli/epics.py b/slicops/pkcli/epics.py index 18235ce..2293371 100644 --- a/slicops/pkcli/epics.py +++ b/slicops/pkcli/epics.py @@ -16,6 +16,8 @@ # Local so should connect quickly _SIM_DETECTOR_TIMEOUT = 5 _LOG_BASE = "sim_detector.log" +_CAM1_STATUS_PV = "13SIM1:cam1:ShutterMode" +_CAM1_CONTROL_PV = "13SIM1:cam1:TriggerMode" def init_sim_detector(): @@ -37,6 +39,8 @@ def init_sim_detector(): "13SIM1:cam1:SizeX": 1024, "13SIM1:cam1:SizeY": 768, "13SIM1:image1:EnableCallbacks": 1, + _CAM1_STATUS_PV: 1, + _CAM1_CONTROL_PV: 0, }.items(): pv = epics.PV(name) v = pv.put(value, wait=True, timeout=_SIM_DETECTOR_TIMEOUT) @@ -83,6 +87,30 @@ def _st_cmd(dir_path): "rb" ) + def _watch_target(): + def _control_monitor(pvname, value, **kwargs): + nonlocal new_status + # control value: 0: move out, 1: move in + # status: 1: out, 2: in + new_status = value + 1 + + new_status = None + current_status = new_status + s = epics.PV(_CAM1_STATUS_PV) + epics.PV(_CAM1_CONTROL_PV).add_callback(_control_monitor) + try: + while True: + if current_status != new_status: + # transition through the "0 moving" state + if current_status == 0: + current_status = new_status + else: + current_status = 0 + s.put(current_status, wait=True, timeout=3) + time.sleep(1) + except KeyboardInterrupt: + pass + d = _dir() with _log() as o: p = subprocess.Popen( @@ -100,6 +128,7 @@ def _st_cmd(dir_path): time.sleep(2) pkdlog("initializing sim detector") init_sim_detector() + _watch_target() pkdlog("waiting for pid={} to exit", p.pid) p.wait() finally: diff --git a/slicops/unit_util.py b/slicops/unit_util.py index 7b4feb7..1671bf7 100644 --- a/slicops/unit_util.py +++ b/slicops/unit_util.py @@ -163,7 +163,7 @@ def _path(path, arg): finally: os._exit(0) try: - time.sleep(1) + time.sleep(2) yield None finally: os.kill(p, signal.SIGKILL) From 49fc839d96dfc1f385f90336cdf6567975f4957f Mon Sep 17 00:00:00 2001 From: "Eloise Y." <98988862+eloise-nebula@users.noreply.github.com> Date: Thu, 9 Oct 2025 13:59:23 -0700 Subject: [PATCH 28/29] Renamed "check_upstream" to "await_upstream" --- slicops/device/screen.py | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/slicops/device/screen.py b/slicops/device/screen.py index 6e826c8..4639ce7 100644 --- a/slicops/device/screen.py +++ b/slicops/device/screen.py @@ -77,11 +77,11 @@ class _FSM: def __init__(self, worker): self.worker = worker self.curr = PKDict( - acquire=False, - check_upstream=False, - move_target_arg=None, - target_status=None, - upstream_problems=None, + acquire=False, # status of screen acquire + move_target_arg=None, # where do we want the target to be? + target_status=None, # where is the status right now? + await_upstream=False, # are we checking upstream? + upstream_problems=None, # are there problems upstream? ) self.prev = self.curr.copy() @@ -93,7 +93,7 @@ def event(self, name, arg): def _event_handle_monitor( self, arg, - check_upstream, + await_upstream, move_target_arg, target_status, upstream_problems, @@ -124,16 +124,13 @@ def _event_handle_monitor( rv = PKDict(acquire=arg.value) elif n == "target_status": v = TargetStatus(arg.value) + rv = PKDict(target_status=v) if target_status is None and move_target_arg: - rv = PKDict(target_status=v) - u = self.__move_with_upstream_check( - move_target_arg, upstream_problems, check_upstream + rv.await_upstream = self.__move_target_upstream_check( + move_target_arg, upstream_problems, await_upstream ) - if u is not None: - rv.check_upstream = u else: - v = TargetStatus(arg.value) - rv = PKDict(move_target_arg=None, target_status=v) + rv.move_target_arg = None else: raise AssertionError(f"unsupported accessor={n} {self}") self.worker.action("call_handler", PKDict(accessor_name=n, value=v)) @@ -142,7 +139,7 @@ def _event_handle_monitor( def _event_move_target( self, arg, - check_upstream, + await_upstream, move_target_arg, target_status, upstream_problems, @@ -171,15 +168,13 @@ def _event_move_target( return # TODO(robnagler) allow moving without checking upstream rv = PKDict(move_target_arg=arg) - v = self.__move_with_upstream_check( - arg.want_in, upstream_problems, check_upstream + rv.await_upstream = self.__move_target_upstream_check( + arg.want_in, upstream_problems, await_upstream ) - if v is not None: - rv.check_upstream = v return rv def _event_upstream_status(self, arg, move_target_arg, **kwargs): - rv = PKDict(check_upstream=False, upstream_problems=arg.problems) + rv = PKDict(await_upstream=False, upstream_problems=arg.problems) if arg.problems: self.worker.action( "call_handler", @@ -193,11 +188,11 @@ def _event_upstream_status(self, arg, move_target_arg, **kwargs): self.worker.action("move_target", move_target_arg) return rv - def __move_with_upstream_check(self, want_in, upstream_problems, check_upstream): - rv = None + def __move_target_upstream_check(self, want_in, upstream_problems, await_upstream): + rv = False if want_in and upstream_problems is None or upstream_problems: # Recheck the upstream - if not check_upstream: + if not await_upstream: self.worker.action("check_upstream", None) rv = True else: From d6c90c671ca5fbcf9fea12f5171651ee7a34cabd Mon Sep 17 00:00:00 2001 From: Paul Moeller Date: Wed, 15 Oct 2025 22:53:12 +0000 Subject: [PATCH 29/29] use thread event rather than polling in target simulator --- slicops/device/screen.py | 2 -- slicops/pkcli/epics.py | 43 +++++++++++++++++++++++----------------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/slicops/device/screen.py b/slicops/device/screen.py index 4639ce7..b212a2d 100644 --- a/slicops/device/screen.py +++ b/slicops/device/screen.py @@ -14,7 +14,6 @@ import queue import slicops.device import slicops.device_db -import threading # TODO(robnagler) these should be reused for both cases _MOVE_TARGET_IN = PKDict({False: 0, True: 1}) @@ -236,7 +235,6 @@ def _names(): "PROF", "target_control", worker.beam_path, worker.device.device_name ) - self.__is_ready = threading.Event() self.__worker = worker self.__problems = PKDict() self.__devices = PKDict({u: slicops.device.Device(u) for u in _names()}) diff --git a/slicops/pkcli/epics.py b/slicops/pkcli/epics.py index 2293371..75fb943 100644 --- a/slicops/pkcli/epics.py +++ b/slicops/pkcli/epics.py @@ -11,6 +11,7 @@ import pykern.pkcli import pykern.pkio import subprocess +import threading import time # Local so should connect quickly @@ -18,6 +19,7 @@ _LOG_BASE = "sim_detector.log" _CAM1_STATUS_PV = "13SIM1:cam1:ShutterMode" _CAM1_CONTROL_PV = "13SIM1:cam1:TriggerMode" +_INITIAL_TARGET_STATUS = 1 def init_sim_detector(): @@ -39,7 +41,7 @@ def init_sim_detector(): "13SIM1:cam1:SizeX": 1024, "13SIM1:cam1:SizeY": 768, "13SIM1:image1:EnableCallbacks": 1, - _CAM1_STATUS_PV: 1, + _CAM1_STATUS_PV: _INITIAL_TARGET_STATUS, _CAM1_CONTROL_PV: 0, }.items(): pv = epics.PV(name) @@ -88,28 +90,31 @@ def _st_cmd(dir_path): ) def _watch_target(): + def _control_monitor(pvname, value, **kwargs): - nonlocal new_status # control value: 0: move out, 1: move in # status: 1: out, 2: in - new_status = value + 1 + state.new_status = value + 1 + state.trigger.set() - new_status = None - current_status = new_status - s = epics.PV(_CAM1_STATUS_PV) - epics.PV(_CAM1_CONTROL_PV).add_callback(_control_monitor) - try: + def _watch_status(): while True: - if current_status != new_status: - # transition through the "0 moving" state - if current_status == 0: - current_status = new_status - else: - current_status = 0 - s.put(current_status, wait=True, timeout=3) - time.sleep(1) - except KeyboardInterrupt: - pass + state.trigger.wait() + state.trigger.clear() + if state.status != state.new_status: + state.status_pv.put(0, wait=True, timeout=3) + time.sleep(2) + state.status = state.new_status + state.status_pv.put(state.status, wait=True, timeout=3) + + state = PKDict( + new_status=_INITIAL_TARGET_STATUS, + status=_INITIAL_TARGET_STATUS, + status_pv=epics.PV(_CAM1_STATUS_PV), + trigger=threading.Event(), + ) + epics.PV(_CAM1_CONTROL_PV).add_callback(_control_monitor) + threading.Thread(target=_watch_status, daemon=True).start() d = _dir() with _log() as o: @@ -131,6 +136,8 @@ def _control_monitor(pvname, value, **kwargs): _watch_target() pkdlog("waiting for pid={} to exit", p.pid) p.wait() + except KeyboardInterrupt: + pass finally: if p.poll() is not None: p.terminate()