Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 31 additions & 18 deletions etc/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,7 @@
set -eou pipefail
shopt -s nullglob

_root_dir=$(cd "$(dirname "$0")/.." && pwd)
_run_dir=$_root_dir/run
_docker_image=docker://radiasoft/slicops
declare -A _port_map=(
[api]=0
[repeater]=2
[server]=3
[vue]=1
)
_bashrc=$_run_dir/bashrc.sh
_python_version=3.13.9
_sif=${SLICOPS_APPTAINER_SIF:-$_run_dir/slicops.sif}
_sim_dir=/home/vagrant/.local/epics/extensions/synApps/support/areaDetector-R3-12-1/ADSimDetector/iocs/simDetectorIOC/iocBoot/iocSimDetector
_vue_dir=$_root_dir/ui
_start_msg="To develop, start the servers in this order in separate terminals:
bash etc/run.sh sim # sim-detector for DEV_CAMERA
bash etc/run.sh vue # vue server for javascript
bash etc/run.sh api # tornado for business logic and main server"
declare -A _port_map

_dispatch_op() {
declare op=$1
Expand Down Expand Up @@ -251,12 +234,42 @@ _err() {
exit 1
}

_globals() {
declare r=$(cd "$(dirname "$0")/.." && pwd)
_run_dir=$r/run
_bashrc=$_run_dir/bashrc.sh
_docker_image=docker://radiasoft/slicops
_port_map=(
[api]=0
[repeater]=2
[server]=3
[vue]=1
)
_python_version=3.13.9
_sif=${SLICOPS_APPTAINER_SIF:-}
if [[ ! $_sif ]]; then
# POSIT: path defined by SLAC
_sif=/sdf/group/ad/org/lfd/hla/apptainer/slicops.sif
if [[ ! -r $_sif ]]; then
_sif=$_run_dir/slicops.sif
fi
fi
# POSIT: same as radiasoft/download/installers/epics-area-detector
_sim_dir=/home/vagrant/.local/epics/extensions/synApps/support/areaDetector-R3-12-1/ADSimDetector/iocs/simDetectorIOC/iocBoot/iocSimDetector
_vue_dir=$r/ui
_start_msg="To develop, start the servers in this order in separate terminals:
bash etc/run.sh sim # sim-detector for DEV_CAMERA (for dev only)
bash etc/run.sh vue # vue/vite server for javascript (for dev only)
bash etc/run.sh api # tornado serving vue/vite and APIs"
}

_log() {
_msg $(date +%H%M%S) "$@"
}

_main() {
declare op=${1:-<not set>}
_globals
_env_check
_dispatch_op "$op" "${@:2}"
}
Expand Down
34 changes: 21 additions & 13 deletions slicops/ctx.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,29 +129,29 @@ def is_field_value_valid(self, name, value):
self.__field(name).value_check(value), slicops.field.InvalidFieldValue
)

def field_get(self, name):
return self.__field(name).value_get()

def field_names(self):
# keys are always the same
return tuple(self.__ctx.fields.keys())

def field_set(self, name, value):
def field_value(self, name):
return self.__field(name).value()

def field_value_set(self, name, value):
# TODO(robnagler) optimize this case to not validate constraints/ui
# could possibly optimize the ui and constraints parts when a copy
# vs new with _defaults() (which should get validated first time)
self.__field_update(name, self.__field(name), PKDict(value=value))

def field_set_via_api(self, name, value, on_method):
def field_value_set_via_api(self, name, value, on_method):
def _update(old, new):
rv = PKDict(field_name=name, on_method=on_method, txn=self)
if on_method.kind == "click":
if new.group_get("ui", "clickable"):
if new.group_attr("ui", "clickable"):
return rv
pkdlog("on_click_{} exists and clickable=False", c.field_name)
return None
if on_method.kind == "change":
rv.pkupdate(value=n.value_get(), old_value=o.value_get())
rv.pkupdate(value=n.value(), old_value=o.value())
if rv.value == rv.old_value:
return None
return rv
Expand All @@ -161,7 +161,7 @@ def _update(old, new):

try:
o = self.__field(name)
if not o.group_get("ui", "writable"):
if not o.group_attr("ui", "writable"):
raise pykern.util.APIError(
"field={} is not writable value={}", name, value
)
Expand All @@ -172,13 +172,21 @@ def _update(old, new):
raise
raise pykern.util.APIError("invalid value for field={} error={}", name, e)

def group_get(self, field, group, attr=None):
return self.__ctx.fields[field].group_get(group, attr)
def group_attr(self, field_or_dotted, group=None, attr=None):
if group is None:
p = field_or_dotted.split(".")
(f, group, attr) = tuple(p + [None] * (3 - len(p)))
else:
f = field_or_dotted
return self.__field(f).group_attr(group, attr)

