feat(flomni): realtime_system.help(name) / help("all"); marked beamline scientists / local contact only

- help("<setting>"): what it does (help text of the flOMNI definition, as the web HMI tooltips), range, live and
  saved value, the loop parameters it sets, its rules; help("all"): every setting by group
- realtime_system is for beamline scientists / local contact only (Mirko 2026-10-23): module docstring, a line at
  show() / help(), user guide section heading and note
- tests: help output

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aWcwG89wi7oVyrefukXX6
This commit is contained in:
Claude committed 2026-10-23 18:56:12 +00:00
1 parent 533fca8c24
commit 76aa2b9e56
3 files changed
+78 -8

No files matched your search

@@ -1,4 +1,8 @@
"""flomni.realtime_system: the operating settings of the flOMNI real-time loop (expert functions).
"""flomni.realtime_system: the operating settings of the flOMNI real-time loop.
BEAMLINE SCIENTISTS / LOCAL CONTACT ONLY - not for users. These settings change how the FZP scanner, the detector
trigger and the laser tracker work; a wrong combination can spoil a beamtime's data. The rules refuse combinations
that do not work, but not ones that are only unsuitable for an experiment.
The settings, the parameters they map onto and the rules between them live in the flOMNI instrument repository
(FeedbackLoop/loopsettings/flomni.json) and are applied by the web HMI on the loop machine (RT-system
@@ -23,6 +27,9 @@ from csaxs_bec.devices.omny.webhmi_client import WebHmiClient
from csaxs_bec.devices.omny.webhmi_client import WebHmiError as RealtimeSystemError # name kept for BEC users
STAFF_ONLY = "flomni.realtime_system: beamline scientists / local contact only - not for users."
def _fmt(v):
if v is None:
return "-"
@@ -82,6 +89,7 @@ class FlomniRealtimeSystem(WebHmiClient):
snap = self.read()
groups = {g["name"]: g for g in snap["groups"]}
sc = snap.get("scan") or {}
print(STAFF_ONLY)
print(f"Loop settings {snap.get('instrument', '')} ({self.route}) - "
+ (f"scan running (mode {_fmt(sc.get('mode'))})" if sc.get("active") else "no scan"))
for gname, g in groups.items():
@@ -216,8 +224,49 @@ class FlomniRealtimeSystem(WebHmiClient):
v = self.get()
return v["tracking_target_y"], v["tracking_target_z"]
def help(self):
"""Print the functions with a short explanation."""
print(__doc__.split("\n\nConnection:")[0])
print("\nShortcuts: trigger_show(), trigger_set_tolerance_nm(v), trigger_set_hold_ms(v),"
" trigger_set_after_move(True/False), tracking_targets(); profiles(), unsaved(), get(name)")
def help(self, name: str = None):
"""help(): the functions; help("all"): every setting with its explanation; help(name): one setting (what it
does, unit, range, the loop parameters it sets, live and saved value), from the definition on the loop
machine (the same text as the tooltips of the web HMI tab)."""
if name is None:
print(__doc__.split("\n\nConnection:")[0])
print("\nShortcuts: trigger_show(), trigger_set_tolerance_nm(v), trigger_set_hold_ms(v),"
" trigger_set_after_move(True/False), tracking_targets(); profiles(), unsaved(), get(name)")
print('Explanations: help("all") or help("<setting>"), e.g. help("move_beyond_um_x")')
return
snap = self.read()
settings = {s["name"]: s for s in snap["settings"]}
groups = {g["name"]: g for g in snap["groups"]}
if name == "all":
print(STAFF_ONLY)
for gname, g in groups.items():
rows = [s for s in snap["settings"] if s["group"] == gname]
if not rows:
continue
print(f"\n{g.get('title', gname)}" + (" (only between scans)" if g.get("interlock") else ""))
for s in rows:
unit = f" [{s['unit']}]" if s.get("unit") else ""
print(f" {s['name']}{unit}: {s.get('title', '')}")
if s.get("help"):
print(f" {s['help']}")
return
s = settings.get(name)
if s is None:
raise RealtimeSystemError(f"unknown setting '{name}'; help('all') lists them")
g = groups.get(s["group"], {})
print(STAFF_ONLY)
print(f"{s['name']} - {s.get('title', '')} (group {g.get('title', s['group'])}"
+ (", only between scans)" if g.get("interlock") else ")"))
if s.get("help"):
print(f" {s['help']}")
rng = "0 / 1" if s.get("type") == "bool" else f"{_fmt(s.get('min'))} ... {_fmt(s.get('max'))} {s.get('unit', '')}"
print(f" range: {rng}")
print(f" live: {_fmt(s.get('value'))} saved (.xmp): {_fmt(s.get('saved'))}"
+ (" NOT SAVED" if s.get("unsaved") else "") + ("" if s.get("consistent", True) else " INCONSISTENT"))
if s.get("info"):
print(f" {s['info'].get('label', '')} {s['info']['value']:.4g} {s['info'].get('unit', '')}")
print(" sets: " + ", ".join(f"{t['module']}.{t['param']}[{t['index']}] = {t.get('write', 'v')}"
for t in s.get("targets", [])))
rules = [r for r in snap.get("rules", []) if name in r.get("settings", [])]
if rules:
print(" rules: " + ", ".join(r["name"] + ("" if r.get("ok") else " (VIOLATED)") for r in rules))
+8 -2
View File
@@ -224,6 +224,8 @@ In step scans the detector is triggered when the sample is stable at the scan po
sample vs FZP (interferometer minus FZP cap sensor) must stay within a tolerance in x and in y (each averaged
over 0.5 ms) for a hold time. Both values are live and can be changed between scans:
(Beamline scientists / local contact only, see below.)
- `flomni.realtime_system.trigger_show()` — current tolerance, hold time, minimum delay, trigger after a move
- `flomni.realtime_system.trigger_set_tolerance_nm(10)` — tolerance per axis in nm: about 30 for a fast
low-resolution scan, 10 standard, 2 for the highest resolution (a few ms more per point);
@@ -782,7 +784,10 @@ removed later.
[See short version](flomni.md#sample-storage-and-transfer)
### Real-time loop settings (expert): `flomni.realtime_system`
### Real-time loop settings: `flomni.realtime_system` (beamline scientists / local contact only)
**Not for users.** These settings change how the FZP scanner, the detector trigger and the laser tracker work; a
wrong combination can spoil the data of a beamtime. Users ask the local contact.
The operating settings of the real-time loop — FZP scanner on/off, FZP range, feedforward, move-beyond, exposure
during the motion, Abbe correction, detector trigger, laser tracker targets and thresholds — with the rules between
@@ -798,7 +803,8 @@ them. The rules and the mapping onto the loop parameters are kept on the loop ma
- `flomni.realtime_system.profiles()`, `apply_profile("production" | "fzp_off" | "fzp_calibration" | "fast_scan")`
- `flomni.realtime_system.unsaved()`, `save_as_default()` — the changes are live only until the next start of the
control loop; `save_as_default()` writes them into the loop's parameter files
- `flomni.realtime_system.help()`
- `flomni.realtime_system.help()`; `help("all")` explains every setting, `help("move_beyond_um_x")` one setting
(what it does, range, the loop parameters it sets, live and saved value, its rules)
Every scan stores the loop settings it ran with: the baseline device `flomni_loop_settings` (one value per
setting, `available`, unsaved / inconsistent counts, the definition's sha1, a JSON of all settings);
@@ -197,3 +197,18 @@ def test_routes_through_the_device():
dm.devices.flomni_loop_settings.enabled = False # disabled: direct HTTP
with pytest.raises(RealtimeSystemError, match="No API token"):
rts.get()
def test_help(server, capsys):
"""help(name) / help("all"): explanations from the definition, marked as beamline-scientist only."""
url, tok, _ = server
rts = FlomniRealtimeSystem(url=url, token_file=tok)
rts.help("trigger_tolerance_nm")
out = capsys.readouterr().out
assert "local contact only" in out and "trigger_tolerance_nm" in out and "live: 10" in out
rts.help("all")
assert "Detector trigger" in capsys.readouterr().out
rts.help()
assert "BEAMLINE SCIENTISTS" in capsys.readouterr().out
with pytest.raises(RealtimeSystemError, match="unknown setting"):
rts.help("nosuch")