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
1 change: 1 addition & 0 deletions .github/workflows/ci-scripts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ jobs:
python3 test/test_generate_dependency_report.py
python3 test/verify_coordinate_map.py
python3 test/validate_bob_xml.py
python3 test/validate_bob_prefix_macros.py
reuse lint
git diff --check

Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ dbd/
include/
.vscode/
TemplateReadme.md

# Python bytecode caches
__pycache__/
*.py[cod]

# IOC autosave runtime data (README.md is tracked)
iocs/tpx3IOC/iocBoot/iocTimePix/autosave/*
!iocs/tpx3IOC/iocBoot/iocTimePix/autosave/README.md
Expand Down
179 changes: 179 additions & 0 deletions test/validate_bob_prefix_macros.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""Static regression checks for ADTimePix3 service-prefix inheritance."""

from pathlib import Path
import sys
import xml.etree.ElementTree as ET


ROOT = Path(__file__).resolve().parents[1]
BOB_ROOT = ROOT / "tpx3App" / "op" / "bob"
LEGACY_PREFIXES = ("SERVAL-TEST:", "EMU-TEST:")

CHILD_SCREENS = {
BOB_ROOT / "Serval/tpx3serval.bob": ("TPX3-TEST:", "Serval:"),
BOB_ROOT / "Emulator/emulator.bob": ("TPX3-TEST:", "Emulator:"),
BOB_ROOT / "Emulator/emulator_tpx3_embed.bob": ("TPX3-TEST:", "Emulator:"),
BOB_ROOT / "Emulator/emulator_mpx3_embed.bob": ("MPX3-TEST:", "Emulator:"),
}

ENTRY_SCREENS = {
BOB_ROOT / "TimePix3.bob": ("TPX3-TEST:", "profiles/tpx3/main.bob"),
BOB_ROOT / "MediPix3.bob": ("MPX3-TEST:", "profiles/mpx3/main.bob"),
BOB_ROOT / "MediPix3/MediPix3.bob": ("MPX3-TEST:", "../profiles/mpx3/main.bob"),
}

LAUNCHERS = {
BOB_ROOT / "profiles/tpx3/TimePix3Status.bob": "TPX3-TEST:",
BOB_ROOT / "profiles/tpx3/Acquire/DetectorConfig.bob": "TPX3-TEST:",
BOB_ROOT / "profiles/mpx3/Mpx3Status.bob": "MPX3-TEST:",
BOB_ROOT / "profiles/mpx3/Detector/Mpx3DetectorConfig.bob": "MPX3-TEST:",
}

TARGETS = {
"emulator.bob": "Emulator:",
"tpx3serval.bob": "Serval:",
}

STANDALONE_DEFAULT_SCREENS = {
BOB_ROOT / "Events/tpx3RawStream.bob",
}


def relative(path: Path) -> str:
return str(path.relative_to(ROOT))


def main() -> int:
errors: list[str] = []
parsed: dict[Path, ET.Element] = {}
allowed_default_screens = (
set(CHILD_SCREENS)
| set(ENTRY_SCREENS)
| set(LAUNCHERS)
| STANDALONE_DEFAULT_SCREENS
)

for path in sorted(BOB_ROOT.rglob("*.bob")):
text = path.read_text(encoding="utf-8")
for prefix in LEGACY_PREFIXES:
if prefix in text:
errors.append(f"{relative(path)}: obsolete prefix {prefix}")
if "$(P=" in text and path not in allowed_default_screens:
errors.append(
f"{relative(path)}: P fallback belongs at a display boundary, "
"not in an ordinary embedded panel"
)
try:
parsed[path] = ET.fromstring(text)
except ET.ParseError as exc:
errors.append(f"{relative(path)}: invalid XML: {exc}")

for path, (fallback, service_r) in CHILD_SCREENS.items():
root = parsed.get(path)
if root is None:
errors.append(f"{relative(path)}: child screen was not parsed")
continue

if root.find("./macros/P") is not None:
errors.append(f"{relative(path)}: display-level P overrides callers")
if root.findtext("./macros/R") != service_r:
errors.append(f"{relative(path)}: expected display-level R={service_r}")

expected = f"$(P={fallback})$(R)"
pv_names = [node.text or "" for node in root.iter("pv_name")]
if not any(expected in value for value in pv_names):
errors.append(f"{relative(path)}: missing fallback {fallback}")
for value in pv_names:
if "$(P" in value and expected not in value:
errors.append(
f"{relative(path)}: unexpected P expression in PV {value!r}"
)

for path, (fallback, profile_file) in ENTRY_SCREENS.items():
root = parsed.get(path)
if root is None:
errors.append(f"{relative(path)}: entry screen was not parsed")
continue

if root.find("./macros/P") is not None or root.find("./macros/R") is not None:
errors.append(f"{relative(path)}: display-level P/R overrides callers")

embedded = next(
(
widget
for widget in root.findall("./widget[@type='embedded']")
if widget.findtext("file") == profile_file
),
None,
)
if embedded is None:
errors.append(f"{relative(path)}: missing embedded profile {profile_file}")
continue

expected_p = f"$(P={fallback})"
actual_p = embedded.findtext("macros/P")
actual_r = embedded.findtext("macros/R")
if actual_p != expected_p:
errors.append(
f"{relative(path)}: embedded profile passes P={actual_p!r}, "
f"expected {expected_p!r}"
)
if actual_r != "$(R=cam1:)":
errors.append(
f"{relative(path)}: embedded profile passes R={actual_r!r}, "
"expected '$(R=cam1:)'"
)

action_count = 0
for path, root in parsed.items():
for action in root.findall(".//action[@type='open_display']"):
target_file = action.findtext("file") or ""
matching_target = next(
(target for target in TARGETS if target_file.endswith(target)), None
)
if matching_target is None:
continue

action_count += 1
fallback = LAUNCHERS.get(path)
if fallback is None:
errors.append(
f"{relative(path)}: unclassified launcher action for {target_file}"
)
continue

expected_p = f"$(P={fallback})"
expected_r = TARGETS[matching_target]
actual_p = action.findtext("macros/P")
actual_r = action.findtext("macros/R")
if actual_p != expected_p:
errors.append(
f"{relative(path)}: {target_file} passes P={actual_p!r}, "
f"expected {expected_p!r}"
)
if actual_r != expected_r:
errors.append(
f"{relative(path)}: {target_file} passes R={actual_r!r}, "
f"expected {expected_r!r}"
)

if action_count != 8:
errors.append(f"expected 8 Serval/emulator launcher actions, found {action_count}")

if errors:
print("ADTimePix3 screen checks failed:", file=sys.stderr)
for error in errors:
print(f"- {error}", file=sys.stderr)
return 1

print(
f"Validated {len(parsed)} BOB files, {len(ENTRY_SCREENS)} entry screens, "
f"{len(CHILD_SCREENS)} child screens, and {action_count} "
"Serval/emulator launcher actions."
)
return 0


if __name__ == "__main__":
raise SystemExit(main())
27 changes: 13 additions & 14 deletions tpx3App/op/bob/Emulator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ This directory contains Phoebus **Display Builder** (`.bob`) screens for the **m
See the `.bob` files for exact widget placement. Logical sections:

### 1. Main Control Section (Top)
- **Start/Stop Slide Button**: Controls the TPX3 emulator process (connected to `$(P)$(R)START` PV)
- **Status LED**: Visual indicator showing if the process is running (green) or stopped (red) (connected to `$(P)$(R)STATUS` PV)
- **Process ID Display**: Shows the current process ID when running (connected to `$(P)$(R)PROCESS_ID` PV)
- **Error Message Display**: Shows any error messages or status updates (connected to `$(P)$(R)ERROR_MSG` PV)
- **Start/Stop Slide Button**: Controls the TPX3 emulator process (connected to `$(P=TPX3-TEST:)$(R)START` PV)
- **Status LED**: Visual indicator showing if the process is running (green) or stopped (red) (connected to `$(P=TPX3-TEST:)$(R)STATUS` PV)
- **Process ID Display**: Shows the current process ID when running (connected to `$(P=TPX3-TEST:)$(R)PROCESS_ID` PV)
- **Error Message Display**: Shows any error messages or status updates (connected to `$(P=TPX3-TEST:)$(R)ERROR_MSG` PV)

### 2. Shell configuration ( **`emulator.bob`** )
- **`EMULATOR_TYPE`**: Choosing 0 vs 1 toggles visibility of the two embedded `.bob` files (rules use `$(P)$(R)EMULATOR_TYPE`; keep `$(P)` defined when launching the screen).
- **`EMULATOR_TYPE`**: Choosing 0 vs 1 toggles visibility of the two embedded `.bob` files. The shell uses `$(P=TPX3-TEST:)$(R)EMULATOR_TYPE`, so caller-provided `P` wins and a direct open uses the TPX3 test fallback.
- **Firmware / network**: Shared widgets on the shell (same PVs as the IOC DB).
- **Embedded Timepix3 panel**: Hit rate, TDC, chipboard, JVM `-DchipMask`, and all raw replay widgets — see **`emulator_tpx3_embed.bob`**.
- **Embedded Medipix3 panel**: MPX CLI options — see **`emulator_mpx3_embed.bob`**.
Expand All @@ -31,7 +31,7 @@ See the `.bob` files for exact widget placement. Logical sections:
- **JAR**, **`-Xmx`**, **`-DcmdPort`**: Applicable to both emulator JAR types; replay controls are **not** here anymore (they are on the TPX embed).

### 4. Command line (bottom)
- **Generated Command Line**: Shows the complete Java command that will be executed (connected to `$(P)$(R)COMMAND_LINE` PV)
- **Generated Command Line**: Shows the complete Java command that will be executed (connected to `$(P=TPX3-TEST:)$(R)COMMAND_LINE` PV)
- **Real-time Updates**: Updates automatically as parameters are changed

## Supported Widget Types
Expand Down Expand Up @@ -79,15 +79,14 @@ The display uses the following Phoebus Display Builder compatible widgets:

## PV Naming Convention

The display uses the standard EPICS PV naming convention with predefined macros:
- **Prefix**: `$(P)` = `EMU-TEST:` (dedicated emulator service; not the camera `TPX3-TEST:` / `MPX3-TEST:` prefix)
The displays use the standard EPICS PV naming convention with caller-overridable macros:
- **Prefix**: `emulator.bob` and `emulator_tpx3_embed.bob` use `$(P=TPX3-TEST:)`; `emulator_mpx3_embed.bob` uses `$(P=MPX3-TEST:)`
- **Record**: `$(R)` = `Emulator:`
- **Example**: The START PV becomes `EMU-TEST:Emulator:START`
- **Example**: The TPX3 lab-default START PV is `TPX3-TEST:Emulator:START`

Lab default matches `/epics/iocs/emulator/iocBoot/ioctpx3emulator/st.cmd` (`Sys=EMU-TEST:`).
Beamline override: open with `P=EMU-$(BL):` (e.g. `EMU-BL7:`) and set the IOC `Sys` to the same value.
**Do not inherit camera `P`** — Phoebus caller macros override display defaults, so every Emulator
`open_display` action should pass `P`/`R` explicitly.
No source screen defines a display-level `P`, because that would override `P` inherited from an upper beamline screen. Opening a source screen directly uses its detector-specific fallback; opening it from an upper screen uses the caller-provided `P`. `R=Emulator:` remains defined at display level.

The IOC is configured independently with `DETECTOR_PREFIX`, which defaults to `TPX3-TEST:` in `iocBoot/ioctpx3emulator/st.cmd`. For beamline use, set `DETECTOR_PREFIX` to the corresponding ADTimePix3 IOC's system prefix and pass that same value as `P` from the upper screen.

Sync source of truth: `/epics/iocs/emulator/tpx3emulatorApp/op/bob/` → this directory.

Expand Down Expand Up @@ -129,7 +128,7 @@ Sync source of truth: `/epics/iocs/emulator/tpx3emulatorApp/op/bob/` → this di

### **PVs Not Connecting**
- Verify EPICS Channel Access is working
- Check that macros `$(P)` and `$(R)` are properly defined
- Check that caller macro `P`, or the appropriate TPX3/MPX3 fallback, matches the IOC's `DETECTOR_PREFIX`; `R` should remain `Emulator:`
- Ensure the IOC is running and accessible

### **Text Not Updating**
Expand Down
Loading