def group_attr_set(self, dotted, value):
self.multi_group_attr_set((dotted, value))

def multi_get(self, fields):
return PKDict((k, self.__field(k).value_get()) for k in fields)
def multi_field_value(self, fields):
return PKDict((k, self.__field(k).value()) for k in fields)

def multi_set(self, *args):
def multi_group_attr_set(self, *args):
def _args():
if len(args) > 1:
# (("a", 1), ("b", 2), ..)
Expand Down
10 changes: 5 additions & 5 deletions slicops/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def _copy():
def as_dict(self):
return PKDict((k, copy.deepcopy(self._attrs[k])) for k in self.__TOP_ATTRS)

def group_get(self, group, attr=None):
def group_attr(self, group, attr=None):
if group not in self.__GROUP_ATTRS:
raise AssertionError(f"invalid group={group} must be {self.__GROUP_ATTRS}")
g = self._attrs[group]
Expand All @@ -77,6 +77,9 @@ def renew(self, overrides):
# Updating an instance inherits everything
return self.__class__(self, overrides)

def value(self):
return self._attrs.value

def value_check(self, value):
if value is None or hasattr(value, "__len__") and len(value) == 0:
if self._attrs.constraints.nullable:
Expand All @@ -90,9 +93,6 @@ def value_check(self, value):
rv.kwargs.field_name = self._attrs.name
return rv

def value_get(self):
return self._attrs.value

def value_set(self, value):
v = self.value_check(value)
if isinstance(v, InvalidFieldValue):
Expand Down Expand Up @@ -225,7 +225,7 @@ def _defaults(self, *overrides):
return super()._defaults(
PKDict(
name="Button",
ui=PKDict(widget="button", clickable=True),
ui=PKDict(widget="button", css_kind="primary", clickable=True),
# value is always None
value=None,
),
Expand Down
2 changes: 1 addition & 1 deletion slicops/sliclet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ def _click(updates):
def _updates():
m = self.__on_methods
for k, v in field_values.items():
if c := txn.field_set_via_api(k, v, m.get(k)):
if c := txn.field_value_set_via_api(k, v, m.get(k)):
yield c

with self.lock_for_update(log_op="ctx_write") as txn:
Expand Down
2 changes: 1 addition & 1 deletion slicops/sliclet/fractals.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class Fractals(slicops.sliclet.yaml_db.YAMLDb):

def on_change_mode(self, txn, value, **kwargs):
j = value == "Julia"
txn.multi_set(
txn.multi_group_attr_set(
("density_i.ui.visible", j),
("density_r.ui.visible", j),
)
Expand Down
44 changes: 25 additions & 19 deletions slicops/sliclet/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def handle_destroy(self):
self.__device_destroy()

def on_change_camera(self, txn, value, **kwargs):
self.__device_change(txn, txn.field_get("beam_path"), value)
self.__device_change(txn, txn.field_value("beam_path"), value)

def on_change_beam_path(self, txn, value, **kwargs):
self.__beam_path_change(txn, value)
Expand Down Expand Up @@ -137,7 +137,9 @@ def handle_init(self, txn):
self.__device = None
self.__handler = None
self.__single_button = False
txn.multi_set(("beam_path.constraints.choices", slicops.device_db.beam_paths()))
txn.multi_group_attr_set(
("beam_path.constraints.choices", slicops.device_db.beam_paths())
)
self.__beam_path_change(txn, None)
self.__device_change(txn, None, None)
b = c = None
Expand All @@ -146,44 +148,48 @@ def handle_init(self, txn):
c = _cfg.dev.camera
# the values are None by default, but this initializes
# the state of the choices, buttons and fields appropriately
txn.field_set("beam_path", b)
txn.field_value_set("beam_path", b)
self.__beam_path_change(txn, b)
txn.field_set("camera", c)
txn.field_value_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_value("beam_path"), txn.field_value("camera")
)

def __beam_path_change(self, txn, value):
def _choices():
if value is None:
return ()
return slicops.device_db.device_names(_DEVICE_TYPE, value)

txn.multi_set(
txn.multi_group_attr_set(
("camera.constraints.choices", _choices()),
("camera.value", None),
)
# This technically shouldn't happen
if value is None:
txn.multi_set(
txn.multi_group_attr_set(
_DEVICE_DISABLE
+ (("camera.ui.enabled", False), ("camera.ui.visible", False))
)
else:
txn.multi_set((("camera.ui.enabled", True), ("camera.ui.visible", True)))
txn.multi_group_attr_set(
(("camera.ui.enabled", True), ("camera.ui.visible", True))
)
if not self.__device:
# No device change
return
c = self.__device.device_name
if txn.is_field_value_valid("camera", c):
# Camera is the same so restore the value, no device change
txn.field_set("camera", c)
txn.field_value_set("camera", c)
else:
self.__device_change(txn, value, None)

def __device_change(self, txn, beam_path, camera):
self.__device_destroy(txn)
txn.multi_set(_DEVICE_DISABLE)
txn.multi_group_attr_set(_DEVICE_DISABLE)
self.__device_setup(txn, beam_path, camera)

def __device_destroy(self, txn=None):
Expand Down Expand Up @@ -230,15 +236,15 @@ def __device_setup(self, txn, beam_path, camera):
s = PKDict(_DEVICE_ENABLE + (("csi_name.value", self.__device.meta.csi_name),))
if self.__device.has_accessor("target_status"):
s.update(_TARGET_VISIBLE)
txn.multi_set(s)
txn.multi_group_attr_set(s)
self.__new_image_set(txn)

def __handle_acquire(self, acquire):
with self.lock_for_update() as txn:
self.__current_value["acquire"] = acquire
n = not acquire
# Leave plot alone
txn.multi_set(
txn.multi_group_attr_set(
("single_button.ui.enabled", n),
("start_button.ui.enabled", n),
(
Expand All @@ -257,14 +263,14 @@ def __handle_image(self, image):
self.__current_value["image"] = image
if self.__update_plot(txn) and self.__single_button:
self.__set(txn, "acquire", False, _BUTTONS_DISABLE)
txn.multi_set(
txn.multi_group_attr_set(
("single_button.ui.enabled", True),
("start_button.ui.enabled", True),
)

def __new_image_set(self, txn):
self.__image_set = slicops.plot.ImageSet(
txn.multi_get(
txn.multi_field_value(
(
"beam_path",
"camera",
Expand All @@ -278,7 +284,7 @@ def __new_image_set(self, txn):
def __handle_target_status(self, status):
with self.lock_for_update() as txn:
self.__current_value["target"] = status
txn.multi_set(
txn.multi_group_attr_set(
("target_status", status.name),
(
"target_in_button.ui.enabled",
Expand Down Expand Up @@ -306,7 +312,7 @@ def __set(self, txn, accessor, value, txn_set, method=None):
if v is not None and v == value:
# No button disable since nothing changed
return
txn.multi_set(txn_set)
txn.multi_group_attr_set(txn_set)
try:
if method is None:
self.__device.put(accessor, value)
Expand All @@ -326,9 +332,9 @@ def __update_plot(self, txn):
return False
if (p := self.__image_set.add_frame(i, pykern.pkcompat.utcnow())) is None:
return False
if not txn.group_get("plot", "ui", "visible"):
txn.multi_set(_PLOT_ENABLE)
txn.field_set("plot", p)
if not txn.group_attr("plot", "ui", "visible"):
txn.multi_group_attr_set(_PLOT_ENABLE)
txn.field_value_set("plot", p)
return True

def __user_alert(self, txn, fmt, *args):
Expand Down
14 changes: 7 additions & 7 deletions slicops/sliclet/yaml_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,21 +76,21 @@ def _visibility(value):
p = PKDict(raw_pixels=None)
v = False
try:
if not (l := txn.field_get(n)):
if not (l := txn.field_value(n)):
return None
p.raw_pixels = numpy.load(l)
v = True
return p
except Exception as e:
pkdlog("numpy.load error={} path={} link={} stack={}", e, l, n, pkdexc())
finally:
txn.field_set(plot, p)
txn.multi_set(tuple(_visibility(v)))
txn.field_value_set(plot, p)
txn.multi_group_attr_set(tuple(_visibility(v)))

def __read_db(self, txn):
def _numpy_files():
for k in txn.field_names():
if l := txn.group_get(k, "links"):
if l := txn.group_attr(k, "links"):
if v := self.__numpy_file(txn, k, l):
yield k, v

Expand All @@ -107,7 +107,7 @@ def _set(db):
# If cache (read/wrote last time) is unchanged,
# there will be no updates. Avoids churn
if k in db and db[k] != self.__db_cache.get(k):
txn.field_set(k, db[k])
txn.field_value_set(k, db[k])
yield k, db[k]

if not (r := slicops.pkcli.yaml_db.read(self.name)):
Expand All @@ -120,13 +120,13 @@ def _set(db):
def __write(self, txn):
def _keys():
for k in txn.field_names():
g = txn.group_get(k, "ui")
g = txn.group_attr(k, "ui")
if g.get("clickable") or not g.get("writable"):
continue
yield k

# TODO(robnagler) work: maybe should happen outside lock
self.__db_cache = PKDict((k, txn.field_get(k)) for k in _keys())
self.__db_cache = PKDict((k, txn.field_value(k)) for k in _keys())
slicops.pkcli.yaml_db.write(self.name, self.__db_cache)


Expand Down
Loading
Loading