diff --git a/slsDetectorCalibration/dataStructures/jungfrauLGADStrixelsData_new.h b/slsDetectorCalibration/dataStructures/jungfrauLGADStrixelsData_new.h index cbc2d5518..8774159a6 100644 --- a/slsDetectorCalibration/dataStructures/jungfrauLGADStrixelsData_new.h +++ b/slsDetectorCalibration/dataStructures/jungfrauLGADStrixelsData_new.h @@ -296,8 +296,8 @@ class jungfrauLGADStrixelsData : public slsDetectorData { /*nr_strixel*/ 2 * g1_nrows + 2 * g2_nrows + 2 * g3_nrows, g1_ncols * (2 * g1_nrows + 2 * g2_nrows + 2 * g3_nrows) * 2 + sizeof(header)) { - std::cout << "Jungfrau strixels 2X single chip with full module data " - << std::endl; +// std::cout << "Jungfrau strixels 2X single chip with full module data " +// << std::endl; // Fill all strixels with dummy values for (int ix = 0; ix != g1_ncols; ++ix) { @@ -307,9 +307,9 @@ class jungfrauLGADStrixelsData : public slsDetectorData { } } - std::cout << "sizeofheader = " << sizeof(header) << std::endl; - std::cout << "Jungfrau strixels 2X single chip with full module data " - << std::endl; +// std::cout << "sizeofheader = " << sizeof(header) << std::endl; +// std::cout << "Jungfrau strixels 2X single chip with full module data " +// << std::endl; if (xmin < xmax && ymin < ymax) { @@ -331,7 +331,7 @@ class jungfrauLGADStrixelsData : public slsDetectorData { } iframe = 0; - std::cout << "data struct created" << std::endl; + // std::cout << "data struct created" << std::endl; // per frame: too noisy }; /** diff --git a/slsDetectorCalibration/jungfrauExecutables/CMakeLists.txt b/slsDetectorCalibration/jungfrauExecutables/CMakeLists.txt index 6f1fa6fd1..7ea3b4537 100644 --- a/slsDetectorCalibration/jungfrauExecutables/CMakeLists.txt +++ b/slsDetectorCalibration/jungfrauExecutables/CMakeLists.txt @@ -55,6 +55,13 @@ target_compile_definitions(jungfrauRawDataProcessStrxOldAldo PRIVATE JFSTRXOLD A list(APPEND JUNGFRAU_EXECUTABLES jungfrauRawDataProcessStrxOldAldo) +# jf_online_display_gui (Qt5 + ROOT online display / event finder). Off by +# default: it needs Qt5 and ROOT, which the rest of this directory does not. +option(BUILD_JF_ONLINE_DISPLAY_GUI "build jf_online_display_gui" OFF) +if(BUILD_JF_ONLINE_DISPLAY_GUI) + add_subdirectory(jf_online_display_gui) +endif() + # others to be added if needed (might already be there in Makefile.cluster_finder TO BE CHECKED) diff --git a/slsDetectorCalibration/jungfrauExecutables/Makefile b/slsDetectorCalibration/jungfrauExecutables/Makefile deleted file mode 120000 index bde0d5288..000000000 --- a/slsDetectorCalibration/jungfrauExecutables/Makefile +++ /dev/null @@ -1 +0,0 @@ -Makefile.zmqrootdisp \ No newline at end of file diff --git a/slsDetectorCalibration/jungfrauExecutables/Makefile b/slsDetectorCalibration/jungfrauExecutables/Makefile new file mode 100644 index 000000000..1ec21eac7 --- /dev/null +++ b/slsDetectorCalibration/jungfrauExecutables/Makefile @@ -0,0 +1,22 @@ +ROOTSYS=/opt/cern/v6/build + +LIBZMQDIR = $(PWD) +LIBZMQ = -L$(LIBZMQDIR) -lzmq +SHLIB_PATH=/opt/cern/v6/root/lib +CMAKE_PREFIX_PATH=/opt/cern/v6/root +DYLD_LIBRARY_PATH=/opt/cern/v6/root/lib + +QT_CXXFLAGS = $(shell pkg-config --cflags Qt5Core Qt5Gui Qt5Widgets 2>/dev/null) +QT_LIBS = $(shell pkg-config --libs Qt5Core Qt5Gui Qt5Widgets 2>/dev/null) + +INCDIR= -I. -I../dataStructures ../tiffio/src/tiffIO.cpp -I../ -I../interpolations/ -I../../slsSupportLib/include/ -I../../slsReceiverSoftware/include/ -I../../libs/rapidjson/ -I../tiffio/include +LDFLAG= -L/usr/lib64/ -lpthread -lm -lstdc++ -lzmq -pthread -lrt -ltiff -O3 -std=c++17 -Wall -Wl,-rpath-link,$(ROOTSYS)/lib + + +default: onlinedisp_zmq + +onlinedisp_zmq: onlinedisp_zmq.cpp onlinedisp_zmq.h ../dataStructures/jungfrauLGADStrixelsData_new.h + g++ -o onlinedisp_zmq onlinedisp_zmq.cpp $(QT_CXXFLAGS) $(QT_LIBS) -I. -I$(ROOTSYS)/include -Wall -g -lm -L. -lzmq -pthread -lrt -L$(ROOTSYS)/lib -lGui -lCore -lRIO -lNet -lHist -lGraf -lGraf3d -lGpad -lTree -lRint -lPostscript -lMatrix -lPhysics -lMathCore -lThread -m64 -L../../build/bin/ -lSlsSupportStatic ../../build/_deps/libzmq-build/lib/libzmq.a -lstdc++ -lsodium $(LDFLAG) $(INCDIR) + +#-lCint + diff --git a/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/.gitignore b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/.gitignore new file mode 100644 index 000000000..f0d94cdf9 --- /dev/null +++ b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/.gitignore @@ -0,0 +1,16 @@ +# build outputs (make and cmake) +cmake-build/ +jf_online_display_gui +jf_online_display_gui_JFSTRX +gate_contig_test +*.o +*.JFSTRX.o +test/jf_feeder +test/xclick +# run artefacts +current_pede.dat +jfroi_*.json +*.json.lock +roi_list.dat +save_dest_list.dat +testPHmapsave/ diff --git a/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/CMakeLists.txt b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/CMakeLists.txt new file mode 100644 index 000000000..f7f52f936 --- /dev/null +++ b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/CMakeLists.txt @@ -0,0 +1,170 @@ +# SPDX-License-Identifier: LGPL-3.0-or-other +# Copyright (C) 2021 Contributors to the SLS Detector Package +# +# jf_online_display_gui -- single multithreaded program merging +# jf_event_finder_cli / jf_event_finder_gui (ZMQ gate) +# onlinedisp_zmq (Qt5 + ROOT display) +# +# Standalone build (this directory): +# cmake -S . -B build && cmake --build build -j +# ./build/jf_online_display_gui (normal) +# ./build/jf_online_display_gui_JFSTRX (strip-xel) +# +# The Makefile in this directory builds the same two programs; this file exists +# so the program can be built like the rest of the package. +# +# Targets +# jf_online_display_gui normal geometry +# jf_online_display_gui_JFSTRX same sources with -DJFSTRX (strip-xel) +# gate_contig_test gate unit test (JF_BUILD_TESTS=ON, default) +# jf_feeder ZMQ test feeder (JF_BUILD_TESTS=ON, default) +# +# The two variants must never share object files: the geometry and the struct +# layouts differ, so they are compiled twice (JFSTRX/common object libraries). + +cmake_minimum_required(VERSION 3.16) +project(jf_online_display_gui LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE) +endif() + +option(JF_BUILD_TESTS "build the gate unit test and the ZMQ feeder" ON) + +# --------------------------------------------------------------------------- +# paths relative to this directory (same layout as the Makefile) +# PKG = ../../.. package root +# CAL = ../.. slsDetectorCalibration +# --------------------------------------------------------------------------- +get_filename_component(PKG "${CMAKE_CURRENT_SOURCE_DIR}/../../.." ABSOLUTE) +get_filename_component(CAL "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE) + +# --------------------------------------------------------------------------- +# dependencies +# --------------------------------------------------------------------------- +find_package(Qt5 5.9 COMPONENTS Widgets Gui Core REQUIRED) +# online_jf.h includes Qt headers, and every translation unit of this program +# sees them (the core/root/h5 objects too), so the include paths are added to the +# flag list rather than left to the link interface. +set(JF_QT_INCLUDES + ${Qt5Widgets_INCLUDE_DIRS} ${Qt5Gui_INCLUDE_DIRS} ${Qt5Core_INCLUDE_DIRS}) +find_package(ROOT REQUIRED COMPONENTS Core RIO Hist Graf Gpad Thread) + +# HDF5: CMake's own module finds both C and C++ (the C++ headers are not in the +# default include path on every distro). +find_package(HDF5 REQUIRED COMPONENTS C CXX) + +# libzmq: prefer the prebuilt static library from the package build tree (that is +# what the Makefile links), then a system library as a fallback. +set(JF_ZMQ_STATIC "${PKG}/build/_deps/libzmq-build/lib/libzmq.a" + CACHE FILEPATH "static libzmq to link") +if(EXISTS "${JF_ZMQ_STATIC}") + set(JF_ZMQ_LIBRARY "${JF_ZMQ_STATIC}") + set(JF_ZMQ_INCLUDE "${PKG}/build/_deps/libzmq-src/include") + find_package(Threads REQUIRED) # the static build needs pthread/sodium + set(JF_ZMQ_EXTRA Threads::Threads sodium) +else() + find_package(PkgConfig REQUIRED) + pkg_check_modules(ZMQ REQUIRED IMPORTED_TARGET libzmq) + set(JF_ZMQ_LIBRARY PkgConfig::ZMQ) + set(JF_ZMQ_INCLUDE "") + set(JF_ZMQ_EXTRA "") +endif() + +# --------------------------------------------------------------------------- +# includes / flags shared by every source of this program +# --------------------------------------------------------------------------- +set(JF_INCLUDES + "${CMAKE_CURRENT_SOURCE_DIR}" + "${CAL}/dataStructures" + "${CAL}/interpolations" + "${CAL}/tiffio/include" + "${PKG}/slsSupportLib/include" + "${PKG}/slsReceiverSoftware/include" + "${PKG}/libs/rapidjson" + "${JF_ZMQ_INCLUDE}" + ${JF_QT_INCLUDES} + ${HDF5_INCLUDE_DIRS} + ${ROOT_INCLUDE_DIRS} +) + +# The strip-xel decoder header is included by the -DJFSTRX build only; listing it +# as a dependency makes both build systems rebuild when it changes. +set(JF_STRIXEL_HDR "${CAL}/dataStructures/jungfrauLGADStrixelsData_new.h") + +set(JF_SOURCES + online_jf_core.cpp + online_jf_root.cpp + online_jf_h5.cpp +) +set(JF_GUI_SOURCE online_jf_gui.cpp) + +# Everything except the definition of JFSTRX: the two object libraries below +# differ only in that definition (and therefore in their compiled geometry). +set(JF_COMMON_WARN -Wall -O3 -pthread) + +foreach(variant IN ITEMS "" "JFSTRX") + if(variant STREQUAL "") + set(objlib jf_common) + set(app jf_online_display_gui) + else() + set(objlib jf_common_jfstrx) + set(app jf_online_display_gui_JFSTRX) + endif() + + add_library(${objlib} OBJECT ${JF_SOURCES}) + target_include_directories(${objlib} PRIVATE ${JF_INCLUDES}) + target_compile_options(${objlib} PRIVATE ${JF_COMMON_WARN}) + if(NOT variant STREQUAL "") + target_compile_definitions(${objlib} PRIVATE ${variant}) + endif() + set_source_files_properties(${JF_SOURCES} PROPERTIES + OBJECT_DEPENDS "${JF_STRIXEL_HDR}") + + add_executable(${app} ${JF_GUI_SOURCE} $) + target_include_directories(${app} PRIVATE ${JF_INCLUDES}) + target_compile_options(${app} PRIVATE ${JF_COMMON_WARN}) + if(NOT variant STREQUAL "") + target_compile_definitions(${app} PRIVATE ${variant}) + endif() + target_link_libraries(${app} PRIVATE + Qt5::Widgets Qt5::Gui Qt5::Core + ${ROOT_LIBRARIES} + ${JF_ZMQ_LIBRARY} + ${JF_ZMQ_EXTRA} + ${HDF5_LIBRARIES} + pthread rt m + ) +endforeach() + +# --------------------------------------------------------------------------- +# tests / helpers +# --------------------------------------------------------------------------- +if(JF_BUILD_TESTS) + enable_testing() + add_test(NAME gate_contig COMMAND gate_contig_test) + # gate decision logic in isolation (no Qt, no ROOT usage at run time) + add_executable(gate_contig_test + test/gate_contig_test.cpp + $) + target_include_directories(gate_contig_test PRIVATE ${JF_INCLUDES}) + target_compile_options(gate_contig_test PRIVATE ${JF_COMMON_WARN}) + target_link_libraries(gate_contig_test PRIVATE + Qt5::Widgets Qt5::Gui Qt5::Core + ${ROOT_LIBRARIES} + ${JF_ZMQ_LIBRARY} + ${JF_ZMQ_EXTRA} + ${HDF5_LIBRARIES} + pthread rt m + ) + + # stand-in slsReceiver: single- or dual-interface ZMQ publisher + add_executable(jf_feeder test/jf_feeder.cpp) + target_include_directories(jf_feeder PRIVATE + "${PKG}/libs/rapidjson" "${JF_ZMQ_INCLUDE}") + target_compile_options(jf_feeder PRIVATE ${JF_COMMON_WARN}) + target_link_libraries(jf_feeder PRIVATE ${JF_ZMQ_LIBRARY} ${JF_ZMQ_EXTRA}) +endif() diff --git a/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/Makefile b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/Makefile new file mode 100644 index 000000000..2c575f14b --- /dev/null +++ b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/Makefile @@ -0,0 +1,106 @@ +# =========================================================================== +# jf_online_display_gui -- single multithreaded program merging +# jf_event_finder_cli / jf_event_finder_gui (ZMQ gate) +# onlinedisp_zmq (Qt5 + ROOT display) +# +# Usage: +# make -> ./jf_online_display_gui (normal) +# ./jf_online_display_gui_JFSTRX (strip-xel) +# make normal | make jfstrx single variant (aliases) +# ./jf_online_display_gui [rx_port] [gate_threshold] [min_pixels] [pede_file] +# [--pub PORT] [--contig] [--no-gate] [--pede-frames N] +# +# Deps: system ROOT (root-config), Qt5 (pkg-config), the prebuilt static +# libzmq from the package build tree, and rapidjson headers. +# =========================================================================== +QT_CXXFLAGS = $(shell pkg-config --cflags Qt5Core Qt5Gui Qt5Widgets 2>/dev/null) +QT_LIBS = $(shell pkg-config --libs Qt5Core Qt5Gui Qt5Widgets 2>/dev/null) +ROOT_CFLAGS = $(shell root-config --cflags 2>/dev/null) +ROOT_LIBS = $(shell root-config --libs 2>/dev/null) + +# paths relative to this directory (jf_online_display_gui/) +PKG = ../../.. +CAL = ../.. + +ZMQ_STATIC = $(PKG)/build/_deps/libzmq-build/lib/libzmq.a + +# HDF5 C++ headers are NOT in the default include path on every distro (Debian/ +# Ubuntu put them in /usr/include/hdf5/serial). Discover the include/library +# directories with pkg-config (the .pc may be "hdf5" or "hdf5-serial"), then the +# h5c++ wrapper, and let the user override both explicitly: +# make HDF5_INC=-I/usr/include/hdf5/serial \ +# HDF5_LIB=-L/usr/lib/x86_64-linux-gnu/hdf5/serial +HDF5_PC := $(shell pkg-config --exists hdf5 && echo hdf5 || (pkg-config --exists hdf5-serial && echo hdf5-serial)) +HDF5_INC ?= $(shell pkg-config --cflags $(HDF5_PC) 2>/dev/null) \ + $(shell h5c++ -show 2>/dev/null | tr ' ' '\n' | grep '^-I' | tr '\n' ' ') +HDF5_LIB ?= $(shell pkg-config --libs $(HDF5_PC) 2>/dev/null) \ + $(shell h5c++ -show 2>/dev/null | tr ' ' '\n' | grep '^-L' | tr '\n' ' ') +HDF5_LIBS = $(HDF5_LIB) -lhdf5_cpp -lhdf5 + +INCDIR = -I. \ + $(HDF5_INC) \ + -I$(CAL)/dataStructures \ + -I$(CAL)/interpolations \ + -I$(CAL)/tiffio/include \ + -I$(PKG)/slsSupportLib/include \ + -I$(PKG)/slsReceiverSoftware/include \ + -I$(PKG)/libs/rapidjson \ + -I$(PKG)/build/_deps/libzmq-src/include + +CXX ?= g++ +CXXFLAGS ?= -std=c++17 -O3 -Wall -pthread + +LIBS = $(QT_LIBS) $(ROOT_LIBS) $(ZMQ_STATIC) $(HDF5_LIBS) -lsodium -lpthread -lrt -lm + +OBJS = online_jf_core.o online_jf_root.o online_jf_gui.o online_jf_h5.o +# The strip-xel variant compiles the same sources with -DJFSTRX into its own +# objects: the two builds have different geometry and struct layouts, so they +# must never share object files (mixing them is a hard crash). +JFSTRX_OBJS = online_jf_core.JFSTRX.o online_jf_root.JFSTRX.o \ + online_jf_gui.JFSTRX.o online_jf_h5.JFSTRX.o + +BIN = jf_online_display_gui +BIN_JFSTRX = jf_online_display_gui_JFSTRX + +ALL_CXXFLAGS = $(CXXFLAGS) $(ROOT_CFLAGS) $(QT_CXXFLAGS) $(INCDIR) + +# `make` builds BOTH executables: the normal one and the LGAD strip-xel one +# (the decoder is header-only, no extra library). +all: $(BIN) $(BIN_JFSTRX) + +# the strip-xel decoder header is header-only and lives outside this directory: +# touch it and the variant that uses it must be rebuilt +STRIXEL_HDR = $(CAL)/dataStructures/jungfrauLGADStrixelsData_new.h + +%.JFSTRX.o: %.cpp online_jf.h $(STRIXEL_HDR) + $(CXX) $(ALL_CXXFLAGS) -DJFSTRX -c $< -o $@ + +%.o: %.cpp online_jf.h + $(CXX) $(ALL_CXXFLAGS) -c $< -o $@ + +gate_contig_test: test/gate_contig_test.cpp online_jf_core.o online_jf_root.o online_jf_h5.o + $(CXX) $(ALL_CXXFLAGS) -o $@ $^ $(LIBS) + +$(BIN): $(OBJS) + $(CXX) -o $@ $(OBJS) $(LIBS) + +$(BIN_JFSTRX): $(JFSTRX_OBJS) + $(CXX) -o $@ $(JFSTRX_OBJS) $(LIBS) + +# kept so the old habits still work +normal: $(BIN) +jfstrx: $(BIN_JFSTRX) + +# Synthetic slsReceiver stand-in used to exercise the program without +# hardware (see test/jf_feeder.cpp). +feeder: test/jf_feeder + +test/jf_feeder: test/jf_feeder.cpp + $(CXX) -std=c++17 -O2 -pthread $(INCDIR) -o $@ $< $(ZMQ_STATIC) -lsodium -lpthread -lrt -lm + +clean: + rm -f $(OBJS) $(JFSTRX_OBJS) $(BIN) $(BIN_JFSTRX) test/jf_feeder + +FORCE: + +.PHONY: all clean jfstrx normal feeder diff --git a/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/NOTES.md b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/NOTES.md new file mode 100644 index 000000000..483b075a1 --- /dev/null +++ b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/NOTES.md @@ -0,0 +1,950 @@ +# Notes + +Working notes for `jf_online_display_gui`. The README is the reference for how the +program behaves; this file records provenance, verification and things that are +easy to trip over. + +## 2026-09-23 — handoff from another session (verified here) + +An all-night run showed the **display followed the pedestal drift but the saved +HDF5 did not**. Three changes were made by that session and are on disk; I +re-verified them in this workspace on 2026-09-23. + +### What changed + +| file | change | +|---|---| +| `online_jf_h5.cpp` | `RoiSaver::offer()` corrects each frame with the **live** pedestal (`PedestalStore::instance().get()`), not the snapshot taken by the GUI at Start Save. Periodic checkpoint: shrink `/data` + the 14 parameter datasets to exactly `framesWritten` (undoing the `kExtendBlock = 128` over-allocation), `H5Fflush(SCOPE_GLOBAL)`, rewrite the JSON. JSON writer factored into `writeJsonSidecar(bool complete)` writing `.json.part` → `fsync` → `rename` → directory `fsync` (`syncDirOf`). File created as **v110 (SWMR)**: `H5Pset_libver_bounds(LATEST)` + `H5Fcreate`, all objects created up front, then `H5Fstart_swmr_write()` before the first frame. | +| `online_jf.h` | `StartInfo::checkpointMs = 30000` (0 disables) | +| `README.md` | pedestal tracking / saving / file format / JSON / crash-and-mid-run sections, test description, files table | +| `test/pede_live_test.cpp` | standalone check: live pedestal + checkpoint + SWMR live read | + +Consequences of SWMR worth remembering: objects cannot be created after it +starts, so `/pedestal tracking` and `/pedestal tracking frame` are now created +(empty) for **every** save, and the baseline pedestal image is written just after +the SWMR switch. (The 10-minute cadence of that series, and its ROI-shaped +images, are unchanged.) + +### Verification done here + +* `make` — clean, binary current with the sources. +* `test/pede_live_test` (built with the README's line, plus `$(root-config --cflags)` + which that line omits) — **PASS**: + +``` +checkpoint1: periodic json (complete:false)=true live swmr read=true h5 extent=1 px=900.0 +checkpoint2: live swmr read=true h5 extent=2 slice1 px=850.0 +final: complete=true no .part left=true +PASS: live pedestal + periodic clean checkpoint +``` + +The two different pixel values are the point: the first frame was corrected with +pedestal A, the second with pedestal B installed mid-run, so the file follows the +tracked pedestal. Generated artefacts were deleted afterwards. + +### Operational caveats + +* **v110 needs HDF5 ≥ 1.10.** Older analysis installs cannot open these files at + all — this is the one to check before an overnight run. +* **Reading mid-run needs the SWMR read flag**: `h5py.File(path, "r", + libver="latest", swmr=True)` and `dset.refresh()` to see new frames. + `h5dump`/`h5ls` on the live file will not work. A closed file reads normally. +* `Pedestal corrected` is still **off by default** (raw data). Pedestal tracking + only reaches `/data` when it is ticked; in raw mode the only pedestal in the + file is slice 0. +* `/pedestal data` is the pedestal at save start; `/pedestal tracking` + (+ `/pedestal tracking frame`) is the history of what was actually used, one + image per checkpoint interval. +* `checkpointMs` defaults to 30 s and is **not exposed** in the GUI or on the + command line yet. + +### Open / optional + +* `--no-swmr` (or a `StartInfo` fallback) if a downstream reader turns out to be + on HDF5 < 1.10. +* Expose the checkpoint interval (`--checkpoint MS` and/or a GUI field). +* If an external reader does not see new frames after a checkpoint, add explicit + per-dataset `H5Dflush`; `H5Fflush(SCOPE_GLOBAL)` was enough in the test. +* Unrelated: the older `onlinedisp_zmq.cpp` extracts the ROI from + `zHeader.detSpec1`, which is a hack (that field is the bunch id) — the stream + header already carries `zHeader.rx_roi` (`std::array`, filled from the + receiver's per-port ROI). That file was not touched. + +## Workspace health + +* `../_AGENT.txt` was rewritten (2026-09-23, on request): it now points at + `jf_online_display_gui/` as the active work, with the old `onlinedisp_zmq` + + `jf_event_finder_*` set kept in a clearly marked "legacy / superseded" + section. The design rules that both generations share (ROOT-thread rule, + photon map, display cadence, gate semantics, SWMR files) are listed there too. +* `test/_run/` was removed (2026-09-23, on request). For the record, what was + in it was not worth keeping: the `..._084120_...` run had only its sidecar left + (the HDF5 was already gone) and `..._090709_...` had `frames.saved = 0` — an + empty save written before the handoff, with no `complete` field. `test/` now + holds only `jf_feeder[.cpp]` and `pede_live_test.cpp`. +* `jf_online_display_gui/` and the h5/write path are untracked in git; nothing here is + committed. +* `.gitignore` in this directory excludes `*.o`, the `jf_online_display_gui` and + `test/jf_feeder` binaries, and runtime data (`current_pede.dat`, + `roi_list.dat`, `save_dest_list.dat`, `jfroi_*.h5/json`). Note it does *not* + cover `test/pede_live_test` (the test binary), so that one should be deleted + after use. + +## 2026-09-30 — JFSTRX display + startup defaults + +* `-DJFSTRX`: the **ADC and gain windows are now unmapped** (raw 1024x512), so + gate-ROI selection and the hover read-out stay in raw coordinates. The mapped + view is a separate, display-only window, **"ADC Strixel Panels (JFSTRX)"**, + with the six groups arranged like the ROOT canvas (left chip1 g3/g2/g1, right + chip6 g1/g2/g3). +* The six panels use the strixel **pixel aspect** `X = 75*(75/Y)`, `Y = + 25/15/18.75 um` -> 9:1, 25:1, 16:1, via `FastImageView::setPixelAspectX` + (non-uniform x/y scaling; ADC and gain stay square). +* The receiver ROI is parsed from the JSON `rx_roi` array (the old `detSpec1` + hack is gone) and drawn as the **red** rectangle on the raw ADC image. +* Raw geometry is now `kRawWidth`/`kRawHeight` (1024x512); `nx`/`ny` under + JFSTRX are the strixel *display* geometry only. Fixed the assembled-frame + height, the multi-interface slab split, the gate-ROI defaults and the HDF5 + pedestal extraction, which all wrongly used the mapped `nx`/`ny`. +* **Startup defaults changed**: the signal gate is **off** (`--gate` enables it) + and pedestal subtraction is **off** (`--use-pede` enables it; the "No pede" + button starts checked). README examples now pass `--gate` where gating is + expected. +* `test/panel_map_test.cpp` (JFSTRX objects) checks the six-panel fill geometry + against the decoder: all six panels fully covered, only the decoder's 183 + extra right-edge columns dropped (same as the ROOT histograms). +* The strixel ROI-outline experiment (`buildReceiverRoiOutline`) was removed - + ROI on the mapped panels was judged too complex. +* At ~14:40 the on-disk `jf_online_display_gui` was found truncated to 0 bytes + (concurrent build in this directory); `make jfstrx` rebuilt it (14:41). + +## 2026-09-30 (later) — three reported bugs + +1. **Zoom-out stopped early** (`FastImageView::wheelEvent`): it called + `matchViewportToWindow()` on every wheel step, so the image dimension that + reached its limit first froze the other and the whole frame could never be + shown. Outward zoom now only `clampViewport()`s; the window-shape matching + still runs while zooming in and on resize. +2. **ROI PH (A7) empty**: the fill condition used `gate_.roiActive()`, which is + only true while "ROI only" is on, so a merely *selected* ROI left A7 empty. + The ROI snapshot/spectra now use `gate_.roiSet()`; "ROI only" still controls + the gate restriction (unrelated to showing the ROI). +3. **"Reset His" took 30-50 s**: the ROOT thread executed the reset and blocked + on `gHistoMtx`, which the analysis thread holds around every frame - under + the heavy JFSTRX fill it can starve. The reset is now performed by the + analysis thread at the top of its next frame (lock already held), with a + non-blocking `try_lock` fallback on the ROOT thread for the idle case. The + ROOT thread also now wakes immediately (`gCmdCv`) for commands and forced + redraws instead of waiting for the `ROOT update (ms)` tick, so a large + update interval no longer delays a button. + +**Build footgun:** the Makefile does not track flags. After `make jfstrx`, +a plain `make` only rebuilds changed files and then links a mix of JFSTRX and +non-JFSTRX objects (I hit a link error that way). Always `make clean` before +switching between `make` and `make jfstrx`. + +## HDF5 C++ headers (build dependency) + +`#include "H5Cpp.h"` needs the HDF5 **C++ dev** package, not just the runtime: + +| distro | package | where the header lands | +|---|---|---| +| RHEL / Rocky / Alma / Fedora | `hdf5-devel` | `/usr/include/H5Cpp.h` | +| Debian / Ubuntu | `libhdf5-dev` | `/usr/include/hdf5/serial/H5Cpp.h` | +| openSUSE / SLES | `hdf5-devel` | default include dir | +| Arch | `hdf5` | `/usr/include` | +| conda-forge | `hdf5` | `$CONDA_PREFIX/include` | +| macOS (brew) | `hdf5` | `$(brew --prefix hdf5)/include` | + +The Makefile now discovers the include/library dirs via `pkg-config` (`hdf5` or +`hdf5-serial`) and `h5c++ -show`, and both can be overridden, e.g. on Ubuntu: + +``` +make HDF5_INC=-I/usr/include/hdf5/serial \ + HDF5_LIB=-L/usr/lib/x86_64-linux-gnu/hdf5/serial +``` + +## 2026-10-06 — external review (`../../../REVIEW_CLAUDE.md`): triage and what was applied + +A 39-item review by Claude covers this program plus the Qt6 port, the legacy +tools, `test_bench/` and repo hygiene. **Part of it was written against a +different checkout**: items 2, 3, 4 (mostly) and 9 are already fixed in *this* +tree, and the review's own item 37 mentions `build-qt6/` cache paths under +`~/swtests/slsDetectorPackage/...`, which does not exist here. Everything below +was checked against the code in this workspace before touching it. + +### Applied and verified here + +| item | what was wrong | fix | +|---|---|---| +| 4 (rest) | a header with **no data part** would make the data `recv` swallow the next message's header as pixels | require `ZMQ_RCVMORE` after a valid header, else count an error and drop the header (the header-length clamp and the drain-on-reject paths were already present) | +| 8 | the save recorded the **startup** gate settings, so `gate` in the HDF5 attribute and JSON sidecar was wrong after any live edit | read `enabled/threshold/minPixels/contiguous` from `GateEngine` (new getters) instead of `g_cfg`; verified end to end - a save started from the command line records the live values (`threshold 137`, `min_pixels 9`) | +| 11 (part) | the acq thread **busy-spun at 100 % CPU** waiting for a lagging partner: a held slab kept its socket polled and level-triggered `zmq_poll` fired forever | clear `events` for a socket whose slab is held, so it is not polled until it is released | +| 18 | ten shortcuts never fired: `QKeySequence("s") == QKeySequence("S")`, so s/S, b/B, r/R, o/O, p/P were ambiguous and Qt activated neither | upper-case actions are now `Shift+S/B/R/O/P` (verified: `Shift+S` opens Set Ranges, 352 → 366 windows) | +| 19 | the run examples still passed the removed `` argument, so `2000` was parsed as the host | README examples, the header comment and the Makefile comment corrected (the two remaining `` mentions are the *legacy* chain and a historical note, both correct) | +| 22 (part) | `--pede-tracking` left the button unchecked, and `--gate-roi` left `ROI only` unchecked so the first press toggled the wrong way | the tracking button is initialised from `gPedeTracking`, and a restricting `--gate-roi` now checks `ROI only` (through the `m_applyingRoi` guard, so nothing re-captures or warns) | + +### Found while fixing item 22 — a bug of my own + +The tracking button's enable rule lived in the 500 ms `updateFlow()` poll, which +also *unchecked* the button when the gate was off. During start-up that cleared +a `--pede-tracking` button (`ptrack=off`, reproduced headless). The force-off is +now event-driven (in the gate checkbox's own handler); the poll only enables. +After the change: `--pede-tracking` gives `ptrack=on`. + +`test/jf_feeder` had also gone missing from the tree (the Makefile has no target +for the test programs — review item 34); rebuilt by hand for the verification. + +### Already fixed in this tree (nothing to do) + +| item | state | +|---|---| +| 2 (HDF5 never closed) | the file is built with the C++ constructor `H5File(h5Path, …)` so it owns the only reference; the code comments say so explicitly | +| 3 (budget/stop not enforced) | `offer()` returns early on `stopRequested`, and the writer stops at `saved >= maxFrames` | +| 4 (clamps) | `hn > hdr.size()` clamp, `truncated` data check and the drain-on-reject paths are all present | +| 9 (cumulative drop rate) | uses the 1 Hz window (`pedeTrackSignalDropRate(passCur, dropCur, …)`) — fixed earlier the same day | + +### Checked and found *correct* (not defects) + +While verifying item 8 I chased two values in a saved file that looked wrong and +are not: the file recorded `enabled: false` and `no_pede: true` for a run whose +command line had neither `--gate` nor `--use-pede`. Both are the documented +defaults - `online_jf.h:189` (`gateEnabled = false`) with `--gate` to enable, and +`gNoPede{true}` with `--use-pede` to clear. The review's item 22 is the flip +side of this (the widgets were not synced from the config); the config itself is +intentional. Since it is easy to lose an hour to this, both switches are now +spelled out in the README's options section, and a regression run with +`--gate --use-pede` behaves as expected (with a 100 ADU pedestal and a 250 ADU +threshold on 3000/200 ADU frames: `pass=0 drop=148`, i.e. the ROI half is +reduced to 100 and correctly rejected). + +### Deliberately deferred, with reasons + +* **13 — chip/ROI spectra saturate (top physics item).** `TH1I` bin contents + saturate at `INT_MAX` and the `uint32_t` accumulators wrap: ~55 k pixels per + chip per frame land in 1–2 noise bins, so at 500–1300 fps the spectra are + wrong within an hour, and an all-night run at 25 fps is close to the limit too. + The fix is mechanical but wide (37 `TH1I` sites across four files plus the + accumulator and sync signatures) and touches the core data path, so it gets + its own careful pass with a test rather than a rushed edit at the end of a + long session. Plan: accumulators → `uint64_t`, histograms/clones → `TH1D`, + then check that entries, means and the ADU/keV ratio (40.0000) are unchanged. +* **10** (the tracker can overwrite a just-loaded pedestal — re-prime from the + store under the lock, skip a stale install), **12** (per-port `rx_roi` with the + `rowBase` offset), **20** (closing the window should quit — the parentless ADC + /Gain windows keep the process alive), **21** (Delete in the ROI/save-destination + dialogs uses stale indices), **23–30** (races on `g_pheneThreshold`/the ROI, + `renderImage`'s 524 k `setPixel` calls and the 500 ms `roi_list.dat` re-read, + NaN pedestals, rapidjson asserts, the finish/start save race, frame 0, and the + small GUI defects). +* **11 (the rest)** — partial-frame assembly is a feature, not a fix; the README + now documents the real behaviour and calls the promise an open item. +* **Everything outside `jf_online_display_gui`** — the Qt6 port (1, 16, 17, 35, 36), the + legacy tools (5, 6, 7, 14, 15, 31 — the project's own `_AGENT.txt` marks them + superseded), `test_bench` (32), repo hygiene (33, 37, 38 — `git checkout --` + for the symlinks needs repository write access, which is still deferred) and + maintainability (39). These need decisions of their own; none of them were + changed here. + +## 2026-10-14 — virtual photon counting: display side done, save side specified + +Requested: take the photon map out of ROOT, give it its own Qt window filled the +same way but **not cumulative by default** and plotted every N images (default +2000), put those controls plus the photon threshold in a "Virtual photon +counting" panel left of the Display, and add a save mode that writes the +non-cumulative maps every M images (M >= 200) with the first frame number and +timestamp, M and the thresholds recorded. + +### Done (build green, window present, no crashes) + +* Core: `gPmapEvery` (default 2000) and `gPmapCumulative` (default **false**) + atomics; the photon map is published into the image mailbox every + `gPmapEvery` **analysed frames** (independent of the ADC image decimation), + and when not cumulative the accumulator is zeroed right after the publish, so + each image holds the photons counted since the previous one. The counting + itself is unchanged. +* GUI: a **"Virtual photon counting"** panel (left column, under the gates - + the Display panel is in the right column at that height) with the + `cumulative` checkbox, `plot every N images` (1..1000000, default 2000) and + the **photon threshold (ADU)** spin moved out of the Display group. +* GUI: a new top-level window **"Photon Map (virtual photon counting)"** + (1104x552, same colour palette as the ADC image, colour range 0..8 photons + per pixel), refreshed from the mailbox in `processImages()`. +* Fixed while doing it: the panel was built before `photonThrSpin_` existed, so + the grid received a null widget and the spin floated at (0,0) - the same + add-before-create mistake as the Quit button earlier. The panel is now + created after the spin and inserted with `leftCol->insertWidget(2, ...)`. + +### Stage 3, part A — done and verified + +* **The ROOT photon map is gone**: no `his3000` booking, no `disp3000` clone and + no `A5` canvas. All the remaining references are behind `if (his3000)` / + `if (disp3000)` / `if (A5)` guards, so they are simply inert; the two + unguarded dereferences (the clone and the z-range) were the only ones that + needed deleting. Verified: no `Plotting Canvas Photon Map` window appears. +* The **P.Map** range pair now reads and writes the Qt photon window's colour + range (`liveMapRange` -> `m_photonView->colorRange`, and the panel's opening + range), and the ADC zoom also sets the photon window's viewport. +* **Save with no gate ROI works**: the early refusal in `toggleRoiSave` is gone; + `roiSnapshot()` already reports the full frame when no ROI is set. Verified: + with no `--gate-roi`, `/data` came out `(151, 512, 1024)` - the pedestal slice + plus 150 full frames. + +### Stage 3, part B — the photon-map save mode (next) + +Design, so this can be written mechanically: + +1. Save panel: a radio pair (`save frames` / `virtual photon counting`) plus an + `M` spinbox, minimum **200**, default 2000; the spinbox is only meaningful in + the photon mode. +2. Core: `gPmapSaveEvery` (int, default 2000) and `gSaveMode` (0 = frames, + 1 = photon maps) atomics; a **second accumulator** `g_pmapSaveAccum` (NCH + floats) filled in the same worker pass (one extra add per counted photon + pixel, gated by a `FrameJob::pmapSave` flag so there is no atomic read per + pixel); every `gPmapSaveEvery` analysed frames it is copied into a single-slot + mailbox and zeroed - never cumulative. +3. Window provenance: the acq thread owns the saver, so let the acq loop keep the + window bookkeeping while in this mode: on the first gate-passed frame after a + publish record `firstFrame = hdr.frameNumber` and `firstStamp = + hdr.timestamp`, count frames, and when the analysis publishes a map, tag it + with those two values. The map's own frame count goes in too. +4. Writer: a new dataset `/virtual photon counting` (float32, 512x1024, one image + per publish, extendable) plus `/virtual photon counting frame` (uint64) and + `/virtual photon counting timestamp`; JSON gains `save_mode`, + `photon_map_every` (M), `photon_threshold` (ADU) and the window count. + `/data` stays frames-only and is not created in this mode. +5. `--pmap-every N` on the command line would make this testable without the GUI + (the display cadence is otherwise 2000 frames = 80 s at 25 fps). + +### Stage 3, part B — plumbing in place, inert + +Done in the tree and building: + +* `gSaveMode` (0 = frames, 1 = virtual photon counting) and `gPmapSaveEvery` + (default 2000) atomics, plus `pmapSaveReset()`. +* A **second accumulator** `g_pmapSaveAccum` filled in the same worker pass + (`FrameJob::pmapSave`, so no atomic read per pixel), published into a + single-slot `PmapSlot` every `gPmapSaveEvery` analysed frames and zeroed + immediately - never cumulative. +* `Pipeline::offerToSaver()` replaces the two direct `saver_.offer()` calls: in + the frame mode it is the old path, in the photon mode it keeps the window + bookkeeping (first frame number, first timestamp, frame count) in the acq + thread and tags each published map with it. +* `RoiSaver::offerPhotonMap()` declared; its body currently only counts the map + in `pmapPending` and writes nothing - **deliberately inert**: `gSaveMode` is + still 0 everywhere, so no path can select the photon mode yet and the existing + behaviour is unchanged. Nothing can silently produce a frames-only file while + claiming the photon mode. + +Still to write (the design is above): + +1. The writer body: `/virtual photon counting` (float32, 512x1024, extendable, + **created up front** because SWMR forbids creating objects after the start) + plus `/virtual photon counting frame` and `/virtual photon counting timestamp` + (uint64), drained from a `pmapQueue` in the writer loop, and the JSON fields + (`save_mode`, `photon_map_every`, `photon_threshold`, window count). +2. The Save panel controls: the radio pair (`save frames` / + `virtual photon counting`) and the `M` spinbox (minimum 200), filling + `StartInfo::photonMode/photonEvery/photonThreshold` (the fields are already + there) and calling `pmapSaveReset()` when a save starts. +3. `--pmap-every N` and `--pmap-save-every N` (or one `--save-photon` switch) so + the mode can be exercised headlessly; with the display default of 2000 frames + a test needs 80 s at 25 fps otherwise. + +### Stage 3, part B — finished and verified (this session) + +Everything listed as "still to write" above is in the tree, plus the GUI +controls and the headless switches: + +* Writer: `/virtual photon counting` (float32, {nmaps, h, w}, chunked, created + before SWMR starts) + `/virtual photon counting frame` + `/virtual photon + counting timestamp` (U64, one value per map), drained from `Impl::pmapQueue` + in the writer loop; `RoiSaver::offerPhotonMap()` is live and inert unless + `info.photonMode` and a save is running. JSON: `save_mode`, + `photon_map_every`, `photon_threshold`, and a `photon_maps` block with + `saved`, `every`, `threshold_adu`, `cumulative: false`, `pedestal_active`, + `dataset`. In photon mode `frames.saved` reports the **maps**. +* Budget: in photon mode "Number of frames to be saved" budgets the **maps** + (the GUI relabels the row to "Number of photon maps to be saved"), and the + run stops by itself with `stop_reason = "photon-map budget reached"`, leaving + a closed `complete: true` pair. Frame mode is unchanged + (`frame budget reached`). +* Save panel: radio pair + `photon map every N frames` spinbox (minimum 200, + greyed while "save frames" is selected). The radio also re-labels the budget + row. The photon-map mode is allowed with **no gate ROI** (full frame). +* Virtual photon counting panel (left column, under the gates): `cumulative` + (default **off**), `plot every N images` (default 2000), `photon threshold + (ADU)` (moved out of the Display panel). The map lives in its own Qt window + `Photon Map (virtual photon counting)`; it is **not** a ROOT histogram any + more (that was the old `his3000`, removed). +* CLI: `--pmap-every N`, `--pmap-save-every N` (min 200), `--save-photon`, + `--save-budget N`, `--start-save` — the same actions as the GUI controls, so + a save can run headless. + +**Bug found and fixed while verifying (important).** Step 5a of the analysis +loop (the save-map publish) took `gHistoMtx` although the enclosing frame scope +already holds it (`histoLock`, top of the frame). `std::mutex` is not +recursive, so the **analysis thread deadlocked on the first frame** whenever +`gSaveMode == 1` — with `--save-photon`, and even with no save running. The +symptom was a program that kept counting frames (`in=` growing, because the acq +thread does not depend on the analysis thread) while nothing downstream ever +appeared: no photon map, no ROOT update, `save=0/0`. Two `gdb` snapshots +caught it (`analysisLoop` parked in `pthread_mutex_lock` at a call site that +only exists in the photon path). Fix: no second lock (the frame scope holds it) +and the whole save-map path is now gated on `saver_.running()` as well, so +`gSaveMode == 1` without a running save costs nothing and cannot block. + +**Verified end to end** (Xvfb + mwm, `test/jf_feeder`, zero-filled pedestal — +the real `current_pede.dat` is ~2000 ADU, so the synthetic 100/300 ADU frames +would count nothing): + +| check | result | +|---|---| +| `--save-photon --pmap-save-every 200 --start-save`, 40 s | maps at frames 0/201/401/601/801, `[pmap] map N written (200 frames, first frame F)` | +| `/virtual photon counting` | (5, 512, 1024) float32; every map 40 non-zero pixels, sum ~5320 (40 px x 133 counts = 2/3 of 200 frames), identical maps = non-cumulative | +| provenance | `/virtual photon counting frame` `[0 201 401 601 801]` | +| `--save-budget 2` | exactly 2 maps, self-stop `photon-map budget reached`, `complete: true` | +| frame mode `--save-budget 100` | `/data` (101, 512, 1024), 100 frames, `frame budget reached`, no photon datasets | +| GUI path | clicking `virtual photon counting` re-labels the row to *Number of photon maps to be saved*, enables the M spinbox and sets `Status: saving virtual photon counting maps`; `Start/Stop save` then writes maps (first map at the frame the save started, e.g. 431) | +| photon window | `Photon Map (virtual photon counting)` renders the 40-pixel cluster (green, ~133 counts on the 0..256 scale) | +| no pedestal | `--save-photon` with `--no-pede` (or no pedestal file) is **refused**: `[save] refusing virtual photon counting: "No pede" is on, ...`, no HDF5 file is created, and the scripted `--start-save` path is not blocked by a dialog; frame mode with no pedestal still saves | + +The photon-map window draws **zero-count pixels white** (`FastImageView:: +setZeroWhite`, opt-in and used only there), reproducing the ROOT `colz` look +where empty cells show the canvas background; the palette and the colour bar are +untouched, and single photons are still coloured (the special case is `v <= 0`, +not "below the range"). Verified on screen: white map, cluster still green. + +Housekeeping: `test/_scratch/` (XTest driver, screenshots, run logs, HDF5 +outputs, the synthetic pedestal) was removed after the runs; the tree keeps only +`test/jf_feeder[.cpp]`, `test/pede_live_test.cpp`, `test/panel_map_test.cpp`. + +Two things left open for the next session, both decisions rather than bugs: + +1. ~~**Photon counting needs a pedestal.**~~ **Decided by the operator: no + photon-map saving without a pedestal.** Counting still requires `g0` + (unchanged), and a virtual-photon-counting save now *refuses to start* when + no pedestal is active (`No pede`, or nothing loaded): the button drops back, + the status line and stdout say why, no file is created, and the modal warning + is skipped under `--start-save` so scripted runs never wait on a dialog. + Frame saving is unaffected. +2. The map's `first frame number`/`timestamp` come from the acq thread, i.e. + from the first **gate-passed** frame of the window (none is available if the + gate drops the whole window, in which case the previous values persist). The + analysed-frame count in `PmapItem::frames` is exact. + +### ROI in photon mode, greying convention, control names (follow-up) + +* **The gate ROI applies to the saved photon maps too** (operator's choice): + a map is the ROI window of the full-frame counting map, cropped in the same + data coordinates as `/data`. This fixed a real bug: `writePhotonMap` used to + hand `h*w` floats straight to HDF5, i.e. the first `h*w` pixels of the + 1024x512 map, which is the *wrong region* whenever a ROI is set. Verified + against the full-frame map: `/virtual photon counting` for a `0 0 100 50` ROI + is bit-identical to `full[0:50, 0:100]`, with the 16 non-zero pixels exactly + where the feeder's wrapped cluster is (row 1, x 0..15); a frame save with the + same ROI gives `/data` `{nframes, 50, 100}`, so both modes crop alike. +* **Greying is a look, not an enablement** (`greyStage(QGroupBox*, bool, + std::initializer_list keepActive)`): the box, its title and its + other contents dim, while the widget that *switches the stage back on* keeps + normal text - otherwise a bypassed stage looks stuck. Kept readable: + `Pede/Raw toggle` (Pedestal), `Gate enabled` (signal gate), `Pedestal + tracking`, `gain correction on/off` (Gain), and the storage-cell combo. The + `keV PH` checkbox stays disabled+greyed while the gain is off (earlier + request: it keeps its tick for next time). +* **Virtual photon counting box greys without a pedestal** (`greyStage( + pmapGroup_, !pede)`), matching the fact that the counting needs `g0` and the + save refuses to start: one rule, the panel and the file agree. +* Renames: `No pede (raw ADC)` -> **`Pede/Raw toggle`** (state still spelled out + by the label next to it), `Apply gain (g)` -> **`gain correction on/off (g)`**, + plus a read-only **`gain corrected`** indicator beside it (no mouse events, no + focus; it ticks with the flag in `updateFlow()` and dims with the panel). + +* Pedestal tracking panel follow-up: the button is `pedestal tracking on/off` + and the box now greys on **selectability**, not on activity - `greyStage( + pedeTrackGroup_, !gateOn)`, matching the button's own `setEnabled(gateOn)`, + while the value label under it (`off` / `on: N samples, ...`) reports the + activation. Verified in three states: gate off -> box and button greyed; + gate on + tracking off -> box black and label `off`; after clicking the + toggle -> button pressed and label `on: 0 samples, delta 0.0 ADU, 0 updates` + (0 samples is correct with the feeder, which only drops a third of the frames + while the tracker needs >90 %). + +### Photon threshold in keV while the gain correction is on + +Requested: leave the photon-counting threshold in ADU with the gain off, switch +it to keV (default **4 keV**) with the gain on, and remember both values. + +* Core: `g_pheneThresholdKev` (default `PHOTON_THRESHOLD_KEV_DEFAULT` 4.0) next + to `g_pheneThreshold` (150 ADU). The analysis picks by `gain_flag` and hands + the worker `FrameJob::pcGain` / `pcNominalGain`, so the counting value is + `adcpedecorr / (g0[i] or 40)` - the same ADU/keV the keV spectra use - and the + increment follows in the same unit: `(value + thr) / (2 x thr)`. The unit + follows the gain stage itself, not the `keV PH` checkbox (that one only + decides what the spectra display). +* GUI: the threshold row is a `QDoubleSpinBox` (integer ADU, one decimal keV) + whose label switches `(ADU)` <-> `(keV)`; `setApplyGain()` stores the value + that is on screen into its own unit and loads the other one (signals blocked), + so toggling never destroys either. `--apply-gain` added as the CLI mirror of + the Gain button (the gain stage was the only one without one) - needed to test + this headlessly. +* Reported: `--stats` prints `pthr=150ADU` / `pthr=4keV`; the JSON carries + `photon_threshold_unit` and `photon_maps.threshold` + `threshold_unit` + (`threshold_adu` is gone - the key was wrong as soon as the unit could + change). +* Verified with the synthetic feeder, a zero pedestal and a **g0 = 1.0 + ADU/keV** gain map (float64 - the loader wants 6 x 1024x512 doubles; a float32 + file is rejected with the byte count): + - gain off: `pthr=150ADU`, one 200-frame map with **40 non-zero pixels, sum + 5320, max 133** - i.e. only the 300 ADU cluster, unchanged from before; + - `--apply-gain`: `pthr=4keV`, the **same frames** give **524288 non-zero + pixels** (every pixel: keV = ADC/1.0, so even the 100 ADU baseline is + 100 keV > 4 keV) with max 5925 - proof that the counting really divides by + ADU/keV before comparing; + - GUI: starting with `--apply-gain` shows `photon threshold (keV): 4.0`; + clicking the gain off shows `photon threshold (ADU): 150`; clicking it back + on gives a screenshot byte-identical to the first keV one, so both values + survive the round trip. +* Mid-save guard: toggling the gain while a photon-map save runs changes the + unit of the maps written after it, so `setApplyGain()` prints + `[gain] warning: the photon-counting unit is now keV while a photon-map save + is running ...` (and the same for ADU). Verified with a photon save started + by `--start-save` and a gain click: the warning appears once, the save keeps + running and writes its map, the JSON keeps the start-of-save threshold/unit. + +### Window handling, labels and the flow arrows + +* **Quit closes every window.** The ADC / gain / photon-map windows are + separate top-levels and the ROOT canvases live in the ROOT thread, so + `quitApp()` and `closeEvent()` now call `closeSideWindows()` (hide + close + each) and `QApplication::quit()`. Closing only the control window used to + leave the side windows - and the process, because Qt quits on the *last* + window - on screen. Verified on all three paths with the window count taken + from `xwininfo -root -tree`: + - Quit button, dialog confirmed: 6 app windows -> **0**, process gone; + - Quit button with a photon-map save running: the save is flushed + (`[shutdown] flushing the ROI save ...`, JSON `complete: true`), then + **0** windows / no process; + - window-manager close (a `WM_DELETE_WINDOW` ClientMessage, the path that was + broken): confirmed -> **0** windows / no process. +* Gain panel label: the no-map case is now + `no gain map loaded : using defaults` (the "defaults" are the nominal + 40 ADU/keV, used for the keV spectra *and* the keV photon threshold). +* Flow arrows: two new connectors - **Gain -> Virtual photon counting** (the + calibration; its threshold is keV while the gain is on) and **Virtual photon + counting -> Display** (the map has a window of its own) - and the save path is + now **orange**: the Gain -> ROI rail and the ROI -> Save arrow. Blue stays + the analysis/display chain. `FlowStrip::Path` gained a `save` flag and + `makeFlowArrow()` a colour argument (`#4673af` blue, `#de7d14` orange). + Verified on screen in the gain-on/pede-on state: both new arrows land on the + right panels, the rail and the ROI -> Save arrow are orange, the rest blue. +* The `JF_TIMING` `[rect]` diagnostic also prints `quit`, `track` and `pmap` + now, which is how the click-based tests above locate those widgets. + +### Dynamic flow colours (arrows static, colours not) + +Reworked after the operator's feedback: the geometry of the diagram is fixed, +only the colour class of each connector changes. + +* Three classes (`FlowClass` + `flowColor()`): `FlowData` orange `#de7d14`, + `FlowNormal` light blue `#5b9bd5`, `FlowPedestal` lighter blue `#a8c8e8`. +* The **data path follows the save mode**: `FlowStrip::setSaveMode(photonMaps)` + flips the Gain -> ROI rail and the Gain -> counting connector, and + `updateFlow()` (called from the radio handler) flips the two label arrows + ROI -> Save and counting -> Save. So in `save frames` the orange pair is + rail + ROI -> Save, and in `virtual photon counting` it is Gain -> counting + + counting -> Save - exactly two arrows at a time. +* Completeness: every stage now has an incoming and an outgoing connector. + The missing one was **Virtual photon counting -> Save** (a new arrow under the + counting panel, the left-hand twin of ROI -> Save). Display and Save are the + sinks; the storage cell has no incoming connector because the receiver stream + enters there. +* The colour legend is in the flow strip's tooltip and in the README table. +* Verified on screen: frames mode -> rail + ROI -> Save orange, counting arrows + light blue; after clicking `virtual photon counting` (and the label switching + to "Number of photon maps to be saved") -> Gain -> counting and counting -> + Save orange, rail + ROI -> Save light blue. `[save] mode: ...` is logged on + every change. +* Test note: the `JF_TIMING` `[rect]` lines can be ~30 px stale (they are printed + on a layout pass that may run before the panels settle), so a scripted click + should target the widget with a small downward tolerance - found the hard way + when a radio click missed. + +### The gate is a funnel on the line (not a stage) + +Operator's rule: the signal gate does not modify the data, so no connector may +enter or leave its box. Reworked accordingly: + +* The flow is now `Pedestal -> [funnel] -> Gain` (and `[funnel] -> tracking`), + i.e. one junction where the gate acts, drawn as a small trapezoid on the line. + The gate panel's only connector is a short blue **control arrow** into the + funnel's side; with the gate off the funnel and that arrow are drawn pale + (`FlowOff`, #c9c9c9) instead of disappearing. +* Two feeders reach the funnel and both are always drawn: through the **Pedestal** + (`FlowPedestal`) and straight from the **storage cell** (`FlowNormal`), the + raw bypass that avoids the greyed Pedestal box. `setStages(pede, gate, pmap)` + makes the live one coloured and the idle one pale grey - so in raw mode the + data visibly comes from block 1, as the operator asked. +* `FlowStrip::Path` grew `x` (vertical position), `head` (false = merge into the + junction) and `startAtJog` (begin on the vertical, for the funnel -> Gain and + funnel -> tracking branches); `cross()` honours them; `drawFunnel()` paints the + symbol plus its control arrow; `kFunnelX = 24` is where the flow passes the + gate. +* `updateFlow()` calls `setStages(pede, gateOn, gSaveMode == 1)`, so the four + states (pede/raw x gate on/off) are all live: verified with screenshots in all + four combinations - pedestal feeder live + funnel blue (pede+gate), funnel and + control arrow pale (pede+no gate), bypass live and pedestal feeder pale + (raw+gate), and the bypass stays live with the funnel pale (raw+no gate). + +### Flow diagram, final pass (operator's corrections) + +* **Lighter blue is now only the pedestal tracking pair** - the gate block -> + tracking input and the tracking -> Pedestal feedback. The storage cell -> + Pedestal and Pedestal -> funnel connectors are part of the data flow, so they + are orange like the rest of the route. +* **Orange is the whole save route**, entry point to Save panel, and it follows + the mode: frames = storage cell -> Pedestal -> funnel -> Gain -> ROI -> Save, + maps = ... -> Gain -> VPC -> Save. Display-only branches stay light blue, and + in photon mode the Gain -> ROI rail is light blue as well. +* **A route that carries no data is not drawn** (the operator found the pale + grey confusing): the raw bypass exists only in raw mode, the Pedestal path + only while the pedestal is in use, and only the selected mode's Save arrow + (ROI -> Save or VPC -> Save) is shown. The one thing that still greys is the + gate funnel when the gate is off. +* The **tracking input** now leaves the top-right corner of the gate block + (`yIn(sigGroup_, 6)`), not the funnel. +* Verified with screenshots in four states: pede+gate frames (orange: SC -> + Pedestal -> funnel -> Gain -> rail -> ROI -> Save; bypass absent; tracking + pair lighter blue), the same in photon mode (orange through Gain -> counting + and the counting -> Save arrow, rail light blue, ROI -> Save absent), + raw+gate frames (orange storage cell -> funnel -> Gain -> ROI -> Save; the + Pedestal box carries no in/out at all, only the tracking feedback), + and pede+no-gate (funnel and its control arrow pale, everything else + unchanged). + +### The display path and the save path can differ (last refinement) + +The operator pointed out that the orange route must follow the *save* logic, not +the `Pede/Raw` toggle: `flowStageStates()` now computes + +* `displayPede = PedestalStore::loaded() && !gNoPede` (unchanged: the display, + gate, spectra and the map accumulation), +* `savePede = (gSaveMode == 1) || (saveCorrected_ && !gNoPede)` (the maps always + need `g0`; the frames are corrected only with the checkbox), +* and `FlowStrip::setStages(displayPede, savePede, gateOn, photonMaps)` draws: + +| situation | what the diagram shows | +|---|---| +| display pede, `save frames`, `Pedestal corrected` **off** (the default!) | Pedestal feeder **light blue** (display only) + bypass **orange** (the file) - both live, the divergence visible | +| `Pedestal corrected` **on** (or photon maps) | Pedestal feeder **orange**, bypass not drawn | +| `Pede/Raw` = raw (both raw) | Pedestal path not drawn at all, bypass orange | +| photon mode | pedestal route orange through Gain -> counting -> Save; the Gain -> ROI rail is light blue and the ROI -> Save arrow is absent | + +`saveCorrected_` also got a `toggled -> updateFlow()` connection (it had none) so +the diagram follows the checkbox, and a `saveCorrected` entry in the `[rect]` +geometry diagnostics for the click tests. + +Verified with screenshots: `--use-pede --gate` in frames mode showed the blue +storage cell -> Pedestal -> funnel next to the orange storage cell -> funnel -> +Gain -> ROI -> Save (both feeders into one funnel), and clicking `Pedestal +corrected` turned the pedestal route orange and removed the bypass. + +### ROI input arrow + "Unset ROI" button + +* The ROI's incoming connector is now mode-dependent and always orange: + `Gain -> ROI` (the rail) while frames are written, `counting -> ROI` (a new + connector) while the maps are - they are cropped to the ROI on the way out, so + the ROI is only ever on the save path. Only one of the two is drawn at a + time; `setStages()` flips them with `p4_.on = !photonMaps` / `p9_.on = + photonMaps`. +* New **Unset ROI** button in the ROI panel (`unsetRoi()`): releases the "ROI + only" restriction if it is on, then `GateEngine::clearRoi()` (so `roiSet_` and + `roiOn_` are false and the rectangle is the full frame), clears the yellow + overlay, resets the counters and updates the read-outs. The ROI list file is + not touched. +* Verified: with `--gate-roi 100 50 200 100 --gate --stats` the live line showed + `roi=200x100@100,50`; clicking **Unset ROI** logged `[roi] ROI unset: full + frame` and the same line then showed `roi=off`, the panel reading + "no ROI selected | gate: full frame". In `--save-photon` (photon mode) the + screenshot shows the orange connector from the counting panel into the ROI and + no rail from the Gain. + +### Save-time locking, truthful JSON, ROI->Save, and small labels + +Operator requests, all in and verified with a running `--save-photon` run: + +* **The arrow into the Save panel always leaves the ROI** (never the counting + panel): `arrPmapDown_` is gone, `arrRoiDown_` is always drawn and always + orange. The maps reach the file through the ROI, which is also why the ROI's + own input flips between the Gain (frames) and the counting (maps). +* `cumulative` -> **`cumulative (toggle to reset)`**, and the toggle now really + resets (`resetHis()`), so the label is true. +* **`Pedestal corrected` moved under the `save frames` radio** (it is a property + of the saved frames; the maps are always pedestal-subtracted) and is greyed + while the photon-map mode is selected. +* **The save counters no longer read 0 in photon mode**: `RoiSaver::Status` + gained `pmaps` + `photonMode` (Impl::pmapsInFile, fed by the writer loop), so + the GUI status line says "N maps written, M dropped" and `--stats` says + `save=1map/0` instead of `0/0`. +* **Settings are locked while a save runs** (`applySaveLock()`): the whole save + panel except `Start/Stop save` and the status label, the storage-cell filter, + the manual pedestal work, the gain correction (keV PH stays live) and the + ROI select/load/unset buttons. Live on purpose: everything display-only, the + pedestal tracker, the gate **threshold** and **min pixels**. Those two open + `Changing settings during a run, are you sure?`; No puts the old value back + (`pipeline_.gate().threshold()/minPixels()`), Yes applies it and sets + `gGateChangedDuringSave`, which makes `writeJsonSidecar()` add + `gate_settings_modified_during_run: true` and a `gate_settings_note` (the + `gate` block stays the criterion from save start). The lock is applied on + start, released on stop - including the automatic stop (budget/error), which + `updateStatus()` detects through `m_saveWasRunning`. +* Verified on screen during a running photon-map save: save panel greyed except + Start/Stop, SC/pede/gain greyed, Threshold 150.0 and Min pixels 5 live, the + Virtual photon counting panel's two display controls live with the new label, + the photon threshold greyed, `Pedestal corrected` greyed under the frames + radio, and the Save status reading "1 map written, 0 dropped". + +### Layout bug (mine), destination list, "queue full" + +* **The `virtual photon counting` radio was rendered outside the Save box.** When + I moved `Pedestal corrected` under the `save frames` radio I dropped the + `saveForm->addRow(saveModePhoton_);` line, so the radio had no layout slot and + Qt gave it a default geometry (measured: x=9, y=915, 100x30 - i.e. floating at + the left edge above the Save box, which is exactly what showed up as a stray + "Virtual phot..." fragment). Re-added; the `[rect]` diagnostic now reads + `saveFramesRadio 21,996 | saveCorrected 21,1025 | savePhotonRadio 21,1054`, + all 788 px wide inside the box. (`layoutFlow()` also repaints the central + widget after solving now, which is cheap insurance against leftovers in the + pads' wake.) +* **The previous-destinations list is no longer polluted by the default**: the + start-up `--h5-dir`/`--h5-base` pair is captured once (`saveDefaultDir_`, + `saveDefaultBase_`) and a save that uses exactly that pair logs + `[save] / is the default destination, not added to the + previous-destinations list` instead of recording it. Verified: the list stayed + at 22 lines over a `--start-save` run with the default destination. My own + test entries (`*_scratch*`, 23 of them) were pruned from + `save_dest_list.dat`; the real `/mnt/sls_det_storage/...` destinations and the + `test/_run`/`testPHmapsave` entries were left alone. +* The dropped counter now reads `(writer behind: queue full)` and the README + explains it: the writer buffers at most 4096 items / 256 MB, so a slower disk + than the stream means the excess is dropped (and still counted by the gate + counters). + +* The drop note was attached unconditionally, so a healthy run read + "0 dropped (queue full)" - a problem that was not there. It now says + `0 dropped (nothing lost)` while nothing is lost and only switches to + `(writer behind: queue full)` when the counter is non-zero. Verified on a + running photon-map save: "1 map written, 0 dropped (nothing lost)". + +* The "Changing settings during a run, are you sure?" popup is now raised only + when **`Gate enabled` is ticked** (`confirmGateChangeDuringRun` returns early + on `!pipeline_.gate().enabled()`): with the gate off (passthrough) the + threshold and min-pixels change nothing about the saved frames, so they go + through silently and no `gate_settings_*` note is written. Verified with a + running save: gate on + clicking a gate spinbox -> dialog shown and declining + logged `[save] gate change refused during the run (Min pixels above threshold + -> 6)`; the same click with `--no-gate` -> no dialog, no log line. + +### Image windows redrawn as plots (frame, axes, pixel grid, scale panel) + +Operator request: replace the side colour bar with a panel and make the images +read as plots. + +* `FastImageView::imageRect()` now reserves axis margins (52 left / 26 bottom / + 6 top-right) instead of the old bar strip; `paintEvent()` draws the frame, + tick labels outside the plot (Y = data rows, row 0 at the bottom), a dotted + grid inside it, and keeps the gate-ROI / receiver-ROI overlays as before. +* `m_sx` (screen pixels per image pixel, from the last paint) drives the pixel + detail: thin outlines from 16 px, in-pixel values from 50 px + (`setIntegerValues(true)` for the photon map -> whole numbers, one decimal for + the real-valued images). The label colour is chosen from the luminance of the + pixel that was actually painted; the loops are capped (<=256 px visible for + the grid, <=64 for the labels) so a deep zoom can never stall the painter. +* The colour bar is off (`m_showBar = false`; `scaleRect()` returns an empty + rect so the old drag code is inert) and each image window now carries a + `ScalePanel` under it: `ScaleHisto` (the image's own 256-bin distribution with + the mapped range highlighted, refreshed at most every 300 ms from + `renderImage()`), a palette chooser wired to `paletteLut()` (kept in sync with + the `b`/`Shift+B`/`c` shortcuts), a lin/log switch, and min/max spin boxes + (also synced from `Set Ranges...`). +* log mode maps `log10(max(v, 1e-2))` against `log10(max(range, 1e-2))`, so + values <= 0 land at 1e-2 = -2 instead of -inf, and the "zero is white" rule + for the photon map still applies first. +* Verified on screen: the framed plot with the X labels (475..505) and Y labels + (97..90, high at the top) outside the frame, the dotted grid, the per-pixel + outlines at ~33 screen px/pixel, and the panel below with the histogram, + `palette: Col (jet)`, `lin`, `min -200.0`, `max 1000.0` (the ADC defaults). + Not yet seen on screen: the in-pixel numbers (they need >50 px/pixel, my + scripted wheel zoom stopped at ~33) and the histogram with a varied image + (a uniform test frame gives a single bin). The old bar's drag-to-stretch on + the scale is gone by design: the range is set with the spin boxes (dragging on + the histogram is a possible follow-up). + +* Scale histogram refined to the operator's spec: fixed 38 px strip (the image + keeps the window stretch, the panel has a fixed height - about a tenth of the + window), one **solid** bar per bin whose width is the bin spacing on screen, + and the two range markers pinned at **1/4 and 3/4** of the width with the value + axis rescaling under them (the drawn window is the selected range widened by + half a range on each side, so dragging a marker moves that end of the range + and the marker snaps back to its fixed spot). The bars are draggable + (`onRangeChanged` -> the panel pushes the new range into the view and the spin + boxes) and the ends are printed next to the markers. A **second lin/log + switch** was added: `image` for the intensity mapping, `histo` for the bar + heights (log10(count+1)); the histogram's value axis always follows the image + switch. Verified on screen: the thin strip with the two orange markers at + 1/4 and 3/4 labelled `702.7` and `1000`, and a drag of the left marker moving + `min` from -200 to 702.7 (the bars were off-window by then, which is the + fixed-marker behaviour). + +### Palette list widened to the matplotlib family + +The scale panel's palette chooser now offers 18 maps instead of 3 (the original +`Col (jet)`, the two ROOT-like greys, then the new ones). The `Palette` enum +keeps `Color/BW1/BW2` first so the `b` / `Shift+B` / `c` shortcuts and +`setPaletteChoice()` keep working, and `paletteName()` (new, in +`online_jf_root.cpp`, deliberately outside the anonymous namespace) feeds the +combo so the names live in one place. + +* New: Turbo, Viridis, Plasma, Inferno, Magma, Cividis, Hot, Grey (linear), + Grey reversed, Coolwarm, BWR, Seismic, RdBu, YlGnBu, Sunset. +* Anchors: the perceptually uniform ones are matplotlib's published anchors and + Turbo is Google's; the diverging/sequential set uses matplotlib's definitions + and the ColorBrewer / Paul Tol anchors (YlGnBu, Sunset) as republished in + `bokeh/palettes.py`, which is where I looked them up - bokeh's licence header + attributes Viridis/Magma/Plasma/Inferno to Nathaniel Smith et al. (CC0), + Turbo to Google (Apache-2.0), the Brewer maps to Cynthia Brewer (Apache-2.0) + and Tol's to Paul Tol (BSD-3). Everything is interpolated by the existing + piecewise-linear `PaletteDef`, whose stop/colour arrays were grown from 5 to + 24 entries to hold the 15-anchor turbo ramp; the LUTs are now one + `g_luts[kNumPalettes][256]` table built by the same `buildLut()` loop, so a new + palette is only an enum entry plus its anchors. +* Verified: the entry list builds and the app runs; selecting `Seismic + (diverging)` in the ADC window turned the image seismic-blue (raw ~100 ADU on + the -200..1000 range lands just below the white midpoint, as expected) with + the panel and histogram following. + +* The **gain window** lost its scale panel entirely (no histogram, no palette / + lin-log / range controls): its image holds four valid numbers - the gain bits + 0..3 - so there is neither a distribution to show nor a scale to choose. It + now uses a fixed `Palette::Gain4` map (0 muted blue, 1 muted green, 2 muted + yellow, 3 muted red, all desaturated on purpose so a large flat map does not + glare) with the range pinned to 0..3, and whole-number in-pixel labels. The + palette is in the enum but deliberately **not** in the chooser loop + (`i <= int(Palette::Sunset)`), so the three image windows keep their own + choices. + +* In-pixel numbers: the font now also has to fit the *width* of the label, not + just the pixel height - five characters for the photon counts, six for the + real-valued images ("XXXXX.y"), at ~0.62 em per digit, still capped at 0.30 em + of the pixel so the "small tidy number" look is kept (range now 7..20 px). + At 50 px/pixel that is 12 px instead of 15, which is what makes five digits + fit; the widest label stays under the pixel edge for every zoom level above + the 50 px threshold. + +### Zoom coordination switch (Display panel) + +Operator request: zooming used to be ADC-master only (the ADC view propagated to +the gain/photon windows and the ROOT plots, never the other way round). Now +`zoomTo(FastImageView *src, ...)` takes the view that asked, and every image view +reports its zoom through the same wiring (`wireZoom()` for ADC, gain and photon), +so **any** window zooms all of them. A new `zoom coordination` checkbox in the +Display panel (default **on**) gates the propagation: when it is off, `zoomTo()` +returns immediately, so each window keeps its own zoom and nothing is pushed to +the others - including the ROOT canvases, which as a consequence no longer follow +the ADC while the box is off (say the word if they should keep following it +regardless). Both transitions are logged (`[zoom] coordination on/off`) and +every coordinated zoom logs its source (`[zoom] coordinated from ADC/gain/photon`). + +Verified: with the default on, five wheel steps over the ADC window produced 10 +`coordinated from ADC` events (two per step: wheel plus the drag path); the +checkbox is present at a sane place in the Display panel +(`[rect] zoomCoord 476,730 333x23`, ticked). The OFF case is a single +`if (!zoomCoord_) return;` on that same path. + +### Makefile: both variants from one `make` + +`make jfstrx` used to rebuild the *same* binary with `-DJFSTRX` (the old +`.buildflags` stamp existed only to force a full rebuild when switching), so the +two variants overwrote each other. Now the objects and the executables are +separate: + +* `online_jf_*.o` + `jf_online_display_gui` (normal), +* `online_jf_*.JFSTRX.o` + `jf_online_display_gui_JFSTRX` (compiled with + `-DJFSTRX`), + +and `all` builds both, so the two can never be mixed by accident (they have +different geometry and struct layouts). `make normal` and `make jfstrx` remain +as aliases, the flag stamp is gone, `clean` removes both sets, and `.gitignore` +covers the second binary and the `.JFSTRX.o` files. + +* Zoomed images were painted over the axes: the scaled image is much larger than + the plot area (only the viewport window is meant to be visible), and the ROI + overlays were drawn in widget coordinates without a clip, so a ROI partly + outside the visible window ran across the tick labels and up to the window + edge. `paintEvent()` now sets `setClipRect(ir)` around the image and around + the two overlays (restoring before the axes so the frame, grid and labels are + drawn on top of a clean widget). Verified by sampling a deep-zoom screenshot: + the margin left of the frame is the widget's black background, not image + colour, while the plot interior still carries the image. + +* The strip-xel decoder (../../dataStructures/jungfrauLGADStrixelsData_new.h) + dumped its header on every construction - "Jungfrau strixels 2X single chip + with full module data" / "sizeofheader = 112" - once per displayed frame in + the JFSTRX build. Those three `std::cout` statements are commented out (the + header is shared, outside this project, so the edit needed the wider sandbox + permission; it is a comment-only change and easy to revert). +* Deepest zoom is now build-dependent (`kMinViewport`): 8 image pixels across in + the plain build as before, **2** in the JFSTRX build, so single strixel pixels + can be inspected (the mapped panels are ~1000 x 1900). The same constant + guards `setViewport()` and `zoomTo()`, so a coordinated zoom propagates down + to the new limit. + +### Dual-interface alignment: drain fast, pair index-aligned, shed on backlog + +Operator report: after the machine had been busy for a while the two interfaces +stopped being aligned and never recovered, with the deltas fluctuating. There +were two causes, one on each side. + +* **Holding a slab is back-pressure.** The old pairing stopped polling an + interface whose slab was waiting for its partner (`events = 0`) and accepted + only *equal* frame numbers. That fills slsReceiver's ZMQ ring, and the + receiver then skips frames **on its own side** - per interface, not per + frame - so the two frame numbers drift apart and, with exact matching, never + meet again. Waiting "until the partner catches up" made it worse. + +What the code does now (`Pipeline::tryAssemble()` in `online_jf_core.cpp`): + +* **Always drain.** Every socket is polled on every turn and drained down to + its newest message (bounded to 32 messages per socket and wake-up, so no + interface is starved). Nothing is ever held, so the receiver's buffer stays + empty - the whole point at 2 kHz. +* **Pair index-aligned or not at all.** When the newest slabs carry different + frame numbers (one interface skipped), the pair is **dropped** (`resync`) and + the next one is tried, so every frame that is displayed or saved is stitched + from a single detector frame. No chasing, no unbounded wait, no half frames. +* **Latch for a permanent offset.** If nothing pairs for 500 ms the offset is + structural (an interface that keeps skipping), so the newest slabs are paired + anyway (`skewedPairs_`, logged) to keep the stream alive; strict pairing + comes back by itself as soon as a pair matches again. +* **Decimation is manual by default**, `--decimate 2|4|8` (1 frame in N, dropped + by detector frame number so both interfaces drop the same ones). The automatic + ladder (1 -> 2 -> 4 -> 8, stepping back up after 5 s clean) only runs with + `--auto-decimate`: frames are queued in the *gate*, so the backlog is + `framesIn - framesPass - framesDrop - framesScDrop` - counting gate rejects as + backlog used to pin the ladder at 1/8 whenever the gate rejected anything. +* **A silent interface is reported, not waited for**, so a dead stream cannot + freeze the pipeline. +* Audit: `--stats` prints `resync` / `behind` / `incomplete` and the + per-second `[align] keep=1/ shed=` line; ladder steps and "dropping the + pair" messages are rate-limited. + +Verified against the live receiver `10.1.1.106 30026+30027` at ~2000 frames/s: +20 s runs alone and with eight CPU hogs (`yes`) both keep `drop=0`, +`in == pass`, `keep=1/1`, `shed=0`, with no unpaired-interface message. The +synthetic feeder with equal keys gives `resync=0`; with a permanent one-frame +offset (`dual ... 1`) the latch keeps ~16 frames/s at 20 fps instead of +stalling. At 2 kHz the interfaces are a frame apart often enough that roughly +one pair in four is dropped by the strict rule - the intended trade (aligned +frames only). A one-frame lookahead per interface (swap two buffers, no copy) +would recover them; it is the obvious follow-up if the saved-frame rate ever +matters. diff --git a/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/README.md b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/README.md new file mode 100644 index 000000000..12d6c0c8a --- /dev/null +++ b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/README.md @@ -0,0 +1,1389 @@ +# jf_online_display_gui — online Jungfrau display **+** event finder in one process + +A rewrite that merges the two separate programs + +| old | role | +|---|---| +| `jf_filter.cpp` + `jf_event_finder_cli/gui.cpp` | ZMQ subscriber → pedestal subtract → threshold gate → ZMQ publisher | +| `onlinedisp_zmq.cpp` | Qt5 GUI + ROOT plots, subscribed to the filter's output | + +into a **single multithreaded program**. The intermediate ZMQ hop between the +filter and the display is gone: one subscriber reads `slsReceiver`, the gate +decides pass/drop on every frame, and only passing frames reach the display and +the analysis. + +It also supports slsReceiver's **multi-UDP-interface ("threads") mode**, where +the module is streamed as several ZMQ ports — see +[Multi-interface mode](#multi-interface-mode-two-udp-interfaces). + +The originals in the parent directory are untouched. + +--- + +## Build + +Two equivalent builds exist: **CMake** (same as the rest of the package) and the +self-contained Makefile. Both produce the same two programs from the same +sources. + +```sh +# CMake (out-of-tree, nothing is written into the source directory) +cmake -S . -B cmake-build -DCMAKE_BUILD_TYPE=Release +cmake --build cmake-build -j # both variants + tests +./cmake-build/jf_online_display_gui # normal +./cmake-build/jf_online_display_gui_JFSTRX # LGAD strip-xel variant +./cmake-build/gate_contig_test # gate unit test (ctest also runs it) +./cmake-build/jf_feeder # synthetic slsReceiver +# -DJF_BUILD_TESTS=OFF skips the test and the feeder +# -DJF_ZMQ_STATIC=/path/libzmq.a overrides the ZMQ library +# (Qt5, ROOT and HDF5 are found with find_package; without a usable +# static libzmq the build falls back to the system one via pkg-config) + +# Makefile (identical binaries, in-tree objects) +make # -> ./jf_online_display_gui (Qt5 + ROOT + static libzmq) +make # also builds ./jf_online_display_gui_JFSTRX, the + # LGAD strip-xel variant of the same sources + # (-DJFSTRX; `make normal` / `make jfstrx` build one) +make feeder # -> ./test/jf_feeder (synthetic slsReceiver, testing) +make clean +make jfstrx # optional -DJFSTRX (LGAD strip-xel) build +``` + +Deps: system ROOT (`root-config`), Qt5 (`pkg-config`), HDF5 (`libhdf5_cpp`, +`libhdf5`), rapidjson headers, and the prebuilt `libzmq.a` from +`../../../build/_deps/libzmq-build/lib/`. + +## Run + +```sh +./jf_online_display_gui [rx_port] [gate_threshold] [min_pixels] [pede_file] \ + [--pub PORT] [--contig] [--gate|--no-gate] [--stats] \ + [--use-pede|--no-pede] [--acquire-pede] [--pede-frames N] +``` + +The positional arguments are the union of the two originals: + +| arg | meaning | +|---|---| +| `rx_host` / `rx_port` | slsReceiver stream (default port 30001). `rx_port` may be a `+` list for the multi-interface mode: `30001+30002` | +| `gate_threshold` | event-finder gate threshold, pede-subtracted ADC (default **150**) | +| `min_pixels` | gate: pass when ≥ N pixels are above threshold | +| `pede_file` | pedestal file (default `./current_pede.dat`) | + +Options: `--pub PORT` additionally republishes gated frames (header+data, same +format as the old filter) for third-party subscribers; `--contig` enables the +contiguity proxy; the **gate is off by default** (`--gate` enables it, +`--no-gate` is the default transparent passthrough); **pedestal subtraction is +off by default** (`--no-pede` is the default "raw ADC" mode, `--use-pede` +starts with subtraction enabled); `--stats` prints +the old CLI counter line once per second; `--acquire-pede` averages +`--pede-frames N` frames into a live pedestal as soon as acquisition starts; +`--gate-roi X Y W H` starts with a gate ROI already set (data rows: y = 0 is the +bottom row, the same convention as the ROI list and every plot - see +[One coordinate system](#one-coordinate-system-image-and-root)); +`--sc N` passes only frames from storage cell N (0..15, bits 8-11 of +`detSpec3`; `--sc all` for no cell gating); `--raw-map` shows raw ADC in the +main map instead of photon counts. The **photon-map threshold** (ADU, default +150) is no longer an argument: it is the control in the *Virtual photon +counting* panel, and the `--stats` line reports it with its unit (`pthr=150ADU` +or `pthr=4keV`), together with `kev=off|loaded|nominal`. `--apply-gain` starts +with the gain correction on, so a scripted run can use the keV threshold. + +Saving and the photon-map display can also be pre-set, which is what the +headless checks use: `--pmap-every N` (photon-map display cadence, default +2000), `--pmap-save-every N` (saved-map cadence, minimum 200, default 2000), +`--save-photon` (write the virtual photon counting maps instead of the frames), +`--save-budget N` (stop after N saved frames, or N saved maps in photon mode) +`--start-save` (press the Save button at startup) and `--apply-gain` (press the +Gain button). Each of them does exactly what the corresponding control does, so +a run can be driven from a script. + +```sh +# was: jf_event_finder_gui 30001 30011 ./current_pede.dat +# then: LD_LIBRARY_PATH=/usr/lib64/root ./onlinedisp_zmq 30011 +# now: +./jf_online_display_gui 30001 100 5 ./current_pede.dat +``` + +## Threads + +| thread | owns / does | +|---|---| +| main / Qt | `QApplication::exec`, all widgets, button + shortcut handlers | +| **acq** | `zmq_poll` over every interface, receive, frame pairing/assembly, gate decision + counters + rates, optional republish, live pedestal acquisition, hands the frame to the mailbox | +| **analysis** | pedestal subtraction, ADC/gain images, photon counting, and the **only** thread that touches the source ROOT histograms (per-frame work accumulates into plain arrays; the histograms are synced at the redraw cadence) | +| **ROOT** | canvas painting, `gSystem->ProcessEvents`, the user command queue, paints the display clones | + +Why the gate is not done in the analysis thread: the gate must count **every** +received frame, while the display is allowed to fall behind. A single-slot +"latest frame wins" mailbox decouples them — dropping an old frame there is the +merged equivalent of the ZMQ receive-HWM drops of the old chain, and the gate +counters stay exact. + +All interfaces are read by the **one** acq thread via `zmq_poll`, so pairing +needs no extra synchronisation. + +## Multi-interface mode (two UDP interfaces) + +`slsReceiver` with N UDP interfaces ("threads") publishes **one ZMQ stream per +interface on consecutive ports**, each carrying one Y-slab of the module: + +* `DetectorImpl.cpp` → `portnum = getClientStreamingPort() + (iSocket % numUDPInterfaces)` +* `JungfrauData::UpdateImageSize()` → `nPixelsX = 256*4 = 1024`, + `nPixelsY = (256*2)/numUDPInterfaces` → **256 rows per interface for N=2** +* `GetPortGeometry()` for JUNGFRAU → `portGeometry.y = numUDPInterfaces` + (`numPorts = {1,2}`), i.e. the split is along **Y** + +So for one Jungfrau module split in two, each port streams 1024×256 +(524288 B) and the two stack into 1024×512: + +```sh +# slsReceiver started with 2 interfaces -> ports 30001 (bottom) and 30002 (top) +./jf_online_display_gui 30001+30002 100 5 ./current_pede.dat +``` + +* **Order = image order.** The list is read bottom → top, the first port + occupying the lowest rows. To swap the halves, reverse the list + (`30002+30001`) — no extra flag. +* **Assembly.** Each slab's geometry comes from its own JSON header + (`shape`, `size`), and the slabs are copied into the full 1024×512 frame. + An interface that streams a *full* 1024×512 buffer instead is handled too: + only its own slab is taken out of it. +* **Pairing and alignment.** Every socket is drained continuously - nothing is + ever held back, so the receiver's buffer stays empty - and the newest slab of + each interface is paired. If their frame numbers differ the pair is dropped, + so an assembled frame is always stitched from a single detector frame (see + [Keeping the interfaces aligned](#keeping-the-interfaces-aligned)). If no + pair succeeds for 500 ms the interfaces are permanently offset (one of them + keeps skipping): the newest slabs are then paired anyway to keep the stream + alive, and strict pairing returns as soon as the numbers match again. A dead + interface never stops the others; it is reported, not waited for. +* **Everything downstream is unchanged**: gate, images, photon map, T vs B / + L vs R, ROOT fills and the pedestal layout all operate on the assembled + 1024×512 frame, so the existing `current_pede.dat` stays valid. +* **Status** is per interface in the GUI and in `--stats` + (`stream=connected ports=connected,idle`), each with its own + `zmq_socket_monitor` connection state. +* **Start-up checks** are printed once per interface, which is how you confirm + the orientation on real hardware: + + ``` + [stream] port 30001: 1024x256 (524288 B) -> rows 0..255 of 1024x512 + [stream] port 30002: 1024x256 (524288 B) -> rows 256..511 of 1024x512 + [frames] first assembled frame: port 30001 -> row 0 = 100, port 30002 -> row 256 = 150 + ``` + + If the second line shows the *top* half arriving on the first port, reverse + the port list. +* **`--pub`** republishes the assembled full frame (a synthesised header with + `size=1048576`, `shape=[1024,512]`), so downstream consumers see the same + single-stream format as before. +* **Limits.** This is the single-module display (1024×512). A port count that + does not divide 512 rows evenly (or a slab height that is neither 512/N nor + 512) is refused with an explicit error instead of producing a wrong image. + +## Analysis stage: deferred histogram accumulation + +`TH1::Fill()` was called once per pixel per frame for the chip spectra and +again for the photon map — 0.6-1 M fills/frame, ~4-6 ms, i.e. the whole cost +of the analysis stage (measured: 239 fps without the photon map, 179 fps with). +Filling a histogram that is painted once per `rootUpdateMs` at frame rate is +pure waste, and ROOT histograms are not thread safe anyway (bin array *and* +shared statistics), so calling `Fill()` from several threads is not an option. + +Instead the analysis thread accumulates each frame into plain arrays: + +* `g_pmapAccum[NCH]` — photon map (and the raw-ADC map, `--raw-map`); +* `g_chipAccum[8][NBIN+2]` — chip spectra, including under/overflow; + +and pushes them into `his3000` / `hchip` at the redraw cadence +(`clamp(rootUpdateMs/2, 50, 500)` ms), under `gHistoMtx`: + +* the photon map is written with `SetBinContent` (absolute contents, since the + accumulator is cumulative) — a 524288-bin copy at most 20x/s is free; +* the chip spectra are filled with the **delta** per bin, so ROOT's own + statistics keep working; the bin index is computed directly and was verified + against `TAxis::FindBin` over every bin edge and 20M random values; +* `SetEntries` restores the true entry count, because `Fill(x,w)` increments + `fEntries` once per *call*, not per weight; +* "Reset His" zeroes the accumulators and the histograms together. + +Verified exactly against an analytic prediction (two 1024x256 slabs of ADC 100 +and 150, zero pedestal, threshold 150): every chip bin/entry and every photon-map +bin matched to the last digit. + +Measured on a live receiver streaming ~1500-2000 frames/s: + +| | before | after | +|---|---|---| +| analysis frames/s (chip fills only) | 239 | **538** | +| analysis frames/s (+ photon map) | 179 | **513** | +| photon-map cost | -25% | -5% | + +The display still subsamples (analysis ~500 fps vs stream ~2000 fps), so +`displaydrop` remains the counter to watch — it is now ~68% instead of ~88%. +One cosmetic difference: the chip-spectrum stats box mean/RMS is computed from +bin centres rather than exact ADC values (sub-bin, <0.1% on the mean), while +entries and all bin contents are exact. + +### Where the analysis time goes + +`JF_TIMING=1 ./jf_online_display_gui ...` prints a per-step breakdown when the program +exits. On a live stream (~2000 fps, two interfaces, full pedestal set): + +| step | us/frame | share | +|---|---|---| +| mailbox take + frame memcpy (1 MB) | 36 + 21 | 3 % | +| step 1: ADC/gain image build (524288 px) | 527 | 27 % | +| step 2: pedestal subtraction + photon counting + chip binning (524288 px) | 1294 | 67 % | +| step 3: T vs B / L vs R (1536 ROOT fills) | 16 | 1 % | +| step 4: raw-ADC map (only with `--raw-map`) | 0 | - | +| accumulator sync (amortised) | 10 | 1 % | +| step 5: image publish (every `HDraw_every` frames) | 16 | 1 % | +| **total** | **1920** | 521 fps | + +(One run; a second run on the same shared machine gave 2294 us / 436 fps, so +treat these as +/-25 %. The *shape* is stable: copies ~2 %, step 1 ~30-40 %, +step 2 ~55-65 %.) + +So the bulk memcpys are *not* the limit (1 %). The two per-pixel passes are +94 %, at ~1.0 ns/px (step 1) and ~2.5 ns/px (step 2) — i.e. ~3 and ~7 cycles +per pixel of scalar, branchy work (gain decode, `i%1024` / `i/1024` index +arithmetic, `chipBin`, the accumulator scatter). Memory is not the limit +either: the working set is ~15 MB and the loops use ~5-12 GB/s, while a +single-core memcpy on this machine sustains ~110 GB/s (L3-resident). +A live-acquired pedestal (no g1/g2 streams) only saves ~80 us of step 2, so +halving the pedestal traffic would buy ~4 %. + +## Parallel analysis (worker threads) + +Steps 1 and 2 are pure per-pixel work over disjoint ranges, and after the +accumulator change they touch no ROOT state at all (`adc_image`, `gain_image`, +`hpmapAccum` and `hchptr` are written per pixel; only the chip spectra are a +scatter, handled with small per-worker copies that the controller merges once +per frame). So they can simply run on several threads: + +``` +--analysis-workers N 1 = single threaded, 0/omitted = auto (cores/4, max 8) +``` + +The worker threads are persistent; a frame is handed over with a generation +counter (two condvar handshakes per frame, a few us). Worker 0 is the analysis +thread itself, so nothing changes for `N=1`. Step 3 (T vs B / L vs R), the +histogram sync and the image publish stay on the controller. + +Measured on the live stream (~2000 fps, both interfaces, full gain paths): + +| workers | analysis rate | +|---|---| +| 1 | 407-415 fps | +| 4 | 952-1220 fps | +| **8** | **1340-1376 fps** | +| 12 | 1255-1343 fps (saturated) | + +The results are **bit-identical** to the single-threaded run: verified with the +deterministic two-slab setup for `N = 1, 2, 4, 8` - every chip bin, entry and +photon-map bin matched to the last digit. Integer per-pixel accumulation into +disjoint ranges plus an ordered merge makes the outcome independent of +scheduling. + +Practical effect at ~2000 fps: analysis coverage goes from ~20 % to ~67 %, i.e. +`displaydrop` falls from ~80 % to ~33 %. + +This also leaves the door open for the planned AFC/gain-split histograms: the +per-worker accumulators are L1/L2 resident and simply grow with the number of +gain groups, with no locking added. + +## Keeping the interfaces aligned + +`frameIndex` cannot be used as a shared key: `DataStreamer` computes it as +`frameNumber - firstIndex` and `firstIndex` is per-streamer, so the two +interfaces can legitimately report different `frameIndex` for the same frame. +Pairing therefore uses the **absolute `frameNumber`**. + +Three rules, in order of importance: + +1. **Never hold a slab (no back-pressure).** slsReceiver has its own ring + buffer; if our ZMQ queue fills, the *receiver* skips frames on that + interface - per interface, not per frame - and the two frame numbers are + offset from then on. So every socket is drained to its newest message on + every wake-up and nothing is ever kept back. +2. **Pair index-aligned or drop the pair.** The newest slab of each interface + is paired; if the numbers differ, both are discarded (`resync`) and the next + pair is tried. An assembled frame is therefore always stitched from one + detector frame, and no frame is ever shown or saved half-filled. +3. **Latch when the offset is structural.** After 500 ms without a successful + pair the newest slabs are paired anyway (counted separately and logged) so a + permanently offset interface still streams; the first matching pair returns + the pipeline to strict pairing. + +Decimation is a manual choice: `--decimate 2|4|8` assembles one frame in N, +chosen by the shared frame number so both interfaces drop the same frames. The +automatic ladder (`--auto-decimate`) reacts only to frames actually queued in +the gate (`framesIn - framesPass - framesDrop - framesScDrop`) - counting gate +rejects as backlog used to pin it at 1/8 - and climbs back after 5 s without a +backlog. Everything is audited through `--stats` (`resync`, `behind`, +`incomplete`, `[align] keep=1/N shed=n`) and the rate-limited log messages. + +## Display controls + +| button | key | action | +|---|---|---| +| Start/Pause | `s` | pause/resume processing | +| Faster / Slower | `+` / `-` | main image every N *analysed* (gate-passed) frames, but never slower than one every 250 ms | +| gain correction on/off | `g` | divide by ADU/keV (gain map, else nominal 40) - the `gain corrected` tick next to it is a read-only indicator | +| BW Pal. / BW Pal. 2 | `b` / `Shift+B` | monochrome palettes | +| **Col Pal** | `c` | back to the colour palette | +| Set Ranges | `S` | ADC / photon-map / chip-X ranges | +| Update Rates | | main image cadence + ROOT redraw period | +| Reset His | `r` | clear the photon map and spectra | + +The photon map itself is **not** in the control window any more: it has its own +window, `Photon Map (virtual photon counting)`, whose panel is *Virtual photon +counting* (left column, under the gates): + +| control | action | +|---|---| +| cumulative (toggle to reset) | off by default: each map covers the frames since the previous publish, so the picture shows the *rate* (turn it on for a running total). Toggling it restarts the map from scratch, so the switch doubles as the reset | +| plot every N images | publish cadence of the map, counted in **analysed frames** (default 2000); independent of the ADC image cadence | +| photon threshold | threshold of the photon map; a pixel above it counts `(value + threshold) / (2 x threshold)` photons. It is in **ADU** while the gain correction is off (default 150) and in **keV** while it is on (default 4.0): the counting value is then the pedestal-subtracted ADC divided by the pixel's ADU/keV (gain map, else the nominal 40). Both numbers are kept, and switching the gain swaps which one the row shows, so neither is ever overwritten. Toggling the gain while a photon-map save runs changes the unit of the maps written from that moment on, which the run announces (`[gain] warning: the photon-counting unit is now keV ...`); the JSON keeps the value it recorded at save start | + +The photon window has no ROI and no gate: the ADC image, the gain image and the +photon map are three independent windows. + +**The image windows are plots.** Each one draws the image inside a frame with +X/Y axes, tick labels *outside* the plot area and a light dotted grid inside it +(the Y labels are data rows, row 0 at the bottom, like everywhere else). When +the zoom makes an image pixel at least **16 screen pixels** wide, every pixel is +outlined with a thin line; above **50 screen pixels** the value is written inside +each pixel - whole numbers for the photon map (counts), one decimal for the +real-valued ADC/keV images. The colour bar that used to sit at the right of the +image is replaced by a thin **scale panel** under it (its histogram strip is +~38 px, about a tenth of the window): + +* the **histogram** of the last image, one solid bar per bin - the bar is as wide + as the bin spacing - with the mapped range highlighted; +* the two range **markers are pinned at 1/4 and 3/4** and the value axis + rescales under them (the strip shows the selected range widened by half a + range on each side), so **dragging a marker** moves that end of the range; the + ends are printed next to the markers and in the spin boxes; +* a **palette** chooser with the matplotlib family that is useful for detector + images: the original `Col (jet)` / `BW grey 1` / `BW grey 2` (also on `b` / + `Shift+B` / `c`), `Turbo`, the perceptually uniform `Viridis`, `Plasma`, + `Inferno`, `Magma` and `Cividis` (colour-blind friendly), `Hot`, + `Grey (linear)`, `Grey reversed`, the diverging `Coolwarm`, `BWR`, `Seismic` + and `RdBu` (handy for pedestal-corrected data that goes negative), + `YlGnBu (Brewer)` and `Sunset (Tol)`. Pick one per window; the ROOT plots + follow the `b` / `Shift+B` / `c` keys as before; +* **two lin/log switches** - `image` for the intensity mapping (log maps + log10(value) and caps values <= 0 at 1e-2 so empty pixels stay visible) and + `histo` for the bar heights (log10(count + 1)); the histogram's *value* axis + always follows the image switch; +* `min` / `max` spin boxes for the same pair as `Set Ranges...`. Pixels with **no photons are drawn +white** - the way ROOT's `colz` leaves empty cells on the white canvas - while +every pixel above zero takes the palette colour, so a faint map stays readable +instead of drowning in the palette's lowest colour. The colour bar keeps the +palette itself (dark blue at the bottom), exactly as the ROOT plot's did; the +ADC and gain images still map zero to the palette's first colour. +| Reset Axis | `R` | unzoom everything: the ADC and gain images *and* the ROOT plots | +| ROI only | `i` | capture the ADC zoom as the gate ROI (see below) | +| Save ROI (ROI panel) | `j` | add the active gate ROI to the ROI list | +| Unset ROI (ROI panel) | | forget the current ROI: gate, ROI spectra and the saved data go back to the full frame (the ROI list on disk is untouched) | +| Load ROI... (ROI panel) | `k` | pick a ROI from the list and apply it | +| keV PH (Gain panel) | `e` | show the PH spectra in keV (see below) | +| Start/Stop save | `n` | write gate-passed ROI frames to HDF5 (see below) | +| Acquire/Save/Read/Load Pede | `p` / `O` / `o` / `P` | pedestals | +| Quit | `q` | | + +The `Surf2Z`, `ColZ` and `G1+/G1-/G2+/G2-` buttons (and their keys `u`, `U`, +`C`, `[`, `]`, `{`, `}`) were removed: in the standard build the surf/colz +commands only changed a histogram that is never drawn, and the gain offsets are +constant 0 until the AFC/gain-split work lands. + +The three palette buttons (`BW Pal.`, `BW Pal. 2`, `Col Pal`) sit together in +one row of the Display group. + +**ROI panel** (below the Display). Everything about *which* ROI the gate works +on lives here: + +| control | what it does | +|---|---| +| `Select ROI (ADC zoom)` | makes the region currently zoomed in the ADC image the **current ROI** | +| `Save ROI` (`j`) | adds the current ROI to `./roi_list.dat` | +| `Load ROI...` (`k`) | picks one from that list (the dialog can also delete) and makes it current | + +Selecting and loading only *choose* the ROI - they do not restrict anything. The +read-out underneath shows the current ROI, whether the gate is restricted to it, +and how many are saved. + +Next to the signal gate there is still `ROI only (i)`, which switches the +restriction on: the gate then counts only inside **the current ROI**, whatever it +is at that moment (re-selecting or loading another one while it is on takes +effect immediately). Pressing it with no ROI selected pops a warning and leaves +the button unselected; switching it off goes back to the full frame but keeps the +current ROI. + +**Gain panel.** Holds `gain correction on/off (g)` - a checkable button - the +read-only `gain corrected` tick next to it, and the gain map used for the keV +spectra (see below). The tick only *reports* the state (the button is the only +control); it is greyed together with the panel while the correction is off. + +`gain correction on/off` pushed means *divide every pixel by its ADU/keV*: the +per-pixel +value from the gain map when one is loaded, or the **nominal 40 ADU/keV** for +all pixels otherwise. That is what makes the keV spectra physical - with a +320 ADU pixel, the gain correction gives 8 keV either way, but the map corrects for the +pixel-to-pixel spread (30-55 ADU/keV in the lab file) while the nominal treats +every pixel alike. + +The label under the row says exactly which calibration is in use: the gain-map +file name when one is loaded, `no gain map loaded : using defaults` when the +correction runs on the nominal 40 ADU/keV, and `gain off: PH spectra in ADU` +when the stage is bypassed. + +`keV PH` then chooses what the PH canvases show: the **keV** spectra when it is +ticked *and* the gain correction is on, the **raw pedestal-subtracted ADC** +otherwise. Pressed state, tick and greyed-out states line up with this: with +the correction off the Gain box, the `keV PH` checkbox and the `gain corrected` +tick go grey (the `keV PH` tick is kept for next time) and the plots show ADC. + +The saved data is unaffected: `Save` always writes pedestal-subtracted ADC +(with the Jungfrau gain-bit conversion), never the display's keV choice. + +**Pedestal tracking.** Pedestals drift slowly, so there is a `Pedestal +tracking` panel of its own, directly under `Pedestal`, with a `pedestal +tracking on/off` toggle (also `--pede-tracking`); a small up-arrow shows that +its result goes back into the Pedestal box. The box greys while tracking +cannot be *selected* - i.e. while the signal gate is off - not while tracking is +simply inactive: whether it is on is shown by the value label under the button +(`off`, or `on: N samples, delta D ADU, U updates`). It only makes sense with +the signal gate on - that is the button's enable condition, and turning the gate +off turns the tracking off - because it works from the frames the gate *drops*, +i.e. while the beam is off. The rules: + +| step | default | +|---|---| +| feed the average only while the signal gate drops | > 90 % of the frames reaching it | +| sample of every dropped frame | 0.2 % of its pixels, with the stride phase **rotated** each frame so every pixel is covered within 500 frames | +| per-pixel running average, pooled window | 10 M pixel samples (~19 updates/pixel, weight `NCH/10M` per sample, ~6 min at 25 fps) | +| replace the pedestal in use when the largest pixel difference **inside the gate ROI** exceeds | 5 ADU | +| rate limits | at most once per 10 s, at least once per 10 min | +| pedestal tracking image in the save file | every 10 min, while tracking is on | + +Sampling happens on every dropped frame; the (ROI-wide) distance test is the only +expensive part and runs about once a second. The 5 ADU test looks only at the +**gate ROI** - drift outside it is not worth +acting on - while the average itself is still accumulated over the whole frame. +With no ROI selected the whole frame is used. High-gain / gain-3 pixels are +skipped when feeding the average (they live on a different scale), and the +pedestal is replaced in memory only - the file you +loaded is untouched, and loading a pedestal (or `Acquire Pede`) re-primes the +average from it. The panel read-out shows `samples`, the current `delta` and +the number of `updates`, the `Pedestal:` line in Status gains a `[tracked]` tag, +and `--stats` carries `ptrack=off|on|on-upd`. Each update logs + +``` +[pedetrack] pedestal updated #3: max delta 7.41 ADU in the ROI over 152340 pixel samples (drift > 5 ADU) +``` + +While tracking is on, the save file gets a time series: `/pedestal tracking` +(float32, one 512x1024 image per 10 min, in the ROI shape) with the matching +`/pedestal tracking frame`, so the drift during a run is recoverable. The two +datasets are created (empty) for every save because SWMR mode cannot create +objects after it starts; a baseline image is always written when a save starts, +and further images only while tracking is on. + +In `Pedestal corrected` mode the saved frames follow the tracker too: every +frame is corrected with the pedestal **in use when it was captured**, exactly +like the display and the gate, so a live-tracked pedestal is in the file and +not just in the display. A save that spans several pedestal updates therefore +uses several pedestals, documented by `/pedestal tracking` and +`/pedestal tracking frame`; the single `/pedestal data` image is the pedestal at +save start (the baseline), not a description of every slice. + +**Pede/Raw.** The Pedestal panel has the `Pede/Raw toggle` (equivalently +`--no-pede` / `--use-pede` on the command line), and raw ADC is the **startup +default** - the button starts pressed; `--use-pede` starts with subtraction +enabled instead. Raw mode +ignores the loaded pedestal *everywhere* - +display, photon map, spectra, event finder and the ROI save all work on raw ADC +- and the Pedestal panel, the *Virtual photon counting* box (photon counting +thresholds the pedestal-subtracted ADC, so the map goes empty with it) and the +flow box grey out. The state is recorded in +the saved file's `gate` JSON as `"no_pede"`, and the panel says `pede subtracted` +/ `no pede: raw ADC` next to the button. + +Note what that does to the **signal gate**: with no pedestal in play it compares +the threshold against the raw 14-bit value, `frame[i] & 0x3fff` (and skips the +gain-bit scaling), so the threshold is in raw ADC units - the gate row label +changes from `Threshold (ADC, pede-subtracted)` to +`Threshold (raw ADC - no pede)` to make that visible. The same happens with no +pedestal file loaded at all, since that is the same code path. Measured with a +200 ADU pedestal, a 120 ADU threshold and halves at 3000/200 ADU: 262144 +pixels/frame with the pedestal, 524288 with `--no-pede` (the 200 ADU half stops +being pushed below the threshold) - i.e. the raw half really is counted. (The gain-1/gain-3 +pedestal offsets still have no controls; the panel is the natural home for them +if the gain-split work needs them back.) + +**Display cadence.** The ADC/Gain image windows are refreshed on the frames the +gate passes, decimated by the `Faster (+)` / `Slower (-)` counter (default every +20). The counter alone would freeze the image when the gate passes rarely - at +0.1 Hz and `every=20` that is one image every 200 s - so a frame is also +displayed when the previous one is older than `DISPLAY_FLOOR_MS` (250 ms). +Under that floor every passed frame is shown, and above it the decimation is +untouched: at a 2 Hz pass rate `shown/pass = 1.00`, at 200 Hz `shown/pass = 0.05` +(1/20). The floor only takes over when `pass rate / every` drops below 4 Hz. +`--stats` reports `shown=`, the images actually handed to the view, next to +`pass=` so the ratio is auditable. The spectra, the photon map and the ROOT +repaint timer are unaffected by this: they follow every passed frame. + +**Ranges.** `Set Ranges (S)` sets the axes explicitly instead of letting ROOT +auto-scale them. It shows the ranges that are **in force right now** - read +back from the displayed histograms, not from stored numbers - so it opens on +what you are looking at (including a scale you dragged on the ROOT canvas), and +**nothing is applied unless you edit it**: + +| field | applies to | default | +|---|---|---| +| ADC min / max | the colour scale of the **ADC image** window - the Qt "main plot", also draggable on its colour bar | -200 / 1000 | +| P.Map min / max | the colour scale of the ROOT 2D map (canvas `A5`) | 0 / 256 | +| PH min / max | the shared x window of the spectra (`A4` and the ROI spectrum `A7`) | -200 / 3000 | + +One pair per display, and **each field opens on the scale that display is +using right now** (read back from the widget / the histogram, so it also picks +up a drag on the image colour bar or on the ROOT canvas). Only the values you +actually edit are applied, and each goes to its own display: the ADC pair is a +Qt widget and repaints immediately, the ROOT pair is applied once by the ROOT +thread. Because it is applied once, a scale you then change on the ROOT canvas +is not reset by the next redraw (`TH1::Reset()` does not touch it either). +`Reset Axis (R)` releases the fixed ranges and the ROOT axes auto-scale again; +a double-click on the ADC image colour bar restores its last applied ADC range. + +ROOT snaps a requested window to the nearest bin edges, which is why the +`-200 / 3000` default reads back as `-212 / 3010` on the 500-bin spectrum axis. +`JF_TIMING=1` prints the ranges actually in force on exit. + +**The control window is a flow chart.** The left column is the gate flow (1. +Storage cell, then 2. Signal gate), the right column carries the rest of the +chain (Pedestal, then Gain, then Display - the display shows the result, so it +comes after the processing), the ROI-save panel spans the full width under both, +and Status (left) and Counters (right) sit across the bottom. + +The arrows are **static** - the chain is always drawn the same way - and a +stage that is not in use is **greyed out** (title, text and background), so the +bypass is obvious without anything moving. Greyed means: + +| panel | greyed while | its own switch (kept readable) | +|---|---|---| +| 1. Storage cell | the filter is `ALL` (no cell gating) | the cell combo | +| 2. Signal gate | `Gate enabled` is off (passthrough) | `Gate enabled` | +| Pedestal | no pedestal is loaded, or `Pede/Raw` is on raw | `Pede/Raw toggle` | +| Pedestal tracking | the signal gate is off, so it cannot be selected (inactive-but-selectable stays black; the value label below shows on/off) | `pedestal tracking on/off` | +| Gain | the gain correction is off | `gain correction on/off` | +| Virtual photon counting | no pedestal in play (the map needs `g0`) | - | +| ROI | never (the ROI list is independent of the gate) | - | + +Greying is a **look, not an enablement**: a greyed panel is still clickable, and +the control that switches the stage back on keeps its normal text colour (only +the box, its title and the other contents dim), so a bypassed stage never looks +stuck. The `keV PH` checkbox is the deliberate exception: it is disabled *and* +greyed while the gain is off (its tick is remembered for next time). + +``` +1. Storage cell --+ Pedestal + | ^ | + (down) +--> Pedestal --+ | +--> Pedestal tracking + | | (drops feed the average) + (raw: bypass) | v + +----------> [funnel] <-- 2. Signal gate (the gate CONTROLS + | | the funnel; it does + | | not modify data) + | +--> Gain --+--> Display + | | ^ ^ + | | | | + | Virtual photon counting -----+ (its own window) + | ^ + | +-- calibration: the threshold is keV + | while the gain is on + | + | +----> ROI --> Save +``` + +The gate is drawn as a **funnel on the line**, not as a stage the flow enters and +leaves: nothing goes in or out of the gate box, and a short blue arrow from the +gate panel into the funnel's side shows what does the gating. With the gate off +the funnel and that arrow grey out. Two feeders reach the funnel, and they do +**not** have to agree, because the display and the file can use the pedestal +differently: + +* the **display** chain uses the pedestal when `Pede/Raw` is on "pede subtracted" + and one is loaded (the same condition that greys the Pedestal panel); +* the **saved** data uses it when `virtual photon counting` is the save mode (the + maps always need `g0`) **or** when `save frames` is selected with + `Pedestal corrected` ticked. So in the default `save frames` + raw setting the + file takes the *bypass* straight from the storage cell while a corrected + display still goes through the Pedestal. + +Both feeders can therefore be live at once, and the colour says what each one +carries: + +| colour | meaning | +|---|---| +| **orange** | everything the saved data travels through, entry point to the Save panel: `save frames` = storage cell -> (Pedestal or bypass) -> funnel -> Gain -> ROI -> Save; `virtual photon counting` = ... -> funnel -> Gain -> counting -> Save. The ROI is only there for the file, so its input is always orange too - from the **Gain** when frames are written, from the **counting** when the maps are (they are cropped to it on the way out) - and the arrow into the Save panel always leaves the ROI, never the counting panel | +| **light blue** | the branches that only feed the display: storage cell -> Pedestal and Pedestal -> funnel while the file bypasses them, Gain -> Display, Gain -> counting, counting -> Display, and Gain -> ROI when the maps are the saved product | +| **lighter blue** | the pedestal tracking pair only: the gate block -> tracking input and the tracking -> Pedestal feedback | +| **pale grey** | the gate funnel while the gate is off (the symbol greys instead of vanishing) | + +Nothing is drawn greyed-out any more: a feeder that carries nothing is simply not +there, and the Save arrow of the mode that is *not* selected is not there either. +The tracking input leaves the **top-right corner of the gate block**. (The flow +strip's tooltip repeats the legend.) + +The `Pedestal tracking` panel sits directly under `Pedestal` with a small `^` +arrow back up into it: it is fed by the frames the *signal gate drops* (the `-->` +from the gate) and its result is written back into the Pedestal box above it. + +Those connectors are drawn (orthogonal lines with a filled head) in the narrow +strip between the two columns, which is why the boxes are padded to line up. +The two remaining vertical arrows (Gain -> Display, ROI -> Save) are arrow +glyphs in the layout; the gap between the two gates is plain empty space, since +the path between them goes through the Pedestal and is drawn by the strip. +Each connector turns near the box it enters, so their vertical parts never line +up into one long line down the strip. Nothing +moves or flickers: the geometry is solved once at start-up (and again on +resize) and the bypass is shown only by greying the stage. + +`Quit (q)` sits at the top right, out of the gate and arrow area, the ROI-save +panel spans the full width above the Status/Counters strip, and the boxes are +lined up so the flow is readable: the middle of the storage-cell +box sits on the top edge of the Pedestal, and the Gain starts at the middle of +the signal gate (both measured and padded at start-up, so it survives a +different font or DPI). + +The event finder panel holds two separate gates: + +| panel | what it keeps | cost | +|---|---|---| +| **1. Storage cell (header)** | only frames acquired in the selected storage cell (dropdown `ALL` or `0`..`15`) | a header field - filtered frames touch no pixel at all | +| **2. Signal gate (pixel counting)** | only frames with `min_pixels` pixels above the threshold (and, optionally, clustered inside the gate ROI) | the full-frame scan | + +The storage cell is **bits 8-11 of `detector specific 3`** in the frame header +(0..15, 15 = the default cell), so the signal gate and the analysis only ever +see frames that already survived it - selecting a cell cuts the CPU load to +roughly 1/16 with a uniformly cycling acquisition. Each gate can be off +independently: `ALL` switches the storage-cell gate off, the `Gate enabled` +checkbox switches the signal gate off, and the four combinations are just those +two controls (no gating, cell only, signal only, both). + +`JF_TIMING=1` (or the GUI) reports the cell of the last frame seen +("last frame: SC n"), which is the quickest way to confirm the decoding on a +new setup; `scdrop` counts the frames the cell gate rejected +(`frames passed + frames dropped + frames dropped (SC) = frames in`). +`--sc N` (`N` = 0..15, or `all`) selects the cell at start-up. + +**Gate ROI.** `ROI only (i)` sits next to "Gate enabled" and is a latching +button. Pressing it captures the region currently shown in the ADC image and +restricts the event finder to it: from then on only pixels inside that +rectangle are counted for the gate (threshold, min-pixels and the contiguity +test all use it). The rectangle is a **snapshot** - panning or zooming the ADC +image afterwards does not move it, so you can inspect other areas while the +gate keeps looking at the same place. It is outlined with a thin yellow box on +the ADC image, the captured size/position is shown next to the button, and +`--stats` reports it as `roi=WxH@x,y` (`roi=off` when inactive). Pressing the +button again releases it (full frame). Capturing a new ROI is un-toggle then +toggle. The receiver's own ROI (from the stream header) is a separate **red** +box on the same image - see "One coordinate system" below. + +Saved ROIs live in **one list, backed by one small file** in the program +folder (`./roi_list.dat`, override with `--roi-file PATH`). `Save ROI` adds the +active rectangle (or refreshes its timestamp if it is already there), `Load +ROI...` shows the list - **most recently saved or loaded first** - with the X/Y +coordinates, width, height and when it was last used, and the chosen entry is +applied and moves to the top. The dialog also has a `Delete` button. The list +is capped at 200 entries, dropping the least recently used, so the file stays a +few kB indefinitely, and it is rewritten on every change (a crash cannot lose +it). + +A ROI can also be set before the program is started, which is handy for +scripts and for headless runs: + +``` +./jf_online_display_gui 1000 30001+30002 --gate-roi 446 114 20 11 +``` + +### One coordinate system, image and ROOT + +The operator only ever thinks in **data coordinates**: `x` grows to the right, +`y` grows **upwards**, and **(0,0) is the bottom-left pixel** of the module. +Every part of the program uses them: + +| | convention | +|---|---| +| ADC/gain image | draws data row 0 at the *bottom* of the widget (the image is flipped once when it is turned into a `QImage`) | +| pixel read-out (hover) | reports data coordinates, so the number matches the plots | +| gate ROI, ROI list, `--gate-roi`, HDF5 crop | data coordinates, row 0 at the bottom | +| ROOT photon map / spectra | data coordinates, so y=`0` is at the bottom of the plot | + +The two places where the flip has to be applied are the ADC overlay (data -> +screen) and the zoom that is forwarded to ROOT (screen -> data); both are done +in one place each, so the ADC box, the ROOT box and the gate rectangle are +always the *same* rectangle. A bottom ROI in the ADC image is a bottom box in +ROOT. + +**Receiver ROI (red box).** Independent of the gate ROI, the receiver puts its +own ROI in every stream header as `"rx_roi": [xmin, xmax, ymin, ymax]` in raw +port coordinates. It is drawn as a **red** box on the ADC image, next to the +yellow gate ROI. It is parsed from `rx_roi`; the old `detSpec1` +reinterpretation was a hack (`detSpec1` is the bunch id) and is gone. + +**The ROI has the shape of the ADC window.** The visible region of the ADC +image takes the shape of the window it is shown in, so resizing the window to +be tall makes the region - and therefore the captured ROI - narrow and tall +(the shorter side grows, so nothing you were looking at disappears; the +whole-frame view is left alone so it still captures the full sensor). ROOT then +draws that window with square pixels, so the yellow box has the same shape it +has on the ADC image instead of being stretched across the canvas: the axis +range is widened (never narrowed) around the ROI until it matches the shape of +the pad frame, which also leaves a margin of context around the box. + +### JFSTRX (LGAD strip-xel) build + +`make jfstrx` (adds `-DJFSTRX`) compiles in the strixel decoder +(`jungfrauLGADStrixelsData_new.h`). Two things change: + +* the **ADC and gain windows stay the raw 1024x512 layout**, exactly as in a + normal build, so the gate-ROI selection and the hover read-out keep working in + raw data coordinates, and the receiver's **red `rx_roi` box** is a plain raw + rectangle; +* a separate window, **"ADC Strixel Panels (JFSTRX)"**, opens with the six ADC + groups arranged like the original ROOT ADC canvas: left column chip1 + g3/g2/g1, right column chip6 g1/g2/g3. It is display only (no ROI, no + receiver box) and shows `raw - pede`, the same `dataMap`/`value` mapping the + original `onlinedisp_zmq` filled its ROOT panels with. + +The panels are drawn with the **rectangular pixel aspect of the strixel +groups**: the mapped pixel is `X = 75*(75/Y)` wide and `Y` tall, with +`Y = 25 / 15 / 18.75 um`, i.e. 9:1, 25:1 and 16:1. `FastImageView` therefore +scales x and y independently for these panels (the ADC and gain windows keep +square pixels). + +The six panels are fed from separate group buffers, not from one tall +`335x1922` image (that geometry is larger than `NCH = 524288`, so writing it +into the raw `adc_image`/`gain_image` overflowed them). The ROOT photon map and +the T-vs-B / L-vs-R scatters also keep the raw 1024x512 layout. + +### keV spectra from a gain map (`keV PH` checkbox) + +The PH spectra can be calibrated to **keV**. Press **`Gain map (keV)...`** in +the **Gain** panel to pick the file (or start with `--gainmap PATH`); the file +is + +``` +6 images of 1024x512 doubles, in this order: + g0, g1, g2, hg0, hg1, hg2 (24 MB) +``` + +which is exactly what a `gainMaps__<...>.bin` from the lab holds. In +normal mode **g0** is the per-pixel gain in **ADU/keV** (the module's file has +30-55 ADU/keV, mean ~42, and hg0 is the high-gain map), so + +``` +keV = pedestal-corrected ADC / g0[pixel] +``` + +**Both sets of spectra are always accumulated** - 8 chip spectra plus the +gate-ROI spectrum, in ADU *and* in keV - but only one of them is on screen, +selected by the **`keV PH`** checkbox next to the gain-map button (loading a map +ticks it for you). The keV view needs the **gain stage active** + (`gain correction on/off`, or `g`): while the gain is off the checkbox keeps its tick for next time but is +greyed, and the plots show ADU: + +| `keV PH` | canvases `A4` (chips) and `A7` (ROI) show | +|---|---| +| unticked | the ADU spectra, x axis in ADU (e.g. -200..3000) | +| ticked, map loaded | the keV spectra, x axis in keV (the same window / 40) | +| ticked, no map | the same, using the **nominal 40 ADU/keV**; the panel says `no gain map selected - using nominal values (40 ADU/keV)` | + +With the nominal constant the keV set is the ADU set divided by exactly 40, so +the two histograms have identical bin contents (the `[root] keV entries` line +reports `kev/adu=1.0000`), which is a quick way to check the conversion is +wired up right. Nothing is converted at all while the gain is off (the Gain +panel then says `gain off: PH spectra in ADU`), so switching the gain back on +resumes the keV spectra exactly where the tick left them. + +It is the *same two canvas windows*, redrawn from the other set - so nothing +goes stale, and the pad titles/stat boxes name the histogram in use (`hchip0` +vs `hchip0_keV`, `hroi` vs `hroi_keV`), which is how you can tell which one you +are looking at. + +The keV axes are the ADU axes divided by the nominal **40 ADU/keV**, so a PH +window from `Set Ranges` covers the same physical region in both sets +(`-200..3000 ADU` = `-5..75 keV`), the bin indices are identical, and switching +does not move the window. Both sets are filled from the same pixels with the +same border trim, so their entry counts are identical and the keV mean is the +ADU mean divided by the gain; with a g0 of 40 the ratio is exactly 40. + +Loading a gain map is optional and only the PH spectra are affected: the ADC +image, the photon map and the HDF5 writer keep working in ADU. Without a map +`--stats` reports `gainmap=off`, the checkbox stays disabled and the keV +accumulators are never touched (`gainmap=loaded` and matching entries once it +is there). The two spectrum canvases are painted one per ROOT tick, only the +selected set, so lowering "Chip spectra every N redraws" in *Update Rates* +refreshes them more often. + +### ROI spectrum (canvas `A7`) + +`A7` holds **one** spectrum of the gate-ROI pixels: same binning, same border +trim and the same x-axis as the chip spectra, but summed over the whole +selection instead of split per chip, and drawn with the normal ROOT histogram +rendering (the chip pads use a polyline instead, because painting eight pads +with the default costs ~1.7 s per redraw). `A9` is the same spectrum in keV +once a gain map is loaded, see below. Its entries are therefore directly +comparable with the *sum* of the eight pads of `A4` (equal when the ROI covers +the full frame). When no ROI is set it stays empty - there is nothing selected +- and the window title always names the rectangle it was filled with (or says +that there is no ROI). A ROI that is merely *selected* fills it; `ROI only` is +a separate control and only decides whether the gate is restricted to the ROI. + +**Palettes are shared.** One palette definition per palette feeds both the ROOT +gradient colour table and the 256-entry lookup table of the fast ADC widget, so +the photon map and the ADC image always show the same colours. `b`/`B` switch +both to the monochrome ramps, `c` switches both back to colour; the gain image +keeps its own fixed 0..4 range colouring. + +**Zoom is master/slave.** The ADC image is the master: dragging (live, not only +on release) or wheel-zooming it moves the gain image viewport immediately and +repaints the ROOT photon map (converted to data rows on the way, see above). `Reset Axis` (or double-clicking the image) +undoes it everywhere at once; a degenerate viewport is never pushed to ROOT, so +pressing it before the first frame is harmless. The queued zoom commands are *coalesced*, so a +continuous drag never floods the ROOT thread - only the newest viewport is +drawn. Double-click resets the view (and the photon map with it). Zooming *out* +keeps growing both directions until the whole frame is shown (the +window-shape matching that keeps the captured ROI in the window shape applies +while zooming in and on resize, but it must not stop a zoom-out in the longer +direction). + + + +* ROOT graphics only ever run on the ROOT thread; other threads post `RootCmd` + entries (`ResetAxes`, `Surf`, `Colz`, palettes, zoom, chip range). +* Source histograms are touched only by the analysis thread; the ROOT thread + paints clones whose bins are copied once per redraw. `copyBins()` is + deliberately lock-free (the originals accepted a torn read during a redraw). + **"Reset His" is performed by the analysis thread** at the top of its next + frame, where `gHistoMtx` is already held (with a non-blocking `try_lock` + fallback on the ROOT thread when the stream is idle). The ROOT thread must + never *wait* for `gHistoMtx`: under a heavy stream it could starve, which made + the reset appear to take tens of seconds. Commands and forced redraws wake the + ROOT thread immediately, so they do not wait for the `ROOT update (ms)` tick. +* `T vs B` / `L vs R` stay windowed per redraw. The photon map is no longer a + ROOT histogram (it has its own Qt window, see *Virtual photon counting*); + "Reset His" clears the spectra and the map accumulator alike. +* ADC zoom propagates to the gain view and, through the coalesced command, to + the photon-map window. + +## ROOT canvas responsiveness + +The ROOT thread owns every canvas and also runs the ROOT event loop, so a slow +redraw makes the plots feel dead. `JF_TIMING=1` prints what the thread spends +per redraw: + +``` +[root] loops=15 redraws=14 | per-redraw us: copy=5726 plot2d=345119 + plot1d=216270 | per-loop us: cmd=0 pe=248 +``` + +Two things dominated and are fixed: + +* the chip spectra were drawn with the ROOT default (a marker plus error bars + per bin) - about 2000 X11 operations per pad, 8 pads, which cost **1.7 s** + per redraw and starved `ProcessEvents` for ~2 s at a time. They are now + drawn as one polyline per pad (`Draw("L")`), which is **0.22 s**, and can be + redrawn every N-th redraw (`Update Rates...` -> "Chip spectra every N + redraws", default 4) instead of every second. +* `ProcessEvents` now runs *inside* a redraw (between the 2D canvases and + between chip pads), so the canvases stay interactive even while a redraw is + in progress rather than only between redraws. + +The photon map is a Qt window now, so it costs one `QImage` fill per publish +(no ROOT redraw at all): the ROOT thread no longer pays for it, and the map can +be published on its own cadence without slowing the canvases down. Raise the +ROOT redraw period in `Update Rates...` if the remaining canvases are still too +expensive, and note that over X11 forwarding each canvas repaint is a network +round trip, so the absolute numbers there will be larger. + +A continuous zoom drag no longer queues a repaint per mouse move either: the +axis ranges are updated for every event but the expensive `colz` repaint is +skipped while a newer viewport is already queued. + +## Saving gate-passed ROI frames (HDF5) + +`Save` in the control window writes to one HDF5 file (plus a JSON sidecar) +either the **active gate ROI** of every frame that passes the gate, or the +**virtual photon counting maps**. A gate ROI is *not* required: with no ROI +selected the full frame is saved (the ROI panel says +`no ROI selected | gate: full frame`). **The ROI applies to both modes**: a +saved map is the gate-ROI window of the full-frame map, cropped in the same +data coordinates as `/data` (same `x`/`y`/`w`/`h`), so a ROI of 400x280 gives +`/virtual photon counting` the shape `{nmaps, 280, 400}`; with no ROI the maps +are `{nmaps, 512, 1024}`. That is a write-side crop - the counting itself is +always full-frame. + +* `save frames` / `virtual photon counting` - what the run writes. The mode is + recorded in the JSON (`save_mode`). +* `Number of frames to be saved` - the run stops by itself once that many + frames are in the file (`0` = unlimited, stop with the button). In virtual + photon counting mode the label changes to *Number of photon maps to be saved* + and the same number budgets the **maps**. +* `photon map every N frames` (virtual photon counting only, minimum **200**) - + how many analysed frames go into one saved map. Independent of the display + cadence in the *Virtual photon counting* panel: the display can run every 200 + frames while the file takes one map every 2000, or the other way round. The + maps are never cumulative in the file, and each one stores the frame number + and timestamp of the **first gate-passed frame** of its window, so a map can + be matched to the frames recorded around it. +* **A pedestal is required.** Photon counting thresholds the + *pedestal-subtracted* ADC, so virtual photon counting **refuses to start** + without an active pedestal (`No pede` on, or no pedestal file): the button + drops back and the status line says + `virtual photon counting needs a pedestal (...)`, instead of writing a file + full of zeros. Load or acquire a pedestal and clear `No pede`, or save + frames. (The JSON records `photon_maps.pedestal_active`, `true` for every + map that does get written.) +* `Directory` / `Base name` - where the files go and what they are called; + `...` browses for the directory. The file is + `/__f0_0.h5` (and the JSON the same with + `.json`), so re-using a base never overwrites an earlier run, and the + directory is created if needed. +* `Previous destinations...` - the list of directories and bases used before, + **most recent first**, with `Use` and `Delete`. A destination is recorded + (and moved to the top) every time a save actually starts, **except** when it is + still the start-up default (`--h5-dir`/`--h5-base`, i.e. `./` + `jfroi` with + no options): the default is not a "previous" destination and would only + clutter the list on every run. The list is one + small file in the program folder (`./save_dest_list.dat`), capped at 100 + entries. +* `Pedestal corrected` - sits under the `save frames` radio, because it only + applies to that mode: the photon maps are always pedestal-subtracted. It is + greyed while `virtual photon counting` is selected. Off by default (raw + data). With it on, `/data` + contains pedestal-corrected **float32** values and no pedestal slice, and the + pedestal image goes to the separate `/pedestal data` dataset instead. It + needs a loaded pedestal (the run refuses to start without one). Each frame is + corrected with the pedestal in force when it was captured, so a live-tracked + pedestal is followed (see "Pedestal tracking" above). +* `Start/Stop save (n)` latches; it also releases when the budget is reached, and + the status line shows the file, how many frames were saved and how many were + dropped. Stopping flushes the queue instead of discarding it. +* **Periodic checkpoint** (`checkpointMs`, default 30 s; 0 disables): while a + save runs, the writer shrinks the over-allocated datasets back to the frames + actually written, flushes the HDF5, and rewrites the JSON sidecar atomically + (`.part` -> rename, see below). Every checkpoint boundary therefore leaves a + self-consistent pair on disk, so the run can be checked without stopping it, + and a crash loses at most one interval. + +The status line spells out what the counters mean: +`N frames written, M dropped (nothing lost)` is the healthy case, and +`... M dropped (writer behind: queue full)` means the writer could not take M +frames/maps because its queue was full (at most **4096 items or 256 MB** are +buffered) - i.e. the disk was slower than the incoming stream. The note only +appears when something was actually dropped; the gate counters keep counting +those frames, they are simply not in the file. + +**A running save locks the settings that would change the file.** While the +writer runs, the save panel (except `Start/Stop save`), the storage-cell filter, +the manual pedestal work, the gain correction and the ROI buttons are disabled - +their JSON sidecar describes the settings at save start, so a quiet change +halfway would make it a lie. The *display* controls stay live (palettes, ranges, +update rates, `Reset His/Axis`, `keV PH`, the cumulative switch and the map +cadence), and the pedestal tracker keeps following drift as it should. The +gate's **threshold** and **min pixels** also stay live, but while **`Gate +enabled` is ticked** changing either one asks first (`Changing settings during a +run, are you sure?`); if you decline, the old value is put back, and if you +accept, the change is applied *and* the file's JSON gets +`gate_settings_modified_during_run: true` plus a `gate_settings_note` saying the +`gate` block is the criterion from save start. With the gate off (passthrough) +those two decide nothing about the saved frames, so they change silently and +nothing is added to the JSON. + +**Quitting asks first, and flushes a running save.** `Quit (q)`, closing the +window, or the shortcut all raise a confirmation; if a ROI save is still +running the dialog says how many frames are written and where, and states that +it will be flushed. Only *Yes* quits: the writer drains its queue, closes the +HDF5 file (extending it to exactly the frames written), writes the JSON sidecar +with `stop_reason = "program exit"`, and only then do the threads stop. +**Every window goes with it**: the ADC, gain and photon-map windows are +separate top-levels and the ROOT canvases live in the ROOT thread, so quitting +closes them explicitly - closing only the control window used to leave them (and +the process) behind. `No` +(or `Escape`) leaves everything running untouched. `Ctrl-C`/`SIGTERM` cannot +ask (a signal is not a dialog), so they flush the save and exit; a second +`Ctrl-C` quits immediately. + +Each mode ends by itself when its budget is reached (`stop_reason` +`frame budget reached` / `photon-map budget reached`), leaving a closed, +`complete: true` file and JSON behind - the useful way to run a save +unattended. `--save-budget N` sets the budget without the GUI. + +Saves can be run back to back: a save that ends by itself (frame budget +reached, `Start/Stop save` pressed again, or an I/O error) leaves a finished writer +thread behind, and the next start joins it before spawning a new one. (A +`std::thread` that is still joinable may not be overwritten - doing so calls +`std::terminate`, which is what aborted the program the second time a file was +started.) The acq, analysis, worker and ROOT threads are also wrapped so that +an exception is reported and stops that stage instead of calling +`std::terminate`; the GUI keeps running and shows the message in "Last error". + +### File format (slsReceiver-compatible) + +The layout is the one written by `slsReceiverSoftware/src/HDF5DataFile.cpp`, so +existing receiver readers work unchanged. The file is **v110 format** (SWMR, see +"Crash recovery and mid-run inspection"), so readers need HDF5 >= 1.10: + +``` +/jfroi__f0_0.h5 + /data rank 3, U16LE, {nframes, h, w}, + unlimited, chunk {1, h, w} + /frame number U64, one value per frame + /exp length or sub exposure time U32 + /packets caught U32 + /detector specific 1 U64 + /timestamp U64 + /mod id, /row, /column U16 + /detector specific 2 U16 + /detector specific 3 U32 + /detector specific 4 U16 + /detector type, /detector header version U8 + /packets caught bit mask fixed size string (zero here) + /pedestal data float32, corrected mode only + /pedestal tracking float32, {n, h, w} (empty unless tracked) + /pedestal tracking frame U64, one value per tracked image + /virtual photon counting float32, {nmaps, h, w}, photon mode only + /virtual photon counting frame U64, first frame number of each map + /virtual photon counting timestamp U64, its header timestamp + attribute "version" = 7.0 (as the receiver writes it) + attribute "roi_x/roi_y/roi_w/roi_h", "pedestal_in_frame_0", "gate" +``` + +The parameter datasets are filled from the streamed JSON header (frame number, +exp length, packets caught, detSpec1..4, timestamp, mod id, row, column, +detector type/version), so a reader keys on `/frame number` as usual. The +packets-caught bit mask is not part of the ZMQ header and is written zero. + +Deliberate differences from a receiver file: + +* `/data` is the **ROI crop**, not the whole module; +* the file is **v110 format** and written in SWMR mode, so it can be read while + it is being written and stays valid if the writer crashes (readers need + HDF5 >= 1.10); +* `/pedestal tracking` and `/pedestal tracking frame` are always present (empty + when tracking is off) because SWMR cannot create objects after it starts; +* in **virtual photon counting** mode the frames are not written at all: `/data` + keeps only its slice 0 (the pedestal) and the maps go to + `/virtual photon counting`, with the provenance of each map in + `/virtual photon counting frame` (first frame number of its window) and + `/virtual photon counting timestamp`. The maps are photon counts per pixel, + float32, never cumulative, and cropped to the gate ROI exactly like `/data`; +* `Pedestal corrected` mode changes `/data` to float32 and drops the slice-0 + pedestal, so the layout then is `/data` (n frames), `/pedestal data` + (one image, float32) and the same parameter datasets; +* only **gate-passed** frames are written - each keeps its **original frame + number**, so the gaps in `/frame number` are exactly the frames the gate + rejected; +* in raw mode **slice 0 is the gain-0 pedestal in the same ROI** (frame number + 0, flagged by the `pedestal_in_frame_0` attribute), so a reader can subtract + it directly; with no pedestal loaded the slice is zero; +* in corrected mode the correction is the same one the display and the gate + use (g0: `raw - pede0`, g1: `(pede1 + G1off - raw) * 30`, g3: + `(pede2 + G2off - raw) * 340`), written as float32, so negatives and the + gain-normalised scale survive. The pedestal applied is the one in force when + the frame was captured, so pedestal tracking is followed slice by slice; use + `/pedestal tracking` (with `/pedestal tracking frame`) to know which pedestal + each slice saw. `/pedestal data` is the pedestal at save start; +* `/data` slices are the raw 16-bit words as received (ADC in bits 0-13, gain + in bits 14-15), exactly like the receiver; slice 0 is in ADC units; +* extra self-describing root attributes: `roi_x/roi_y/roi_w/roi_h`, + `pedestal_in_frame_0`, `gate`. + +### JSON sidecar + +Same base name with `.json`: ROI, the gate criteria in force, the data +encoding, start time and duration, stop reason, frames saved / dropped / +budget / first and last frame number, the full gate statistics and rates, the +alignment counters and the per-interface state. + +The save mode is spelled out: `save_mode` (`frames` or +`virtual photon counting`), `photon_map_every`, `photon_threshold` and +`photon_threshold_unit` (`ADU` or `keV`), and - only when it happened - +`gate_settings_modified_during_run` with `gate_settings_note`, and in photon mode a `photon_maps` +block with `saved`, `every`, `threshold`, `threshold_unit`, `cumulative: +false`, `pedestal_active` and the dataset name. In that mode +`frames.saved` counts the **maps** (the frames budget is unused), so a +reader does not have to know which counter applies. + +The sidecar is rewritten at every checkpoint, not only at the end. Each rewrite +fills `..._f0_0.json.part`, fsyncs it and renames it over the `.json`, so +a reader always sees a complete document - the previous one until the rename - +and a crash mid-write leaves only a stale `.part` behind. While the save runs +the document carries `"complete": false`; the final one, written when the writer +stops, carries `"complete": true`. The statistics are refreshed by the writer +thread just before each write, so the final document holds the true end-of-run +values. + +### Crash recovery and mid-run inspection + +The HDF5 is written in **SWMR** mode (Single Writer / Multiple Reader). The file +is created with `libver = latest` (v110 format), all objects are created up +front, and `H5Fstart_swmr_write` is called before the first frame. Every +checkpoint then shrinks the datasets to the frames actually written, flushes, +and rewrites the JSON atomically. + +What that buys: + +* **Open it mid-run.** A reader can open the live file with the SWMR read flag + while the save continues, e.g. `h5py.File(path, "r", libver="latest", + swmr=True)`. HDF5 guarantees a valid, consistent view at every flush; without + SWMR a second open would be refused by the file lock. +* **Crash recovery.** SWMR guarantees the file stays valid even if the writer + dies without closing it, so the last flushed checkpoint is readable. The JSON + sidecar carries the matching `frames.saved` / `last_frame_number`. +* **The JSON is never torn** - it is published with a single `.part` -> rename, + so a reader sees the previous complete document until the new one is ready. + +Trade-offs: + +* v110 files need HDF5 >= 1.10 to read (finished or in progress). Older analysis + installations cannot open them. +* Because SWMR forbids creating objects after it starts, `/pedestal tracking` + and `/pedestal tracking frame` are now created (empty) for every save; the + series is still only filled while tracking is on. +* Tools that do not know about SWMR should read the file after the save has + closed (then it is an ordinary v110 file), or take a copy at a checkpoint + boundary. + +### Throughput + +The acq thread only copies the ROI into a queue (up to 256 MB); a separate +writer thread does the HDF5 I/O. If the gate passes frames faster than the disk +absorbs them, the queue fills and the excess is counted as +`dropped_queue_full` in the JSON and shown in the GUI - the gate counters stay +exact, but those frames are not in the file. + +`--h5-dir DIR` and `--h5-base NAME` set the initial directory and base name +(defaults: the program folder and `jfroi`). + +## Behaviour differences worth knowing + +1. **One pedestal store.** The gate and the display share one immutable + `PedeSet` (shared_ptr swap). File loads and live acquisition publish a new + set, so the old `stop() → loadPedestal() → start()` contract is gone and + loading pedestals never interrupts acquisition. +2. **Gate offsets unified.** The old filter had its own `G1offset/G2offset` + (always 0); the gate now uses the display's live `G1Poffset/G2Poffset`. +3. **Gate threshold vs photon threshold** are separate values in one process + (they only collided as one global named `threshold` because they lived in + two programs). The gate one is an argument or the gate panel and is always + ADU. The photon-map one lives in the *Virtual photon counting* panel and + follows the gain stage: **ADU** while the gain correction is off (default + 150) and **keV** while it is on (default 4.0) - one control, two remembered + values, so the keV spectra and the photon counting can never disagree about + the calibration. +4. **`Acquire Pede` follows the storage-cell filter.** Pedestals depend on the + storage cell, so only frames the SC filter accepts are averaged: with + `--sc 7` (or the drop-down set to 7) a 25-frame acquisition takes frames from + cell 7 only - measured: it completed after 365 incoming frames instead of 25 + - and the log says which cell it used + (`[pede] installed pedestals averaged over 25 frames (storage cell 7)`). + With the filter on `ALL` every cell contributes and the log says + `storage cell ALL - every cell contributed`, which mixes their pedestals: set + the filter when the pedestal is cell-dependent. The *signal* gate is + deliberately not applied here - the frames it drops are exactly the empty + ones a pedestal wants. +5. **Live pedestal acquisition is asynchronous** (`p` / `--acquire-pede`); it + averages the next N frames on the acq thread instead of blocking the GUI + and restarting threads. It fills gain 0 only, so `hasAll` stays false — the + same fallback behaviour as the old `loadpede()`. +5. **Keyboard shortcuts actually work, and upper case needs Shift.** `s`/`Shift+S`, +`b`/`Shift+B`, `r`/`Shift+R`, `o`/`Shift+O` and `p`/`Shift+P` are distinct +(`QKeySequence("s") == QKeySequence("S")` made the plain upper-case forms +ambiguous, so Qt activated neither). The original only *documented* `s`, + `p`, `o`/`O`, `r`/`R`, `+`/`-`, `q`, `u`, `C`, `[`/`]`, `{`/`}`, `g`, `b`, + `B` in the button labels; they are now real `QShortcut`s. +6. **"Load All Pede (P)" is functional** (file chooser); the original printed + *not implemented*. +7. Leaks fixed: the ROI `TBox` is now a single reused box, and the live + pedestal decoder object is freed. +8. `phene == 0` can no longer divide by zero in the photon-counting increment. +9. **The stream state is real.** `jf_filter` treated every 200 ms receive + timeout as "Receiver stopped, waiting for new stream", which fired whenever + the receiver was merely idle. The merged program monitors the ZMQ socket + (`zmq_socket_monitor`) and derives the state from actual connect / + disconnect / handshake events: + + | state | GUI text | `--stats` token | + |---|---|---| + | never connected yet | `waiting for receiver...` | `connecting` | + | peer up, frames arriving | `connected` | `connected` | + | peer up, no frame for > 2 s | `connected (no frames for N s)` | `idle` | + | end-of-acquisition marker seen | `connected (end of acquisition)` | `eoa` | + | peer gone | `disconnected` | `disconnected` | + + stdout lines are emitted only on real transitions: + `Receiver connected` / `Receiver connected but no frames for N s (still alive)` + / `Receiver sent end-of-acquisition, waiting for new stream` / + `Receiver disconnected, waiting to reconnect...`. + If `zmq_socket_monitor` is unavailable the code falls back to frame-arrival + staleness (3 s) instead of failing. + +## Quirks deliberately preserved + +* `factor = 1.84` scaling of the reference rows/columns in the ADC image. +* `fgaind[]` is still never initialised to anything but 0, so the gain + correction path multiplies gain-0 pixels by zero (pre-existing behaviour, + kept as-is). +* `hchptr[]` is still accumulated and never displayed. +* The gate's no-pedestal path still compares raw 14-bit ADC values, while the + display's photon counting and the saved photon maps need a pedestal (`g0`) and + cannot be saved at all in the default `--no-pede` mode: the photon-map save + is refused with an explanation, while the frame save (and the gate's own raw + thresholding) keep working. +* `--no-gate` performs no pixel counting, so `pixelsAbove` stays 0. + +## Testing without hardware + +`test/jf_feeder.cpp` stands in for `slsReceiver`: it publishes the same +`` pairs. Every 3rd frame carries no signal; the +others carry 40 pixels at ADC 300 over a baseline of 100. + +```sh +make feeder +./test/jf_feeder tcp://*:30777 200 20 & +./jf_online_display_gui 400 127.0.0.1 30777 --gate --stats --pede /nonexistent.dat +# gate threshold 200 -> expect pass = 2/3, drop = 1/3, px = pass*40 + +# virtual photon counting save, headless: a zero-filled pedestal makes the +# 40-pixel cluster count (with the real ~2000 ADU pedestal the maps are empty) +/usr/bin/python3.9 -c "import numpy as np; np.zeros(6*1024*512,' "connected but no frames for 2.0 s +# (still alive)" -> frames -> "end-of-acquisition" -> "disconnected" + +# multi-interface check: two 1024x256 slabs on 30777 (bot, value 100) and +# 30778 (top, value 150); gate threshold 120 -> only the top slab counts +./test/jf_feeder dual 30777 200 10 4 100 150 0 & +./jf_online_display_gui 400 127.0.0.1 30777+30778 120 5 --gate --stats --pede /nonexistent.dat +# expected: px = in * 262144, mispaired=0, drop=0 + +# storage-cell gate: the feeder cycles SC = frame % 16 in detSpec3, so a +# selected cell must pass exactly 1/16 of the frames and nothing else +./test/jf_feeder dual 30777 1600 400 1 100 150 0 & +./jf_online_display_gui 1000 127.0.0.1 30777+30778 120 5 --gate --stats \ + --pede /nonexistent.dat --sc 3 +# expected: pass ~= in/16, scdrop ~= 15*in/16, pass + scdrop = in, sc=3 +# (--sc all passes everything; with --no-gate the cell gate still filters) + +# gate-ROI convention + ROI spectra, same feeder (bottom slab dim, top bright): +# a ROI on the TOP half must be the one that passes, and only hchip 4..7 of +# the ROI spectra may be filled; the BOTTOM half must drop everything +JF_TIMING=1 ./jf_online_display_gui 400 127.0.0.1 30777+30778 120 5 --gate --stats \ + --pede /nonexistent.dat --gate-roi 0 256 1024 256 +# expected: px = in * 262144, hroi/hchip = 0.5214 (chips 4..7 of A4) +JF_TIMING=1 ./jf_online_display_gui 400 127.0.0.1 30777+30778 120 5 --gate --stats \ + --pede /nonexistent.dat --gate-roi 0 0 1024 256 +# expected: pass = 0, px = 0 (the dim half is at the bottom) +JF_TIMING=1 ./jf_online_display_gui 400 127.0.0.1 30777+30778 120 5 --gate --stats \ + --pede /nonexistent.dat --gate-roi 0 0 1024 512 +# expected: hroi == the sum of hchip (a full-frame ROI selects every pixel) +``` + +A zero-filled 12 MB file works as a pedestal for these runs (the photon map +and the photon counting need `g0` to be present, so `--pede /nonexistent.dat` +leaves the photon map empty by design). + +Verified end-to-end under `xvfb-run`: gate counts/rates, pixels-above, display +hand-off with zero drops, ROOT redraws, live pedestal acquisition, 12 MB +pedestal file load, optional republish to an external subscriber, +end-of-acquisition handling, connect/idle/disconnect/reconnect reporting, +graceful `SIGINT`/`SIGTERM` shutdown, and for the multi-interface mode: slab +geometry detection, bottom→top placement (and the reversed order), pairing by +frame number, straggler timeout, mispair detection/recovery, a permanently +missing interface running at full rate without freezing, dual-mode republish, +and the live/loaded pedestal paths on the assembled frame. The ROI work was +verified against synthetic frames by counting the pixels per chip (a top-half +ROI gives `hroi/chip = 0.5214`, a bottom-half ROI passes the gate only when the +bottom half is the bright one, a full-frame ROI gives `hroi == sum(hchip)`, and +`hroi/hchip` matches rows x columns exactly), the range defaults were read back +from the live axes (`PH x -212..3010`, `main z 0..256`), the Set Ranges dialog +was driven through XTest (it opens on the live values `ADC -200..1000` / +`P.Map 0..256` / `PH -213..3010`; typing `ADC max = 300` turned the dim half of +the ADC image from blue `(0,150,221)` into yellow-green `(214,253,39)` while +the ROOT map kept its own scale, and `P.Map max = 251` left the ROOT map at +`0..251` across several redraws), and the geometry was checked on +screen under Xvfb: with a 400x1000 ADC window the captured ROI came out +78x224 (narrow and tall), the yellow box was measured at 162x463 px against an +ideal 2.872 aspect (0.5% off), and the bright half of the frame is at the +bottom in the ADC image and in the ROOT photon map alike. + +`test/pede_live_test.cpp` is a standalone check (no hardware, no GUI) with two +claims: a `Pedestal corrected` save follows the live pedestal instead of the +snapshot taken at Start Save (it offers one frame, installs a different +`PedeSet` mid-run, offers a second frame, and expects the two `/data` slices to +carry the two different pedestals), and the periodic checkpoint leaves a clean +pair on disk while the save still runs (the JSON has `"complete": false`, no +`.part` is left, and the **live** HDF5 read back with the SWMR flag has exactly +the frames written, not the over-allocated block). Build and run it against the +already-built objects: + +``` +g++ -std=c++17 -O2 -pthread $(root-config --cflags) \ + $(pkg-config --cflags Qt5Core Qt5Gui Qt5Widgets) \ + -I. -I../../dataStructures -I../../interpolations -I../../tiffio/include \ + -I../../../slsSupportLib/include -I../../../slsReceiverSoftware/include \ + -I../../../libs/rapidjson -I../../../build/_deps/libzmq-src/include \ + test/pede_live_test.cpp online_jf_h5.o online_jf_core.o online_jf_root.o \ + -o test/pede_live_test \ + $(pkg-config --libs Qt5Core Qt5Gui Qt5Widgets) $(root-config --libs) \ + ../../../build/_deps/libzmq-build/lib/libzmq.a -lhdf5_cpp -lhdf5 -lsodium \ + -lpthread -lrt -lm +cd test && LD_LIBRARY_PATH=/usr/lib64/root ./pede_live_test +``` + +`test/panel_map_test.cpp` is a JFSTRX-only check that the six-panel fill +geometry matches the strixel decoder: every booked panel (79x165, 47x320, +59x476 twice) must be fully covered by the decoder map, and only the decoder's +few extra right-edge columns may fall outside. Build it with `-DJFSTRX` against +the JFSTRX objects (same command as above plus `-DJFSTRX`). + +## Files + +| file | contents | +|---|---| +| `online_jf.h` | shared state, `Config`, `PedeSet`/`PedestalStore`, `GateEngine`, mailboxes, `Pipeline`, ROOT declarations | +| `online_jf_core.cpp` | globals, pedestal file I/O, gate math, acq + analysis threads | +| `online_jf_root.cpp` | ROOT objects, command queue, ROOT redraw thread, plotting | +| `online_jf_gui.cpp` | `FastImageView`, merged `MainWindow`, CLI parsing, `main()` | +| `online_jf_h5.cpp` | decimated ROI HDF5 writer (receiver-compatible) + JSON sidecar | +| `test/jf_feeder.cpp` | synthetic slsReceiver / republish subscriber | +| `test/pede_live_test.cpp` | standalone check: live pedestal + periodic clean checkpoint | +| `test/panel_map_test.cpp` | JFSTRX-only check of the six-panel fill geometry | diff --git a/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/changes.partch b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/changes.partch new file mode 100644 index 000000000..efb10fa15 --- /dev/null +++ b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/changes.partch @@ -0,0 +1,7842 @@ +diff --git a/bash_autocomplete.sh b/bash_autocomplete.sh +deleted file mode 120000 +index f79749d0d..000000000 +--- a/bash_autocomplete.sh ++++ /dev/null +@@ -1 +0,0 @@ +-slsDetectorSoftware/generator/autocomplete/bash_autocomplete.sh +\ No newline at end of file +diff --git a/bash_autocomplete.sh b/bash_autocomplete.sh +new file mode 100644 +index 000000000..5fe099a7d +--- /dev/null ++++ b/bash_autocomplete.sh +@@ -0,0 +1,3448 @@ ++# GENERATED FILE - DO NOT EDIT ++# ANY CHANGES TO THIS FILE WILL BE OVERWRITTEN ++ ++_sd() { ++ ++ # Taken from https://github.com/scop/bash-completion/blob/15b74b1050333f425877a7cbd99af2998b95c476/bash_completion#L770C12-L770C12 ++ # Reassemble command line words, excluding specified characters from the ++ # list of word completion separators (COMP_WORDBREAKS). ++ # @param $1 chars Characters out of $COMP_WORDBREAKS which should ++ # NOT be considered word breaks. This is useful for things like scp where ++ # we want to return host:path and not only path, so we would pass the ++ # colon (:) as $1 here. ++ # @param $2 words Name of variable to return words to ++ # @param $3 cword Name of variable to return cword to ++ # ++ _comp__reassemble_words() ++{ ++ local exclude="" i j line ref ++ # Exclude word separator characters? ++ if [[ $1 ]]; then ++ # Yes, exclude word separator characters; ++ # Exclude only those characters, which were really included ++ exclude="[${1//[^$COMP_WORDBREAKS]/}]" ++ fi ++ ++ # Default to cword unchanged ++ printf -v "$3" %s "$COMP_CWORD" ++ # Are characters excluded which were former included? ++ if [[ $exclude ]]; then ++ # Yes, list of word completion separators has shrunk; ++ line=$COMP_LINE ++ # Re-assemble words to complete ++ for ((i = 0, j = 0; i < ${#COMP_WORDS[@]}; i++, j++)); do ++ # Is current word not word 0 (the command itself) and is word not ++ # empty and is word made up of just word separator characters to ++ # be excluded and is current word not preceded by whitespace in ++ # original line? ++ while [[ $i -gt 0 && ${COMP_WORDS[i]} == +($exclude) ]]; do ++ # Is word separator not preceded by whitespace in original line ++ # and are we not going to append to word 0 (the command ++ # itself), then append to current word. ++ [[ $line != [[:blank:]]* ]] && ((j >= 2)) && ((j--)) ++ # Append word separator to current or new word ++ ref="$2[$j]" ++ printf -v "$ref" %s "${!ref-}${COMP_WORDS[i]}" ++ # Indicate new cword ++ ((i == COMP_CWORD)) && printf -v "$3" %s "$j" ++ # Remove optional whitespace + word separator from line copy ++ line=${line#*"${COMP_WORDS[i]}"} ++ # Indicate next word if available, else end *both* while and ++ # for loop ++ if ((i < ${#COMP_WORDS[@]} - 1)); then ++ ((i++)) ++ else ++ break 2 ++ fi ++ # Start new word if word separator in original line is ++ # followed by whitespace. ++ [[ $line == [[:blank:]]* ]] && ((j++)) ++ done ++ # Append word to current word ++ ref="$2[$j]" ++ printf -v "$ref" %s "${!ref-}${COMP_WORDS[i]}" ++ # Remove optional whitespace + word from line copy ++ line=${line#*"${COMP_WORDS[i]}"} ++ # Indicate new cword ++ ((i == COMP_CWORD)) && printf -v "$3" %s "$j" ++ done ++ ((i == COMP_CWORD)) && printf -v "$3" %s "$j" ++ else ++ # No, list of word completions separators hasn't changed; ++ for i in "${!COMP_WORDS[@]}"; do ++ printf -v "$2[i]" %s "${COMP_WORDS[i]}" ++ done ++ fi ++} ++ ++ ++ local FCN_RETURN="" ++ local IS_PATH=0 ++ ++ ++local SLS_COMMANDS=" acquire activate adcclk adcenable adcenable10g adcindex adcinvert adclist adcname adcphase adcpipeline adcreg adcvpp apulse asamples autocompdisable badchannels blockingtrigger burstmode burstperiod bursts burstsl bustest cdsgain chipversion clearbit clearbusy clientversion clkdiv clkfreq clkphase collectionmode column compdisabletime confadc config configtransceiver counters currentsource dac dacindex daclist dacname dacvalues datastream dbitclk dbitphase dbitpipeline defaultdac defaultpattern delay delayl detectorserverversion detsize diodelay dpulse dr drlist dsamples execcommand exptime exptime1 exptime2 exptime3 extrastoragecells extsampling extsamplingsrc extsig fformat filtercells filterresistor findex firmwaretest firmwareversion fliprows flowcontrol10g fmaster fname foverwrite fpath framecounter frames framesl frametime free fwrite gaincaps gainmode gappixels gatedelay gatedelay1 gatedelay2 gatedelay3 gates getbit hardwareversion highvoltage hostname im_a im_b im_c im_d im_io imagetest initialchecks inj_ch interpolation interruptsubframe kernelversion lastclient led lock master maxadcphaseshift maxclkphaseshift maxdbitphaseshift measuredperiod measuredsubperiod moduleid nextframenumber nmod numinterfaces overflow packageversion parallel parameters partialreset patfname patioctrl patlimits patloop patloop0 patloop1 patloop2 patmask patnloop patnloop0 patnloop1 patnloop2 patsetbit pattern patternstart patwait patwait0 patwait1 patwait2 patwaittime patwaittime0 patwaittime1 patwaittime2 patword pedestalmode period periodl polarity port powerchip powerindex powerlist powername powervalues programfpga pulse pulsechip pulsenmove pumpprobe quad ratecorr readnrows readout readoutspeed readoutspeedlist rebootcontroller reg resetdacs resetfpga romode row runclk runtime rx_arping rx_clearroi rx_dbitlist rx_dbitoffset rx_dbitreorder rx_discardpolicy rx_fifodepth rx_frameindex rx_framescaught rx_framesperfile rx_hostname rx_jsonaddheader rx_jsonpara rx_lastclient rx_lock rx_missingpackets rx_padding rx_printconfig rx_realudpsocksize rx_roi rx_silent rx_start rx_status rx_stop rx_tcpport rx_threads rx_udpsocksize rx_version rx_zmqfreq rx_zmqhwm rx_zmqip rx_zmqport rx_zmqstartfnum rx_zmqstream samples savepattern scan scanerrmsg selinterface serialnumber setbit settings settingslist settingspath signalindex signallist signalname sleep slowadc slowadcindex slowadclist slowadcname slowadcvalues start status stop stopport storagecell_delay storagecell_start subdeadtime subexptime sync syncclk temp_10ge temp_adc temp_control temp_dcdc temp_event temp_fpga temp_fpgaext temp_fpgafl temp_fpgafr temp_slowadc temp_sodl temp_sodr temp_threshold templist tempvalues tengiga threshold thresholdnotb timing timing_info_decoder timinglist timingsource top transceiverenable trigger triggers triggersl trimbits trimen trimval tsamples txdelay txdelay_frame txdelay_left txdelay_right type udp_cleardst udp_dstip udp_dstip2 udp_dstlist udp_dstmac udp_dstmac2 udp_dstport udp_dstport2 udp_firstdst udp_numdst udp_reconfigure udp_srcip udp_srcip2 udp_srcmac udp_srcmac2 udp_validate update updatedetectorserver updatekernel updatemode user v_a v_b v_c v_chip v_d v_io v_limit vchip_comp_adc vchip_comp_fe vchip_cs vchip_opa_1st vchip_opa_fd vchip_ref_comp_fe versions veto vetoalg vetofile vetophoton vetoref vetostream virtual vm_a vm_b vm_c vm_d vm_io zmqhwm zmqip zmqport " ++__acquire() { ++FCN_RETURN="" ++return 0 ++} ++__activate() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__adcclk() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__adcenable() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__adcenable10g() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__adcindex() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__adcinvert() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__adclist() { ++FCN_RETURN="" ++return 0 ++} ++__adcname() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__adcphase() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="deg" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="deg" ++fi ++fi ++return 0 ++} ++__adcpipeline() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__adcreg() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__adcvpp() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="mV mv" ++fi ++fi ++return 0 ++} ++__apulse() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__asamples() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__autocompdisable() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__badchannels() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__blockingtrigger() { ++FCN_RETURN="" ++return 0 ++} ++__burstmode() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="burst_external burst_internal cw_external cw_internal" ++fi ++fi ++return 0 ++} ++__burstperiod() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__bursts() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__burstsl() { ++FCN_RETURN="" ++return 0 ++} ++__bustest() { ++FCN_RETURN="" ++return 0 ++} ++__cdsgain() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__chipversion() { ++FCN_RETURN="" ++return 0 ++} ++__clearbit() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="--validate" ++fi ++fi ++return 0 ++} ++__clearbusy() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="--validate" ++fi ++fi ++return 0 ++} ++__clientversion() { ++FCN_RETURN="" ++return 0 ++} ++__clkdiv() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__clkfreq() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__clkphase() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="deg" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="deg" ++fi ++fi ++return 0 ++} ++__collectionmode() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="electron hole" ++fi ++fi ++return 0 ++} ++__column() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__compdisabletime() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__confadc() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__config() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__configtransceiver() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__counters() { ++FCN_RETURN="" ++return 0 ++} ++__currentsource() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="fix nofix" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "5" ]]; then ++FCN_RETURN="low normal" ++fi ++fi ++return 0 ++} ++__dac() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="mV mv" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="mV mv" ++fi ++fi ++return 0 ++} ++__dacindex() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__daclist() { ++FCN_RETURN="" ++return 0 ++} ++__dacname() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__dacvalues() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="mV mv" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="mV mv" ++fi ++fi ++return 0 ++} ++__datastream() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="bottom left right top" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="bottom left right top" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__dbitclk() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__dbitphase() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="deg" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="deg" ++fi ++fi ++return 0 ++} ++__dbitpipeline() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__defaultdac() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="`sls_detector_get settingslist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="`sls_detector_get settingslist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++fi ++return 0 ++} ++__defaultpattern() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="`sls_detector_get settingslist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++fi ++return 0 ++} ++__delay() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__delayl() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__detectorserverversion() { ++FCN_RETURN="" ++return 0 ++} ++__detsize() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__diodelay() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__dpulse() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__dr() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__drlist() { ++FCN_RETURN="" ++return 0 ++} ++__dsamples() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__execcommand() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__exptime() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__exptime1() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__exptime2() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__exptime3() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__extrastoragecells() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__extsampling() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__extsamplingsrc() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__extsig() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="inversion_off inversion_on trigger_in_falling_edge trigger_in_rising_edge" ++fi ++fi ++return 0 ++} ++__fformat() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="binary hdf5" ++fi ++fi ++return 0 ++} ++__filtercells() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__filterresistor() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__findex() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__firmwaretest() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__firmwareversion() { ++FCN_RETURN="" ++return 0 ++} ++__fliprows() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__flowcontrol10g() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__fmaster() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__fname() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__foverwrite() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__fpath() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__framecounter() { ++FCN_RETURN="" ++return 0 ++} ++__frames() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__framesl() { ++FCN_RETURN="" ++return 0 ++} ++__frametime() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__free() { ++FCN_RETURN="" ++return 0 ++} ++__fwrite() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__gaincaps() { ++FCN_RETURN="" ++return 0 ++} ++__gainmode() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="dynamic fixg0 fixg1 fixg2 forceswitchg1 forceswitchg2" ++fi ++fi ++return 0 ++} ++__gappixels() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__gatedelay() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__gatedelay1() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__gatedelay2() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__gatedelay3() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__gates() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__getbit() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__hardwareversion() { ++FCN_RETURN="" ++return 0 ++} ++__highvoltage() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__hostname() { ++FCN_RETURN="" ++return 0 ++} ++__im_a() { ++FCN_RETURN="" ++return 0 ++} ++__im_b() { ++FCN_RETURN="" ++return 0 ++} ++__im_c() { ++FCN_RETURN="" ++return 0 ++} ++__im_d() { ++FCN_RETURN="" ++return 0 ++} ++__im_io() { ++FCN_RETURN="" ++return 0 ++} ++__imagetest() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__initialchecks() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__inj_ch() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__interpolation() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__interruptsubframe() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__kernelversion() { ++FCN_RETURN="" ++return 0 ++} ++__lastclient() { ++FCN_RETURN="" ++return 0 ++} ++__led() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__lock() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__master() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__maxadcphaseshift() { ++FCN_RETURN="" ++return 0 ++} ++__maxclkphaseshift() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__maxdbitphaseshift() { ++FCN_RETURN="" ++return 0 ++} ++__measuredperiod() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__measuredsubperiod() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__moduleid() { ++FCN_RETURN="" ++return 0 ++} ++__nextframenumber() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__nmod() { ++FCN_RETURN="" ++return 0 ++} ++__numinterfaces() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__overflow() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__packageversion() { ++FCN_RETURN="" ++return 0 ++} ++__parallel() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__parameters() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__partialreset() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__patfname() { ++FCN_RETURN="" ++return 0 ++} ++__patioctrl() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patlimits() { ++FCN_RETURN="" ++return 0 ++} ++__patloop() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patloop0() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patloop1() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patloop2() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patmask() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patnloop() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patnloop0() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patnloop1() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patnloop2() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patsetbit() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__pattern() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__patternstart() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__patwait() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patwait0() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patwait1() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patwait2() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patwaittime() { ++FCN_RETURN="" ++return 0 ++} ++__patwaittime0() { ++FCN_RETURN="" ++return 0 ++} ++__patwaittime1() { ++FCN_RETURN="" ++return 0 ++} ++__patwaittime2() { ++FCN_RETURN="" ++return 0 ++} ++__patword() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__pedestalmode() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN=" 0" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__period() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__periodl() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__polarity() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="neg pos" ++fi ++fi ++return 0 ++} ++__port() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__powerchip() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__powerindex() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__powerlist() { ++FCN_RETURN="" ++return 0 ++} ++__powername() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__powervalues() { ++FCN_RETURN="" ++return 0 ++} ++__programfpga() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="--force-delete-normal-file" ++fi ++fi ++return 0 ++} ++__pulse() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="--force-delete-normal-file" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__pulsechip() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__pulsenmove() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__pumpprobe() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__quad() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__ratecorr() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__readnrows() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__readout() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__readoutspeed() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1 108 144 2 full_speed half_speed quarter_speed" ++fi ++fi ++return 0 ++} ++__readoutspeedlist() { ++FCN_RETURN="" ++return 0 ++} ++__rebootcontroller() { ++FCN_RETURN="" ++return 0 ++} ++__reg() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="--validate" ++fi ++fi ++return 0 ++} ++__resetdacs() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="--validate" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="hard" ++fi ++fi ++return 0 ++} ++__resetfpga() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="hard" ++fi ++fi ++return 0 ++} ++__romode() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="analog analog_digital digital digital_transceiver transceiver" ++fi ++fi ++return 0 ++} ++__row() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__runclk() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__runtime() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__rx_arping() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__rx_clearroi() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__rx_dbitlist() { ++FCN_RETURN="" ++return 0 ++} ++__rx_dbitoffset() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__rx_dbitreorder() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__rx_discardpolicy() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="discardempty discardpartial nodiscard" ++fi ++fi ++return 0 ++} ++__rx_fifodepth() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__rx_frameindex() { ++FCN_RETURN="" ++return 0 ++} ++__rx_framescaught() { ++FCN_RETURN="" ++return 0 ++} ++__rx_framesperfile() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__rx_hostname() { ++FCN_RETURN="" ++return 0 ++} ++__rx_jsonaddheader() { ++FCN_RETURN="" ++return 0 ++} ++__rx_jsonpara() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__rx_lastclient() { ++FCN_RETURN="" ++return 0 ++} ++__rx_lock() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__rx_missingpackets() { ++FCN_RETURN="" ++return 0 ++} ++__rx_padding() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__rx_printconfig() { ++FCN_RETURN="" ++return 0 ++} ++__rx_realudpsocksize() { ++FCN_RETURN="" ++return 0 ++} ++__rx_roi() { ++FCN_RETURN="" ++return 0 ++} ++__rx_silent() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__rx_start() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__rx_status() { ++FCN_RETURN="" ++return 0 ++} ++__rx_stop() { ++FCN_RETURN="" ++return 0 ++} ++__rx_tcpport() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__rx_threads() { ++FCN_RETURN="" ++return 0 ++} ++__rx_udpsocksize() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__rx_version() { ++FCN_RETURN="" ++return 0 ++} ++__rx_zmqfreq() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__rx_zmqhwm() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__rx_zmqip() { ++FCN_RETURN="" ++return 0 ++} ++__rx_zmqport() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__rx_zmqstartfnum() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__rx_zmqstream() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__samples() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__savepattern() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__scan() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "5" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "6" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__scanerrmsg() { ++FCN_RETURN="" ++return 0 ++} ++__selinterface() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__serialnumber() { ++FCN_RETURN="" ++return 0 ++} ++__setbit() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="--validate" ++fi ++fi ++return 0 ++} ++__settings() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get settingslist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++fi ++return 0 ++} ++__settingslist() { ++FCN_RETURN="" ++return 0 ++} ++__settingspath() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__signalindex() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__signallist() { ++FCN_RETURN="" ++return 0 ++} ++__signalname() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__sleep() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__slowadc() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__slowadcindex() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__slowadclist() { ++FCN_RETURN="" ++return 0 ++} ++__slowadcname() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__slowadcvalues() { ++FCN_RETURN="" ++return 0 ++} ++__start() { ++FCN_RETURN="" ++return 0 ++} ++__status() { ++FCN_RETURN="" ++return 0 ++} ++__stop() { ++FCN_RETURN="" ++return 0 ++} ++__stopport() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__storagecell_delay() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__storagecell_start() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__subdeadtime() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__subexptime() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__sync() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__syncclk() { ++FCN_RETURN="" ++return 0 ++} ++__temp_10ge() { ++FCN_RETURN="" ++return 0 ++} ++__temp_adc() { ++FCN_RETURN="" ++return 0 ++} ++__temp_control() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__temp_dcdc() { ++FCN_RETURN="" ++return 0 ++} ++__temp_event() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__temp_fpga() { ++FCN_RETURN="" ++return 0 ++} ++__temp_fpgaext() { ++FCN_RETURN="" ++return 0 ++} ++__temp_fpgafl() { ++FCN_RETURN="" ++return 0 ++} ++__temp_fpgafr() { ++FCN_RETURN="" ++return 0 ++} ++__temp_slowadc() { ++FCN_RETURN="" ++return 0 ++} ++__temp_sodl() { ++FCN_RETURN="" ++return 0 ++} ++__temp_sodr() { ++FCN_RETURN="" ++return 0 ++} ++__temp_threshold() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__templist() { ++FCN_RETURN="" ++return 0 ++} ++__tempvalues() { ++FCN_RETURN="" ++return 0 ++} ++__tengiga() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__threshold() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="`sls_detector_get settingslist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "5" ]]; then ++FCN_RETURN="`sls_detector_get settingslist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++fi ++return 0 ++} ++__thresholdnotb() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="`sls_detector_get settingslist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "5" ]]; then ++FCN_RETURN="`sls_detector_get settingslist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++fi ++return 0 ++} ++__timing() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get timinglist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++fi ++return 0 ++} ++__timing_info_decoder() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="shine swissfel" ++fi ++fi ++return 0 ++} ++__timinglist() { ++FCN_RETURN="" ++return 0 ++} ++__timingsource() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="external internal" ++fi ++fi ++return 0 ++} ++__top() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__transceiverenable() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__trigger() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__triggers() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__triggersl() { ++FCN_RETURN="" ++return 0 ++} ++__trimbits() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__trimen() { ++FCN_RETURN="" ++return 0 ++} ++__trimval() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__tsamples() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__txdelay() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__txdelay_frame() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__txdelay_left() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__txdelay_right() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__type() { ++FCN_RETURN="" ++return 0 ++} ++__udp_cleardst() { ++FCN_RETURN="" ++return 0 ++} ++__udp_dstip() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__udp_dstip2() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__udp_dstlist() { ++FCN_RETURN="" ++return 0 ++} ++__udp_dstmac() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__udp_dstmac2() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__udp_dstport() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__udp_dstport2() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__udp_firstdst() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__udp_numdst() { ++FCN_RETURN="" ++return 0 ++} ++__udp_reconfigure() { ++FCN_RETURN="" ++return 0 ++} ++__udp_srcip() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__udp_srcip2() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__udp_srcmac() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__udp_srcmac2() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__udp_validate() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__update() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__updatedetectorserver() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__updatekernel() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__updatemode() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__user() { ++FCN_RETURN="" ++return 0 ++} ++__v_a() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__v_b() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__v_c() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__v_chip() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__v_d() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__v_io() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__v_limit() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__vchip_comp_adc() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__vchip_comp_fe() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__vchip_cs() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__vchip_opa_1st() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__vchip_opa_fd() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__vchip_ref_comp_fe() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__versions() { ++FCN_RETURN="" ++return 0 ++} ++__veto() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__vetoalg() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="10gbe lll none" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="hits raw" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="10gbe lll none" ++fi ++fi ++return 0 ++} ++__vetofile() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__vetophoton() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "5" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__vetoref() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__vetostream() { ++FCN_RETURN="" ++return 0 ++} ++__virtual() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__vm_a() { ++FCN_RETURN="" ++return 0 ++} ++__vm_b() { ++FCN_RETURN="" ++return 0 ++} ++__vm_c() { ++FCN_RETURN="" ++return 0 ++} ++__vm_d() { ++FCN_RETURN="" ++return 0 ++} ++__vm_io() { ++FCN_RETURN="" ++return 0 ++} ++__zmqhwm() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__zmqip() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__zmqport() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++ ++ ++ COMPREPLY=() ++ local OPTIONS_NEW="" ++ ++ # check if bash or zsh ++ # _get_comp_words_by_ref is a bash built-in function, we check if it exists ++ declare -Ff _get_comp_words_by_ref > /dev/null && IS_BASH=1 || IS_BASH=0 ++ ++ ++ # bash interprets the colon character : as a special character and splits the argument in two ++ # different than what zsh does ++ # https://stackoverflow.com/a/3224910 ++ # https://stackoverflow.com/a/12495727 ++ local cur ++ local cword words=() ++ _comp__reassemble_words ":" words cword ++ cur=${words[cword]} ++ ++ # check the action (get or put) ++ case "${words[0]}" in ++ "sls_detector_get" | "g" | "detg") ++ local IS_GET=1 ++ ;; ++ *) ++ local IS_GET=0 ++ ;; ++ esac ++ ++ # if no command is written, autocomplete with the commands ++ if [[ ${cword} -eq 1 ]]; then ++# local SLS_COMMANDS="trimbits exptime" ++ local SLS_COMMANDS_NEW="" ++ ++ case "$cur" in ++ [0-9]*:*) ++ local suggestions=($(compgen -W "${SLS_COMMANDS}" -- "${cur#*:}")) ++ COMPREPLY=( ${suggestions[*]} ) ++ ;; ++ [0-9]*) ++ COMPREPLY=() ++ ;; ++ *) ++ COMPREPLY=( $( compgen -W "$SLS_COMMANDS -h" -- "$cur" ) );; ++ ++ esac ++ return 0 ++ fi ++ ++ if [[ ${cword} -eq 2 ]] && [[ ${words[1]} == "-h" ]]; then ++ COMPREPLY=( $( compgen -W "$SLS_COMMANDS" -- "$cur" ) ) ++ return 0 ++ fi ++ ++ # if a command is written, autocomplete with the options ++ # call the function for the command ++ ++ if [[ "$SLS_COMMANDS" == *"${words[1]##*:}"* ]]; then ++ __"${words[1]##*:}" ++ fi ++ ++ # if IS_PATH is activated, autocomplete with the path ++ if [[ ${IS_PATH} -eq 1 ]]; then ++ COMPREPLY=($(compgen -f -- "${cur}")) ++ return 0 ++ fi ++ ++ # autocomplete with the options ++ COMPREPLY=($(compgen -W "${FCN_RETURN}" -- "${cur}")) ++ ++ ++ ++} ++ ++complete -F _sd -o filenames sls_detector_get ++complete -F _sd -o filenames g ++complete -F _sd -o filenames detg ++ ++complete -F _sd -o filenames sls_detector_put ++complete -F _sd -o filenames p ++complete -F _sd -o filenames detp ++ ++complete -F _sd -o filenames sls_detector ++complete -F _sd -o filenames det +diff --git a/slsDetectorCalibration/jungfrauExecutables/Makefile b/slsDetectorCalibration/jungfrauExecutables/Makefile +deleted file mode 120000 +index bde0d5288..000000000 +--- a/slsDetectorCalibration/jungfrauExecutables/Makefile ++++ /dev/null +@@ -1 +0,0 @@ +-Makefile.zmqrootdisp +\ No newline at end of file +diff --git a/slsDetectorCalibration/jungfrauExecutables/Makefile b/slsDetectorCalibration/jungfrauExecutables/Makefile +new file mode 100644 +index 000000000..1ec21eac7 +--- /dev/null ++++ b/slsDetectorCalibration/jungfrauExecutables/Makefile +@@ -0,0 +1,22 @@ ++ROOTSYS=/opt/cern/v6/build ++ ++LIBZMQDIR = $(PWD) ++LIBZMQ = -L$(LIBZMQDIR) -lzmq ++SHLIB_PATH=/opt/cern/v6/root/lib ++CMAKE_PREFIX_PATH=/opt/cern/v6/root ++DYLD_LIBRARY_PATH=/opt/cern/v6/root/lib ++ ++QT_CXXFLAGS = $(shell pkg-config --cflags Qt5Core Qt5Gui Qt5Widgets 2>/dev/null) ++QT_LIBS = $(shell pkg-config --libs Qt5Core Qt5Gui Qt5Widgets 2>/dev/null) ++ ++INCDIR= -I. -I../dataStructures ../tiffio/src/tiffIO.cpp -I../ -I../interpolations/ -I../../slsSupportLib/include/ -I../../slsReceiverSoftware/include/ -I../../libs/rapidjson/ -I../tiffio/include ++LDFLAG= -L/usr/lib64/ -lpthread -lm -lstdc++ -lzmq -pthread -lrt -ltiff -O3 -std=c++17 -Wall -Wl,-rpath-link,$(ROOTSYS)/lib ++ ++ ++default: onlinedisp_zmq ++ ++onlinedisp_zmq: onlinedisp_zmq.cpp onlinedisp_zmq.h ../dataStructures/jungfrauLGADStrixelsData_new.h ++ g++ -o onlinedisp_zmq onlinedisp_zmq.cpp $(QT_CXXFLAGS) $(QT_LIBS) -I. -I$(ROOTSYS)/include -Wall -g -lm -L. -lzmq -pthread -lrt -L$(ROOTSYS)/lib -lGui -lCore -lRIO -lNet -lHist -lGraf -lGraf3d -lGpad -lTree -lRint -lPostscript -lMatrix -lPhysics -lMathCore -lThread -m64 -L../../build/bin/ -lSlsSupportStatic ../../build/_deps/libzmq-build/lib/libzmq.a -lstdc++ -lsodium $(LDFLAG) $(INCDIR) ++ ++#-lCint ++ +diff --git a/slsDetectorCalibration/jungfrauExecutables/onlinedisp_zmq.cpp b/slsDetectorCalibration/jungfrauExecutables/onlinedisp_zmq.cpp +index 4ddacd260..e1d3c7556 100644 +--- a/slsDetectorCalibration/jungfrauExecutables/onlinedisp_zmq.cpp ++++ b/slsDetectorCalibration/jungfrauExecutables/onlinedisp_zmq.cpp +@@ -7,6 +7,121 @@ sls::zmqHeader zHeader; + #define NPRO 50 + #define NPRI 50 + ++// ── ROOT thread safety ─────────────────────────────────────────────────────── ++// The main thread FILLS the source accumulator histograms on every processed ++// frame. The ROOT redraw thread must never paint those (ROOT is not ++// thread-safe for concurrent Fill vs Paint - crashes in TH1::SetContour). ++// Instead the ROOT thread paints DISPLAY CLONES whose bin content is copied ++// from the sources once per redraw tick (copying while a fill runs only ++// yields a transiently torn pixel, never a crash). ++std::atomic gFramesForRoot{0}; ++std::atomic gResetPending{false}; ++ ++// display clones painted by the ROOT thread (single-owner objects) ++TH2F *disp3000 = nullptr; ++TH2F *disp4500 = nullptr; ++TH2F *dispLvsR = nullptr; ++TH1I *dispChip[8] = {nullptr, nullptr, nullptr, nullptr, ++ nullptr, nullptr, nullptr, nullptr}; ++#ifdef JFSTRX ++TH2F *disp1000 = nullptr; TH2F *disp1001 = nullptr; TH2F *disp1002 = nullptr; ++TH2F *disp1060 = nullptr; TH2F *disp1061 = nullptr; TH2F *disp1062 = nullptr; ++#endif ++ ++// copies bin contents + entry count from a source accumulator to its ++// display clone (same geometry). ROOT-thread only. ++static void copyBins(TH1 *dst, TH1 *src) { ++ if (!dst || !src) return; ++ const int bx = dst->GetNbinsX(); ++ const int by = dst->GetNbinsY(); ++ dst->Reset(); ++ if (by <= 1) { ++ for (int b = 1; b <= bx; b++) ++ dst->SetBinContent(b, src->GetBinContent(b)); ++ } else { ++ for (int y = 1; y <= by; y++) ++ for (int x = 1; x <= bx; x++) ++ dst->SetBinContent(x, y, src->GetBinContent(x, y)); ++ } ++ dst->SetEntries(src->GetEntries()); ++} ++ ++// ── ROOT GUI ownership ─────────────────────────────────────────────────────── ++// ROOT canvas painting + gSystem->ProcessEvents() may ONLY run in the ++// dedicated ROOT thread (thread that drew them crashes otherwise, e.g. in ++// TGaxis/TPaletteAxis). The main thread never touches ROOT graphics; user ++// actions (reset, axes, palette, surf/colz, zoom...) are queued here and ++// executed by the ROOT thread. ++enum class RootCmdType { ++ None, ResetHis, ResetAxes, Surf, Colz, ++ PaletteFalse, PaletteBW1, PaletteBW2, Zoom, SetChipRange ++}; ++struct RootCmd { ++ RootCmdType type = RootCmdType::None; ++ double a = 0, b = 0, c = 0, d = 0; // zoom coords / chip range ++}; ++std::mutex gCmdMtx; ++std::vector gCmds; ++ ++static void pushRootCmd(RootCmd cmd) { ++ std::lock_guard lock(gCmdMtx); ++ gCmds.push_back(cmd); ++} ++ ++// Executes one queued ROOT command. Only ever called from the ROOT thread. ++static void execRootCmd(const RootCmd &cmd) { ++ switch (cmd.type) { ++ case RootCmdType::ResetHis: ++ historeset(); ++ break; ++ case RootCmdType::ResetAxes: ++ axisreset(); ++ break; ++ case RootCmdType::Surf: ++ his1000->SetOption("surf2z"); ++#ifdef JFSTRX ++ disp1000->SetOption("surf2z"); ++#endif ++ Plot2DHistos(); ++ break; ++ case RootCmdType::Colz: ++ his1000->SetOption("colz"); ++#ifdef JFSTRX ++ disp1000->SetOption("colz"); ++#endif ++ if (bw_flag) { bw_flag = false; LoadPaletteFalse(); } ++ Plot2DHistos(); ++ break; ++ case RootCmdType::PaletteFalse: ++ LoadPaletteFalse(); ++ break; ++ case RootCmdType::PaletteBW1: ++ LoadPaletteBW(1.1111f); bw_flag = true; ++ break; ++ case RootCmdType::PaletteBW2: ++ LoadPaletteBW(0.9f); bw_flag = true; ++ break; ++ case RootCmdType::Zoom: ++ if (disp3000) { ++ disp3000->GetXaxis()->SetRangeUser(cmd.a, cmd.c); ++ disp3000->GetYaxis()->SetRangeUser(cmd.b, cmd.d); ++ A5->cd(); disp3000->Draw("colz"); ++ box = new TBox(ROIxmin, ROIymin, ROIxmax, ROIymax); ++ box->SetLineColor(kRed); box->SetFillStyle(0); ++ box->SetLineWidth(2); box->Draw(); ++ A5->Update(); ++ } ++ break; ++ case RootCmdType::SetChipRange: ++ for (int ih = 0; ih < 8; ih++) ++ dispChip[ih]->GetXaxis()->SetRangeUser(cmd.a, cmd.b); ++ Plot1DHistos(); ++ break; ++ default: ++ break; ++ } ++} ++ + //#define JFSTRX + #ifdef JFSTRX + #include "jungfrauLGADStrixelsData_new.h" +@@ -352,7 +467,7 @@ MainWindow::MainWindow(int argc, char* argv[]) + his2000->GetZaxis()->SetRangeUser(0, 4); + + if (dophotonmap) +- his3000 = new TH2F("his3000","photon map", 1024, -0.5, 1024-0.5, 512, -0.5, 512-0.5); ++ his3000 = new TH2F("his3000","photon map (cumulative)", 1024, -0.5, 1024-0.5, 512, -0.5, 512-0.5); + else + his3000 = new TH2F("his3000","raw adc", 1024, -0.5, 1024-0.5, 512, -0.5, 512-0.5); + +@@ -390,6 +505,25 @@ MainWindow::MainWindow(int argc, char* argv[]) + if (A5 == NULL) A5 = new TCanvas("A5","Plotting Canvas Photon Map", 750, 300, 1000, 600); + if (A6 == NULL) A6 = new TCanvas("A6","Plotting Canvas LvsR", 650, 250, 650, 660); + ++ // display clones painted by the ROOT thread (source histograms stay ++ // exclusive to the filling main thread) ++ disp3000 = (TH2F *)his3000->Clone("disp3000"); ++ disp4500 = (TH2F *)his4500->Clone("disp4500"); ++ dispLvsR = (TH2F *)hisLvsR->Clone("dispLvsR"); ++ for (int ih = 0; ih < 8; ih++) { ++ char nm[64]; ++ sprintf(nm, "dispChip%d", ih); ++ dispChip[ih] = (TH1I *)hchip[ih]->Clone(nm); ++ } ++#ifdef JFSTRX ++ disp1000 = (TH2F *)his1000->Clone("disp1000"); ++ disp1001 = (TH2F *)his1001->Clone("disp1001"); ++ disp1002 = (TH2F *)his1002->Clone("disp1002"); ++ disp1060 = (TH2F *)his1060->Clone("disp1060"); ++ disp1061 = (TH2F *)his1061->Clone("disp1061"); ++ disp1062 = (TH2F *)his1062->Clone("disp1062"); ++#endif ++ + printhelp(); + } + +@@ -407,7 +541,8 @@ void MainWindow::setupUI() { + auto *statusLayout = new QVBoxLayout(statusGroup); + frameLabel = new QLabel("Frame: --", statusGroup); + statusLabel = new QLabel("Status: idle", statusGroup); +- drawLabel = new QLabel("Draw every: 20 frames", statusGroup); ++ drawLabel = new QLabel(statusGroup); ++ drawLabel->setText(QString("Main: every %1 frames | ROOT: every %2 ms").arg(HDraw_every).arg(rootUpdateMs)); + statusLayout->addWidget(frameLabel); + statusLayout->addWidget(statusLabel); + statusLayout->addWidget(drawLabel); +@@ -488,12 +623,14 @@ void MainWindow::setupUI() { + HDraw_every = int(HDraw_every * 0.8f); + if (HDraw_every < 1) HDraw_every = 1; + cout << endl << "Drawing every " << HDraw_every << " frames" << endl; +- drawLabel->setText(QString("Draw every: %1 frames").arg(HDraw_every)); ++ updateDrawLabel(); + }); + connect(btnSlower, &QPushButton::clicked, [this]() { +- HDraw_every = int(HDraw_every * 1.25f); ++ int newevery = int(HDraw_every * 1.25f); ++ if (newevery <= HDraw_every) newevery = HDraw_every + 1; // avoid 1->1 ++ HDraw_every = newevery; + cout << endl << "Drawing every " << HDraw_every << " frames" << endl; +- drawLabel->setText(QString("Draw every: %1 frames").arg(HDraw_every)); ++ updateDrawLabel(); + }); + connect(btnGainToggle, &QPushButton::clicked, [this]() { + gain_flag = !gain_flag; +@@ -501,11 +638,11 @@ void MainWindow::setupUI() { + else { cout << "gain corr disab." << endl; statusLabel->setText("Gain: off"); } + }); + connect(btnBWPalette, &QPushButton::clicked, [this]() { +- LoadPaletteBW(1.1111f); bw_flag = true; ++ RootCmd c; c.type = RootCmdType::PaletteBW1; pushRootCmd(c); + statusLabel->setText("Status: BW palette"); + }); + connect(btnBWPalette2, &QPushButton::clicked, [this]() { +- LoadPaletteBW(0.9f); bw_flag = true; ++ RootCmd c; c.type = RootCmdType::PaletteBW2; pushRootCmd(c); + statusLabel->setText("Status: BW palette 2"); + }); + connect(btnSetRanges, &QPushButton::clicked, [this]() { showRangeDialog(); }); +@@ -528,17 +665,18 @@ void MainWindow::setupUI() { + loadallpede(); + statusLabel->setText("Status: all pede loaded"); + }); +- connect(btnResetHis, &QPushButton::clicked, [this]() { historeset(); }); +- connect(btnResetAxis, &QPushButton::clicked, [this]() { axisreset(); }); ++ connect(btnResetHis, &QPushButton::clicked, [this]() { ++ RootCmd c; c.type = RootCmdType::ResetHis; pushRootCmd(c); ++ }); ++ connect(btnResetAxis, &QPushButton::clicked, [this]() { ++ RootCmd c; c.type = RootCmdType::ResetAxes; pushRootCmd(c); ++ }); + connect(btnSurf2Z, &QPushButton::clicked, [this]() { +- his1000->SetOption("surf2z"); +- Plot2DHistos(); ++ RootCmd c; c.type = RootCmdType::Surf; pushRootCmd(c); + statusLabel->setText("Display: surf2z"); + }); + connect(btnColZ, &QPushButton::clicked, [this]() { +- his1000->SetOption("colz"); +- if (bw_flag) { bw_flag = false; LoadPaletteFalse(); } +- Plot2DHistos(); ++ RootCmd c; c.type = RootCmdType::Colz; pushRootCmd(c); + statusLabel->setText("Display: colz"); + }); + connect(btnG1OffsetDown, &QPushButton::clicked, [this]() { +@@ -585,16 +723,11 @@ void MainWindow::setupUI() { + // Master-slave zoom: ADC master → gain slave + ROOT canvases + m_adcView->onZoomChanged = [this](double x1, double y1, double x2, double y2) { + m_gainView->setViewport(x1, y1, x2, y2); +- if (his3000) { +- his3000->GetXaxis()->SetRangeUser(x1, x2); +- his3000->GetYaxis()->SetRangeUser(y1, y2); +- A5->cd(); his3000->Draw("colz"); +- box = new TBox(ROIxmin, ROIymin, ROIxmax, ROIymax); +- box->SetLineColor(kRed); box->SetFillStyle(0); +- box->SetLineWidth(2); box->Draw(); +- A5->Update(); +- } +- gSystem->ProcessEvents(); ++ // ROOT canvas update is executed by the ROOT thread (command queue) ++ RootCmd c; ++ c.type = RootCmdType::Zoom; ++ c.a = x1; c.b = y1; c.c = x2; c.d = y2; ++ pushRootCmd(c); + }; + } + +@@ -614,6 +747,11 @@ void MainWindow::start() { + connect(statusTimer, &QTimer::timeout, this, &MainWindow::updateStatusBar); + statusTimer->start(1000); + ++ // ROOT redraw thread: plots on a configurable period (rootUpdateMs), ++ // only when at least one frame was processed in that period. ++ rootThreadRun.store(true); ++ rootDrawThread = new std::thread(&MainWindow::rootDrawLoop, this); ++ + statusLabel->setText("Status: running"); + } + +@@ -624,10 +762,74 @@ void MainWindow::stop() { + delete workerThread; + workerThread = nullptr; + } ++ rootThreadRun.store(false); ++ if (rootDrawThread && rootDrawThread->joinable()) { ++ rootDrawThread->join(); ++ delete rootDrawThread; ++ rootDrawThread = nullptr; ++ } + if (processTimer) { processTimer->stop(); delete processTimer; processTimer = nullptr; } + if (statusTimer) { statusTimer->stop(); delete statusTimer; statusTimer = nullptr; } + } + ++void MainWindow::rootDrawLoop() { ++ auto next = std::chrono::steady_clock::now(); ++ while (rootThreadRun.load()) { ++ // service loop every ~20 ms: commands + ROOT GUI events, with ++ // periodic plotting on the rootUpdateMs cadence ++ while (rootThreadRun.load() && ++ std::chrono::steady_clock::now() < next) ++ std::this_thread::sleep_for(std::chrono::milliseconds(20)); ++ if (!rootThreadRun.load()) ++ break; ++ ++ // 1) execute any user ROOT commands (from the Qt thread) ++ for (;;) { ++ RootCmd cmd; ++ { ++ std::lock_guard lock(gCmdMtx); ++ if (gCmds.empty()) break; ++ cmd = gCmds.front(); ++ gCmds.erase(gCmds.begin()); ++ } ++ execRootCmd(cmd); ++ } ++ ++ // 2) ROOT GUI event processing lives HERE (single owner thread), ++ // keeping ROOT canvases interactive (zoom etc.) ++ gSystem->ProcessEvents(); ++ ++ // 3) periodic plot ++ auto period = std::chrono::milliseconds(rootUpdateMs); ++ if (period.count() < 50) ++ period = std::chrono::milliseconds(50); ++ next = std::chrono::steady_clock::now() + period; ++ ++ if (gFramesForRoot.exchange(0) == 0) ++ continue; ++ ++ // snapshot the source accumulators into the display clones that the ++ // canvases actually paint (never paint the objects being filled) ++ copyBins(disp3000, his3000); ++ copyBins(disp4500, his4500); ++ copyBins(dispLvsR, hisLvsR); ++ for (int ih = 0; ih < 8; ih++) ++ copyBins(dispChip[ih], hchip[ih]); ++#ifdef JFSTRX ++ copyBins(disp1000, his1000); copyBins(disp1001, his1001); ++ copyBins(disp1002, his1002); copyBins(disp1060, his1060); ++ copyBins(disp1061, his1061); copyBins(disp1062, his1062); ++#endif ++ ++ Plot2DHistos(); ++ Plot1DHistos(); ++ // keep the TvsB/LvsR scatter windows fresh: reset them on the main ++ // thread before the first fill of the next period (the photon map ++ // stays cumulative) ++ gResetPending.store(true); ++ } ++} ++ + void MainWindow::processData() { + if (!threadData.data_ready.load()) return; + +@@ -635,181 +837,172 @@ void MainWindow::processData() { + memcpy(image_data, threadData.image_data_work, NCH*2*sizeof(short)); + threadData.data_ready.store(false); + +- if (goout == 0) { +- gSystem->ProcessEvents(); // keep ROOT canvases interactive while paused +- return; +- } ++ if (goout == 0) ++ return; // ROOT canvases stay interactive via the ROOT thread + +- // Process frame (same logic as original main loop) ++ ++ // Process frame + memcpy(&croi, &zHeader.detSpec1, 8); + ROIxmin = croi.xmin; ROIxmax = croi.xmax; + ROIymin = croi.ymin; ROIymax = croi.ymax; + framesinstream++; +- + show2Ds = ((framesinstream % HDraw_every) == (HDraw_every - 1)); +- fill1Ds = fillPerFrame || show2Ds; +- static int mainDrawCount = 0; +- bool doRootDraw = false; +- if (show2Ds) { +- mainDrawCount++; +- doRootDraw = ((mainDrawCount % rootDrawDivider) == 0); ++ ++ processedf++; ++ gFramesForRoot.fetch_add(1); ++ ++ // ----------------------------------------------------------------- ++ // ROOT histogram FILLING happens on EVERY processed frame. ++ // Only the PLOTTING is periodic (dedicated thread, rootUpdateMs). ++ // Histograms are accumulated without locking; the rare transient ++ // read-during-fill overlap with the ROOT plot thread is acceptable ++ // for a realtime display (no GUI blocking ever). ++ // ----------------------------------------------------------------- ++ ++ // open a fresh accumulation window after each ROOT redraw ++ // Per-second ROOT redraw: keep the scatter correlations (TvsB/LvsR) ++ // windowed to the last second, but the PHOTON MAP (and hchptr, JFSTRX ++ // images) is CUMULATIVE: it accumulates until "Reset His" is pressed. ++ if (gResetPending.exchange(false)) { ++ his4500->Reset(); ++ hisLvsR->Reset(); + } +- bool doFill = fillPerFrame || show2Ds || dophotonmap; + +- if (doRootDraw) { +-#ifdef JFSTRX +- his1000->Reset(); his1001->Reset(); his1002->Reset(); +- his1060->Reset(); his1061->Reset(); his1062->Reset(); +-#endif +- if (!dophotonmap) his3000->Reset(); ++ // 1) per-frame ADC/gain image buffers (fed to fast widgets, TvsB/LvsR) ++ for (i = 0; i < NCH; i++) { ++ adcvalue = image_data[i] & 0x3fff; ++ gain = (image_data[i] & 0xc000) ? ((image_data[i] >> 14) & 0x3) : 0; ++ float disp = float(adcvalue) - fpeded[i]; ++ if ((i % 256 == 0) || (i % 256 == 255)) disp = int(disp / factor); ++ if ((i / 1024 == 255) || (i / 1024 == 256) || (i / 1024 == 767) || (i / 1024 == 768)) ++ disp = int(disp / factor); ++ adc_image[i] = disp; ++ gain_image[i] = gain; + } + +- if (doFill) { +- processedf++; +- for (i = 0; i < NCH; i++) { +- adcvalue = image_data[i] & 0x3fff; +- gain = (image_data[i] & 0xc000) ? ((image_data[i] >> 14) & 0x3) : 0; +- +- if (pede_flag) { +- if (gain_flag) { +- if (gain == 0 || !hasallpede) +- adcpedecorr = (adcvalue & 0x3fff) * fgaind[i] - fpeded[i] * fgaind[i]; +- if (gain == 1 && hasallpede) +- adcpedecorr = (fpedeG1d[i] * fgaind[i] + G1Poffset - adcvalue * fgaind[i]) * 30.0; +- if (gain == 3 && hasallpede) +- adcpedecorr = (fpedeG2d[i] * fgaind[i] + G2Poffset - adcvalue * fgaind[i]) * 340.0; +- } else { +- if (gain == 0 || !hasallpede) +- adcpedecorr = (adcvalue & 0x3fff) - fpeded[i]; +- if (gain == 1 && hasallpede) +- adcpedecorr = (fpedeG1d[i] + G1Poffset - adcvalue) * 30.0; +- if (gain == 3 && hasallpede) +- adcpedecorr = (fpedeG2d[i] + G2Poffset - adcvalue) * 340.0; +- } ++ // 2) photon counting + PH (hchip) fills + photon map, every frame ++ for (i = 0; i < NCH; i++) { ++ adcvalue = image_data[i] & 0x3fff; ++ gain = (image_data[i] & 0xc000) ? ((image_data[i] >> 14) & 0x3) : 0; ++ ++ if (pede_flag) { ++ if (gain_flag) { ++ if (gain == 0 || !hasallpede) ++ adcpedecorr = (adcvalue & 0x3fff) * fgaind[i] - fpeded[i] * fgaind[i]; ++ if (gain == 1 && hasallpede) ++ adcpedecorr = (fpedeG1d[i] * fgaind[i] + G1Poffset - adcvalue * fgaind[i]) * 30.0; ++ if (gain == 3 && hasallpede) ++ adcpedecorr = (fpedeG2d[i] * fgaind[i] + G2Poffset - adcvalue * fgaind[i]) * 340.0; + } else { +- adcpedecorr = float(adcvalue); ++ if (gain == 0 || !hasallpede) ++ adcpedecorr = (adcvalue & 0x3fff) - fpeded[i]; ++ if (gain == 1 && hasallpede) ++ adcpedecorr = (fpedeG1d[i] + G1Poffset - adcvalue) * 30.0; ++ if (gain == 3 && hasallpede) ++ adcpedecorr = (fpedeG2d[i] + G2Poffset - adcvalue) * 340.0; + } ++ } else { ++ adcpedecorr = float(adcvalue); ++ } + +- if (adcpedecorr > threshold && pede_flag) +- hchptr[i] = hchptr[i] + (int)((adcpedecorr + threshold) / phene); ++ if (adcpedecorr > threshold && pede_flag) { ++ int inc = (int)((adcpedecorr + threshold) / phene); ++ hchptr[i] = hchptr[i] + inc; ++ if (dophotonmap) ++ his3000->Fill(float(i % 1024), float(i / 1024), inc); ++ } + +- if (fill1Ds) { +- if ((i % 1024) < 1004 && (i % 1024) > 20 && (i / 1024) > 20) { +- ichip = i / (256 * 256 * 4) * 4 + ((i / 256) % 4); ++ if ((i % 1024) < 1004 && (i % 1024) > 20 && (i / 1024) > 20) { ++ ichip = i / (256 * 256 * 4) * 4 + ((i / 256) % 4); + #ifdef JFSTRX +- int new_ichip = 0; +- if (ichip == 1) { +- if (i <= (1024*(64-2))) new_ichip = 0; +- if (i > (1024*(64+6)) && i <= (1024*(64*2-4))) new_ichip = 1; +- if (i > (1024*(64*2+5))) new_ichip = 2; +- } +- if (ichip == 6) { +- if (i <= (1024*(256+2*64-5))) new_ichip = 7; +- if (i > (1024*(256+2*64+4)) && i <= (1024*(256+64*3-6))) new_ichip = 6; +- if (i > (1024*(256+64*3-4))) new_ichip = 5; +- } +- hchip[new_ichip]->Fill(adcpedecorr, 1); ++ int new_ichip = 0; ++ if (ichip == 1) { ++ if (i <= (1024*(64-2))) new_ichip = 0; ++ if (i > (1024*(64+6)) && i <= (1024*(64*2-4))) new_ichip = 1; ++ if (i > (1024*(64*2+5))) new_ichip = 2; ++ } ++ if (ichip == 6) { ++ if (i <= (1024*(256+2*64-5))) new_ichip = 7; ++ if (i > (1024*(256+2*64+4)) && i <= (1024*(256+64*3-6))) new_ichip = 6; ++ if (i > (1024*(256+64*3-4))) new_ichip = 5; ++ } ++ hchip[new_ichip]->Fill(adcpedecorr, 1); + #else +- hchip[ichip]->Fill(adcpedecorr, 1); ++ hchip[ichip]->Fill(adcpedecorr, 1); + #endif +- adcpedecorrold = adcpedecorr; +- } ++ adcpedecorrold = adcpedecorr; ++ } ++ } ++ ++ // 3) T vs B / L vs R scatter, every frame (from the current frame) ++ for (ipx = 0; ipx < 1024; ipx++) { ++ his4500->Fill(adc_image[ipx + (512-21)*1024], adc_image[ipx + 20*1024]); ++ } ++ for (ipy = 0; ipy < 512; ipy++) { ++ hisLvsR->Fill(adc_image[20 + ipy*1024], adc_image[1024-21 + ipy*1024]); ++ } ++ ++ // 4) raw-ADC photon map accumulation (kept on the display cadence: ++ // one full-frame add per displayed frame is plenty for raw mode) ++ if (!dophotonmap && show2Ds) { ++ for (ipx = 0; ipx < 1024; ipx++) { ++ for (ipy = 0; ipy < 512; ipy++) { ++ his3000->Fill(float(ipx), float(ipy), ++ adc_image[ipx + ipy * 1024]); + } + } ++ } + +- if (show2Ds) { + #ifdef JFSTRX +- jungfrauLGADStrixelsData *decoder = new jungfrauLGADStrixelsData(); +- int group; +- for (ipx = 0; ipx < nx; ipx++) { +- for (ipy = 0; ipy < ny; ipy++) { +- i = int(((decoder->dataMap[ipy][ipx]) - sizeof(header)) / 2); +- group = decoder->groupmap[ipy][ipx]; +- value = (image_data[i] & 0x3fff) - fpeded[i]; +- static const int mc1g1y_start = 0; +- static const int mc1g2y_start = g1_nrows; +- static const int mc1g3y_start = g1_nrows + g2_nrows; +- static const int mc6g3y_start = g1_nrows + g2_nrows + g3_nrows; +- static const int mc6g2y_start = mc6g3y_start + g3_nrows; +- static const int mc6g1y_start = mc6g3y_start + g3_nrows + g2_nrows; +- if (group == 0) { +- if (ipy < 165) his1000->Fill(float(ipx), float(ipy - mc1g1y_start), value); +- if (ipy >= mc6g1y_start) his1060->Fill(float(ipx), float(ipy - mc6g1y_start), value); +- } +- if (group == 1) { +- if (ipy < (165+320)) +- his1001->Fill(float(ipx), float(ipy - mc1g2y_start), value); +- if (ipy >= mc6g2y_start && ipy < mc6g1y_start) { +- his4500->Fill(value, oldvalue, 1.0); +- oldvalue = value; +- his1061->Fill(float(ipx), float(ipy - mc6g2y_start), value); +- } +- } +- if (group == 2) { +- if (ipy < (165+320+476)) +- his1002->Fill(float(ipx), float(ipy - mc1g3y_start), value); +- if (ipy >= mc6g3y_start && ipy < mc6g2y_start) +- his1062->Fill(float(ipx), float(ipy - mc6g3y_start), value); +- } +- gain_image[ipx + ipy * nx] = gain; +- } +- } +- // T vs B / L vs R +- his4500->Reset(); +- for (ipx = 0; ipx < nx; ipx++) { +- his4500->Fill(adc_image[ipx + (ny-21)*nx], adc_image[ipx + 20*nx]); +- } +- hisLvsR->Reset(); ++ if (show2Ds) { ++ jungfrauLGADStrixelsData *decoder = new jungfrauLGADStrixelsData(); ++ int group; ++ for (ipx = 0; ipx < nx; ipx++) { + for (ipy = 0; ipy < ny; ipy++) { +- hisLvsR->Fill(adc_image[20 + ipy*nx], adc_image[nx-21 + ipy*nx]); +- } +- m_adcView->setImage(adc_image, nx, ny); +- m_gainView->setImage(gain_image, nx, ny); +-#else +- for (ipx = 0; ipx < nx; ipx++) { +- for (ipy = 0; ipy < ny; ipy++) { +- i = ipx + ipy * 1024; +- value = (image_data[i] & 0x3fff) - fpeded[i]; +- if ((i % 256 == 0) || (i % 256 == 255)) value = int(value / factor); +- if ((i / 1024 == 255) || (i / 1024 == 256) || (i / 1024 == 767) || (i / 1024 == 768)) +- value = int(value / factor); +- adc_image[ipx + ipy * 1024] = value; +- gain_image[ipx + ipy * 1024] = gain; ++ i = int(((decoder->dataMap[ipy][ipx]) - sizeof(header)) / 2); ++ group = decoder->groupmap[ipy][ipx]; ++ value = (image_data[i] & 0x3fff) - fpeded[i]; ++ static const int mc1g1y_start = 0; ++ static const int mc1g2y_start = g1_nrows; ++ static const int mc1g3y_start = g1_nrows + g2_nrows; ++ static const int mc6g3y_start = g1_nrows + g2_nrows + g3_nrows; ++ static const int mc6g2y_start = mc6g3y_start + g3_nrows; ++ static const int mc6g1y_start = mc6g3y_start + g3_nrows + g2_nrows; ++ if (group == 0) { ++ if (ipy < 165) his1000->Fill(float(ipx), float(ipy - mc1g1y_start), value); ++ if (ipy >= mc6g1y_start) his1060->Fill(float(ipx), float(ipy - mc6g1y_start), value); + } +- } +-#endif +- for (ipx = 0; ipx < 1024; ipx++) { +- for (ipy = 0; ipy < 512; ipy++) { +- i = ipx + ipy * 1024; +- value = (image_data[i] & 0x3fff) - fpeded[i]; +- if (!dophotonmap) his3000->Fill(float(ipx), float(ipy), value); +- if (dophotonmap) his3000->Fill(float(ipx), float(ipy), float(hchptr[i])); ++ if (group == 1) { ++ if (ipy < (165+320)) ++ his1001->Fill(float(ipx), float(ipy - mc1g2y_start), value); ++ if (ipy >= mc6g2y_start && ipy < mc6g1y_start) { ++ his4500->Fill(value, oldvalue, 1.0); ++ oldvalue = value; ++ his1061->Fill(float(ipx), float(ipy - mc6g2y_start), value); ++ } + } ++ if (group == 2) { ++ if (ipy < (165+320+476)) ++ his1002->Fill(float(ipx), float(ipy - mc1g3y_start), value); ++ if (ipy >= mc6g3y_start && ipy < mc6g2y_start) ++ his1062->Fill(float(ipx), float(ipy - mc6g3y_start), value); ++ } ++ gain_image[ipx + ipy * nx] = gain; + } +- // T vs B / L vs R +- his4500->Reset(); +- for (ipx = 0; ipx < 1024; ipx++) { +- his4500->Fill(adc_image[ipx + (512-21)*1024], adc_image[ipx + 20*1024]); +- } +- hisLvsR->Reset(); +- for (ipy = 0; ipy < 512; ipy++) { +- hisLvsR->Fill(adc_image[20 + ipy*1024], adc_image[1024-21 + ipy*1024]); +- } +- m_adcView->setImage(adc_image, nx, ny); +- m_gainView->setImage(gain_image, nx, ny); +- } +- +- if (doRootDraw) { +- for (i = 0; i < NCH; i++) hchptr[i] = 0; +- Plot2DHistos(); +- Plot1DHistos(); + } ++ m_adcView->setImage(adc_image, nx, ny); ++ m_gainView->setImage(gain_image, nx, ny); ++ } ++#else ++ if (show2Ds) { ++ m_adcView->setImage(adc_image, nx, ny); ++ m_gainView->setImage(gain_image, nx, ny); + } ++#endif + +- if ((framesinstream % NPRO) == NPRO - 1) +- gSystem->ProcessEvents(); + } + ++ + void MainWindow::closeEvent(QCloseEvent *event) { + stop(); + event->accept(); +@@ -852,9 +1045,10 @@ void MainWindow::setRanges(int amin, int amax, int pmin, int pmax, int cmin, int + fixranges = true; + cout << "Ranges set: ADC [" << adcmin << "," << adcmax << "] PM [" << pmmin << "," << pmmax + << "] ChipX [" << chipxmin << "," << chipxmax << "]" << endl; +- for (int ih = 0; ih < 8; ih++) { +- hchip[ih]->GetXaxis()->SetRangeUser(chipxmin, chipxmax); +- } ++ RootCmd c; ++ c.type = RootCmdType::SetChipRange; ++ c.a = chipxmin; c.b = chipxmax; ++ pushRootCmd(c); + } + + void MainWindow::showRateDialog() { +@@ -867,26 +1061,19 @@ void MainWindow::showRateDialog() { + mainRateSpin->setValue(HDraw_every); + form->addRow("Main image (frames):", mainRateSpin); + +- auto *rootMulSpin = new QSpinBox(&dialog); +- rootMulSpin->setRange(1, 1000); +- rootMulSpin->setValue(rootDrawDivider); +- form->addRow("ROOT plots (x main draws):", rootMulSpin); +- +- auto *effLabel = new QLabel(QString("= every %1 frames").arg(HDraw_every * rootDrawDivider), &dialog); +- form->addRow("Effective ROOT rate:", effLabel); ++ auto *rootMsSpin = new QSpinBox(&dialog); ++ rootMsSpin->setRange(50, 60000); ++ rootMsSpin->setSingleStep(100); ++ rootMsSpin->setValue(rootUpdateMs); ++ form->addRow("ROOT update (ms):", rootMsSpin); + +- auto *fillChk = new QCheckBox("Fill histograms every received frame", &dialog); +- fillChk->setChecked(fillPerFrame); +- form->addRow(fillChk); ++ auto *effLabel = new QLabel(QString("ROOT redraws every %1 ms when frames arrived").arg(rootUpdateMs), &dialog); ++ effLabel->setWordWrap(true); ++ form->addRow(effLabel); + +- // Update effective label when spin values change +- QObject::connect(mainRateSpin, static_cast(&QSpinBox::valueChanged), +- [mainRateSpin, rootMulSpin, effLabel](int) { +- effLabel->setText(QString("= every %1 frames").arg(mainRateSpin->value() * rootMulSpin->value())); +- }); +- QObject::connect(rootMulSpin, static_cast(&QSpinBox::valueChanged), +- [mainRateSpin, rootMulSpin, effLabel](int) { +- effLabel->setText(QString("= every %1 frames").arg(mainRateSpin->value() * rootMulSpin->value())); ++ QObject::connect(rootMsSpin, static_cast(&QSpinBox::valueChanged), ++ [effLabel](int ms) { ++ effLabel->setText(QString("ROOT redraws every %1 ms when frames arrived").arg(ms)); + }); + + auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); +@@ -896,12 +1083,9 @@ void MainWindow::showRateDialog() { + + if (dialog.exec() == QDialog::Accepted) { + HDraw_every = mainRateSpin->value(); +- rootDrawDivider = rootMulSpin->value(); +- fillPerFrame = fillChk->isChecked(); +- drawLabel->setText(QString("Draw every: %1 ROOT: %2x%1=%3") +- .arg(HDraw_every).arg(rootDrawDivider).arg(HDraw_every * rootDrawDivider)); +- cout << "Rates: main=" << HDraw_every << " rootMul=" << rootDrawDivider +- << " fillPerFrame=" << fillPerFrame << endl; ++ rootUpdateMs = rootMsSpin->value(); ++ updateDrawLabel(); ++ cout << "Rates: main=" << HDraw_every << " rootMs=" << rootUpdateMs << endl; + } + } + +@@ -909,6 +1093,12 @@ void MainWindow::updateStatusBar() { + frameLabel->setText(QString("Frame: %1").arg(framesinstream)); + } + ++void MainWindow::updateDrawLabel() { ++ if (drawLabel) ++ drawLabel->setText(QString("Main: every %1 frames | ROOT: every %2 ms") ++ .arg(HDraw_every).arg(rootUpdateMs)); ++} ++ + + // ── Main ──────────────────────────────────────────────────────────────────── + int main(int argc, char* argv[]) +@@ -956,59 +1146,61 @@ void printhelp(){ + cout<< endl << "s=start/pause| p/n=getnewpede/raw | o/O=read/save pede | n=nopede(raw) | r/R=rst His/Axis | +/- = faster/slower ref. |q=exit | U/C sUrf2/Colz " <Reset(); +- hisLvsR->Reset(); +- his3000->Reset(); +- for (i=0;i<8;i++) { +- hchip[i]->Reset(); +- } +- Plot2DHistos(); +- Plot1DHistos(); +-} +- +-void axisreset(){ +- fixranges=false; ++ void historeset(){ // ROOT-thread only (executed via command queue) ++ his4500->Reset(); ++ hisLvsR->Reset(); ++ his3000->Reset(); ++ for (i=0;i<8;i++) { ++ hchip[i]->Reset(); ++ } ++ // keep display clones consistent ++ copyBins(disp3000, his3000); ++ copyBins(disp4500, his4500); ++ copyBins(dispLvsR, hisLvsR); ++ for (int ih = 0; ih < 8; ih++) ++ copyBins(dispChip[ih], hchip[ih]); ++ Plot2DHistos(); ++ Plot1DHistos(); ++ } ++ ++ void axisreset(){ // ROOT-thread only (executed via command queue) ++ fixranges=false; + #ifdef JFSTRX +- his1000->GetXaxis()->UnZoom(); +- his1000->GetYaxis()->UnZoom(); +- his1000->GetZaxis()->UnZoom(); +- his1001->GetXaxis()->UnZoom(); +- his1001->GetYaxis()->UnZoom(); +- his1001->GetZaxis()->UnZoom(); +- his1002->GetXaxis()->UnZoom(); +- his1002->GetYaxis()->UnZoom(); +- his1002->GetZaxis()->UnZoom(); +- his1060->GetXaxis()->UnZoom(); +- his1060->GetYaxis()->UnZoom(); +- his1060->GetZaxis()->UnZoom(); +- his1061->GetXaxis()->UnZoom(); +- his1061->GetYaxis()->UnZoom(); +- his1061->GetZaxis()->UnZoom(); +- his1062->GetXaxis()->UnZoom(); +- his1062->GetYaxis()->UnZoom(); +- his1062->GetZaxis()->UnZoom(); +-#else +- his1000->GetXaxis()->UnZoom(); +- his1000->GetYaxis()->UnZoom(); +- his1000->GetZaxis()->UnZoom(); ++ disp1000->GetXaxis()->UnZoom(); ++ disp1000->GetYaxis()->UnZoom(); ++ disp1000->GetZaxis()->UnZoom(); ++ disp1001->GetXaxis()->UnZoom(); ++ disp1001->GetYaxis()->UnZoom(); ++ disp1001->GetZaxis()->UnZoom(); ++ disp1002->GetXaxis()->UnZoom(); ++ disp1002->GetYaxis()->UnZoom(); ++ disp1002->GetZaxis()->UnZoom(); ++ disp1060->GetXaxis()->UnZoom(); ++ disp1060->GetYaxis()->UnZoom(); ++ disp1060->GetZaxis()->UnZoom(); ++ disp1061->GetXaxis()->UnZoom(); ++ disp1061->GetYaxis()->UnZoom(); ++ disp1061->GetZaxis()->UnZoom(); ++ disp1062->GetXaxis()->UnZoom(); ++ disp1062->GetYaxis()->UnZoom(); ++ disp1062->GetZaxis()->UnZoom(); + #endif +- his2000->GetXaxis()->UnZoom(); +- his2000->GetYaxis()->UnZoom(); +- his3000->GetZaxis()->UnZoom(); +- for (i=0;i<8;i++) { +- hchip[i]->GetXaxis()->UnZoom(); +- hchip[i]->GetYaxis()->UnZoom(); +- } +- his4500->GetXaxis()->UnZoom(); +- his4500->GetYaxis()->UnZoom(); +- his4500->GetZaxis()->UnZoom(); +- hisLvsR->GetXaxis()->UnZoom(); +- hisLvsR->GetYaxis()->UnZoom(); +- hisLvsR->GetZaxis()->UnZoom(); +- Plot2DHistos(); +- Plot1DHistos(); +-} ++ disp3000->GetXaxis()->UnZoom(); ++ disp3000->GetYaxis()->UnZoom(); ++ disp3000->GetZaxis()->UnZoom(); ++ for (i=0;i<8;i++) { ++ dispChip[i]->GetXaxis()->UnZoom(); ++ dispChip[i]->GetYaxis()->UnZoom(); ++ } ++ disp4500->GetXaxis()->UnZoom(); ++ disp4500->GetYaxis()->UnZoom(); ++ disp4500->GetZaxis()->UnZoom(); ++ dispLvsR->GetXaxis()->UnZoom(); ++ dispLvsR->GetYaxis()->UnZoom(); ++ dispLvsR->GetZaxis()->UnZoom(); ++ Plot2DHistos(); ++ Plot1DHistos(); ++ } + + void LoadPaletteFalse(){ + const Int_t NRGBs = 5; +@@ -1043,43 +1235,43 @@ void LoadPaletteBW(float gammatune){ + } + + void Plot1DHistos(void){ +- if (hchip[0]->GetXaxis()->GetLast()!=oldh0xlast){ +- oldh0xlast=hchip[0]->GetXaxis()->GetLast(); +- oldh0xfirst=hchip[0]->GetXaxis()->GetFirst(); +- for (int ipad=1; ipad<8;ipad++) { +- hchip[ipad]->GetXaxis()->SetRange(oldh0xfirst,oldh0xlast); +- } +- } +- for (int ipad=0; ipad<8;ipad++) { +- A4->cd(ipad+1); +- gStyle->SetOptStat(1); gPad->SetLogy(); +- hchip[ipad%4+(1-int(ipad/4))*4]->Draw(); +- } +- A4->cd(); +- A4->Update(); +-} ++ if (dispChip[0]->GetXaxis()->GetLast()!=oldh0xlast){ ++ oldh0xlast=dispChip[0]->GetXaxis()->GetLast(); ++ oldh0xfirst=dispChip[0]->GetXaxis()->GetFirst(); ++ for (int ipad=1; ipad<8;ipad++) { ++ dispChip[ipad]->GetXaxis()->SetRange(oldh0xfirst,oldh0xlast); ++ } ++ } ++ for (int ipad=0; ipad<8;ipad++) { ++ A4->cd(ipad+1); ++ gStyle->SetOptStat(1); gPad->SetLogy(); ++ dispChip[ipad%4+(1-int(ipad/4))*4]->Draw(); ++ } ++ A4->cd(); ++ A4->Update(); ++ } + + void Plot2DHistos(void){ + gStyle->SetOptStat(0); + if (fixranges) { +- his3000->GetZaxis()->SetRangeUser(float(pmmin),float(pmmax)); ++ disp3000->GetZaxis()->SetRangeUser(float(pmmin),float(pmmax)); + } + #ifdef JFSTRX + p5->cd(); +- his1000->Draw("colz"); ++ disp1000->Draw("colz"); + p3->cd(); +- his1001->Draw("colz"); ++ disp1001->Draw("colz"); + p1->cd(); +- his1002->Draw("colz"); ++ disp1002->Draw("colz"); + p2->cd(); +- his1060->Draw("colz"); ++ disp1060->Draw("colz"); + p4->cd(); +- his1061->Draw("colz"); ++ disp1061->Draw("colz"); + p6->cd(); +- his1062->Draw("colz"); ++ disp1062->Draw("colz"); + #endif + A5->cd(); +- his3000->Draw("colz"); ++ disp3000->Draw("colz"); + box = new TBox(ROIxmin,ROIymin,ROIxmax,ROIymax); + box->SetLineColor(kRed); + box->SetFillStyle(0); +@@ -1090,8 +1282,8 @@ void Plot2DHistos(void){ + if (!A6->GetListOfPrimitives() || !A6->GetListOfPrimitives()->GetSize()) { + A6->Divide(1,2); + } +- A6->cd(1); his4500->Draw("colz"); +- A6->cd(2); hisLvsR->Draw("colz"); ++ A6->cd(1); disp4500->Draw("colz"); ++ A6->cd(2); dispLvsR->Draw("colz"); + A6->Update(); + } + +diff --git a/slsDetectorCalibration/jungfrauExecutables/onlinedisp_zmq.h b/slsDetectorCalibration/jungfrauExecutables/onlinedisp_zmq.h +index 14c593e8b..082f9cdf0 100644 +--- a/slsDetectorCalibration/jungfrauExecutables/onlinedisp_zmq.h ++++ b/slsDetectorCalibration/jungfrauExecutables/onlinedisp_zmq.h +@@ -173,7 +173,6 @@ bool gain_flag; + bool bw_flag; + bool fill2Ds; + bool show2Ds; +-bool fill1Ds; + bool pede_flag; + bool dophotonmap; + +@@ -230,8 +229,7 @@ void readpede(void); + void tryconnect(void) ; + + int HDraw_every; +-int rootDrawDivider = 10; +-bool fillPerFrame = false; ++int rootUpdateMs = 1000; + + + float oldh0xfirst,oldh0xlast; +@@ -317,15 +315,21 @@ private: + void processData(); + void startWorker(); + void stopWorker(); ++ void rootDrawLoop(); + void showRangeDialog(); + void showRateDialog(); + void updateStatusBar(); ++ void updateDrawLabel(); + void setRanges(int adcmin, int adcmax, int pmmin, int pmmax, int cmin = 0, int cmax = 8500); + + // Worker thread + std::thread *workerThread = nullptr; + std::atomic workerRunning{false}; + ++ // ROOT plotting thread (periodic redraw off the Qt thread) ++ std::thread *rootDrawThread = nullptr; ++ std::atomic rootThreadRun{false}; ++ + // Thread-safe data + ThreadData threadData; + +diff --git a/zsh_autocomplete.sh b/zsh_autocomplete.sh +deleted file mode 120000 +index 143520002..000000000 +--- a/zsh_autocomplete.sh ++++ /dev/null +@@ -1 +0,0 @@ +-slsDetectorSoftware/generator/autocomplete/zsh_autocomplete.sh +\ No newline at end of file +diff --git a/zsh_autocomplete.sh b/zsh_autocomplete.sh +new file mode 100644 +index 000000000..64598dd2b +--- /dev/null ++++ b/zsh_autocomplete.sh +@@ -0,0 +1,3358 @@ ++# GENERATED FILE - DO NOT EDIT ++# ANY CHANGES TO THIS FILE WILL BE OVERWRITTEN ++ ++_sd() { ++ ++ ++local SLS_COMMANDS=" acquire activate adcclk adcenable adcenable10g adcindex adcinvert adclist adcname adcphase adcpipeline adcreg adcvpp apulse asamples autocompdisable badchannels blockingtrigger burstmode burstperiod bursts burstsl bustest cdsgain chipversion clearbit clearbusy clientversion clkdiv clkfreq clkphase collectionmode column compdisabletime confadc config configtransceiver counters currentsource dac dacindex daclist dacname dacvalues datastream dbitclk dbitphase dbitpipeline defaultdac defaultpattern delay delayl detectorserverversion detsize diodelay dpulse dr drlist dsamples execcommand exptime exptime1 exptime2 exptime3 extrastoragecells extsampling extsamplingsrc extsig fformat filtercells filterresistor findex firmwaretest firmwareversion fliprows flowcontrol10g fmaster fname foverwrite fpath framecounter frames framesl frametime free fwrite gaincaps gainmode gappixels gatedelay gatedelay1 gatedelay2 gatedelay3 gates getbit hardwareversion highvoltage hostname im_a im_b im_c im_d im_io imagetest initialchecks inj_ch interpolation interruptsubframe kernelversion lastclient led lock master maxadcphaseshift maxclkphaseshift maxdbitphaseshift measuredperiod measuredsubperiod moduleid nextframenumber nmod numinterfaces overflow packageversion parallel parameters partialreset patfname patioctrl patlimits patloop patloop0 patloop1 patloop2 patmask patnloop patnloop0 patnloop1 patnloop2 patsetbit pattern patternstart patwait patwait0 patwait1 patwait2 patwaittime patwaittime0 patwaittime1 patwaittime2 patword pedestalmode period periodl polarity port powerchip powerindex powerlist powername powervalues programfpga pulse pulsechip pulsenmove pumpprobe quad ratecorr readnrows readout readoutspeed readoutspeedlist rebootcontroller reg resetdacs resetfpga romode row runclk runtime rx_arping rx_clearroi rx_dbitlist rx_dbitoffset rx_dbitreorder rx_discardpolicy rx_fifodepth rx_frameindex rx_framescaught rx_framesperfile rx_hostname rx_jsonaddheader rx_jsonpara rx_lastclient rx_lock rx_missingpackets rx_padding rx_printconfig rx_realudpsocksize rx_roi rx_silent rx_start rx_status rx_stop rx_tcpport rx_threads rx_udpsocksize rx_version rx_zmqfreq rx_zmqhwm rx_zmqip rx_zmqport rx_zmqstartfnum rx_zmqstream samples savepattern scan scanerrmsg selinterface serialnumber setbit settings settingslist settingspath signalindex signallist signalname sleep slowadc slowadcindex slowadclist slowadcname slowadcvalues start status stop stopport storagecell_delay storagecell_start subdeadtime subexptime sync syncclk temp_10ge temp_adc temp_control temp_dcdc temp_event temp_fpga temp_fpgaext temp_fpgafl temp_fpgafr temp_slowadc temp_sodl temp_sodr temp_threshold templist tempvalues tengiga threshold thresholdnotb timing timing_info_decoder timinglist timingsource top transceiverenable trigger triggers triggersl trimbits trimen trimval tsamples txdelay txdelay_frame txdelay_left txdelay_right type udp_cleardst udp_dstip udp_dstip2 udp_dstlist udp_dstmac udp_dstmac2 udp_dstport udp_dstport2 udp_firstdst udp_numdst udp_reconfigure udp_srcip udp_srcip2 udp_srcmac udp_srcmac2 udp_validate update updatedetectorserver updatekernel updatemode user v_a v_b v_c v_chip v_d v_io v_limit vchip_comp_adc vchip_comp_fe vchip_cs vchip_opa_1st vchip_opa_fd vchip_ref_comp_fe versions veto vetoalg vetofile vetophoton vetoref vetostream virtual vm_a vm_b vm_c vm_d vm_io zmqhwm zmqip zmqport " ++__acquire() { ++FCN_RETURN="" ++return 0 ++} ++__activate() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__adcclk() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__adcenable() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__adcenable10g() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__adcindex() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__adcinvert() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__adclist() { ++FCN_RETURN="" ++return 0 ++} ++__adcname() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__adcphase() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="deg" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="deg" ++fi ++fi ++return 0 ++} ++__adcpipeline() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__adcreg() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__adcvpp() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="mV mv" ++fi ++fi ++return 0 ++} ++__apulse() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__asamples() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__autocompdisable() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__badchannels() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__blockingtrigger() { ++FCN_RETURN="" ++return 0 ++} ++__burstmode() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="burst_external burst_internal cw_external cw_internal" ++fi ++fi ++return 0 ++} ++__burstperiod() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__bursts() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__burstsl() { ++FCN_RETURN="" ++return 0 ++} ++__bustest() { ++FCN_RETURN="" ++return 0 ++} ++__cdsgain() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__chipversion() { ++FCN_RETURN="" ++return 0 ++} ++__clearbit() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="--validate" ++fi ++fi ++return 0 ++} ++__clearbusy() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="--validate" ++fi ++fi ++return 0 ++} ++__clientversion() { ++FCN_RETURN="" ++return 0 ++} ++__clkdiv() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__clkfreq() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__clkphase() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="deg" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="deg" ++fi ++fi ++return 0 ++} ++__collectionmode() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="electron hole" ++fi ++fi ++return 0 ++} ++__column() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__compdisabletime() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__confadc() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__config() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__configtransceiver() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__counters() { ++FCN_RETURN="" ++return 0 ++} ++__currentsource() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="fix nofix" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "5" ]]; then ++FCN_RETURN="low normal" ++fi ++fi ++return 0 ++} ++__dac() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="mV mv" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="mV mv" ++fi ++fi ++return 0 ++} ++__dacindex() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__daclist() { ++FCN_RETURN="" ++return 0 ++} ++__dacname() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__dacvalues() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="mV mv" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="mV mv" ++fi ++fi ++return 0 ++} ++__datastream() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="bottom left right top" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="bottom left right top" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__dbitclk() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__dbitphase() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="deg" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="deg" ++fi ++fi ++return 0 ++} ++__dbitpipeline() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__defaultdac() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="`sls_detector_get settingslist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="`sls_detector_get settingslist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++fi ++return 0 ++} ++__defaultpattern() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="`sls_detector_get settingslist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++fi ++return 0 ++} ++__delay() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__delayl() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__detectorserverversion() { ++FCN_RETURN="" ++return 0 ++} ++__detsize() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__diodelay() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__dpulse() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__dr() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__drlist() { ++FCN_RETURN="" ++return 0 ++} ++__dsamples() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__execcommand() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__exptime() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__exptime1() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__exptime2() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__exptime3() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__extrastoragecells() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__extsampling() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__extsamplingsrc() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__extsig() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="inversion_off inversion_on trigger_in_falling_edge trigger_in_rising_edge" ++fi ++fi ++return 0 ++} ++__fformat() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="binary hdf5" ++fi ++fi ++return 0 ++} ++__filtercells() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__filterresistor() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__findex() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__firmwaretest() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__firmwareversion() { ++FCN_RETURN="" ++return 0 ++} ++__fliprows() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__flowcontrol10g() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__fmaster() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__fname() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__foverwrite() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__fpath() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__framecounter() { ++FCN_RETURN="" ++return 0 ++} ++__frames() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__framesl() { ++FCN_RETURN="" ++return 0 ++} ++__frametime() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__free() { ++FCN_RETURN="" ++return 0 ++} ++__fwrite() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__gaincaps() { ++FCN_RETURN="" ++return 0 ++} ++__gainmode() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="dynamic fixg0 fixg1 fixg2 forceswitchg1 forceswitchg2" ++fi ++fi ++return 0 ++} ++__gappixels() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__gatedelay() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__gatedelay1() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__gatedelay2() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__gatedelay3() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__gates() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__getbit() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__hardwareversion() { ++FCN_RETURN="" ++return 0 ++} ++__highvoltage() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__hostname() { ++FCN_RETURN="" ++return 0 ++} ++__im_a() { ++FCN_RETURN="" ++return 0 ++} ++__im_b() { ++FCN_RETURN="" ++return 0 ++} ++__im_c() { ++FCN_RETURN="" ++return 0 ++} ++__im_d() { ++FCN_RETURN="" ++return 0 ++} ++__im_io() { ++FCN_RETURN="" ++return 0 ++} ++__imagetest() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__initialchecks() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__inj_ch() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__interpolation() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__interruptsubframe() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__kernelversion() { ++FCN_RETURN="" ++return 0 ++} ++__lastclient() { ++FCN_RETURN="" ++return 0 ++} ++__led() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__lock() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__master() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__maxadcphaseshift() { ++FCN_RETURN="" ++return 0 ++} ++__maxclkphaseshift() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__maxdbitphaseshift() { ++FCN_RETURN="" ++return 0 ++} ++__measuredperiod() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__measuredsubperiod() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__moduleid() { ++FCN_RETURN="" ++return 0 ++} ++__nextframenumber() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__nmod() { ++FCN_RETURN="" ++return 0 ++} ++__numinterfaces() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__overflow() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__packageversion() { ++FCN_RETURN="" ++return 0 ++} ++__parallel() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__parameters() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__partialreset() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__patfname() { ++FCN_RETURN="" ++return 0 ++} ++__patioctrl() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patlimits() { ++FCN_RETURN="" ++return 0 ++} ++__patloop() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patloop0() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patloop1() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patloop2() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patmask() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patnloop() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patnloop0() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patnloop1() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patnloop2() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patsetbit() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__pattern() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__patternstart() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__patwait() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patwait0() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patwait1() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patwait2() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__patwaittime() { ++FCN_RETURN="" ++return 0 ++} ++__patwaittime0() { ++FCN_RETURN="" ++return 0 ++} ++__patwaittime1() { ++FCN_RETURN="" ++return 0 ++} ++__patwaittime2() { ++FCN_RETURN="" ++return 0 ++} ++__patword() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__pedestalmode() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN=" 0" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__period() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__periodl() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__polarity() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="neg pos" ++fi ++fi ++return 0 ++} ++__port() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__powerchip() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__powerindex() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__powerlist() { ++FCN_RETURN="" ++return 0 ++} ++__powername() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__powervalues() { ++FCN_RETURN="" ++return 0 ++} ++__programfpga() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="--force-delete-normal-file" ++fi ++fi ++return 0 ++} ++__pulse() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="--force-delete-normal-file" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__pulsechip() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__pulsenmove() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__pumpprobe() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__quad() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__ratecorr() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__readnrows() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__readout() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__readoutspeed() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1 108 144 2 full_speed half_speed quarter_speed" ++fi ++fi ++return 0 ++} ++__readoutspeedlist() { ++FCN_RETURN="" ++return 0 ++} ++__rebootcontroller() { ++FCN_RETURN="" ++return 0 ++} ++__reg() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="--validate" ++fi ++fi ++return 0 ++} ++__resetdacs() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="--validate" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="hard" ++fi ++fi ++return 0 ++} ++__resetfpga() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="hard" ++fi ++fi ++return 0 ++} ++__romode() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="analog analog_digital digital digital_transceiver transceiver" ++fi ++fi ++return 0 ++} ++__row() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__runclk() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__runtime() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__rx_arping() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__rx_clearroi() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__rx_dbitlist() { ++FCN_RETURN="" ++return 0 ++} ++__rx_dbitoffset() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__rx_dbitreorder() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__rx_discardpolicy() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="discardempty discardpartial nodiscard" ++fi ++fi ++return 0 ++} ++__rx_fifodepth() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__rx_frameindex() { ++FCN_RETURN="" ++return 0 ++} ++__rx_framescaught() { ++FCN_RETURN="" ++return 0 ++} ++__rx_framesperfile() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__rx_hostname() { ++FCN_RETURN="" ++return 0 ++} ++__rx_jsonaddheader() { ++FCN_RETURN="" ++return 0 ++} ++__rx_jsonpara() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__rx_lastclient() { ++FCN_RETURN="" ++return 0 ++} ++__rx_lock() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__rx_missingpackets() { ++FCN_RETURN="" ++return 0 ++} ++__rx_padding() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__rx_printconfig() { ++FCN_RETURN="" ++return 0 ++} ++__rx_realudpsocksize() { ++FCN_RETURN="" ++return 0 ++} ++__rx_roi() { ++FCN_RETURN="" ++return 0 ++} ++__rx_silent() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__rx_start() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__rx_status() { ++FCN_RETURN="" ++return 0 ++} ++__rx_stop() { ++FCN_RETURN="" ++return 0 ++} ++__rx_tcpport() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__rx_threads() { ++FCN_RETURN="" ++return 0 ++} ++__rx_udpsocksize() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__rx_version() { ++FCN_RETURN="" ++return 0 ++} ++__rx_zmqfreq() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__rx_zmqhwm() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__rx_zmqip() { ++FCN_RETURN="" ++return 0 ++} ++__rx_zmqport() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__rx_zmqstartfnum() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__rx_zmqstream() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__samples() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__savepattern() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__scan() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "5" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "6" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__scanerrmsg() { ++FCN_RETURN="" ++return 0 ++} ++__selinterface() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__serialnumber() { ++FCN_RETURN="" ++return 0 ++} ++__setbit() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="--validate" ++fi ++fi ++return 0 ++} ++__settings() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get settingslist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++fi ++return 0 ++} ++__settingslist() { ++FCN_RETURN="" ++return 0 ++} ++__settingspath() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__signalindex() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__signallist() { ++FCN_RETURN="" ++return 0 ++} ++__signalname() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__sleep() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__slowadc() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__slowadcindex() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__slowadclist() { ++FCN_RETURN="" ++return 0 ++} ++__slowadcname() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get daclist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__slowadcvalues() { ++FCN_RETURN="" ++return 0 ++} ++__start() { ++FCN_RETURN="" ++return 0 ++} ++__status() { ++FCN_RETURN="" ++return 0 ++} ++__stop() { ++FCN_RETURN="" ++return 0 ++} ++__stopport() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__storagecell_delay() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__storagecell_start() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__subdeadtime() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__subexptime() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="ms ns s us" ++fi ++fi ++return 0 ++} ++__sync() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__syncclk() { ++FCN_RETURN="" ++return 0 ++} ++__temp_10ge() { ++FCN_RETURN="" ++return 0 ++} ++__temp_adc() { ++FCN_RETURN="" ++return 0 ++} ++__temp_control() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__temp_dcdc() { ++FCN_RETURN="" ++return 0 ++} ++__temp_event() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__temp_fpga() { ++FCN_RETURN="" ++return 0 ++} ++__temp_fpgaext() { ++FCN_RETURN="" ++return 0 ++} ++__temp_fpgafl() { ++FCN_RETURN="" ++return 0 ++} ++__temp_fpgafr() { ++FCN_RETURN="" ++return 0 ++} ++__temp_slowadc() { ++FCN_RETURN="" ++return 0 ++} ++__temp_sodl() { ++FCN_RETURN="" ++return 0 ++} ++__temp_sodr() { ++FCN_RETURN="" ++return 0 ++} ++__temp_threshold() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__templist() { ++FCN_RETURN="" ++return 0 ++} ++__tempvalues() { ++FCN_RETURN="" ++return 0 ++} ++__tengiga() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__threshold() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="`sls_detector_get settingslist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "5" ]]; then ++FCN_RETURN="`sls_detector_get settingslist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++fi ++return 0 ++} ++__thresholdnotb() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="`sls_detector_get settingslist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "5" ]]; then ++FCN_RETURN="`sls_detector_get settingslist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++fi ++return 0 ++} ++__timing() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="`sls_detector_get timinglist | sed -e 's/.*\[\(.*\)\].*/\1/' | sed 's/,//g'`" ++fi ++fi ++return 0 ++} ++__timing_info_decoder() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="shine swissfel" ++fi ++fi ++return 0 ++} ++__timinglist() { ++FCN_RETURN="" ++return 0 ++} ++__timingsource() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="external internal" ++fi ++fi ++return 0 ++} ++__top() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__transceiverenable() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__trigger() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__triggers() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__triggersl() { ++FCN_RETURN="" ++return 0 ++} ++__trimbits() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__trimen() { ++FCN_RETURN="" ++return 0 ++} ++__trimval() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__tsamples() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__txdelay() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__txdelay_frame() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__txdelay_left() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__txdelay_right() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__type() { ++FCN_RETURN="" ++return 0 ++} ++__udp_cleardst() { ++FCN_RETURN="" ++return 0 ++} ++__udp_dstip() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__udp_dstip2() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__udp_dstlist() { ++FCN_RETURN="" ++return 0 ++} ++__udp_dstmac() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__udp_dstmac2() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__udp_dstport() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__udp_dstport2() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__udp_firstdst() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__udp_numdst() { ++FCN_RETURN="" ++return 0 ++} ++__udp_reconfigure() { ++FCN_RETURN="" ++return 0 ++} ++__udp_srcip() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__udp_srcip2() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__udp_srcmac() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__udp_srcmac2() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__udp_validate() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__update() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__updatedetectorserver() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__updatekernel() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++IS_PATH=1 ++fi ++fi ++return 0 ++} ++__updatemode() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__user() { ++FCN_RETURN="" ++return 0 ++} ++__v_a() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__v_b() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__v_c() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__v_chip() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__v_d() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__v_io() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__v_limit() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__vchip_comp_adc() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__vchip_comp_fe() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__vchip_cs() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__vchip_opa_1st() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__vchip_opa_fd() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__vchip_ref_comp_fe() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__versions() { ++FCN_RETURN="" ++return 0 ++} ++__veto() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="0 1" ++fi ++fi ++return 0 ++} ++__vetoalg() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="10gbe lll none" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="hits raw" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="10gbe lll none" ++fi ++fi ++return 0 ++} ++__vetofile() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__vetophoton() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 1 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "4" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "5" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__vetoref() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__vetostream() { ++FCN_RETURN="" ++return 0 ++} ++__virtual() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++if [[ "${cword}" == "3" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__vm_a() { ++FCN_RETURN="" ++return 0 ++} ++__vm_b() { ++FCN_RETURN="" ++return 0 ++} ++__vm_c() { ++FCN_RETURN="" ++return 0 ++} ++__vm_d() { ++FCN_RETURN="" ++return 0 ++} ++__vm_io() { ++FCN_RETURN="" ++return 0 ++} ++__zmqhwm() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__zmqip() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++__zmqport() { ++FCN_RETURN="" ++if [[ ${IS_GET} -eq 0 ]]; then ++if [[ "${cword}" == "2" ]]; then ++FCN_RETURN="" ++fi ++fi ++return 0 ++} ++ ++ ++ local FCN_RETURN="" ++ local IS_PATH=0 ++ COMPREPLY=() ++ local OPTIONS_NEW="" ++ words=("${COMP_WORDS[@]}") ++ cword=$COMP_CWORD ++ ++ local cur=${words[cword]} ++ # check the action (get or put) ++ case "${words[0]}" in ++ "sls_detector_get" | "g" | "detg") ++ local IS_GET=1 ++ ;; ++ *) ++ local IS_GET=0 ++ ;; ++ esac ++ ++ # if no command is written, autocomplete with the commands ++ if [[ ${cword} -eq 1 ]]; then ++ ++ case "$cur" in ++ [0-9]*) ++ for i in $SLS_COMMANDS; do ++ SLS_COMMANDS_NEW="${SLS_COMMANDS_NEW} ${cur%%:*}:$i" ++ done ++ COMPREPLY=( $( compgen -W "${SLS_COMMANDS_NEW}" -- "$cur" ) );; ++ *) ++ COMPREPLY=( $( compgen -W "$SLS_COMMANDS -h" -- "$cur" ) );; ++ esac ++ return 0 ++ fi ++ ++ if [[ ${cword} -eq 2 ]] && [[ ${words[1]} == "-h" ]]; then ++ COMPREPLY=( $( compgen -W "$SLS_COMMANDS" -- "$cur" ) ) ++ return 0 ++ fi ++ ++ # if a command is written, autocomplete with the options ++ # call the function for the command ++ ++ if [[ "$SLS_COMMANDS" == *"${words[1]##*:}"* ]]; then ++ __"${words[1]##*:}" ++ fi ++ ++ # if IS_PATH is activated, autocomplete with the path ++ if [[ ${IS_PATH} -eq 1 ]]; then ++ COMPREPLY=($(compgen -f -- "${cur}")) ++ return 0 ++ fi ++ ++ # autocomplete with the options ++ COMPREPLY=($(compgen -W "${FCN_RETURN}" -- "${cur}")) ++ ++ ++ ++} ++ ++complete -F _sd -o filenames sls_detector_get ++complete -F _sd -o filenames g ++complete -F _sd -o filenames detg ++ ++complete -F _sd -o filenames sls_detector_put ++complete -F _sd -o filenames p ++complete -F _sd -o filenames detp ++ ++complete -F _sd -o filenames sls_detector ++complete -F _sd -o filenames det diff --git a/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/online_jf.h b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/online_jf.h new file mode 100644 index 000000000..883427d94 --- /dev/null +++ b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/online_jf.h @@ -0,0 +1,1103 @@ +// SPDX-License-Identifier: LGPL-3.0-or-other +// =========================================================================== +// online_jf.h -- merged online Jungfrau display + event finder +// =========================================================================== +// +// This is a rewrite of TWO separate programs into ONE multithreaded process: +// +// onlinedisp_zmq.cpp Qt5 GUI + ROOT plots, subscribed to the filter +// jf_filter.cpp ZMQ sub -> pedestal subtract -> threshold gate -> pub +// jf_event_finder_gui.cpp Qt control window for that filter +// +// The intermediate ZMQ hop between filter and display is gone. A single +// subscriber reads slsReceiver's stream, the gate decides pass/drop on every +// frame, and only passing frames are handed to the display/analysis stage. +// +// Thread map +// ---------- +// main / Qt thread : QApplication::exec, all widgets, button handlers +// acq thread : zmq_recv(header + data), gate counters, optional ZMQ +// republish, hand passing frames to the frame mailbox +// analysis thread : pedestal subtraction, ADC/gain images, and the ONLY +// thread that FILLS the source ROOT histograms +// ROOT thread : canvas painting + gSystem->ProcessEvents; executes the +// user ROOT command queue, paints display clones +// +// Invariants carried over from the originals +// ------------------------------------------ +// * ROOT graphics (Draw/Update/ProcessEvents) run ONLY on the ROOT thread. +// Every other thread posts RootCmd entries. +// * The source histograms (his*, hchip) are filled ONLY by the analysis +// thread; the ROOT thread paints display clones whose bins are copied +// once per redraw. Never paint an object that is being filled. +// * Pedestals are immutable once published (shared_ptr swap), so loading a +// file or acquiring a live pedestal no longer needs stop()/load()/start(). +// * The gate threshold and the display's photon threshold are DIFFERENT +// quantities (they collided as one global named "threshold" only because +// they used to live in two processes). +// +// Argument compatibility (superset of both originals) +// --------------------------------------------------- +// jf_online_display_gui [rx_port] [gate_threshold] [min_pixels] +// [pede_file] [--pub PORT] [--contig] [--no-gate] +// energy < 0 -> raw ADC map (no photon counting), as onlinedisp_zmq. +#ifndef ONLINE_JF_H +#define ONLINE_JF_H + +#define SLS_DETECTOR_JSON_HEADER_VERSION 0x5 + +/* ------------------------------------------------------------------ std */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include // json header inside the zmq stream + +/* ----------------------------------------------------------------- ROOT */ +#include "TCanvas.h" +#include "TH1F.h" +#include "TF1.h" +#include "TH1I.h" +#include "TH2F.h" +#include "TMath.h" +#include "TFile.h" +#include "TStyle.h" +#include "TBox.h" +#include "TSystem.h" +#include "TTimer.h" +#include "TProfile.h" +#include "TColor.h" +#include "TList.h" +#include "TPad.h" +#include + +/* ------------------------------------------------------------------ Qt5 */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; // legacy style of the original sources +using namespace std::chrono; + +/* ------------------------------------------------------------- geometry */ +#define PI 3.14159265 +#define OFFSET 0 +#define NBIN 500 +#define MIN_POS -500.5 // 400.5 +#define MAX_POS 8499.5 //-100.5 + +// A Jungfrau module frame: 1024 x 512 = 524288 pixels (16 bit). +#define NCH 524288 +// The raw module geometry of one UDP port. This is fixed and independent of +// nx/ny, which under JFSTRX hold the strixel *display* geometry: the data, +// gate, ROI and HDF5 paths always work in these raw coordinates. +constexpr int kRawWidth = 1024; +constexpr int kRawHeight = 512; +static_assert(kRawWidth * kRawHeight == NCH, "raw geometry vs NCH"); +// Legacy pedestal-file array size: 3 arrays of 2*NCH floats each +// (the factor 2 is a leftover of an early half-module layout). +#define JF_PEDE_ARRAY_FLOATS (2 * NCH) +#define PEDEFNAME "current_pede.dat" +#define DEFAULT_RX_PORT 30001 +#define DEFAULT_N_PEDE_FRAMES 50 +// Multi-interface mode: slsReceiver with N UDP interfaces ("threads") +// publishes one ZMQ stream per interface on consecutive ports, each carrying +// one Y-slab of the module: +// JungfrauData::nPixelsX = 256*4 = 1024 +// JungfrauData::nPixelsY = (256*2)/numUDPInterfaces -> 256 for N=2 +// so 30001+30002 delivers two 1024x256 halves that stack into 1024x512. +#define JF_MAX_PORTS 8 + +/* --------------------------------------------------------------- config */ +struct Config { + // The photon map threshold is a GUI control in ADU (default 150); the + // photon multiplicity used when filling the map is derived from it + // (photon peak ~= 2 x threshold), exactly as it used to be derived from + // the argument. + int phene = 300; // photon peak in ADC = 2 x the ADU threshold + bool dophotonmap = true; // false -> his3000 shows raw ADC + bool rawMap = false; // --raw-map: show raw ADC instead of counts + std::string rxHost; // slsReceiver host + // ports, ordered BOTTOM -> TOP in the assembled image. One entry is the + // classic single-interface mode; "30001+30002" selects two interfaces. + std::vector rxPorts{DEFAULT_RX_PORT}; + // Slab placement per interface: 0 = unknown (order of the '+' list), + // 1 = bottom half, 2 = top half. Set by the ":bottom"/":top" suffix of a + // port, which is what makes a single interface placeable: "30027:top". + std::vector rxSlab; + float threshold = 100.0f; // GATE threshold (pede-subtracted ADC) + uint64_t minPixels = 5; // GATE min pixels above threshold + bool contiguous = false; // GATE contiguity proxy + bool hotSuppress = false; // GATE hot pixel suppression + double hotFraction = 0.10; // fraction of frames that makes a pixel hot + bool gateEnabled = false; // default: gate off (passthrough); --gate enables + std::string pedeFile = "./current_pede.dat"; + // calibration for the PH spectra (keV axes); empty = ADU only + std::string gainMapFile; + bool gainMapExplicit = false; + // Gate-ROI list: a single small file in the program folder, rewritten on + // every save/load/delete. See roiList*() below. + std::string roiFile = "./roi_list.dat"; + // directory and file-name base for the HDF5 ROI files + JSON sidecars + std::string h5Dir = "."; + std::string h5Base = "jfroi"; + // list of previously used destinations (directory + base), newest first + std::string saveListFile = "./save_dest_list.dat"; + bool pedeExplicit = false; // user named the file explicitly + int pubPort = 0; // 0 -> no republish of gated frames + int pedeFrames = DEFAULT_N_PEDE_FRAMES; + bool printCounters = false; // print the old CLI counter line on stdout + int decimate = 1; // keep 1 frame in N (1/2/4/8), operator choice + bool autoDecimate = false; // let the ladder follow a real backlog + bool acquirePede = false; // start a live pedestal acquisition at boot + bool startSave = false; // press Save at startup (--start-save) + bool applyGain = false; // gain correction on at startup (--apply-gain) + uint64_t saveBudget = 0; // 0 = keep the GUI default (--save-budget) + // Analysis worker threads for the per-pixel loops. + // 0 = auto (cores/4, clamped to 1..8); 1 = single threaded. + // The loops are pure per-pixel work on disjoint ranges with no ROOT + // access, so they scale across cores; see the README. + int analysisWorkers = 0; + // Receive high-water mark per interface. Bounded on purpose: the program + // consumes ~1.2-1.3 kframes/s, so a deep queue would only buy latency and + // make the catch-up skip expensive. Frames beyond this are dropped by + // ZMQ before they cost us anything. + int rxHwm = 64; + // Optional storage-cell selection at startup (--sc N, 0..15; -1 = all) + int scFilter = -1; + // Optional gate ROI at startup (--gate-roi X Y W H), in DATA coordinates: + // row 0 is the BOTTOM row, same as the ROI list. gateRoiX < 0 = unset. + int gateRoiX = -1, gateRoiY = 0, gateRoiW = 0, gateRoiH = 0; +}; + +extern Config g_cfg; + +/* ------------------------------------------------------ pedestal tracking */ +// The pedestals drift a little, so while the beam is off (the signal gate is +// dropping almost everything) a small fraction of each dropped frame is fed +// into a per-pixel running average, and the pedestal actually used is replaced +// when that average has moved far enough. Defaults: +// * a sample of 0.2 % of the pixels of every dropped frame, +// * per-pixel exponential average with a 10 M *pixel-sample* window +// (10 M / NCH ~ 19 updates per pixel, so the weight per update is +// NCH / 10 M ~ 0.05), +// * only while the signal gate drops more than 90 % of the frames reaching +// it, and while "Gate enabled" is on, +// * the used pedestal is replaced when the largest per-pixel difference +// exceeds 5 ADU - at most once per 10 s, and at least once per 10 min, +// * the HDF5 file gets a pedestal image every 10 min while tracking is on. +#define PEDE_TRACK_FRACTION 0.002 +#define PEDE_TRACK_WINDOW 1.0e7 +#define PEDE_TRACK_MAXDIFF 5.0f +#define PEDE_TRACK_MIN_PERIOD_MS 10000 +#define PEDE_TRACK_MAX_PERIOD_MS 600000 +#define PEDE_TRACK_H5_PERIOD_MS 600000 +#define PEDE_TRACK_DROP_RATE 0.90 + +struct PedeSet; // defined with the pedestal store below + +extern std::atomic gPedeTracking; // "Pedestal tracking" button +extern std::atomic gPedeTracked; // true once the pedestal was replaced + +struct PedeTrackStats { + bool on = false; + bool tracked = false; // the pedestal in use came from the tracker + uint64_t samples = 0; // pixel samples fed into the average + uint64_t updates = 0; // times the used pedestal was replaced + double maxDiff = 0.0; // largest |average - used| in ADU + int64_t sinceUpdateMs = 0; +}; +PedeTrackStats pedeTrackerStats(); +// Fraction of the frames reaching the signal gate that it dropped, over the +// last 1 Hz window (falls back to the held value, then the cumulative one, so +// a quiet window does not read as "no drops"). +double pedeTrackSignalDropRate(uint64_t passCur, uint64_t dropCur, + uint64_t passTotal, uint64_t dropTotal); +// Called by the acq thread for a frame the *signal* gate dropped. Cheap when +// tracking is off or when the drop rate is below the threshold. +// The 5 ADU test looks only at the gate ROI (roiSet == false -> whole frame); +// the average itself is still accumulated over the whole frame. +void pedeTrackerFeed(const short *frame, uint64_t nPixels, const PedeSet *peds, + double signalDropRate, int roiX0, int roiY0, int roiX1, + int roiY1); +// Re-primes the running average from the pedestal now in use (called whenever a +// pedestal is installed, so a fresh file starts from itself). +void pedeTrackerReset(); + +/* ------------------------------------------------------------ gain map */ +// Calibration: the gain-map file holds SIX 1024x512 images of doubles, in the +// order g0, g1, g2, hg0, hg1, hg2. In normal mode g0 holds ADU/keV per +// pixel, so a pedestal-corrected ADC value becomes +// keV = ADU / g0[pixel] +// Only the PH spectra are converted; the ADC image, the photon map and the +// HDF5 writer keep working in ADU. KEV_NOMINAL_ADU gives the keV axes the +// same span as the ADU ones (the operator's PH window divided by 40), so the +// two sets of spectra show the same physical region. +#define JF_GAINMAP_IMAGES 6 +#define KEV_NOMINAL_ADU 40.0 +#define JF_GAINMAP_PIXELS (1024 * 512) +struct GainMap { + std::vector img[JF_GAINMAP_IMAGES]; + GainMap() { + for (int i = 0; i < JF_GAINMAP_IMAGES; i++) + img[i].assign(JF_GAINMAP_PIXELS, 0.0); + } + // the map used for the conversion (normal mode) + const double *g0() const { return img[0].data(); } +}; +class GainMapStore { + public: + static GainMapStore &instance(); + // snapshot; empty shared_ptr means "no calibration, spectra stay in ADU" + std::shared_ptr get() const; + void install(const std::shared_ptr &m, const std::string &path); + bool loaded() const; + std::string path() const; + + private: + mutable std::mutex mtx_; + std::shared_ptr cur_; + std::string path_; +}; +// Reads the 6 x 1024 x 512 doubles and installs them. Returns false + error +// when the file is missing or has the wrong size. +bool gainMapLoadFile(const std::string &path, std::string &error); +// true once a usable map is installed (the keV spectra are filled and drawn) +bool gainMapLoaded(); +// Which of the two PH sets the ROOT thread paints: false = ADU (A4/A7), +// true = keV (A8/A9). Both sets are always accumulated; this only selects +// what is on screen, and it falls back to ADU when no gain map is loaded. +extern std::atomic kevSpectra; + +/* ------------------------------------------------------- display state */ +// defined in online_jf_core.cpp +extern short *hchptr; // cumulative photon counted map +extern float factor; +#define PHOTON_THRESHOLD_DEFAULT 150.0f // ADU, Virtual photon counting control +#define PHOTON_THRESHOLD_KEV_DEFAULT 4.0f // keV, same control with gain on +// Photon map for its own Qt window: how many analysed frames between publishes +// (default 2000), and whether it keeps adding up (off by default, so each image +// shows the photons counted since the previous one). +extern std::atomic gPmapEvery; +extern std::atomic gPmapCumulative; +// Saving mode: 0 = gate-passed frames, 1 = the virtual photon counting maps. +// gPmapSaveEvery is their cadence in analysed frames (minimum 200). +extern std::atomic gSaveMode; +extern std::atomic gPmapSaveEvery; +// ---- hot pixel mask ------------------------------------------------------- +// Pixels whose photon-map value is above a threshold, marked by the operator +// from the GUI. The mask is a shared immutable vector, so the GUI can swap a +// new one in while the acquisition and analysis threads read the current one. +// Masked pixels are dropped from the gate, the PH spectra, the photon map and +// the display replacement happens in the ADC image. +void hotSetMaskFromMap(const float *map, uint64_t n, double threshold); +void hotClearMask(); +uint64_t hotPixelCount(); +// Shared snapshot: load once per frame, then index it (avoids a load per pixel). +std::shared_ptr> hotMaskSnapshot(); +bool hotMaskActive(); // a mask exists and gate suppression is on + +void pmapSaveReset(); + +// One photon map on its way from the analysis (which counts the photons) to the +// acq thread (which owns the saver). +struct PmapSlot { + std::mutex mtx; + std::vector map; // NCH, photons per pixel + uint64_t frames = 0; // analysed frames counted into this map + uint64_t seq = 0; + bool ready = false; +}; +// Set when the operator confirms a gate-criterion change while a run is +// writing: the saved file keeps the criterion it started with, so its JSON has +// to say that it was moved. +extern std::atomic gGateChangedDuringSave; +// Alignment load shedding (multi-interface): keep 1 frame out of g_keepDiv, +// chosen by the frame number so the two interfaces drop the SAME frames. 1 = +// keep everything (the default); 2, 4, 8 = drop 1/2, 3/4, 7/8. +extern std::atomic g_keepDiv; +extern std::atomic g_autoDecimate; // ladder enabled (off: 1/N is manual) +extern float g_pheneThreshold; // photon-map threshold in ADU (GUI, default 150) +// Same control while the gain correction is on: the counting value is then +// adcpedecorr / ADU-per-keV, so the threshold means keV (default 4). Both are +// kept, and switching the gain swaps which one the spinbox shows. +extern float g_pheneThresholdKev; + +extern std::atomic gain_flag; +extern std::atomic dophotonmap; +extern std::atomic goout; // 1 = process, 0 = paused +extern std::atomic HDraw_every; +// While the gate passes frames slowly, "every N frames" would freeze the image, +// so a frame is also displayed when the previous one is older than this. +#define DISPLAY_FLOOR_MS 250 +extern std::atomic gImagesShown; // images handed to the Qt view // main image every N frames +// ROOT redraws of the chip spectra every N-th redraw (they are the most +// expensive canvas; 1 = every redraw) +extern std::atomic chipUpdateEvery; +extern std::atomic rootUpdateMs; // ROOT redraw period +// Gain-1 / gain-3 pedestal offsets used by the correction. The GUI controls +// for them were removed on request, so they are constant 0 for now; they are +// kept because the planned AFC/gain-split work needs per-gain offsets. +extern std::atomic G1Poffset; +extern std::atomic G2Poffset; +// Axis ranges (Set Ranges dialog), one pair per display: +// adcmin/adcmax colour scale of the ADC image (the Qt "main plot") +// pmmin/pmmax colour scale of the ROOT 2D map (canvas A5) +// chipxmin/max shared x window of the spectra (A4 and the ROI spectrum A7) +// The dialog opens on the scale each display is using right now (it can be +// dragged on the image colour bar or on the ROOT canvas) and applies only what +// the operator actually changed. +extern std::atomic adcmin, adcmax, pmmin, pmmax, chipxmin, chipxmax; +extern std::atomic fixranges; // a fixed ROOT range is in force (Reset + // Axis releases it and it auto-scales) +extern std::atomic framesinstream; // frames reaching the display +extern std::atomic gFramesForRoot; // frames since last ROOT redraw + +// analysis-thread accumulators (single owner: the analysis thread) +extern short image_data[JF_PEDE_ARRAY_FLOATS]; +extern float adc_image[NCH]; +extern float gain_image[NCH]; +// legacy gain map: never initialised to anything but 0 in the original +// sources, so the "gain correction" toggle multiplies by zero for gain 0. +extern float fgaind[JF_PEDE_ARRAY_FLOATS]; +extern int nx, ny; + +// The GATE ROI currently shown on screen, in DATA coordinates: x grows to the +// right, y grows upwards, (0,0) is the BOTTOM-LEFT pixel. This is the one and +// only rectangle the operator sees: the yellow box on the ADC image, the box +// on the ROOT photon map and the ROI spectra all read it. Mirrored once per +// analysed frame by the analysis thread from the GateEngine snapshot. +// ROIactive == false means "no ROI set"; the rectangle is then the full frame +// and nothing is drawn. +extern std::atomic ROIxmin, ROIxmax, ROIymin, ROIymax; +extern std::atomic ROIactive; +// The ROI the RECEIVER is configured with, from the "rx_roi" array in the ZMQ +// JSON header ([xmin, xmax, ymin, ymax], raw port coordinates). It is NOT the +// gate ROI and is drawn as a separate RED box on the ADC image; rxRoiValid is +// false until a non-degenerate one has been seen. +extern std::atomic rxRoiXmin, rxRoiXmax, rxRoiYmin, rxRoiYmax; +extern std::atomic rxRoiValid; + +// cross-thread reset / redraw requests +extern std::atomic gSoftResetPending; // windowed TvsB/LvsR reset +extern std::atomic gRedrawRequest; // force a ROOT redraw +extern std::atomic gFrameCountReset; // restart display frame count +// "Reset His": the GUI sets this and the ANALYSIS thread performs the reset at +// the top of its next frame, while it already holds gHistoMtx. The ROOT thread +// must never wait for that lock - under a heavy stream (JFSTRX fills) it can +// starve, which is what made the reset take tens of seconds. +extern std::atomic gResetHisPending; +// "No pede": ignore the loaded pedestal everywhere - the display, the photon +// map, the spectra, the event finder and the ROI save all work on raw ADC. +// Set by the button in the Pedestal panel; drawn as a bypass in the flow. +extern std::atomic gNoPede; +// Protects the source histograms against a Reset() racing the analysis +// thread's per-frame Fill(). copyBins() is deliberately NOT protected: the +// original sources accepted a torn read during a redraw, and taking the lock +// for a 524288-bin copy would stall the analysis thread every redraw. +extern std::mutex gHistoMtx; + +/* ------------------------------------------------------- ROOT objects */ +// defined in online_jf_root.cpp +extern TApplication *rootapp; +extern TCanvas *A2, *A3, *A4, *A5, *A6, *A7; +extern TPad *p1, *p2, *p3, *p4, *p5, *p6; +extern TH1I **hchip; // 8 chip / gain-group spectra +extern TH1I *hroi; // one spectrum over the gate-ROI pixels only +// the same two spectra again in keV (canvases A8/A9); they are filled and +// drawn only when a gain map is loaded +extern TH1I **hchipKev; +extern TH1I *hroiKev; +extern TH1I *hproj, *hchcum; +extern TH2F *his1000, *his1001, *his1002, *his1060, *his1061, *his1062; +extern TH2F *his2000, *his3000, *his4500, *hisLvsR; +extern TBox *box; + +// display clones painted by the ROOT thread (single-owner objects) +extern TH2F *disp3000, *disp4500, *dispLvsR; +extern TH1I *dispChip[8]; +extern TH1I *dispRoi; +extern TH1I *dispChipKev[8]; +extern TH1I *dispRoiKev; +extern TH2F *disp1000, *disp1001, *disp1002, *disp1060, *disp1061, *disp1062; + +// ROOT thread control + command queue +enum class RootCmdType { + None, + ResetHis, + ResetAxes, + // palette changes are applied to the ROOT gradient table AND to the fast + // image widgets, so both show the same colours + PaletteColor, + PaletteBW1, + PaletteBW2, + Zoom, + SetChipRange, + // colour scale of the ROOT 2D map (canvas A5): a, b = min, max + SetMapRange, + // show (a = 1) or hide (a = 0) every ROOT canvas: the operator can start + // with them hidden and bring them back from the Display panel + ShowPlots +}; + +// One palette definition drives both the ROOT colour table and the 256-entry +// lookup table used by the fast ADC/photon image widgets. +// The palettes offered by the image scale panel. Color / BW1 / BW2 are the +// original three (kept first: the b / Shift+B / c shortcuts use those indices); +// the rest are the matplotlib family that is useful for detector images - +// perceptually uniform (viridis, plasma, inferno, magma, cividis), turbo, +// hot, grey, and the diverging maps for pedestal-corrected data that can go +// negative. +enum class Palette { + Color, BW1, BW2, + Turbo, Viridis, Plasma, Inferno, Magma, Cividis, + Hot, Gray, Gray_r, + Coolwarm, Bwr, Seismic, RdBu, YlGnBu, Sunset, + // not in the chooser: the gain map has four valid values (the gain bits + // 0..3) and gets a fixed, calm blue/green/yellow/red code + Gain4 +}; +const char *paletteName(Palette p); // label for the scale panel +void loadRootPalette(Palette p); // ROOT thread only +const QRgb *paletteLut(Palette p); // static table, safe from any thread +struct RootCmd { + RootCmdType type = RootCmdType::None; + double a = 0, b = 0, c = 0, d = 0; // zoom coords / chip range +}; +void pushRootCmd(const RootCmd &cmd); +// like pushRootCmd, but replaces a pending command of the same type instead +// of queueing another one (used for the continuous zoom stream) +void pushRootCmdCoalesced(const RootCmd &cmd); +void requestHistoReset(); // safe "Reset His": analysis thread clears sources +void forceRootRedraw(); +void startRootThread(); +void stopRootThread(); + +/* root plotting helpers (ROOT thread only) */ +void historesetSources(); // takes gHistoMtx itself +void historesetSourcesLocked(); // gHistoMtx already held (analysis thread) +void axisreset(); + +void Plot1DHistos(); +void Plot2DHistos(); +// the same two spectra canvases in keV (A8/A9), no-ops without a gain map +void Plot1DHistosKev(); +void PlotRoiHistosKev(); +// the per-chip spectra of the gate-ROI pixels only (canvas A7); same pad +// layout as Plot1DHistos so the two can be compared pad by pad +void PlotRoiHistos(); +// runs the ROOT GUI event loop once; called inside long redraws so the +// canvases stay interactive even while a redraw is in progress +void serviceRootEvents(); +void drawRoiBox(); +void printhelp(); + +/* ------------------------------------------------------------ pedestal */ +// Immutable pedestal snapshot. g0 = gain 0 (also the fallback for every +// gain when hasAll is false); g1/g2 = gain 1 / gain 3 arrays. +struct PedeSet { + std::vector g0, g1, g2; + bool hasAll = false; // true only when all three arrays came from a file + PedeSet() + : g0(JF_PEDE_ARRAY_FLOATS, 0.0f), g1(JF_PEDE_ARRAY_FLOATS, 0.0f), + g2(JF_PEDE_ARRAY_FLOATS, 0.0f) {} +}; + +class PedestalStore { + public: + static PedestalStore &instance(); + // snapshot; empty shared_ptr means "no pedestals, run on raw values" + std::shared_ptr get() const; + void install(const std::shared_ptr &set); + bool loaded() const; + + private: + mutable std::mutex mtx_; + std::shared_ptr cur_; +}; + +// Reads the 3-array legacy layout into a new set and installs it. +// Returns false + error when the file is missing / has the wrong size. +// ---- gate-ROI list ------------------------------------------------------- +// One entry of the saved gate-ROI list. Half-open rectangle in data +// coordinates (row 0 at the bottom), w/h are the size in pixels. +struct RoiEntry { + int x = 0, y = 0, w = 0, h = 0; + int64_t usedMs = 0; // epoch ms of the last save/load (ordering key) +}; +// Reads the list, sorted most-recently-used first. +std::vector roiListLoad(); +// Adds the rectangle to the list (or refreshes its timestamp if it is already +// there) and writes the file back. Returns false on I/O failure. +bool roiListUpsert(int x, int y, int w, int h); +// Removes the rectangle and writes the file back. +bool roiListRemove(int x, int y, int w, int h); + +// ---- previously used save destinations ----------------------------------- +struct SaveDest { + std::string dir; // output directory + std::string base; // file-name base + int64_t usedMs = 0; +}; +// Same idea as the ROI list: one small file, newest used first. +std::vector saveDestListLoad(); +bool saveDestListUpsert(const std::string &dir, const std::string &base); +bool saveDestListRemove(const std::string &dir, const std::string &base); + +bool pedeLoadFile(const std::string &path, std::string &error); +// Writes the currently installed set in the legacy 3-array layout. +bool pedeSaveFile(const std::string &path, std::string &error); +bool pathExists(const std::string &path); + +/* ---------------------------------------------------------------- gate */ +// Event-finder gate. One instance; the acq thread calls evaluate() for every +// received frame, the Qt thread reads snapshot() and changes parameters. +class GateEngine { + public: + struct Counters { + uint64_t framesIn = 0; + uint64_t framesPass = 0; + uint64_t framesDrop = 0; + uint64_t framesScDrop = 0; // rejected by the storage-cell filter + uint64_t pixelsAbove = 0; + uint64_t framesInCur = 0; + uint64_t framesPassCur = 0; + uint64_t framesDropCur = 0; + uint64_t framesScDropCur = 0; + uint64_t pixelsAboveCur = 0; + int lastSc = -1; // storage cell of the last frame seen (-1 unknown) + int scFilter = -1; // -1 = all storage cells + double rateIn = 0; + double ratePass = 0; + double rateInMBs = 0; + double ratePassMBs = 0; + double dropFrac = 0; + bool connected = false; + bool pedeLoaded = false; + std::string lastError; + }; + + void setEnabled(bool on); + bool enabled() const { return enabled_.load(); } + void setThreshold(float adc); + void setMinPixels(uint64_t n); + void setContiguous(bool on); + // ---- hot pixel suppression ------------------------------------------- + // Pixels that are above the threshold in a large fraction of the frames + // (hot pixels, charged traps) are learned in a table and excluded from the + // gate, both from the count and from the contiguity search. + void setHotSuppress(bool on); + bool hotSuppress() const; + // The mask comes from the photon map: every pixel whose accumulated map + // value is above 'threshold' is marked hot. Masked pixels are excluded + // from the gate count and from the contiguity search; the display replaces + // them (local pedestal average in the raw view, 0 in the corrected view + // and in the photon map) and the PH plots skip them. + uint64_t hotPixels() const; // delegated to the shared mask + // Storage-cell selection: only frames acquired in cell 'sc' (0..15, from + // bits 8-11 of the header's detSpec3) are passed to the display and the + // analysis; -1 = all of them. Checked before the pixel work, so filtered + // frames cost nothing. + void setScFilter(int sc); + int scFilter() const { return scFilter_.load(); } + // Restrict the gate search to a rectangle of the assembled frame, in + // pixel coordinates with row 0 at the BOTTOM (the display's data order). + // Half-open [x0,x1) x [y0,y1); an unusable rectangle clears the ROI. + // This is the "gate ROI" chosen from the ADC zoom, not the receiver ROI + // (detSpec1) that the photon map draws in red. + void setRoi(int x0, int y0, int x1, int y1); + void clearRoi(); + bool roiActive() const { return roiOn_.load(); } + // The ROI rectangle and the decision to gate on it are separate: selecting + // a ROI ("Select ROI" / Load ROI) only stores it, "ROI only" switches the + // restriction on, and the gate then uses whatever is current. + bool roiSet() const { return roiSet_.load(); } + // live settings - used when a file records what the gate is doing + float threshold() const { return threshold_.load(); } + uint64_t minPixels() const { return minPixels_.load(); } + bool contiguous() const { return contiguous_.load(); } + void setRoiOnly(bool on) { roiOn_.store(on && roiSet_.load()); } + void roiSnapshot(int &x0, int &y0, int &x1, int &y1) const; + void resetCounters(); + Counters snapshot() const; + + // ---- acq-thread side ------------------------------------------------ + // Decides whether a frame passes. Returns true when it must be + // forwarded; 'above' receives the number of pixels above threshold + // (only computed when the gate is enabled, else 0). + // 'sc' is the storage cell of this frame (-1 when the header did not + // carry one); it is filtered before anything else is looked at. + // scDropped (optional) tells the caller that the frame was rejected by the + // storage-cell filter rather than by the signal gate + bool evaluate(const short *frame, uint64_t nPixels, int sc, + const PedeSet *peds, int g1off, int g2off, uint64_t &above, + bool *scDropped = nullptr); + void noteReceived(uint64_t bytes, bool broken); + void noteForwarded(uint64_t bytes); + void setConnected(bool on); + void setLastError(const std::string &msg); + + private: + uint64_t countPixelsAbove(const short *frame, uint64_t nPixels, + const PedeSet *peds, int g1off, + int g2off) const; + bool hitsClustered(const short *frame, uint64_t nPixels, + const PedeSet *peds, int g1off, int g2off) const; + + + std::atomic enabled_{true}; + std::atomic contiguous_{false}; + // hot pixel table (written on the acquisition thread only; the atomics are + // for the GUI) + std::atomic hotSuppress_{true}; + // storage-cell filter (-1 = all) and the cell of the last frame seen + std::atomic scFilter_{-1}; + std::atomic lastSc_{-1}; + // gate ROI (see setRoi); read once per frame by the acq thread + std::atomic roiOn_{false}; + std::atomic roiSet_{false}; // a ROI rectangle exists + std::atomic roiX0_{0}, roiY0_{0}, roiX1_{1024}, roiY1_{512}; + std::atomic threshold_{100.0f}; + std::atomic minPixels_{5}; + + struct AtomCounters { + std::atomic framesIn{0}; + std::atomic framesPass{0}; + std::atomic framesDrop{0}; + std::atomic framesScDrop{0}; // storage-cell filter + std::atomic pixelsAbove{0}; + std::atomic bytesIn{0}; + std::atomic bytesOut{0}; + }; + AtomCounters c_, cCur_; + // 1 Hz smoothed rates (written by the acq loop, read by the GUI) + struct RateAtoms { + std::atomic rateIn{0.0}; + std::atomic ratePass{0.0}; + std::atomic rateInMBs{0.0}; + std::atomic ratePassMBs{0.0}; + }; + RateAtoms r_; + std::atomic connected_{false}; + mutable std::mutex errMtx_; + std::string lastError_; + + friend class Pipeline; +}; + +/* ------------------------------------------------------------- mailbox */ +// Single-slot "latest frame wins" mailbox: acq -> analysis. Overwriting the +// slot is the merged equivalent of the ZMQ receive-high-water-mark drops of +// the old chain (the display always works on the freshest frame, and the gate +// counters stay exact because they are updated before the hand-off). +struct FrameMailbox { + std::mutex mtx; + std::condition_variable cv; + std::vector buf; + size_t len = 0; // valid shorts in buf + bool have = false; + uint64_t seq = 0; + uint64_t pushed = 0; + uint64_t dropped = 0; + + void publish(const short *data, size_t nBytes); + // waits up to 100 ms; false when nothing was available + bool waitTake(std::vector &out, size_t &lenOut, uint64_t &seqOut, + const std::atomic &running); +}; + +// Image hand-off: analysis -> Qt timer. The Qt thread copies the buffers +// into the widgets' own storage, so the widgets never read a live buffer. +#ifdef JFSTRX +// Strixel ADC panels, index order: +// 0 = chip1 group1, 1 = chip1 group2, 2 = chip1 group3, +// 3 = chip6 group1, 4 = chip6 group2, 5 = chip6 group3. +// The GUI arranges them like the original ROOT ADC canvas (left column +// chip1 g3/g2/g1, right column chip6 g1/g2/g3). +constexpr int kNumAdcPanels = 6; +constexpr int kAdcPanelW[kNumAdcPanels] = {79, 47, 59, 79, 47, 59}; +constexpr int kAdcPanelH[kNumAdcPanels] = {165, 320, 476, 165, 320, 476}; +#endif + +struct ImageMailbox { + std::mutex mtx; + std::vector adc, gain; // always raw 1024x512 (ROI selection uses it) + // the photon map, published every gPmapEvery frames + std::vector photon; + bool photonReady = false; + uint64_t photonSeq = 0; + bool ready = false; + uint64_t seq = 0; +#ifdef JFSTRX + // mapped strixel ADC groups for the separate six-panel window + std::array, kNumAdcPanels> panels; + bool panelsReady = false; +#endif +}; + +// ---- HDF5 ROI save ------------------------------------------------------- +// Per-frame header values, written into the same parameter datasets as the +// receiver (see online_jf_h5.cpp). +struct SavedFrameHeader { + uint64_t frameNumber = 0; + uint32_t expLength = 0; + uint32_t packetNumber = 0; + uint64_t detSpec1 = 0; + uint64_t timestamp = 0; + uint16_t modId = 0, row = 0, column = 0, detSpec2 = 0, detSpec4 = 0; + uint32_t detSpec3 = 0; + uint8_t detType = 0, version = 0; +}; + +struct GateCriteria { + bool enabled = true; + float threshold = 0.0f; + uint64_t minPixels = 0; + bool contiguous = false; + bool roiActive = false; + bool noPede = false; // pedestal subtraction disabled (raw ADC) + int roiX = 0, roiY = 0, roiW = 0, roiH = 0; +}; + +struct RunStats { + uint64_t resync = 0, behind = 0, unpaired = 0; + uint64_t displayFrames = 0, displayDropped = 0; +}; + +struct StreamInfo { + int port = 0; + std::string state; + uint64_t frames = 0, lastFrame = 0; +}; + +// Decimated ROI writer: one HDF5 file per run (receiver-compatible layout, +// ROI crop, gate-passed frames only, slice 0 = pedestal) plus a JSON sidecar. +// A bounded queue decouples the acq thread from the writer; overflow is +// counted as dropped and reported. +class RoiSaver { + public: + struct StartInfo { + int x = 0, y = 0, w = 0, h = 0; // ROI in data coordinates + uint64_t maxFrames = 0; // 0 = unlimited + std::string dir = "."; // output directory + std::string baseName = "jfroi"; // file-name base (receiver "prefix") + // false: raw 16-bit words in /data with the pedestal image as slice 0 + // true : pedestal-corrected float32 in /data (no slice 0) and the + // pedestal image in the separate "pedestal data" dataset + bool corrected = false; + std::vector pedestal; // w*h gain-0 pedestal (may be empty) + // full pedestal set + gain offsets used for the correction + std::shared_ptr peds; + bool useGain = false; + int g1off = 0, g2off = 0; + // Refresh the on-disk HDF5 (exact extents + flush) and rewrite the JSON + // sidecar atomically every this many ms while the save runs, so a + // snapshot on disk is always clean and a crash loses at most one + // interval. 0 disables the periodic checkpoint (final state at stop). + int checkpointMs = 30000; + // virtual photon counting save: write the maps instead of the frames + bool photonMode = false; + int photonEvery = 2000; // analysed frames per saved map + double photonThreshold = 150.0; // photon threshold, in photonKev units + bool photonKev = false; // true: photonThreshold is keV + GateCriteria gate; // criteria in force at start + GateEngine::Counters gateAtEnd; // fallback values for the JSON + RunStats stats; + std::vector streams; + std::string gateJson; // gate criteria as a JSON object + // Called by the writer thread just before the JSON sidecar is written, + // so the reported statistics are the exact end-of-run ones. + std::function &)> + statsProvider; + }; + struct Status { + bool running = false; + uint64_t saved = 0, dropped = 0; + uint64_t pmaps = 0; // maps written (photon-map mode) + bool photonMode = false; // which of the two counters applies + std::string h5Path, jsonPath, stopReason, lastError; + }; + + RoiSaver(); + ~RoiSaver(); + RoiSaver(const RoiSaver &) = delete; + RoiSaver &operator=(const RoiSaver &) = delete; + + bool start(const StartInfo &info, std::string &error); + // acq thread, for a frame that passed the gate + // Virtual photon counting mode: one map (NCH floats, photons per pixel) + // plus the window it covers. The writer appends it to + // "/virtual photon counting" and records the first frame's number and + // timestamp of the window. + void offerPhotonMap(const std::vector &map, uint64_t frames, + uint64_t firstFrame, uint64_t firstStamp); + void offer(const short *frame, int frameW, int frameH, + const SavedFrameHeader &hdr); + void requestStop(const std::string &reason); + bool running() const; + Status status() const; + // waits for the writer, shrinks the datasets and writes the JSON sidecar + void finish(); + + private: + struct Impl; + std::unique_ptr impl_; +}; + +/* ------------------------------------------------------------- pipeline */ +class Pipeline { + public: + // Stream state derived from ZMQ socket monitoring, NOT from receive + // timeouts. An idle-but-alive receiver is "Idle"/"Connected", never + // "stopped" (the old jf_filter behaviour). + enum class StreamState { + Disconnected, // no peer connected + Connecting, // peer not up yet / reconnecting + Connected, // peer connected, frames arriving + Idle, // peer connected but no frame for a while + EndOfAcq // peer sent the end-of-acquisition marker + }; + + Pipeline(); + ~Pipeline(); + + bool init(std::string &error); // create the ZMQ sockets + void start(); + void stop(); + bool running() const { return running_.load(); } + + GateEngine &gate() { return gate_; } + RoiSaver &saver() { return saver_; } + ImageMailbox &images() { return images_; } + FrameMailbox &frames() { return frames_; } + + bool multiPort() const { return rxPorts_.size() > 1; } + size_t portCount() const { return rxPorts_.size(); } + const std::vector &portList() const { return rxPorts_; } + + bool connected() const; + // overall state (worst of all ports) and per-port state + StreamState streamState(double *sinceSec = nullptr) const; + StreamState streamState(int portIdx, double *sinceSec = nullptr) const; + // short token for --stats: connected|idle|eoa|connecting|disconnected + const char *streamStateName() const; + static const char *streamStateName(StreamState st); + // "connected,idle" for N ports + std::string streamStateSummary() const; + uint64_t portFrames(int portIdx) const; + int portWidth(int portIdx) const; // detected per-port width (0 unknown) + int portHeight(int portIdx) const; // detected per-port height (0 unknown) + // ---- frame-alignment audit (see "Keeping the interfaces aligned") ----- + uint64_t portLastFrame(int portIdx) const; // newest frame number seen + uint64_t resyncDrops() const; // stale slabs discarded to restore alignment + uint64_t behindDrops() const; // frames discarded while catching up + uint64_t incompleteFrames() const; // assembled while an interface was absent + uint64_t flushes() const; // pairs dropped (halves were different frames) + uint64_t skippedPairs() const; // pairs dropped by the load-shedding ladder + uint64_t shedFrames() const; // frames deliberately dropped by the ladder + int decimation() const; // 1 = keep all, 2 = keep half, ... 8 + + uint64_t displayFrames() const; + uint64_t displayDropped() const; + // placement of each interface's slab (see Config::rxSlab) + std::vector rxSlab_; + + // hot pixel mask taken from the current photon map (GUI action) + void setHotMaskFromPmap(double threshold); + uint64_t pmapFrames() const; // frames the photon map has accumulated + uint64_t hotPixels() const { return gate_.hotPixels(); } + double hotThreshold() const { return hotThreshold_.load(); } + + PmapSlot &pmapSave() { return pmapSave_; } + PmapSlot pmapSave_; + std::atomic hotThreshold_{0.0}; // last "Get hot pixels" threshold + // window bookkeeping for the photon-map save (acq thread) + uint64_t pmapWinFirstFrame = 0, pmapWinFirstStamp = 0, pmapWinCount = 0; + // route a gate-passed frame to the saver: the frame itself, or the photon + // map bookkeeping when saving virtual photon counting + void offerToSaver(const short *frame, int w, int h, + const SavedFrameHeader &hdr); + uint64_t imagesShown() const { return gImagesShown.load(); } + + // Ask the acq thread to average the next N frames into a new gain-0 + // pedestal set (the old "Load Pede (p)" blocking loop, made async). + void requestPedeAcquire(int nFrames); + bool pedeAcquireActive() const; + int pedeAcquireProgress() const; // frames accumulated so far + + private: + void acqLoop(); + void analysisLoop(); + void acquirePede(const short *frame); // acq thread, during acquisition + void drainMonitor(int portIdx); // acq thread: consume monitor events + bool readOne(int portIdx); // acq thread: one header+data pair + void readStream(int portIdx); // drain a socket, newest frame wins + void drainTo(int portIdx, uint64_t targetKey); // drop until caught up + bool tryAssemble(bool force); // acq thread: pair + stack the slabs + void refreshStreamState(); // acq thread: status + messages + void refreshStreamState(int portIdx); + + // per-port receive slot: the latest slab waiting to be paired + struct StreamSlot { + std::vector data; + size_t len = 0; // valid shorts + int w = 0, h = 0; // geometry as reported by that stream's header + int rowBase = 0; // first destination row in the assembled frame + uint64_t frameKey = 0; // absolute frame number (pairing key) + SavedFrameHeader hdr; // header fields for the ROI-save datasets + int64_t stampMs = 0; + bool valid = false; + bool logged = false; // first-frame geometry already reported + std::vector prevData; // payload of the previous frame (swap, no copy) + size_t prevLen = 0; + uint64_t prevKey = 0; + bool hasPrev = false; + bool usePrev = false; // assemble this interface from prevData + }; + + // per-port connection tracking (atomics: fixed size, one per port) + struct StreamTrack { + std::atomic peerConnected{false}; + std::atomic everConnected{false}; + std::atomic endOfAcq{false}; + std::atomic lastHeaderMs{0}; + std::atomic connectedSinceMs{0}; + std::atomic frames{0}; + std::atomic lastFrame{0}; + std::atomic errors{0}; + std::atomic w{0}; + std::atomic h{0}; + std::atomic sc{-1}; // storage cell, detSpec3 bits 8-11 + }; + + std::atomic running_{false}; + std::thread *acqThread_ = nullptr; + std::thread *analysisThread_ = nullptr; + + GateEngine gate_; + RoiSaver saver_; + FrameMailbox frames_; + ImageMailbox images_; + + std::vector rxPorts_; + void *zmqCtx_ = nullptr; + std::vector zmqSubs_; // one SUB per interface + std::vector zmqMons_; // one monitor PAIR per interface (may be null) + void *zmqPub_ = nullptr; // optional republish socket + + std::array track_; + std::vector slots_; + std::vector hdrBytes_; // raw header of the last received slab + int hdrLen_ = 0; + std::vector assembled_; // full-frame assembly buffer + // acq-thread only pairing bookkeeping: + // pairWaitStartMs_ : when the current pairing attempt started (0 = none) + // lastProgressMs_ : per interface, when its frame number last advanced + // (a stalled interface must not freeze the display) + // firstDataMs_ : when the first slab ever arrived + // acqStartMs_ : when acquisition started (start-up tolerance) + int64_t pairWaitStartMs_ = 0; + std::array lastProgressMs_{}; + int64_t firstDataMs_ = 0; + int64_t acqStartMs_ = 0; + std::atomic fullW_{0}; + std::atomic fullH_{0}; + // alignment: flushes = "a pair was dropped because its halves carried + // different detector frames", skewedPairs_ = "paired anyway after 500 ms + // without a match (structural offset)". Old text follows: both + // slabs dropped"; decimate_ = keep 1 in N aligned pairs (1, 2, 4, 8) + std::atomic flushes_{0}, skippedPairs_{0}, pairSeq_{0}; + std::atomic skewedPairs_{0}; // paired although the keys differed + int64_t lastAssembledMs_ = 0; // last index-aligned assembly + bool allowSkewedPair_ = false; // latched: keys offset for good + std::atomic shedDrops_{0}; + // acq-thread-only bookkeeping for the ladder + uint64_t shedLastTrouble_ = 0; + int shedCleanSeconds_ = 0; + std::atomic decimate_{1}; + std::atomic lastFlushMs_{0}, lastAlignWarnMs_{0}; + std::atomic resyncDrops_{0}; + std::atomic behindDrops_{0}; + std::atomic incomplete_{0}; + std::atomic lastPairWarnMs_{0}; + + std::atomic monitorActive_{false}; + std::atomic stateShown_{-1}; // overall StreamState last printed + std::array, JF_MAX_PORTS> portStateShown_; + + std::atomic pedeAcqActive_{false}; + std::atomic pedeAcqWant_{0}; + std::atomic pedeAcqDone_{0}; + std::shared_ptr pedeAcq_; +}; + +/* ---------------------------------------------------------------- misc */ +bool parseCommandLine(int argc, char **argv, Config &cfg, bool &help, + std::string &error); +void printUsage(const char *prog); +void initGeometry(); // sets nx/ny (JFSTRX decoder or the 1024x512 default) +#endif // ONLINE_JF_H diff --git a/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/online_jf_core.cpp b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/online_jf_core.cpp new file mode 100644 index 000000000..6e8d95d31 --- /dev/null +++ b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/online_jf_core.cpp @@ -0,0 +1,3545 @@ +// SPDX-License-Identifier: LGPL-3.0-or-other +// =========================================================================== +// online_jf_core.cpp -- acquisition + gate + analysis (no ROOT graphics here +// except the histogram FILLING, which is the analysis thread's job). +// +// Merged from: +// jf_filter.cpp (gate, counters, pedestal loading) +// onlinedisp_zmq.cpp receiverWorker()/processData() (images, histos) +// =========================================================================== +#include "online_jf.h" + +#include + +#ifdef JFSTRX +#include "jungfrauLGADStrixelsData_new.h" +#include "sls/sls_detector_defs.h" +#include "slsDetectorData.h" +using header = sls::defs::sls_receiver_header; +#endif + +// --------------------------------------------------------------------------- +// Global state (see online_jf.h for the ownership rules) +// --------------------------------------------------------------------------- +Config g_cfg; + +short *hchptr = nullptr; +float factor = 1.84f; +std::atomic gGateChangedDuringSave{false}; +std::atomic g_keepDiv{1}; // 1 = keep all, 2/4/8 = drop 1/2, 3/4, 7/8 +std::atomic g_autoDecimate{false}; +namespace { +// Acquisition-path profile: where the loop's time goes, printed once a second. +double acqNow() { + return std::chrono::duration( + std::chrono::steady_clock::now().time_since_epoch()).count(); +} +struct AcqProf { + double poll = 0, read = 0, pair = 0; // seconds in each stage + double copy = 0; // of which: the assembly memcpy + double t0 = 0, t1 = 0, t2 = 0; + uint64_t n = 0; // loop iterations + uint64_t msgs = 0; // frames delivered by the receiver +} g_acq; +} // namespace // --auto-decimate enables the ladder +float g_pheneThreshold = 0.0f; +float g_pheneThresholdKev = 0.0f; + +std::atomic gain_flag{false}; +std::atomic dophotonmap{true}; +std::atomic goout{1}; +std::atomic HDraw_every{20}; +std::atomic gImagesShown{0}; +// when the Qt view last got an image (analysis thread only) +int64_t g_lastShownMs = 0; +std::atomic rootUpdateMs{1000}; +// one spectrum canvas is painted per tick, in round-robin; with the keV +// set enabled there are four of them, so the default is one per redraw +std::atomic chipUpdateEvery{1}; +std::atomic G1Poffset{0}; +std::atomic G2Poffset{0}; +// defaults from the operator: single-photon ADC window for the main +// plot (-200..1000), photon counts 0..256, spectra -200..3000 +std::atomic adcmin{-200}, adcmax{1000}, pmmin{0}, pmmax{256}; +std::atomic chipxmin{-200}, chipxmax{3000}; +std::atomic fixranges{true}; +std::atomic framesinstream{0}; +std::atomic gFramesForRoot{0}; + +// analysis-thread accumulators +short image_data[JF_PEDE_ARRAY_FLOATS]; +float adc_image[NCH]; +float gain_image[NCH]; +#ifdef JFSTRX +// Strixel ADC groups for the separate six-panel window. The Qt ADC/gain +// images stay raw 1024x512 (that is what the ROI selection works on), and the +// mapped groups are only used by this display path. +static std::array, kNumAdcPanels> g_adcPanel; +#endif +float fgaind[JF_PEDE_ARRAY_FLOATS]; +int nx = 1024, ny = 512; + +std::atomic ROIxmin{0}, ROIxmax{0}, ROIymin{0}, ROIymax{0}; +std::atomic ROIactive{false}; +std::atomic kevSpectra{false}; // show keV PH instead of ADU PH +std::atomic rxRoiXmin{0}, rxRoiXmax{0}, rxRoiYmin{0}, rxRoiYmax{0}; +std::atomic rxRoiValid{false}; + +std::atomic gSoftResetPending{false}; +std::atomic gRedrawRequest{false}; +std::atomic gResetHisPending{false}; +std::atomic gFrameCountReset{false}; +std::atomic gNoPede{true}; // startup default: raw ADC (--use-pede clears) +std::atomic gPmapEvery{2000}; +std::atomic gSaveMode{0}; +std::atomic gPmapSaveEvery{2000}; +std::atomic gPmapCumulative{false}; +std::atomic gPedeTracking{false}; +std::atomic gPedeTracked{false}; +std::mutex gHistoMtx; + +namespace { +int processedf = 0; // analysis thread only +constexpr size_t kHeaderBytes = 16384; + +// --------------------------------------------------------------------------- +// Deferred histogram accumulation (analysis thread, under gHistoMtx) +// --------------------------------------------------------------------------- +// Calling TH1::Fill() per pixel per frame dominated the analysis stage +// (~0.6-1 M fills/frame -> ~4-6 ms) and ROOT histograms are not thread safe, +// so the per-frame work accumulates into plain arrays here and is pushed into +// the ROOT histograms only at the redraw cadence. The histograms are painted +// once per rootUpdateMs, so syncing faster than that is pure waste. +// TEMPORARY timing instrumentation (JF_TIMING) +bool g_timing = false; +double tTake = 0, tCopy = 0, tStep1 = 0, tStep3 = 0, tStep4 = 0, + tSync = 0, tStep5 = 0; +uint64_t tFrames = 0; +struct TT { // scoped timer; zero overhead when disabled (acc == nullptr) + double *acc; + std::chrono::steady_clock::time_point t0; + explicit TT(double *a) : acc(a) { + if (acc) + t0 = std::chrono::steady_clock::now(); + } + ~TT() { + if (!acc) + return; + *acc += std::chrono::duration( + std::chrono::steady_clock::now() - t0) + .count(); + } +}; + +constexpr int kChipOverflowBins = NBIN + 2; // + underflow + overflow +std::vector g_pmapAccum; // NCH, photon map / raw ADC map +std::vector g_pmapSaveAccum; // NCH, the map being saved +uint64_t g_pmapSaveCount = 0; // analysed frames in that map +uint32_t g_chipAccum[8][kChipOverflowBins]; +uint32_t g_chipSynced[8][kChipOverflowBins]; // already pushed into hchip +uint64_t g_chipSyncedTotal[8]; // entries already pushed +// ONE spectrum over the gate-ROI pixels (canvas A7): same binning, same +// border trim, but no chip split - it is the sum over the selected pixels +uint32_t g_roiAccum[kChipOverflowBins]; +uint32_t g_roiSynced[kChipOverflowBins]; +uint64_t g_roiSyncedTotal = 0; +// the same two sets again in keV (canvases A8/A9); their axes are the ADU +// axes divided by KEV_NOMINAL_ADU, so the bin index is identical and only the +// value that is filled differs +uint32_t g_kevAccum[8][kChipOverflowBins]; +uint32_t g_kevSynced[8][kChipOverflowBins]; +uint64_t g_kevSyncedTotal[8]; +uint32_t g_roiKevAccum[kChipOverflowBins]; +uint32_t g_roiKevSynced[kChipOverflowBins]; +uint64_t g_roiKevSyncedTotal = 0; +double g_pmapFills = 0; +uint64_t g_pmapFrames = 0; // analysed frames in the map (hot-pixel default) +double g_pmapFillsSynced = 0; +int64_t g_lastAccumSyncMs = 0; +bool g_accumReady = false; + +// Bin index exactly as ROOT's TAxis::FindBin computes it for the uniform +// NBIN-bin axis [MIN_POS, MAX_POS] (verified against FindBin over all bin +// edges and 20M random values, 0 mismatches). 0 = underflow, NBIN+1 = over. +inline int chipBin(double x) { + const double inv = static_cast(NBIN) / (MAX_POS - MIN_POS); + if (x < MIN_POS) + return 0; + if (x >= MAX_POS) + return NBIN + 1; + return 1 + static_cast((x - MIN_POS) * inv); +} + +void ensureAccumulators() { + if (g_accumReady) + return; + g_pmapAccum.assign(static_cast(NCH), 0.0f); + memset(g_chipAccum, 0, sizeof(g_chipAccum)); + memset(g_chipSynced, 0, sizeof(g_chipSynced)); + memset(g_chipSyncedTotal, 0, sizeof(g_chipSyncedTotal)); + memset(g_roiAccum, 0, sizeof(g_roiAccum)); + memset(g_roiSynced, 0, sizeof(g_roiSynced)); + g_roiSyncedTotal = 0; + memset(g_kevAccum, 0, sizeof(g_kevAccum)); + memset(g_kevSynced, 0, sizeof(g_kevSynced)); + memset(g_kevSyncedTotal, 0, sizeof(g_kevSyncedTotal)); + memset(g_roiKevAccum, 0, sizeof(g_roiKevAccum)); + memset(g_roiKevSynced, 0, sizeof(g_roiKevSynced)); + g_roiKevSyncedTotal = 0; + g_pmapFills = 0; + g_pmapFillsSynced = 0; + g_pmapFrames = 0; + g_lastAccumSyncMs = 0; + g_accumReady = true; +} + +// Zeroes the accumulators so they match the freshly Reset() ROOT histograms. +// Called under gHistoMtx from historesetSources(). +void resetAccumulators() { + if (!g_accumReady) + return; + std::fill(g_pmapAccum.begin(), g_pmapAccum.end(), 0.0f); + memset(g_chipAccum, 0, sizeof(g_chipAccum)); + memset(g_chipSynced, 0, sizeof(g_chipSynced)); + memset(g_chipSyncedTotal, 0, sizeof(g_chipSyncedTotal)); + memset(g_roiAccum, 0, sizeof(g_roiAccum)); + memset(g_roiSynced, 0, sizeof(g_roiSynced)); + g_roiSyncedTotal = 0; + memset(g_kevAccum, 0, sizeof(g_kevAccum)); + memset(g_kevSynced, 0, sizeof(g_kevSynced)); + memset(g_kevSyncedTotal, 0, sizeof(g_kevSyncedTotal)); + memset(g_roiKevAccum, 0, sizeof(g_roiKevAccum)); + memset(g_roiKevSynced, 0, sizeof(g_roiKevSynced)); + g_roiKevSyncedTotal = 0; + g_pmapFills = 0; + g_pmapFillsSynced = 0; + g_lastAccumSyncMs = 0; // force a sync on the next frame +} + +// Pushes the accumulators into his3000 / hchip. Analysis thread, gHistoMtx +// held. The chip spectra are filled with the DELTA so ROOT's own statistics +// (mean/RMS in the stats box) stay exactly as if Fill() had been called. +// Pushes one 8-chip accumulator into its ROOT histograms. Only changed bins +// are pushed, using Fill(x, delta), so a sync costs O(changed bins) instead of +// O(8*NBIN). fEntries is restored explicitly: ROOT's Fill(x,w) bumps it once +// per *call*, not per weight, so the delta fills would otherwise report the +// number of syncs instead of the number of pixels. +// scale = 1 for the ADU histograms, 1/KEV_NOMINAL_ADU for the keV ones (their +// axes are the ADU axes divided by that constant, same bin count) +void syncOneAccum(TH1I *dst, const uint32_t *accum, uint32_t *synced, + uint64_t &syncedTotal, double scale = 1.0) { + if (!dst) + return; + const double w = (MAX_POS - MIN_POS) / NBIN; + uint64_t total = 0; + for (int b = 0; b < kChipOverflowBins; b++) { + const uint32_t cur = accum[b]; + total += cur; + if (cur == synced[b]) + continue; + const double delta = static_cast(cur - synced[b]); + if (b == 0) + dst->Fill((MIN_POS - 1.0) * scale, delta); + else if (b == NBIN + 1) + dst->Fill((MAX_POS + 1.0) * scale, delta); + else + dst->Fill((MIN_POS + (b - 0.5) * w) * scale, delta); + synced[b] = cur; + } + // every pixel lands in exactly one bin (including under/overflow) + if (total != syncedTotal) { + dst->SetEntries(static_cast(total)); + syncedTotal = total; + } +} + +void syncChipAccum(TH1I **dst, uint32_t accum[8][kChipOverflowBins], + uint32_t synced[8][kChipOverflowBins], + uint64_t syncedTotal[8], double scale = 1.0) { + if (!dst) + return; + for (int ic = 0; ic < 8; ic++) + syncOneAccum(dst[ic], accum[ic], synced[ic], syncedTotal[ic], scale); +} + +void syncAccumulators(int64_t now, bool force = false) { + if (!g_accumReady) + return; + int64_t period = rootUpdateMs.load() / 2; + if (period < 50) + period = 50; + if (period > 500) + period = 500; + if (!force && g_lastAccumSyncMs != 0 && + now - g_lastAccumSyncMs < period) + return; + g_lastAccumSyncMs = now; + + if (his3000 && his3000->GetNbinsX() * his3000->GetNbinsY() == NCH) { + const int nbx = his3000->GetNbinsX(); + for (int iy = 0; iy < his3000->GetNbinsY(); iy++) { + for (int ix = 0; ix < nbx; ix++) { + his3000->SetBinContent(ix + 1, iy + 1, + g_pmapAccum[ix + iy * nbx]); + } + } + if (g_pmapFills != g_pmapFillsSynced) { + his3000->SetEntries(g_pmapFills); + g_pmapFillsSynced = g_pmapFills; + } + } + + if (hchip) + syncChipAccum(hchip, g_chipAccum, g_chipSynced, g_chipSyncedTotal); + if (hroi) + syncOneAccum(hroi, g_roiAccum, g_roiSynced, g_roiSyncedTotal); + if (hchipKev) + syncChipAccum(hchipKev, g_kevAccum, g_kevSynced, g_kevSyncedTotal, + 1.0 / KEV_NOMINAL_ADU); + if (hroiKev) + syncOneAccum(hroiKev, g_roiKevAccum, g_roiKevSynced, + g_roiKevSyncedTotal, 1.0 / KEV_NOMINAL_ADU); +} +// after this long without a header the stream is reported as "idle" +constexpr int64_t kIdleWarnMs = 2000; +// fallback staleness for when socket monitoring is unavailable +constexpr int64_t kNoMonitorStaleMs = 3000; +// how long to wait for the partner slab of the same frame before assembling +// from what is available (keeps a dead interface from freezing the display) +constexpr int64_t kPairTimeoutMs = 100; +// ... but while an interface has never delivered a single frame (start-up, +// or it comes up later) wait much longer, otherwise the first frames are +// assembled from one slab only +constexpr int64_t kInitialPairTimeoutMs = 2000; +// how long after the first data we keep using the long "wait for the partner +// to come up" timeout (covers a second interface that starts late) +constexpr int64_t kSyncWindowMs = 5000; +// how long a given-up interface is ignored before it is retried +constexpr int64_t kRelatchMs = 1000; +// max messages consumed from one socket per catch-up burst +constexpr int kMaxDrainPerCall = 256; +// an interface whose frame number has not advanced for this long is stalled +// (dead, or not in this acquisition) and must not block the display +constexpr int64_t kStallMs = 500; +// how long a mismatch may repeat before the newest slabs are paired anyway +constexpr int64_t kNoPairFallbackMs = 500; + +int64_t nowMs() { + return duration_cast(steady_clock::now().time_since_epoch()) + .count(); +} +} // namespace + +void initGeometry() { +#ifdef JFSTRX + cout << "JFSTRX" << endl; + jungfrauLGADStrixelsData *decoder = new jungfrauLGADStrixelsData(); + decoder->getDetectorSize(nx, ny); + cout << decoder->dataMap[22][22] << endl; + delete decoder; +#else + nx = 1024; + ny = 512; +#endif +} + +void printhelp() { + cout << endl + << "s=start/pause| p/n=getnewpede/raw | o/O=read/save pede | n=nopede(raw)" + " | r/R=rst His/Axis | +/- = faster/slower ref. |q=exit | U/C sUrf2/Colz" + << endl; +} + +// --------------------------------------------------------------------------- +// Pedestal store +// --------------------------------------------------------------------------- +PedestalStore &PedestalStore::instance() { + static PedestalStore store; + return store; +} + +// ---- gain map (calibration for the PH spectra) -------------------------- +GainMapStore &GainMapStore::instance() { + static GainMapStore store; + return store; +} + +std::shared_ptr GainMapStore::get() const { + std::lock_guard lock(mtx_); + return cur_; +} + +void GainMapStore::install(const std::shared_ptr &m, + const std::string &path) { + std::lock_guard lock(mtx_); + cur_ = m; + path_ = path; +} + +bool GainMapStore::loaded() const { + std::lock_guard lock(mtx_); + return cur_ != nullptr; +} + +bool gainMapLoaded() { return GainMapStore::instance().loaded(); } + +std::string GainMapStore::path() const { + std::lock_guard lock(mtx_); + return path_; +} + +// Six 1024x512 images of doubles, in file order g0, g1, g2, hg0, hg1, hg2. +bool gainMapLoadFile(const std::string &path, std::string &error) { + const long long expected = static_cast(JF_GAINMAP_IMAGES) * + JF_GAINMAP_PIXELS * (long long)sizeof(double); + std::ifstream f(path.c_str(), std::ios::binary); + if (!f.is_open()) { + error = "cannot open gain map '" + path + "'"; + return false; + } + f.seekg(0, std::ios::end); + const long long bytes = static_cast(f.tellg()); + f.seekg(0, std::ios::beg); + if (bytes != expected) { + std::ostringstream oss; + oss << "'" << path << "' has " << bytes << " bytes, expected " + << expected << " (" << JF_GAINMAP_IMAGES << " x " << 1024 << "x" + << 512 << " doubles)"; + error = oss.str(); + return false; + } + std::shared_ptr m = std::make_shared(); + for (int i = 0; i < JF_GAINMAP_IMAGES; i++) { + f.read(reinterpret_cast(m->img[i].data()), + static_cast(JF_GAINMAP_PIXELS * + sizeof(double))); + if (!f.good()) { + error = "short read from '" + path + "'"; + return false; + } + } + // sanity: the normal-mode map must be usable + uint64_t bad = 0; + double g0sum = 0, hg0sum = 0; + for (int i = 0; i < JF_GAINMAP_PIXELS; i++) { + if (!(m->img[0][i] > 0.0) || !std::isfinite(m->img[0][i])) + bad++; + g0sum += m->img[0][i]; + hg0sum += m->img[3][i]; + } + GainMapStore::instance().install(m, path); + printf("[gain] loaded %s: g0 ADU/keV mean %.2f (%llu unusable pixels), " + "hg0 mean %.2f\n", + path.c_str(), g0sum / JF_GAINMAP_PIXELS, (unsigned long long)bad, + hg0sum / JF_GAINMAP_PIXELS); + fflush(stdout); + return true; +} + +std::shared_ptr PedestalStore::get() const { + std::lock_guard lock(mtx_); + return cur_; +} + +void PedestalStore::install(const std::shared_ptr &set) { + pedeTrackerReset(); // a fresh pedestal re-primes the running average + std::lock_guard lock(mtx_); + cur_ = set; +} + +bool PedestalStore::loaded() const { + std::lock_guard lock(mtx_); + return static_cast(cur_); +} + +bool pathExists(const std::string &path) { + struct stat st; + return (::stat(path.c_str(), &st) == 0) && S_ISREG(st.st_mode); +} + +// Legacy layout written by onlinedisp_zmq "Save Pede (O)": +// 3 arrays (fpeded, fpedeG1d, fpedeG2d), each 2*NCH floats. +bool pedeLoadFile(const std::string &path, std::string &error) { + const long long expected = + (long long)3 * JF_PEDE_ARRAY_FLOATS * (long long)sizeof(float); + + std::ifstream f(path.c_str(), std::ios::binary); + if (!f.is_open()) { + error = "cannot open pedestal file '" + path + "'"; + return false; + } + f.seekg(0, std::ios::end); + const long long fileBytes = static_cast(f.tellg()); + f.seekg(0, std::ios::beg); + if (fileBytes != expected) { + std::ostringstream oss; + oss << "'" << path << "' has " << fileBytes << " bytes, expected " + << expected << " (3 x " << JF_PEDE_ARRAY_FLOATS << " floats)"; + error = oss.str(); + return false; + } + + std::shared_ptr set = std::make_shared(); + const size_t arrayBytes = + static_cast(JF_PEDE_ARRAY_FLOATS) * sizeof(float); + f.read(reinterpret_cast(set->g0.data()), + static_cast(arrayBytes)); + f.read(reinterpret_cast(set->g1.data()), + static_cast(arrayBytes)); + f.read(reinterpret_cast(set->g2.data()), + static_cast(arrayBytes)); + if (!f.good()) { + error = "short read from '" + path + "'"; + return false; + } + set->hasAll = true; + PedestalStore::instance().install(set); + return true; +} + +// --------------------------------------------------------------------------- +// Gate-ROI list (single file, rewritten on every change, newest first) +// --------------------------------------------------------------------------- +namespace { +constexpr size_t kMaxRoiEntries = 200; // keeps the file at a few kB forever + +bool roiListWrite(const std::vector &v) { + const std::string path = g_cfg.roiFile; + std::ofstream f(path.c_str(), std::ios::trunc); + if (!f.is_open()) + return false; + f << "# gate ROI list: x y w h lastUsedEpochMillis\n"; + for (const RoiEntry &e : v) + f << e.x << ' ' << e.y << ' ' << e.w << ' ' << e.h << ' ' + << e.usedMs << '\n'; + return f.good(); +} +} // namespace + +std::vector roiListLoad() { + std::vector v; + std::ifstream f(g_cfg.roiFile.c_str()); + if (!f.is_open()) + return v; + std::string line; + while (std::getline(f, line)) { + if (line.empty() || line[0] == '#') + continue; + std::istringstream is(line); + RoiEntry e; + if (is >> e.x >> e.y >> e.w >> e.h >> e.usedMs) + v.push_back(e); + } + std::sort(v.begin(), v.end(), [](const RoiEntry &a, const RoiEntry &b) { + return a.usedMs > b.usedMs; // last saved/loaded first + }); + return v; +} + +bool roiListUpsert(int x, int y, int w, int h) { + std::vector v = roiListLoad(); + const int64_t now = duration_cast( + system_clock::now().time_since_epoch()) + .count(); + bool found = false; + for (RoiEntry &e : v) { + if (e.x == x && e.y == y && e.w == w && e.h == h) { + e.usedMs = now; + found = true; + break; + } + } + if (!found) { + RoiEntry e; + e.x = x; e.y = y; e.w = w; e.h = h; + e.usedMs = now; + v.push_back(e); + } + std::sort(v.begin(), v.end(), [](const RoiEntry &a, const RoiEntry &b) { + return a.usedMs > b.usedMs; + }); + if (v.size() > kMaxRoiEntries) + v.resize(kMaxRoiEntries); // drop the least recently used + return roiListWrite(v); +} + +bool roiListRemove(int x, int y, int w, int h) { + std::vector v = roiListLoad(); + const size_t before = v.size(); + v.erase(std::remove_if(v.begin(), v.end(), [&](const RoiEntry &e) { + return e.x == x && e.y == y && e.w == w && e.h == h; + }), + v.end()); + if (v.size() == before) + return true; // nothing to do + return roiListWrite(v); +} + +// --------------------------------------------------------------------------- +// Previously used save destinations (single tab-separated file, newest first) +// --------------------------------------------------------------------------- +namespace { +constexpr size_t kMaxSaveDests = 100; + +int64_t epochMsNow() { + return duration_cast(system_clock::now().time_since_epoch()) + .count(); +} + +bool saveDestListWrite(const std::vector &v) { + std::ofstream f(g_cfg.saveListFile.c_str(), std::ios::trunc); + if (!f.is_open()) + return false; + f << "# save destinations: \t\t\n"; + for (const SaveDest &d : v) + f << d.dir << '\t' << d.base << '\t' << d.usedMs << '\n'; + return f.good(); +} +} // namespace + +std::vector saveDestListLoad() { + std::vector v; + std::ifstream f(g_cfg.saveListFile.c_str()); + if (!f.is_open()) + return v; + std::string line; + while (std::getline(f, line)) { + if (line.empty() || line[0] == '#') + continue; + const size_t t1 = line.find('\t'); + if (t1 == std::string::npos) + continue; + const size_t t2 = line.find('\t', t1 + 1); + if (t2 == std::string::npos) + continue; + SaveDest d; + d.dir = line.substr(0, t1); + d.base = line.substr(t1 + 1, t2 - t1 - 1); + d.usedMs = std::strtoll(line.c_str() + t2 + 1, nullptr, 10); + if (!d.base.empty()) + v.push_back(d); + } + std::sort(v.begin(), v.end(), [](const SaveDest &a, const SaveDest &b) { + return a.usedMs > b.usedMs; // last used first + }); + return v; +} + +bool saveDestListUpsert(const std::string &dir, const std::string &base) { + if (base.empty()) + return false; + std::vector v = saveDestListLoad(); + const int64_t now = epochMsNow(); + bool found = false; + for (SaveDest &d : v) { + if (d.dir == dir && d.base == base) { + d.usedMs = now; + found = true; + break; + } + } + if (!found) { + SaveDest d; + d.dir = dir; + d.base = base; + d.usedMs = now; + v.push_back(d); + } + std::sort(v.begin(), v.end(), [](const SaveDest &a, const SaveDest &b) { + return a.usedMs > b.usedMs; + }); + if (v.size() > kMaxSaveDests) + v.resize(kMaxSaveDests); + return saveDestListWrite(v); +} + +bool saveDestListRemove(const std::string &dir, const std::string &base) { + std::vector v = saveDestListLoad(); + const size_t before = v.size(); + v.erase(std::remove_if(v.begin(), v.end(), + [&](const SaveDest &d) { + return d.dir == dir && d.base == base; + }), + v.end()); + if (v.size() == before) + return true; + return saveDestListWrite(v); +} + +bool pedeSaveFile(const std::string &path, std::string &error) { + std::shared_ptr set = PedestalStore::instance().get(); + if (!set) { + error = "no pedestals loaded"; + return false; + } + const int pfd = ::open(path.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666); + if (pfd == -1) { + error = std::string("open ") + path + ": " + std::strerror(errno); + return false; + } + const size_t arrayBytes = + static_cast(JF_PEDE_ARRAY_FLOATS) * sizeof(float); + size_t written = 0; + const float *arrays[3] = {set->g0.data(), set->g1.data(), set->g2.data()}; + for (int a = 0; a < 3; a++) { + const char *p = reinterpret_cast(arrays[a]); + size_t left = arrayBytes; + while (left > 0) { + const ssize_t n = ::write(pfd, p, left); + if (n <= 0) { + error = std::string("write ") + path + ": " + std::strerror(errno); + ::close(pfd); + return false; + } + left -= static_cast(n); + p += n; + written += static_cast(n); + } + } + ::close(pfd); + (void)written; + return true; +} + +// --------------------------------------------------------------------------- +// GateEngine +// --------------------------------------------------------------------------- +// Calls fn(i) for every pixel of the frame, or only for the gate ROI when one +// is set. fn returns false to stop early. Templated so it inlines into the +// pixel loops. +template +static inline void forEachGatePixel(uint64_t nPixels, bool roiOn, int x0, + int y0, int x1, int y1, F &&fn) { + if (!roiOn) { + for (uint64_t i = 0; i < nPixels; i++) { + if (!fn(i)) + return; + } + return; + } + for (int y = y0; y < y1; y++) { + const uint64_t row = static_cast(y) * 1024u; + if (row >= nPixels) + return; + for (int x = x0; x < x1; x++) { + const uint64_t i = row + static_cast(x); + if (i >= nPixels) + return; + if (!fn(i)) + return; + } + } +} + +void GateEngine::setRoi(int x0, int y0, int x1, int y1) { + // clamp to the assembled frame (1024 x 512) and require a usable area + if (x0 < 0) x0 = 0; + if (y0 < 0) y0 = 0; + if (x1 > 1024) x1 = 1024; + if (y1 > 512) y1 = 512; + if (x1 - x0 < 4 || y1 - y0 < 4) { + clearRoi(); + return; + } + roiX0_.store(x0); + roiY0_.store(y0); + roiX1_.store(x1); + roiY1_.store(y1); + roiSet_.store(true); // selected; setRoiOnly() decides whether it is used +} + +void GateEngine::clearRoi() { + roiOn_.store(false); + roiSet_.store(false); + roiX0_.store(0); + roiY0_.store(0); + roiX1_.store(1024); + roiY1_.store(512); +} + +void GateEngine::roiSnapshot(int &x0, int &y0, int &x1, int &y1) const { + x0 = roiX0_.load(); + y0 = roiY0_.load(); + x1 = roiX1_.load(); + y1 = roiY1_.load(); +} + +void GateEngine::setEnabled(bool on) { enabled_.store(on); } + +void GateEngine::setThreshold(float adc) { threshold_.store(adc); } + +void GateEngine::setMinPixels(uint64_t n) { minPixels_.store(n); } + +void GateEngine::setContiguous(bool on) { contiguous_.store(on); } + +// ---- hot pixel mask ------------------------------------------------------ +// The mask is not learned from frames: the operator marks it from the photon +// map ("Get hot pixels"), which integrates over many frames, so a hot pixel +// stands out by orders of magnitude instead of by a fraction of frames. +namespace { +std::shared_ptr> g_hotMask; +std::atomic g_hotCount{0}; +} // namespace + +void hotSetMaskFromMap(const float *map, uint64_t n, double threshold) { + auto m = std::make_shared>( + static_cast(NCH), static_cast(0)); + uint64_t c = 0; + const uint64_t lim = std::min(n, static_cast(NCH)); + for (uint64_t i = 0; map && i < lim; i++) { + if (static_cast(map[i]) > threshold) { + (*m)[static_cast(i)] = 1; + c++; + } + } + std::atomic_store( + &g_hotMask, std::shared_ptr>(std::move(m))); + g_hotCount.store(c); +} +void hotClearMask() { + std::atomic_store(&g_hotMask, + std::shared_ptr>()); + g_hotCount.store(0); +} +uint64_t hotPixelCount() { return g_hotCount.load(); } +std::shared_ptr> hotMaskSnapshot() { + return std::atomic_load(&g_hotMask); +} +bool hotMaskActive() { return g_hotCount.load() > 0; } + +void GateEngine::setHotSuppress(bool on) { hotSuppress_.store(on); } +bool GateEngine::hotSuppress() const { return hotSuppress_.load(); } +uint64_t GateEngine::hotPixels() const { return hotPixelCount(); } + +void GateEngine::setScFilter(int sc) { + if (sc < 0 || sc > 15) + sc = -1; // anything else means "all" + scFilter_.store(sc); +} + +void GateEngine::resetCounters() { + c_.framesIn.store(0); + c_.framesPass.store(0); + c_.framesDrop.store(0); + c_.framesScDrop.store(0); + c_.pixelsAbove.store(0); + c_.bytesIn.store(0); + c_.bytesOut.store(0); + cCur_.framesIn.store(0); + cCur_.framesPass.store(0); + cCur_.framesDrop.store(0); + cCur_.framesScDrop.store(0); + cCur_.pixelsAbove.store(0); + r_.rateIn.store(0.0); + r_.ratePass.store(0.0); + r_.rateInMBs.store(0.0); + r_.ratePassMBs.store(0.0); +} + +void GateEngine::noteReceived(uint64_t bytes, bool broken) { + c_.framesIn.fetch_add(1); + cCur_.framesIn.fetch_add(1); + c_.bytesIn.fetch_add(bytes); + if (broken) { + c_.framesDrop.fetch_add(1); + cCur_.framesDrop.fetch_add(1); + } +} + +void GateEngine::noteForwarded(uint64_t bytes) { + c_.bytesOut.fetch_add(bytes); +} + +void GateEngine::setConnected(bool on) { connected_.store(on); } + +void GateEngine::setLastError(const std::string &msg) { + std::lock_guard lock(errMtx_); + lastError_ = msg; +} + +GateEngine::Counters GateEngine::snapshot() const { + Counters cs; + cs.framesIn = c_.framesIn.load(); + cs.framesPass = c_.framesPass.load(); + cs.framesDrop = c_.framesDrop.load(); + cs.pixelsAbove = c_.pixelsAbove.load(); + cs.framesInCur = cCur_.framesIn.load(); + cs.framesPassCur = cCur_.framesPass.load(); + cs.framesDropCur = cCur_.framesDrop.load(); + cs.pixelsAboveCur = cCur_.pixelsAbove.load(); + cs.rateIn = r_.rateIn.load(); + cs.ratePass = r_.ratePass.load(); + cs.rateInMBs = r_.rateInMBs.load(); + cs.ratePassMBs = r_.ratePassMBs.load(); + cs.dropFrac = (cs.framesIn > 0) + ? static_cast(cs.framesDrop) / + static_cast(cs.framesIn) + : 0.0; + cs.connected = connected_.load(); + cs.pedeLoaded = PedestalStore::instance().loaded() && !gNoPede.load(); + cs.framesScDrop = c_.framesScDrop.load(); + cs.framesScDropCur = cCur_.framesScDrop.load(); + cs.lastSc = lastSc_.load(); + cs.scFilter = scFilter_.load(); + { + std::lock_guard lock(errMtx_); + cs.lastError = lastError_; + } + return cs; +} + +namespace { +// ------------------------------------------------------------ pedestal drift +// Per-pixel running average of the "empty" frames the signal gate drops. All +// of it runs on the acq thread (one call per dropped frame, rate limited), so +// the accumulator needs a mutex only because the GUI/HDF5 read the stats. +struct PedeTracker { + std::mutex mtx; + std::vector acc; // running average, raw ADC units + std::vector used; // the pedestal the average is compared against + uint64_t samples = 0; // pixel samples fed in + uint64_t updates = 0; // times the used pedestal was replaced + double maxDiff = 0.0; // last measured largest |acc - used| + int64_t lastUpdateMs = 0; + int64_t lastDecideMs = 0; // the ROI distance test runs ~1 Hz + uint64_t phase = 0; // sampling phase, rotated every frame +}; +PedeTracker g_ptrack; + +// The average is compared against the pedestal in use; replacing it means +// installing a copy of the current set with g0 = the average, which every +// consumer picks up on its next snapshot. +void pedeTrackInstall(const PedeSet *cur, const std::vector &acc) { + auto set = std::make_shared(); + if (cur) + *set = *cur; // keeps g1/g2/hasAll + set->g0 = acc; + PedestalStore::instance().install(set); + gPedeTracked.store(true); +} +} // namespace + +double pedeTrackSignalDropRate(uint64_t passCur, uint64_t dropCur, + uint64_t passTotal, uint64_t dropTotal) { + // The Cur counters are reset by the 1 Hz rate update, so this is the recent + // behaviour. Using the cumulative ones was the bug: after any beam-on + // stretch the cumulative drop rate stays low, so the tracking never fed + // again even when the beam went off. + static double held = -1.0; + const uint64_t curTot = passCur + dropCur; + if (curTot >= 1) { + held = static_cast(dropCur) / static_cast(curTot); + return held; + } + if (held >= 0.0) + return held; // a window with no frames yet: keep the last value + const uint64_t tot = passTotal + dropTotal; + return tot > 0 ? static_cast(dropTotal) / static_cast(tot) + : 0.0; +} + +PedeTrackStats pedeTrackerStats() { + PedeTrackStats st; + st.on = gPedeTracking.load(); + st.tracked = gPedeTracked.load(); + std::lock_guard lk(g_ptrack.mtx); + st.samples = g_ptrack.samples; + st.updates = g_ptrack.updates; + st.maxDiff = g_ptrack.maxDiff; + st.sinceUpdateMs = g_ptrack.lastUpdateMs + ? (int64_t)nowMs() - g_ptrack.lastUpdateMs + : 0; + return st; +} + +// Called from PedestalStore::install(), i.e. both when the operator loads a +// pedestal and when the tracker itself replaces it. Only the running average +// is dropped (it re-primes from whatever pedestal is now in use); samples and +// updates are cumulative diagnostics and survive. +void pedeTrackerReset() { + std::lock_guard lk(g_ptrack.mtx); + g_ptrack.acc.clear(); + g_ptrack.used.clear(); + g_ptrack.maxDiff = 0.0; + g_ptrack.lastUpdateMs = 0; + gPedeTracked.store(false); +} + +void pedeTrackerFeed(const short *frame, uint64_t nPixels, const PedeSet *peds, + double signalDropRate, int roiX0, int roiY0, int roiX1, + int roiY1) { + if (!gPedeTracking.load() || !frame || !peds) + return; + const int64_t now = (int64_t)nowMs(); + // only while the beam is effectively off: the gate is dropping the frames + if (signalDropRate < PEDE_TRACK_DROP_RATE) + return; + + // A fixed stride of 1/0.2 % would sample the same pixels on every frame, so + // the phase is rotated: after `stride` frames every pixel has been taken + // once. The weight is per sample, so the pooled window stays + // PEDE_TRACK_WINDOW samples. + const uint64_t stride = + std::max(1, (uint64_t)std::lround(1.0 / PEDE_TRACK_FRACTION)); + const float alpha = (float)((double)nPixels / PEDE_TRACK_WINDOW); + + bool doInstall = false; + bool safety = false; + double md = 0.0, usedMean = 0.0; + uint64_t updNo = 0, samNo = 0; + std::vector snapshot; + { + std::lock_guard lk(g_ptrack.mtx); + if (g_ptrack.acc.size() != peds->g0.size()) { + g_ptrack.acc = peds->g0; // re-prime (also after an install) + g_ptrack.used = peds->g0; + g_ptrack.lastUpdateMs = now; + g_ptrack.lastDecideMs = 0; + g_ptrack.phase = 0; + } + const uint64_t start = g_ptrack.phase % stride; + g_ptrack.phase++; + uint64_t fed = 0; + for (uint64_t i = start; i < nPixels; i += stride) { + if (frame[i] & 0xc000) + continue; // high-gain / gain-3 pixels live on another scale + const float raw = static_cast(frame[i] & 0x3fff); + g_ptrack.acc[i] += (raw - g_ptrack.acc[i]) * alpha; + fed++; + } + if (fed == 0) + fed = 1; + g_ptrack.samples += fed; + + // The ROI-wide distance test is the only expensive part (~0.5 ms), so + // it runs about once a second; the sampling above runs on every frame. + if (now - g_ptrack.lastDecideMs >= 1000) { + g_ptrack.lastDecideMs = now; + for (size_t i = 0; i < nPixels && i < g_ptrack.acc.size(); i++) { + const int px = static_cast(i % 1024); + const int py = static_cast(i / 1024); + if (px < roiX0 || px >= roiX1 || py < roiY0 || py >= roiY1) + continue; + const double d = std::fabs((double)g_ptrack.acc[i] - + (double)g_ptrack.used[i]); + if (d > md) + md = d; + } + g_ptrack.maxDiff = md; + for (uint64_t i = 0; i < nPixels && i < g_ptrack.used.size(); i++) + usedMean += g_ptrack.used[i]; + usedMean = nPixels ? usedMean / double(nPixels) : 0.0; + + // at most 0.1 Hz, and at least once every 10 min + const bool rateOk = (now - g_ptrack.lastUpdateMs) >= + PEDE_TRACK_MIN_PERIOD_MS; + const bool overdue = + (now - g_ptrack.lastUpdateMs) >= PEDE_TRACK_MAX_PERIOD_MS; + doInstall = (md > PEDE_TRACK_MAXDIFF && rateOk) || overdue; + safety = overdue && md <= PEDE_TRACK_MAXDIFF; + if (doInstall) { + snapshot = g_ptrack.acc; + updNo = ++g_ptrack.updates; + samNo = g_ptrack.samples; + g_ptrack.lastUpdateMs = now; + } + } + } // lock released: install() below resets the tracker and takes it again + + if (doInstall) { + pedeTrackInstall(peds, snapshot); + double mAcc = 0.0; + for (uint64_t i = 0; i < nPixels; i++) + mAcc += snapshot[i]; + printf("[pedetrack] pedestal updated #%llu: max delta %.2f ADU in the " + "ROI over %llu pixel samples (%s); mean(raw average)=%.2f, " + "mean(pedestal being replaced)=%.2f\n", + (unsigned long long)updNo, md, (unsigned long long)samNo, + safety ? "10 min safety update" : "drift > 5 ADU", + nPixels ? mAcc / double(nPixels) : 0.0, usedMean); + fflush(stdout); + } +} + +// The gate decision for one frame. Called by the acq thread only. +bool GateEngine::evaluate(const short *frame, uint64_t nPixels, int sc, + const PedeSet *peds, int g1off, int g2off, + uint64_t &above, bool *scDropped) { + above = 0; + if (scDropped) + *scDropped = false; + lastSc_.store(sc); + // Storage-cell selection first: it is a pure header check, so frames from + // other cells are dropped before any pixel is touched. + const int wantSc = scFilter_.load(); + if (wantSc >= 0 && sc != wantSc) { + c_.framesScDrop.fetch_add(1); + cCur_.framesScDrop.fetch_add(1); + if (scDropped) + *scDropped = true; + return false; + } + if (!enabled_.load()) { // transparent passthrough + c_.framesPass.fetch_add(1); + cCur_.framesPass.fetch_add(1); + return true; + } + above = countPixelsAbove(frame, nPixels, peds, g1off, g2off); + const bool passCount = (above >= minPixels_.load()); + bool passCluster = true; + if (passCount && contiguous_.load()) + passCluster = hitsClustered(frame, nPixels, peds, g1off, g2off); + + if (passCount && passCluster) { + c_.pixelsAbove.fetch_add(above); + cCur_.pixelsAbove.fetch_add(above); + c_.framesPass.fetch_add(1); + cCur_.framesPass.fetch_add(1); + return true; + } + c_.framesDrop.fetch_add(1); + cCur_.framesDrop.fetch_add(1); + return false; +} + +// Gain-aware pixel counting, identical to the original jf_filter: +// g0: raw - pede0 +// g1: (pede1 + G1off - raw) * 30 +// g3: (pede2 + G2off - raw) * 340 (g==2 unused -> treated as g0) +// When only the gain-0 array is available (live pedestal acquisition) every +// gain falls back to raw - pede0, exactly like the display's own fallback. +uint64_t GateEngine::countPixelsAbove(const short *frame, uint64_t nPixels, + const PedeSet *peds, int g1off, + int g2off) const { + const float thr = threshold_.load(); + const bool roiOn = roiOn_.load(); + const int x0 = roiX0_.load(), y0 = roiY0_.load(); + const int x1 = roiX1_.load(), y1 = roiY1_.load(); + + const float *p0 = peds ? peds->g0.data() : nullptr; + const bool gainAware = peds && peds->hasAll; + const float *p1 = gainAware ? peds->g1.data() : nullptr; + const float *p2 = gainAware ? peds->g2.data() : nullptr; + + const auto hmask = hotSuppress_.load() ? hotMaskSnapshot() : nullptr; + const bool hotOn = hmask && hmask->size() == static_cast(NCH); + uint64_t nAbove = 0; + forEachGatePixel(nPixels, roiOn, x0, y0, x1, y1, [&](uint64_t i) { + if (hotOn && (*hmask)[i]) + return true; // hot pixels never reach the criterion + float v; + if (!p0) { // no pedestals at all: raw 14-bit values + v = static_cast(frame[i] & 0x3fff); + } else if (!gainAware) { + v = static_cast(frame[i] & 0x3fff) - p0[i]; + } else { + const int raw = frame[i] & 0x3fff; + const int g = (frame[i] & 0xc000) ? ((frame[i] >> 14) & 0x3) : 0; + if (g == 0) { + v = static_cast(raw) - p0[i]; + } else if (g == 1) { + v = (p1[i] + static_cast(g1off) - + static_cast(raw)) * 30.0f; + } else if (g == 3) { + v = (p2[i] + static_cast(g2off) - + static_cast(raw)) * 340.0f; + } else { + v = static_cast(raw) - p0[i]; + } + } + if (v > thr) + nAbove++; + return true; + }); + return nAbove; +} + +// Contiguity proxy: "N px within a circle". +// +// Pass when SOME circle of radius (minPixels/2 + 1) contains at least +// minPixels above-threshold pixels. The previous version required EVERY +// above-threshold pixel to lie within that radius of the *centroid*, so two +// well-separated clusters - each of which satisfies the requirement on its own +// - put the centroid between them and the frame was always rejected. That is +// why the option blocked frames it should have passed. +bool GateEngine::hitsClustered(const short *frame, uint64_t nPixels, + const PedeSet *peds, int g1off, + int g2off) const { + const float thr = threshold_.load(); + const uint64_t nReq = std::max(1, minPixels_.load()); + const double radius = static_cast(nReq) / 2.0 + 1.0; + const double r2 = radius * radius; + const bool roiOn = roiOn_.load(); + const int x0 = roiX0_.load(), y0 = roiY0_.load(); + const int x1 = roiX1_.load(), y1 = roiY1_.load(); + + const float *p0 = peds ? peds->g0.data() : nullptr; + const bool gainAware = peds && peds->hasAll; + const float *p1 = gainAware ? peds->g1.data() : nullptr; + const float *p2 = gainAware ? peds->g2.data() : nullptr; + + auto valueAt = [&](uint64_t i) -> float { + if (!p0) + return static_cast(frame[i] & 0x3fff); + const int raw = frame[i] & 0x3fff; + if (!gainAware) + return static_cast(raw) - p0[i]; + const int g = (frame[i] & 0xc000) ? ((frame[i] >> 14) & 0x3) : 0; + if (g == 1) + return (p1[i] + static_cast(g1off) - + static_cast(raw)) * 30.0f; + if (g == 3) + return (p2[i] + static_cast(g2off) - + static_cast(raw)) * 340.0f; + return static_cast(raw) - p0[i]; + }; + + // positions of the above-threshold pixels (reused buffer: this runs per + // frame on the acquisition path, so no allocation in the steady state) + static thread_local std::vector hx, hy; + hx.clear(); + hy.clear(); + const auto hmask = hotSuppress_.load() ? hotMaskSnapshot() : nullptr; + const bool hotOn = hmask && hmask->size() == static_cast(NCH); + forEachGatePixel(nPixels, roiOn, x0, y0, x1, y1, [&](uint64_t i) { + if (hotOn && (*hmask)[i]) + return true; // hot pixels must not form a cluster either + if (valueAt(i) > thr) { + hx.push_back(static_cast(i % kRawWidth)); + hy.push_back(static_cast(i / kRawWidth)); + } + return true; + }); + const size_t n = hx.size(); + if (n < nReq) + return false; + // Enough hits that a cluster is guaranteed in practice; the pairwise scan + // below would only burn acquisition time on such a frame. + if (n > 4096) + return true; + + for (size_t a = 0; a < n; a++) { + size_t cnt = 1; + for (size_t b = 0; b < n && cnt < nReq; b++) { + if (a == b) + continue; + const double dx = static_cast(hx[a]) - hx[b]; + const double dy = static_cast(hy[a]) - hy[b]; + if (dx * dx + dy * dy <= r2) + cnt++; + } + if (cnt >= nReq) + return true; + } + return false; +} + +// --------------------------------------------------------------------------- +// Mailboxes +// --------------------------------------------------------------------------- +void FrameMailbox::publish(const short *data, size_t nBytes) { + const size_t nShorts = nBytes / sizeof(short); + std::lock_guard lock(mtx); + if (have) + dropped++; // the previous frame never made it to the analysis stage + if (buf.size() < nShorts) + buf.resize(nShorts); + if (nShorts) + memcpy(buf.data(), data, nShorts * sizeof(short)); + len = nShorts; + have = true; + pushed++; + seq++; + cv.notify_all(); +} + +bool FrameMailbox::waitTake(std::vector &out, size_t &lenOut, + uint64_t &seqOut, const std::atomic &running) { + std::unique_lock lock(mtx); + cv.wait_for(lock, std::chrono::milliseconds(100), + [&] { return have || !running.load(); }); + if (!have) + return false; + out = buf; + lenOut = len; + seqOut = seq; + have = false; + return true; +} + +// --------------------------------------------------------------------------- +// Pipeline +// --------------------------------------------------------------------------- +namespace { +void startAnalysisWorkers(int n); +void stopAnalysisWorkers(); +} // namespace + +Pipeline::Pipeline() { + // std::atomic default construction leaves the value uninitialised in + // C++17, which would randomly swallow the first status line + for (auto &v : portStateShown_) + v.store(-1); +} + +Pipeline::~Pipeline() { + stop(); + for (void *m : zmqMons_) { + if (m) + zmq_close(m); + } + zmqMons_.clear(); + for (void *sub : zmqSubs_) { + if (sub) + zmq_close(sub); + } + zmqSubs_.clear(); + if (zmqPub_) { + zmq_close(zmqPub_); + zmqPub_ = nullptr; + } + if (zmqCtx_) { + zmq_ctx_shutdown(zmqCtx_); + zmq_ctx_term(zmqCtx_); + zmqCtx_ = nullptr; + } +} + +bool Pipeline::init(std::string &error) { + zmqCtx_ = zmq_ctx_new(); + // One ZMQ I/O thread per interface: a single I/O thread sat at ~90 % + // moving ~2 GB/s for both sockets and was a serialisation point. + { + int nio = 2; + if (const char *e = getenv("JF_ZMQ_IO_THREADS")) + nio = atoi(e); + if (nio > 0) + zmq_ctx_set(zmqCtx_, ZMQ_IO_THREADS, nio); + } + + if (!zmqCtx_) { + error = "zmq_ctx_new failed"; + return false; + } + + int linger = 0, rcvtimeo = 200, sndtimeo = 200; + int hwm = g_cfg.rxHwm > 0 ? g_cfg.rxHwm : 1000; + int rcvbuf = 8 * 1024 * 1024; + + rxPorts_ = g_cfg.rxPorts; + if (rxPorts_.empty()) + rxPorts_.push_back(DEFAULT_RX_PORT); + rxSlab_ = g_cfg.rxSlab; + rxSlab_.resize(rxPorts_.size(), 0); // placement per interface (0 = by order) + if (rxPorts_.size() > JF_MAX_PORTS) { + error = "too many rx ports (max " + std::to_string(JF_MAX_PORTS) + ")"; + return false; + } + slots_.assign(rxPorts_.size(), StreamSlot()); + + // One SUB socket + one socket monitor per interface. The monitors must + // be armed BEFORE connecting: they tell us whether each receiver stream is + // really connected, so an idle-but-alive receiver is never mistaken for a + // stopped one. + for (size_t i = 0; i < rxPorts_.size(); i++) { + void *sub = zmq_socket(zmqCtx_, ZMQ_SUB); + if (!sub) { + error = "zmq_socket(SUB) failed"; + return false; + } + zmq_setsockopt(sub, ZMQ_LINGER, &linger, sizeof(linger)); + zmq_setsockopt(sub, ZMQ_RCVTIMEO, &rcvtimeo, sizeof(rcvtimeo)); + zmq_setsockopt(sub, ZMQ_RCVHWM, &hwm, sizeof(hwm)); + zmq_setsockopt(sub, ZMQ_RCVBUF, &rcvbuf, sizeof(rcvbuf)); + zmq_setsockopt(sub, ZMQ_SUBSCRIBE, "", 0); + + const std::string monAddr = + "inproc://online_jf_monitor_" + std::to_string(i); + void *mon = nullptr; + if (zmq_socket_monitor(sub, monAddr.c_str(), ZMQ_EVENT_ALL) == 0) { + mon = zmq_socket(zmqCtx_, ZMQ_PAIR); + if (!mon || zmq_connect(mon, monAddr.c_str()) != 0) { + if (mon) { + zmq_close(mon); + mon = nullptr; + } + fprintf(stderr, + "[zmq] socket monitor unavailable for port %d, " + "falling back to frame-arrival staleness\n", + rxPorts_[i]); + } + } else { + fprintf(stderr, + "[zmq] zmq_socket_monitor failed for port %d: %s\n", + rxPorts_[i], zmq_strerror(zmq_errno())); + } + + const std::string addr = + "tcp://" + g_cfg.rxHost + ":" + std::to_string(rxPorts_[i]); + if (zmq_connect(sub, addr.c_str()) != 0) { + const std::string why = zmq_strerror(zmq_errno()); + zmq_close(sub); + if (mon) + zmq_close(mon); + error = "zmq_connect(" + addr + ") failed: " + why; + return false; + } + zmqSubs_.push_back(sub); + zmqMons_.push_back(mon); + if (mon) + monitorActive_.store(true); + } + + if (g_cfg.pubPort > 0) { + zmqPub_ = zmq_socket(zmqCtx_, ZMQ_PUB); + if (!zmqPub_) { + error = "zmq_socket(PUB) failed"; + return false; + } + zmq_setsockopt(zmqPub_, ZMQ_LINGER, &linger, sizeof(linger)); + zmq_setsockopt(zmqPub_, ZMQ_SNDTIMEO, &sndtimeo, sizeof(sndtimeo)); + const std::string bindAddr = "tcp://*:" + std::to_string(g_cfg.pubPort); + if (zmq_bind(zmqPub_, bindAddr.c_str()) != 0) { + error = "zmq_bind(" + bindAddr + ") failed: " + + zmq_strerror(zmq_errno()); + return false; + } + } + + gate_.setEnabled(g_cfg.gateEnabled); + gate_.setThreshold(g_cfg.threshold); + gate_.setMinPixels(g_cfg.minPixels); + gate_.setContiguous(g_cfg.contiguous); + gate_.setHotSuppress(g_cfg.hotSuppress); + return true; +} + +void Pipeline::start() { + if (running_.load()) + return; + running_.store(true); + startAnalysisWorkers(g_cfg.analysisWorkers); + acqThread_ = new std::thread(&Pipeline::acqLoop, this); + analysisThread_ = new std::thread(&Pipeline::analysisLoop, this); +} + +void Pipeline::stop() { + running_.store(false); + frames_.cv.notify_all(); + if (acqThread_) { + if (acqThread_->joinable()) + acqThread_->join(); + delete acqThread_; + acqThread_ = nullptr; + } + if (analysisThread_) { + if (analysisThread_->joinable()) + analysisThread_->join(); + delete analysisThread_; + analysisThread_ = nullptr; + } + // only after the analysis thread is done, so no worker is mid-frame + stopAnalysisWorkers(); +} + +bool Pipeline::connected() const { return gate_.connected_.load(); } + +uint64_t Pipeline::portFrames(int portIdx) const { + if (portIdx < 0 || portIdx >= static_cast(rxPorts_.size())) + return 0; + return track_[portIdx].frames.load(); +} + +int Pipeline::portWidth(int portIdx) const { + if (portIdx < 0 || portIdx >= static_cast(rxPorts_.size())) + return 0; + return track_[portIdx].w.load(); +} + +int Pipeline::portHeight(int portIdx) const { + if (portIdx < 0 || portIdx >= static_cast(rxPorts_.size())) + return 0; + return track_[portIdx].h.load(); +} + +uint64_t Pipeline::portLastFrame(int portIdx) const { + if (portIdx < 0 || portIdx >= static_cast(rxPorts_.size())) + return 0; + return track_[portIdx].lastFrame.load(); +} + +uint64_t Pipeline::resyncDrops() const { return resyncDrops_.load(); } + +uint64_t Pipeline::behindDrops() const { return behindDrops_.load(); } + +uint64_t Pipeline::incompleteFrames() const { return incomplete_.load(); } + +uint64_t Pipeline::flushes() const { return flushes_.load(); } + +uint64_t Pipeline::skippedPairs() const { return skippedPairs_.load(); } + +uint64_t Pipeline::shedFrames() const { return shedDrops_.load(); } + +int Pipeline::decimation() const { return decimate_.load(); } + +uint64_t Pipeline::displayFrames() const { return framesinstream.load(); } + +uint64_t Pipeline::displayDropped() const { + std::lock_guard lock(const_cast(frames_).mtx); + return frames_.dropped; +} + +void Pipeline::requestPedeAcquire(int nFrames) { + if (nFrames < 1) + nFrames = 1; + pedeAcqWant_.store(nFrames); + pedeAcqDone_.store(0); + { + std::lock_guard lock(frames_.mtx); + pedeAcq_.reset(); + } + pedeAcqActive_.store(true); + printf("[pede] acquiring from %d frames...\n", nFrames); + fflush(stdout); +} + +bool Pipeline::pedeAcquireActive() const { return pedeAcqActive_.load(); } + +int Pipeline::pedeAcquireProgress() const { return pedeAcqDone_.load(); } + +// acq thread, while a live pedestal acquisition is running +// In "save frames" mode this is the old path. In virtual-photon-counting mode +// the frames are not written: instead the acq thread keeps the window +// bookkeeping (first frame number and timestamp, frame count) and hands each +// published map to the saver tagged with it. +void Pipeline::offerToSaver(const short *frame, int w, int h, + const SavedFrameHeader &hdr) { + if (gSaveMode.load() != 1) { + saver_.offer(frame, w, h, hdr); + return; + } + if (pmapWinCount == 0) { + pmapWinFirstFrame = hdr.frameNumber; + pmapWinFirstStamp = hdr.timestamp; + } + pmapWinCount++; + PmapSlot &slot = pmapSave(); + std::vector map; + uint64_t frames = 0; + { + std::lock_guard lock(slot.mtx); + if (slot.ready) { + map.swap(slot.map); + frames = slot.frames; + slot.ready = false; + } + } + if (map.empty()) + return; + saver_.offerPhotonMap(map, frames, pmapWinFirstFrame, pmapWinFirstStamp); + pmapWinCount = 0; +} + +void Pipeline::setHotMaskFromPmap(double threshold) { + std::vector copy; + { + std::lock_guard lock(gHistoMtx); + copy = g_pmapAccum; // written per pixel by the workers, published here + } + hotSetMaskFromMap(copy.data(), copy.size(), threshold); + hotThreshold_.store(threshold); + gate_.resetCounters(); +} + +uint64_t Pipeline::pmapFrames() const { return g_pmapFrames; } + +void pmapSaveReset() { + std::lock_guard lock(gHistoMtx); + // assign (not fill): this also allocates, so the very first analysed frame + // after the save started is counted instead of being dropped while the + // accumulator is still empty + g_pmapSaveAccum.assign(static_cast(NCH), 0.0f); + g_pmapSaveCount = 0; +} + +void Pipeline::acquirePede(const short *frame) { + if (!pedeAcq_) + pedeAcq_ = std::make_shared(); + const int done = pedeAcqDone_.load(); + float *g0 = pedeAcq_->g0.data(); + for (uint64_t i = 0; i < NCH; i++) { + const float raw = static_cast(frame[i] & 0x3fff); + g0[i] = (g0[i] * static_cast(done) + raw) / + static_cast(done + 1); + } + const int n = done + 1; + pedeAcqDone_.store(n); + if (n < pedeAcqWant_.load()) + return; + + // publish: gain-0 only, so hasAll stays false (same as the old live + // acquisition, which never filled the G1/G2 arrays) + PedestalStore::instance().install(pedeAcq_); + { + std::lock_guard lock(frames_.mtx); + pedeAcq_.reset(); + } + pedeAcqActive_.store(false); + gFrameCountReset.store(true); + printf("[pede] installed pedestals averaged over %d frames (storage cell " + "%s)\n", + n, gate_.scFilter() < 0 + ? "ALL - every cell contributed" + : std::to_string(gate_.scFilter()).c_str()); + fflush(stdout); +} + +// --------------------------------------------------------------------------- +// Connection tracking (per interface) +// --------------------------------------------------------------------------- +// A receive timeout only means "no frame in the last 200 ms"; it says nothing +// about the receiver being alive. The ZMQ socket monitor tells us the truth: +// an idle-but-connected receiver stays "Connected"/"Idle", and "stopped" is +// only reported on a real disconnect or on the end-of-acquisition marker. +void Pipeline::drainMonitor(int portIdx) { + if (portIdx < 0 || portIdx >= static_cast(zmqMons_.size())) + return; + void *mon = zmqMons_[portIdx]; + if (!mon) + return; + StreamTrack &t = track_[portIdx]; + for (int k = 0; k < 64; k++) { // bounded burst per iteration + unsigned char buf[256]; + const int n = zmq_recv(mon, buf, sizeof(buf), ZMQ_DONTWAIT); + if (n < 0) + break; // EAGAIN: no more events + uint16_t event = 0; + if (n >= static_cast(sizeof(uint16_t))) + memcpy(&event, buf, sizeof(uint16_t)); + int more = 0; + size_t moreSize = sizeof(more); + zmq_getsockopt(mon, ZMQ_RCVMORE, &more, &moreSize); + if (more) { // drain the address part + char addr[256]; + zmq_recv(mon, addr, sizeof(addr), ZMQ_DONTWAIT); + } + + switch (event) { + case ZMQ_EVENT_CONNECTED: + case ZMQ_EVENT_HANDSHAKE_SUCCEEDED: + if (!t.peerConnected.exchange(true)) + t.connectedSinceMs.store(nowMs()); + t.everConnected.store(true); + break; + case ZMQ_EVENT_DISCONNECTED: + case ZMQ_EVENT_CLOSED: + case ZMQ_EVENT_HANDSHAKE_FAILED_NO_DETAIL: + case ZMQ_EVENT_HANDSHAKE_FAILED_PROTOCOL: + case ZMQ_EVENT_HANDSHAKE_FAILED_AUTH: + t.peerConnected.store(false); + break; + default: + break; // CONNECT_DELAYED / RETRIED / LISTENING / ... + } + } +} + +Pipeline::StreamState Pipeline::streamState(int portIdx, + double *sinceSec) const { + if (portIdx < 0 || portIdx >= static_cast(rxPorts_.size())) { + if (sinceSec) + *sinceSec = 0.0; + return StreamState::Disconnected; + } + const StreamTrack &t = track_[portIdx]; + const int64_t now = nowMs(); + double since = 0.0; + StreamState st; + + if (zmqMons_[portIdx]) { + if (!t.peerConnected.load()) { + st = t.everConnected.load() ? StreamState::Disconnected + : StreamState::Connecting; + if (t.lastHeaderMs.load() > 0) + since = (now - t.lastHeaderMs.load()) / 1000.0; + } else if (t.endOfAcq.load()) { + st = StreamState::EndOfAcq; + if (t.lastHeaderMs.load() > 0) + since = (now - t.lastHeaderMs.load()) / 1000.0; + } else { + const int64_t lastHdr = t.lastHeaderMs.load(); + if (lastHdr > 0) + since = (now - lastHdr) / 1000.0; + else { + const int64_t sinceConnect = t.connectedSinceMs.load(); + since = (sinceConnect > 0) ? (now - sinceConnect) / 1000.0 : 0.0; + } + st = (since * 1000.0 > kIdleWarnMs) ? StreamState::Idle + : StreamState::Connected; + } + } else { + // no monitor: fall back to frame-arrival staleness + const int64_t lastHdr = t.lastHeaderMs.load(); + if (lastHdr <= 0) { + st = t.everConnected.load() ? StreamState::Disconnected + : StreamState::Connecting; + } else { + since = (now - lastHdr) / 1000.0; + st = (since * 1000.0 > kNoMonitorStaleMs) ? StreamState::Disconnected + : StreamState::Connected; + } + } + if (sinceSec) + *sinceSec = since; + return st; +} + +// Overall state = the worst port: any dead port dominates, then idle. +Pipeline::StreamState Pipeline::streamState(double *sinceSec) const { + StreamState worst = StreamState::Connected; + double worstSince = 0.0; + int rank = 0; // Connected < Idle < EndOfAcq < Connecting < Disconnected + for (size_t i = 0; i < rxPorts_.size(); i++) { + double since = 0.0; + const StreamState st = streamState(static_cast(i), &since); + int r = 0; + switch (st) { + case StreamState::Connected: + r = 0; + break; + case StreamState::Idle: + r = 1; + break; + case StreamState::EndOfAcq: + r = 2; + break; + case StreamState::Connecting: + r = 3; + break; + case StreamState::Disconnected: + r = 4; + break; + } + if (i == 0 || r > rank) { + rank = r; + worst = st; + worstSince = since; + } + } + if (sinceSec) + *sinceSec = worstSince; + return worst; +} + +const char *Pipeline::streamStateName(StreamState st) { + switch (st) { + case StreamState::Disconnected: + return "disconnected"; + case StreamState::Connecting: + return "connecting"; + case StreamState::Connected: + return "connected"; + case StreamState::Idle: + return "idle"; + case StreamState::EndOfAcq: + return "eoa"; + } + return "?"; +} + +const char *Pipeline::streamStateName() const { + return streamStateName(streamState()); +} + +std::string Pipeline::streamStateSummary() const { + std::string out; + for (size_t i = 0; i < rxPorts_.size(); i++) { + if (i) + out += ","; + out += streamStateName(streamState(static_cast(i))); + } + return out; +} + +// acq thread: per-port status line, printed only on a real transition +void Pipeline::refreshStreamState(int portIdx) { + if (portIdx < 0 || portIdx >= static_cast(rxPorts_.size())) + return; + double since = 0.0; + const StreamState st = streamState(portIdx, &since); + const int shown = static_cast(st); + const int prev = portStateShown_[portIdx].exchange(shown); + if (prev == shown) + return; + + const int port = rxPorts_[portIdx]; + switch (st) { + case StreamState::Connected: + if (prev < 0 || prev == static_cast(StreamState::Connecting) || + prev == static_cast(StreamState::Disconnected)) + printf("Receiver connected on port %d\n", port); + break; + case StreamState::Disconnected: + printf("Receiver on port %d disconnected, waiting to reconnect...\n", + port); + break; + case StreamState::Idle: + printf("Receiver on port %d connected but no frames for %.1f s " + "(still alive)\n", + port, since); + break; + case StreamState::EndOfAcq: + printf("Receiver on port %d sent end-of-acquisition, waiting for new " + "stream\n", + port); + break; + case StreamState::Connecting: + break; + } + fflush(stdout); +} + +void Pipeline::refreshStreamState() { + for (size_t i = 0; i < rxPorts_.size(); i++) + refreshStreamState(static_cast(i)); + + const StreamState st = streamState(); + const bool up = (st != StreamState::Disconnected && + st != StreamState::Connecting); + gate_.setConnected(up); + + if (!multiPort()) // single-port wording is handled per port already + return; + const int shown = static_cast(st); + const int prev = stateShown_.exchange(shown); + if (prev == shown) + return; + if (st == StreamState::Disconnected) + printf("Stream lost: not all %zu interfaces are connected\n", + rxPorts_.size()); + else if (st == StreamState::Idle) + printf("Waiting for frames on at least one interface\n"); + fflush(stdout); +} + +// --------------------------------------------------------------------------- +// Reading one interface +// --------------------------------------------------------------------------- +// Reads exactly one header+data pair from an interface into its slot. +// Returns false when there is nothing to read right now. +bool Pipeline::readOne(int portIdx) { + if (portIdx < 0 || portIdx >= static_cast(zmqSubs_.size())) + return false; + StreamTrack &t = track_[portIdx]; + StreamSlot &sl = slots_[portIdx]; + + // Discards the rest of the current multipart message, so a rejected + // header never leaves its data part to be read as the next "header". + auto drainParts = [&]() { + int more = 0; + size_t moreSize = sizeof(more); + zmq_getsockopt(zmqSubs_[portIdx], ZMQ_RCVMORE, &more, &moreSize); + char sink[65536]; + while (more) { + zmq_recv(zmqSubs_[portIdx], sink, sizeof(sink), 0); + zmq_getsockopt(zmqSubs_[portIdx], ZMQ_RCVMORE, &more, &moreSize); + } + }; + + std::vector hdr(kHeaderBytes); + const int hn = + zmq_recv(zmqSubs_[portIdx], hdr.data(), hdr.size(), ZMQ_DONTWAIT); + if (hn <= 0) { + if (hn < 0 && zmq_errno() != EAGAIN) + t.errors.fetch_add(1); + return false; + } + // zmq_recv returns the full message length even when it truncated it + if (static_cast(hn) > hdr.size()) { + t.errors.fetch_add(1); + drainParts(); + return false; + } + + rapidjson::Document d; + if (d.Parse(hdr.data(), static_cast(hn)).HasParseError()) { + drainParts(); + return false; + } + if (!d.HasMember("jsonversion") || !d.HasMember("data")) { + drainParts(); + return false; + } + if (d["jsonversion"].GetUint() != SLS_DETECTOR_JSON_HEADER_VERSION) { + drainParts(); + return false; + } + + // A valid header must be followed by its data part. The parts of a + // multipart message are queued together, so RCVMORE is authoritative here; + // without this check a header alone would make the data recv swallow the + // next message's header as pixels. + { + int moreAfterHdr = 0; + size_t moreAfterHdrSize = sizeof(moreAfterHdr); + zmq_getsockopt(zmqSubs_[portIdx], ZMQ_RCVMORE, &moreAfterHdr, + &moreAfterHdrSize); + if (!moreAfterHdr) { + t.errors.fetch_add(1); + return false; + } + } + + hdrBytes_.assign(hdr.begin(), hdr.begin() + hn); + hdrLen_ = hn; + + const int64_t now = nowMs(); + t.lastHeaderMs.store(now); + // Storage cell: bits 8-11 of "detector specific 3" (0..15, 15 = default). + // Kept per port; the assembled frame uses the first slab's value. + if (d.HasMember("detSpec3")) { + const uint32_t ds3 = d["detSpec3"].GetUint(); + t.sc.store(static_cast((ds3 >> 8) & 0xF)); + } + if (d["data"].GetUint() == 0) { // end-of-acquisition marker + t.endOfAcq.store(true); + refreshStreamState(portIdx); + drainParts(); // none expected (sent without SNDMORE) + return false; + } + t.endOfAcq.store(false); + { + // a data header must be followed by its data part + int more = 0; + size_t moreSize = sizeof(more); + zmq_getsockopt(zmqSubs_[portIdx], ZMQ_RCVMORE, &more, &moreSize); + if (!more) + return false; + } + + // Receiver ROI from the "rx_roi" array the receiver puts in every stream + // header: [xmin, xmax, ymin, ymax] in RAW port coordinates. This is NOT + // the gate ROI: it says which part of the port the receiver itself handled. + // It is drawn as the red box on the ADC image (mapped to the display when + // the strixel mapping is active). The old detSpec1 reinterpretation was a + // hack - detSpec1 is the bunch id - and is gone. + if (d.HasMember("rx_roi") && d["rx_roi"].IsArray() && + d["rx_roi"].Size() >= 4) { + const int xmin = d["rx_roi"][0].GetInt(); + const int xmax = d["rx_roi"][1].GetInt(); + const int ymin = d["rx_roi"][2].GetInt(); + const int ymax = d["rx_roi"][3].GetInt(); + if (xmax > xmin && ymax > ymin) { + // Each interface reports its own slice in RAW port coordinates, so + // a ROI crossing the interface boundary (e.g. y 28..356 on a 2x256 + // module) arrives as 28..255 on the bottom port and 0..100 on the + // top one. Translate each slice into assembled rows and draw the + // union, otherwise the box stopped at the interface boundary. + static std::atomic px0[JF_MAX_PORTS], px1[JF_MAX_PORTS], + py0[JF_MAX_PORTS], py1[JF_MAX_PORTS]; + static std::atomic seen[JF_MAX_PORTS]; + int slabH = 0; + if (d.HasMember("shape") && d["shape"].IsArray() && + d["shape"].Size() >= 2) + slabH = d["shape"][1].GetInt(); + const int nIf = static_cast(rxPorts_.size()); + const int totalH = fullH_.load() > 0 ? fullH_.load() : kRawHeight; + // A single interface already reports the ROI in module rows (the + // receiver owns the whole module), so it is drawn exactly as it + // arrives: never shift it, or the box lands in the dead region. + // With several interfaces each one reports only its own slice, + // which is translated by its row offset and unioned below. + int rowBase = 0; + if (nIf > 1 && slabH > 0 && slabH < totalH && totalH % nIf == 0) + rowBase = portIdx * (totalH / nIf); + px0[portIdx].store(xmin); + px1[portIdx].store(xmax); + py0[portIdx].store(ymin + rowBase); + py1[portIdx].store(ymax + rowBase); + seen[portIdx].store(true); + int ux0 = 1 << 30, ux1 = -1, uy0 = 1 << 30, uy1 = -1; + for (int k = 0; k < nIf; k++) { + if (!seen[k].load()) + continue; + ux0 = std::min(ux0, px0[k].load()); + ux1 = std::max(ux1, px1[k].load()); + uy0 = std::min(uy0, py0[k].load()); + uy1 = std::max(uy1, py1[k].load()); + } + const bool changed = (ux0 != rxRoiXmin.load() || + ux1 != rxRoiXmax.load() || + uy0 != rxRoiYmin.load() || + uy1 != rxRoiYmax.load()); + rxRoiXmin.store(static_cast(ux0)); + rxRoiXmax.store(static_cast(ux1)); + rxRoiYmin.store(static_cast(uy0)); + rxRoiYmax.store(static_cast(uy1)); + rxRoiValid.store(ux1 > ux0 && uy1 > uy0); + if (changed && rxRoiValid.load()) { + printf("[rx] receiver ROI x=%d..%d y=%d..%d (assembled rows, " + "drawn as the red box)\n", + ux0, ux1, uy0, uy1); + fflush(stdout); + } + } else { + rxRoiValid.store(false); + } + } else { + rxRoiValid.store(false); + } + + int w = 0, h = 0; + if (d.HasMember("shape") && d["shape"].IsArray() && + d["shape"].Size() >= 2) { + w = static_cast(d["shape"][0].GetUint()); + h = static_cast(d["shape"][1].GetUint()); + } + size_t needBytes = static_cast(NCH) * 2; + if (d.HasMember("size")) { + const unsigned sz = d["size"].GetUint(); + if (sz > 0) + needBytes = sz; + } else if (w > 0 && h > 0) { + needBytes = static_cast(w) * static_cast(h) * 2; + } + if (needBytes > static_cast(NCH) * 2 * 4) // sanity clamp + needBytes = static_cast(NCH) * 2; + + if (sl.data.size() < needBytes / 2) + sl.data.resize(needBytes / 2); + const int received = zmq_recv(zmqSubs_[portIdx], sl.data.data(), + static_cast(needBytes), 0); + if (received <= 0) { + t.errors.fetch_add(1); + return false; + } + // larger than advertised: zmq truncated it to needBytes but returns the + // full length, which must not become sl.len (it sizes the memcpy later) + const bool truncated = static_cast(received) > needBytes; + int more = 0; + size_t moreSize = sizeof(more); + zmq_getsockopt(zmqSubs_[portIdx], ZMQ_RCVMORE, &more, &moreSize); + if (more) { // payload larger than advertised: drain the remainder + char sink[65536]; + while (more) { + zmq_recv(zmqSubs_[portIdx], sink, sizeof(sink), 0); + zmq_getsockopt(zmqSubs_[portIdx], ZMQ_RCVMORE, &more, &moreSize); + } + } + if (truncated) { + t.errors.fetch_add(1); + return false; + } + + // Overwriting a slab that was never assembled means it is being discarded + // rather than shown late (only happens in the deliberate skip-ahead path). + if (sl.valid) + behindDrops_.fetch_add(1); + + g_acq.msgs++; + sl.len = static_cast(received) / sizeof(short); + sl.w = w; + sl.h = h; + sl.valid = true; + sl.stampMs = now; + sl.frameKey = d.HasMember("frameNumber") + ? d["frameNumber"].GetUint64() + : (d.HasMember("frameIndex") ? d["frameIndex"].GetUint64() + : 0); + // Load shedding, applied here so it costs nothing extra: the message is + // already in the slot buffer, what it saves is everything downstream + // (assembly, gate, analysis, display, save), which is what lets the sockets + // keep draining and slsReceiver's ring buffer stay empty. The frame number + // is shared by both interfaces, so they drop exactly the same frames and + // stay in step. + { + const int keepDiv = g_keepDiv.load(); + if (keepDiv > 1 && + (sl.frameKey % static_cast(keepDiv)) != 0) { + sl.valid = false; // read and forgotten, never assembled + shedDrops_.fetch_add(1); + return false; + } + } + // header fields mirrored into the ROI-save parameter datasets + { + SavedFrameHeader &h = sl.hdr; + h = SavedFrameHeader(); + h.frameNumber = sl.frameKey; + if (d.HasMember("expLength")) + h.expLength = d["expLength"].GetUint(); + if (d.HasMember("packetNumber")) + h.packetNumber = d["packetNumber"].GetUint(); + if (d.HasMember("detSpec1")) + h.detSpec1 = d["detSpec1"].GetUint64(); + if (d.HasMember("timestamp")) + h.timestamp = d["timestamp"].GetUint64(); + if (d.HasMember("modId")) + h.modId = static_cast(d["modId"].GetUint()); + if (d.HasMember("row")) + h.row = static_cast(d["row"].GetUint()); + if (d.HasMember("column")) + h.column = static_cast(d["column"].GetUint()); + if (d.HasMember("detSpec2")) + h.detSpec2 = static_cast(d["detSpec2"].GetUint()); + if (d.HasMember("detSpec3")) + h.detSpec3 = d["detSpec3"].GetUint(); + if (d.HasMember("detSpec4")) + h.detSpec4 = static_cast(d["detSpec4"].GetUint()); + if (d.HasMember("detType")) + h.detType = static_cast(d["detType"].GetUint()); + if (d.HasMember("version")) + h.version = static_cast(d["version"].GetUint()); + } + t.frames.fetch_add(1); + t.lastFrame.store(sl.frameKey); + if (portIdx < JF_MAX_PORTS) + lastProgressMs_[portIdx] = now; // this interface is alive and moving + if (firstDataMs_ == 0) + firstDataMs_ = now; + if (w > 0) { + t.w.store(w); + fullW_.store(w); + } + if (h > 0) + t.h.store(h); + fullH_.store(kRawHeight); // N slabs of kRawHeight/N rows assemble + + if (!sl.logged) { + sl.logged = true; + const int slabRows = + multiPort() ? kRawHeight / static_cast(rxPorts_.size()) + : kRawHeight; + const int rowBase = static_cast(portIdx) * slabRows; + printf("[stream] port %d: %dx%d (%zu B) -> rows %d..%d of %dx%d\n", + rxPorts_[portIdx], w, h, sl.len * sizeof(short), rowBase, + rowBase + (h > 0 ? h : slabRows) - 1, fullW_.load(), + fullH_.load()); + fflush(stdout); + } + refreshStreamState(portIdx); + return true; +} + +// One frame per poll cycle. The acq loop only calls this for an interface +// whose slot is EMPTY (flow control): a slab waiting to be paired is never +// overwritten, which is what lets a persistently lagging interface still be +// paired instead of losing the leader's matching frame. +void Pipeline::readStream(int portIdx) { readOne(portIdx); } + +// Discards a lagging interface's queued frames until it reaches targetKey. +void Pipeline::drainTo(int portIdx, uint64_t targetKey) { + StreamSlot &sl = slots_[portIdx]; + for (int k = 0; k < kMaxDrainPerCall; k++) { + if (sl.valid && sl.frameKey >= targetKey) + return; + if (!readOne(portIdx)) + return; + } +} + +// --------------------------------------------------------------------------- +// Frame assembly (multi-interface) +// --------------------------------------------------------------------------- +// Each interface delivers one Y-slab of the module (1024x256 for a Jungfrau +// split in two). Slabs are stacked in the order the ports were given, the +// first port occupying the LOWEST rows, so "30001+30002" means (bottom, top). +// +// Slabs are paired on the DETECTOR frame number (frameIndex is per-streamer +// and offset by each interface's own first index, so it cannot be a shared +// key). +// +// Alignment policy - an assembled frame is complete and always as fresh as +// the streams allow: +// * every socket is polled and drained at full speed, always - holding a +// slab makes slsReceiver's queue fill and the receiver then skips frames on +// that interface, which is what makes the two streams drift apart; +// * the newest slab of every interface is paired, whatever its frame number: +// after a per-interface skip the numbers stay offset for ever, so matching +// on equal keys would never pair again and holding a slab to let the mate +// "catch up" only shifts the problem into the receiver's buffer. A skewed +// pair costs one frame with a one-frame-old half, counted in 'resync'; +// * a frame number gap is reported, and a persistently silent interface is +// reported once in a while (counted in 'incomplete'); +// * load shedding stays available for a real analysis backlog (see the +// ladder in the acquisition loop): 1/2, 1/4, 7/8 of the frames are dropped +// synchronously, keyed on the shared frame number. +bool Pipeline::tryAssemble(bool force) { + const size_t n = rxPorts_.size(); + if (n == 0) + return false; + + int totalH = fullH_.load(); + int width = fullW_.load(); + if (totalH <= 0 || width <= 0) { + totalH = kRawHeight; + width = kRawWidth; + } + static bool geomWarned = false; + if (totalH % static_cast(n) != 0) { + if (!geomWarned) { + geomWarned = true; + printf("[frames] ERROR: %zu interfaces cannot evenly split %d rows; " + "assembling is disabled (check the rx_port list)\n", + n, totalH); + fflush(stdout); + } + return false; + } + const int slabRows = totalH / static_cast(n); + for (size_t i = 0; i < n; i++) { + const int h = slots_[i].h; + if (slots_[i].valid && h > 0 && h != slabRows && h != totalH && + !geomWarned) { + geomWarned = true; + printf("[frames] ERROR: port %d streams %d rows but this " + "configuration expects %d (or a full %d); assembling is " + "disabled\n", + rxPorts_[i], h, slabRows, totalH); + fflush(stdout); + } + } + + // ---- how many interfaces have a slab ready? -------------------------- + size_t validCount = 0; + uint64_t lo = 0, hi = 0; + for (size_t i = 0; i < n; i++) { + if (!slots_[i].valid) + continue; + if (validCount == 0) + lo = hi = slots_[i].frameKey; + else { + lo = std::min(lo, slots_[i].frameKey); + hi = std::max(hi, slots_[i].frameKey); + } + validCount++; + } + if (validCount == 0) + return false; // nothing pending + + if (validCount < n) { + // An interface has nothing pending. Nothing is held back: the socket + // is drained continuously, so the pending slab is simply replaced by + // the next frame if the partner stays away. Only a genuinely silent + // interface deserves a message. + const int64_t now = nowMs(); + bool stalled = false; + for (size_t i = 0; i < n; i++) { + if (slots_[i].valid) + continue; + const int64_t last = (i < JF_MAX_PORTS) ? lastProgressMs_[i] : 0; + if (last > 0) { + if (now - last > kStallMs) + stalled = true; + } else if (now - acqStartMs_ > kInitialPairTimeoutMs) { + stalled = true; + } + } + if (stalled && now - lastPairWarnMs_.load() > 5000) { + lastPairWarnMs_.store(now); + incomplete_.fetch_add(1); + printf("[frames] an interface has no data: check the interface " + "states\n"); + fflush(stdout); + } + return false; + } + + // ---- all interfaces have a slab -------------------------------------- + // A skip on ONE interface (slsReceiver does that per interface) leaves the + // two frame numbers permanently offset: the interface that skipped keeps + // sending one number further on, for ever. Matching on equal keys would + // therefore never succeed again, and holding the leader until the mate + // "catches up" is worse - it fills that socket, so the receiver skips even + // more. So the two newest slabs are always paired; the frame numbers are + // used to *report* the skew, and a skew costs exactly one slightly + // mismatched frame instead of a stream that never assembles again. + if (lo != hi) { + const uint64_t gap = hi - lo; + const int64_t now3 = nowMs(); + // Hard dropping: the halves are not the same detector frame, so the + // pair is discarded instead of being shown or saved. Both sockets stay + // drained, nothing is held back, and every frame that *is* processed is + // index-aligned - which is what matters once frames are written to disk. + // One-frame lookahead: one interface already moved on, so pair its + // *previous* payload with the other one's current payload. The keys + // then match and no frame is dropped. + bool handled = false; + // One-frame lookahead: the leading interface already consumed `lo`, so + // pair its previous payload with the lagging slab of the same frame. + if (gap == 1) { + bool ok = true; + for (size_t i = 0; i < n; i++) { + slots_[i].usePrev = false; + if (slots_[i].frameKey == lo) + continue; + if (slots_[i].hasPrev && slots_[i].prevKey == lo) + slots_[i].usePrev = true; + else + ok = false; + } + if (!ok) // never leave a half-selected pair behind + for (size_t i = 0; i < n; i++) + slots_[i].usePrev = false; + handled = ok; + } + if (!handled && !allowSkewedPair_ && lastAssembledMs_ != 0 && + now3 - lastAssembledMs_ < kNoPairFallbackMs) { + // Only the OLDER slabs are dropped: the leading slab is kept, so + // the very next frame from the lagging interface carries the same + // frame number and pairs. Discarding both (what we did before) + // threw away a frame and left the two streams just as far apart. + resyncDrops_.fetch_add(1); + for (size_t i = 0; i < n; i++) + if (slots_[i].valid && slots_[i].frameKey < hi) + slots_[i].valid = false; + if (now3 - lastPairWarnMs_.load() > 5000) { + lastPairWarnMs_.store(now3); + printf("[frames] interfaces are %llu frame(s) apart; dropping " + "the pair to keep every frame aligned\n", + static_cast(gap)); + fflush(stdout); + } + return false; + } + if (handled) { + pairWaitStartMs_ = 0; + allowSkewedPair_ = false; + } else { + // Nothing has paired for kNoPairFallbackMs: the two frame numbers are + // offset for good (an interface that keeps skipping). Pair the newest + // slabs anyway so the stream stays live, and report it. + allowSkewedPair_ = true; // stay in this mode until a pair matches again + skewedPairs_.fetch_add(1); + if (now3 - lastPairWarnMs_.load() > 5000) { + lastPairWarnMs_.store(now3); + printf("[frames] interfaces are %llu frame(s) apart and nothing has " + "paired for %lld ms; pairing the newest slabs\n", + static_cast(gap), + static_cast(kNoPairFallbackMs)); + fflush(stdout); + } + } + } + + const double tCopy0 = acqNow(); + // ---- index-aligned: assemble ----------------------------------------- + if (assembled_.size() != static_cast(NCH)) + assembled_.assign(static_cast(NCH), 0); + + for (size_t i = 0; i < n; i++) { + StreamSlot &sl = slots_[i]; + const int rowBase = static_cast(i) * slabRows; + sl.rowBase = rowBase; + + const size_t dstOffset = static_cast(rowBase) * width; + const size_t dstAvail = (dstOffset < assembled_.size()) + ? assembled_.size() - dstOffset + : 0; + const short *srcPtr = + sl.usePrev ? sl.prevData.data() : sl.data.data(); + const size_t srcLen = sl.usePrev ? sl.prevLen : sl.len; + size_t srcOffset = 0; + const size_t rows = static_cast(sl.h > 0 ? sl.h : slabRows); + if (sl.h == totalH && n > 1) // interface streams the whole module + srcOffset = static_cast(rowBase) * width; + size_t count = rows * static_cast(width); + if (srcOffset + count > srcLen) + count = (srcLen > srcOffset) ? srcLen - srcOffset : 0; + if (count > dstAvail) + count = dstAvail; + if (count) + memcpy(assembled_.data() + dstOffset, srcPtr + srcOffset, + count * sizeof(short)); + if (count < dstAvail) + memset(reinterpret_cast(assembled_.data()) + dstOffset + + count * sizeof(short), + 0, (dstAvail - count) * sizeof(short)); + } + + uint64_t bytes = 0; + const uint64_t pairKey = lo; // the frame that was actually assembled + for (size_t i = 0; i < n; i++) { + bytes += slots_[i].len * sizeof(short); + if (slots_[i].usePrev) { + // Contributed its PREVIOUS frame: the newer slab is still the + // leading frame, so keep it pending for the next pairing. + slots_[i].usePrev = false; + slots_[i].hasPrev = false; + } else { + slots_[i].valid = false; + // The frame just assembled becomes the lookahead candidate; the + // swap keeps a free buffer for the next read (no memory traffic). + std::swap(slots_[i].data, slots_[i].prevData); + slots_[i].prevKey = slots_[i].frameKey; + slots_[i].prevLen = slots_[i].len; + slots_[i].hasPrev = true; + } + } + g_acq.copy += acqNow() - tCopy0; // just the per-interface memcpy/memset + pairWaitStartMs_ = 0; + lastAssembledMs_ = nowMs(); + if (lo == hi) + allowSkewedPair_ = false; // realigned, back to strict pairing + + if (bytes == 0) + return false; + + static bool firstAssembledLogged = false; + if (!firstAssembledLogged) { + firstAssembledLogged = true; + printf("[frames] first assembled frame: "); + for (size_t i = 0; i < n; i++) { + const size_t px = + static_cast(static_cast(i) * slabRows) * + static_cast(width); + printf("port %d -> row %d = %d%s", rxPorts_[i], + static_cast(i) * slabRows, + px < assembled_.size() + ? static_cast(assembled_[px] & 0x3fff) + : -1, + (i + 1 < n) ? ", " : "\n"); + } + fflush(stdout); + } + + gate_.noteReceived(bytes, false); + + // "Acquire Pede": the pedestal depends on the storage cell, so only frames + // that the SC filter accepts contribute (SC filter == ALL -> every cell, + // which would mix their pedestals). The signal gate is deliberately not + // applied: its *dropped* frames are the empty ones a pedestal wants. + if (pedeAcqActive_.load() && + (gate_.scFilter() < 0 || gate_.scFilter() == track_[0].sc.load())) + acquirePede(assembled_.data()); + + std::shared_ptr peds = + gNoPede.load() ? nullptr : PedestalStore::instance().get(); + const int g1off = G1Poffset.load(); + const int g2off = G2Poffset.load(); + uint64_t above = 0; + // the storage cell comes from the first (bottom) interface's header; both + // interfaces belong to the same frame, so they report the same cell + bool scDropped = false; + const bool forward = + gate_.evaluate(assembled_.data(), static_cast(NCH), + track_[0].sc.load(), peds.get(), g1off, g2off, above, + &scDropped); + // pedestal tracking: a frame the *signal* gate dropped is "beam off" data + if (!forward && !scDropped && gPedeTracking.load() && gate_.enabled()) { + const GateEngine::Counters gc = gate_.snapshot(); + int rx0 = 0, ry0 = 0, rx1 = 1024, ry1 = 512; + if (gate_.roiSet()) + gate_.roiSnapshot(rx0, ry0, rx1, ry1); + pedeTrackerFeed(assembled_.data(), static_cast(NCH), + peds.get(), + pedeTrackSignalDropRate(gc.framesPassCur, + gc.framesDropCur, + gc.framesPass, gc.framesDrop), + rx0, ry0, rx1, ry1); + } + + if (forward) { + if (saver_.running()) + offerToSaver(assembled_.data(), width, totalH, slots_[0].hdr); + const size_t fullBytes = static_cast(NCH) * 2; + if (zmqPub_) { // assembled full frame with a synthesised header + char hdr[512]; + const int hn = snprintf( + hdr, sizeof(hdr), + "{\"jsonversion\":%u,\"data\":1,\"size\":%zu," + "\"shape\":[%d,%d],\"frameNumber\":%llu}", + static_cast(SLS_DETECTOR_JSON_HEADER_VERSION), + fullBytes, width, totalH, + static_cast(pairKey)); + zmq_send(zmqPub_, hdr, hn, ZMQ_SNDMORE); + zmq_send(zmqPub_, assembled_.data(), + static_cast(fullBytes), 0); + } + frames_.publish(assembled_.data(), fullBytes); + gate_.noteForwarded(static_cast(NCH) * 2); + } + return true; +} + +// --------------------------------------------------------------------------- +// acq thread: poll all interfaces -> pair -> assemble -> gate -> hand off +// --------------------------------------------------------------------------- +void Pipeline::acqLoop() { + acqStartMs_ = nowMs(); + + // 1 Hz rate bookkeeping + auto lastRateTime = steady_clock::now(); + uint64_t prevIn = 0, prevPass = 0, prevInB = 0, prevOutB = 0; + + // poll set: one entry per SUB socket and per monitor socket + std::vector items; + for (size_t i = 0; i < zmqSubs_.size(); i++) { + items.push_back(zmq_pollitem_t{zmqSubs_[i], 0, ZMQ_POLLIN, 0}); + // A null socket with fd 0 would poll STDIN: use fd -1 (ignored) when + // a socket monitor could not be created. + items.push_back(zmqMons_[i] + ? zmq_pollitem_t{zmqMons_[i], 0, ZMQ_POLLIN, 0} + : zmq_pollitem_t{nullptr, -1, 0, 0}); + } + const int nSub = static_cast(zmqSubs_.size()); + + while (running_.load()) { + { + auto now = steady_clock::now(); + const double dt = duration(now - lastRateTime).count(); + if (dt >= 1.0) { + GateEngine::Counters s = gate_.snapshot(); + gate_.r_.rateIn.store(s.framesIn > prevIn + ? (s.framesIn - prevIn) / dt + : 0.0); + gate_.r_.ratePass.store(s.framesPass > prevPass + ? (s.framesPass - prevPass) / dt + : 0.0); + const uint64_t inB = gate_.c_.bytesIn.load(); + const uint64_t outB = gate_.c_.bytesOut.load(); + gate_.r_.rateInMBs.store( + inB > prevInB ? (inB - prevInB) / dt / 1e6 : 0.0); + gate_.r_.ratePassMBs.store( + outB > prevOutB ? (outB - prevOutB) / dt / 1e6 : 0.0); + // ---- alignment load-shedding ladder ---------------------- + // "Trouble" is the interfaces having to drop slabs to stay in + // step (lagging resyncs, overwritten slabs, half frames). While + // it keeps growing, deliberately drop a synchronised fraction of + // the frames (1/2, then 3/4, then 7/8) so the rest of the chain + // keeps up and the sockets stay drained; when it has been clean + // for a few seconds, step back up. + if (multiPort() && g_autoDecimate.load()) { + // A resync or a replaced slab is the normal cost of pairing + // the newest slabs (one frame, never a stall), so it must + // not drive the ladder: only frames that the analysis has + // accepted but not finished do. + // Frames that entered the gate but have not been + // classified yet. Rejects and storage-cell drops are + // finished frames, not a backlog - counting them pinned the + // ladder at 1/8 whenever the gate rejected anything. + const int64_t backlog = + static_cast(s.framesIn) - + static_cast(s.framesPass) - + static_cast(s.framesDrop) - + static_cast(s.framesScDrop); + const uint64_t trouble = + backlog > 50 ? static_cast(backlog) : 0; + const int d = g_keepDiv.load(); + if (trouble != shedLastTrouble_) { + shedLastTrouble_ = trouble; + shedCleanSeconds_ = 0; + if (d < 8) { + g_keepDiv.store(d * 2); + printf("[align] analysis backlog (%llu frame(s)): " + "keeping 1/%d of the frames\n", + static_cast(trouble), + d * 2); + fflush(stdout); + } + } else if (d > 1 && ++shedCleanSeconds_ >= 5) { + shedCleanSeconds_ = 0; + g_keepDiv.store(d / 2); + printf("[align] streams clean: keeping 1/%d of the " + "frames\n", d / 2); + fflush(stdout); + } + } + { + const double tot = g_acq.poll + g_acq.read + g_acq.pair; + const double other = + tot < dt ? dt - tot : 0.0; + const double inv = (dt > 0.0) ? 100.0 / dt : 0.0; + printf("[perf] poll=%.0f%% read=%.0f%% pair=%.0f%% " + "(copy=%.0f%%) other=%.0f%% loops=%llu " + "offered=%.0f/s\n", + g_acq.poll * inv, g_acq.read * inv, + g_acq.pair * inv, g_acq.copy * inv, other * inv, + (unsigned long long)g_acq.n, + (dt > 0.0) ? g_acq.msgs / dt : 0.0); + fflush(stdout); + g_acq.poll = g_acq.read = g_acq.pair = g_acq.copy = 0.0; + g_acq.n = 0; + g_acq.msgs = 0; + } + prevIn = s.framesIn; + prevPass = s.framesPass; + prevInB = inB; + prevOutB = outB; + lastRateTime = now; + } + } + + // Every socket is polled and drained at full speed, always. Holding a + // slab makes the *receiver* keep sending into our queue until it is + // full, and slsReceiver then skips frames on that interface - which is + // precisely how the two streams drift apart. Draining keeps the queues + // shallow and lets the pairing resolve a gap in one frame instead. + for (int i = 0; i < nSub; i++) + items[2 * i].events = ZMQ_POLLIN; + + g_acq.t0 = acqNow(); + const int rc = zmq_poll(items.data(), static_cast(items.size()), + 200 /* ms: also lets us poll running_ */); + if (rc < 0) { + if (zmq_errno() == ETERM) + break; + continue; + } + if (rc == 0) { // idle: NOT "receiver stopped" + refreshStreamState(); + if (multiPort()) + tryAssemble(false); + continue; + } + + g_acq.poll += acqNow() - g_acq.t0; // waiting in zmq_poll + g_acq.t1 = acqNow(); + for (int i = 0; i < nSub; i++) { + if (items[2 * i + 1].revents & ZMQ_POLLIN) + drainMonitor(i); + // Drain the socket down to its newest message. Pairing the two + // newest slabs is what keeps the halves from the same instant, and + // an empty queue is what keeps slsReceiver from skipping on that + // interface. Bounded per wake-up so the other sockets are not + // starved. + if (items[2 * i].revents & ZMQ_POLLIN) { + for (int k = 0; k < 32; k++) { + if (!readOne(i)) + break; + int ev = 0; + size_t evs = sizeof(ev); + if (zmq_getsockopt(zmqSubs_[i], ZMQ_EVENTS, &ev, &evs) != 0 || + !(ev & ZMQ_POLLIN)) + break; + } + } + } + g_acq.read += acqNow() - g_acq.t1; // recv + JSON + slab copies + g_acq.t2 = acqNow(); + refreshStreamState(); + + // Catch the slower interface up instead of discarding the leading + // frames: while the newest slabs disagree, take extra messages from the + // socket whose frame number is lower (about two reads for its one), so + // it reaches the same frame number. Bounded, and it stops as soon as + // that socket has nothing queued - it is then at the live edge, and + // only a frame the receiver skipped can explain the remaining gap. + if (multiPort()) { + for (int round = 0; round < 4; round++) { + size_t vc = 0; + uint64_t lo2 = 0, hi2 = 0; + int behind = -1; + for (int i = 0; i < nSub; i++) { + if (!slots_[i].valid) + continue; + if (vc == 0) { + lo2 = hi2 = slots_[i].frameKey; + behind = i; + } else if (slots_[i].frameKey < lo2) { + lo2 = slots_[i].frameKey; + behind = i; + } else { + hi2 = slots_[i].frameKey; + } + vc++; + } + if (vc < 2 || lo2 == hi2 || behind < 0) + break; + int got = 0; + for (int k = 0; k < 8; k++) { + if (!readOne(behind)) + break; + got++; + // stop the moment it reaches the leader: one message too + // many and it becomes the leader itself, which only flips + // the mismatch around + if (slots_[behind].valid && + slots_[behind].frameKey >= hi2) + break; + int ev = 0; + size_t evs = sizeof(ev); + if (zmq_getsockopt(zmqSubs_[behind], ZMQ_EVENTS, &ev, + &evs) != 0 || + !(ev & ZMQ_POLLIN)) + break; + } + if (!got) + break; + } + } + + if (multiPort()) { + // Pair the slabs; only index-aligned frames are processed. The + // latency is bounded by the small receive queue (--rx-hwm): when + // the stream is faster than this program the excess is discarded + // by ZMQ, not copied and thrown away here. + tryAssemble(false); + g_acq.pair += acqNow() - g_acq.t2; // pairing + assemble copy + g_acq.n++; + } else { + StreamSlot &sl = slots_[0]; + if (!sl.valid) + continue; + if (assembled_.size() != static_cast(NCH)) + assembled_.assign(static_cast(NCH), 0); + const size_t fullBytes = static_cast(NCH) * 2; + const uint64_t bytes = sl.len * sizeof(short); + const size_t copyBytes = std::min(bytes, fullBytes); + // A single interface may carry the top half: "30027:top" puts the + // slab in the upper rows and zeroes the rest of the frame. Without + // a suffix (or for a full-frame stream) it stays at offset 0. + size_t dstOffset = 0; + const int width = fullW_.load() > 0 ? fullW_.load() : kRawWidth; + const int height = fullH_.load() > 0 ? fullH_.load() : kRawHeight; + const int rows = (width > 0) ? static_cast(copyBytes / + (2 * width)) + : 0; + if (rxSlab_.size() > 0 && rxSlab_[0] == 2 && height > 0 && + rows > 0 && rows < height) { + dstOffset = static_cast(height - rows) * width * 2; + } + memset(assembled_.data(), 0, fullBytes); // both halves start clean + if (copyBytes) + memcpy(reinterpret_cast(assembled_.data()) + dstOffset, + sl.data.data(), copyBytes); + { // say once where a single interface lands + static bool told = false; + if (!told && rows > 0) { + told = true; + printf("[frames] single interface %d: %dx%d at rows %d..%d" + " (%s)%s\n", + rxPorts_[0], width, rows, + static_cast(dstOffset / (2 * width)), + static_cast(dstOffset / (2 * width)) + rows, + dstOffset ? "top" : "bottom", + (rows == height) ? " [full frame]" : ""); + fflush(stdout); + } + } + sl.valid = false; + + gate_.noteReceived(bytes, false); + + if (pedeAcqActive_.load() && + (gate_.scFilter() < 0 || gate_.scFilter() == track_[0].sc.load())) + acquirePede(assembled_.data()); + + std::shared_ptr peds = + gNoPede.load() ? nullptr : PedestalStore::instance().get(); + const int g1off = G1Poffset.load(); + const int g2off = G2Poffset.load(); + uint64_t above = 0; + bool scDropped = false; + const bool forward = gate_.evaluate( + assembled_.data(), static_cast(NCH), + track_[0].sc.load(), peds.get(), g1off, g2off, above, + &scDropped); + if (!forward && !scDropped && gPedeTracking.load() && + gate_.enabled()) { + const GateEngine::Counters gc = gate_.snapshot(); + int rx0 = 0, ry0 = 0, rx1 = 1024, ry1 = 512; + if (gate_.roiSet()) + gate_.roiSnapshot(rx0, ry0, rx1, ry1); + pedeTrackerFeed( + assembled_.data(), static_cast(NCH), peds.get(), + pedeTrackSignalDropRate(gc.framesPassCur, gc.framesDropCur, + gc.framesPass, gc.framesDrop), + rx0, ry0, rx1, ry1); + } + + if (forward) { + if (saver_.running()) + offerToSaver(assembled_.data(), fullW_.load(), + fullH_.load(), sl.hdr); + if (zmqPub_) { // verbatim receiver header + payload + if (hdrLen_ > 0) + zmq_send(zmqPub_, hdrBytes_.data(), hdrLen_, + ZMQ_SNDMORE); + zmq_send(zmqPub_, assembled_.data(), + static_cast(std::min(bytes, + fullBytes)), + 0); + } + frames_.publish(assembled_.data(), fullBytes); + gate_.noteForwarded(static_cast(NCH) * 2); + } + } + } +} + +// --------------------------------------------------------------------------- +// Analysis worker pool +// --------------------------------------------------------------------------- +// Steps 1 and 2 are pure per-pixel work on disjoint ranges, and since the +// ROOT histograms are only touched by the deferred sync they are free of ROOT +// state: they can run on several threads with no locking at all. adc_image, +// gain_image, g_pmapAccum and hchptr are written per pixel (disjoint), so only +// the chip spectra - a scatter into 8x502 counters - need per-worker copies +// that the controller merges once per frame (a few thousand adds). +// +// The worker threads are persistent; a frame is handed over with a generation +// counter, so the per-frame cost is two condvar handshakes (~us). +namespace { + +struct FrameJob { + const short *img = nullptr; + const float *g0 = nullptr; + const float *g1 = nullptr; + const float *g2 = nullptr; + bool hasAll = false; + bool useGain = false; + bool doPhotonMap = false; + int G1 = 0, G2 = 0; + int phene = 0; + float pthr = 0.0f; + // gate ROI of this frame (DATA coordinates, half-open, row 0 at the + // bottom): drives the ROI spectra. roiOn == false -> no ROI pixel, the + // ROI spectra stay empty for this frame. + bool pmapSave = false; // also accumulate into the save map + bool roiOn = false; + int roiX0 = 0, roiY0 = 0, roiX1 = 1024, roiY1 = 512; + // photon counting value: adcpedecorr, or adcpedecorr / (ADU per keV) when + // the gain correction is on (the threshold then means keV). The map is + // used when loaded, the nominal 40 ADU/keV otherwise. + const double *pcGain = nullptr; + double pcNominalGain = 0.0; + // calibration (g0 of the gain map, ADU/keV); nullptr -> no keV spectra + const double *gain = nullptr; + // ADU/keV to use when no map is loaded but the keV view is on (nominal + // 40 ADU/keV); 0 -> do not accumulate the keV set at all + double nominalGain = 0.0; +}; + +std::mutex g_poolMtx; +std::condition_variable g_poolWorkCv; +std::condition_variable g_poolDoneCv; +FrameJob g_job; +uint64_t g_poolGen = 0; +int g_poolPending = 0; +bool g_poolStop = false; +int g_nWorkers = 1; +std::vector g_workerThreads; +std::atomic g_workerError{false}; +struct ChipCounts { + uint32_t c[8][kChipOverflowBins]; + uint32_t k[8][kChipOverflowBins]; // same, in keV +}; +struct RoiCounts { + uint32_t c[kChipOverflowBins]; + uint32_t k[kChipOverflowBins]; +}; +std::vector g_workerChip; +std::vector g_workerRoi; // one spectrum over the gate-ROI pixels +std::vector g_workerFills; + +// processes pixel range [i0,i1) of the current job (steps 1 and 2) +void analysisRange(int widx, int i0, int i1, const FrameJob &job) { + const short *img = job.img; + const float *g0 = job.g0, *g1 = job.g1, *g2 = job.g2; + uint32_t(*chip)[kChipOverflowBins] = g_workerChip[widx].c; + uint32_t(*kevChip)[kChipOverflowBins] = g_workerChip[widx].k; + uint32_t *roi = g_workerRoi[widx].c; + uint32_t *roiKev = g_workerRoi[widx].k; + double fills = 0.0; + // hot pixel mask for this frame (one atomic load, not one per pixel) + const auto hmask = hotMaskSnapshot(); + + // ---- step 1: ADC / gain images -------------------------------------- + for (int i = i0; i < i1; i++) { + const int adcvalue = img[i] & 0x3fff; + const int gain = (img[i] & 0xc000) ? ((img[i] >> 14) & 0x3) : 0; + float disp = float(adcvalue) - (g0 ? g0[i] : 0.0f); + if ((i % 256 == 0) || (i % 256 == 255)) + disp = int(disp / factor); + if ((i / 1024 == 255) || (i / 1024 == 256) || (i / 1024 == 767) || + (i / 1024 == 768)) + disp = int(disp / factor); + adc_image[i] = disp; + gain_image[i] = gain; + } + + // ---- step 2: photon counting + PH spectra + photon map --------------- + for (int i = i0; i < i1; i++) { + // Masked pixels contribute nothing to the PH spectra, the chip spectra + // or the photon map - they are not part of the detector signal. + if (hmask && i < hmask->size() && (*hmask)[static_cast(i)]) + continue; + const int adcvalue = img[i] & 0x3fff; + const int gain = (img[i] & 0xc000) ? ((img[i] >> 14) & 0x3) : 0; + float adcpedecorr = 0.0f; + + // Pedestal-subtracted ADC, with the Jungfrau gain-bit conversion for + // pixels flagged as high-gain (g1) or gain-3 (g2). "Apply gain" does + // not touch this: it is what divides the value by ADU/keV for the keV + // spectra (see the kbin block), so the ADU side stays raw ADC. + if (job.g0) { + if (gain == 0 || !job.hasAll) + adcpedecorr = (adcvalue & 0x3fff) - g0[i]; + if (gain == 1 && job.hasAll) + adcpedecorr = (g1[i] + job.G1 - adcvalue) * 30.0; + if (gain == 3 && job.hasAll) + adcpedecorr = (g2[i] + job.G2 - adcvalue) * 340.0; + } else { + adcpedecorr = float(adcvalue); + } + // Photon counting: the threshold (and the multiplicity that follows + // from it) is in ADU, or in keV while the gain correction is on - the + // same ADU/keV the keV spectra use, so the two never disagree. + float pcval = adcpedecorr; + if (job.pcGain) + pcval = adcpedecorr / static_cast(job.pcGain[i]); + else if (job.pcNominalGain > 0.0) + pcval = static_cast(adcpedecorr / job.pcNominalGain); + if (pcval > job.pthr && job.g0) { + int inc = 1; + if (job.phene > 0) + inc = (int)((pcval + job.pthr) / + static_cast(job.phene)); + hchptr[i] = hchptr[i] + inc; + if (job.doPhotonMap) { + g_pmapAccum[i] += static_cast(inc); + fills += 1.0; + if (job.pmapSave && i < g_pmapSaveAccum.size()) + g_pmapSaveAccum[i] += static_cast(inc); + } + } + + // Chip spectra, with the same 20-column / 20-row border exclusion the + // original display used. i is the linear data index, so i%1024 is the + // column and i/1024 the DATA row (row 0 = bottom, see README.md). + const int px = i % 1024; + const int py = i / 1024; + if (px < 1004 && px > 20 && py > 20) { + const int ichip = i / (256 * 256 * 4) * 4 + ((i / 256) % 4); + int schip = ichip; +#ifdef JFSTRX + schip = 0; + if (ichip == 1) { + if (i <= (1024 * (64 - 2))) + schip = 0; + if (i > (1024 * (64 + 6)) && i <= (1024 * (64 * 2 - 4))) + schip = 1; + if (i > (1024 * (64 * 2 + 5))) + schip = 2; + } + if (ichip == 6) { + if (i <= (1024 * (256 + 2 * 64 - 5))) + schip = 7; + if (i > (1024 * (256 + 2 * 64 + 4)) && + i <= (1024 * (256 + 64 * 3 - 6))) + schip = 6; + if (i > (1024 * (256 + 64 * 3 - 4))) + schip = 5; + } +#endif + const int bin = chipBin(adcpedecorr); + chip[schip][bin]++; + // calibration for the keV spectra: the same pixel/trim, but the + // value divided by the pixel's ADU/keV. The keV axes are the ADU + // axes / KEV_NOMINAL_ADU with the same bin count, so multiplying + // by the nominal constant maps keV onto the ADU bin index. + int kbin = -1; + const double gainVal = job.gain ? job.gain[i] : job.nominalGain; + if (gainVal > 0.1) { + const double kev = adcpedecorr / gainVal; + kbin = chipBin(kev * KEV_NOMINAL_ADU); + kevChip[schip][kbin]++; + } + // the ROI spectra are the same thing restricted to the gate ROI, + // and keep the border trim so they are a strict subset of hchip + if (job.roiOn && px >= job.roiX0 && px < job.roiX1 && + py >= job.roiY0 && py < job.roiY1) { + roi[bin]++; + if (kbin >= 0) + roiKev[kbin]++; + } + } + } + g_workerFills[widx] = fills; +} + +void analysisWorkerBody(int widx) { + uint64_t seen = 0; + for (;;) { + int i0 = 0, i1 = 0; + FrameJob job; + { + std::unique_lock lk(g_poolMtx); + g_poolWorkCv.wait(lk, [&] { + return g_poolGen != seen || g_poolStop; + }); + if (g_poolStop) + return; + seen = g_poolGen; + job = g_job; + i0 = widx * (NCH / 1024 / g_nWorkers) * 1024; + i1 = (widx + 1 == g_nWorkers) + ? NCH + : (widx + 1) * (NCH / 1024 / g_nWorkers) * 1024; + } + // An exception must never leave a std::thread's function: that calls + // std::terminate and kills the whole program. Report and carry on. + try { + analysisRange(widx, i0, i1, job); + } catch (const std::exception &e) { + if (!g_workerError.exchange(true)) + fprintf(stderr, "[analysis] worker %d exception: %s\n", widx, + e.what()); + } catch (...) { + if (!g_workerError.exchange(true)) + fprintf(stderr, "[analysis] worker %d unknown exception\n", + widx); + } + { + std::lock_guard lk(g_poolMtx); + if (--g_poolPending == 0) + g_poolDoneCv.notify_one(); + } + } +} + +void startAnalysisWorkers(int n) { + if (n < 1) + n = 1; + if (n > 32) + n = 32; + g_nWorkers = n; + g_workerChip.resize(n); + g_workerRoi.resize(n); + for (int k = 0; k < n; k++) { + memset(&g_workerChip[k], 0, sizeof(ChipCounts)); + memset(&g_workerRoi[k], 0, sizeof(RoiCounts)); + } + g_workerFills.assign(n, 0.0); + g_poolStop = false; + g_poolGen = 0; + for (int k = 1; k < n; k++) // worker 0 is the analysis thread itself + g_workerThreads.emplace_back(analysisWorkerBody, k); +} + +void stopAnalysisWorkers() { + { + std::lock_guard lk(g_poolMtx); + g_poolStop = true; + } + g_poolWorkCv.notify_all(); + for (auto &t : g_workerThreads) { + if (t.joinable()) + t.join(); + } + g_workerThreads.clear(); +} + +// Runs steps 1+2 for a whole frame: worker 0 is the analysis thread, the +// others are woken through the generation counter; the chip spectra of every +// worker are merged (and cleared) once the frame is done. +void runAnalysisFrame(const FrameJob &job) { + if (g_nWorkers <= 1) { + analysisRange(0, 0, NCH, job); + } else { + { + std::lock_guard lk(g_poolMtx); + g_job = job; + g_poolPending = g_nWorkers - 1; + ++g_poolGen; + } + g_poolWorkCv.notify_all(); + const int rows = NCH / 1024; + const int rowsPer = rows / g_nWorkers; + analysisRange(0, 0, rowsPer * 1024, job); + { + std::unique_lock lk(g_poolMtx); + g_poolDoneCv.wait(lk, [] { return g_poolPending == 0; }); + } + } + for (int k = 0; k < g_nWorkers; k++) { + auto *w = g_workerChip[k].c; + auto *wk = g_workerChip[k].k; + auto *r = g_workerRoi[k].c; + auto *rk = g_workerRoi[k].k; + for (int ic = 0; ic < 8; ic++) { + for (int b = 0; b < kChipOverflowBins; b++) { + const uint32_t v = w[ic][b]; + if (v) { + g_chipAccum[ic][b] += v; + w[ic][b] = 0; + } + const uint32_t vk = wk[ic][b]; + if (vk) { + g_kevAccum[ic][b] += vk; + wk[ic][b] = 0; + } + } + } + for (int b = 0; b < kChipOverflowBins; b++) { + const uint32_t vr = r[b]; + if (vr) { + g_roiAccum[b] += vr; + r[b] = 0; + } + const uint32_t vrk = rk[b]; + if (vrk) { + g_roiKevAccum[b] += vrk; + rk[b] = 0; + } + } + g_pmapFills += g_workerFills[k]; + g_pmapFrames++; + g_workerFills[k] = 0.0; + } +} + +} // namespace + +// --------------------------------------------------------------------------- +// analysis thread: pedestals -> images -> ROOT fills +// --------------------------------------------------------------------------- +void Pipeline::analysisLoop() { + std::vector in; + size_t inLen = 0; + uint64_t inSeq = 0; + g_timing = (getenv("JF_TIMING") != nullptr); + const auto tStart = std::chrono::steady_clock::now(); + + while (running_.load()) { + try { // an escaping exception would abort the process + if (g_workerError.exchange(false)) + gate_.setLastError("analysis worker failed - see stderr"); + { + TT tt(g_timing ? &tTake : nullptr); + if (!frames_.waitTake(in, inLen, inSeq, running_)) + continue; + } + if (!running_.load()) + break; + + if (gFrameCountReset.exchange(false)) + framesinstream.store(0); + + { + TT tt(g_timing ? &tCopy : nullptr); + const size_t copyBytes = + std::min(inLen * sizeof(short), sizeof(image_data)); + if (copyBytes) + memcpy(image_data, in.data(), copyBytes); + } + tFrames += g_timing ? 1 : 0; + + if (goout.load() == 0) + continue; // paused: no counters, no fills, no ROOT redraw + + const std::shared_ptr pedsSp = + gNoPede.load() ? nullptr : PedestalStore::instance().get(); + const PedeSet *peds = pedsSp.get(); + const bool havePede = (peds != nullptr); + const bool hasAll = havePede && peds->hasAll; + const float *g0 = havePede ? peds->g0.data() : nullptr; + const float *g1 = hasAll ? peds->g1.data() : nullptr; + const float *g2 = hasAll ? peds->g2.data() : nullptr; + const int G1 = G1Poffset.load(); + const int G2 = G2Poffset.load(); + const bool useGain = gain_flag.load(); + + const uint64_t fsn = framesinstream.fetch_add(1) + 1; + int hde = HDraw_every.load(); + if (hde < 1) + hde = 1; + // "every N analysed frames" (the Faster/Slower knob), plus a floor so a + // slowly gated stream still shows every frame: display anyway when the + // previous image is older than DISPLAY_FLOOR_MS + const int64_t dispNow = nowMs(); + bool show2Ds = ((fsn % static_cast(hde)) == + static_cast(hde - 1)); + if (!show2Ds && dispNow - g_lastShownMs >= DISPLAY_FLOOR_MS) + show2Ds = true; + if (show2Ds) { + g_lastShownMs = dispNow; + gImagesShown.fetch_add(1); + } + + processedf++; + gFramesForRoot.fetch_add(1); + + // Every mutation of the source histograms below (including the + // windowed scatter reset) happens under gHistoMtx. The ROOT thread's + // "Reset His" takes the same lock, so a Reset() can never race the + // per-frame Fill(). The lock is released at the end of this + // iteration (copyBins() on the ROOT side is intentionally lock-free). + std::lock_guard histoLock(gHistoMtx); + ensureAccumulators(); // allocation + zeroing under the same lock + + // open a fresh accumulation window for the scatter plots after each + // ROOT redraw (the photon map itself stays cumulative) + if (gSoftResetPending.exchange(false)) { + his4500->Reset(); + hisLvsR->Reset(); + } + // "Reset His" requested by the GUI: the analysis thread performs it + // here, where the lock is already held, so the ROOT thread never has to + // wait for gHistoMtx (which it can starve under a heavy stream) + if (gResetHisPending.exchange(false)) { + historesetSourcesLocked(); + forceRootRedraw(); // repaint the cleared histograms + wake the ROOT thread + } + + // ---- 1+2) per-frame images, photon counting, spectra, photon map -- + // (pure per-pixel work over disjoint ranges - see runAnalysisFrame) + { + TT tt(g_timing ? &tStep1 : nullptr); + FrameJob job; + job.img = image_data; + job.g0 = g0; + job.g1 = g1; + job.g2 = g2; + job.hasAll = hasAll; + job.useGain = useGain; + job.doPhotonMap = dophotonmap.load(); + // only while a save is actually running: the save map is pure + // overhead otherwise + job.pmapSave = (gSaveMode.load() == 1 && saver_.running()); + job.G1 = G1; + job.G2 = G2; + // photon peak ~= 2 x the ADU threshold (the historic relation + // between the argument and its threshold) + // ADU normally, keV while the gain correction is on (the GUI keeps + // one spinbox per unit and swaps them with the gain button) + const bool gainStage = gain_flag.load(); + const float pthr = gainStage ? g_pheneThresholdKev + : g_pheneThreshold; + job.phene = static_cast(2.0f * pthr + 0.5f); + job.pthr = pthr; + // calibration snapshot (shared_ptr keeps it alive for this frame) + const std::shared_ptr gainSp = + GainMapStore::instance().get(); + // keV spectra exist only while the gain stage is active: with the + // gain off the display is ADU, so nothing is converted (and the + // per-pixel divide is skipped) + const bool kevOn = kevSpectra.load() && gain_flag.load(); + job.gain = (gainSp && kevOn) ? gainSp->g0() : nullptr; + // no map but the operator asked for keV: use the nominal + // 40 ADU/keV ("no gain map selected - using nominal values") + job.nominalGain = (!gainSp && kevOn) ? KEV_NOMINAL_ADU : 0.0; + // the photon counting follows the gain stage itself, not the keV PH + // checkbox (that one only decides what the spectra display) + job.pcGain = (gainSp && gainStage) ? gainSp->g0() : nullptr; + job.pcNominalGain = (!gainSp && gainStage) ? KEV_NOMINAL_ADU : 0.0; + // The gate ROI is read ONCE per frame and published for everything + // that shows it (ADC overlay is set by the GUI from the same + // snapshot, ROOT box + ROI spectra from this job), so image and + // ROOT can never disagree about where the ROI is. The rectangle + // is in DATA rows (row 0 = bottom); a torn/degenerate snapshot + // falls back to "no ROI". + { + int rx0 = 0, ry0 = 0, rx1 = kRawWidth, ry1 = kRawHeight; + // A ROI that exists is enough for the ROI spectra and the box: + // "ROI only" controls whether the GATE is restricted to it, not + // whether the ROI is shown. (roiActive() is only true while + // "ROI only" is on, which left A7 empty for a plain selection.) + bool on = gate_.roiSet(); + if (on) + gate_.roiSnapshot(rx0, ry0, rx1, ry1); + if (rx1 <= rx0 || ry1 <= ry0) { + on = false; + rx0 = 0; + ry0 = 0; + rx1 = kRawWidth; + ry1 = kRawHeight; + } + job.roiOn = on; + job.roiX0 = rx0; + job.roiY0 = ry0; + job.roiX1 = rx1; + job.roiY1 = ry1; + ROIxmin.store(static_cast(rx0)); + ROIxmax.store(static_cast(rx1)); + ROIymin.store(static_cast(ry0)); + ROIymax.store(static_cast(ry1)); + ROIactive.store(on); + } + runAnalysisFrame(job); + } + + // ---- 3) T vs B / L vs R from the current frame ------------------- + { + TT tt(g_timing ? &tStep3 : nullptr); + for (int ipx = 0; ipx < 1024; ipx++) { + his4500->Fill(adc_image[ipx + (512 - 21) * 1024], + adc_image[ipx + 20 * 1024]); + } + for (int ipy = 0; ipy < 512; ipy++) { + hisLvsR->Fill(adc_image[20 + ipy * 1024], + adc_image[1024 - 21 + ipy * 1024]); + } + } + + // ---- 4) raw-ADC map accumulation on the display cadence ---------- + { + TT tt(g_timing ? &tStep4 : nullptr); + if (!dophotonmap.load() && show2Ds) { + for (int ipx = 0; ipx < 1024; ipx++) { + for (int ipy = 0; ipy < 512; ipy++) { + g_pmapAccum[ipx + ipy * 1024] += + adc_image[ipx + ipy * 1024]; + g_pmapFills += 1.0; + } + } + } + } + + // ---- 4b) push the accumulators into the ROOT histograms ---------- + // (at the redraw cadence - the canvases are painted once per + // rootUpdateMs, so nothing is lost by not doing this per frame) + { + TT tt(g_timing ? &tSync : nullptr); + syncAccumulators(nowMs()); + } + +#ifdef JFSTRX + if (show2Ds) { + jungfrauLGADStrixelsData *decoder = new jungfrauLGADStrixelsData(); + const int g1_nrows = 165, g2_nrows = 320, g3_nrows = 476; + // Six-panel output, index 0..2 = chip1 g1/g2/g3, 3..5 = chip6 + // g1/g2/g3, and the mapped-image row at which each panel starts. + static const int panelStartY[kNumAdcPanels] = {0, 165, 485, + 1757, 1437, 961}; + for (int p = 0; p < kNumAdcPanels; p++) { + const size_t n = static_cast(kAdcPanelW[p]) * + static_cast(kAdcPanelH[p]); + if (g_adcPanel[p].size() != n) + g_adcPanel[p].assign(n, 0.0f); + } + for (int ipx = 0; ipx < nx; ipx++) { + for (int ipy = 0; ipy < ny; ipy++) { + const int i = + int(((decoder->dataMap[ipy][ipx]) - sizeof(header)) / 2); + const int group = decoder->groupmap[ipy][ipx]; + const float value = + static_cast(image_data[i] & 0x3fff) - + (g0 ? g0[i] : 0.0f); + static const int mc1g1y_start = 0; + static const int mc1g2y_start = g1_nrows; + static const int mc1g3y_start = g1_nrows + g2_nrows; + static const int mc6g3y_start = + g1_nrows + g2_nrows + g3_nrows; + static const int mc6g2y_start = mc6g3y_start + g3_nrows; + static const int mc6g1y_start = + mc6g3y_start + g3_nrows + g2_nrows; + if (group == 0) { + if (ipy < 165) + his1000->Fill(float(ipx), + float(ipy - mc1g1y_start), value); + if (ipy >= mc6g1y_start) + his1060->Fill(float(ipx), + float(ipy - mc6g1y_start), value); + } + if (group == 1) { + if (ipy < (165 + 320)) + his1001->Fill(float(ipx), + float(ipy - mc1g2y_start), value); + if (ipy >= mc6g2y_start && ipy < mc6g1y_start) { + his1061->Fill(float(ipx), + float(ipy - mc6g2y_start), value); + } + } + if (group == 2) { + if (ipy < (165 + 320 + 476)) + his1002->Fill(float(ipx), + float(ipy - mc1g3y_start), value); + if (ipy >= mc6g3y_start && ipy < mc6g2y_start) + his1062->Fill(float(ipx), + float(ipy - mc6g3y_start), value); + } + // Feed the separate six-panel window with the mapped + // groups. The Qt ADC/gain images stay raw 1024x512. + if (group >= 0 && group <= 2) { + const int base = (ipy >= mc6g3y_start) ? 3 : 0; + const int p = base + group; + const int lx = ipx; + const int ly = ipy - panelStartY[p]; + if (lx >= 0 && lx < kAdcPanelW[p] && ly >= 0 && + ly < kAdcPanelH[p]) { + g_adcPanel[p][static_cast(lx) + + static_cast(ly) * + kAdcPanelW[p]] = value; + } + } + } + } + delete decoder; + } +#endif + + // ---- 5) hand the displayed images to the Qt timer ---------------- + { + TT tt(g_timing ? &tStep5 : nullptr); + if (show2Ds) { + std::lock_guard lock(images_.mtx); + // The ADC/gain windows are ALWAYS the raw 1024x512 layout, so + // the gate-ROI selection stays in raw data coordinates even + // under JFSTRX. + const size_t nPix = static_cast(NCH); + if (images_.adc.size() != nPix) { + images_.adc.assign(nPix, 0.0f); + images_.gain.assign(nPix, 0.0f); + } + memcpy(images_.adc.data(), adc_image, nPix * sizeof(float)); + memcpy(images_.gain.data(), gain_image, nPix * sizeof(float)); +#ifdef JFSTRX + // the mapped groups for the separate six-panel window + for (int p = 0; p < kNumAdcPanels; p++) { + const size_t n = g_adcPanel[p].size(); + if (images_.panels[p].size() != n) + images_.panels[p].resize(n); + if (n) + memcpy(images_.panels[p].data(), g_adcPanel[p].data(), + n * sizeof(float)); + } + images_.panelsReady = true; +#endif + images_.ready = true; + images_.seq = inSeq; + } + } + + // ---- 5a) the map that will be saved ------------------------------ + // Filled in the same worker pass (job.pmapSave), published every + // gPmapSaveEvery analysed frames, never cumulative. + // NOTE: gHistoMtx is already held here (histoLock, top of the frame): + // taking it again deadlocked the analysis thread on the first frame. + if (gSaveMode.load() == 1 && saver_.running()) { + if (g_pmapSaveAccum.size() != static_cast(NCH)) + g_pmapSaveAccum.assign(static_cast(NCH), 0.0f); + if (++g_pmapSaveCount >= + static_cast(std::max(1, gPmapSaveEvery.load()))) { + PmapSlot &slot = pmapSave(); + { + std::lock_guard sl(slot.mtx); + if (slot.map.size() != static_cast(NCH)) + slot.map.assign(static_cast(NCH), 0.0f); + memcpy(slot.map.data(), g_pmapSaveAccum.data(), + static_cast(NCH) * sizeof(float)); + slot.frames = g_pmapSaveCount; + slot.seq++; + slot.ready = true; + } + std::fill(g_pmapSaveAccum.begin(), g_pmapSaveAccum.end(), 0.0f); + g_pmapSaveCount = 0; + } + } + + // ---- 5b) the photon map, on its own cadence ---------------------- + // Every gPmapEvery ANALYSED frames the window gets the map: the photons + // counted since the previous publish when it is not cumulative (the + // default), or the running total when it is. "Analysed" means frames + // that survived the gate and the decimation, so the cadence is not + // independent of them: rejecting or skipping frames stretches the + // wall-clock interval between two maps. + { + const int every = std::max(1, gPmapEvery.load()); + if (static_cast(every) > 0 && + (fsn % static_cast(every)) == 0 && + !g_pmapAccum.empty()) { + std::lock_guard lock(images_.mtx); + if (images_.photon.size() != static_cast(NCH)) + images_.photon.assign(static_cast(NCH), 0.0f); + memcpy(images_.photon.data(), g_pmapAccum.data(), + static_cast(NCH) * sizeof(float)); + images_.photonReady = true; + images_.photonSeq = fsn; + if (!gPmapCumulative.load()) { // fresh image every time + std::fill(g_pmapAccum.begin(), g_pmapAccum.end(), 0.0f); + g_pmapFills = 0; + g_pmapFillsSynced = 0; + } + } + } + } catch (const std::exception &e) { + fprintf(stderr, "[analysis] exception: %s (analysis stopped)\n", + e.what()); + fflush(stderr); + gate_.setLastError(std::string("analysis: ") + e.what()); + break; + } catch (...) { + fprintf(stderr, "[analysis] unknown exception (analysis stopped)\n"); + fflush(stderr); + gate_.setLastError("analysis: unknown exception"); + break; + } + } + + // final flush so the histograms match the accumulators when we stop + { + std::lock_guard lock(gHistoMtx); + syncAccumulators(nowMs(), true); + } + + if (g_timing && tFrames > 0) { + const double n = static_cast(tFrames); + const double wall = std::chrono::duration( + std::chrono::steady_clock::now() - tStart) + .count(); + printf("[timing] frames=%llu wall=%.1fs implied_rate=%.0ffps\n", + static_cast(tFrames), wall, + tFrames / (wall > 0 ? wall : 1)); + printf("[timing] per-frame us: take=%.0f copy=%.0f pixelwork=%.0f " + "step3_scatter=%.0f step4_rawmap=%.0f sync=%.0f " + "step5_publish=%.0f | sum=%.0f\n", + tTake / n, tCopy / n, tStep1 / n, tStep3 / n, tStep4 / n, + tSync / n, tStep5 / n, + (tTake + tCopy + tStep1 + tStep3 + tStep4 + tSync + tStep5) / n); + fflush(stdout); + } +} + +// --------------------------------------------------------------------------- +// "Reset His": reset the source accumulators. The analysis thread is the +// only writer of the source histograms, so a reset from the ROOT thread must +// take gHistoMtx (the analysis thread holds it around every frame's fills). +// --------------------------------------------------------------------------- +void historesetSources() { + std::lock_guard lock(gHistoMtx); + historesetSourcesLocked(); +} + +// gHistoMtx already held (the analysis thread applies a pending "Reset His" +// here, at the top of a frame, so the ROOT thread never waits for the lock). +void historesetSourcesLocked() { + resetAccumulators(); // must match the Reset() below + if (his4500) + his4500->Reset(); + if (hisLvsR) + hisLvsR->Reset(); + if (his3000) + his3000->Reset(); + if (hchip) { + for (int k = 0; k < 8; k++) { + if (hchip[k]) + hchip[k]->Reset(); + } + } + if (hroi) + hroi->Reset(); + if (hchipKev) { + for (int k = 0; k < 8; k++) { + if (hchipKev[k]) + hchipKev[k]->Reset(); + } + } + if (hroiKev) + hroiKev->Reset(); +#ifdef JFSTRX + if (his1000) + his1000->Reset(); + if (his1001) + his1001->Reset(); + if (his1002) + his1002->Reset(); + if (his1060) + his1060->Reset(); + if (his1061) + his1061->Reset(); + if (his1062) + his1062->Reset(); +#endif +} diff --git a/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/online_jf_gui.cpp b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/online_jf_gui.cpp new file mode 100644 index 000000000..12abee7a2 --- /dev/null +++ b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/online_jf_gui.cpp @@ -0,0 +1,5739 @@ +// SPDX-License-Identifier: LGPL-3.0-or-other +// =========================================================================== +// online_jf_gui.cpp -- Qt5 GUI, fast image widgets, CLI parsing, main(). +// +// Merged from: +// onlinedisp_zmq.cpp (FastImageView, MainWindow, display controls) +// jf_event_finder_gui.cpp (gate parameters, counters, pedestal loading) +// +// The GUI thread never touches ROOT graphics and never touches a frame +// buffer that an +//other thread is writing: frames come from the analysis +// thread through ImageMailbox and are copied into the widgets' own storage. +// =========================================================================== +#include "online_jf.h" + +#include +#include +#include +#include +#include + +#include + +namespace { + +std::sig_atomic_t g_sigCount = 0; + +void handleSigint(int) { + // Async-signal-safe: only count. The Qt timer performs the shutdown + // (blocking joins are not signal-safe). A second Ctrl-C force-quits. + if (++g_sigCount > 1) + std::_Exit(0); +} + +// --------------------------------------------------------------------------- +// The flow chart is drawn with arrow labels placed in the layout between the +// boxes: 1. storage cell -> Pedestal -> 2. signal gate -> Gain -> ROI save. +// A stage that is switched off is hidden, so the arrows that pointed at it +// disappear and the flow reads as the bypass it is. +// --------------------------------------------------------------------------- +// The strip between the two columns. It draws the connectors that cross from +// one column to the other as proper orthogonal arrows with a filled head, so +// the flow reads as one path instead of a few loose glyphs. +// Flow colours. The geometry of the diagram is fixed; only the class of an +// arrow can change (see FlowStrip::setSaveMode), so a bypassed stage is greyed +// as a panel while the data flow stays readable. +enum FlowClass { + FlowNormal = 0, // analysis / display flow + FlowPedestal, // the pedestal path (incl. the tracking feedback) + FlowData, // the chain that ends in the saved file -> orange + FlowOff // a feeder that is bypassed at the moment -> pale +}; +static QString flowColor(int cls) { + switch (cls) { + case FlowPedestal: + return QString("#a8c8e8"); // lighter blue + case FlowData: + return QString("#de7d14"); // orange + case FlowOff: + return QString("#c9c9c9"); // bypassed: drawn, but not carrying data + case FlowNormal: + default: + return QString("#5b9bd5"); // light blue + } +} + +class FlowStrip : public QWidget { + public: + explicit FlowStrip(QWidget *parent) : QWidget(parent) { + setMinimumWidth(64); + setToolTip( + "Data flow. Orange: everything the saved data travels through -\n" + "frames: storage cell -> Pedestal -> Gain -> ROI -> Save, photon\n" + "maps: ... -> Gain -> counting -> (ROI ->) Save (the maps always\n" + "use the pedestal). The ROI is fed by the Gain for frames and by\n" + "the counting for the maps, always in orange: it only exists for\n" + "the file. Light blue: the branches that only feed the display.\n" + "Lighter blue: the pedestal tracking pair. The display can use the\n" + "pedestal while the file does not (or the other way round), which is\n" + "why both feeders into the gate funnel can be drawn at once."); + // Fixed width for the strip, Preferred (not Expanding) height: an + // Expanding vertical policy made the whole column row as tall as the + // window, which left a large empty band above the save panel. + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + } + // The endpoints are computed by MainWindow::layoutFlow() (main thread, + // layout settled) and handed over as plain coordinates in this widget's + // space, so painting never has to walk the widget tree. + static const int kFunnelX = 24; // where the flow passes the gate + + struct Path { + int yFrom = 0, yTo = 0; + int dir = 1; // +1 left->right, -1 right->left, +2 right rail (Gain->ROI) + bool on = true; + int cls = FlowNormal; + int x = 0; // 0: default jog, else the vertical runs here + bool head = true; // false: the line merges into a junction + bool startAtJog = false; // begin on the vertical, not at an edge + }; + // Everything that depends on the stage states: which feeder carries the + // data (pedestal or the raw bypass), which branch feeds the file, and + // whether the gate funnel is active. Geometry never moves. + // `displayPede`: the display chain uses the pedestal (the Pede/Raw toggle). + // `savePede`: the data being written does (Pedestal corrected, or photon + // maps, which need g0). They can differ - the usual case is a corrected + // display with raw frames going to the file - so the two feeders into the + // funnel can be live at the same time, and their colour says what each one + // carries: orange for the file, light blue for the display. + void setStages(bool displayPede, bool savePede, bool gateOn, + bool photonMaps) { + const bool pedeUsed = displayPede || savePede; + // A route that carries nothing is not drawn at all (grey was more + // confusing than helpful). + p1_.on = pedeUsed; // storage cell -> Pedestal + p2_.on = pedeUsed; // Pedestal -> funnel + p8_.on = !savePede; // storage cell -> funnel: the raw save route + const int pedeCls = savePede ? FlowData : FlowNormal; + p1_.cls = pedeCls; // storage cell -> Pedestal + p2_.cls = pedeCls; // Pedestal -> funnel + p3_.cls = FlowData; // funnel -> Gain: always on the route to the file + p8_.cls = FlowData; // storage cell -> funnel (raw save) + // The ROI only exists for the file, so its input is always orange - and + // it comes from whichever stage produces the saved product: the Gain + // for frames, the counting for the maps. + p4_.cls = FlowData; // Gain -> ROI + p4_.on = !photonMaps; + p9_.cls = FlowData; // counting -> ROI (the maps are ROI-cropped too) + p9_.on = photonMaps; + p6_.cls = photonMaps ? FlowData : FlowNormal; // Gain -> counting + funOn_ = gateOn; + update(); + } + void setPaths(const Path &a, const Path &b, const Path &c, const Path &d, + const Path &e, const Path &f, const Path &g, const Path &h, + const Path &i, int funnelY) { + p1_ = a; + p2_ = b; + p3_ = c; + p4_ = d; + p5_ = e; + p6_ = f; + p7_ = g; + p8_ = h; + p9_ = i; + funY_ = funnelY; + update(); + } + + protected: + void paintEvent(QPaintEvent *) override { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing, true); + cross(p, p1_); // 1. storage cell -> Pedestal + cross(p, p2_); // Pedestal -> 2. signal gate + cross(p, p3_); // 2. signal gate -> Gain + cross(p, p4_); // Gain -> ROI, down the right-hand rail + cross(p, p5_); // 2. signal gate -> Pedestal tracking (dropped frames) + cross(p, p6_); // Gain -> Virtual photon counting (calibration) + cross(p, p7_); // Virtual photon counting -> Display (the map) + cross(p, p8_); // storage cell -> funnel: the raw (no-pede) bypass + cross(p, p9_); // Virtual photon counting -> ROI (maps) + drawFunnel(p); // the signal gate, drawn as the filter it is + } + + private: + Path p1_, p2_, p3_, p4_, p5_, p6_, p7_, p8_, p9_; + int funY_ = -1; + bool funOn_ = true; + + // The signal gate does not change the data, it only decides which frames + // travel on: it is drawn as a funnel on the line, with a short blue arrow + // from the gate panel into its side (the gate is what "gates"). With the + // gate switched off the symbol greys out instead of disappearing. + void drawFunnel(QPainter &p) { + if (funY_ < 0) + return; + const QColor c(flowColor(funOn_ ? FlowNormal : FlowOff)); + QPen pen(c); + pen.setWidth(2); + p.setPen(pen); + const int hw = 10, hh = 18; + QPolygon f; + f << QPoint(kFunnelX - hw, funY_ - hh / 2) + << QPoint(kFunnelX + hw, funY_ - hh / 2) + << QPoint(kFunnelX + hw / 3, funY_ + hh / 2) + << QPoint(kFunnelX - hw / 3, funY_ + hh / 2); + p.setBrush(Qt::NoBrush); + p.drawPolygon(f); + // the gate controls the funnel: arrow from the panel into its side + const int tipX = kFunnelX - hw / 3 - 1; + const int fromX = kFunnelX - hw - 14; + p.drawLine(fromX, funY_, tipX, funY_); + QPolygon head; + const int s = 6; + head << QPoint(tipX, funY_) << QPoint(tipX - s, funY_ - s) + << QPoint(tipX - s, funY_ + s); + p.setBrush(c); + p.drawPolygon(head); + p.setBrush(Qt::NoBrush); + } + + // dir = +1 : left edge -> right edge (head on the right) + // dir = -1 : right edge -> left edge (head on the left) + void cross(QPainter &p, const Path &l) { + if (!l.on) + return; + const int yFrom = l.yFrom, yTo = l.yTo, dir = l.dir; + const int w = width(); + QPen pen(QColor(flowColor(l.cls))); + pen.setWidth(2); + p.setPen(pen); + // The vertical part of each connector is put near the edge it ends on, + // and never in the middle: otherwise the three connectors line up into + // one long vertical that looks like it crosses the arrows. + const int jog = (l.x > 0) ? l.x : ((dir > 0) ? (w - 14) : 14); + QPolygon pts; + if (dir == 2) { // both ends on the right: a rail beside the panels + pts << QPoint(w, yFrom) << QPoint(w - 10, yFrom) + << QPoint(w - 10, yTo) << QPoint(w - 2, yTo); + p.drawPolyline(pts); + const QPoint tip(w - 1, yTo); + const int s2 = 7; + QPolygon head; + head << tip << QPoint(tip.x() - s2, tip.y() - s2) + << QPoint(tip.x() - s2, tip.y() + s2); + p.setBrush(p.pen().color()); + p.drawPolygon(head); + p.setBrush(Qt::NoBrush); + return; + } + if (dir > 0) { + if (!l.startAtJog) + pts << QPoint(0, yFrom); + pts << QPoint(jog, yFrom) << QPoint(jog, yTo); + if (l.head) + pts << QPoint(w - 8, yTo); + } else { + if (!l.startAtJog) + pts << QPoint(w, yFrom); + pts << QPoint(jog, yFrom) << QPoint(jog, yTo); + if (l.head) + pts << QPoint(8, yTo); + } + p.drawPolyline(pts); + if (!l.head) + return; // merged into the junction (the funnel) + // filled head at the target end + const QPoint tip = (dir > 0) ? QPoint(w - 1, yTo) : QPoint(1, yTo); + QPolygon head; + const int s = 7; + head << tip << QPoint(tip.x() - dir * s, tip.y() - s) + << QPoint(tip.x() - dir * s, tip.y() + s); + p.setBrush(p.pen().color()); + p.drawPolygon(head); + p.setBrush(Qt::NoBrush); + } +}; + +// Greys a stage box (title and contents) without disabling it, so a bypassed +// stage still works the moment it is switched on again. The widgets in +// 'keepActive' keep their normal text colour: they are the stage's own on/off +// control, and a bypassed stage must still look switchable (everything stays +// clickable either way - this is a look, not an enablement). +void greyStage(QGroupBox *g, bool grey, + std::initializer_list keepActive = {}) { + if (!g) + return; + // grey text plus a tinted background, so a bypassed stage is obvious + static const char *kGrey = + "QGroupBox { color: #9a9a9a; background-color: #ececec; }" + "QGroupBox::title { color: #9a9a9a; }" + "QGroupBox QLabel { color: #9a9a9a; }" + "QGroupBox QPushButton { color: #9a9a9a; }" + "QGroupBox QCheckBox { color: #9a9a9a; }"; + // only restyle on a change, so the panel never flickers + const QVariant was = g->property("flowGrey"); + if (!(was.isValid() && was.toBool() == grey)) { + g->setProperty("flowGrey", grey); + g->setStyleSheet(grey ? kGrey : QString()); + } + for (QWidget *w : keepActive) { + if (!w) + continue; + const QVariant wWas = w->property("flowKeep"); + if (wWas.isValid() && wWas.toBool() == grey) + continue; // only restyle on a change, so nothing flickers + w->setProperty("flowKeep", grey); + // a per-widget rule wins over the group's, so the toggle stays readable + w->setStyleSheet(grey ? "color: #000000;" : QString()); + } +} + +QLabel *makeFlowArrow(const QString &glyph, QWidget *parent, + int cls = FlowNormal) { + auto *l = new QLabel(glyph, parent); + l->setAlignment(Qt::AlignCenter); + l->setStyleSheet(QString("color: %1; font-size: 20px; font-weight: bold;") + .arg(flowColor(cls))); + l->setFixedHeight(24); + return l; +} + +// recolours one of those arrows when the save mode changes +void setFlowArrowClass(QLabel *l, int cls) { + if (l) + l->setStyleSheet(QString("color: %1; font-size: 20px; font-weight: " + "bold;") + .arg(flowColor(cls))); +} + +// --------------------------------------------------------------------------- +// FastImageView: the original widget, with its own pixel storage so that the +// analysis thread can never hand it a buffer it is still writing. +// --------------------------------------------------------------------------- +const QRgb *makePalette() { + static QRgb pal[256]; + for (int i = 0; i < 256; i++) { + double v = i / 255.0; + double r, g, b; + if (v < 0.25) { + r = 0; + g = v * 4; + b = 1; + } else if (v < 0.5) { + r = 0; + g = 1; + b = 1 - (v - 0.25) * 4; + } else if (v < 0.75) { + r = (v - 0.5) * 4; + g = 1; + b = 0; + } else { + r = 1; + g = 1 - (v - 0.75) * 4; + b = 0; + } + pal[i] = qRgb(int(r * 255), int(g * 255), int(b * 255)); + } + return pal; +} +const QRgb *kPalette = makePalette(); +const int MARGIN = 5; +const int SCALE_W = 50; +// The image is drawn as a framed plot: axes with tick labels *outside* the +// plot area (left/bottom), a thin frame, grid lines inside. +const int AXIS_L = 52, AXIS_B = 26, AXIS_T = 6, AXIS_R = 6; +// Deepest zoom: the smallest viewport, in image pixels. The strip-xel build +// maps 25/15/18.75 um pixels into panels of ~1000 x 1900, so it is allowed two +// image pixels across (single-pixel inspection); the plain build stops at 8. +#ifdef JFSTRX +const double kMinViewport = 2.0; +#else +const double kMinViewport = 8.0; +#endif + +class FastImageView : public QWidget { + public: + FastImageView(QWidget *parent = nullptr); + + // copies 'data' into widget-owned storage (GUI thread only) + void setImageCopy(const float *data, int nx, int ny); + // replaces the 256-entry colour table (GUI thread only) + void setPaletteLut(const QRgb *lut); + // log intensity mapping: values <= 0 are capped at 1e-2 before the log + void setLogScale(bool on) { + m_logScale = on; + m_dirty = true; + update(); + } + bool logScale() const { return m_logScale; } + // the in-pixel labels: whole numbers for photon counts, one decimal for the + // real-valued images (ADC, keV) + void setIntegerValues(bool on) { + m_integerValues = on; + update(); + } + // histogram of the last image over its own value range (256 bins), for the + // scale panel that replaced the in-view colour bar + const std::vector &histogram() const { return m_hist; } + void histogramRange(double &lo, double &hi) const { + lo = m_histLo; + hi = m_histHi; + } + // called (GUI thread) when a new image or histogram is ready + std::function onImageChanged; + // called when the colour range changes: the strixel panels mirror the ADC + // scale instead of keeping a stale copy of it + std::function onColorRangeChanged; + std::function onPaletteChanged; + // draw pixels with no counts (value <= 0) white instead of the palette's + // lowest colour, the way ROOT's colz draws empty cells on a white canvas. + // Used by the photon map only; the ADC/gain images keep the full palette. + void setZeroWhite(bool on) { + m_zeroWhite = on; + m_dirty = true; + update(); + } + bool hasImage() const { return m_nx > 0 && m_ny > 0; } + void viewport(double &x, double &y, double &w, double &h) const { + x = m_vpX; + y = m_vpY; + w = vpW(); + h = vpH(); + } + int imageWidth() const { return m_nx; } + int imageHeight() const { return m_ny; } + // Thin yellow rectangle drawn over the image, in DATA coordinates (data + // row 0 is displayed at the bottom, so the view flips it here). Used to + // show the gate ROI, which is a snapshot and does not follow the zoom. + void setOverlayRect(double x0, double y0, double x1, double y1, bool on) { + m_ovX0 = x0; + m_ovY0 = y0; + m_ovX1 = x1; + m_ovY1 = y1; + m_ovOn = on; + update(); + } + + // ---- ROI editor ------------------------------------------------------- + // While active the yellow rectangle can be dragged (inside = move, edges + // and corners = resize, outside = draw a new one) instead of being captured + // from the current zoom. onRoiEdited() reports the rectangle in DATA + // coordinates when the mouse button is released. + void beginRoiEdit(bool on) { + m_roiEdit = on; + m_roiGrab = 0; + setCursor(on ? Qt::CrossCursor : Qt::ArrowCursor); + update(); + } + bool roiEditActive() const { return m_roiEdit; } + std::function onRoiEdited; + // widget pixel -> data coordinates (x right, y up, (0,0) bottom-left) + QPointF widgetToData(QPoint wpos) const { + const QPointF i = widgetToImage(wpos); + return QPointF(i.x(), double(m_ny) - i.y()); + } + + // Red receiver-ROI box, in the same DATA coordinates as the gate ROI. + void setReceiverRoiRect(double x0, double y0, double x1, double y1, + bool on) { + m_rxX0 = x0; + m_rxY0 = y0; + m_rxX1 = x1; + m_rxY1 = y1; + m_rxOn = on; + update(); + } + + void setColorRange(double lo, double hi); + // Horizontal size of one image pixel relative to its height (1 = square). + // Used by the strixel panels, whose mapped pixels are 9, 25 and 16 times + // wider than tall (group pitch X = 75*(75/Y), Y = 25/15/18.75 um). + void setPixelAspectX(double a) { + m_pixelAspectX = (a > 0.0) ? a : 1.0; + update(); + } + // the colour scale currently in use (also draggable on the colour bar) + void colorRange(double &lo, double &hi) const { + lo = m_clo; + hi = m_chi; + } + void setViewport(double x1, double y1, double x2, double y2); + void resetView(); + std::function onZoomChanged; + + protected: + void paintEvent(QPaintEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void wheelEvent(QWheelEvent *event) override; + void mouseDoubleClickEvent(QMouseEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + + private: + void renderImage(); + void clampViewport(); + // makes the visible region the same shape as the image area of the widget + void matchViewportToWindow(); + double vpW() const { return (m_vpW < 0) ? m_nx : m_vpW; } + double vpH() const { return (m_vpH < 0) ? m_ny : m_vpH; } + QRect imageRect() const; + QRect scaleRect() const; + double valueAt(const QPoint &pos) const; + QPointF widgetToImage(QPoint wpos) const; + + std::vector m_owned; // widget-owned copy of the last frame + const float *m_data = nullptr; + int m_nx = 0, m_ny = 0; + QImage m_image; + bool m_dirty = true; + QRgb m_palette[256]; + + double m_clo = 0, m_chi = 4000; + double m_defClo = 0, m_defChi = 4000; + bool m_zeroWhite = false; // photon map: no counts -> white + bool m_showBar = false; // the in-view colour bar (replaced by the panel) + bool m_logScale = false; // log intensity mapping (cap <= 0 at 1e-2) + bool m_integerValues = false; // in-pixel labels: ints (counts) or 0.0 + double m_sx = 1.0; // screen pixels per image pixel (last paint) + std::vector m_hist; // histogram of the last image + double m_histLo = 0.0, m_histHi = 1.0; + qint64 m_histMs = 0; + double m_pixelAspectX = 1.0; // >1: pixels wider than tall + double m_vpX = 0, m_vpY = 0; + double m_vpW = -1, m_vpH = -1; + + // gate ROI overlay (data coordinates) + bool m_ovOn = false; + double m_ovX0 = 0, m_ovY0 = 0, m_ovX1 = 0, m_ovY1 = 0; + + // ROI editor state + bool m_roiEdit = false; + int m_roiGrab = 0; // 0 none, 1 move, 2 left, 3 right, 4 bottom, 5 top, + // 6 bl, 7 br, 8 tl, 9 tr, 10 new rectangle + QPointF m_roiPress{0, 0}; // data coordinates at press + double m_roiStart[4] = {0, 0, 0, 0}; // x0,y0,x1,y1 at press + + // receiver ROI overlay (RED), same data coordinates + bool m_rxOn = false; + double m_rxX0 = 0, m_rxY0 = 0, m_rxX1 = 0, m_rxY1 = 0; + + bool m_dragging = false; + QPoint m_lastPos; + double m_dragVpX, m_dragVpY; + mutable QTransform m_widgetToImg; + bool m_dragScale = false; + + public: + // the Z-scale strip inside the image is being dragged: the panel must not + // re-fill its histogram until the button comes back up + bool isScaleDragging() const { return m_dragScale; } + std::function onScaleReleased; + + private: + bool m_dragMax = false; // true = dragging max, false = dragging min + int m_dragScaleY = 0; + double m_dragClo, m_dragChi; +}; + +FastImageView::FastImageView(QWidget *parent) : QWidget(parent) { + setMouseTracking(true); + setFocusPolicy(Qt::StrongFocus); + setMinimumSize(200, 200); + m_chi = 4000; + m_defChi = 4000; + m_clo = 0; + m_defClo = 0; + for (int i = 0; i < 256; i++) + m_palette[i] = kPalette[i]; +} + +void FastImageView::setImageCopy(const float *data, int nx, int ny) { + if (!data || nx <= 0 || ny <= 0) + return; + const size_t n = static_cast(nx) * static_cast(ny); + if (m_owned.size() != n) + m_owned.resize(n); + memcpy(m_owned.data(), data, n * sizeof(float)); + m_data = m_owned.data(); + m_nx = nx; + m_ny = ny; + m_dirty = true; + if (m_vpW < 0) + m_vpW = nx; + if (m_vpH < 0) + m_vpH = ny; + update(); +} + +void FastImageView::setPaletteLut(const QRgb *lut) { + if (onPaletteChanged && lut) + onPaletteChanged(lut); + if (!lut) + return; + for (int i = 0; i < 256; i++) + m_palette[i] = lut[i]; + m_dirty = true; + update(); +} + +void FastImageView::setColorRange(double lo, double hi) { + m_clo = lo; + m_chi = hi; + m_defClo = lo; + m_defChi = hi; + m_histMs = 0; // the bins follow the window: recompute on the next render + m_dirty = true; + if (onColorRangeChanged) + onColorRangeChanged(lo, hi); + update(); +} + +void FastImageView::setViewport(double x1, double y1, double x2, double y2) { + if (x2 - x1 < kMinViewport || y2 - y1 < kMinViewport) + return; + m_vpX = x1; + m_vpY = y1; + m_vpW = x2 - x1; + m_vpH = y2 - y1; + update(); +} + +void FastImageView::resetView() { + if (!hasImage()) + return; // nothing to reset yet (and (0,0,0,0) would collapse the map) + m_vpX = 0; + m_vpY = 0; + m_vpW = m_nx; + m_vpH = m_ny; + if (onZoomChanged) + onZoomChanged(0, 0, m_nx, m_ny); + update(); +} + +QRect FastImageView::imageRect() const { + int x0 = AXIS_L, y0 = AXIS_T; + int x1 = width() - AXIS_R - (m_showBar ? SCALE_W : 0); + int y1 = height() - AXIS_B; + if (x1 - x0 < 10) + x1 = x0 + 10; + if (y1 - y0 < 10) + y1 = y0 + 10; + return QRect(x0, y0, x1 - x0, y1 - y0); +} + +QRect FastImageView::scaleRect() const { + if (!m_showBar) + return QRect(); // the scale panel took its place + QRect ir = imageRect(); + return QRect(ir.right() + 4, ir.top(), SCALE_W - 4, ir.height()); +} + +// keeps the viewport inside the image; used after pan, zoom and resize +void FastImageView::clampViewport() { + if (!hasImage()) + return; + if (m_vpW > m_nx) + m_vpW = m_nx; + if (m_vpH > m_ny) + m_vpH = m_ny; + if (m_vpX < 0) + m_vpX = 0; + if (m_vpX + vpW() > m_nx) + m_vpX = m_nx - vpW(); + if (m_vpY < 0) + m_vpY = 0; + if (m_vpY + vpH() > m_ny) + m_vpY = m_ny - vpH(); +} + +// The ADC window is the tool that selects the gate ROI, so the region it +// shows takes the shape of the window: make the window tall and "ROI only" +// captures a narrow, tall region. The shorter side is grown so that nothing +// the operator was looking at disappears; the whole-frame view is left alone +// (it is letterboxed on purpose) so that it still captures the full sensor. +void FastImageView::matchViewportToWindow() { + if (!hasImage()) + return; + if (m_vpW >= m_nx - 0.5 && m_vpH >= m_ny - 0.5) + return; // whole frame: keep it, whatever the window shape + QRect ir = imageRect(); + if (ir.width() <= 0 || ir.height() <= 0) + return; + // shape the viewport so the physical pixels (m_pixelAspectX) fill the + // window with the same shape, not the square-pixel shape + const double ar = double(ir.width()) / double(ir.height()) / m_pixelAspectX; + double w = vpW(), h = vpH(); + if (w / h > ar) + h = w / ar; // too wide -> grow vertically + else + w = h * ar; // too tall -> grow horizontally + if (w > m_nx) { // grown past the sensor: shrink both, keep the shape + h *= m_nx / w; + w = m_nx; + } + if (h > m_ny) { + w *= m_ny / h; + h = m_ny; + } + const double cx = m_vpX + vpW() / 2, cy = m_vpY + vpH() / 2; + m_vpW = w; + m_vpH = h; + m_vpX = cx - w / 2; + m_vpY = cy - h / 2; + clampViewport(); + if (onZoomChanged) + onZoomChanged(m_vpX, m_vpY, m_vpX + vpW(), m_vpY + vpH()); +} + +void FastImageView::resizeEvent(QResizeEvent *) { + matchViewportToWindow(); + update(); +} + +void FastImageView::renderImage() { if (!m_data || m_nx <= 0 || m_ny <= 0) + return; + if (m_image.size() != QSize(m_nx, m_ny)) + m_image = QImage(m_nx, m_ny, QImage::Format_ARGB32); + // log: values <= 0 are capped at 1e-2 (log10 = -2), so an empty pixel is + // still distinguishable instead of being -inf + const double loD = m_logScale ? std::log10(std::max(m_clo, 1e-2)) : m_clo; + const double hiD = m_logScale ? std::log10(std::max(m_chi, 1e-2)) : m_chi; + const double range = (hiD > loD) ? (hiD - loD) : 1.0; + static const QRgb kWhite = qRgb(255, 255, 255); + double dmin = 0.0, dmax = 1.0; + bool first = true; + for (int p = 0; p < m_nx * m_ny; p++) { + const double v = m_data[p]; + if (std::isfinite(v)) { + if (first) { + dmin = dmax = v; + first = false; + } else { + if (v < dmin) + dmin = v; + if (v > dmax) + dmax = v; + } + } + if (m_zeroWhite && !(v > 0.0)) { + // empty cell: white background, as the ROOT colz plot showed it + m_image.setPixel(p % m_nx, m_ny - 1 - p / m_nx, kWhite); + continue; + } + const double vv = m_logScale ? std::log10(std::max(v, 1e-2)) : v; + int idx = int((vv - loD) / range * 255); + if (idx < 0) + idx = 0; + if (idx > 255) + idx = 255; + m_image.setPixel(p % m_nx, m_ny - 1 - p / m_nx, m_palette[idx]); + } + m_dirty = false; + + // Histogram for the scale panel: walking the whole frame is cheap enough at + // the image cadence, but the panel only needs the shape, so a few times a + // second is plenty. + const qint64 now = QDateTime::currentMSecsSinceEpoch(); + if (m_hist.empty() || now - m_histMs > 300) { + m_histMs = now; + if (first || dmax <= dmin) + dmax = dmin + 1.0; + // Bin over the range the strip actually shows (the colour range plus + // half a range either side), not over the image's whole value range: a + // photon map accumulates to thousands of counts while its scale shows + // 0..12, so full-range binning put the entire visible window inside one + // bin - the "too big bins". Pixels outside the window are skipped + // instead of being piled into the edge bins. + const double caspan = (m_chi > m_clo) ? (m_chi - m_clo) : 0.0; + double hlo = dmin, hhi = dmax; + if (caspan > 0.0 && std::isfinite(m_clo) && std::isfinite(m_chi)) { + hlo = m_clo - 0.5 * caspan; + hhi = m_chi + 0.5 * caspan; + } + if (m_logScale) { + hlo = std::log10(std::max(hlo, 1e-2)); + hhi = std::log10(std::max(hhi, 1e-2)); + if (hhi <= hlo) + hhi = hlo + 1.0; + } + if (!(hhi > hlo)) + hhi = hlo + 1.0; + m_histLo = hlo; + m_histHi = hhi; + m_hist.assign(256, 0); + const double sc = 255.0 / (hhi - hlo); + for (int p = 0; p < m_nx * m_ny; p++) { + const double raw = m_data[p]; + if (!std::isfinite(raw)) + continue; + const double v = m_logScale ? std::log10(std::max(raw, 1e-2)) : raw; + if (v < hlo || v > hhi) + continue; // outside the visible window + int b = int((v - hlo) * sc); + if (b < 0) + b = 0; + if (b > 255) + b = 255; + m_hist[b]++; + } + if (onImageChanged) + onImageChanged(); + } +} + +void FastImageView::paintEvent(QPaintEvent *) { + if (m_dirty) + renderImage(); + QPainter p(this); + p.fillRect(rect(), Qt::black); + if (!m_data || m_nx <= 0 || m_ny <= 0) + return; + + QRect ir = imageRect(); + // Pixel aspect: a value > 1 makes each image pixel that many times wider + // than tall (the strixel groups have rectangular pixels), so the fit is + // computed in physical units and the image is scaled non-uniformly. + double sx = 0.0, sy = 0.0; + if (m_pixelAspectX != 1.0) { + const double fitY = double(ir.height()) / vpH(); + const double fitX = double(ir.width()) / (vpW() * m_pixelAspectX); + sy = (fitY < fitX) ? fitY : fitX; + sx = sy * m_pixelAspectX; + } else { + const double fitX = double(ir.width()) / vpW(); + const double fitY = double(ir.height()) / vpH(); + sx = sy = (fitX < fitY) ? fitX : fitY; + } + + p.save(); + // Zoomed in, the scaled image is larger than the plot area: without this + // clip it spills over the frame and the tick labels. + p.setClipRect(ir); + p.translate(ir.left(), ir.top()); + p.translate((ir.width() - vpW() * sx) * 0.5, + (ir.height() - vpH() * sy) * 0.5); + p.translate(-m_vpX * sx, -m_vpY * sy); + p.scale(sx, sy); + const QTransform imgToWidget = p.combinedTransform(); + m_widgetToImg = imgToWidget.inverted(); + m_sx = sx; // screen pixels per image pixel: drives the pixel grid/labels + p.drawImage(QPoint(0, 0), m_image); + p.restore(); + + // the overlays live in the same plot area: clip them as well + p.save(); + p.setClipRect(ir); + // gate ROI overlay: convert data rows (row 0 = bottom) to display space + if (m_ovOn && m_ovX1 > m_ovX0 && m_ovY1 > m_ovY0) { + const double dx = m_ovX0; + const double dy = double(m_ny) - m_ovY1; + const double dw = m_ovX1 - m_ovX0; + const double dh = m_ovY1 - m_ovY0; + const QRectF r = imgToWidget.mapRect(QRectF(dx, dy, dw, dh)); + QPen pen(Qt::yellow); + pen.setWidth(m_roiEdit ? 2 : 1); + pen.setCosmetic(true); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + p.drawRect(r); + if (m_roiEdit) { // corner and edge handles: what can be grabbed + const double hw = 3.0; // half handle size, in widget pixels + const double xs[3] = {r.left(), r.center().x(), r.right()}; + const double ys[3] = {r.top(), r.center().y(), r.bottom()}; + p.setBrush(QColor(255, 255, 80)); + for (int iy = 0; iy < 3; iy++) { + for (int ix = 0; ix < 3; ix++) { + if (ix == 1 && iy == 1) + continue; // centre handle would only be confusing + p.drawRect(QRectF(xs[ix] - hw, ys[iy] - hw, 2 * hw, + 2 * hw)); + } + } + } + } + + // receiver ROI overlay (RED), from the "rx_roi" field of the stream + // header. The ADC image is the raw layout, so this is a plain rectangle. + if (m_rxOn && m_rxX1 > m_rxX0 && m_rxY1 > m_rxY0) { + QPen pen(Qt::red); + pen.setWidth(1); + pen.setCosmetic(true); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + const double dx = m_rxX0; + const double dy = double(m_ny) - m_rxY1; + const double dw = m_rxX1 - m_rxX0; + const double dh = m_rxY1 - m_rxY0; + p.drawRect(imgToWidget.mapRect(QRectF(dx, dy, dw, dh))); + } + p.restore(); // back to the full widget for the axes + + // ── axes: a frame around the plot, ticks and labels outside, grid inside ── + { + QFont f = p.font(); + f.setPixelSize(10); + p.setFont(f); + // 0..8 grid lines, on 1/2/5 steps + auto step = [](double span) { + static const double cand[] = {1, 2, 5, 10, 20, 50, + 100, 200, 500, 1000, 2000, 5000}; + for (double c : cand) + if (span / c <= 8.0) + return c; + return 10000.0; + }; + const double x0 = m_vpX, x1 = m_vpX + vpW(); + const double y0 = m_vpY, y1 = m_vpY + vpH(); // display rows (top down) + const double sxStep = step(x1 - x0), syStep = step(y1 - y0); + const QPen grid(QColor(95, 95, 95)); + const QPen tick(QColor(215, 215, 215)); + for (double gx = std::ceil(x0 / sxStep) * sxStep; gx <= x1; + gx += sxStep) { + const double wx = imgToWidget.map(QPointF(gx, 0)).x(); + QPen g2 = grid; + g2.setStyle(Qt::DotLine); + p.setPen(g2); + p.drawLine(QPointF(wx, ir.top()), QPointF(wx, ir.bottom())); + p.setPen(tick); + p.drawLine(QPointF(wx, ir.bottom() + 1), QPointF(wx, ir.bottom() + 4)); + p.drawText(QRectF(wx - 26, ir.bottom() + 5, 52, 14), + Qt::AlignHCenter | Qt::AlignTop, + QString::number(gx, 'f', 0)); + } + for (double gy = std::ceil(y0 / syStep) * syStep; gy <= y1; + gy += syStep) { + const double wy = imgToWidget.map(QPointF(0, gy)).y(); + QPen g2 = grid; + g2.setStyle(Qt::DotLine); + p.setPen(g2); + p.drawLine(QPointF(ir.left(), wy), QPointF(ir.right(), wy)); + p.setPen(tick); + p.drawLine(QPointF(ir.left() - 4, wy), QPointF(ir.left() - 1, wy)); + // the label is the DATA row (row 0 at the bottom), as everywhere else + p.drawText(QRectF(ir.left() - 50, wy - 7, 44, 14), + Qt::AlignRight | Qt::AlignVCenter, + QString::number(double(m_ny) - 1.0 - gy, 'f', 0)); + } + p.setPen(QPen(QColor(225, 225, 225))); + p.setBrush(Qt::NoBrush); + p.drawRect(ir.adjusted(0, 0, -1, -1)); + } + + // ── pixel grid and in-pixel values, once a pixel is big enough ────────── + if (m_sx >= 16.0) { + const double x0 = m_vpX, x1 = m_vpX + vpW(); + const double y0 = m_vpY, y1 = m_vpY + vpH(); + // thin outline around every pixel (only when a sane number is visible) + if (vpW() <= 256 && vpH() <= 256) { + QPen px(QColor(55, 55, 55)); + px.setCosmetic(true); + p.setPen(px); + for (double gx = std::floor(x0); gx <= x1 + 1; gx += 1.0) { + const double wx = imgToWidget.map(QPointF(gx, 0)).x(); + p.drawLine(QPointF(wx, ir.top()), QPointF(wx, ir.bottom())); + } + for (double gy = std::floor(y0); gy <= y1 + 1; gy += 1.0) { + const double wy = imgToWidget.map(QPointF(0, gy)).y(); + p.drawLine(QPointF(ir.left(), wy), QPointF(ir.right(), wy)); + } + } + if (m_sx >= 50.0 && vpW() <= 64 && vpH() <= 64) { + QFont f = p.font(); + // The label has to fit *inside* the pixel: six characters for the + // real-valued images ("XXXXX.y"), five for the photon counts, at + // roughly 0.62 em per digit. The 0.30 em cap still gives the + // "small, tidy number" look at deep zoom. + const int maxChars = m_integerValues ? 5 : 6; + const double bySize = m_sx * 0.30; + const double byWidth = m_sx * 0.92 / (0.62 * maxChars); + f.setPixelSize(std::max( + 7, std::min(20, int(std::min(bySize, byWidth))))); + p.setFont(f); + for (int gy = int(std::floor(y0)); gy <= int(y1); gy++) { + for (int gx = int(std::floor(x0)); gx <= int(x1); gx++) { + if (gx < 0 || gy < 0 || gx >= m_nx || gy >= m_ny) + continue; + const double v = m_data[gx + (m_ny - 1 - gy) * m_nx]; + if (!std::isfinite(v)) + continue; + // text colour from the pixel that was actually painted + const QRgb c = m_image.pixel(gx, gy); + const int lum = (qRed(c) * 299 + qGreen(c) * 587 + + qBlue(c) * 114) / + 1000; + p.setPen(lum > 140 ? QColor(15, 15, 15) + : QColor(245, 245, 245)); + const QString txt = + m_integerValues + ? QString::number(qint64(std::llround(v))) + : QString::number(v, 'f', 1); + const QPointF ctr = + imgToWidget.map(QPointF(gx + 0.5, gy + 0.5)); + p.drawText(QRectF(ctr.x() - m_sx * 0.5, + ctr.y() - m_sx * 0.5, m_sx, m_sx), + Qt::AlignCenter, txt); + } + } + } + } + + // in-view colour bar: kept for builds that still ask for it (the scale + // panel replaced it in the windows) + if (!m_showBar) + return; + QRect sr = scaleRect(); + for (int yy = sr.top(); yy <= sr.bottom(); yy++) { + double frac = 1.0 - double(yy - sr.top()) / sr.height(); + int idx = int(frac * 255); + if (idx < 0) + idx = 0; + if (idx > 255) + idx = 255; + p.setPen(m_palette[idx]); + p.drawLine(sr.left(), yy, sr.right(), yy); + } + p.setPen(Qt::white); + p.drawRect(sr.adjusted(-1, -1, 1, 1)); + QFont f = p.font(); + f.setPixelSize(10); + p.setFont(f); + p.drawText(sr.left(), sr.top() + 1, sr.width(), 12, Qt::AlignCenter, + QString::number(m_chi, 'f', 0)); + p.drawText(sr.left(), sr.bottom() - 11, sr.width(), 12, Qt::AlignCenter, + QString::number(m_clo, 'f', 0)); +} + +void FastImageView::mousePressEvent(QMouseEvent *e) { + if (e->button() != Qt::LeftButton) + return; + QPoint ip = e->pos(); + if (m_roiEdit && hasImage()) { + // which part of the yellow rectangle was grabbed? + const QPointF d = widgetToData(ip); + const QPointF d2 = widgetToData(ip + QPoint(8, 0)); + const double tol = std::max(1e-9, std::abs(d2.x() - d.x())); + const bool nearL = std::abs(d.x() - m_ovX0) <= tol; + const bool nearR = std::abs(d.x() - m_ovX1) <= tol; + const bool nearB = std::abs(d.y() - m_ovY0) <= tol; + const bool nearT = std::abs(d.y() - m_ovY1) <= tol; + const bool inX = d.x() >= m_ovX0 - tol && d.x() <= m_ovX1 + tol; + const bool inY = d.y() >= m_ovY0 - tol && d.y() <= m_ovY1 + tol; + m_roiGrab = 0; + if (nearL && nearB) + m_roiGrab = 6; + else if (nearR && nearB) + m_roiGrab = 7; + else if (nearL && nearT) + m_roiGrab = 8; + else if (nearR && nearT) + m_roiGrab = 9; + else if (nearL && inY) + m_roiGrab = 2; + else if (nearR && inY) + m_roiGrab = 3; + else if (nearB && inX) + m_roiGrab = 4; + else if (nearT && inX) + m_roiGrab = 5; + else if (inX && inY) + m_roiGrab = 1; + else + m_roiGrab = 10; // start a new rectangle at the cursor + m_roiPress = d; + m_roiStart[0] = m_ovX0; + m_roiStart[1] = m_ovY0; + m_roiStart[2] = m_ovX1; + m_roiStart[3] = m_ovY1; + if (m_roiGrab == 10) { + m_ovX0 = m_ovX1 = std::max(0.0, std::min(double(m_nx), d.x())); + m_ovY0 = m_ovY1 = std::max(0.0, std::min(double(m_ny), d.y())); + m_ovOn = true; + } + update(); + return; + } + QRect sr = scaleRect(); + if (sr.contains(ip)) { + m_dragScale = true; + m_dragScaleY = ip.y(); + m_dragClo = m_clo; + m_dragChi = m_chi; + m_dragMax = (ip.y() < sr.top() + sr.height() / 2); + setCursor(Qt::SizeVerCursor); + return; + } + m_dragging = true; + m_lastPos = ip; + m_dragVpX = m_vpX; + m_dragVpY = m_vpY; + setCursor(Qt::ClosedHandCursor); +} + +void FastImageView::mouseMoveEvent(QMouseEvent *e) { + if (m_roiEdit && hasImage()) { + const QPointF d = widgetToData(e->pos()); + const double x = std::max(0.0, std::min(double(m_nx), d.x())); + const double y = std::max(0.0, std::min(double(m_ny), d.y())); + constexpr double kMin = 2.0; // a ROI smaller than this is unusable + if (m_roiGrab == 10) { // dragging out a new rectangle + m_ovX0 = std::min(m_roiPress.x(), x); + m_ovX1 = std::max(m_roiPress.x(), x); + m_ovY0 = std::min(m_roiPress.y(), y); + m_ovY1 = std::max(m_roiPress.y(), y); + m_ovOn = true; + } else if (m_roiGrab == 1) { // move the whole rectangle + const double dx = x - m_roiPress.x(); + const double dy = y - m_roiPress.y(); + double x0 = m_roiStart[0] + dx, x1 = m_roiStart[2] + dx; + double y0 = m_roiStart[1] + dy, y1 = m_roiStart[3] + dy; + if (x0 < 0) { x1 -= x0; x0 = 0; } + if (x1 > m_nx) { x0 -= (x1 - m_nx); x1 = m_nx; } + if (y0 < 0) { y1 -= y0; y0 = 0; } + if (y1 > m_ny) { y0 -= (y1 - m_ny); y1 = m_ny; } + m_ovX0 = x0; + m_ovX1 = x1; + m_ovY0 = y0; + m_ovY1 = y1; + } else if (m_roiGrab != 0) { // one edge or one corner + double x0 = m_roiStart[0], y0 = m_roiStart[1]; + double x1 = m_roiStart[2], y1 = m_roiStart[3]; + const bool left = (m_roiGrab == 2 || m_roiGrab == 6 || m_roiGrab == 8); + const bool right = (m_roiGrab == 3 || m_roiGrab == 7 || m_roiGrab == 9); + const bool bottom = (m_roiGrab == 4 || m_roiGrab == 6 || m_roiGrab == 7); + const bool top = (m_roiGrab == 5 || m_roiGrab == 8 || m_roiGrab == 9); + if (left && x <= x1 - kMin) + x0 = x; + if (right && x >= x0 + kMin) + x1 = x; + if (bottom && y <= y1 - kMin) + y0 = y; + if (top && y >= y0 + kMin) + y1 = y; + m_ovX0 = x0; + m_ovY0 = y0; + m_ovX1 = x1; + m_ovY1 = y1; + } + update(); + if (m_roiGrab == 0) { // hovering: show what grabbing here would do + const QPointF dd = widgetToData(e->pos()); + const QPointF dd2 = widgetToData(e->pos() + QPoint(8, 0)); + const double tol = std::max(1e-9, std::abs(dd2.x() - dd.x())); + const bool nearL = std::abs(dd.x() - m_ovX0) <= tol; + const bool nearR = std::abs(dd.x() - m_ovX1) <= tol; + const bool nearB = std::abs(dd.y() - m_ovY0) <= tol; + const bool nearT = std::abs(dd.y() - m_ovY1) <= tol; + const bool inside = dd.x() >= m_ovX0 - tol && + dd.x() <= m_ovX1 + tol && + dd.y() >= m_ovY0 - tol && + dd.y() <= m_ovY1 + tol; + if ((nearL && nearT) || (nearR && nearB)) + setCursor(Qt::SizeFDiagCursor); + else if ((nearL && nearB) || (nearR && nearT)) + setCursor(Qt::SizeBDiagCursor); + else if (nearL || nearR) + setCursor(Qt::SizeHorCursor); + else if (nearT || nearB) + setCursor(Qt::SizeVerCursor); + else + setCursor(inside ? Qt::OpenHandCursor : Qt::CrossCursor); + } + return; + } + if (m_dragScale) { + QRect sr = scaleRect(); + double delta = double(e->y() - m_dragScaleY) / double(sr.height()) * + (m_dragChi - m_dragClo); + if (m_dragMax) { + m_chi = m_dragChi - delta; + if (m_chi < m_clo + 1) + m_chi = m_clo + 1; + } else { + m_clo = m_dragClo - delta; + if (m_clo > m_chi - 1) + m_clo = m_chi - 1; + } + m_dirty = true; + update(); + return; + } + if (!m_dragging) { + QPoint hp = e->pos(); + QRect ir2 = imageRect(); + QRect sr = scaleRect(); + if (sr.contains(hp)) { + setCursor(Qt::SizeVerCursor); + double v = valueAt(hp); + setToolTip(QString("Scale: %1").arg(v, 0, 'f', 0)); + } else if (ir2.contains(hp)) { + setCursor(Qt::OpenHandCursor); + // widgetToImage() is in QImage space (row 0 = TOP); the reported + // pixel and the value are converted to DATA rows so that the + // numbering matches the ROOT plots and the ROI list: (0,0) is the + // bottom-left pixel everywhere. + QPointF ipt = widgetToImage(hp); + const int ix = int(ipt.x()); + const int dy = int(ipt.y()); + const int iy = m_ny - 1 - dy; + if (ix >= 0 && ix < m_nx && iy >= 0 && iy < m_ny) + setToolTip(QString("(x=%1, y=%2) = %3") + .arg(ix) + .arg(iy) + .arg(m_data[ix + iy * m_nx], 0, 'f', 1)); + } else { + setCursor(Qt::ArrowCursor); + setToolTip(QString()); + } + return; + } + QPointF now = widgetToImage(e->pos()); + QPointF then = widgetToImage(m_lastPos); + m_vpX = m_dragVpX - (now.x() - then.x()); + m_vpY = m_dragVpY - (now.y() - then.y()); + clampViewport(); + update(); + // follow live: the gain view and the photon map pan with the ADC view + if (onZoomChanged) + onZoomChanged(m_vpX, m_vpY, m_vpX + vpW(), m_vpY + vpH()); +} + +void FastImageView::mouseReleaseEvent(QMouseEvent *e) { + if (e->button() == Qt::LeftButton && m_roiGrab != 0) { + m_roiGrab = 0; + if (onRoiEdited && m_ovX1 - m_ovX0 >= 2.0 && m_ovY1 - m_ovY0 >= 2.0) + onRoiEdited(int(std::lround(m_ovX0)), int(std::lround(m_ovY0)), + int(std::lround(m_ovX1)), int(std::lround(m_ovY1))); + return; + } + if (e->button() == Qt::LeftButton) { + if (m_dragging && onZoomChanged) + onZoomChanged(m_vpX, m_vpY, m_vpX + vpW(), m_vpY + vpH()); + m_dragging = false; + const bool wasScale = m_dragScale; + m_dragScale = false; + setCursor(Qt::ArrowCursor); + if (wasScale && onScaleReleased) + onScaleReleased(); // one histogram/scale refresh, after the drag + } +} + +void FastImageView::wheelEvent(QWheelEvent *e) { + if (!m_data || m_nx <= 0 || m_ny <= 0) + return; + QPointF imgPt = widgetToImage(e->pos()); + double mx = imgPt.x(), my = imgPt.y(); + + double factor = (e->angleDelta().y() > 0) ? 1.0 / 1.18 : 1.18; + double nw = vpW() * factor, nh = vpH() * factor; + if (nw < kMinViewport) + nw = kMinViewport; + if (nh < kMinViewport) + nh = kMinViewport; + if (nw > m_nx) + nw = m_nx; + if (nh > m_ny) + nh = m_ny; + + double fracX = (mx - m_vpX) / vpW(); + double fracY = (my - m_vpY) / vpH(); + m_vpX = mx - fracX * nw; + m_vpY = my - fracY * nh; + m_vpW = nw; + m_vpH = nh; + if (factor > 1.0) { + // Zooming OUT: do not reshape to the window here. matchViewportToWindow + // grows/shrinks to the window aspect, so the image dimension that hits + // its limit first froze the other one and the whole frame could never + // be shown. Let both grow to the image bounds; the window-shape rule + // still applies while zooming IN and on resize. + clampViewport(); + } else { + // keep the visible region the same shape as the window also while + // zooming, so that the captured ROI has the shape of the window + matchViewportToWindow(); + } + + if (onZoomChanged) + onZoomChanged(m_vpX, m_vpY, m_vpX + vpW(), m_vpY + vpH()); + setToolTip(QString("Zoom: %1%").arg(int(100.0 * m_nx / vpW()))); + update(); +} + +void FastImageView::mouseDoubleClickEvent(QMouseEvent *e) { + QRect sr = scaleRect(); + if (sr.contains(e->pos())) { + m_clo = m_defClo; + m_chi = m_defChi; + m_dirty = true; + update(); + } else { + resetView(); + } +} + +double FastImageView::valueAt(const QPoint &pos) const { + QRect sr = scaleRect(); + if (!sr.contains(pos)) + return m_clo - 1; + double frac = 1.0 - double(pos.y() - sr.top()) / sr.height(); + return m_clo + frac * (m_chi - m_clo); +} + +QPointF FastImageView::widgetToImage(QPoint wpos) const { + return m_widgetToImg.map(QPointF(wpos)); +} + +QLabel *makeValueLabel(const QString &text, QWidget *parent) { + QLabel *label = new QLabel(text, parent); + label->setTextInteractionFlags(Qt::TextSelectableByMouse); + return label; +} + +} // namespace + +// --------------------------------------------------------------------------- +// MainWindow +// --------------------------------------------------------------------------- +// The histogram the scale panel shows: the image's own distribution with the +// currently mapped range drawn over it, so the effect of the two ends is +// visible while they are set. +// The histogram strip of the scale panel. It is deliberately thin (a fixed +// ~38 px) and the two range ends are drawn at FIXED positions - a quarter and +// three quarters of the width - so the *value axis* moves under them: the data +// window shown is the selected range widened by half a range on either side. +// Dragging a marker changes that end of the range; the histogram rescales so +// the marker returns to its fixed spot. The horizontal axis follows the image +// mapping (lin or log), the bar heights have their own lin/log switch. +class ScaleHisto : public QWidget { + public: + explicit ScaleHisto(QWidget *parent = nullptr) : QWidget(parent) { + setFixedHeight(38); + setMouseTracking(true); + setToolTip("Distribution of the last image (its own value range); the\n" + "band between the two markers is the range mapped to the\n" + "palette. Markers sit at 1/4 and 3/4 and the axis rescales\n" + "under them - drag either one to change that end."); + } + // dlo/dhi: value range of the bins; clo/chi: the mapped range + void setData(const std::vector *h, double dlo, double dhi, double clo, + double chi, bool logX, bool logY) { + // Frozen while a marker is being dragged: re-filling the bars from the + // live image mid-drag fed the drag back into the histogram (the bars + // moved, which moved the marker, which moved the bars...). The strip + // is refreshed once, when the button comes back up. + if (dragging_) + return; + h_ = h; + dlo_ = dlo; + dhi_ = dhi; + clo_ = clo; + chi_ = chi; + logX_ = logX; + logY_ = logY; + update(); + } + // dragging a marker reports the new range (GUI thread) + std::function onRangeChanged; + + protected: + void paintEvent(QPaintEvent *) override { + QPainter p(this); + p.fillRect(rect(), QColor(28, 28, 28)); + const int W = width(), H = height(); + // While a marker is dragged the axis stays exactly as it was when the + // drag started: the bars (and the two numbers) must not move with the + // cursor, only the dragged end is previewed as a line. Releasing + // rebuilds the axis from the new range. + const double dclo = dragging_ ? preClo_ : clo_; + const double dchi = dragging_ ? preChi_ : chi_; + // the shown window is the selected range +- half a range + const double span = (dchi > dclo) ? (dchi - dclo) : 1.0; + double wlo = dclo - 0.5 * span, whi = dchi + 0.5 * span; + if (logX_) { // work in log space, capping <= 0 at 1e-2 + wlo = std::log10(std::max(wlo, 1e-2)); + whi = std::log10(std::max(whi, 1e-2)); + if (whi <= wlo) + whi = wlo + 1.0; + } + auto mapv = [&](double v) { + return logX_ ? std::log10(std::max(v, 1e-2)) : v; + }; + auto xof = [&](double v) { + return (mapv(v) - wlo) / (whi - wlo) * W; + }; + // The image keeps arriving while the marker is dragged and its + // histogram is recomputed on every repaint, so the bars themselves were + // still moving under the frozen axis. While dragging, paint from the + // snapshot taken when the button went down. + const std::vector *hv = + (dragging_ && !snap_.empty()) ? &snap_ : h_; + if (hv && !hv->empty() && dhi_ > dlo_) { + int mx = 1; + for (int v : *hv) + if (v > mx) + mx = v; + const double lmx = std::log10(double(mx) + 1.0); + const double binW = (dhi_ - dlo_) / double(hv->size()); + QBrush bar(QColor(120, 190, 255)); + p.setPen(Qt::NoPen); + p.setBrush(bar); + for (size_t i = 0; i < hv->size(); i++) { + if ((*hv)[i] <= 0) + continue; + // solid: the bar is as wide as the bin spacing on screen + const double x0 = xof(dlo_ + double(i) * binW); + const double x1 = xof(dlo_ + double(i + 1) * binW); + const double bh = + logY_ ? (std::log10(double((*hv)[i]) + 1.0) / lmx) + : (double((*hv)[i]) / double(mx)); + const double w = std::max(1.0, x1 - x0); + if (x0 > W || x0 + w < 0) + continue; + p.drawRect(QRectF(x0, H - 3 - bh * (H - 6), w, + std::max(1.0, bh * (H - 6)))); + } + } + // the two markers at their fixed places (they are what the axis follows) + const double xa = 0.25 * W, xb = 0.75 * W; + if (dragging_) { // the dragged end, previewed only + QPen pv(QColor(255, 205, 110, 180)); + pv.setWidth(1); + pv.setStyle(Qt::DashLine); + p.setPen(pv); + const double x = std::max(0.0, std::min(double(W - 1), double(previewX_))); + p.drawLine(QPointF(x, 0), QPointF(x, H)); + } + p.fillRect(QRectF(xa, 0, xb - xa, H), QColor(255, 255, 255, 26)); + QPen mk(QColor(255, 205, 110)); + mk.setWidth(2); + p.setPen(mk); + p.setBrush(Qt::NoBrush); + p.drawLine(QPointF(xa, 0), QPointF(xa, H)); + p.drawLine(QPointF(xb, 0), QPointF(xb, H)); + QFont f = p.font(); + f.setPixelSize(9); + p.setFont(f); + p.setPen(QColor(255, 225, 160)); + p.drawText(QRectF(xa + 3, 1, W - xa - 6, 11), Qt::AlignLeft | Qt::AlignTop, + QString::number(dclo, 'g', 4)); + p.drawText(QRectF(3, 1, xb - 3, 11), Qt::AlignRight | Qt::AlignTop, + QString::number(dchi, 'g', 4)); + } + void mousePressEvent(QMouseEvent *e) override { + const double xa = 0.25 * width(), xb = 0.75 * width(); + const double d = std::abs(e->pos().x() - xa); + const double d2 = std::abs(e->pos().x() - xb); + dragMin_ = d <= d2; + if (std::min(d, d2) > 10) { + dragMin_ = -1; // too far from either marker: ignore + return; + } + // Dragging only moves this strip: re-rendering a megapixel image on + // every mouse move is what made it crawl, so the new range is applied + // when the button comes back up (see mouseReleaseEvent). + dragging_ = true; + if (h_) + snap_ = *h_; // the bars: frozen as well (the image keeps updating) + preClo_ = clo_; // the axis basis: frozen until the button is released + preChi_ = chi_; + previewX_ = e->pos().x(); + mouseMoveEvent(e); + } + void mouseMoveEvent(QMouseEvent *e) override { + if (dragMin_ < 0) + return; + const int W = width(); + // The cursor -> value mapping uses the axis FROZEN at press time. Using + // the live clo_/chi_ here was the positive feedback: the move changed the + // range, the range changed the window, and the same cursor position then + // mapped to a different value, so the marker chased itself. + const double span = (preChi_ > preClo_) ? (preChi_ - preClo_) : 1.0; + double wlo = preClo_ - 0.5 * span, whi = preChi_ + 0.5 * span; + if (logX_) { + wlo = std::log10(std::max(wlo, 1e-2)); + whi = std::log10(std::max(whi, 1e-2)); + if (whi <= wlo) + whi = wlo + 1.0; + } + // the value under the cursor in the window in force right now + const double t = std::max(0.0, std::min(1.0, double(e->pos().x()) / W)); + const double v = + logX_ ? std::pow(10.0, wlo + t * (whi - wlo)) : wlo + t * (whi - wlo); + if (dragMin_) + clo_ = std::min(v, chi_ - 1e-9); + else + chi_ = std::max(v, clo_ + 1e-9); + previewX_ = e->pos().x(); + update(); // only the preview line moves; bars and axis stay frozen + } + void mouseReleaseEvent(QMouseEvent *) override { + if (dragMin_ >= 0 && dragging_) { + dragging_ = false; // unfreeze first, so the refresh is applied + snap_.clear(); // back to the live histogram + if (onRangeChanged) + onRangeChanged(clo_, chi_); // one update, as soon as released + update(); + } + dragMin_ = -1; + } + + private: + const std::vector *h_ = nullptr; + double dlo_ = 0.0, dhi_ = 1.0, clo_ = 0.0, chi_ = 1.0; + bool logX_ = false, logY_ = false; + int dragMin_ = -1; + bool dragging_ = false; // a marker drag in progress: preview only + double preClo_ = 0.0, preChi_ = 1.0; // axis basis captured when the drag began + std::vector snap_; // bins captured at press (frozen bars) + int previewX_ = 0; // cursor x, for the dashed preview line +}; + +// What replaced the colour bar at the side of the image: a small panel with the +// histogram, the palette, a lin/log switch and the two ends of the mapped range. +class ScalePanel : public QWidget { + public: + ScalePanel(FastImageView *view, QWidget *parent = nullptr) + : QWidget(parent), view_(view) { + auto *col = new QVBoxLayout(this); + col->setContentsMargins(4, 2, 4, 4); + col->setSpacing(3); + histo_ = new ScaleHisto(this); + col->addWidget(histo_); + auto *row = new QHBoxLayout(); + row->setSpacing(4); + pal_ = new QComboBox(this); + for (int i = 0; i <= int(Palette::Sunset); i++) + pal_->addItem(paletteName(static_cast(i))); + scale_ = new QComboBox(this); + scale_->addItem("lin"); + scale_->addItem("log"); + scale_->setToolTip("image: log10(value), values <= 0 are capped at\n" + "1e-2 so empty pixels stay visible"); + hscale_ = new QComboBox(this); + hscale_->addItem("lin"); + hscale_->addItem("log"); + hscale_->setToolTip("histogram bars: linear counts, or log10(count+1).\n" + "The histogram's value axis always follows the image\n" + "mapping above."); + lo_ = new QDoubleSpinBox(this); + hi_ = new QDoubleSpinBox(this); + for (QDoubleSpinBox *b : {lo_, hi_}) { + b->setRange(-1e9, 1e9); + b->setDecimals(1); + b->setSingleStep(1.0); + b->setKeyboardTracking(false); + } + row->addWidget(new QLabel("palette:", this)); + row->addWidget(pal_); + row->addWidget(new QLabel("image", this)); + row->addWidget(scale_); + row->addWidget(new QLabel("histo", this)); + row->addWidget(hscale_); + row->addWidget(new QLabel("min", this)); + row->addWidget(lo_); + row->addWidget(new QLabel("max", this)); + row->addWidget(hi_); + row->addStretch(1); + col->addLayout(row); + connect(pal_, QOverload::of(&QComboBox::currentIndexChanged), this, + [this](int) { push(); }); + connect(scale_, QOverload::of(&QComboBox::currentIndexChanged), + this, [this](int i) { + if (suppress_ || !view_) + return; + view_->setLogScale(i == 1); + refresh(); + }); + connect(hscale_, QOverload::of(&QComboBox::currentIndexChanged), + this, [this](int) { refresh(); }); + connect(lo_, QOverload::of(&QDoubleSpinBox::valueChanged), this, + [this](double) { push(); }); + connect(hi_, QOverload::of(&QDoubleSpinBox::valueChanged), this, + [this](double) { push(); }); + if (histo_) + histo_->onRangeChanged = [this](double a, double b) { + if (suppress_ || !view_) + return; + printf("[scale] range %.4g .. %.4g\n", a, b); + fflush(stdout); + view_->setColorRange(a, b); + suppress_++; + if (lo_) + lo_->setValue(a); + if (hi_) + hi_->setValue(b); + suppress_--; + refresh(); + }; + // as thin as asked: the image takes the window, the panel stays small + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + setFixedHeight(38 + 30); + if (view_) { + view_->onImageChanged = [this]() { refresh(); }; + refresh(); + } + } + // pull the view's state into the controls (after Set Ranges, a palette key…) + void syncFromView() { refresh(); } + // 0 = colour, 1 = BW, 2 = BW 2 - used by the b/B/c shortcuts + void setPaletteChoice(int i) { + if (pal_ && i >= 0 && i < pal_->count()) + pal_->setCurrentIndex(i); + } + + private: + void push() { // controls -> view + if (suppress_ || !view_) + return; + const int pi = pal_ ? pal_->currentIndex() : 0; + view_->setPaletteLut(paletteLut(static_cast(pi))); + double a = lo_ ? lo_->value() : 0.0, b = hi_ ? hi_->value() : 1.0; + if (b <= a) { + b = a + 1.0; + suppress_++; + if (hi_) + hi_->setValue(b); + suppress_--; + } + view_->setColorRange(a, b); + refresh(); + } + public: + void refresh() { // view -> controls + if (!view_) + return; + // Dragging the Z scale on the image changes the range on every mouse + // move; re-filling the histogram from the image with that half-applied + // range fed the drag back into the bars. Frozen until release, when + // onScaleReleased() calls this again. + if (view_->isScaleDragging()) + return; + suppress_++; + double a = 0.0, b = 1.0; + view_->colorRange(a, b); + if (lo_) + lo_->setValue(a); + if (hi_) + hi_->setValue(b); + if (scale_) + scale_->setCurrentIndex(view_->logScale() ? 1 : 0); + double lo = 0.0, hi = 1.0; + view_->histogramRange(lo, hi); + if (histo_) + histo_->setData(&view_->histogram(), lo, hi, a, b, + view_->logScale(), + hscale_ && hscale_->currentIndex() == 1); + suppress_--; + } + private: + FastImageView *view_ = nullptr; + ScaleHisto *histo_ = nullptr; + QComboBox *pal_ = nullptr, *scale_ = nullptr, *hscale_ = nullptr; + QDoubleSpinBox *lo_ = nullptr, *hi_ = nullptr; + int suppress_ = 0; +}; + +class MainWindow : public QMainWindow { + public: + MainWindow(); + ~MainWindow() override; + void start(); + void stop(); + protected: + void closeEvent(QCloseEvent *event) override; + + private: + void setupUI(); + void connectShortcuts(); + + // periodic GUI work + void processImages(); // every 30 ms: mailbox -> widgets + void updateReceiverRoiOverlay(); // red rx_roi box on the ADC image + void updateStatus(); // every 500 ms: counters / labels + void checkSignals(); // every 200 ms: Ctrl-C shutdown + + // shared actions (buttons + keyboard shortcuts) + void toggleStartPause(); + void faster(); + void slower(); + void toggleGain(); + void setApplyGain(bool on); + void zoomTo(FastImageView *src, double x1, double y1, double x2, + double y2); + void bwPalette1(); + void bwPalette2(); + void colPal(); + void toggleGateRoi(bool on); + void setRoiEditMode(bool on); // in-image ROI editor + void saveRoi(); + void loadRoiDialog(); + // applies a gate ROI directly (no capture from the ADC view) + // restrict == true (command line) also turns "ROI only" on; picking an + // entry from the list only makes it the current ROI + void applyRoi(int x, int y, int w, int h, const QString &src, + bool restrict = false); + void resetHis(); + void resetAxis(); + void toggleRoiSave(bool on); + void browseSaveDir(); + void saveDestDialog(); + void updateSaveLabel(); + void acquirePede(); + void savePede(); + void readPede(); + void loadAllPede(); + void loadGainMap(); + // lines up the boxes of the flow chart (gates on the left, Pedestal/Gain + // on the right) and repaints the arrows + void layoutFlow(); + void updateFlow(); + void updateGainMapLabel(); + void updateRoiLabel(); + void captureRoi(); // select the ADC zoom as the current ROI + void resizeEvent(QResizeEvent *event) override; + void showEvent(QShowEvent *event) override; + void showRangeDialog(); + void showRateDialog(); + void unsetRoi(); // forget the current ROI (full frame again) + // While a run writes, the settings that would change what lands in the file + // are locked (display-only controls stay live). + void applySaveLock(bool saving); + // Gate criterion changes stay possible during a run, but they ask first and + // leave a note in the JSON of the file being written. + bool confirmGateChangeDuringRun(const QString &what); + void quitApp(); + void closeSideWindows(); // the ADC / gain / photon / panel windows + // what the flow diagram has to show (see FlowStrip::setStages) + void flowStageStates(bool &displayPede, bool &savePede, bool &gateOn, + bool &pmap) const; + // asked before quitting; returns false when the operator cancels + bool confirmQuit(); + + void updateDrawLabel(); + void setRanges(int amin, int amax, int pmin, int pmax, int cmin, int cmax); + // current on-screen ranges (read from the displayed histograms) + void liveAdcImageRange(int &lo, int &hi) const; + void liveMapRange(int &lo, int &hi) const; + void liveChipRange(int &lo, int &hi) const; + + Pipeline pipeline_; + QWidget *leftPad_ = nullptr, *pedePad_ = nullptr, *gainPad_ = nullptr; + // leftPad, pedePad, gainPad, gatePad - kept between solving passes + int flowPads_[4] = {0, 0, 0, 24}; + int flowPass_ = 0; + bool flowQueued_ = false; + QVBoxLayout *midCol_ = nullptr, *leftCol_ = nullptr, *rightCol_ = nullptr; + QWidget *midSpacer_ = nullptr; + FlowStrip *strip_ = nullptr; + QWidget *gatePad_ = nullptr; // alignment gap between the two gates + QLabel *arrGainDown_ = nullptr; + QLabel *arrRoiDown_ = nullptr; + QLabel *arrTrackUp_ = nullptr; + QGroupBox *scGroup_ = nullptr, *sigGroup_ = nullptr; + QGroupBox *displayGroup_ = nullptr, *saveGroup_ = nullptr; + QGroupBox *pedeGroup_ = nullptr, *gainGroup_ = nullptr; + QGroupBox *pmapGroup_ = nullptr; // virtual photon counting (needs a pede) + QCheckBox *gainStateChk_ = nullptr; // read-only "gain corrected" indicator + + QTimer *processTimer = nullptr; + QTimer *statusTimer = nullptr; + QTimer *sigTimer = nullptr; + + QLabel *frameLabel = nullptr; + QLabel *statusLabel = nullptr; + QLabel *drawLabel = nullptr; + QLabel *streamVal_ = nullptr; + QLabel *pedeVal_ = nullptr; + QLabel *errVal_ = nullptr; + QLabel *framesInCumVal_ = nullptr; + QLabel *framesInCurVal_ = nullptr; + QLabel *framesPassCumVal_ = nullptr; + QLabel *framesPassCurVal_ = nullptr; + QLabel *framesDropCumVal_ = nullptr; + QLabel *framesDropCurVal_ = nullptr; + QLabel *pixelsAboveCumVal_ = nullptr; + QLabel *pixelsAboveCurVal_ = nullptr; + QLabel *rateInVal_ = nullptr; + QLabel *ratePassVal_ = nullptr; + QLabel *dropFracVal_ = nullptr; + QLabel *displayDropVal_ = nullptr; + QLabel *alignVal_ = nullptr; + QLabel *gateRoiVal_ = nullptr; + QSpinBox *saveFrames_ = nullptr; + QCheckBox *saveCorrected_ = nullptr; + QRadioButton *saveModeFrames_ = nullptr; + QRadioButton *saveModePhoton_ = nullptr; + QSpinBox *pmapSaveSpin_ = nullptr; + QLabel *saveBudgetLabel_ = nullptr; + QLineEdit *saveDirEdit_ = nullptr; + // the destination as it was at start-up: that pair is the default, and the + // "previous destinations" list must not collect it on every run + QString saveDefaultDir_, saveDefaultBase_; + QLineEdit *saveBaseEdit_ = nullptr; + QPushButton *btnSaveDest_ = nullptr; + QPushButton *btnSaveStart = nullptr; + QLabel *saveVal_ = nullptr; + bool m_saveWasRunning = false; + + QCheckBox *gateChk_ = nullptr; + QLabel *thrLabel_ = nullptr; // says whether the gate threshold is on raw ADC + QCheckBox *contigChk_ = nullptr; + QCheckBox *hotChk_ = nullptr; + QPushButton *btnGetHot_ = nullptr; + std::vector adcScratch_; // ADC image with hot pixels replaced + std::vector photonScratch_; // photon map with hot pixels zeroed + QLabel *hotVal_ = nullptr; + QPushButton *btnResetHot_ = nullptr; + QDoubleSpinBox *thrSpin_ = nullptr; + QSpinBox *minPixSpin_ = nullptr; + QPushButton *btnGateRoi = nullptr; + QComboBox *scCombo_ = nullptr; + QLabel *scLastVal_ = nullptr; + QLabel *scDropCumVal_ = nullptr; + QLabel *scDropCurVal_ = nullptr; + QPushButton *btnSaveRoi = nullptr; + QPushButton *btnLoadRoi = nullptr; + QPushButton *btnSelectRoi_ = nullptr; + QPushButton *btnUnsetRoi_ = nullptr; + QGroupBox *roiGroup_ = nullptr; + QLabel *roiListVal_ = nullptr; + bool m_applyingRoi = false; // guards toggleGateRoi() during applyRoi() + + QPushButton *btnStartPause = nullptr; + QPushButton *btnFaster = nullptr; + QPushButton *btnSlower = nullptr; + QPushButton *btnGainToggle = nullptr; + QPushButton *btnBWPalette = nullptr; + QPushButton *btnBWPalette2 = nullptr; + QPushButton *btnLoadPede = nullptr; + QPushButton *btnSavePede = nullptr; + QPushButton *btnReadPede = nullptr; + QPushButton *btnLoadAllPede = nullptr; + QPushButton *btnGainMap = nullptr; + QCheckBox *kevChk_ = nullptr; + QPushButton *btnNoPede_ = nullptr; + QPushButton *btnPedeTrack_ = nullptr; + QLabel *pedeTrackVal_ = nullptr; + QGroupBox *pedeTrackGroup_ = nullptr; + QLabel *noPedeVal_ = nullptr; + QLabel *gainMapVal_ = nullptr; + QPushButton *btnResetHis = nullptr; + QPushButton *btnResetAxis = nullptr; + QPushButton *btnQuit = nullptr; + QPushButton *btnSetRanges = nullptr; + QPushButton *btnRates = nullptr; + QDoubleSpinBox *photonThrSpin_ = nullptr; // ADU, or keV with gain on + QLabel *photonThrLabel_ = nullptr; + QPushButton *btnColPal = nullptr; + QPushButton *btnResetCounters = nullptr; + + FastImageView *m_adcView = nullptr; + FastImageView *m_gainView = nullptr; + QWidget *m_adcWin = nullptr; + ScalePanel *m_adcScale_ = nullptr; + ScalePanel *m_photonScale_ = nullptr; + QWidget *m_gainWin = nullptr; + QWidget *m_photonWin = nullptr; + FastImageView *m_photonView = nullptr; + QCheckBox *pmapCumulative_ = nullptr; + QCheckBox *zoomCoordChk_ = nullptr; + QPushButton *btnShowGain_ = nullptr; // Display: reveal the Gain window + QPushButton *btnShowPlots_ = nullptr; // Display: reveal the ROOT canvases // Display: link the image zoom levels + bool zoomCoord_ = true; // on = any window zooms them all + QSpinBox *pmapEverySpin_ = nullptr; + uint64_t pmapSeq_ = 0; +#ifdef JFSTRX + // separate strixel window: six group panels, display only (no ROI) + std::array m_adcPanelView{}; + QWidget *m_adcPanelWin = nullptr; + QLabel *m_adcPanelZ = nullptr; // "Z: lo .. hi" legend of the strixel panels +#endif + + // last receiver ROI mirrored onto the ADC image (red box) + bool m_rxRoiShown = false; + int m_rxRoiLast[4] = {-1, -1, -1, -1}; +}; + +MainWindow::MainWindow() : QMainWindow(nullptr) { + setWindowTitle("Online JF Display + Event Finder"); + setMinimumWidth(420); + + setupUI(); + connectShortcuts(); + + goout = 1; + gain_flag = false; + HDraw_every = 20; + // the operator's range defaults (Set Ranges) are in force from the start; + // "Reset Axis" releases them and the axes auto-scale again + fixranges = true; + + initGeometry(); + + hchptr = (short *)malloc(NCH * sizeof(short)); + for (int k = 0; k < NCH; k++) + hchptr[k] = 0; + + cout << "opening the rootapp" << endl; + int argc_copy = 1; + char dummy_name[] = "onlinedisp"; + char *argv_copy[] = {dummy_name, nullptr}; + rootapp = new TApplication("Online JF display", &argc_copy, argv_copy); + + loadRootPalette(Palette::Color); + + char hname[100]; +#ifdef JFSTRX + int nxx = 79; + int nyy = 165; + his1000 = new TH2F("his1000", "2d, chip 1 group 1 (25um) pede corr.", nxx, + -0.5, nxx - 0.5, nyy, -0.5, nyy - 0.5); + his1060 = new TH2F("his1060", "2d, chip 6 group 1 (25um) pede corr.", nxx, + -0.5, nxx - 0.5, nyy, -0.5, nyy - 0.5); + nxx = 47; + nyy = 320; + his1001 = new TH2F("his1001", "2d, chip 1 group 2 (15um) pede corr.", nxx, + -0.5, nxx - 0.5, nyy, -0.5, nyy - 0.5); + his1061 = new TH2F("his1061", "2d, chip 6 group 2 (15um) pede corr.", nxx, + -0.5, nxx - 0.5, nyy, -0.5, nyy - 0.5); + nxx = 59; + nyy = 476; + his1002 = new TH2F("his1002", "2d, chip 1 group 3 (18.75um) pede corr.", nxx, + -0.5, nxx - 0.5, nyy, -0.5, nyy - 0.5); + his1062 = new TH2F("his1062", "2d, chip 6 group 3 (18.75um) pede corr.", nxx, + -0.5, nxx - 0.5, nyy, -0.5, nyy - 0.5); + his1000->SetOption("colz"); + his1001->SetOption("colz"); + his1002->SetOption("colz"); + his1060->SetOption("colz"); + his1061->SetOption("colz"); + his1062->SetOption("colz"); +#else + his1000 = new TH2F("his1000", "2d, ev. pede corrected", nx, -0.5, nx - 0.5, + ny, -0.5, ny - 0.5); + his1000->SetOption("colz"); +#endif + + his2000 = new TH2F("his2000", "2d gain", nx, -0.5, nx - 0.5, ny, -0.5, + ny - 0.5); + his2000->GetZaxis()->SetRangeUser(0, 4); + + // The photon map has no ROOT histogram any more: it is filled in the + // analysis and shown in its own Qt window (see "virtual photon counting"). + + his4500 = new TH2F("his4500", "T vs B", 101, -500, MAX_POS, 101, -500, + MAX_POS); + hisLvsR = new TH2F("hisLvsR", "L vs R", 101, -500, MAX_POS, 101, -500, + MAX_POS); + hchip = new TH1I *[8]; +#ifdef JFSTRX + for (int k = 0; k < 8; k++) { + if (k < 3) + sprintf(hname, "%d_hchip1group%d", k, k + 1); + else if (k > 4) + sprintf(hname, "%d_hchip6group%d", k, k - 4); + else + sprintf(hname, "%d_", k); + hchip[k] = new TH1I(hname, hname, NBIN, MIN_POS, MAX_POS); + } +#else + for (int k = 0; k < 8; k++) { + sprintf(hname, "hchip%d", k); + hchip[k] = new TH1I(hname, hname, NBIN, MIN_POS, MAX_POS); + } +#endif + // ONE spectrum of the gate-ROI pixels (canvas A7): identical binning to + // the chip spectra, summed over the whole selection + hroi = new TH1I("hroi", "hroi", NBIN, MIN_POS, MAX_POS); + // the same two sets again in keV (canvases A8/A9). The axes are the ADU + // axes divided by KEV_NOMINAL_ADU with the same number of bins, so a + // pixel's keV value selects the same bin index as its ADU value and the + // two sets always show the same window. + hchipKev = new TH1I *[8]; + for (int k = 0; k < 8; k++) { + char kn[64]; + sprintf(kn, "hchip%d_keV", k); + hchipKev[k] = new TH1I(kn, kn, NBIN, MIN_POS / KEV_NOMINAL_ADU, + MAX_POS / KEV_NOMINAL_ADU); + } + hroiKev = new TH1I("hroi_keV", "hroi_keV", NBIN, MIN_POS / KEV_NOMINAL_ADU, + MAX_POS / KEV_NOMINAL_ADU); + + cout << "end of histo booking" << endl; +#ifdef JFSTRX + if (A3 == nullptr) { + A3 = new TCanvas("A3", "Plotting Canvas ADC", 150, 360, 1200, 750); + p1 = new TPad("p1", "p1", 0.01, 0.5, 0.49, 1.); + p1->Draw(); + p2 = new TPad("p2", "p2", 0.51, 0.75, 0.99, 0.97); + p2->Draw(); + p3 = new TPad("p3", "p3", 0.01, 0.25, 0.49, 0.47); + p3->Draw(); + p4 = new TPad("p4", "p4", 0.51, 0.53, 0.99, 0.75); + p4->Draw(); + p5 = new TPad("p5", "p5", 0.01, 0.03, 0.49, 0.25); + p5->Draw(); + p6 = new TPad("p6", "p6", 0.51, 0., 0.99, 0.5); + p6->Draw(); + } +#endif + if (A4 == nullptr) + A4 = new TCanvas("A4", "Plotting Canvas PHs", 750, 300, 1000, 800); + A4->Clear(); + A4->Divide(4, 2, 0.005, 0.005); + if (false) // the photon map canvas is gone; keep the pointer unused + A5 = new TCanvas("A5", "Plotting Canvas Photon Map", 750, 300, 1000, + 600); + if (A6 == nullptr) + A6 = new TCanvas("A6", "Plotting Canvas LvsR", 650, 250, 650, 660); + if (A7 == nullptr) + A7 = new TCanvas("A7", "ROI PH", 770, 340, 1000, 700); + A7->Clear(); // one pad: all the gate-ROI pixels in one spectrum + // The keV canvases are NOT created here: a ROOT canvas maps its window in + // the constructor, and these two must stay invisible until a gain map is + // loaded. They are created lazily by the ROOT thread in + // Plot1DHistosKev()/PlotRoiHistosKev() the first time they are needed. + + + // display clones painted by the ROOT thread (sources stay exclusive to + // the analysis thread) + disp4500 = (TH2F *)his4500->Clone("disp4500"); + dispLvsR = (TH2F *)hisLvsR->Clone("dispLvsR"); + for (int ih = 0; ih < 8; ih++) { + char nm[64]; + sprintf(nm, "dispChip%d", ih); + dispChip[ih] = (TH1I *)hchip[ih]->Clone(nm); + } + dispRoi = (TH1I *)hroi->Clone("dispRoi"); + for (int ih = 0; ih < 8; ih++) { + char nm[64]; + sprintf(nm, "dispChipKev%d", ih); + dispChipKev[ih] = (TH1I *)hchipKev[ih]->Clone(nm); + } + dispRoiKev = (TH1I *)hroiKev->Clone("dispRoiKev"); + + // start with the operator's ranges (Set Ranges dialog): the spectra open + // on the PH window, the main plot keeps its value window (fixranges) + for (int ih = 0; ih < 8; ih++) { + if (dispChip[ih]) + dispChip[ih]->GetXaxis()->SetRangeUser(float(chipxmin.load()), + float(chipxmax.load())); + } + if (dispRoi) + dispRoi->GetXaxis()->SetRangeUser(float(chipxmin.load()), + float(chipxmax.load())); + if (dispRoiKev) + dispRoiKev->GetXaxis()->SetRangeUser( + chipxmin.load() / KEV_NOMINAL_ADU, + chipxmax.load() / KEV_NOMINAL_ADU); + for (int ih = 0; ih < 8; ih++) { + if (dispChipKev[ih]) + dispChipKev[ih]->GetXaxis()->SetRangeUser( + chipxmin.load() / KEV_NOMINAL_ADU, + chipxmax.load() / KEV_NOMINAL_ADU); + } + // the photon window opens on the photon-map colour range (the ADC image + // was given the ADC range in setupUI()) + if (m_photonView) + m_photonView->setColorRange(pmmin.load(), pmmax.load()); +#ifdef JFSTRX + disp1000 = (TH2F *)his1000->Clone("disp1000"); + disp1001 = (TH2F *)his1001->Clone("disp1001"); + disp1002 = (TH2F *)his1002->Clone("disp1002"); + disp1060 = (TH2F *)his1060->Clone("disp1060"); + disp1061 = (TH2F *)his1061->Clone("disp1061"); + disp1062 = (TH2F *)his1062->Clone("disp1062"); +#endif + + printhelp(); +} + +MainWindow::~MainWindow() { stop(); } + +// --------------------------------------------------------------------------- +// UI +// --------------------------------------------------------------------------- +void MainWindow::setupUI() { + auto *central = new QWidget(this); + setCentralWidget(central); + auto *rootLayout = new QVBoxLayout(central); + auto *columns = new QHBoxLayout(); + rootLayout->addLayout(columns); + + auto *leftCol = new QVBoxLayout(); + auto *rightCol = new QVBoxLayout(); + // The connectors that cross from one column to the other are drawn by a + // strip placed by hand in the gap (not in the layout: an item here would + // stretch the column row and leave empty space above the save panel). A + // zero-height spacer reserves the width. + midCol_ = new QVBoxLayout(); + leftCol_ = leftCol; + rightCol_ = rightCol; + midSpacer_ = new QWidget(central); + midSpacer_->setFixedWidth(64); + midSpacer_->setFixedHeight(0); + midCol_->addWidget(midSpacer_); + strip_ = new FlowStrip(central); + columns->addLayout(leftCol, 1); + columns->addLayout(midCol_, 0); + columns->addLayout(rightCol, 1); + // bottom row: what the program is doing (Status) and what it has counted. + // It is added to the layout *after* the save panel (see the end of this + // function) so the save ends up above the counters. + auto *bottomRow = new QHBoxLayout(); + + // ── Status ── + auto *statusGroup = new QGroupBox("Status", central); + auto *statusLayout = new QVBoxLayout(statusGroup); + frameLabel = new QLabel("Frame: --", statusGroup); + statusLabel = new QLabel("Status: idle", statusGroup); + drawLabel = new QLabel(statusGroup); + streamVal_ = makeValueLabel("disconnected", statusGroup); + pedeVal_ = makeValueLabel("not loaded", statusGroup); + errVal_ = makeValueLabel(QString(), statusGroup); + errVal_->setWordWrap(true); + displayDropVal_ = makeValueLabel("0", statusGroup); + alignVal_ = makeValueLabel(QString(), statusGroup); + alignVal_->setWordWrap(true); + statusLayout->addWidget(frameLabel); + statusLayout->addWidget(statusLabel); + statusLayout->addWidget(drawLabel); + statusLayout->addWidget(new QLabel("Stream:", statusGroup)); + statusLayout->addWidget(streamVal_); + statusLayout->addWidget(new QLabel("Pedestal:", statusGroup)); + statusLayout->addWidget(pedeVal_); + statusLayout->addWidget(new QLabel("Display drops (analysis too slow):", + statusGroup)); + statusLayout->addWidget(displayDropVal_); + statusLayout->addWidget(new QLabel("Frame alignment:", statusGroup)); + statusLayout->addWidget(alignVal_); + statusLayout->addWidget(errVal_); + bottomRow->addWidget(statusGroup, 1); + + // ── Gate (event finder): two gates, applied in series ── + auto *gateGroup = new QGroupBox("Event finder gate", central); + auto *gateCol = new QVBoxLayout(gateGroup); + + // gate 1: storage cell. This is a header field, so it is checked first + // and frames from other cells cost no CPU at all. + auto *scGroup = new QGroupBox("1. Storage cell (header)", gateGroup); + scGroup_ = scGroup; + auto *scForm = new QFormLayout(scGroup); + scCombo_ = new QComboBox(scGroup); + scCombo_->addItem("ALL"); // index 0 == no storage-cell gating + for (int i = 0; i <= 15; i++) + scCombo_->addItem(QString::number(i)); + scCombo_->setToolTip( + "Pass only frames acquired in this storage cell (bits 8-11 of the\n" + "header's detector specific 3; 0..15, 15 = default). \"ALL\" passes\n" + "every cell. Checked before any pixel is touched, so the signal\n" + "gate only ever sees frames that survive this one."); + scLastVal_ = makeValueLabel("last frame: ?", scGroup); + scForm->addRow("Only storage cell:", scCombo_); + scForm->addRow(scLastVal_); + gateCol->addWidget(scGroup); + // Plain invisible spacer between the two gates: the path from the storage + // cell to the signal gate goes through the Pedestal and is drawn by the + // strip, so a direct arrow here would be wrong. layoutFlow() stretches + // this gap so the signal gate's middle lands on the Gain's top. + gatePad_ = new QWidget(gateGroup); + gatePad_->setFixedHeight(24); + gateCol->addWidget(gatePad_); + + // gate 2: the signal gate (the pixel counting) + auto *sigGroup = new QGroupBox("2. Signal gate (pixel counting)", gateGroup); + sigGroup_ = sigGroup; + auto *gateForm = new QFormLayout(sigGroup); + gateChk_ = new QCheckBox("Gate enabled (off = passthrough)", sigGroup); + gateChk_->setChecked(g_cfg.gateEnabled); + contigChk_ = new QCheckBox("Contiguous (N px within circle, r = N/2+1)", + sigGroup); + contigChk_->setChecked(g_cfg.contiguous); + thrSpin_ = new QDoubleSpinBox(sigGroup); + thrSpin_->setRange(0.0, 1000000.0); + thrSpin_->setDecimals(1); + thrSpin_->setSingleStep(10.0); + thrSpin_->setValue(g_cfg.threshold); + minPixSpin_ = new QSpinBox(sigGroup); + minPixSpin_->setRange(1, 10000000); + minPixSpin_->setValue(static_cast(g_cfg.minPixels)); + btnResetCounters = new QPushButton("Reset counters", gateGroup); + btnGateRoi = new QPushButton("ROI only (i)", sigGroup); + btnGateRoi->setCheckable(true); + btnGateRoi->setToolTip( + "Count only inside the ROI captured from the ADC zoom at the moment\n" + "this is pressed. The ROI is a snapshot: panning or zooming the ADC\n" + "image afterwards does not move it (it stays outlined in yellow)."); + gateRoiVal_ = makeValueLabel("full frame", sigGroup); + { + auto *gateRow = new QHBoxLayout(); + gateRow->addWidget(gateChk_); + gateRow->addWidget(btnGateRoi); + gateForm->addRow(gateRow); + } + gateForm->addRow(contigChk_); + hotChk_ = new QCheckBox("Suppress hot pixels in the gate", sigGroup); + hotChk_->setChecked(g_cfg.hotSuppress); + hotChk_->setToolTip( + "Masked pixels (see Get hot pixels) are excluded from the gate: they\n" + "do not count towards min pixels and cannot form a contiguous cluster."); + hotVal_ = makeValueLabel("hot: 0 px", sigGroup); + btnGetHot_ = new QPushButton("Get hot pixels...", sigGroup); + btnGetHot_->setToolTip( + "Mark hot pixels from the current photon map: every pixel whose map\n" + "value is above the threshold you enter becomes a hot pixel."); + btnResetHot_ = new QPushButton("Clear hot pixels", sigGroup); + { + auto *hotRow = new QHBoxLayout(); + hotRow->addWidget(hotChk_); + hotRow->addWidget(hotVal_); + gateForm->addRow("Hot pixels:", hotRow); + auto *hotBtnRow = new QHBoxLayout(); + hotBtnRow->addWidget(btnGetHot_); + hotBtnRow->addWidget(btnResetHot_); + gateForm->addRow(hotBtnRow); + } + thrLabel_ = new QLabel("Threshold (ADC, pede-subtracted):", sigGroup); + gateForm->addRow(thrLabel_, thrSpin_); + gateForm->addRow("Min pixels above threshold:", minPixSpin_); + gateForm->addRow("Gate ROI:", gateRoiVal_); + gateCol->addWidget(sigGroup); + + auto *seriesHint = new QLabel( + "Applied in series - storage cell first, so frames from other cells " + "never reach the pixel counting (all 0-15, no gating, and both " + "together are all possible).", + gateGroup); + seriesHint->setWordWrap(true); + gateCol->addWidget(seriesHint); + gateCol->addWidget(btnResetCounters); + // 1) Storage cell then 2) Signal gate: the left column *is* the gate flow. + // leftPad_ is sized at run time so that the middle of the storage-cell box + // lands on the top edge of the Pedestal box (see layoutFlow()). + // (Quit is added at the top of the right column, right aligned) + leftPad_ = new QWidget(central); + leftPad_->setFixedHeight(0); + leftCol->addWidget(leftPad_); + leftCol->addWidget(gateGroup); + + // ── Counters ── + auto *counterBox = new QGroupBox("Counters", central); + auto *grid = new QGridLayout(counterBox); + grid->setColumnStretch(1, 1); + grid->setColumnStretch(2, 1); + auto *headCum = new QLabel("Cumulative", counterBox); + auto *headCur = new QLabel("Current", counterBox); + headCum->setStyleSheet("font-weight: bold;"); + headCur->setStyleSheet("font-weight: bold;"); + grid->addWidget(new QLabel(QString(), counterBox), 0, 0); + grid->addWidget(headCum, 0, 1); + grid->addWidget(headCur, 0, 2); + + framesInCumVal_ = makeValueLabel("0", counterBox); + framesInCurVal_ = makeValueLabel("0", counterBox); + grid->addWidget(new QLabel("Frames in", counterBox), 1, 0); + grid->addWidget(framesInCumVal_, 1, 1); + grid->addWidget(framesInCurVal_, 1, 2); + + framesPassCumVal_ = makeValueLabel("0", counterBox); + framesPassCurVal_ = makeValueLabel("0", counterBox); + grid->addWidget(new QLabel("Frames passed", counterBox), 2, 0); + grid->addWidget(framesPassCumVal_, 2, 1); + grid->addWidget(framesPassCurVal_, 2, 2); + + framesDropCumVal_ = makeValueLabel("0", counterBox); + framesDropCurVal_ = makeValueLabel("0", counterBox); + grid->addWidget(new QLabel("Frames dropped", counterBox), 3, 0); + grid->addWidget(framesDropCumVal_, 3, 1); + grid->addWidget(framesDropCurVal_, 3, 2); + + scDropCumVal_ = makeValueLabel("0", counterBox); + scDropCurVal_ = makeValueLabel("0", counterBox); + grid->addWidget(new QLabel("Frames dropped (SC)", counterBox), 4, 0); + grid->addWidget(scDropCumVal_, 4, 1); + grid->addWidget(scDropCurVal_, 4, 2); + + pixelsAboveCumVal_ = makeValueLabel("0", counterBox); + pixelsAboveCurVal_ = makeValueLabel("0", counterBox); + grid->addWidget(new QLabel("Pixels above", counterBox), 5, 0); + grid->addWidget(pixelsAboveCumVal_, 5, 1); + grid->addWidget(pixelsAboveCurVal_, 5, 2); + + rateInVal_ = makeValueLabel("0 fps / 0 MB/s", counterBox); + grid->addWidget(new QLabel("Rate in (fps / MB/s)", counterBox), 6, 0); + grid->addWidget(rateInVal_, 6, 1, 1, 2); + ratePassVal_ = makeValueLabel("0 fps / 0 MB/s", counterBox); + grid->addWidget(new QLabel("Rate pass (fps / MB/s)", counterBox), 7, 0); + grid->addWidget(ratePassVal_, 7, 1, 1, 2); + dropFracVal_ = makeValueLabel("0.000", counterBox); + grid->addWidget(new QLabel("Drop fraction (cum.)", counterBox), 8, 0); + grid->addWidget(dropFracVal_, 8, 1, 1, 2); + bottomRow->addWidget(counterBox, 1); + + // ── Display controls ── + auto *ctrlGroup = new QGroupBox("Display", central); + displayGroup_ = ctrlGroup; + auto *ctrlGrid = new QGridLayout(ctrlGroup); + btnStartPause = new QPushButton("Start/Pause (s)", ctrlGroup); + btnFaster = new QPushButton("Faster (+)", ctrlGroup); + btnSlower = new QPushButton("Slower (-)", ctrlGroup); + btnGainToggle = new QPushButton("gain correction on/off (g)", ctrlGroup); + btnGainToggle->setCheckable(true); + btnGainToggle->setToolTip( + "Divide every pixel by its ADU/keV - the per-pixel value from the gain\n" + "map when one is loaded, the nominal 40 ADU/keV otherwise - to get keV.\n" + "\"keV PH\" then shows the PH spectra in keV; with this off (or keV PH\n" + "unticked) they show the raw pedestal-subtracted ADC."); + btnBWPalette = new QPushButton("BW Pal. (b)", ctrlGroup); + btnBWPalette2 = new QPushButton("BW Pal. 2 (B)", ctrlGroup); + btnSetRanges = new QPushButton("Set Ranges (S)", ctrlGroup); + btnColPal = new QPushButton("Col Pal (c)", ctrlGroup); + btnRates = new QPushButton("Update Rates", ctrlGroup); + btnResetHis = new QPushButton("Reset His (r)", ctrlGroup); + btnResetAxis = new QPushButton("Reset Axis (R)", ctrlGroup); + photonThrSpin_ = new QDoubleSpinBox(central); + photonThrSpin_->setRange(0.0, 100000.0); + photonThrSpin_->setDecimals(0); // ADU: whole numbers + photonThrSpin_->setSingleStep(10.0); + photonThrSpin_->setValue(static_cast(g_pheneThreshold)); + photonThrSpin_->setToolTip( + "Photon-map threshold (pede-subtracted ADC): only pixels above this\n" + "contribute to the map, and the number of photons a pixel counts for is\n" + "(value + threshold) / (2 x threshold). In ADU while the gain\n" + "correction is off, in keV while it is on (the value is then divided by\n" + "the pixel's ADU/keV first); both are remembered separately. It does\n" + "not affect the event-finder gate, which has its own threshold."); + // ── Virtual photon counting ── + // Left of the Display column (the Display lives in the right column at this + // height). The map itself is the same photon counting as before, but shown + // in its own Qt window: a fresh image every N analysed frames, and + // cumulative only if asked. + auto *pmapGroup = new QGroupBox("Virtual photon counting", central); + pmapGroup_ = pmapGroup; + auto *pmapGrid = new QGridLayout(pmapGroup); + pmapCumulative_ = new QCheckBox("cumulative (toggle to reset)", pmapGroup); + pmapCumulative_->setChecked(gPmapCumulative.load()); + pmapCumulative_->setToolTip( + "off: every displayed image shows only the photons counted since the\n" + "previous one; on: the map keeps adding up (the old behaviour)"); + pmapGrid->addWidget(pmapCumulative_, 0, 0, 1, 2); + pmapGrid->addWidget( + new QLabel("write plot every N (gated) images:", pmapGroup), 1, 0); + pmapEverySpin_ = new QSpinBox(pmapGroup); + pmapEverySpin_->setRange(1, 1000000); + pmapEverySpin_->setSingleStep(100); + pmapEverySpin_->setValue(gPmapEvery.load()); + pmapEverySpin_->setToolTip( + "How many analysed frames are accumulated into one photon map image.\n" + "It counts the frames that reach the analysis, i.e. AFTER the gate and\n" + "the frame decimation, so the wall-clock interval between two maps\n" + "grows when frames are rejected or skipped (it is not independent of\n" + "them). Default 2000."); + pmapGrid->addWidget(pmapEverySpin_, 1, 1); + photonThrLabel_ = new QLabel("photon threshold (ADU):", pmapGroup); + pmapGrid->addWidget(photonThrLabel_, 2, 0); + // the photon threshold moves here from the Display panel + pmapGrid->addWidget(photonThrSpin_, 2, 1); + // under the gates, left of the Display column + leftCol->insertWidget(2, pmapGroup); + leftCol->addStretch(1); + + ctrlGrid->addWidget(btnStartPause, 0, 0); + ctrlGrid->addWidget(btnFaster, 0, 1); + ctrlGrid->addWidget(btnSlower, 0, 2); + // the three palette buttons share one row + ctrlGrid->addWidget(btnBWPalette, 1, 0); + ctrlGrid->addWidget(btnBWPalette2, 1, 1); + ctrlGrid->addWidget(btnColPal, 1, 2); + // (btnGainToggle lives in the Gain panel, below) + ctrlGrid->addWidget(btnSetRanges, 2, 0); + ctrlGrid->addWidget(btnRates, 2, 1); + ctrlGrid->addWidget(btnResetHis, 3, 0); + ctrlGrid->addWidget(btnResetAxis, 3, 1); + zoomCoordChk_ = new QCheckBox("zoom coordination", ctrlGroup); + zoomCoordChk_->setChecked(true); // the historic ADC-master behaviour + zoomCoordChk_->setToolTip( + "On: zooming any image window zooms all of them (and the ROOT plots)\n" + "to the same rectangle. Off: every window keeps its own zoom, and\n" + "nothing is propagated."); + ctrlGrid->addWidget(zoomCoordChk_, 4, 0, 1, 3); + // Both start hidden ("disabled by default"): the operator reveals them. + btnShowGain_ = new QPushButton("show gain image", ctrlGroup); + btnShowGain_->setCheckable(true); + btnShowGain_->setChecked(false); + btnShowGain_->setToolTip( + "Show or hide the separate Gain Image window. It is hidden at\n" + "startup; the gain map itself is still used when the gain correction\n" + "is on."); + btnShowPlots_ = new QPushButton("show ROOT plots", ctrlGroup); + btnShowPlots_->setCheckable(true); + btnShowPlots_->setChecked(false); + btnShowPlots_->setToolTip( + "Show or hide the ROOT spectra canvases (PH spectra, ROI PH, L vs R,\n" + "the 2D map). They are hidden at startup; the histograms keep being\n" + "filled, so showing them later shows the accumulated data."); + ctrlGrid->addWidget(btnShowGain_, 5, 0, 1, 2); + ctrlGrid->addWidget(btnShowPlots_, 6, 0, 1, 2); + connect(btnShowGain_, &QPushButton::toggled, this, [this](bool on) { + if (m_gainWin) + m_gainWin->setVisible(on); + printf("[display] gain image %s\n", on ? "shown" : "hidden"); + fflush(stdout); + statusLabel->setText(on ? "Status: gain image shown" + : "Status: gain image hidden"); + }); + connect(btnShowPlots_, &QPushButton::toggled, this, [this](bool on) { + RootCmd c; + c.type = RootCmdType::ShowPlots; + c.a = on ? 1.0 : 0.0; + pushRootCmd(c); + printf("[display] ROOT plots %s\n", on ? "shown" : "hidden"); + fflush(stdout); + statusLabel->setText(on ? "Status: ROOT plots shown" + : "Status: ROOT plots hidden"); + }); + connect(zoomCoordChk_, &QCheckBox::toggled, this, [this](bool on) { + zoomCoord_ = on; + printf("[zoom] coordination %s\n", on ? "on" : "off"); + fflush(stdout); + statusLabel->setText(on ? "Status: zoom coordination on (all linked)" + : "Status: zoom coordination off (independent)"); + }); + + // (the Display panel is added below the Gain panel, see below: it shows + // the result, so it comes after the chain that produces it) + + // ── ROI save (HDF5) ── + auto *saveGroup = new QGroupBox("Save", central); + saveGroup_ = saveGroup; + auto *saveForm = new QFormLayout(saveGroup); + saveFrames_ = new QSpinBox(saveGroup); + saveFrames_->setRange(0, 1000000000); + saveFrames_->setValue(1000); + saveFrames_->setSpecialValueText("unlimited"); + if (g_cfg.saveBudget > 0) // --save-budget: frames, or maps in photon mode + saveFrames_->setValue(static_cast(g_cfg.saveBudget)); + saveFrames_->setToolTip( + "Number of gate-passed frames to write; 0 = unlimited (stop with the\n" + "button). The run also stops by itself when this many frames are in\n" + "the file. In virtual photon counting mode it counts the maps."); + btnSaveStart = new QPushButton("Start/Stop save (n)", saveGroup); + btnSaveStart->setCheckable(true); + btnSaveStart->setToolTip( + "Write the active gate ROI of every gate-passed frame to an HDF5\n" + "file (slsReceiver layout, slice 0 = pedestal) plus a JSON sidecar."); + saveCorrected_ = new QCheckBox("Pedestal corrected", saveGroup); + saveCorrected_->setChecked(false); + saveCorrected_->setToolTip( + "Off (default): /data holds the raw 16-bit words and slice 0 is the\n" + "pedestal image.\n" + "On: /data holds pedestal-corrected float32 values with no pedestal\n" + "slice, and the pedestal image is written to the separate dataset\n" + "/pedestal data. Needs a loaded pedestal."); + saveDirEdit_ = new QLineEdit(QString::fromStdString(g_cfg.h5Dir), saveGroup); + saveDirEdit_->setToolTip("Directory the HDF5 file and JSON sidecar go to."); + auto *dirBtn = new QPushButton("...", saveGroup); + dirBtn->setFixedWidth(32); + auto *dirRow = new QHBoxLayout(); + dirRow->addWidget(saveDirEdit_); + dirRow->addWidget(dirBtn); + saveBaseEdit_ = new QLineEdit(QString::fromStdString(g_cfg.h5Base), saveGroup); + saveDefaultBase_ = saveBaseEdit_->text().trimmed(); // start-up default + saveBaseEdit_->setToolTip( + "File-name base. The file is __f0_0.h5 (and the\n" + "JSON the same with .json), so re-using a base never overwrites."); + btnSaveDest_ = new QPushButton("Previous destinations...", saveGroup); + btnSaveDest_->setToolTip( + "List of destinations used before (directory + base), most recent\n" + "first; pick one to fill the fields, or delete entries."); + saveDefaultDir_ = saveDirEdit_->text().trimmed(); // start-up default + saveVal_ = makeValueLabel("idle", saveGroup); + saveVal_->setWordWrap(true); + saveBudgetLabel_ = + new QLabel("Number of frames to be saved:", saveGroup); + saveForm->addRow(saveBudgetLabel_, saveFrames_); + // what gets written: the gate-passed frames, or the virtual photon + // counting maps (non-cumulative, one every M analysed frames) + saveModeFrames_ = new QRadioButton("save frames", saveGroup); + saveModePhoton_ = new QRadioButton("virtual photon counting", saveGroup); + saveModeFrames_->setChecked(gSaveMode.load() != 1); + saveModePhoton_->setChecked(gSaveMode.load() == 1); + saveForm->addRow(saveModeFrames_); + // the correction is a property of the *frames* being saved: the photon maps + // are always pedestal-subtracted, so the box lives under that radio and is + // greyed while the maps are selected + saveForm->addRow(saveCorrected_); + saveForm->addRow(saveModePhoton_); // (was dropped by the move above) + pmapSaveSpin_ = new QSpinBox(saveGroup); + pmapSaveSpin_->setRange(200, 1000000); // the requested minimum + pmapSaveSpin_->setSingleStep(100); + pmapSaveSpin_->setValue(gPmapSaveEvery.load()); + pmapSaveSpin_->setEnabled(gSaveMode.load() == 1); + pmapSaveSpin_->setToolTip( + "virtual photon counting: one non-cumulative map every this many\n" + "analysed frames (minimum 200), independent of the display cadence"); + saveForm->addRow("photon map every N frames:", pmapSaveSpin_); + saveForm->addRow("Directory:", dirRow); + saveForm->addRow("Base name:", saveBaseEdit_); + saveForm->addRow(btnSaveDest_); + saveForm->addRow(btnSaveStart); + saveForm->addRow("Save status:", saveVal_); + // (the ROI-save panel is added below the Gain panel, see the end of this + // function: the right column reads Display, Pedestal, Gain, Save) + + // ── Pedestal ── + auto *pedeGroup = new QGroupBox("Pedestal", central); + pedeGroup_ = pedeGroup; + auto *pedeGrid = new QGridLayout(pedeGroup); + btnLoadPede = new QPushButton("Acquire Pede (p)", pedeGroup); + btnSavePede = new QPushButton("Save Pede (O)", pedeGroup); + btnReadPede = new QPushButton("Read Pede (o)", pedeGroup); + btnLoadAllPede = new QPushButton("Load Pede File (P)", pedeGroup); + btnGainMap = new QPushButton("Gain map (keV)...", pedeGroup); + kevChk_ = new QCheckBox("keV PH", pedeGroup); + kevChk_->setToolTip( + "Show the PH spectra in keV instead of ADU. With a gain map the " + "pixel's own ADU/keV is used; without one the nominal 40 ADU/keV is " + "used and the panel says so."); + // usable without a map: the nominal 40 ADU/keV is used then + gainMapVal_ = new QLabel("no gain map loaded", pedeGroup); + btnNoPede_ = new QPushButton("Pede/Raw toggle", pedeGroup); + btnNoPede_->setCheckable(true); + btnNoPede_->setChecked(gNoPede.load()); // startup default: raw ADC + btnNoPede_->setToolTip( + "Switch between the loaded pedestal and raw ADC. Pressed = raw ADC:\n" + "the display, the photon map, the spectra, the event finder and the ROI\n" + "save then work on raw 14-bit values, and the Pedestal box is greyed\n" + "(this button stays active, it is how you switch back)."); + noPedeVal_ = new QLabel("pede subtracted", pedeGroup); + pedeGrid->addWidget(btnLoadPede, 0, 0); + pedeGrid->addWidget(btnSavePede, 0, 1); + pedeGrid->addWidget(btnReadPede, 1, 0); + pedeGrid->addWidget(btnLoadAllPede, 1, 1); + // calibration for the keV spectra (the ADC plots stay in ADU) + // (the gain map moved to the Gain panel below) + pedeTrackGroup_ = new QGroupBox("Pedestal tracking", central); + auto *trackCol = new QVBoxLayout(pedeTrackGroup_); + btnPedeTrack_ = + new QPushButton("pedestal tracking on/off", pedeTrackGroup_); + btnPedeTrack_->setCheckable(true); + btnPedeTrack_->setChecked(gPedeTracking.load()); // --pede-tracking + btnPedeTrack_->setToolTip( + "Follow the pedestal drift from the frames the signal gate drops\n" + "(the beam is off then). While the gate drops more than 90 % of the\n" + "frames reaching it, 0.2 % of every dropped frame feeds a per-pixel\n" + "running average over a 10 M sample window; the pedestal in use is\n" + "replaced when the largest difference exceeds 5 ADU, at most once\n" + "every 10 s and at least every 10 min. Needs the signal gate on."); + pedeTrackVal_ = makeValueLabel("off", pedeTrackGroup_); + pedeGrid->addWidget(btnNoPede_, 2, 0); + pedeGrid->addWidget(noPedeVal_, 2, 1); + trackCol->addWidget(btnPedeTrack_); + trackCol->addWidget(pedeTrackVal_); + gainMapVal_->setWordWrap(true); + pedePad_ = new QWidget(central); + pedePad_->setFixedHeight(0); + rightCol->addWidget(pedePad_); + rightCol->addWidget(pedeGroup); + // the tracker reads the frames the signal gate drops and feeds its result + // back into the Pedestal box above it, so the arrow points up + // the tracking feedback goes back into the pedestal, so it is pedestal path + arrTrackUp_ = makeFlowArrow(QString::fromUtf8("\u2191"), central, + FlowPedestal); + rightCol->addWidget(arrTrackUp_, 0, Qt::AlignHCenter); + rightCol->addWidget(pedeTrackGroup_); + gainPad_ = new QWidget(central); + gainPad_->setFixedHeight(0); + rightCol->addWidget(gainPad_); + + auto *gainGroup = new QGroupBox("Gain", central); + gainGroup_ = gainGroup; + auto *gainCol = new QVBoxLayout(gainGroup); + { + auto *gainRow = new QHBoxLayout(); + gainRow->addWidget(btnGainToggle); + // Read-only state indicator (the button is the only control): it shows + // at a glance whether the gain correction is applied - itself if a map + // is loaded, the nominal 40 ADU/keV otherwise. + gainStateChk_ = new QCheckBox("gain corrected", gainGroup); + gainStateChk_->setAttribute(Qt::WA_TransparentForMouseEvents, true); + gainStateChk_->setFocusPolicy(Qt::NoFocus); + gainStateChk_->setToolTip( + "Whether the gain correction is applied (read-only: switch it with\n" + "the button on the left). Ticked = pixels are divided by their\n" + "ADU/keV, so the PH spectra can be shown in keV."); + gainRow->addWidget(gainStateChk_); + gainRow->addStretch(1); + gainCol->addLayout(gainRow); + } + { + auto *gmRow = new QHBoxLayout(); + gmRow->addWidget(new QLabel("Gain map (keV PH):", gainGroup)); + gmRow->addWidget(btnGainMap); + gmRow->addWidget(kevChk_); + gmRow->addStretch(1); + gainCol->addLayout(gmRow); + } + gainCol->addWidget(gainMapVal_); + rightCol->addWidget(gainGroup); + arrGainDown_ = makeFlowArrow(QString::fromUtf8("\u2193"), central); + rightCol->addWidget(arrGainDown_, 0, Qt::AlignHCenter); + rightCol->addWidget(ctrlGroup); + + // ROI panel: selecting / loading / saving ROIs (the gate keeps its own + // "ROI only" capture and its ROI read-out next to the signal gate) + roiGroup_ = new QGroupBox("ROI", central); + { + auto *roiCol = new QVBoxLayout(roiGroup_); + btnSaveRoi = new QPushButton("Save ROI", roiGroup_); + btnLoadRoi = new QPushButton("Load ROI...", roiGroup_); + btnSaveRoi->setToolTip( + "Add the active gate ROI to the list kept in the program folder\n" + "(updates its timestamp if the same rectangle is already there)."); + btnLoadRoi->setToolTip( + "Pick a ROI from the list (most recently saved/loaded first) and\n" + "apply it to the gate. The list dialog can also delete entries."); + btnSelectRoi_ = new QPushButton("Select ROI", roiGroup_); + btnSelectRoi_->setCheckable(true); + btnSelectRoi_->setToolTip( + "Toggle the ROI editor: a yellow rectangle appears in the ADC\n" + "image. Drag inside it to move it, its edges/corners to resize it,\n" + "or drag on empty space to draw a new one. Selecting a ROI only\n" + "stores it: press \"ROI only\" next to the signal gate to restrict\n" + "the gate to it."); + roiCol->addWidget(btnSelectRoi_); + btnUnsetRoi_ = new QPushButton("Unset ROI", roiGroup_); + btnUnsetRoi_->setToolTip( + "Forget the current ROI: the gate and the saved data go back to the\n" + "full frame (the ROI list is not touched - use \"Load ROI...\" for\n" + "that)."); + roiCol->addWidget(btnUnsetRoi_); + auto *row = new QHBoxLayout(); + row->addWidget(btnSaveRoi); + row->addWidget(btnLoadRoi); + roiCol->addLayout(row); + roiListVal_ = makeValueLabel("no ROI in the list", roiGroup_); + roiCol->addWidget(roiListVal_); + } + rightCol->addWidget(roiGroup_); + // ROI -> Save: on the data path while "save frames" is selected (the + // colour follows the save mode, see updateFlow) + arrRoiDown_ = makeFlowArrow(QString::fromUtf8("\u2193"), central, FlowData); + rightCol->addWidget(arrRoiDown_, 0, Qt::AlignHCenter); + + // the save panel spans the whole window, below the flow but above the + // Status/Counters row + rootLayout->addWidget(saveGroup); + rootLayout->addLayout(bottomRow); + + btnQuit = new QPushButton("Quit (q)", central); + // first row of the right column, right aligned: out of the gate/arrow area + rightCol->insertWidget(0, btnQuit, 0, Qt::AlignRight); + // (Quit was added at the top left, above the gates) + rightCol->addStretch(1); + + // ── Signals: gate parameters ── + connect(gateChk_, &QCheckBox::toggled, this, [this](bool on) { + pipeline_.gate().setEnabled(on); + statusLabel->setText(on ? "Status: gate enabled" + : "Status: passthrough (gate off)"); + // the tracking works off the frames the gate drops, so switching the + // gate off turns it off too + if (!on && btnPedeTrack_ && btnPedeTrack_->isChecked()) + btnPedeTrack_->setChecked(false); + }); + connect(thrSpin_, QOverload::of(&QDoubleSpinBox::valueChanged), this, + [this](double v) { + if (!confirmGateChangeDuringRun( + QString("Signal-gate threshold -> %1 ADU").arg(v))) { + const QSignalBlocker b(thrSpin_); + thrSpin_->setValue( + static_cast(pipeline_.gate().threshold())); + return; + } + pipeline_.gate().setThreshold(static_cast(v)); + pipeline_.gate().resetCounters(); + }); + connect(minPixSpin_, QOverload::of(&QSpinBox::valueChanged), this, + [this](int v) { + if (!confirmGateChangeDuringRun( + QString("Min pixels above threshold -> %1").arg(v))) { + const QSignalBlocker b(minPixSpin_); + minPixSpin_->setValue( + static_cast(pipeline_.gate().minPixels())); + return; + } + pipeline_.gate().setMinPixels(static_cast(v)); + pipeline_.gate().resetCounters(); + }); + connect(contigChk_, &QCheckBox::toggled, this, [this](bool on) { + pipeline_.gate().setContiguous(on); + pipeline_.gate().resetCounters(); + }); + connect(hotChk_, &QCheckBox::toggled, this, [this](bool on) { + if (!confirmGateChangeDuringRun( + QString("Hot pixel suppression -> %1").arg(on ? "on" : "off"))) { + const QSignalBlocker b(hotChk_); + hotChk_->setChecked(pipeline_.gate().hotSuppress()); + return; + } + pipeline_.gate().setHotSuppress(on); + pipeline_.gate().resetCounters(); + }); + connect(btnGetHot_, &QPushButton::clicked, this, [this]() { + QDialog dlg(this); + dlg.setWindowTitle("Get hot pixels from the photon map"); + auto *form = new QFormLayout(&dlg); + form->addRow(new QLabel(QString("Photon map filled with %1 frames.") + .arg(static_cast( + pipeline_.pmapFrames())), + &dlg)); + auto *spin = new QDoubleSpinBox(&dlg); + spin->setRange(0.0, 1e9); + spin->setDecimals(1); + spin->setSingleStep(1.0); + // Default: a tenth of the frames the map was accumulated over, i.e. a + // pixel that fires in more than ~10 % of them. + const double def = + static_cast(pipeline_.pmapFrames()) / 10.0; + spin->setValue(def < 1.0 ? 1.0 : def); + form->addRow("Marking pixels above:", spin); + auto *bb = new QDialogButtonBox( + QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg); + form->addRow(bb); + connect(bb, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); + connect(bb, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); + if (dlg.exec() != QDialog::Accepted) + return; + pipeline_.setHotMaskFromPmap(spin->value()); + statusLabel->setText( + QString("Status: %1 hot pixels (photon map > %2)") + .arg(static_cast(pipeline_.hotPixels())) + .arg(spin->value(), 0, 'f', 1)); + }); + connect(btnResetHot_, &QPushButton::clicked, this, [this]() { + hotClearMask(); + pipeline_.gate().resetCounters(); + statusLabel->setText("Status: hot pixel mask cleared"); + }); + connect(btnResetCounters, &QPushButton::clicked, this, + [this]() { pipeline_.gate().resetCounters(); }); + connect(scCombo_, QOverload::of(&QComboBox::currentIndexChanged), + this, [this](int idx) { + // index 0 is "ALL" (no storage-cell gating), 1..16 are SC 0..15 + const int sc = (idx <= 0) ? -1 : idx - 1; + pipeline_.gate().setScFilter(sc); + statusLabel->setText( + sc < 0 ? "Status: all storage cells" + : QString("Status: only storage cell %1").arg(sc)); + }); + connect(btnGateRoi, &QPushButton::toggled, this, + [this](bool on) { toggleGateRoi(on); }); + connect(btnSaveStart, &QPushButton::toggled, this, + [this](bool on) { toggleRoiSave(on); }); + connect(saveModePhoton_, &QRadioButton::toggled, this, [this](bool on) { + gSaveMode = on ? 1 : 0; + pmapSaveSpin_->setEnabled(on); + // in photon mode the same spinbox budgets MAPS, so say so + if (saveBudgetLabel_) + saveBudgetLabel_->setText( + on ? "Number of photon maps to be saved:" + : "Number of frames to be saved:"); + // the correction only applies to the frames mode + if (saveCorrected_) + saveCorrected_->setEnabled(!on && + !pipeline_.saver().status().running); + updateFlow(); // the data-path arrows follow the mode + printf("[save] mode: %s\n", + on ? "virtual photon counting" : "frames"); + fflush(stdout); + statusLabel->setText(on + ? "Status: saving virtual photon counting maps" + : "Status: saving gate-passed frames"); + }); + connect(pmapSaveSpin_, QOverload::of(&QSpinBox::valueChanged), this, + [this](int v) { + if (v < 200) { + pmapSaveSpin_->setValue(200); + return; + } + gPmapSaveEvery = v; + }); + connect(dirBtn, &QPushButton::clicked, this, + [this]() { browseSaveDir(); }); + connect(btnSaveDest_, &QPushButton::clicked, this, + [this]() { saveDestDialog(); }); + connect(btnSelectRoi_, &QPushButton::toggled, this, + [this](bool on) { setRoiEditMode(on); }); + connect(btnUnsetRoi_, &QPushButton::clicked, this, + [this]() { unsetRoi(); }); + connect(btnSaveRoi, &QPushButton::clicked, this, [this]() { saveRoi(); }); + connect(btnLoadRoi, &QPushButton::clicked, this, + [this]() { loadRoiDialog(); }); + + // ── Signals: everything else ── + connect(btnStartPause, &QPushButton::clicked, this, + [this]() { toggleStartPause(); }); + connect(btnFaster, &QPushButton::clicked, this, [this]() { faster(); }); + connect(btnSlower, &QPushButton::clicked, this, [this]() { slower(); }); + connect(btnGainToggle, &QPushButton::toggled, this, + [this](bool on) { setApplyGain(on); }); + connect(btnBWPalette, &QPushButton::clicked, this, + [this]() { bwPalette1(); }); + connect(btnBWPalette2, &QPushButton::clicked, this, + [this]() { bwPalette2(); }); + connect(btnSetRanges, &QPushButton::clicked, this, + [this]() { showRangeDialog(); }); + connect(btnRates, &QPushButton::clicked, this, + [this]() { showRateDialog(); }); + connect(pmapCumulative_, &QCheckBox::toggled, this, [this](bool on) { + gPmapCumulative = on; + // "toggle to reset": either way the map starts from scratch, so the + // switch itself is the reset (no need to reach for Reset His) + resetHis(); + statusLabel->setText(on ? "Status: photon map cumulative (restarted)" + : "Status: photon map per image (restarted)"); + }); + connect(pmapEverySpin_, QOverload::of(&QSpinBox::valueChanged), this, + [this](int v) { + gPmapEvery = v > 0 ? v : 1; + statusLabel->setText( + QString("Status: one photon map every %1 analysed frames") + .arg(v)); + }); + connect(photonThrSpin_, QOverload::of(&QDoubleSpinBox::valueChanged), + this, [this](double v) { + // the photon map is filled by the analysis thread, which reads + // the atomic every frame; the multiplicity follows as 2 x v. + // Which one is being edited depends on the gain stage. + const bool kev = gain_flag.load(); + if (kev) + g_pheneThresholdKev = static_cast(v); + else + g_pheneThreshold = static_cast(v); + statusLabel->setText( + QString("Status: photon-map threshold %1 %2") + .arg(v, 0, 'f', kev ? 1 : 0) + .arg(kev ? "keV" : "ADU")); + }); + // "Pedestal corrected" decides whether the *saved* frames use the pedestal, + // which is what the orange route in the flow diagram shows + connect(saveCorrected_, &QCheckBox::toggled, this, [this](bool on) { + updateFlow(); + statusLabel->setText( + on ? "Status: saved frames pedestal corrected" + : "Status: saved frames raw (pedestal not subtracted)"); + }); + connect(btnResetHis, &QPushButton::clicked, this, + [this]() { resetHis(); }); + connect(btnResetAxis, &QPushButton::clicked, this, + [this]() { resetAxis(); }); + connect(btnColPal, &QPushButton::clicked, this, + [this]() { colPal(); }); + connect(btnLoadPede, &QPushButton::clicked, this, + [this]() { acquirePede(); }); + connect(btnSavePede, &QPushButton::clicked, this, + [this]() { savePede(); }); + connect(btnReadPede, &QPushButton::clicked, this, + [this]() { readPede(); }); + connect(btnLoadAllPede, &QPushButton::clicked, this, + [this]() { loadAllPede(); }); + connect(btnPedeTrack_, &QPushButton::toggled, this, [this](bool on) { + // needs the signal gate: without it "dropped" has no meaning + if (on && gateChk_ && !gateChk_->isChecked()) { + QMessageBox::information( + this, "Pedestal tracking", + "Pedestal tracking follows the frames the signal gate drops, " + "so it needs \"Gate enabled\" on."); + btnPedeTrack_->setChecked(false); + return; + } + gPedeTracking = on; + printf("[pedetrack] %s\n", on ? "on" : "off"); + fflush(stdout); + statusLabel->setText(on ? "Status: pedestal tracking on" + : "Status: pedestal tracking off"); + updateFlow(); + }); + connect(btnNoPede_, &QPushButton::toggled, this, [this](bool on) { + gNoPede = on; + updateFlow(); + statusLabel->setText(on ? "Status: pedestal NOT subtracted (raw ADC)" + : "Status: pedestal subtraction on"); + }); + connect(btnGainMap, &QPushButton::clicked, this, + [this]() { loadGainMap(); }); + connect(kevChk_, &QCheckBox::toggled, this, [this](bool on) { + kevSpectra = on; + forceRootRedraw(); + updateFlow(); // the Gain panel is in use when keV is shown + if (on && !gainMapLoaded()) { + if (gainMapVal_) + gainMapVal_->setText( + "no gain map selected - using nominal values " + "(40 ADU/keV)"); + statusLabel->setText( + "Status: PH spectra in keV (nominal 40 ADU/keV, no gain map)"); + } else { + updateGainMapLabel(); + statusLabel->setText(on ? "Status: PH spectra in keV" + : "Status: PH spectra in ADU"); + } + }); + connect(btnQuit, &QPushButton::clicked, this, [this]() { quitApp(); }); + + // ── Fast image views (separate top-level windows, as in the original) ── + m_adcView = new FastImageView(); + m_adcView->setPaletteLut(paletteLut(Palette::Color)); + if (m_adcScale_) + m_adcScale_->setPaletteChoice(0); // match the ROOT plots + // the ADC (main) image opens on the ADC range of the Set Ranges dialog + m_adcView->setColorRange(adcmin.load(), adcmax.load()); + m_adcWin = new QWidget(nullptr, Qt::Window); + m_adcWin->setWindowTitle("ADC Image"); + m_adcWin->resize(1024 + 80, 512 + 40 + 90); // + the scale panel + auto *adcLay = new QVBoxLayout(m_adcWin); + adcLay->setContentsMargins(0, 0, 0, 0); + adcLay->addWidget(m_adcView, 1); + m_adcScale_ = new ScalePanel(m_adcView, m_adcWin); + m_adcView->onScaleReleased = [this]() { m_adcScale_->refresh(); }; + m_adcView->onRoiEdited = [this](int x0, int y0, int x1, int y1) { + applyRoi(x0, y0, x1 - x0, y1 - y0, "ADC image", false); + }; + adcLay->addWidget(m_adcScale_, 0); + m_adcWin->show(); + + m_gainView = new FastImageView(); + // The gain image has four valid values, so it gets a fixed four-colour code + // (0 blue, 1 green, 2 yellow, 3 red, all desaturated) and no scale panel: + // there is no colour scale to choose and no distribution worth showing. + m_gainView->setPaletteLut(paletteLut(Palette::Gain4)); + m_gainView->setIntegerValues(true); + m_gainView->setColorRange(0, 3); + m_gainWin = new QWidget(nullptr, Qt::Window); + m_gainWin->setWindowTitle("Gain Image"); + m_gainWin->resize(1024 + 80, 512 + 40); // no scale panel here + auto *gainLay = new QVBoxLayout(m_gainWin); + gainLay->setContentsMargins(0, 0, 0, 0); + gainLay->addWidget(m_gainView, 1); + m_gainWin->hide(); // hidden by default: "show gain image" in Display + + m_photonView = new FastImageView(); + m_photonView->setPaletteLut(paletteLut(Palette::Color)); + m_photonView->setZeroWhite(true); // 0 photons = white, like the ROOT plot + m_photonView->setColorRange(0, 8); // photons per pixel over one image + m_photonWin = new QWidget(nullptr, Qt::Window); + m_photonWin->setWindowTitle("Photon Map (virtual photon counting)"); + m_photonWin->resize(1024 + 80, 512 + 40 + 90); + auto *photonLay = new QVBoxLayout(m_photonWin); + photonLay->setContentsMargins(0, 0, 0, 0); + m_photonView->setIntegerValues(true); // photon counts: whole numbers + photonLay->addWidget(m_photonView, 1); + m_photonScale_ = new ScalePanel(m_photonView, m_photonWin); + m_photonView->onScaleReleased = [this]() { m_photonScale_->refresh(); }; + photonLay->addWidget(m_photonScale_, 0); + m_photonWin->show(); + +#ifdef JFSTRX + // ── Separate strixel window: the six ADC groups, arranged like the + // original ROOT ADC canvas (left column chip1 g3/g2/g1, right column + // chip6 g1/g2/g3). Display only: the gate ROI is selected on the raw + // ADC image, which stays unmapped so the coordinates are raw pixels. + m_adcPanelWin = new QWidget(nullptr, Qt::Window); + m_adcPanelWin->setWindowTitle("ADC Strixel Panels (JFSTRX)"); + m_adcPanelWin->resize(900, 900); + auto *panelOuter = new QVBoxLayout(m_adcPanelWin); + panelOuter->setContentsMargins(4, 2, 4, 2); + panelOuter->setSpacing(2); + m_adcPanelZ = new QLabel(m_adcPanelWin); + m_adcPanelZ->setAlignment(Qt::AlignCenter); + m_adcPanelZ->setToolTip( + "The six strixel panels use the ADC image's Z scale (colour range and\n" + "palette): change it in the ADC window's scale panel and they follow."); + panelOuter->addWidget(m_adcPanelZ); + auto *panelRow = new QHBoxLayout(); + panelOuter->addLayout(panelRow, 1); + panelRow->setContentsMargins(2, 2, 2, 2); + auto *panelCol0 = new QVBoxLayout(); + auto *panelCol1 = new QVBoxLayout(); + panelRow->addLayout(panelCol0); + panelRow->addLayout(panelCol1); + QVBoxLayout *panelCol[2] = {panelCol0, panelCol1}; + // panel index order: 0..2 = chip1 g1/g2/g3, 3..5 = chip6 g1/g2/g3 + static const int panelOrder[2][3] = {{2, 1, 0}, {3, 4, 5}}; // top -> bottom + static const int panelStretch[2][3] = {{5, 2, 2}, {2, 2, 5}}; + // mapped-pixel aspect X/Y: X = 75*(75/Y), Y = 25/15/18.75 um + // -> 225/25 = 9, 375/15 = 25, 300/18.75 = 16 + static const double panelAspect[kNumAdcPanels] = {9.0, 25.0, 16.0, + 9.0, 25.0, 16.0}; + static const char *panelTitle[kNumAdcPanels] = { + "chip 1 group 1 (25um)", "chip 1 group 2 (15um)", + "chip 1 group 3 (18.75um)", "chip 6 group 1 (25um)", + "chip 6 group 2 (15um)", "chip 6 group 3 (18.75um)"}; + for (int col = 0; col < 2; col++) { + for (int k = 0; k < 3; k++) { + const int p = panelOrder[col][k]; + auto *cell = new QWidget(); + auto *cv = new QVBoxLayout(cell); + cv->setContentsMargins(0, 0, 0, 0); + cv->setSpacing(0); + auto *lab = new QLabel(panelTitle[p], cell); + lab->setAlignment(Qt::AlignCenter); + cv->addWidget(lab); + auto *v = new FastImageView(); + v->setPaletteLut(paletteLut(Palette::Color)); + v->setColorRange(adcmin.load(), adcmax.load()); + v->setPixelAspectX(panelAspect[p]); + cv->addWidget(v, 1); + panelCol[col]->addWidget(cell, panelStretch[col][k]); + m_adcPanelView[p] = v; + } + } + // The strixel panels have no scale of their own: they follow the ADC + // image's colour range and palette, which is where the Z scale lives. + auto syncPanels = [this]() { + double lo = 0.0, hi = 1.0; + m_adcView->colorRange(lo, hi); + for (auto *v : m_adcPanelView) + if (v) + v->setColorRange(lo, hi); + if (m_adcPanelZ) + m_adcPanelZ->setText( + QString("Z scale: follows the ADC image (Z: %1 .. %2)") + .arg(lo, 0, 'g', 5) + .arg(hi, 0, 'g', 5)); + }; + m_adcView->onColorRangeChanged = [syncPanels](double, double) { + syncPanels(); + }; + // only the ADC view propagates: the panels must not re-emit (that would + // bounce the palette back and forth for ever) + m_adcView->onPaletteChanged = [this](const QRgb *lut) { + for (auto *w : m_adcPanelView) + if (w) + w->setPaletteLut(lut); + }; + syncPanels(); + m_adcPanelWin->show(); +#endif + + // Master-slave zoom: the ADC view is the master; the gain view follows + // immediately (same thread) and the ROOT photon map follows through a + // coalesced command, so only the newest viewport is ever redrawn. + // all three image windows report their zoom; zoomTo() decides what follows + auto wireZoom = [this](FastImageView *v) { + if (!v) + return; + v->onZoomChanged = [this, v](double x1, double y1, double x2, + double y2) { zoomTo(v, x1, y1, x2, y2); }; + }; + wireZoom(m_adcView); + wireZoom(m_gainView); + wireZoom(m_photonView); + + // ── Timers (started by start()) ── + processTimer = new QTimer(this); + connect(processTimer, &QTimer::timeout, this, + [this]() { processImages(); }); + statusTimer = new QTimer(this); + connect(statusTimer, &QTimer::timeout, this, [this]() { updateStatus(); }); + sigTimer = new QTimer(this); + connect(sigTimer, &QTimer::timeout, this, [this]() { checkSignals(); }); + + updateFlow(); + + updateDrawLabel(); +} + +void MainWindow::connectShortcuts() { + auto sc = [this](const char *key, const std::function &fn) { + QShortcut *s = new QShortcut(QKeySequence(QString::fromLatin1(key)), + this); + connect(s, &QShortcut::activated, this, fn); + }; + sc("s", [this]() { toggleStartPause(); }); + sc("+", [this]() { faster(); }); + sc("=", [this]() { faster(); }); + sc("-", [this]() { slower(); }); + sc("g", [this]() { toggleGain(); }); + sc("b", [this]() { bwPalette1(); }); + sc("Shift+B", [this]() { bwPalette2(); }); + sc("c", [this]() { colPal(); }); + sc("i", [this]() { btnGateRoi->setChecked(!btnGateRoi->isChecked()); }); + sc("e", [this]() { + // keV PH (e for energy): keeps its tick while the gain is off, see + // updateFlow. Not "k": that is already Load ROI. + if (kevChk_) + kevChk_->setChecked(!kevChk_->isChecked()); + }); + sc("n", [this]() { btnSaveStart->setChecked(!btnSaveStart->isChecked()); }); + sc("j", [this]() { saveRoi(); }); + sc("k", [this]() { loadRoiDialog(); }); + sc("r", [this]() { resetHis(); }); + sc("Shift+R", [this]() { resetAxis(); }); + sc("p", [this]() { acquirePede(); }); + sc("Shift+O", [this]() { savePede(); }); + sc("o", [this]() { readPede(); }); + sc("Shift+P", [this]() { loadAllPede(); }); + sc("q", [this]() { quitApp(); }); + sc("Shift+S", [this]() { showRangeDialog(); }); +} + +// --------------------------------------------------------------------------- +// lifecycle +// --------------------------------------------------------------------------- +void MainWindow::start() { + // The ROOT canvases are hidden by default ("show ROOT plots" in Display); + // the command is queued here because the ROOT thread runs by now. + { + RootCmd c; + c.type = RootCmdType::ShowPlots; + c.a = 0.0; + pushRootCmd(c); + } + // Pedestals must be installed before the threads start reading frames. + std::string perr; + if (pathExists(g_cfg.pedeFile)) { + if (pedeLoadFile(g_cfg.pedeFile, perr)) { + printf("[pede] loaded %s\n", g_cfg.pedeFile.c_str()); + pedeVal_->setText(QString("loaded (%1)").arg( + QString::fromStdString(g_cfg.pedeFile))); + pipeline_.gate().setLastError(""); + } else { + fprintf(stderr, "[pede] %s\n", perr.c_str()); + pipeline_.gate().setLastError("loadPedestal: " + perr); + } + } else if (g_cfg.pedeExplicit) { + fprintf(stderr, "[pede] file not found: %s\n", g_cfg.pedeFile.c_str()); + pipeline_.gate().setLastError("loadPedestal: file not found: " + + g_cfg.pedeFile); + } else { + printf("[pede] %s not found, running on raw values\n", + g_cfg.pedeFile.c_str()); + } + + if (!g_cfg.gainMapFile.empty()) { + std::string gerr; + if (gainMapLoadFile(g_cfg.gainMapFile, gerr)) { + if (gainMapVal_) + gainMapVal_->setText(QFileInfo(QString::fromStdString( + g_cfg.gainMapFile)) + .fileName()); + if (kevChk_) + kevChk_->setChecked(true); // show what was just calibrated + updateFlow(); + } else { + fprintf(stderr, "[gain] %s\n", gerr.c_str()); + pipeline_.gate().setLastError("gainMap: " + gerr); + } + } + + std::string err; + if (!pipeline_.init(err)) { + fprintf(stderr, "FATAL: %s\n", err.c_str()); + pipeline_.gate().setLastError(err); + statusLabel->setText("Status: socket error"); + QMessageBox::critical(this, "ZMQ", QString::fromStdString(err)); + } else { + pipeline_.start(); + pipeline_.gate().setScFilter(g_cfg.scFilter); + if (scCombo_) + scCombo_->setCurrentIndex(g_cfg.scFilter < 0 ? 0 + : g_cfg.scFilter + 1); + statusLabel->setText("Status: running"); + if (g_cfg.acquirePede) + pipeline_.requestPedeAcquire(g_cfg.pedeFrames); + if (g_cfg.gateRoiX >= 0) { // --gate-roi: same path as the "ROI only" + // button, so the box and the gate agree + applyRoi(g_cfg.gateRoiX, g_cfg.gateRoiY, g_cfg.gateRoiW, + g_cfg.gateRoiH, "command line", /*restrict=*/true); + // reflect the restriction in the button, so the first press turns + // it off instead of toggling the wrong way (m_applyingRoi keeps the + // toggle handler from re-capturing or warning) + if (btnGateRoi && pipeline_.gate().roiActive() && + !btnGateRoi->isChecked()) { + m_applyingRoi = true; + btnGateRoi->setChecked(true); + m_applyingRoi = false; + } + } + } + + startRootThread(); + processTimer->start(30); + statusTimer->start(500); + sigTimer->start(200); + + // --apply-gain: same as pressing the Gain button (it also swaps the photon + // threshold to keV and relabels its row) + if (g_cfg.applyGain) + btnGainToggle->setChecked(true); + + // --start-save: press the Save button for scripted/headless runs. The + // check triggers toggleRoiSave() exactly like the GUI click. + if (g_cfg.startSave) { + printf("[save] --start-save: starting the %s writer\n", + gSaveMode.load() == 1 ? "virtual photon counting" + : "frame"); + fflush(stdout); + btnSaveStart->setChecked(true); + } + + signal(SIGINT, handleSigint); + signal(SIGTERM, handleSigint); +} + +void MainWindow::stop() { + if (processTimer) + processTimer->stop(); + if (statusTimer) + statusTimer->stop(); + if (sigTimer) + sigTimer->stop(); + { + const RoiSaver::Status st = pipeline_.saver().status(); + if (st.running) { + printf("[shutdown] flushing the ROI save (%llu frames, %s)...\n", + (unsigned long long)st.saved, st.h5Path.c_str()); + fflush(stdout); + if (statusLabel) + statusLabel->setText( + "Status: flushing the ROI save before exit..."); + } + } + pipeline_.saver().requestStop("program exit"); + pipeline_.saver().finish(); // drain the queue, close the file, write JSON + pipeline_.stop(); + stopRootThread(); +} + +// Question asked before quitting (both from the Quit button/shortcut and from +// closing the window). A running ROI save is called out explicitly, because +// quitting flushes it: the writer drains its queue, closes the HDF5 file and +// writes the JSON sidecar before the program exits. +bool MainWindow::confirmQuit() { + const RoiSaver::Status st = pipeline_.saver().status(); + QString text = "Quit the online display?"; + if (st.running) { + text += QString("\n\nA ROI save is still running: %1 frames written " + "to\n%2\n\nIt will be stopped and flushed (queued " + "frames written, file closed, JSON sidecar written) " + "before the program exits.") + .arg(static_cast(st.saved)) + .arg(QString::fromStdString(st.h5Path)); + } else if (!st.h5Path.empty()) { + text += QString("\n\nThe last ROI save is closed:\n%1\n(%2 frames)") + .arg(QString::fromStdString(st.h5Path)) + .arg(static_cast(st.saved)); + } + const QMessageBox::StandardButton answer = QMessageBox::question( + this, "Quit", text, QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); + printf("[quit] %s\n", answer == QMessageBox::Yes ? "confirmed" + : "cancelled"); + fflush(stdout); + return answer == QMessageBox::Yes; +} + +// Sizes the two empty pads that turn the panels into a flow chart: +// * the gates start low enough that the middle of "1. Storage cell" sits on +// the top edge of the Pedestal box, and +// * the Gain box starts at the middle of "2. Signal gate". +// The only thing that matters is that the two columns stay top-aligned, which +// they do, so this runs once at start-up and again if the window is resized +// wider/narrower (text wrapping can change the heights). +void MainWindow::layoutFlow() { + if (!centralWidget() || !leftPad_ || !gainPad_ || !scGroup_ || + !sigGroup_ || !pedeGroup_ || !gainGroup_) + return; + static bool inLayout = false; + if (inLayout) + return; // setFixedHeight() re-enters through resizeEvent() + inLayout = true; + flowQueued_ = false; // this call is the queued one, if it was + + // The first call comes before Qt has computed the layout, when every box + // still has a token size; solving from that produced a huge pad and, with + // it, a window minimum that stayed large for the rest of the session. + if (scGroup_->height() < 40 || sigGroup_->height() < 40 || + pedeGroup_->height() < 40 || gainGroup_->height() < 40) { + inLayout = false; + return; + } + + // Iterative, because every change alters what is being measured (boxes + // grow when their labels wrap, and the pads move the boxes). Three knobs: + // leftPad - pushes the gates down until the storage-cell middle is on + // the top edge of the Pedestal + // gatePad - the gap *between* the two gates: stretching it moves the + // signal gate down without touching the storage cell + // gainPad - fine adjustment so the Gain starts at the signal-gate middle + const int arrowMin = 10; // keep the arrows visible + leftPad_->setFixedHeight(flowPads_[0]); + pedePad_->setFixedHeight(flowPads_[1]); + gainPad_->setFixedHeight(flowPads_[2]); + if (gatePad_) + gatePad_->setFixedHeight(flowPads_[3]); + if (QLayout *l = centralWidget()->layout()) + l->activate(); + + { + const int scMid = scGroup_->mapTo(centralWidget(), + QPoint(0, scGroup_->height() / 2)) + .y(); + const int sigMid = sigGroup_->mapTo(centralWidget(), + QPoint(0, sigGroup_->height() / 2)) + .y(); + const int pedeTop = + pedeGroup_->mapTo(centralWidget(), QPoint(0, 0)).y(); + const int gainTop = + gainGroup_->mapTo(centralWidget(), QPoint(0, 0)).y(); + + // storage-cell middle must sit on the Pedestal's top edge: move + // whichever of the two has room (the Pedestal down, or the gates down) + int wantLeft = flowPads_[0], wantPede = flowPads_[1]; + const int errPede = scMid - pedeTop; // >0: the Pedestal is too high + if (errPede > 0) + wantPede += errPede; + else + wantLeft += -errPede; + if (wantLeft < 0) + wantLeft = 0; + if (wantPede < 0) + wantPede = 0; + // The Gain must start at the signal gate's middle. If it sits too + // high, push it down with gainPad; if it sits too low, push the signal + // gate down instead by stretching the arrow between the gates (the + // storage cell above is not affected by that arrow). + const int err = sigMid - gainTop; // >0: the gain is too high + int wantArrow = flowPads_[3]; + int wantGain = flowPads_[2]; + if (err > 0) { + wantGain += err; // the Gain sits above the signal middle + } else if (err < 0) { + wantArrow += -err; // below it: push the signal gate down instead + wantGain = 0; + } + // err == 0: aligned, leave both pads alone (resetting gainPad here was + // what made the solver oscillate between aligned and 50 px off) + if (wantArrow < arrowMin) + wantArrow = arrowMin; + if (wantGain < 0) + wantGain = 0; + + const bool converged = wantLeft == flowPads_[0] && + wantPede == flowPads_[1] && + wantArrow == flowPads_[3] && + wantGain == flowPads_[2]; + flowPads_[0] = wantLeft; + flowPads_[1] = wantPede; + flowPads_[2] = wantGain; + flowPads_[3] = wantArrow; + if (!converged && getenv("JF_TIMING")) + printf("[flow] pass %d: left %d pede %d gain %d gateArrow %d | " + "scMid %d sigMid %d pedeTop %d gainTop %d\n", + flowPass_, wantLeft, wantPede, wantGain, wantArrow, scMid, + sigMid, pedeTop, gainTop); + if (!converged) { + // re-measure on the next event-loop turn: within one call the + // widget positions are not guaranteed to have settled yet, and + // solving from stale numbers compounds the corrections + flowPass_++; + if (flowPass_ < 12 && !flowQueued_) { + flowQueued_ = true; + QTimer::singleShot(0, this, &MainWindow::layoutFlow); + } + } else { + flowPass_ = 0; + } + } + leftPad_->setFixedHeight(flowPads_[0]); + pedePad_->setFixedHeight(flowPads_[1]); + gainPad_->setFixedHeight(flowPads_[2]); + if (gatePad_) + gatePad_->setFixedHeight(flowPads_[3]); + if (QLayout *l = centralWidget()->layout()) + l->activate(); + + // place the strip over the gap between the two columns, spanning from the + // top of the first box to the bottom of the last one + if (strip_ && scGroup_ && sigGroup_ && pedeGroup_ && gainGroup_) { + const int x0 = scGroup_->mapTo(centralWidget(), QPoint(0, 0)).x() + + scGroup_->width() + 1; + const int x1 = + pedeGroup_->mapTo(centralWidget(), QPoint(0, 0)).x() - 1; + int top = scGroup_->mapTo(centralWidget(), QPoint(0, 0)).y(); + int bot = pedeGroup_->mapTo(centralWidget(), QPoint(0, 0)).y() + + pedeGroup_->height(); + const int gainBot = gainGroup_->mapTo(centralWidget(), QPoint(0, 0)).y() + + gainGroup_->height(); + if (gainBot > bot) + bot = gainBot; + if (roiGroup_) { + const int roiBot = + roiGroup_->mapTo(centralWidget(), QPoint(0, 0)).y() + + roiGroup_->height(); + if (roiBot > bot) + bot = roiBot; + } + if (x1 > x0 && bot > top) + strip_->setGeometry(x0, top - 6, x1 - x0, bot - top + 12); + strip_->raise(); + strip_->show(); + } + + // hand the strip its connectors, in its own coordinates + if (strip_ && scGroup_ && sigGroup_ && pedeGroup_ && gainGroup_ && + pmapGroup_ && displayGroup_) { + auto yIn = [&](QWidget *w, int y) { + return w->mapTo(centralWidget(), QPoint(0, 0)).y() + y; + }; + const int stripTop = + strip_->mapTo(centralWidget(), QPoint(0, 0)).y(); + // The signal gate does not change the data, so nothing enters or + // leaves its box: the flow runs from the previous stage to the Gain and + // simply passes the gate, which is drawn as a funnel on that line (the + // gate panel only points at the funnel - see FlowStrip::drawFunnel). + const int funnelY = + yIn(sigGroup_, sigGroup_->height() / 2) - stripTop; + FlowStrip::Path a, b, c; + // 1. storage cell (middle) -> Pedestal (middle): enter from the left + a.yFrom = yIn(scGroup_, scGroup_->height() / 2) - stripTop; + a.yTo = yIn(pedeGroup_, pedeGroup_->height() / 2) - stripTop; + a.dir = +1; + a.cls = FlowPedestal; // into the Pedestal + // Pedestal (bottom) -> the funnel, travelling leftwards (no head: it + // merges into the funnel) + b.yFrom = yIn(pedeGroup_, pedeGroup_->height()) - stripTop; + b.yTo = funnelY; + b.dir = -1; + b.x = FlowStrip::kFunnelX; + b.head = false; + // the funnel -> Gain (top): the flow continues after the gate + c.yFrom = funnelY; + c.yTo = yIn(gainGroup_, 0) - stripTop; + c.dir = +1; + c.x = FlowStrip::kFunnelX; + c.startAtJog = true; + // Gain -> ROI: a rail just left of the right column, so it does not + // have to cross the Display panel. Frames are the ROI-cropped + // product, so this is the frames-mode input of the ROI. + FlowStrip::Path d; + d.yFrom = yIn(gainGroup_, gainGroup_->height() / 2) - stripTop; + d.yTo = roiGroup_ ? (yIn(roiGroup_, roiGroup_->height() / 2) - stripTop) + : 0; + d.dir = 2; + d.on = roiGroup_ != nullptr; + d.cls = FlowData; // corrected when setSaveMode() knows the mode + // the gate block -> Pedestal tracking: the tracker lives off the frames + // the gate *drops*, so it leaves the gate block's top-right corner + FlowStrip::Path e; + e.yFrom = yIn(sigGroup_, 6) - stripTop; + e.yTo = pedeTrackGroup_ + ? (yIn(pedeTrackGroup_, pedeTrackGroup_->height() / 2) - + stripTop) + : 0; + e.dir = +1; + e.on = pedeTrackGroup_ != nullptr; + e.cls = FlowPedestal; // the tracker is the pedestal's own feedback + // Gain -> Virtual photon counting: the calibration reaches the counting + // (its threshold is keV while the gain correction is on). The map is + // built in the left column, so the connector travels leftwards. + FlowStrip::Path f; + f.yFrom = yIn(gainGroup_, gainGroup_->height() - 8) - stripTop; + f.yTo = pmapGroup_ ? (yIn(pmapGroup_, 24) - stripTop) : 0; + f.dir = -1; + f.on = pmapGroup_ != nullptr; + f.cls = FlowNormal; // flipped by setSaveMode() + // Virtual photon counting -> Display: the map is shown (in its own + // window), so this one travels rightwards into the Display. + FlowStrip::Path g; + g.yFrom = pmapGroup_ + ? (yIn(pmapGroup_, pmapGroup_->height() - 8) - stripTop) + : 0; + g.yTo = displayGroup_ + ? (yIn(displayGroup_, displayGroup_->height() / 2) - stripTop) + : 0; + g.dir = +1; + g.on = pmapGroup_ != nullptr && displayGroup_ != nullptr; + // counting -> ROI: with the maps as the saved product this is the + // input of the ROI (they are cropped to it on the way out) + FlowStrip::Path i; + i.yFrom = yIn(pmapGroup_, pmapGroup_->height() - 8) - stripTop; + i.yTo = roiGroup_ ? (yIn(roiGroup_, roiGroup_->height() / 2) - stripTop) + : 0; + i.dir = +1; + i.cls = FlowData; + i.on = roiGroup_ != nullptr; + // raw mode: the storage cell feeds the funnel directly, so the flow + // does not have to pass through the (greyed) Pedestal box. Both + // feeders are always drawn; setStages() decides which one is live. + FlowStrip::Path h; + h.yFrom = yIn(scGroup_, scGroup_->height()) - stripTop; + h.yTo = funnelY; + h.dir = +1; + h.x = FlowStrip::kFunnelX; + h.head = false; // merges into the funnel + strip_->setPaths(a, b, c, d, e, f, g, h, i, funnelY); + bool dp, sp, go, pm; + flowStageStates(dp, sp, go, pm); + strip_->setStages(dp, sp, go, pm); + } + inLayout = false; + + if (getenv("JF_TIMING")) { + printf("[flow] pads: gates %d pede %d gain %d gateArrow %d (passes " + "%d)\n", + flowPads_[0], flowPads_[1], flowPads_[2], flowPads_[3], + flowPass_); + auto rect = [&](QWidget *w) { + return w ? QString("%1,%2 %3x%4") + .arg(w->mapTo(centralWidget(), QPoint(0, 0)).x()) + .arg(w->mapTo(centralWidget(), QPoint(0, 0)).y()) + .arg(w->width()) + .arg(w->height()) + .toStdString() + : std::string("-"); + }; + printf("[rect] sc %s | signal %s | pede %s | track %s | gain %s | " + "roi %s\n", + rect(scGroup_).c_str(), rect(sigGroup_).c_str(), + rect(pedeGroup_).c_str(), rect(pedeTrackGroup_).c_str(), + rect(gainGroup_).c_str(), rect(roiGroup_).c_str()); + printf("[rect] gainToggle %s | keVchk %s | noPede %s | saveStart %s | " + "selectRoi %s | saveRoi %s | quit %s | track %s | pmap %s\n", + rect(btnGainToggle).c_str(), rect(kevChk_).c_str(), + rect(btnNoPede_).c_str(), rect(btnSaveStart).c_str(), + rect(btnSelectRoi_).c_str(), rect(btnSaveRoi).c_str(), + rect(btnQuit).c_str(), rect(btnPedeTrack_).c_str(), + rect(pmapGroup_).c_str()); + printf("[rect] unsetRoi %s | selectRoi %s | thr %s | minPix %s | " + "zoomCoord %s\n", + rect(btnUnsetRoi_).c_str(), rect(btnSelectRoi_).c_str(), + rect(thrSpin_).c_str(), rect(minPixSpin_).c_str(), + rect(zoomCoordChk_).c_str()); + printf("[rect] saveFramesRadio %s | savePhotonRadio %s | " + "saveCorrected %s\n", + rect(saveModeFrames_).c_str(), rect(saveModePhoton_).c_str(), + rect(saveCorrected_).c_str()); + } + // The pads and the alignment moves push the panels around *after* the first + // paint; repaint the column area so the space they leave behind does not + // keep the old pixels (that is what left a piece of the "Virtual photon + // counting" title floating next to the Save box). + if (centralWidget()) + centralWidget()->update(); +} + +// Tells the overlay which boxes are in the chain right now: a stage that is +// switched off is skipped, so the arrow steps over it. +// What the ROI panel shows: the ROI the gate is using and how many are saved. +void MainWindow::updateRoiLabel() { + if (!roiListVal_) + return; + QString act = "no ROI selected"; + if (pipeline_.gate().roiSet()) { + int x0 = 0, y0 = 0, x1 = 0, y1 = 0; + pipeline_.gate().roiSnapshot(x0, y0, x1, y1); + act = QString("active %1x%2 at %3,%4") + .arg(x1 - x0) + .arg(y1 - y0) + .arg(x0) + .arg(y0); + } + roiListVal_->setText(QString("%1 | gate: %2 | %3 saved") + .arg(act) + .arg(pipeline_.gate().roiActive() ? "ROI only" + : "full frame") + .arg(roiListLoad().size())); +} + +// One place that says what the keV conversion is using. +void MainWindow::updateGainMapLabel() { + if (!gainMapVal_) + return; + if (!gain_flag.load()) + gainMapVal_->setText("gain off: PH spectra in ADU"); + else if (gainMapLoaded()) + gainMapVal_->setText(QFileInfo(QString::fromStdString(g_cfg.gainMapFile)) + .fileName()); + else if (kevChk_ && kevChk_->isChecked()) + gainMapVal_->setText( + "no gain map selected - using nominal values (40 ADU/keV)"); + else + gainMapVal_->setText("no gain map loaded : using defaults"); +} + +void MainWindow::updateFlow() { + const bool pede = PedestalStore::instance().loaded() && !gNoPede.load(); + const bool gain = gain_flag.load(); + const bool scAll = !scCombo_ || scCombo_->currentIndex() <= 0; + const bool gateOn = !gateChk_ || gateChk_->isChecked(); + // The arrows are static: the chain is always drawn the same way, and a + // stage that is not in use is greyed out instead, so the bypass is obvious + // without the arrows moving. + // the stage's own switch stays black: Pede/Raw, Gate enabled and the gain + // button are how a bypassed stage is switched back on + greyStage(pedeGroup_, !pede, {btnNoPede_}); + // the Gain panel follows the gain toggle alone; the keV checkbox keeps its + // tick for next time but is greyed while the gain is off, and the plots + // fall back to ADU + greyStage(gainGroup_, !gain, {btnGainToggle}); + if (kevChk_) + kevChk_->setEnabled(gain); + if (gainStateChk_) + gainStateChk_->setChecked(gain); // read-only indicator, no signals + // a gate that is switched off greys out exactly like a bypassed stage + // Greyed when it cannot be *selected* (the tracker needs the signal gate), + // NOT when it is merely inactive: the label under the button says whether + // tracking is on, so the box must not go grey just because it is off. + greyStage(pedeTrackGroup_, !gateOn); + greyStage(scGroup_, scAll, {scCombo_}); // storage-cell filter == ALL + greyStage(sigGroup_, !gateOn, {gateChk_}); // signal gate off == passthrough + // photon counting needs a pedestal (it thresholds the pede-subtracted + // ADC), so the whole virtual-photon-counting box is a bypassed stage when + // the pedestal is out of the chain - the display map goes empty with it + greyStage(pmapGroup_, !pede); + // pedestal tracking only makes sense with the signal gate on: enable the + // button here, but do not flip it (the gate's own handler does that when it + // is switched off, so --pede-tracking survives start-up) + if (btnPedeTrack_) + btnPedeTrack_->setEnabled(gateOn); + if (pedeTrackVal_) { + const PedeTrackStats ts = pedeTrackerStats(); + if (!ts.on) + pedeTrackVal_->setText("off"); + else + pedeTrackVal_->setText( + QString("on: %1 samples, delta %2 ADU, %3 updates") + .arg(static_cast(ts.samples)) + .arg(ts.maxDiff, 0, 'f', 1) + .arg(static_cast(ts.updates))); + } + // the gate threshold is applied to the raw 14-bit value when no pedestal + // is in play, so say so instead of claiming "pede-subtracted" + if (thrLabel_) { + const bool raw = gNoPede.load() || !PedestalStore::instance().loaded(); + thrLabel_->setText(raw ? "Threshold (raw ADC - no pede):" + : "Threshold (ADC, pede-subtracted):"); + } + // The data path (orange) follows the save mode: frames are written from the + // ROI, photon maps from the virtual photon counter. Only the colours + // change - the connectors themselves never move. + { + // only the branch that actually feeds the file is drawn, and it is + // orange - the frames come from the ROI, the maps from the counting + bool pmap = false, savePede = false; + bool dPede, gOn; + flowStageStates(dPede, savePede, gOn, pmap); + // the file is always entered through the ROI: the maps are cropped to + // it on the way out, so this arrow never moves and is always orange + if (arrRoiDown_) { + setFlowArrowClass(arrRoiDown_, FlowData); + arrRoiDown_->setVisible(true); + } + if (strip_) + strip_->setStages(dPede, savePede, gOn, pmap); + } + updateGainMapLabel(); + updateRoiLabel(); + if (noPedeVal_) { + if (gNoPede.load()) + noPedeVal_->setText("no pede: raw ADC"); + else if (PedestalStore::instance().loaded()) + noPedeVal_->setText("pede subtracted"); + else + noPedeVal_->setText("no pedestal loaded"); + } +} + +void MainWindow::showEvent(QShowEvent *event) { + QMainWindow::showEvent(event); + flowPass_ = 0; + layoutFlow(); +} + +void MainWindow::resizeEvent(QResizeEvent *event) { + QMainWindow::resizeEvent(event); + if (!flowQueued_) + flowPass_ = 0; // a fresh solve for the new geometry + layoutFlow(); +} + +void MainWindow::closeEvent(QCloseEvent *event) { + if (!confirmQuit()) { + event->ignore(); // the operator changed their mind + return; + } + stop(); + closeSideWindows(); + event->accept(); + // ...and end the program even if a side window somehow refused to close + QApplication::quit(); +} + +// --------------------------------------------------------------------------- +// periodic GUI work +// --------------------------------------------------------------------------- +void MainWindow::processImages() { + updateReceiverRoiOverlay(); // cheap when the ROI did not change + if (goout.load() == 0) + return; + std::lock_guard lock(pipeline_.images().mtx); + if (!pipeline_.images().ready) + return; + // ADC and gain are always the raw 1024x512 layout (ROI selection on them + // stays in raw data coordinates, also in a JFSTRX build) + // Hot pixels: in the raw view they are replaced by the average pedestal of + // their 3x3 neighbourhood (so they look like background), in the + // pedestal-corrected view by 0 - they carry no signal. + const float *adcSrc = pipeline_.images().adc.data(); + if (pipeline_.gate().hotPixels() > 0) { + const auto hmask = hotMaskSnapshot(); + if (hmask && hmask->size() == static_cast(NCH)) { + if (adcScratch_.size() != static_cast(NCH)) + adcScratch_.assign(static_cast(NCH), 0.0f); + std::memcpy(adcScratch_.data(), adcSrc, NCH * sizeof(float)); + const std::shared_ptr peds = + gNoPede.load() ? PedestalStore::instance().get() : nullptr; + for (size_t i = 0; i < static_cast(NCH); i++) { + if (!(*hmask)[i]) + continue; + float rep = 0.0f; + if (peds && peds->g0.size() >= static_cast(NCH)) { + const int x = static_cast(i % kRawWidth); + const int y = static_cast(i / kRawWidth); + double sum = 0.0; + int cnt = 0; + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) { + const int xx = x + dx, yy = y + dy; + if (xx < 0 || xx >= kRawWidth || yy < 0 || + yy >= kRawHeight) + continue; + sum += peds->g0[static_cast(yy) * kRawWidth + + xx]; + cnt++; + } + } + rep = cnt ? static_cast(sum / cnt) : 0.0f; + } + adcScratch_[i] = rep; + } + adcSrc = adcScratch_.data(); + } + } + m_adcView->setImageCopy(adcSrc, kRawWidth, kRawHeight); + if (m_photonView && pipeline_.images().photonReady && + pipeline_.images().photonSeq != pmapSeq_ && + !pipeline_.images().photon.empty()) { + pmapSeq_ = pipeline_.images().photonSeq; + const float *pmapSrc = pipeline_.images().photon.data(); + if (pipeline_.gate().hotPixels() > 0) { + const auto hmask = hotMaskSnapshot(); + if (hmask && hmask->size() == static_cast(NCH)) { + if (photonScratch_.size() != static_cast(NCH)) + photonScratch_.assign(static_cast(NCH), 0.0f); + std::memcpy(photonScratch_.data(), pmapSrc, + NCH * sizeof(float)); + for (size_t i = 0; i < static_cast(NCH); i++) + if ((*hmask)[i]) + photonScratch_[i] = 0.0f; + pmapSrc = photonScratch_.data(); + } + } + m_photonView->setImageCopy(pmapSrc, kRawWidth, 512); + m_photonView->update(); + } + m_gainView->setImageCopy(pipeline_.images().gain.data(), kRawWidth, + kRawHeight); +#ifdef JFSTRX + if (pipeline_.images().panelsReady) { + for (int p = 0; p < kNumAdcPanels; p++) { + if (m_adcPanelView[p] && !pipeline_.images().panels[p].empty()) + m_adcPanelView[p]->setImageCopy( + pipeline_.images().panels[p].data(), kAdcPanelW[p], + kAdcPanelH[p]); + } + } +#endif + pipeline_.images().ready = false; +} + +// Mirrors the receiver ROI ("rx_roi" in the stream header) onto the ADC image +// as the RED box, next to the yellow gate ROI. The ADC image is the raw +// 1024x512 layout, so this is always a plain rectangle in raw coordinates. +void MainWindow::updateReceiverRoiOverlay() { + if (!m_adcView) + return; + const int xmin = rxRoiXmin.load(); + const int xmax = rxRoiXmax.load(); + const int ymin = rxRoiYmin.load(); + const int ymax = rxRoiYmax.load(); + if (!rxRoiValid.load() || xmax <= xmin || ymax <= ymin) { + if (m_rxRoiShown) { + m_adcView->setReceiverRoiRect(0, 0, 0, 0, false); + m_rxRoiShown = false; + } + return; + } + if (m_rxRoiShown && xmin == m_rxRoiLast[0] && xmax == m_rxRoiLast[1] && + ymin == m_rxRoiLast[2] && ymax == m_rxRoiLast[3]) + return; + m_adcView->setReceiverRoiRect(xmin, ymin, xmax + 1, ymax + 1, true); + m_rxRoiShown = true; + m_rxRoiLast[0] = xmin; + m_rxRoiLast[1] = xmax; + m_rxRoiLast[2] = ymin; + m_rxRoiLast[3] = ymax; +} + +void MainWindow::updateStatus() { + const GateEngine::Counters s = pipeline_.gate().snapshot(); + const QLocale loc; + + frameLabel->setText( + QString("Frames: display %1 | in stream %2 | gap %3") + .arg(loc.toString(static_cast(pipeline_.displayFrames()))) + .arg(loc.toString(static_cast(s.framesIn))) + .arg(loc.toString(static_cast(s.framesIn - + s.framesPass)))); + + // Stream state comes from ZMQ socket monitoring: an idle-but-alive + // receiver is reported as connected, never as "stopped". + QString streamTxt; + for (size_t k = 0; k < pipeline_.portCount(); k++) { + double since = 0.0; + const Pipeline::StreamState st = + pipeline_.streamState(static_cast(k), &since); + QString one; + switch (st) { + case Pipeline::StreamState::Disconnected: + one = "disconnected"; + break; + case Pipeline::StreamState::Connecting: + one = "waiting..."; + break; + case Pipeline::StreamState::Connected: + one = "connected"; + break; + case Pipeline::StreamState::Idle: + one = QString("connected (no frames for %1 s)").arg(since, 0, 'f', 1); + break; + case Pipeline::StreamState::EndOfAcq: + one = "connected (end of acquisition)"; + break; + } + const int w = pipeline_.portWidth(static_cast(k)); + const int h = pipeline_.portHeight(static_cast(k)); + const QString geo = + (w > 0 && h > 0) ? QString(" [%1x%2]").arg(w).arg(h) : QString(); + if (k) + streamTxt += "\n"; + streamTxt += QString("%1: %2%3 (%4 frames, last #%5)") + .arg(pipeline_.portList()[k]) + .arg(one) + .arg(geo) + .arg(pipeline_.portFrames(static_cast(k))) + .arg(pipeline_.portLastFrame(static_cast(k))); + } + streamVal_->setText(streamTxt); + + // frame-index alignment: delta between the newest frames of each + // interface, plus what had to be discarded to stay aligned + if (pipeline_.portCount() > 1) { + const qlonglong f0 = static_cast( + pipeline_.portLastFrame(0)); + const qlonglong f1 = static_cast( + pipeline_.portLastFrame(1)); + alignVal_->setText( + QString("delta %1 | aligned %2 | resync drops %3 | behind drops " + "%4 | unpaired %5") + .arg(f1 - f0) + .arg(loc.toString(static_cast(s.framesIn))) + .arg(loc.toString( + static_cast(pipeline_.resyncDrops()))) + .arg(loc.toString( + static_cast(pipeline_.behindDrops()))) + .arg(loc.toString( + static_cast(pipeline_.incompleteFrames())))); + alignVal_->setVisible(true); + } else { + alignVal_->setVisible(false); + } + if (s.pedeLoaded) + pedeVal_->setText(QString("loaded (%1)%2") + .arg(QString::fromStdString(g_cfg.pedeFile)) + .arg(gPedeTracked.load() ? " [tracked]" + : "")); + else + pedeVal_->setText("not loaded"); + + displayDropVal_->setText( + loc.toString(static_cast(pipeline_.displayDropped()))); + + const QString err = QString::fromStdString(s.lastError); + errVal_->setText(err.isEmpty() ? QString() : QString("Last error: %1").arg(err)); + + framesInCumVal_->setText(loc.toString(static_cast(s.framesIn))); + // "Current" used to show the cumulative counters (framesInCur was never + // reset), so both columns always agreed. Derive the per-interval delta + // from consecutive snapshots instead. + static uint64_t pIn = 0, pPass = 0, pDrop = 0; + const uint64_t dIn = s.framesIn - pIn; + const uint64_t dPass = s.framesPass - pPass; + const uint64_t dDrop = s.framesDrop - pDrop; + pIn = s.framesIn; + pPass = s.framesPass; + pDrop = s.framesDrop; + framesInCurVal_->setText(loc.toString(static_cast(dIn))); + framesPassCumVal_->setText( + loc.toString(static_cast(s.framesPass))); + framesPassCurVal_->setText(loc.toString(static_cast(dPass))); + framesDropCumVal_->setText( + loc.toString(static_cast(s.framesDrop))); + framesDropCurVal_->setText(loc.toString(static_cast(dDrop))); + updateFlow(); // the pedestal/gain state may have changed since last time + + scDropCumVal_->setText( + loc.toString(static_cast(s.framesScDrop))); + scDropCurVal_->setText( + loc.toString(static_cast(s.framesScDropCur))); + if (scLastVal_) + scLastVal_->setText(s.lastSc < 0 + ? QString("last frame: ? (no SC in header)") + : QString("last frame: SC %1").arg(s.lastSc)); + pixelsAboveCumVal_->setText( + loc.toString(static_cast(s.pixelsAbove))); + pixelsAboveCurVal_->setText( + loc.toString(static_cast(s.pixelsAboveCur))); + rateInVal_->setText(QString("%1 fps / %2 MB/s") + .arg(loc.toString(s.rateIn, 'f', 1)) + .arg(loc.toString(s.rateInMBs, 'f', 1))); + ratePassVal_->setText(QString("%1 fps / %2 MB/s") + .arg(loc.toString(s.ratePass, 'f', 1)) + .arg(loc.toString(s.ratePassMBs, 'f', 1))); + dropFracVal_->setText(loc.toString(s.dropFrac, 'f', 3)); + + if (pipeline_.pedeAcquireActive()) + statusLabel->setText( + QString("Status: acquiring pede... %1 frames") + .arg(pipeline_.pedeAcquireProgress())); + + // ROI save progress + auto-stop detection (budget reached) + if (pipeline_.saver().running()) { + if (m_saveWasRunning != true) { + m_saveWasRunning = true; + if (btnSaveStart) + btnSaveStart->setChecked(true); + } + updateSaveLabel(); + } else if (m_saveWasRunning) { + m_saveWasRunning = false; + if (btnSaveStart) + btnSaveStart->setChecked(false); + applySaveLock(false); // the run ended (user, budget or error) + updateFlow(); + updateSaveLabel(); + const RoiSaver::Status st = pipeline_.saver().status(); + statusLabel->setText(QString("Status: ROI save stopped (%1), %2 saved") + .arg(QString::fromStdString(st.stopReason)) + .arg(st.saved)); + } + + // gate ROI, for the headless line + char roiTxt[64]; + if (pipeline_.gate().roiActive()) { + int rx0, ry0, rx1, ry1; + pipeline_.gate().roiSnapshot(rx0, ry0, rx1, ry1); + snprintf(roiTxt, sizeof(roiTxt), "%dx%d@%d,%d", rx1 - rx0, ry1 - ry0, + rx0, ry0); + } else { + snprintf(roiTxt, sizeof(roiTxt), "off"); + } + + // ROI save state, for the headless line + char saveTxt[96]; + { + const RoiSaver::Status ss = pipeline_.saver().status(); + // the first number is whatever this mode writes: frames or maps + const unsigned long long n = + (unsigned long long)(ss.photonMode ? ss.pmaps : ss.saved); + if (ss.running && ss.photonMode) + snprintf(saveTxt, sizeof(saveTxt), "%llumap/%llu", n, + (unsigned long long)ss.dropped); + else if (ss.running) + snprintf(saveTxt, sizeof(saveTxt), "%llu/%llu", n, + (unsigned long long)ss.dropped); + else if (!ss.h5Path.empty()) + snprintf(saveTxt, sizeof(saveTxt), "done:%llu%s/%llu", n, + ss.photonMode ? "map" : "", + (unsigned long long)ss.dropped); + else + snprintf(saveTxt, sizeof(saveTxt), "off"); + } + + // dual-interface alignment audit: out-of-step flushes and the current + // load-shedding step (keep 1 in N pairs), once per second + if (hotVal_) + hotVal_->setText( + QString("hot: %1 px") + .arg(static_cast(pipeline_.gate().hotPixels()))); + + if (g_cfg.printCounters && pipeline_.portCount() > 1) { + static int atick = 0; + if (++atick >= 2) { + atick = 0; + // Sustained-rate audit: what fraction of the last 120 seconds + // ran within 5 % of the best assembled rate seen, i.e. "2 kHz for + // 99 % of the time" expressed as a number. + static double best = 0.0; + static int good = 0, total = 0; + const double r = s.rateIn; + if (r > best) + best = r; + const bool ok = best > 0.0 && r >= 0.95 * best; + if (ok) + good++; + total++; + if (total > 120) { + total--; + if (!ok) good--; + } + printf("[align] keep=1/%d shed=%llu rate=%.0f/s best=%.0f/s " + "within95=%.0f%% (%d/%d s) hot=%llupx/%lluf\n", + g_keepDiv.load(), + static_cast(pipeline_.shedFrames()), r, + best, total ? 100.0 * good / total : 0.0, good, total, + static_cast( + pipeline_.gate().hotPixels()), + static_cast( + pipeline_.pmapFrames())); + fflush(stdout); + } + } + + // optional headless monitoring: the counter line the old CLI printed + if (g_cfg.printCounters) { + static int tick = 0; + if (++tick >= 2) { // updateStatus() runs every 500 ms + tick = 0; + printf("in=%llu pass=%llu drop=%llu inrate=%.1f passrate=%.1f " + "dropfrac=%.3f px=%llu display=%llu displaydrop=%llu " + "stream=%s resync=%llu behind=%llu incomplete=%llu " + "ports=%s workers=%d roi=%s save=%s gainmap=%s sc=%s " + "scdrop=%llu pthr=%g%s kev=%s ptrack=%s shown=%llu\n", + static_cast(s.framesIn), + static_cast(s.framesPass), + static_cast(s.framesDrop), s.rateIn, + s.ratePass, s.dropFrac, + static_cast(s.pixelsAbove), + static_cast(pipeline_.displayFrames()), + static_cast(pipeline_.displayDropped()), + pipeline_.streamStateName(), + static_cast(pipeline_.resyncDrops()), + static_cast(pipeline_.behindDrops()), + static_cast( + pipeline_.incompleteFrames()), + pipeline_.streamStateSummary().c_str(), + g_cfg.analysisWorkers, roiTxt, saveTxt, + gainMapLoaded() ? "loaded" : "off", + s.scFilter < 0 ? "all" : std::to_string(s.scFilter).c_str(), + static_cast(s.framesScDrop), + static_cast(gain_flag.load() ? g_pheneThresholdKev + : g_pheneThreshold), + gain_flag.load() ? "keV" : "ADU", + (kevSpectra.load() && gain_flag.load()) + ? (gainMapLoaded() ? "loaded" : "nominal") + : "off", + gPedeTracking.load() + ? (pedeTrackerStats().updates > 0 ? "on-upd" : "on") + : "off", + static_cast(pipeline_.imagesShown())); + fflush(stdout); + } + } +} + +void MainWindow::checkSignals() { + if (g_sigCount == 0) + return; + if (g_sigCount > 1) + std::_Exit(0); + printf("[shutdown] signal received, stopping threads\n"); + fflush(stdout); + statusLabel->setText("Status: stopping..."); + stop(); + QApplication::quit(); +} + +// --------------------------------------------------------------------------- +// actions +// --------------------------------------------------------------------------- +void MainWindow::toggleStartPause() { + if (goout.load() == 0) { + goout = 1; + statusLabel->setText("Status: running"); + } else { + goout = 0; + statusLabel->setText("Status: paused"); + } +} + +void MainWindow::faster() { + int v = int(HDraw_every.load() * 0.8f); + if (v < 1) + v = 1; + HDraw_every = v; + cout << endl << "Drawing every " << HDraw_every.load() << " frames" << endl; + updateDrawLabel(); +} + +void MainWindow::slower() { + int cur = HDraw_every.load(); + int v = int(cur * 1.25f); + if (v <= cur) + v = cur + 1; // avoid 1->1 + HDraw_every = v; + cout << endl << "Drawing every " << HDraw_every.load() << " frames" << endl; + updateDrawLabel(); +} + +// "Apply gain": divide the pixels by their ADU/keV (map if loaded, otherwise +// the nominal 40) so the keV spectra are physically meaningful. The plots show +// keV only while this is pushed and "keV PH" is ticked. +void MainWindow::toggleGain() { + if (btnGainToggle) + btnGainToggle->setChecked(!btnGainToggle->isChecked()); +} + +void MainWindow::setApplyGain(bool on) { + // The photon-counting threshold is in ADU with the gain off and in keV with + // it on: keep the value that is on screen in its own unit and show the + // other one, so toggling the gain never destroys either number. + if (photonThrSpin_) { + const double shown = photonThrSpin_->value(); + if (on) + g_pheneThreshold = static_cast(shown); // it was ADU + else + g_pheneThresholdKev = static_cast(shown); // it was keV + const QSignalBlocker block(photonThrSpin_); // no handler round trip + photonThrSpin_->setDecimals(on ? 1 : 0); + photonThrSpin_->setSingleStep(on ? 0.5 : 10.0); + photonThrSpin_->setValue(static_cast( + on ? g_pheneThresholdKev : g_pheneThreshold)); + if (photonThrLabel_) + photonThrLabel_->setText(on ? "photon threshold (keV):" + : "photon threshold (ADU):"); + } + gain_flag = on; + // Toggling the gain mid-save changes the unit of the photon counting, so + // the maps written from here on use the other one while the JSON keeps the + // value it recorded at save start. Say it out loud instead of leaving a + // file with two incompatible scales. + if (gSaveMode.load() == 1 && pipeline_.saver().status().running) { + printf("[gain] warning: the photon-counting unit is now %s while a " + "photon-map save is running - maps written from here on use the " + "new unit, the JSON keeps the one from save start\n", + on ? "keV" : "ADU"); + fflush(stdout); + } + updateFlow(); // the arrows bypass the Gain box when it is off + printf("[gain] apply gain %s (ADU/keV: %s)\n", on ? "ON" : "off", + on ? (gainMapLoaded() ? "from the gain map" : "nominal 40") : "-"); + fflush(stdout); + statusLabel->setText(on ? (gainMapLoaded() + ? "Status: apply gain on (per-pixel map)" + : "Status: apply gain on (nominal 40 ADU/keV)") + : "Status: apply gain off (raw ADC)"); +} + +void MainWindow::bwPalette1() { + RootCmd c; + c.type = RootCmdType::PaletteBW1; + pushRootCmd(c); + m_adcView->setPaletteLut(paletteLut(Palette::BW1)); // same colours as ROOT + if (m_adcScale_) + m_adcScale_->setPaletteChoice(1); + statusLabel->setText("Status: BW palette"); +} + +void MainWindow::bwPalette2() { + RootCmd c; + c.type = RootCmdType::PaletteBW2; + pushRootCmd(c); + m_adcView->setPaletteLut(paletteLut(Palette::BW2)); + if (m_adcScale_) + m_adcScale_->setPaletteChoice(2); + statusLabel->setText("Status: BW palette 2"); +} + +void MainWindow::colPal() { + RootCmd c; + c.type = RootCmdType::PaletteColor; + pushRootCmd(c); + m_adcView->setPaletteLut(paletteLut(Palette::Color)); + statusLabel->setText("Status: colour palette"); +} + +// The ADC widget works in DISPLAY coordinates (row 0 = top of the widget), +// while every ROOT plot and the gate use DATA coordinates (row 0 = bottom, +// (0,0) = bottom-left). This is the single place where the two meet: the +// gain view follows in display coordinates, the ROOT photon map gets the +// range converted to data rows AND framed on pixel edges (the histogram has +// one bin per pixel centred on the integer coordinate, so showing display rows +// [y1,y2) means the data range [ny-y2-0.5, ny-y1-0.5]). +// A window was zoomed. With "zoom coordination" ticked (the default) every +// other window and the ROOT plots follow the rectangle; unticked, each window +// is on its own and nothing is propagated. +void MainWindow::zoomTo(FastImageView *src, double x1, double y1, double x2, + double y2) { + if (x2 - x1 < kMinViewport || y2 - y1 < kMinViewport) + return; // never push a degenerate range to the ROOT canvases + if (!zoomCoord_) + return; + printf("[zoom] coordinated from %s\n", + src == m_adcView ? "ADC" + : src == m_gainView ? "gain" + : src == m_photonView ? "photon" + : "?"); + fflush(stdout); + for (FastImageView *v : {m_adcView, m_gainView, m_photonView}) + if (v && v != src) + v->setViewport(x1, y1, x2, y2); + const double ny = + (m_adcView && m_adcView->imageHeight() > 0) + ? static_cast(m_adcView->imageHeight()) + : 512.0; + RootCmd c; + c.type = RootCmdType::Zoom; + c.a = x1 - 0.5; + c.b = ny - y2 - 0.5; + c.c = x2 - 0.5; + c.d = ny - y1 - 0.5; + pushRootCmdCoalesced(c); +} + +// The gate ROI is captured from the ADC viewport when the button is pressed +// and then stays fixed, so the operator can keep panning/zooming the image. +// The viewport is in DISPLAY coordinates (row 0 at the top) while the gate +// works in DATA rows (row 0 at the bottom), hence the vertical flip. +void MainWindow::toggleGateRoi(bool on) { + if (on && m_applyingRoi) + return; // ROI was set directly by applyRoi(); do not re-capture + + if (!on) { // stop restricting, but keep the current ROI + pipeline_.gate().setRoiOnly(false); + pipeline_.gate().resetCounters(); + printf("[roi] gate over the full frame (current ROI kept)\n"); + fflush(stdout); + statusLabel->setText( + "Status: gate over the full frame (ROI kept as the current one)"); + updateRoiLabel(); + return; + } + if (!pipeline_.gate().roiSet()) { + QMessageBox::warning( + this, "ROI only", + "No ROI selected.\n\nUse \"Select ROI (ADC zoom)\" in the ROI " + "panel (zoom the ADC image first), or pick one with " + "\"Load ROI...\", then press ROI only again."); + btnGateRoi->setChecked(false); // leave it unselected + return; + } + pipeline_.gate().setRoiOnly(true); + pipeline_.gate().resetCounters(); + int x0 = 0, y0 = 0, x1 = 0, y1 = 0; + pipeline_.gate().roiSnapshot(x0, y0, x1, y1); + printf("[roi] gate restricted to the current ROI x=%d..%d y=%d..%d\n", x0, + x1, y0, y1); + fflush(stdout); + statusLabel->setText(QString("Status: gate restricted to the current ROI " + "%1x%2 at %3,%4") + .arg(x1 - x0) + .arg(y1 - y0) + .arg(x0) + .arg(y0)); + updateRoiLabel(); +} + +// Toggles the in-image ROI editor. It is seeded from the current ROI when +// there is one, otherwise from the ADC zoom (the old behaviour) so that the +// rectangle always starts somewhere sensible. Selection only: restricting the +// gate stays the "ROI only" button's business. +void MainWindow::setRoiEditMode(bool on) { + if (!m_adcView) + return; + if (!on) { + m_adcView->beginRoiEdit(false); + statusLabel->setText("Status: ROI editor closed"); + return; + } + const int imgW = m_adcView->imageWidth() > 0 ? m_adcView->imageWidth() + : kRawWidth; + const int imgH = m_adcView->imageHeight() > 0 ? m_adcView->imageHeight() + : kRawHeight; + int x0 = 0, y0 = 0, x1 = imgW, y1 = imgH; + if (pipeline_.gate().roiSet()) { + pipeline_.gate().roiSnapshot(x0, y0, x1, y1); + } else if (m_adcView->hasImage()) { + double vx = 0, vy = 0, vw = 0, vh = 0; + m_adcView->viewport(vx, vy, vw, vh); + x0 = static_cast(std::floor(vx)); + x1 = static_cast(std::ceil(vx + vw)); + y0 = imgH - static_cast(std::ceil(vy + vh)); + y1 = imgH - static_cast(std::floor(vy)); + x0 = std::max(0, std::min(imgW - 2, x0)); + y0 = std::max(0, std::min(imgH - 2, y0)); + x1 = std::max(x0 + 2, std::min(imgW, x1)); + y1 = std::max(y0 + 2, std::min(imgH, y1)); + } + m_adcView->setOverlayRect(x0, y0, x1, y1, true); + m_adcView->beginRoiEdit(true); + m_adcView->setFocus(); + statusLabel->setText( + "Status: ROI editor - drag the yellow rectangle to move it, its edges " + "or corners to resize, empty space to draw a new one; press \"Select " + "ROI\" again when done"); +} + +// Selects the ADC zoom as the current ROI. Selection only: whether the gate +// is restricted to it is the "ROI only" button's business. +void MainWindow::captureRoi() { + if (!m_adcView || !m_adcView->hasImage()) { + QMessageBox::information( + this, "Select ROI", + "No image yet - wait for frames and zoom the ADC image first."); + return; + } + double vx = 0, vy = 0, vw = 0, vh = 0; + m_adcView->viewport(vx, vy, vw, vh); + const int imgW = m_adcView->imageWidth(); + const int imgH = m_adcView->imageHeight(); + + int x0 = static_cast(std::floor(vx)); + int x1 = static_cast(std::ceil(vx + vw)); + const int dy0 = static_cast(std::floor(vy)); + const int dy1 = static_cast(std::ceil(vy + vh)); + // display row d <-> data row (imgH - 1 - d) + int y0 = imgH - dy1; + int y1 = imgH - dy0; + if (x0 < 0) x0 = 0; + if (y0 < 0) y0 = 0; + if (x1 > imgW) x1 = imgW; + if (y1 > imgH) y1 = imgH; + + pipeline_.gate().setRoi(x0, y0, x1, y1); + if (!pipeline_.gate().roiSet()) { // too small to be useful + QMessageBox::information(this, "Select ROI", + "The selected region is too small."); + return; + } + if (m_adcView) + m_adcView->setOverlayRect(x0, y0, x1, y1, true); + if (gateRoiVal_) + gateRoiVal_->setText(QString("%1x%2 at %3,%4 (data rows)") + .arg(x1 - x0) + .arg(y1 - y0) + .arg(x0) + .arg(y0)); + printf("[roi] current ROI x=%d..%d y=%d..%d (%dx%d px, data rows, row 0 = " + "bottom) selected from the ADC zoom\n", + x0, x1, y0, y1, x1 - x0, y1 - y0); + fflush(stdout); + updateRoiLabel(); + statusLabel->setText( + QString("Status: ROI %1x%2 at %3,%4 is the current ROI%5") + .arg(x1 - x0) + .arg(y1 - y0) + .arg(x0) + .arg(y0) + .arg(pipeline_.gate().roiActive() + ? " (gate restricted to it)" + : " - press \"ROI only\" to restrict the gate")); +} + +// Adds the active gate ROI to the list file in the program folder. +void MainWindow::saveRoi() { + if (!pipeline_.gate().roiSet()) { + QMessageBox::information( + this, "Save ROI", + "No ROI selected.\n\nZoom the ADC image and press " + "\"Select ROI (ADC zoom)\" first (or load one from the list)."); + return; + } + int x0, y0, x1, y1; + pipeline_.gate().roiSnapshot(x0, y0, x1, y1); + const int w = x1 - x0, h = y1 - y0; + if (!roiListUpsert(x0, y0, w, h)) { + QMessageBox::warning(this, "Save ROI", + QString("Could not write %1") + .arg(QString::fromStdString(g_cfg.roiFile))); + return; + } + const size_t n = roiListLoad().size(); + updateRoiLabel(); + statusLabel->setText(QString("Status: ROI %1x%2 at %3,%4 saved (%5 in list)") + .arg(w).arg(h).arg(x0).arg(y0).arg(n)); +} +// Applies a ROI to the gate without capturing the ADC viewport, and makes +// sure the "ROI only" button reflects that the gate is restricted. +void MainWindow::applyRoi(int x, int y, int w, int h, const QString &src, + bool restrict) { + pipeline_.gate().setRoi(x, y, x + w, y + h); + if (!pipeline_.gate().roiSet()) { + QMessageBox::information(this, "Load ROI", + "That entry is too small to use."); + return; + } + // -1 = keep the current restriction, 0/1 = force it off/on + if (restrict) + pipeline_.gate().setRoiOnly(true); + pipeline_.gate().resetCounters(); + if (m_adcView) + m_adcView->setOverlayRect(x, y, x + w, y + h, true); + printf("[roi] current ROI x=%d..%d y=%d..%d (%dx%d px, data rows, row 0 = " + "bottom) applied from %s\n", + x, x + w, y, y + h, w, h, src.toLocal8Bit().constData()); + fflush(stdout); + if (gateRoiVal_) + gateRoiVal_->setText(QString("%1x%2 at %3,%4 (data rows)") + .arg(w).arg(h).arg(x).arg(y)); + updateRoiLabel(); + statusLabel->setText( + QString("Status: ROI %1x%2 at %3,%4 is the current ROI (from %5)%6") + .arg(w).arg(h).arg(x).arg(y).arg(src) + .arg(pipeline_.gate().roiActive() + ? " - gate restricted to it" + : " - press \"ROI only\" to restrict the gate")); +} + +// List of saved ROIs, most recently saved/loaded first. +void MainWindow::loadRoiDialog() { + std::vector list = roiListLoad(); + const bool listEmpty = list.empty(); + + QDialog dialog(this); + dialog.setWindowTitle("Load gate ROI"); + dialog.resize(520, 320); + auto *lay = new QVBoxLayout(&dialog); + auto *table = new QTableWidget(static_cast(list.size()), 5, &dialog); + table->setHorizontalHeaderLabels( + QStringList() << "X" << "Y" << "width" << "height" << "last used"); + table->verticalHeader()->setVisible(false); + table->setSelectionBehavior(QAbstractItemView::SelectRows); + table->setSelectionMode(QAbstractItemView::SingleSelection); + table->setEditTriggers(QAbstractItemView::NoEditTriggers); + const QLocale loc; + for (size_t i = 0; i < list.size(); i++) { + const RoiEntry &e = list[i]; + const QString when = QDateTime::fromMSecsSinceEpoch(e.usedMs) + .toString("yyyy-MM-dd HH:mm:ss"); + const QString cells[5] = { + loc.toString(e.x), loc.toString(e.y), loc.toString(e.w), + loc.toString(e.h), when}; + for (int c = 0; c < 5; c++) + table->setItem(static_cast(i), c, + new QTableWidgetItem(cells[c])); + } + table->resizeColumnsToContents(); + table->selectRow(0); // newest first, pre-selected + lay->addWidget(table); + // the same convention as the ADC image and every ROOT plot + auto *hint = new QLabel( + listEmpty + ? QString("The list is empty. Use \"Edit as new...\" to type a " + "ROI, or \"Save ROI\" in the ROI panel to add the current " + "one (entries are kept in %1).") + .arg(QString::fromStdString(g_cfg.roiFile)) + : QString("X/Y are data coordinates: x grows to the right, y grows " + "upwards and (0,0) is the bottom-left pixel."), + &dialog); + hint->setWordWrap(true); + lay->addWidget(hint); + + auto *btns = new QDialogButtonBox(&dialog); + auto *applyBtn = btns->addButton("Apply", QDialogButtonBox::AcceptRole); + auto *delBtn = btns->addButton("Delete", QDialogButtonBox::DestructiveRole); + auto *editBtn = + btns->addButton("Edit as new...", QDialogButtonBox::ActionRole); + editBtn->setToolTip( + "Type the four numbers of a ROI (prefilled from the selected entry, or\n" + "from the current ROI), press Apply: the rectangle becomes the current\n" + "ROI and is added to the list as a new entry."); + btns->addButton(QDialogButtonBox::Cancel); + if (listEmpty) { // nothing to apply or delete, but editing still works + applyBtn->setEnabled(false); + delBtn->setEnabled(false); + } + lay->addWidget(btns); + connect(btns, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(btns, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + connect(table, &QTableWidget::itemDoubleClicked, &dialog, + [&dialog](QTableWidgetItem *) { dialog.accept(); }); + + bool editedChosen = false; // the numbers dialog applied the ROI itself + connect(editBtn, &QPushButton::clicked, &dialog, [&]() { + const int imgW = (m_adcView && m_adcView->imageWidth() > 0) + ? m_adcView->imageWidth() + : kRawWidth; + const int imgH = (m_adcView && m_adcView->imageHeight() > 0) + ? m_adcView->imageHeight() + : kRawHeight; + int x = 0, y = 0, w = imgW, h = imgH; + const int r = table->currentRow(); + if (r >= 0 && r < static_cast(list.size())) { + x = list[r].x; + y = list[r].y; + w = list[r].w; + h = list[r].h; + } else if (pipeline_.gate().roiSet()) { + int a = 0, b = 0, c = 0, d = 0; + pipeline_.gate().roiSnapshot(a, b, c, d); + x = a; + y = b; + w = c - a; + h = d - b; + } + QDialog ed(&dialog); + ed.setWindowTitle("Edit ROI (applied as a new ROI)"); + auto *form = new QFormLayout(&ed); + auto mk = [&](int v, int lo, int hi) { + auto *sb = new QSpinBox(&ed); + sb->setRange(lo, hi); + sb->setValue(std::max(lo, std::min(hi, v))); + return sb; + }; + auto *sx = mk(x, 0, imgW - 1); + auto *sy = mk(y, 0, imgH - 1); + auto *sw = mk(w, 1, imgW); + auto *sh = mk(h, 1, imgH); + form->addRow("X (data, from the left):", sx); + form->addRow("Y (data, from the bottom):", sy); + form->addRow("Width:", sw); + form->addRow("Height:", sh); + auto *eb = new QDialogButtonBox(&ed); + // A QDialogButtonBox only emits accepted() for AcceptRole buttons, so an + // Apply (ApplyRole) button must be wired to accept() explicitly - + // otherwise clicking it did nothing at all. + auto *applyEd = eb->addButton("Apply", QDialogButtonBox::ApplyRole); + eb->addButton(QDialogButtonBox::Cancel); + form->addRow(eb); + connect(applyEd, &QPushButton::clicked, &ed, &QDialog::accept); + connect(eb, &QDialogButtonBox::rejected, &ed, &QDialog::reject); + // keep the rectangle inside the image while the numbers are typed + connect(sx, QOverload::of(&QSpinBox::valueChanged), &ed, + [=](int v) { sw->setMaximum(std::max(1, imgW - v)); }); + connect(sy, QOverload::of(&QSpinBox::valueChanged), &ed, + [=](int v) { sh->setMaximum(std::max(1, imgH - v)); }); + sw->setMaximum(std::max(1, imgW - sx->value())); + sh->setMaximum(std::max(1, imgH - sy->value())); + if (ed.exec() != QDialog::Accepted) + return; + const int nx = sx->value(), ny = sy->value(); + const int nw = std::min(sw->value(), imgW - nx); + const int nh = std::min(sh->value(), imgH - ny); + if (nw < 2 || nh < 2) { + QMessageBox::information(this, "Edit ROI", + "That rectangle is too small to use."); + return; + } + applyRoi(nx, ny, nw, nh, "edited numbers", false); + // it is a NEW entry: store it in the list (this also refreshes the + // timestamp, so it comes back first next time) + if (!roiListUpsert(nx, ny, nw, nh)) + QMessageBox::warning( + this, "Edit ROI", + QString("The ROI was applied but could not be written to %1") + .arg(QString::fromStdString(g_cfg.roiFile))); + updateRoiLabel(); + editedChosen = true; + dialog.accept(); + }); + bool deleted = false; + connect(delBtn, &QPushButton::clicked, &dialog, [&]() { + const int r = table->currentRow(); + if (r < 0 || r >= static_cast(list.size())) + return; + const RoiEntry &e = list[r]; + if (!roiListRemove(e.x, e.y, e.w, e.h)) { + QMessageBox::warning(this, "Load ROI", + QString("Could not write %1") + .arg(QString::fromStdString( + g_cfg.roiFile))); + return; + } + deleted = true; + table->removeRow(r); // keep the dialog open for further edits + }); + + if (dialog.exec() != QDialog::Accepted) { + if (deleted) + statusLabel->setText("Status: ROI entry deleted"); + return; + } + if (editedChosen) + return; // already applied by the "Edit as new..." dialog + const int row = table->currentRow(); + if (row < 0 || row >= static_cast(list.size())) + return; + const RoiEntry e = list[row]; + // refresh the timestamp so it comes back first next time + roiListUpsert(e.x, e.y, e.w, e.h); + applyRoi(e.x, e.y, e.w, e.h, "list"); +} + +void MainWindow::resetHis() { + requestHistoReset(); + statusLabel->setText("Status: histograms reset"); +} + +void MainWindow::resetAxis() { + RootCmd c; + c.type = RootCmdType::ResetAxes; + pushRootCmd(c); + // The ADC view is the master of the zoom, so unzooming only the ROOT + // clones would leave the images (and, through the next drag, the photon + // map) still zoomed: reset the widget viewports as well. + if (m_adcView) + m_adcView->resetView(); // propagates to the gain view + photon map + statusLabel->setText("Status: axes reset"); +} + +// Starts/stops writing the gate-passed ROI frames to HDF5. +void MainWindow::toggleRoiSave(bool on) { + RoiSaver &sv = pipeline_.saver(); + if (!on) { + if (sv.running()) { + sv.requestStop("user"); + statusLabel->setText("Status: finishing ROI save (flushing)..."); + applySaveLock(false); // the run is over, settings are free again + updateFlow(); + updateSaveLabel(); + } + return; + } + if (sv.running()) { + btnSaveStart->setChecked(false); + return; + } + // No gate ROI is fine: roiSnapshot() then reports the full frame, so the + // save writes the whole image instead of refusing to start. + + RoiSaver::StartInfo info; + int x0, y0, x1, y1; + pipeline_.gate().roiSnapshot(x0, y0, x1, y1); + info.x = x0; + info.y = y0; + info.w = x1 - x0; + info.h = y1 - y0; + info.maxFrames = static_cast(saveFrames_->value()); + info.photonMode = saveModePhoton_ && saveModePhoton_->isChecked(); + info.photonEvery = pmapSaveSpin_ ? pmapSaveSpin_->value() : 2000; + info.photonThreshold = gain_flag.load() ? g_pheneThresholdKev + : g_pheneThreshold; + info.photonKev = gain_flag.load(); + gGateChangedDuringSave.store(false); // fresh run, fresh note + pmapSaveReset(); // start the first saved map from scratch + info.dir = saveDirEdit_->text().trimmed().toStdString(); + if (info.dir.empty()) + info.dir = "."; + info.baseName = saveBaseEdit_->text().trimmed().toStdString(); + if (info.baseName.empty()) + info.baseName = "jfroi"; + + // pedestal in the same ROI (gain 0): slice 0 in raw mode, the + // "/pedestal data" dataset in corrected mode + std::shared_ptr peds = PedestalStore::instance().get(); + info.corrected = saveCorrected_->isChecked() && !gNoPede.load(); + if (info.corrected && !peds) { + QMessageBox::information( + this, "ROI save", + "Pedestal-corrected saving needs a pedestal.\n\nRead or acquire " + "one first, or switch the option off to save raw data."); + btnSaveStart->setChecked(false); + return; + } + // the correction itself needs the full-frame pedestal set + info.peds = peds; + // Virtual photon counting thresholds the PEDESTAL-SUBTRACTED ADC, so a map + // without a pedestal would be empty: refuse the save instead of writing + // empty maps (same policy as "Pedestal corrected" above). + if (info.photonMode && (!peds || gNoPede.load())) { + const char *why = peds ? "\"No pede\" is on" : "no pedestal is loaded"; + printf("[save] refusing virtual photon counting: %s, and the photon " + "map thresholds the pedestal-subtracted ADC\n", why); + fflush(stdout); + statusLabel->setText( + QString("Status: virtual photon counting needs a pedestal (%1)") + .arg(QString::fromLatin1(why))); + // a scripted start (--start-save) must not wait on a modal dialog + if (!g_cfg.startSave) { + QMessageBox::warning( + this, "Virtual photon counting", + QString("Virtual photon counting thresholds the " + "pedestal-subtracted ADC, but %1.\n\nLoad or acquire " + "a pedestal and clear \"No pede\" first (or save " + "frames instead).") + .arg(QString::fromLatin1(why))); + } + btnSaveStart->setChecked(false); + updateSaveLabel(); + return; + } + info.useGain = gain_flag.load(); + info.g1off = G1Poffset.load(); + info.g2off = G2Poffset.load(); + if (peds) { + // the ROI is in raw data coordinates (the ADC image is unmapped), and + // g0 is the raw 1024x512 pedestal even in a JFSTRX build + info.pedestal.resize(static_cast(info.w) * info.h, 0.0f); + for (int y = 0; y < info.h; y++) { + const int sy = info.y + y; + for (int x = 0; x < info.w; x++) { + const int sx = info.x + x; + if (sy >= 0 && sy < kRawHeight && sx >= 0 && sx < kRawWidth) + info.pedestal[static_cast(y) * info.w + x] = + peds->g0[static_cast(sy) * kRawWidth + sx]; + } + } + } + + // gate criteria in force at the start + const GateEngine::Counters g0 = pipeline_.gate().snapshot(); + // read from the engine: the operator can change these while running, and + // g_cfg keeps the startup snapshot + info.gate.enabled = pipeline_.gate().enabled(); + info.gate.threshold = pipeline_.gate().threshold(); + info.gate.minPixels = pipeline_.gate().minPixels(); + info.gate.contiguous = pipeline_.gate().contiguous(); + info.gate.roiActive = true; + info.gate.noPede = gNoPede.load(); + info.gate.roiX = x0; + info.gate.roiY = y0; + info.gate.roiW = info.w; + info.gate.roiH = info.h; + info.gateAtEnd = g0; + { + char buf[256]; + snprintf(buf, sizeof(buf), + "{\"enabled\":%s,\"threshold\":%.3f,\"min_pixels\":%llu," + "\"contiguous\":%s,\"storage_cell\":%d,\"no_pede\":%s," + "\"hot_suppress\":%s,\"hot_threshold\":%.2f," + "\"hot_pixels\":%llu}", + g_cfg.gateEnabled ? "true" : "false", + static_cast(g_cfg.threshold), + static_cast(g_cfg.minPixels), + g_cfg.contiguous ? "true" : "false", + pipeline_.gate().scFilter(), + gNoPede.load() ? "true" : "false", + pipeline_.gate().hotSuppress() ? "true" : "false", + pipeline_.hotThreshold(), + static_cast(pipeline_.gate().hotPixels())); + info.gateJson = buf; + } + // exact end-of-run statistics, refreshed by the writer when it finishes + info.statsProvider = [this](GateEngine::Counters &g, RunStats &st, + std::vector &ports) { + g = pipeline_.gate().snapshot(); + st.resync = pipeline_.resyncDrops(); + st.behind = pipeline_.behindDrops(); + st.unpaired = pipeline_.incompleteFrames(); + st.displayFrames = pipeline_.displayFrames(); + st.displayDropped = pipeline_.displayDropped(); + ports.clear(); + for (size_t i = 0; i < pipeline_.portCount(); i++) { + StreamInfo si; + si.port = pipeline_.portList()[i]; + si.state = pipeline_.streamStateName( + pipeline_.streamState(static_cast(i))); + si.frames = pipeline_.portFrames(static_cast(i)); + si.lastFrame = pipeline_.portLastFrame(static_cast(i)); + ports.push_back(si); + } + }; + + std::string err; + if (!sv.start(info, err)) { + QMessageBox::warning(this, "ROI save", + QString("Could not start: %1") + .arg(QString::fromStdString(err))); + btnSaveStart->setChecked(false); + return; + } + // remember the destination, unless it is still the start-up default: that + // one is not a "previous" destination and would only clutter the list + if (info.dir == saveDefaultDir_.toStdString() && + info.baseName == saveDefaultBase_.toStdString()) { + printf("[save] %s/%s is the default destination, not added to the " + "previous-destinations list\n", + info.dir.c_str(), info.baseName.c_str()); + fflush(stdout); + } else { + saveDestListUpsert(info.dir, info.baseName); + } + // keep the CLI defaults in sync so later runs start from the last choice + g_cfg.h5Dir = info.dir; + g_cfg.h5Base = info.baseName; + const RoiSaver::Status st = sv.status(); + statusLabel->setText(QString("Status: saving to %1") + .arg(QString::fromStdString(st.h5Path))); + applySaveLock(true); // nothing that changes the file from here on + updateSaveLabel(); +} + +void MainWindow::browseSaveDir() { + const QString dir = QFileDialog::getExistingDirectory( + this, "Directory for the ROI save", + saveDirEdit_->text().isEmpty() ? QDir::currentPath() + : saveDirEdit_->text()); + if (!dir.isEmpty()) + saveDirEdit_->setText(dir); +} + +// List of destinations used before, most recent first. +void MainWindow::saveDestDialog() { + std::vector list = saveDestListLoad(); + if (list.empty()) { + QMessageBox::information( + this, "Previous destinations", + QString("No destination has been used yet.\n\nThey are recorded " + "when a save starts; the list lives in %1.") + .arg(QString::fromStdString(g_cfg.saveListFile))); + return; + } + + QDialog dialog(this); + dialog.setWindowTitle("Previous save destinations"); + dialog.resize(560, 300); + auto *lay = new QVBoxLayout(&dialog); + auto *table = new QTableWidget(static_cast(list.size()), 3, &dialog); + table->setHorizontalHeaderLabels( + QStringList() << "directory" << "base name" << "last used"); + table->verticalHeader()->setVisible(false); + table->setSelectionBehavior(QAbstractItemView::SelectRows); + table->setSelectionMode(QAbstractItemView::SingleSelection); + table->setEditTriggers(QAbstractItemView::NoEditTriggers); + const QLocale loc; + for (size_t i = 0; i < list.size(); i++) { + const QString when = QDateTime::fromMSecsSinceEpoch(list[i].usedMs) + .toString("yyyy-MM-dd HH:mm:ss"); + table->setItem(static_cast(i), 0, + new QTableWidgetItem( + QString::fromStdString(list[i].dir))); + table->setItem(static_cast(i), 1, + new QTableWidgetItem( + QString::fromStdString(list[i].base))); + table->setItem(static_cast(i), 2, new QTableWidgetItem(when)); + } + table->resizeColumnsToContents(); + table->selectRow(0); + lay->addWidget(table); + + auto *btns = new QDialogButtonBox(&dialog); + btns->addButton("Use", QDialogButtonBox::AcceptRole); + auto *delBtn = btns->addButton("Delete", QDialogButtonBox::DestructiveRole); + btns->addButton(QDialogButtonBox::Cancel); + lay->addWidget(btns); + connect(btns, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(btns, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + connect(table, &QTableWidget::itemDoubleClicked, &dialog, + [&dialog](QTableWidgetItem *) { dialog.accept(); }); + bool deleted = false; + connect(delBtn, &QPushButton::clicked, &dialog, [&]() { + const int r = table->currentRow(); + if (r < 0 || r >= static_cast(list.size())) + return; + saveDestListRemove(list[r].dir, list[r].base); + deleted = true; + table->removeRow(r); + }); + + if (dialog.exec() != QDialog::Accepted) { + if (deleted) + statusLabel->setText("Status: destination removed from the list"); + return; + } + const int row = table->currentRow(); + if (row < 0 || row >= static_cast(list.size())) + return; + saveDirEdit_->setText(QString::fromStdString(list[row].dir)); + saveBaseEdit_->setText(QString::fromStdString(list[row].base)); + statusLabel->setText( + QString("Status: destination set to %1 / %2") + .arg(QString::fromStdString(list[row].dir)) + .arg(QString::fromStdString(list[row].base))); +} + +void MainWindow::updateSaveLabel() { + if (!saveVal_) + return; + const RoiSaver::Status st = pipeline_.saver().status(); + // in photon-map mode the frames counter never moves, so report the maps + const qulonglong n = st.photonMode ? st.pmaps : st.saved; + const QString what = st.photonMode ? (n == 1 ? "map" : "maps") + : (n == 1 ? "frame" : "frames"); + // only mention the queue when something was actually dropped: "0 dropped + // (queue full)" claimed a problem that is not there + const QString dropNote = st.dropped + ? QString(" (writer behind: queue full)") + : QString(" (nothing lost)"); + if (st.running) { + saveVal_->setText(QString("%1\n%2 %3 written, %4 dropped%5") + .arg(QString::fromStdString(st.h5Path)) + .arg(n) + .arg(what) + .arg(st.dropped) + .arg(dropNote)); + } else if (st.h5Path.empty()) { + saveVal_->setText("idle"); + } else { + saveVal_->setText( + QString("%1\n%2 %3 written, %4 dropped - stopped (%5)") + .arg(QString::fromStdString(st.h5Path)) + .arg(n) + .arg(what) + .arg(st.dropped) + .arg(QString::fromStdString(st.stopReason))); + } +} + +void MainWindow::acquirePede() { + if (!pipeline_.running()) { + QMessageBox::information(this, "Acquire pedestal", + "The acquisition pipeline is not running."); + return; + } + if (pipeline_.pedeAcquireActive()) + return; + pipeline_.requestPedeAcquire(g_cfg.pedeFrames); + statusLabel->setText("Status: acquiring pede..."); +} + +void MainWindow::savePede() { + std::string werr; + if (pedeSaveFile(g_cfg.pedeFile, werr)) { + printf("[pede] saved %s\n", g_cfg.pedeFile.c_str()); + statusLabel->setText(QString("Status: pede saved to %1") + .arg(QString::fromStdString(g_cfg.pedeFile))); + } else { + fprintf(stderr, "[pede] save failed: %s\n", werr.c_str()); + pipeline_.gate().setLastError("savePedestal: " + werr); + QMessageBox::warning(this, "Save Pedestal", + QString::fromStdString(werr)); + } +} + +void MainWindow::readPede() { + std::string perr; + if (pedeLoadFile(g_cfg.pedeFile, perr)) { + printf("[pede] read %s\n", g_cfg.pedeFile.c_str()); + pipeline_.gate().setLastError(""); + statusLabel->setText("Status: pede read"); + } else { + fprintf(stderr, "[pede] read failed: %s\n", perr.c_str()); + pipeline_.gate().setLastError("loadPedestal: " + perr); + QMessageBox::warning(this, "Read Pedestal", + QString::fromStdString(perr)); + } +} + +void MainWindow::loadAllPede() { + // The original only printed "not implemented". It is genuinely useful in + // the merged program (it is the only way to load a full G0/G1/G2 set from + // an arbitrary path), so it opens a file chooser now. + const QString path = QFileDialog::getOpenFileName( + this, "Load pedestal file", QDir::currentPath(), + "Pedestal files (*.dat *.txt);;All files (*)"); + if (path.isEmpty()) + return; + std::string perr; + if (pedeLoadFile(path.toStdString(), perr)) { + cout << "[pede] loaded " << path.toStdString() << endl; + pipeline_.gate().setLastError(""); + statusLabel->setText(QString("Status: pede loaded from %1").arg(path)); + } else { + fprintf(stderr, "[pede] load failed: %s\n", perr.c_str()); + pipeline_.gate().setLastError("loadPedestal: " + perr); + QMessageBox::warning(this, "Load Pedestal", + QString::fromStdString(perr)); + } +} + +// The live value range of the main plot: what its colour scale is using right +// now, whatever set it (the defaults, this dialog, or a drag on the ROOT +// canvas). Read from the displayed histogram, so the dialog always opens on +// the values the operator is actually looking at. +// The ADC image (the "main plot") keeps its own colour scale, which the +// operator can also drag on the colour bar: read what the widget is using. +void MainWindow::liveAdcImageRange(int &lo, int &hi) const { + if (m_adcView) { + double a = 0, b = 0; + m_adcView->colorRange(a, b); + lo = static_cast(std::floor(a)); + hi = static_cast(std::ceil(b)); + if (lo != hi) + return; + } + lo = adcmin.load(); + hi = adcmax.load(); +} + +// The ROOT 2D map (canvas A5) keeps its colour scale in min/max. +void MainWindow::liveMapRange(int &lo, int &hi) const { + if (m_photonView) { + double a = 0, b = 0; + m_photonView->colorRange(a, b); + lo = static_cast(a); + hi = static_cast(b); + if (lo != hi) + return; + } + lo = pmmin.load(); + hi = pmmax.load(); +} + +// Same for the shared x window of the spectra. +void MainWindow::liveChipRange(int &lo, int &hi) const { + if (dispChip[0] && dispChip[0]->GetXaxis()) { + TAxis *ax = dispChip[0]->GetXaxis(); + lo = static_cast(std::floor(ax->GetBinLowEdge(ax->GetFirst()))); + hi = static_cast(std::ceil(ax->GetBinUpEdge(ax->GetLast()))); + if (lo != hi) + return; + } + lo = chipxmin.load(); + hi = chipxmax.load(); +} + +// Calibration popup: pick (or change) the gain-map file used for the keV +// spectra. Everything else keeps working in ADU. +void MainWindow::loadGainMap() { + const QString start = + g_cfg.gainMapFile.empty() ? QDir::homePath() + : QString::fromStdString(g_cfg.gainMapFile); + const QString path = QFileDialog::getOpenFileName( + this, "Select gain map (g0,g1,g2,hg0,hg1,hg2 - 1024x512 doubles)", start, + "Gain maps (*.bin *.dat *.raw);;All files (*)"); + if (path.isEmpty()) + return; + std::string err; + if (!gainMapLoadFile(path.toStdString(), err)) { + fprintf(stderr, "[gain] %s\n", err.c_str()); + QMessageBox::warning(this, "Gain map", QString::fromStdString(err)); + return; + } + g_cfg.gainMapFile = path.toStdString(); + const QString base = QFileInfo(path).fileName(); + if (kevChk_) + kevChk_->setChecked(true); // show what was just calibrated + updateFlow(); + statusLabel->setText( + QString("Status: gain map %1 loaded - PH spectra also in keV") + .arg(base)); +} + +void MainWindow::showRangeDialog() { + QDialog dialog(this); + dialog.setWindowTitle("Set Ranges"); + auto *form = new QFormLayout(&dialog); + + int imgLo = 0, imgHi = 0, mapLo = 0, mapHi = 0, chipLo = 0, chipHi = 0; + liveAdcImageRange(imgLo, imgHi); + liveMapRange(mapLo, mapHi); + liveChipRange(chipLo, chipHi); + + auto *adcMinEdit = new QSpinBox(&dialog); + adcMinEdit->setRange(-100000, 100000); + adcMinEdit->setValue(imgLo); + auto *adcMaxEdit = new QSpinBox(&dialog); + adcMaxEdit->setRange(-100000, 100000); + adcMaxEdit->setValue(imgHi); + auto *pmMinEdit = new QSpinBox(&dialog); + pmMinEdit->setRange(-100000, 100000); + pmMinEdit->setValue(mapLo); + auto *pmMaxEdit = new QSpinBox(&dialog); + pmMaxEdit->setRange(-100000, 100000); + pmMaxEdit->setValue(mapHi); + auto *chMinEdit = new QSpinBox(&dialog); + chMinEdit->setRange(-100000, 100000); + chMinEdit->setValue(chipLo); + auto *chMaxEdit = new QSpinBox(&dialog); + chMaxEdit->setRange(-100000, 100000); + chMaxEdit->setValue(chipHi); + + form->addRow("ADC min (ADC image):", adcMinEdit); + form->addRow("ADC max (ADC image):", adcMaxEdit); + form->addRow("P.Map min (ROOT map):", pmMinEdit); + form->addRow("P.Map max (ROOT map):", pmMaxEdit); + form->addRow("PH min (spectra):", chMinEdit); + form->addRow("PH max (spectra):", chMaxEdit); + auto *hint = new QLabel( + "Each field opens on the scale that display is using now; nothing " + "changes unless you edit it.", + &dialog); + hint->setWordWrap(true); + form->addRow(hint); + + auto *buttons = new QDialogButtonBox( + QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); + form->addRow(buttons); + connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + + if (dialog.exec() == QDialog::Accepted) { + setRanges(adcMinEdit->value(), adcMaxEdit->value(), pmMinEdit->value(), + pmMaxEdit->value(), chMinEdit->value(), chMaxEdit->value()); + } +} + +void MainWindow::setRanges(int amin, int amax, int pmin, int pmax, int cmin, + int cmax) { + int imgLo = 0, imgHi = 0, mapLo = 0, mapHi = 0, chipLo = 0, chipHi = 0; + liveAdcImageRange(imgLo, imgHi); + liveMapRange(mapLo, mapHi); + liveChipRange(chipLo, chipHi); + + // the ADC pair is the colour scale of the ADC (main) image - a Qt widget, + // so it is applied straight away - and the P.Map pair the ROOT 2D map, + // which must be changed on the ROOT thread + adcmin = amin; + adcmax = amax; + pmmin = pmin; + pmmax = pmax; + chipxmin = cmin; + chipxmax = cmax; + if ((amin != imgLo || amax != imgHi) && m_adcView) { + m_adcView->setColorRange(amin, amax); + if (m_adcScale_) + m_adcScale_->syncFromView(); + cout << "Ranges set: ADC image [" << amin << "," << amax << "]" + << endl; + } + if (pmin != mapLo || pmax != mapHi) { + fixranges = true; + RootCmd c; + c.type = RootCmdType::SetMapRange; + c.a = pmin; + c.b = pmax; + pushRootCmd(c); + cout << "Ranges set: ROOT map z [" << pmin << "," << pmax << "]" + << endl; + } + if (cmin != chipLo || cmax != chipHi) { + RootCmd c; + c.type = RootCmdType::SetChipRange; + c.a = cmin; + c.b = cmax; + pushRootCmd(c); + cout << "Ranges set: PH x [" << cmin << "," << cmax << "]" << endl; + } + forceRootRedraw(); +} + +void MainWindow::showRateDialog() { + QDialog dialog(this); + dialog.setWindowTitle("Update Rates"); + auto *form = new QFormLayout(&dialog); + + auto *mainRateSpin = new QSpinBox(&dialog); + mainRateSpin->setRange(1, 10000); + mainRateSpin->setValue(HDraw_every.load()); + form->addRow("Main image (frames):", mainRateSpin); + + auto *rootMsSpin = new QSpinBox(&dialog); + rootMsSpin->setRange(50, 60000); + rootMsSpin->setSingleStep(100); + rootMsSpin->setValue(rootUpdateMs.load()); + form->addRow("ROOT update (ms):", rootMsSpin); + + auto *chipSpin = new QSpinBox(&dialog); + chipSpin->setRange(1, 100); + chipSpin->setValue(chipUpdateEvery.load()); + chipSpin->setToolTip( + "Redraw the chip-spectrum canvas (A4) every N-th ROOT redraw. It is\n" + "the most expensive canvas, so a larger N keeps the other canvases\n" + "responsive."); + form->addRow("Chip spectra every N redraws:", chipSpin); + + auto *effLabel = new QLabel( + QString("ROOT redraws every %1 ms when frames arrived") + .arg(rootUpdateMs.load()), + &dialog); + effLabel->setWordWrap(true); + form->addRow(effLabel); + connect(rootMsSpin, QOverload::of(&QSpinBox::valueChanged), + [effLabel](int ms) { + effLabel->setText( + QString("ROOT redraws every %1 ms when frames arrived") + .arg(ms)); + }); + + auto *buttons = new QDialogButtonBox( + QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); + form->addRow(buttons); + connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + + if (dialog.exec() == QDialog::Accepted) { + HDraw_every = mainRateSpin->value(); + rootUpdateMs = rootMsSpin->value(); + chipUpdateEvery = chipSpin->value(); + updateDrawLabel(); + cout << "Rates: main=" << HDraw_every.load() + << " rootMs=" << rootUpdateMs.load() << endl; + } +} + +void MainWindow::updateDrawLabel() { + if (drawLabel) + drawLabel->setText(QString("Main: every %1 frames | ROOT: every %2 ms") + .arg(HDraw_every.load()) + .arg(rootUpdateMs.load())); +} + +// The ADC / gain / photon (and, under JFSTRX, the strixel) windows are separate +// top-level widgets, and the ROOT canvases live in the ROOT thread: closing the +// control window used to leave them on screen, and with them the process alive +// (Qt only quits on the *last* window). Close them all, then leave the loop. +// The display chain and the saved data do not have to agree about the +// pedestal: the display follows the Pede/Raw toggle, while the file is +// corrected only when "Pedestal corrected" is ticked - and the photon maps +// always need the pedestal (their save is refused without it). +void MainWindow::flowStageStates(bool &displayPede, bool &savePede, bool &gateOn, + bool &pmap) const { + displayPede = PedestalStore::instance().loaded() && !gNoPede.load(); + pmap = (gSaveMode.load() == 1); + savePede = pmap || ((saveCorrected_ && saveCorrected_->isChecked()) && + !gNoPede.load()); + gateOn = !gateChk_ || gateChk_->isChecked(); +} + +void MainWindow::closeSideWindows() { + QWidget *wins[] = {m_adcWin, m_gainWin, m_photonWin, +#ifdef JFSTRX + m_adcPanelWin, +#endif + nullptr}; + for (QWidget *w : wins) { + if (!w) + continue; + w->hide(); + w->close(); + } +} + +// While a save is running everything that would change what lands in the file is +// locked: its JSON describes the settings at save start, so a quiet change +// halfway would leave a file nobody can interpret. Display-only controls (and +// the pedestal tracker, which is meant to follow drift during a run) stay live; +// the gate threshold and min-pixels stay live but ask first. +void MainWindow::applySaveLock(bool saving) { + auto lockGroup = [&](QGroupBox *g, std::initializer_list keep) { + if (!g) + return; + for (QObject *c : g->children()) { + auto *w = qobject_cast(c); + if (!w) + continue; + bool kept = false; + for (QWidget *k : keep) + if (k == w) + kept = true; + w->setEnabled(!saving || kept); + } + }; + lockGroup(saveGroup_, {btnSaveStart, saveVal_}); // Start/Stop must stay live + lockGroup(pedeGroup_, {}); // manual pede work changes the saved data + lockGroup(gainGroup_, {kevChk_, gainStateChk_}); // keV PH is display only + lockGroup(scGroup_, {}); // the cell filter changes which frames pass + lockGroup(sigGroup_, {thrSpin_, minPixSpin_}); // the two that ask first + lockGroup(pmapGroup_, {pmapCumulative_, pmapEverySpin_}); // display-only pair + if (btnResetCounters) + btnResetCounters->setEnabled(!saving); + for (QWidget *w : {btnSelectRoi_, btnUnsetRoi_, btnLoadRoi}) + if (w) + w->setEnabled(!saving); // they move the ROI, i.e. the saved crop + // the photon threshold scales the counts that are written in photon-map + // mode, so it is locked there and stays live while frames are saved + if (photonThrSpin_) + photonThrSpin_->setEnabled(!(saving && gSaveMode.load() == 1)); + if (saveCorrected_) // only meaningful for the frames mode + saveCorrected_->setEnabled(!(gSaveMode.load() == 1) && !saving); +} + +bool MainWindow::confirmGateChangeDuringRun(const QString &what) { + if (!pipeline_.saver().status().running) + return true; + // With the gate off (passthrough) the threshold and min-pixels decide + // nothing about the saved frames, so there is nothing to confirm. + if (!pipeline_.gate().enabled()) + return true; + const QMessageBox::StandardButton a = QMessageBox::question( + this, "Gate setting", + QString("Changing settings during a run, are you sure?\n\n%1\n\nThe " + "file being written keeps the gate criterion it started with; a " + "note is added to its JSON sidecar.") + .arg(what), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (a != QMessageBox::Yes) { + printf("[save] gate change refused during the run (%s)\n", + what.toLocal8Bit().constData()); + fflush(stdout); + return false; + } + gGateChangedDuringSave.store(true); + printf("[save] gate setting changed during the run: %s (noted in the JSON)\n", + what.toLocal8Bit().constData()); + fflush(stdout); + return true; +} + +// "Unset ROI": drop the current rectangle, so the gate, the ROI spectra and the +// saved data are back to the full frame. The ROI list on disk is untouched. +void MainWindow::unsetRoi() { + if (!pipeline_.gate().roiSet()) { + statusLabel->setText("Status: no ROI set (already the full frame)"); + return; + } + // release the restriction first, so its own message stays truthful + if (btnGateRoi && btnGateRoi->isChecked()) + btnGateRoi->setChecked(false); + pipeline_.gate().clearRoi(); + pipeline_.gate().resetCounters(); + if (btnSelectRoi_ && btnSelectRoi_->isChecked()) + btnSelectRoi_->setChecked(false); // leaves the editor + if (m_adcView) + m_adcView->setOverlayRect(0, 0, 0, 0, false); + if (gateRoiVal_) + gateRoiVal_->setText("full frame"); + printf("[roi] ROI unset: full frame\n"); + fflush(stdout); + updateRoiLabel(); + statusLabel->setText("Status: ROI unset (full frame)"); +} + +void MainWindow::quitApp() { + if (!confirmQuit()) + return; + stop(); + closeSideWindows(); + QApplication::quit(); +} + +// --------------------------------------------------------------------------- +// command line +// --------------------------------------------------------------------------- +void printUsage(const char *prog) { + printf("Usage: %s [rx_port] [gate_threshold] [min_pixels]\n" + " [pede_file] [--pub PORT] [--contig] [--no-gate]\n" + " a port may carry its slab placement:\n" + " 30026+30027 order of the list = bottom, top\n" + " 30027:top one interface, upper half\n" + " 30026:bottom one interface, lower half\n" + " [--pede-frames N]\n" + "\n" + " rx_host slsReceiver host\n" + " rx_port slsReceiver zmq port, or a '+' list for the\n" + " multi-interface (N UDP interfaces) mode, ordered\n" + " bottom -> top: 30001+30002 = (bot,top)\n" + " (default %d)\n" + " gate_threshold event-finder gate threshold, pede-subtracted ADC\n" + " (default %d)\n" + " min_pixels gate: pass when >= N pixels are above threshold\n" + " (default 5)\n" + " pede_file pedestal file (default %s)\n" + "\n" + " The photon-map threshold (ADU, default %d) is a control in the\n" + " Display panel, not an argument; --raw-map shows raw ADC there\n" + " instead of photon counts.\n" + "\n" + " --pub PORT additionally republish gated frames on PORT\n" + " --contig enable the contiguity proxy (r = N/2+1)\n" + " --hot-suppress learn and suppress hot pixels in the gate\n" + " --hot-frac P a pixel is hot above P % of the frames\n" + " (default 10)\n" + " --gate enable the gate (it is OFF by default)\n" + " --no-gate disable the gate (same as the default)\n" + " --raw-map show raw ADC in the main map instead of\n" + " photon counts (threshold unused then)\n" + " --save-budget N stop after N saved frames, or N saved photon\n" + " maps in virtual photon counting mode\n" + " --apply-gain start with the gain correction on (the photon\n" + " threshold then means keV, default 4 keV)\n" + " --start-save start saving at startup (same as pressing\n" + " the Save button); combine with --save-photon\n" + " --save-photon write the virtual photon counting maps instead\n" + " of the frames (see the Save panel)\n" + " --pmap-every N display cadence of the photon map (default 2000)\n" + " --pmap-save-every N\n" + " saved map cadence, minimum 200 (default 2000)\n" + " --pede-tracking follow the pedestal drift from the frames the\n" + " signal gate drops (see the Pedestal panel)\n" + " --no-pede ignore the pedestal everywhere (same as the\n" + " No pede button); this is the STARTUP DEFAULT,\n" + " so the gate threshold applies to raw 14-bit ADC\n" + " --use-pede start with pedestal subtraction enabled\n" + " --stats print the counter line on stdout every second\n" + " --decimate N process 1 frame in N (N = 2, 4 or 8); the frames\n" + " are dropped by detector frame number, so both\n" + " interfaces drop the same ones\n" + " --auto-decimate let the pipeline decimate on a real analysis\n" + " backlog (off by default: --decimate is manual)\n" + " --acquire-pede average --pede-frames frames into a live\n" + " pedestal as soon as acquisition starts\n" + " --pede-frames N frames averaged by \"Acquire Pede\" (default %d)\n" + " --analysis-workers N\n" + " threads for the per-pixel analysis loops\n" + " (default: auto = cores/4, capped at 8; 1 =\n" + " single threaded). Scales across cores with no\n" + " extra memory and bit-identical results.\n" + " --h5-dir DIR directory for ROI-save HDF5 + JSON (default .)\n" + " --h5-base NAME file-name base, file is __f0_0.h5\n" + " (default jfroi)\n" + " --gainmap PATH gain-map file (6 x 1024x512 doubles: g0 g1 g2\n" + " hg0 hg1 hg2). g0 is ADU/keV and is used to\n" + " fill a second set of PH spectra in keV; the ADC\n" + " image, the photon map and the HDF5 writer stay\n" + " in ADU. Can also be loaded from the GUI.\n" + " --roi-file PATH gate-ROI list file (default ./roi_list.dat)\n" + " --sc N pass only frames from storage cell N (0..15,\n" + " bits 8-11 of detSpec3; 'all' = no SC gating).\n" + " Checked before the signal gate, so other cells\n" + " cost no CPU. The GUI has the same selector.\n" + " --gate-roi X Y W H\n" + " start with a gate ROI (data rows: x to the\n" + " right, y upwards, (0,0) = bottom-left; same\n" + " convention as the ROI list and the ROOT plots)\n" + " --rx-hwm N receive high-water mark per interface (default\n" + " %d); smaller = lower latency, more drops\n" + "\n" + "Run chain: slsReceiver -> %s (no external event finder, no\n" + "intermediate zmq hop: the gate and the display share one process)\n", + prog, DEFAULT_RX_PORT, + static_cast(PHOTON_THRESHOLD_DEFAULT), PEDEFNAME, + static_cast(PHOTON_THRESHOLD_DEFAULT), DEFAULT_N_PEDE_FRAMES, + 1000, prog); +} + +bool parseCommandLine(int argc, char **argv, Config &cfg, bool &help, + std::string &error) { + help = false; + std::vector pos; + for (int i = 1; i < argc; i++) { + const std::string a = argv[i]; + if (a == "-h" || a == "--help") { + help = true; + return true; + } else if (a == "--pub") { + if (i + 1 >= argc) { + error = "--pub needs a port"; + return false; + } + cfg.pubPort = std::atoi(argv[++i]); + } else if (a == "--contig") { + cfg.contiguous = true; + } else if (a == "--hot-suppress") { + cfg.hotSuppress = true; + } else if (a == "--hot-frac") { + cfg.hotFraction = std::atof(argv[++i]) / 100.0; + } else if (a == "--pmap-every") { + if (i + 1 >= argc) { + error = "--pmap-every needs a count"; + return false; + } + const int v = std::atoi(argv[++i]); + gPmapEvery = v > 0 ? v : 1; + } else if (a == "--pmap-save-every") { + if (i + 1 >= argc) { + error = "--pmap-save-every needs a count (>= 200)"; + return false; + } + const int v = std::atoi(argv[++i]); + gPmapSaveEvery = v < 200 ? 200 : v; + } else if (a == "--save-photon") { + gSaveMode = 1; // same as the Save panel's radio button + } else if (a == "--start-save") { + cfg.startSave = true; // start saving as soon as we are up + } else if (a == "--apply-gain") { + // the gain stage is the only one without a CLI mirror; needed for + // scripted keV runs (and it makes the keV photon threshold testable) + cfg.applyGain = true; + } else if (a == "--save-budget") { + if (i + 1 >= argc) { + error = "--save-budget needs a count"; + return false; + } + cfg.saveBudget = static_cast(std::atoll(argv[++i])); + } else if (a == "--pede-tracking") { + gPedeTracking = true; // same as the Pedestal panel button + } else if (a == "--no-pede") { + // same as the "Pede/Raw toggle" button: no pedestal subtraction + // anywhere, so the gate threshold applies to the raw 14-bit value + gNoPede = true; + } else if (a == "--use-pede") { + // re-enable pedestal subtraction (the startup default is --no-pede) + gNoPede = false; + } else if (a == "--raw-map") { + cfg.rawMap = true; // his3000 shows raw ADC instead of counts + } else if (a == "--no-gate") { + cfg.gateEnabled = false; + } else if (a == "--gate") { + cfg.gateEnabled = true; // the gate is off by default + } else if (a == "--decimate") { + const int v = std::atoi(argv[++i]); + cfg.decimate = (v == 2 || v == 4 || v == 8) ? v : 1; + g_keepDiv.store(cfg.decimate); // frames are dropped from now on + } else if (a == "--auto-decimate") { + cfg.autoDecimate = true; + g_autoDecimate.store(true); + } else if (a == "--stats") { + cfg.printCounters = true; + } else if (a == "--acquire-pede") { + cfg.acquirePede = true; + } else if (a == "--h5-base") { + if (i + 1 >= argc) { + error = "--h5-base needs a name"; + return false; + } + cfg.h5Base = argv[++i]; + } else if (a == "--h5-dir") { + if (i + 1 >= argc) { + error = "--h5-dir needs a path"; + return false; + } + cfg.h5Dir = argv[++i]; + } else if (a == "--gainmap") { + if (i + 1 >= argc) { + error = "--gainmap needs a file"; + return false; + } + cfg.gainMapFile = argv[++i]; + cfg.gainMapExplicit = true; + } else if (a == "--roi-file") { + if (i + 1 >= argc) { + error = "--roi-file needs a path"; + return false; + } + cfg.roiFile = argv[++i]; + } else if (a == "--sc") { + if (i + 1 >= argc) { + error = "--sc needs a value (0..15, or all)"; + return false; + } + const std::string v = argv[++i]; + if (v == "all" || v == "ALL") { + cfg.scFilter = -1; + } else { + const int n = std::atoi(v.c_str()); + if (n < 0 || n > 15) { + error = "--sc must be 0..15 or all"; + return false; + } + cfg.scFilter = n; + } + } else if (a == "--gate-roi") { + // X Y W H in data rows (row 0 = bottom), same as the ROI list + if (i + 4 >= argc) { + error = "--gate-roi needs X Y W H"; + return false; + } + cfg.gateRoiX = std::atoi(argv[++i]); + cfg.gateRoiY = std::atoi(argv[++i]); + cfg.gateRoiW = std::atoi(argv[++i]); + cfg.gateRoiH = std::atoi(argv[++i]); + if (cfg.gateRoiW < 1 || cfg.gateRoiH < 1) { + error = "--gate-roi needs a positive width and height"; + return false; + } + } else if (a == "--analysis-workers") { + if (i + 1 >= argc) { + error = "--analysis-workers needs a count"; + return false; + } + const int n = std::atoi(argv[++i]); + if (n < 1 || n > 32) { + error = "--analysis-workers must be 1..32"; + return false; + } + cfg.analysisWorkers = n; + } else if (a == "--rx-hwm") { + if (i + 1 >= argc) { + error = "--rx-hwm needs a value"; + return false; + } + const int n = std::atoi(argv[++i]); + if (n < 1) { + error = "--rx-hwm must be >= 1"; + return false; + } + cfg.rxHwm = n; + } else if (a == "--pede-frames") { + if (i + 1 >= argc) { + error = "--pede-frames needs a count"; + return false; + } + const int n = std::atoi(argv[++i]); + if (n < 1) { + error = "--pede-frames must be >= 1"; + return false; + } + cfg.pedeFrames = n; + } else if (a == "--pede") { + if (i + 1 >= argc) { + error = "--pede needs a file"; + return false; + } + cfg.pedeFile = argv[++i]; + cfg.pedeExplicit = true; + } else if (!a.empty() && a[0] == '-' && a.size() > 1 && + !isdigit(static_cast(a[1]))) { + error = "unknown option: " + a; + return false; + } else { + pos.push_back(a); + } + } + + if (pos.empty()) { + error = "need at least "; + return false; + } + cfg.rxHost = pos[0]; + // the photon-map threshold is a Display control (ADU, default 150); the + // raw-ADC map is still available with --raw-map + cfg.dophotonmap = !cfg.rawMap; + cfg.threshold = static_cast(cfg.phene / 2); // default gate threshold + if (pos.size() >= 2) { + // "30001" (single interface) or "30001+30002" (multi-interface, + // ordered BOTTOM -> TOP in the assembled image) + cfg.rxPorts.clear(); + std::string spec = pos[1]; + size_t start = 0; + while (start <= spec.size()) { + const size_t plus = spec.find('+', start); + const std::string tok = + spec.substr(start, (plus == std::string::npos) + ? std::string::npos + : plus - start); + if (tok.empty()) { + error = "bad rx_port list '" + spec + "'"; + return false; + } + // "30027:top" / "30026:bottom" place a single interface's slab + // inside the module without needing its partner. + std::string num = tok; + int slab = 0; + const size_t colon = tok.find(':'); + if (colon != std::string::npos) { + num = tok.substr(0, colon); + const std::string where = tok.substr(colon + 1); + if (where == "top") + slab = 2; + else if (where == "bottom") + slab = 1; + else { + error = "bad placement '" + where + "' in '" + spec + + "' (use :top or :bottom)"; + return false; + } + } + const int port = std::atoi(num.c_str()); + if (port <= 0 || port > 65535) { + error = "bad rx_port '" + tok + "' in '" + spec + "'"; + return false; + } + cfg.rxPorts.push_back(port); + cfg.rxSlab.push_back(slab); + if (plus == std::string::npos) + break; + start = plus + 1; + } + if (cfg.rxPorts.empty() || + cfg.rxPorts.size() > static_cast(JF_MAX_PORTS)) { + error = "rx_port list must have 1.." + + std::to_string(JF_MAX_PORTS) + " entries"; + return false; + } + } + if (pos.size() >= 3) + cfg.threshold = static_cast(std::atof(pos[2].c_str())); + if (pos.size() >= 4) + cfg.minPixels = std::strtoull(pos[3].c_str(), nullptr, 10); + if (pos.size() >= 5) { + cfg.pedeFile = pos[4]; + cfg.pedeExplicit = true; + } + if (cfg.rxHost.empty()) { + error = "rx_host is empty"; + return false; + } + if (cfg.analysisWorkers == 0) { // auto + unsigned hw = std::thread::hardware_concurrency(); + if (hw == 0) + hw = 4; + cfg.analysisWorkers = + static_cast(std::min(8u, std::max(1u, hw / 4))); + } + return true; +} + +// --------------------------------------------------------------------------- +int main(int argc, char *argv[]) { + Config cfg; + bool help = false; + std::string err; + if (!parseCommandLine(argc, argv, cfg, help, err)) { + if (!err.empty()) + fprintf(stderr, "error: %s\n", err.c_str()); + printUsage(argv[0]); + return help ? 0 : 2; + } + if (help) { + printUsage(argv[0]); + return 0; + } + + QApplication app(argc, argv); + g_cfg = cfg; + g_pheneThreshold = PHOTON_THRESHOLD_DEFAULT; // photon counting, ADU + g_pheneThresholdKev = PHOTON_THRESHOLD_KEV_DEFAULT; // same, keV (gain on) + cfg.phene = static_cast(2 * PHOTON_THRESHOLD_DEFAULT); + dophotonmap.store(cfg.dophotonmap); + + printf("=== Online JF display + event finder (single process) ===\n"); + { + std::string ports; + for (size_t k = 0; k < cfg.rxPorts.size(); k++) { + if (k) + ports += "+"; + ports += std::to_string(cfg.rxPorts[k]); + } + printf("input : %s:%s (subscriber)%s\n", cfg.rxHost.c_str(), + ports.c_str(), + cfg.rxPorts.size() > 1 + ? " [multi-interface: slabs stacked bottom -> top]" + : ""); + } + printf("photon map: threshold %.0f ADU, %.1f keV while the gain correction " + "is on (Virtual photon counting panel)%s\n", + static_cast(g_pheneThreshold), + static_cast(PHOTON_THRESHOLD_KEV_DEFAULT), + cfg.dophotonmap ? "" : " [raw ADC map]"); + printf("gate : %s, threshold %.1f, min_pixels %llu%s\n", + cfg.gateEnabled ? "enabled" : "PASSTHROUGH", cfg.threshold, + static_cast(cfg.minPixels), + cfg.contiguous ? ", contiguous" : ""); + printf("pede : %s\n", cfg.pedeFile.c_str()); + printf("threads: analysis workers %d\n", cfg.analysisWorkers); + if (cfg.pubPort > 0) + printf("pub : gated frames republished on port %d\n", cfg.pubPort); + + MainWindow window; + window.show(); + window.start(); + return app.exec(); +} diff --git a/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/online_jf_gui.cpp~ b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/online_jf_gui.cpp~ new file mode 100644 index 000000000..8dbdd26c5 --- /dev/null +++ b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/online_jf_gui.cpp~ @@ -0,0 +1,4482 @@ +// SPDX-License-Identifier: LGPL-3.0-or-other +// =========================================================================== +// online_jf_gui.cpp -- Qt5 GUI, fast image widgets, CLI parsing, main(). +// +// Merged from: +// onlinedisp_zmq.cpp (FastImageView, MainWindow, display controls) +// jf_event_finder_gui.cpp (gate parameters, counters, pedestal loading) +// +// The GUI thread never touches ROOT graphics and never touches a frame +// buffer that another thread is writing: frames come from the analysis +// thread through ImageMailbox and are copied into the widgets' own storage. +// =========================================================================== +#include "online_jf.h" + +#include +#include + +#include + +namespace { + +std::sig_atomic_t g_sigCount = 0; + +void handleSigint(int) { + // Async-signal-safe: only count. The Qt timer performs the shutdown + // (blocking joins are not signal-safe). A second Ctrl-C force-quits. + if (++g_sigCount > 1) + std::_Exit(0); +} + +// --------------------------------------------------------------------------- +// The flow chart is drawn with arrow labels placed in the layout between the +// boxes: 1. storage cell -> Pedestal -> 2. signal gate -> Gain -> ROI save. +// A stage that is switched off is hidden, so the arrows that pointed at it +// disappear and the flow reads as the bypass it is. +// --------------------------------------------------------------------------- +// The strip between the two columns. It draws the connectors that cross from +// one column to the other as proper orthogonal arrows with a filled head, so +// the flow reads as one path instead of a few loose glyphs. +// Flow colours. The geometry of the diagram is fixed; only the class of an +// arrow can change (see FlowStrip::setSaveMode), so a bypassed stage is greyed +// as a panel while the data flow stays readable. +enum FlowClass { + FlowNormal = 0, // analysis / display flow + FlowPedestal, // the pedestal path (incl. the tracking feedback) + FlowData, // the chain that ends in the saved file -> orange + FlowOff // a feeder that is bypassed at the moment -> pale +}; +static QString flowColor(int cls) { + switch (cls) { + case FlowPedestal: + return QString("#a8c8e8"); // lighter blue + case FlowData: + return QString("#de7d14"); // orange + case FlowOff: + return QString("#c9c9c9"); // bypassed: drawn, but not carrying data + case FlowNormal: + default: + return QString("#5b9bd5"); // light blue + } +} + +class FlowStrip : public QWidget { + public: + explicit FlowStrip(QWidget *parent) : QWidget(parent) { + setMinimumWidth(64); + setToolTip( + "Data flow. Orange: everything the saved data travels through -\n" + "frames: storage cell -> Pedestal -> Gain -> ROI -> Save, photon\n" + "maps: ... -> Gain -> counting -> (ROI ->) Save (the maps always\n" + "use the pedestal). The ROI is fed by the Gain for frames and by\n" + "the counting for the maps, always in orange: it only exists for\n" + "the file. Light blue: the branches that only feed the display.\n" + "Lighter blue: the pedestal tracking pair. The display can use the\n" + "pedestal while the file does not (or the other way round), which is\n" + "why both feeders into the gate funnel can be drawn at once."); + // Fixed width for the strip, Preferred (not Expanding) height: an + // Expanding vertical policy made the whole column row as tall as the + // window, which left a large empty band above the save panel. + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + } + // The endpoints are computed by MainWindow::layoutFlow() (main thread, + // layout settled) and handed over as plain coordinates in this widget's + // space, so painting never has to walk the widget tree. + static const int kFunnelX = 24; // where the flow passes the gate + + struct Path { + int yFrom = 0, yTo = 0; + int dir = 1; // +1 left->right, -1 right->left, +2 right rail (Gain->ROI) + bool on = true; + int cls = FlowNormal; + int x = 0; // 0: default jog, else the vertical runs here + bool head = true; // false: the line merges into a junction + bool startAtJog = false; // begin on the vertical, not at an edge + }; + // Everything that depends on the stage states: which feeder carries the + // data (pedestal or the raw bypass), which branch feeds the file, and + // whether the gate funnel is active. Geometry never moves. + // `displayPede`: the display chain uses the pedestal (the Pede/Raw toggle). + // `savePede`: the data being written does (Pedestal corrected, or photon + // maps, which need g0). They can differ - the usual case is a corrected + // display with raw frames going to the file - so the two feeders into the + // funnel can be live at the same time, and their colour says what each one + // carries: orange for the file, light blue for the display. + void setStages(bool displayPede, bool savePede, bool gateOn, + bool photonMaps) { + const bool pedeUsed = displayPede || savePede; + // A route that carries nothing is not drawn at all (grey was more + // confusing than helpful). + p1_.on = pedeUsed; // storage cell -> Pedestal + p2_.on = pedeUsed; // Pedestal -> funnel + p8_.on = !savePede; // storage cell -> funnel: the raw save route + const int pedeCls = savePede ? FlowData : FlowNormal; + p1_.cls = pedeCls; // storage cell -> Pedestal + p2_.cls = pedeCls; // Pedestal -> funnel + p3_.cls = FlowData; // funnel -> Gain: always on the route to the file + p8_.cls = FlowData; // storage cell -> funnel (raw save) + // The ROI only exists for the file, so its input is always orange - and + // it comes from whichever stage produces the saved product: the Gain + // for frames, the counting for the maps. + p4_.cls = FlowData; // Gain -> ROI + p4_.on = !photonMaps; + p9_.cls = FlowData; // counting -> ROI (the maps are ROI-cropped too) + p9_.on = photonMaps; + p6_.cls = photonMaps ? FlowData : FlowNormal; // Gain -> counting + funOn_ = gateOn; + update(); + } + void setPaths(const Path &a, const Path &b, const Path &c, const Path &d, + const Path &e, const Path &f, const Path &g, const Path &h, + const Path &i, int funnelY) { + p1_ = a; + p2_ = b; + p3_ = c; + p4_ = d; + p5_ = e; + p6_ = f; + p7_ = g; + p8_ = h; + p9_ = i; + funY_ = funnelY; + update(); + } + + protected: + void paintEvent(QPaintEvent *) override { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing, true); + cross(p, p1_); // 1. storage cell -> Pedestal + cross(p, p2_); // Pedestal -> 2. signal gate + cross(p, p3_); // 2. signal gate -> Gain + cross(p, p4_); // Gain -> ROI, down the right-hand rail + cross(p, p5_); // 2. signal gate -> Pedestal tracking (dropped frames) + cross(p, p6_); // Gain -> Virtual photon counting (calibration) + cross(p, p7_); // Virtual photon counting -> Display (the map) + cross(p, p8_); // storage cell -> funnel: the raw (no-pede) bypass + cross(p, p9_); // Virtual photon counting -> ROI (maps) + drawFunnel(p); // the signal gate, drawn as the filter it is + } + + private: + Path p1_, p2_, p3_, p4_, p5_, p6_, p7_, p8_, p9_; + int funY_ = -1; + bool funOn_ = true; + + // The signal gate does not change the data, it only decides which frames + // travel on: it is drawn as a funnel on the line, with a short blue arrow + // from the gate panel into its side (the gate is what "gates"). With the + // gate switched off the symbol greys out instead of disappearing. + void drawFunnel(QPainter &p) { + if (funY_ < 0) + return; + const QColor c(flowColor(funOn_ ? FlowNormal : FlowOff)); + QPen pen(c); + pen.setWidth(2); + p.setPen(pen); + const int hw = 10, hh = 18; + QPolygon f; + f << QPoint(kFunnelX - hw, funY_ - hh / 2) + << QPoint(kFunnelX + hw, funY_ - hh / 2) + << QPoint(kFunnelX + hw / 3, funY_ + hh / 2) + << QPoint(kFunnelX - hw / 3, funY_ + hh / 2); + p.setBrush(Qt::NoBrush); + p.drawPolygon(f); + // the gate controls the funnel: arrow from the panel into its side + const int tipX = kFunnelX - hw / 3 - 1; + const int fromX = kFunnelX - hw - 14; + p.drawLine(fromX, funY_, tipX, funY_); + QPolygon head; + const int s = 6; + head << QPoint(tipX, funY_) << QPoint(tipX - s, funY_ - s) + << QPoint(tipX - s, funY_ + s); + p.setBrush(c); + p.drawPolygon(head); + p.setBrush(Qt::NoBrush); + } + + // dir = +1 : left edge -> right edge (head on the right) + // dir = -1 : right edge -> left edge (head on the left) + void cross(QPainter &p, const Path &l) { + if (!l.on) + return; + const int yFrom = l.yFrom, yTo = l.yTo, dir = l.dir; + const int w = width(); + QPen pen(QColor(flowColor(l.cls))); + pen.setWidth(2); + p.setPen(pen); + // The vertical part of each connector is put near the edge it ends on, + // and never in the middle: otherwise the three connectors line up into + // one long vertical that looks like it crosses the arrows. + const int jog = (l.x > 0) ? l.x : ((dir > 0) ? (w - 14) : 14); + QPolygon pts; + if (dir == 2) { // both ends on the right: a rail beside the panels + pts << QPoint(w, yFrom) << QPoint(w - 10, yFrom) + << QPoint(w - 10, yTo) << QPoint(w - 2, yTo); + p.drawPolyline(pts); + const QPoint tip(w - 1, yTo); + const int s2 = 7; + QPolygon head; + head << tip << QPoint(tip.x() - s2, tip.y() - s2) + << QPoint(tip.x() - s2, tip.y() + s2); + p.setBrush(p.pen().color()); + p.drawPolygon(head); + p.setBrush(Qt::NoBrush); + return; + } + if (dir > 0) { + if (!l.startAtJog) + pts << QPoint(0, yFrom); + pts << QPoint(jog, yFrom) << QPoint(jog, yTo); + if (l.head) + pts << QPoint(w - 8, yTo); + } else { + if (!l.startAtJog) + pts << QPoint(w, yFrom); + pts << QPoint(jog, yFrom) << QPoint(jog, yTo); + if (l.head) + pts << QPoint(8, yTo); + } + p.drawPolyline(pts); + if (!l.head) + return; // merged into the junction (the funnel) + // filled head at the target end + const QPoint tip = (dir > 0) ? QPoint(w - 1, yTo) : QPoint(1, yTo); + QPolygon head; + const int s = 7; + head << tip << QPoint(tip.x() - dir * s, tip.y() - s) + << QPoint(tip.x() - dir * s, tip.y() + s); + p.setBrush(p.pen().color()); + p.drawPolygon(head); + p.setBrush(Qt::NoBrush); + } +}; + +// Greys a stage box (title and contents) without disabling it, so a bypassed +// stage still works the moment it is switched on again. The widgets in +// 'keepActive' keep their normal text colour: they are the stage's own on/off +// control, and a bypassed stage must still look switchable (everything stays +// clickable either way - this is a look, not an enablement). +void greyStage(QGroupBox *g, bool grey, + std::initializer_list keepActive = {}) { + if (!g) + return; + // grey text plus a tinted background, so a bypassed stage is obvious + static const char *kGrey = + "QGroupBox { color: #9a9a9a; background-color: #ececec; }" + "QGroupBox::title { color: #9a9a9a; }" + "QGroupBox QLabel { color: #9a9a9a; }" + "QGroupBox QPushButton { color: #9a9a9a; }" + "QGroupBox QCheckBox { color: #9a9a9a; }"; + // only restyle on a change, so the panel never flickers + const QVariant was = g->property("flowGrey"); + if (!(was.isValid() && was.toBool() == grey)) { + g->setProperty("flowGrey", grey); + g->setStyleSheet(grey ? kGrey : QString()); + } + for (QWidget *w : keepActive) { + if (!w) + continue; + const QVariant wWas = w->property("flowKeep"); + if (wWas.isValid() && wWas.toBool() == grey) + continue; // only restyle on a change, so nothing flickers + w->setProperty("flowKeep", grey); + // a per-widget rule wins over the group's, so the toggle stays readable + w->setStyleSheet(grey ? "color: #000000;" : QString()); + } +} + +QLabel *makeFlowArrow(const QString &glyph, QWidget *parent, + int cls = FlowNormal) { + auto *l = new QLabel(glyph, parent); + l->setAlignment(Qt::AlignCenter); + l->setStyleSheet(QString("color: %1; font-size: 20px; font-weight: bold;") + .arg(flowColor(cls))); + l->setFixedHeight(24); + return l; +} + +// recolours one of those arrows when the save mode changes +void setFlowArrowClass(QLabel *l, int cls) { + if (l) + l->setStyleSheet(QString("color: %1; font-size: 20px; font-weight: " + "bold;") + .arg(flowColor(cls))); +} + +// --------------------------------------------------------------------------- +// FastImageView: the original widget, with its own pixel storage so that the +// analysis thread can never hand it a buffer it is still writing. +// --------------------------------------------------------------------------- +const QRgb *makePalette() { + static QRgb pal[256]; + for (int i = 0; i < 256; i++) { + double v = i / 255.0; + double r, g, b; + if (v < 0.25) { + r = 0; + g = v * 4; + b = 1; + } else if (v < 0.5) { + r = 0; + g = 1; + b = 1 - (v - 0.25) * 4; + } else if (v < 0.75) { + r = (v - 0.5) * 4; + g = 1; + b = 0; + } else { + r = 1; + g = 1 - (v - 0.75) * 4; + b = 0; + } + pal[i] = qRgb(int(r * 255), int(g * 255), int(b * 255)); + } + return pal; +} +const QRgb *kPalette = makePalette(); +const int MARGIN = 5; +const int SCALE_W = 50; + +class FastImageView : public QWidget { + public: + FastImageView(QWidget *parent = nullptr); + + // copies 'data' into widget-owned storage (GUI thread only) + void setImageCopy(const float *data, int nx, int ny); + // replaces the 256-entry colour table (GUI thread only) + void setPaletteLut(const QRgb *lut); + // draw pixels with no counts (value <= 0) white instead of the palette's + // lowest colour, the way ROOT's colz draws empty cells on a white canvas. + // Used by the photon map only; the ADC/gain images keep the full palette. + void setZeroWhite(bool on) { + m_zeroWhite = on; + m_dirty = true; + update(); + } + bool hasImage() const { return m_nx > 0 && m_ny > 0; } + void viewport(double &x, double &y, double &w, double &h) const { + x = m_vpX; + y = m_vpY; + w = vpW(); + h = vpH(); + } + int imageWidth() const { return m_nx; } + int imageHeight() const { return m_ny; } + // Thin yellow rectangle drawn over the image, in DATA coordinates (data + // row 0 is displayed at the bottom, so the view flips it here). Used to + // show the gate ROI, which is a snapshot and does not follow the zoom. + void setOverlayRect(double x0, double y0, double x1, double y1, bool on) { + m_ovX0 = x0; + m_ovY0 = y0; + m_ovX1 = x1; + m_ovY1 = y1; + m_ovOn = on; + update(); + } + + // Red receiver-ROI box, in the same DATA coordinates as the gate ROI. + void setReceiverRoiRect(double x0, double y0, double x1, double y1, + bool on) { + m_rxX0 = x0; + m_rxY0 = y0; + m_rxX1 = x1; + m_rxY1 = y1; + m_rxOn = on; + update(); + } + + void setColorRange(double lo, double hi); + // Horizontal size of one image pixel relative to its height (1 = square). + // Used by the strixel panels, whose mapped pixels are 9, 25 and 16 times + // wider than tall (group pitch X = 75*(75/Y), Y = 25/15/18.75 um). + void setPixelAspectX(double a) { + m_pixelAspectX = (a > 0.0) ? a : 1.0; + update(); + } + // the colour scale currently in use (also draggable on the colour bar) + void colorRange(double &lo, double &hi) const { + lo = m_clo; + hi = m_chi; + } + void setViewport(double x1, double y1, double x2, double y2); + void resetView(); + std::function onZoomChanged; + + protected: + void paintEvent(QPaintEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void wheelEvent(QWheelEvent *event) override; + void mouseDoubleClickEvent(QMouseEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + + private: + void renderImage(); + void clampViewport(); + // makes the visible region the same shape as the image area of the widget + void matchViewportToWindow(); + double vpW() const { return (m_vpW < 0) ? m_nx : m_vpW; } + double vpH() const { return (m_vpH < 0) ? m_ny : m_vpH; } + QRect imageRect() const; + QRect scaleRect() const; + double valueAt(const QPoint &pos) const; + QPointF widgetToImage(QPoint wpos) const; + + std::vector m_owned; // widget-owned copy of the last frame + const float *m_data = nullptr; + int m_nx = 0, m_ny = 0; + QImage m_image; + bool m_dirty = true; + QRgb m_palette[256]; + + double m_clo = 0, m_chi = 4000; + double m_defClo = 0, m_defChi = 4000; + bool m_zeroWhite = false; // photon map: no counts -> white + double m_pixelAspectX = 1.0; // >1: pixels wider than tall + double m_vpX = 0, m_vpY = 0; + double m_vpW = -1, m_vpH = -1; + + // gate ROI overlay (data coordinates) + bool m_ovOn = false; + double m_ovX0 = 0, m_ovY0 = 0, m_ovX1 = 0, m_ovY1 = 0; + + // receiver ROI overlay (RED), same data coordinates + bool m_rxOn = false; + double m_rxX0 = 0, m_rxY0 = 0, m_rxX1 = 0, m_rxY1 = 0; + + bool m_dragging = false; + QPoint m_lastPos; + double m_dragVpX, m_dragVpY; + mutable QTransform m_widgetToImg; + bool m_dragScale = false; + bool m_dragMax = false; // true = dragging max, false = dragging min + int m_dragScaleY = 0; + double m_dragClo, m_dragChi; +}; + +FastImageView::FastImageView(QWidget *parent) : QWidget(parent) { + setMouseTracking(true); + setFocusPolicy(Qt::StrongFocus); + setMinimumSize(200, 200); + m_chi = 4000; + m_defChi = 4000; + m_clo = 0; + m_defClo = 0; + for (int i = 0; i < 256; i++) + m_palette[i] = kPalette[i]; +} + +void FastImageView::setImageCopy(const float *data, int nx, int ny) { + if (!data || nx <= 0 || ny <= 0) + return; + const size_t n = static_cast(nx) * static_cast(ny); + if (m_owned.size() != n) + m_owned.resize(n); + memcpy(m_owned.data(), data, n * sizeof(float)); + m_data = m_owned.data(); + m_nx = nx; + m_ny = ny; + m_dirty = true; + if (m_vpW < 0) + m_vpW = nx; + if (m_vpH < 0) + m_vpH = ny; + update(); +} + +void FastImageView::setPaletteLut(const QRgb *lut) { + if (!lut) + return; + for (int i = 0; i < 256; i++) + m_palette[i] = lut[i]; + m_dirty = true; + update(); +} + +void FastImageView::setColorRange(double lo, double hi) { + m_clo = lo; + m_chi = hi; + m_defClo = lo; + m_defChi = hi; + m_dirty = true; + update(); +} + +void FastImageView::setViewport(double x1, double y1, double x2, double y2) { + if (x2 - x1 < 2 || y2 - y1 < 2) + return; + m_vpX = x1; + m_vpY = y1; + m_vpW = x2 - x1; + m_vpH = y2 - y1; + update(); +} + +void FastImageView::resetView() { + if (!hasImage()) + return; // nothing to reset yet (and (0,0,0,0) would collapse the map) + m_vpX = 0; + m_vpY = 0; + m_vpW = m_nx; + m_vpH = m_ny; + if (onZoomChanged) + onZoomChanged(0, 0, m_nx, m_ny); + update(); +} + +QRect FastImageView::imageRect() const { + int imgW = width() - 2 * MARGIN - SCALE_W; + int imgH = height() - 2 * MARGIN; + if (imgW < 10) + imgW = 10; + if (imgH < 10) + imgH = 10; + return QRect(MARGIN, MARGIN, imgW, imgH); +} + +QRect FastImageView::scaleRect() const { + QRect ir = imageRect(); + return QRect(ir.right() + 4, ir.top(), SCALE_W - 4, ir.height()); +} + +// keeps the viewport inside the image; used after pan, zoom and resize +void FastImageView::clampViewport() { + if (!hasImage()) + return; + if (m_vpW > m_nx) + m_vpW = m_nx; + if (m_vpH > m_ny) + m_vpH = m_ny; + if (m_vpX < 0) + m_vpX = 0; + if (m_vpX + vpW() > m_nx) + m_vpX = m_nx - vpW(); + if (m_vpY < 0) + m_vpY = 0; + if (m_vpY + vpH() > m_ny) + m_vpY = m_ny - vpH(); +} + +// The ADC window is the tool that selects the gate ROI, so the region it +// shows takes the shape of the window: make the window tall and "ROI only" +// captures a narrow, tall region. The shorter side is grown so that nothing +// the operator was looking at disappears; the whole-frame view is left alone +// (it is letterboxed on purpose) so that it still captures the full sensor. +void FastImageView::matchViewportToWindow() { + if (!hasImage()) + return; + if (m_vpW >= m_nx - 0.5 && m_vpH >= m_ny - 0.5) + return; // whole frame: keep it, whatever the window shape + QRect ir = imageRect(); + if (ir.width() <= 0 || ir.height() <= 0) + return; + // shape the viewport so the physical pixels (m_pixelAspectX) fill the + // window with the same shape, not the square-pixel shape + const double ar = double(ir.width()) / double(ir.height()) / m_pixelAspectX; + double w = vpW(), h = vpH(); + if (w / h > ar) + h = w / ar; // too wide -> grow vertically + else + w = h * ar; // too tall -> grow horizontally + if (w > m_nx) { // grown past the sensor: shrink both, keep the shape + h *= m_nx / w; + w = m_nx; + } + if (h > m_ny) { + w *= m_ny / h; + h = m_ny; + } + const double cx = m_vpX + vpW() / 2, cy = m_vpY + vpH() / 2; + m_vpW = w; + m_vpH = h; + m_vpX = cx - w / 2; + m_vpY = cy - h / 2; + clampViewport(); + if (onZoomChanged) + onZoomChanged(m_vpX, m_vpY, m_vpX + vpW(), m_vpY + vpH()); +} + +void FastImageView::resizeEvent(QResizeEvent *) { + matchViewportToWindow(); + update(); +} + +void FastImageView::renderImage() { if (!m_data || m_nx <= 0 || m_ny <= 0) + return; + if (m_image.size() != QSize(m_nx, m_ny)) + m_image = QImage(m_nx, m_ny, QImage::Format_ARGB32); + double range = (m_chi > m_clo) ? (m_chi - m_clo) : 1.0; + static const QRgb kWhite = qRgb(255, 255, 255); + for (int p = 0; p < m_nx * m_ny; p++) { + double v = m_data[p]; + if (m_zeroWhite && !(v > 0.0)) { + // empty cell: white background, as the ROOT colz plot showed it + m_image.setPixel(p % m_nx, m_ny - 1 - p / m_nx, kWhite); + continue; + } + int idx = int((v - m_clo) / range * 255); + if (idx < 0) + idx = 0; + if (idx > 255) + idx = 255; + int x = p % m_nx, y = p / m_nx; + m_image.setPixel(x, m_ny - 1 - y, m_palette[idx]); + } + m_dirty = false; +} + +void FastImageView::paintEvent(QPaintEvent *) { + if (m_dirty) + renderImage(); + QPainter p(this); + p.fillRect(rect(), Qt::black); + if (!m_data || m_nx <= 0 || m_ny <= 0) + return; + + QRect ir = imageRect(); + // Pixel aspect: a value > 1 makes each image pixel that many times wider + // than tall (the strixel groups have rectangular pixels), so the fit is + // computed in physical units and the image is scaled non-uniformly. + double sx = 0.0, sy = 0.0; + if (m_pixelAspectX != 1.0) { + const double fitY = double(ir.height()) / vpH(); + const double fitX = double(ir.width()) / (vpW() * m_pixelAspectX); + sy = (fitY < fitX) ? fitY : fitX; + sx = sy * m_pixelAspectX; + } else { + const double fitX = double(ir.width()) / vpW(); + const double fitY = double(ir.height()) / vpH(); + sx = sy = (fitX < fitY) ? fitX : fitY; + } + + p.save(); + p.translate(ir.left(), ir.top()); + p.translate((ir.width() - vpW() * sx) * 0.5, + (ir.height() - vpH() * sy) * 0.5); + p.translate(-m_vpX * sx, -m_vpY * sy); + p.scale(sx, sy); + const QTransform imgToWidget = p.combinedTransform(); + m_widgetToImg = imgToWidget.inverted(); + p.drawImage(QPoint(0, 0), m_image); + p.restore(); + + // gate ROI overlay: convert data rows (row 0 = bottom) to display space + if (m_ovOn && m_ovX1 > m_ovX0 && m_ovY1 > m_ovY0) { + const double dx = m_ovX0; + const double dy = double(m_ny) - m_ovY1; + const double dw = m_ovX1 - m_ovX0; + const double dh = m_ovY1 - m_ovY0; + const QRectF r = imgToWidget.mapRect(QRectF(dx, dy, dw, dh)); + QPen pen(Qt::yellow); + pen.setWidth(1); + pen.setCosmetic(true); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + p.drawRect(r); + } + + // receiver ROI overlay (RED), from the "rx_roi" field of the stream + // header. The ADC image is the raw layout, so this is a plain rectangle. + if (m_rxOn && m_rxX1 > m_rxX0 && m_rxY1 > m_rxY0) { + QPen pen(Qt::red); + pen.setWidth(1); + pen.setCosmetic(true); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + const double dx = m_rxX0; + const double dy = double(m_ny) - m_rxY1; + const double dw = m_rxX1 - m_rxX0; + const double dh = m_rxY1 - m_rxY0; + p.drawRect(imgToWidget.mapRect(QRectF(dx, dy, dw, dh))); + } + + // Color scale + QRect sr = scaleRect(); + for (int yy = sr.top(); yy <= sr.bottom(); yy++) { + double frac = 1.0 - double(yy - sr.top()) / sr.height(); + int idx = int(frac * 255); + if (idx < 0) + idx = 0; + if (idx > 255) + idx = 255; + p.setPen(m_palette[idx]); + p.drawLine(sr.left(), yy, sr.right(), yy); + } + p.setPen(Qt::white); + p.drawRect(sr.adjusted(-1, -1, 1, 1)); + QFont f = p.font(); + f.setPixelSize(10); + p.setFont(f); + p.drawText(sr.left(), sr.top() + 1, sr.width(), 12, Qt::AlignCenter, + QString::number(m_chi, 'f', 0)); + p.drawText(sr.left(), sr.bottom() - 11, sr.width(), 12, Qt::AlignCenter, + QString::number(m_clo, 'f', 0)); +} + +void FastImageView::mousePressEvent(QMouseEvent *e) { + if (e->button() != Qt::LeftButton) + return; + QPoint ip = e->pos(); + QRect sr = scaleRect(); + if (sr.contains(ip)) { + m_dragScale = true; + m_dragScaleY = ip.y(); + m_dragClo = m_clo; + m_dragChi = m_chi; + m_dragMax = (ip.y() < sr.top() + sr.height() / 2); + setCursor(Qt::SizeVerCursor); + return; + } + m_dragging = true; + m_lastPos = ip; + m_dragVpX = m_vpX; + m_dragVpY = m_vpY; + setCursor(Qt::ClosedHandCursor); +} + +void FastImageView::mouseMoveEvent(QMouseEvent *e) { + if (m_dragScale) { + QRect sr = scaleRect(); + double delta = double(e->y() - m_dragScaleY) / double(sr.height()) * + (m_dragChi - m_dragClo); + if (m_dragMax) { + m_chi = m_dragChi - delta; + if (m_chi < m_clo + 1) + m_chi = m_clo + 1; + } else { + m_clo = m_dragClo - delta; + if (m_clo > m_chi - 1) + m_clo = m_chi - 1; + } + m_dirty = true; + update(); + return; + } + if (!m_dragging) { + QPoint hp = e->pos(); + QRect ir2 = imageRect(); + QRect sr = scaleRect(); + if (sr.contains(hp)) { + setCursor(Qt::SizeVerCursor); + double v = valueAt(hp); + setToolTip(QString("Scale: %1").arg(v, 0, 'f', 0)); + } else if (ir2.contains(hp)) { + setCursor(Qt::OpenHandCursor); + // widgetToImage() is in QImage space (row 0 = TOP); the reported + // pixel and the value are converted to DATA rows so that the + // numbering matches the ROOT plots and the ROI list: (0,0) is the + // bottom-left pixel everywhere. + QPointF ipt = widgetToImage(hp); + const int ix = int(ipt.x()); + const int dy = int(ipt.y()); + const int iy = m_ny - 1 - dy; + if (ix >= 0 && ix < m_nx && iy >= 0 && iy < m_ny) + setToolTip(QString("(x=%1, y=%2) = %3") + .arg(ix) + .arg(iy) + .arg(m_data[ix + iy * m_nx], 0, 'f', 1)); + } else { + setCursor(Qt::ArrowCursor); + setToolTip(QString()); + } + return; + } + QPointF now = widgetToImage(e->pos()); + QPointF then = widgetToImage(m_lastPos); + m_vpX = m_dragVpX - (now.x() - then.x()); + m_vpY = m_dragVpY - (now.y() - then.y()); + clampViewport(); + update(); + // follow live: the gain view and the photon map pan with the ADC view + if (onZoomChanged) + onZoomChanged(m_vpX, m_vpY, m_vpX + vpW(), m_vpY + vpH()); +} + +void FastImageView::mouseReleaseEvent(QMouseEvent *e) { + if (e->button() == Qt::LeftButton) { + if (m_dragging && onZoomChanged) + onZoomChanged(m_vpX, m_vpY, m_vpX + vpW(), m_vpY + vpH()); + m_dragging = false; + m_dragScale = false; + setCursor(Qt::ArrowCursor); + } +} + +void FastImageView::wheelEvent(QWheelEvent *e) { + if (!m_data || m_nx <= 0 || m_ny <= 0) + return; + QPointF imgPt = widgetToImage(e->pos()); + double mx = imgPt.x(), my = imgPt.y(); + + double factor = (e->angleDelta().y() > 0) ? 1.0 / 1.18 : 1.18; + double nw = vpW() * factor, nh = vpH() * factor; + if (nw < 8) + nw = 8; + if (nh < 8) + nh = 8; + if (nw > m_nx) + nw = m_nx; + if (nh > m_ny) + nh = m_ny; + + double fracX = (mx - m_vpX) / vpW(); + double fracY = (my - m_vpY) / vpH(); + m_vpX = mx - fracX * nw; + m_vpY = my - fracY * nh; + m_vpW = nw; + m_vpH = nh; + if (factor > 1.0) { + // Zooming OUT: do not reshape to the window here. matchViewportToWindow + // grows/shrinks to the window aspect, so the image dimension that hits + // its limit first froze the other one and the whole frame could never + // be shown. Let both grow to the image bounds; the window-shape rule + // still applies while zooming IN and on resize. + clampViewport(); + } else { + // keep the visible region the same shape as the window also while + // zooming, so that the captured ROI has the shape of the window + matchViewportToWindow(); + } + + if (onZoomChanged) + onZoomChanged(m_vpX, m_vpY, m_vpX + vpW(), m_vpY + vpH()); + setToolTip(QString("Zoom: %1%").arg(int(100.0 * m_nx / vpW()))); + update(); +} + +void FastImageView::mouseDoubleClickEvent(QMouseEvent *e) { + QRect sr = scaleRect(); + if (sr.contains(e->pos())) { + m_clo = m_defClo; + m_chi = m_defChi; + m_dirty = true; + update(); + } else { + resetView(); + } +} + +double FastImageView::valueAt(const QPoint &pos) const { + QRect sr = scaleRect(); + if (!sr.contains(pos)) + return m_clo - 1; + double frac = 1.0 - double(pos.y() - sr.top()) / sr.height(); + return m_clo + frac * (m_chi - m_clo); +} + +QPointF FastImageView::widgetToImage(QPoint wpos) const { + return m_widgetToImg.map(QPointF(wpos)); +} + +QLabel *makeValueLabel(const QString &text, QWidget *parent) { + QLabel *label = new QLabel(text, parent); + label->setTextInteractionFlags(Qt::TextSelectableByMouse); + return label; +} + +} // namespace + +// --------------------------------------------------------------------------- +// MainWindow +// --------------------------------------------------------------------------- +class MainWindow : public QMainWindow { + public: + MainWindow(); + ~MainWindow() override; + void start(); + void stop(); + protected: + void closeEvent(QCloseEvent *event) override; + + private: + void setupUI(); + void connectShortcuts(); + + // periodic GUI work + void processImages(); // every 30 ms: mailbox -> widgets + void updateReceiverRoiOverlay(); // red rx_roi box on the ADC image + void updateStatus(); // every 500 ms: counters / labels + void checkSignals(); // every 200 ms: Ctrl-C shutdown + + // shared actions (buttons + keyboard shortcuts) + void toggleStartPause(); + void faster(); + void slower(); + void toggleGain(); + void setApplyGain(bool on); + void zoomTo(double x1, double y1, double x2, double y2); + void bwPalette1(); + void bwPalette2(); + void colPal(); + void toggleGateRoi(bool on); + void saveRoi(); + void loadRoiDialog(); + // applies a gate ROI directly (no capture from the ADC view) + // restrict == true (command line) also turns "ROI only" on; picking an + // entry from the list only makes it the current ROI + void applyRoi(int x, int y, int w, int h, const QString &src, + bool restrict = false); + void resetHis(); + void resetAxis(); + void toggleRoiSave(bool on); + void browseSaveDir(); + void saveDestDialog(); + void updateSaveLabel(); + void acquirePede(); + void savePede(); + void readPede(); + void loadAllPede(); + void loadGainMap(); + // lines up the boxes of the flow chart (gates on the left, Pedestal/Gain + // on the right) and repaints the arrows + void layoutFlow(); + void updateFlow(); + void updateGainMapLabel(); + void updateRoiLabel(); + void captureRoi(); // select the ADC zoom as the current ROI + void resizeEvent(QResizeEvent *event) override; + void showEvent(QShowEvent *event) override; + void showRangeDialog(); + void showRateDialog(); + void unsetRoi(); // forget the current ROI (full frame again) + // While a run writes, the settings that would change what lands in the file + // are locked (display-only controls stay live). + void applySaveLock(bool saving); + // Gate criterion changes stay possible during a run, but they ask first and + // leave a note in the JSON of the file being written. + bool confirmGateChangeDuringRun(const QString &what); + void quitApp(); + void closeSideWindows(); // the ADC / gain / photon / panel windows + // what the flow diagram has to show (see FlowStrip::setStages) + void flowStageStates(bool &displayPede, bool &savePede, bool &gateOn, + bool &pmap) const; + // asked before quitting; returns false when the operator cancels + bool confirmQuit(); + + void updateDrawLabel(); + void setRanges(int amin, int amax, int pmin, int pmax, int cmin, int cmax); + // current on-screen ranges (read from the displayed histograms) + void liveAdcImageRange(int &lo, int &hi) const; + void liveMapRange(int &lo, int &hi) const; + void liveChipRange(int &lo, int &hi) const; + + Pipeline pipeline_; + QWidget *leftPad_ = nullptr, *pedePad_ = nullptr, *gainPad_ = nullptr; + // leftPad, pedePad, gainPad, gatePad - kept between solving passes + int flowPads_[4] = {0, 0, 0, 24}; + int flowPass_ = 0; + bool flowQueued_ = false; + QVBoxLayout *midCol_ = nullptr, *leftCol_ = nullptr, *rightCol_ = nullptr; + QWidget *midSpacer_ = nullptr; + FlowStrip *strip_ = nullptr; + QWidget *gatePad_ = nullptr; // alignment gap between the two gates + QLabel *arrGainDown_ = nullptr; + QLabel *arrRoiDown_ = nullptr; + QLabel *arrTrackUp_ = nullptr; + QGroupBox *scGroup_ = nullptr, *sigGroup_ = nullptr; + QGroupBox *displayGroup_ = nullptr, *saveGroup_ = nullptr; + QGroupBox *pedeGroup_ = nullptr, *gainGroup_ = nullptr; + QGroupBox *pmapGroup_ = nullptr; // virtual photon counting (needs a pede) + QCheckBox *gainStateChk_ = nullptr; // read-only "gain corrected" indicator + + QTimer *processTimer = nullptr; + QTimer *statusTimer = nullptr; + QTimer *sigTimer = nullptr; + + QLabel *frameLabel = nullptr; + QLabel *statusLabel = nullptr; + QLabel *drawLabel = nullptr; + QLabel *streamVal_ = nullptr; + QLabel *pedeVal_ = nullptr; + QLabel *errVal_ = nullptr; + QLabel *framesInCumVal_ = nullptr; + QLabel *framesInCurVal_ = nullptr; + QLabel *framesPassCumVal_ = nullptr; + QLabel *framesPassCurVal_ = nullptr; + QLabel *framesDropCumVal_ = nullptr; + QLabel *framesDropCurVal_ = nullptr; + QLabel *pixelsAboveCumVal_ = nullptr; + QLabel *pixelsAboveCurVal_ = nullptr; + QLabel *rateInVal_ = nullptr; + QLabel *ratePassVal_ = nullptr; + QLabel *dropFracVal_ = nullptr; + QLabel *displayDropVal_ = nullptr; + QLabel *alignVal_ = nullptr; + QLabel *gateRoiVal_ = nullptr; + QSpinBox *saveFrames_ = nullptr; + QCheckBox *saveCorrected_ = nullptr; + QRadioButton *saveModeFrames_ = nullptr; + QRadioButton *saveModePhoton_ = nullptr; + QSpinBox *pmapSaveSpin_ = nullptr; + QLabel *saveBudgetLabel_ = nullptr; + QLineEdit *saveDirEdit_ = nullptr; + // the destination as it was at start-up: that pair is the default, and the + // "previous destinations" list must not collect it on every run + QString saveDefaultDir_, saveDefaultBase_; + QLineEdit *saveBaseEdit_ = nullptr; + QPushButton *btnSaveDest_ = nullptr; + QPushButton *btnSaveStart = nullptr; + QLabel *saveVal_ = nullptr; + bool m_saveWasRunning = false; + + QCheckBox *gateChk_ = nullptr; + QLabel *thrLabel_ = nullptr; // says whether the gate threshold is on raw ADC + QCheckBox *contigChk_ = nullptr; + QDoubleSpinBox *thrSpin_ = nullptr; + QSpinBox *minPixSpin_ = nullptr; + QPushButton *btnGateRoi = nullptr; + QComboBox *scCombo_ = nullptr; + QLabel *scLastVal_ = nullptr; + QLabel *scDropCumVal_ = nullptr; + QLabel *scDropCurVal_ = nullptr; + QPushButton *btnSaveRoi = nullptr; + QPushButton *btnLoadRoi = nullptr; + QPushButton *btnSelectRoi_ = nullptr; + QPushButton *btnUnsetRoi_ = nullptr; + QGroupBox *roiGroup_ = nullptr; + QLabel *roiListVal_ = nullptr; + bool m_applyingRoi = false; // guards toggleGateRoi() during applyRoi() + + QPushButton *btnStartPause = nullptr; + QPushButton *btnFaster = nullptr; + QPushButton *btnSlower = nullptr; + QPushButton *btnGainToggle = nullptr; + QPushButton *btnBWPalette = nullptr; + QPushButton *btnBWPalette2 = nullptr; + QPushButton *btnLoadPede = nullptr; + QPushButton *btnSavePede = nullptr; + QPushButton *btnReadPede = nullptr; + QPushButton *btnLoadAllPede = nullptr; + QPushButton *btnGainMap = nullptr; + QCheckBox *kevChk_ = nullptr; + QPushButton *btnNoPede_ = nullptr; + QPushButton *btnPedeTrack_ = nullptr; + QLabel *pedeTrackVal_ = nullptr; + QGroupBox *pedeTrackGroup_ = nullptr; + QLabel *noPedeVal_ = nullptr; + QLabel *gainMapVal_ = nullptr; + QPushButton *btnResetHis = nullptr; + QPushButton *btnResetAxis = nullptr; + QPushButton *btnQuit = nullptr; + QPushButton *btnSetRanges = nullptr; + QPushButton *btnRates = nullptr; + QDoubleSpinBox *photonThrSpin_ = nullptr; // ADU, or keV with gain on + QLabel *photonThrLabel_ = nullptr; + QPushButton *btnColPal = nullptr; + QPushButton *btnResetCounters = nullptr; + + FastImageView *m_adcView = nullptr; + FastImageView *m_gainView = nullptr; + QWidget *m_adcWin = nullptr; + QWidget *m_gainWin = nullptr; + QWidget *m_photonWin = nullptr; + FastImageView *m_photonView = nullptr; + QCheckBox *pmapCumulative_ = nullptr; + QSpinBox *pmapEverySpin_ = nullptr; + uint64_t pmapSeq_ = 0; +#ifdef JFSTRX + // separate strixel window: six group panels, display only (no ROI) + std::array m_adcPanelView{}; + QWidget *m_adcPanelWin = nullptr; +#endif + + // last receiver ROI mirrored onto the ADC image (red box) + bool m_rxRoiShown = false; + int m_rxRoiLast[4] = {-1, -1, -1, -1}; +}; + +MainWindow::MainWindow() : QMainWindow(nullptr) { + setWindowTitle("Online JF Display + Event Finder"); + setMinimumWidth(420); + + setupUI(); + connectShortcuts(); + + goout = 1; + gain_flag = false; + HDraw_every = 20; + // the operator's range defaults (Set Ranges) are in force from the start; + // "Reset Axis" releases them and the axes auto-scale again + fixranges = true; + + initGeometry(); + + hchptr = (short *)malloc(NCH * sizeof(short)); + for (int k = 0; k < NCH; k++) + hchptr[k] = 0; + + cout << "opening the rootapp" << endl; + int argc_copy = 1; + char dummy_name[] = "onlinedisp"; + char *argv_copy[] = {dummy_name, nullptr}; + rootapp = new TApplication("Online JF display", &argc_copy, argv_copy); + + loadRootPalette(Palette::Color); + + char hname[100]; +#ifdef JFSTRX + int nxx = 79; + int nyy = 165; + his1000 = new TH2F("his1000", "2d, chip 1 group 1 (25um) pede corr.", nxx, + -0.5, nxx - 0.5, nyy, -0.5, nyy - 0.5); + his1060 = new TH2F("his1060", "2d, chip 6 group 1 (25um) pede corr.", nxx, + -0.5, nxx - 0.5, nyy, -0.5, nyy - 0.5); + nxx = 47; + nyy = 320; + his1001 = new TH2F("his1001", "2d, chip 1 group 2 (15um) pede corr.", nxx, + -0.5, nxx - 0.5, nyy, -0.5, nyy - 0.5); + his1061 = new TH2F("his1061", "2d, chip 6 group 2 (15um) pede corr.", nxx, + -0.5, nxx - 0.5, nyy, -0.5, nyy - 0.5); + nxx = 59; + nyy = 476; + his1002 = new TH2F("his1002", "2d, chip 1 group 3 (18.75um) pede corr.", nxx, + -0.5, nxx - 0.5, nyy, -0.5, nyy - 0.5); + his1062 = new TH2F("his1062", "2d, chip 6 group 3 (18.75um) pede corr.", nxx, + -0.5, nxx - 0.5, nyy, -0.5, nyy - 0.5); + his1000->SetOption("colz"); + his1001->SetOption("colz"); + his1002->SetOption("colz"); + his1060->SetOption("colz"); + his1061->SetOption("colz"); + his1062->SetOption("colz"); +#else + his1000 = new TH2F("his1000", "2d, ev. pede corrected", nx, -0.5, nx - 0.5, + ny, -0.5, ny - 0.5); + his1000->SetOption("colz"); +#endif + + his2000 = new TH2F("his2000", "2d gain", nx, -0.5, nx - 0.5, ny, -0.5, + ny - 0.5); + his2000->GetZaxis()->SetRangeUser(0, 4); + + // The photon map has no ROOT histogram any more: it is filled in the + // analysis and shown in its own Qt window (see "virtual photon counting"). + + his4500 = new TH2F("his4500", "T vs B", 101, -500, MAX_POS, 101, -500, + MAX_POS); + hisLvsR = new TH2F("hisLvsR", "L vs R", 101, -500, MAX_POS, 101, -500, + MAX_POS); + hchip = new TH1I *[8]; +#ifdef JFSTRX + for (int k = 0; k < 8; k++) { + if (k < 3) + sprintf(hname, "%d_hchip1group%d", k, k + 1); + else if (k > 4) + sprintf(hname, "%d_hchip6group%d", k, k - 4); + else + sprintf(hname, "%d_", k); + hchip[k] = new TH1I(hname, hname, NBIN, MIN_POS, MAX_POS); + } +#else + for (int k = 0; k < 8; k++) { + sprintf(hname, "hchip%d", k); + hchip[k] = new TH1I(hname, hname, NBIN, MIN_POS, MAX_POS); + } +#endif + // ONE spectrum of the gate-ROI pixels (canvas A7): identical binning to + // the chip spectra, summed over the whole selection + hroi = new TH1I("hroi", "hroi", NBIN, MIN_POS, MAX_POS); + // the same two sets again in keV (canvases A8/A9). The axes are the ADU + // axes divided by KEV_NOMINAL_ADU with the same number of bins, so a + // pixel's keV value selects the same bin index as its ADU value and the + // two sets always show the same window. + hchipKev = new TH1I *[8]; + for (int k = 0; k < 8; k++) { + char kn[64]; + sprintf(kn, "hchip%d_keV", k); + hchipKev[k] = new TH1I(kn, kn, NBIN, MIN_POS / KEV_NOMINAL_ADU, + MAX_POS / KEV_NOMINAL_ADU); + } + hroiKev = new TH1I("hroi_keV", "hroi_keV", NBIN, MIN_POS / KEV_NOMINAL_ADU, + MAX_POS / KEV_NOMINAL_ADU); + + cout << "end of histo booking" << endl; +#ifdef JFSTRX + if (A3 == nullptr) { + A3 = new TCanvas("A3", "Plotting Canvas ADC", 150, 360, 1200, 750); + p1 = new TPad("p1", "p1", 0.01, 0.5, 0.49, 1.); + p1->Draw(); + p2 = new TPad("p2", "p2", 0.51, 0.75, 0.99, 0.97); + p2->Draw(); + p3 = new TPad("p3", "p3", 0.01, 0.25, 0.49, 0.47); + p3->Draw(); + p4 = new TPad("p4", "p4", 0.51, 0.53, 0.99, 0.75); + p4->Draw(); + p5 = new TPad("p5", "p5", 0.01, 0.03, 0.49, 0.25); + p5->Draw(); + p6 = new TPad("p6", "p6", 0.51, 0., 0.99, 0.5); + p6->Draw(); + } +#endif + if (A4 == nullptr) + A4 = new TCanvas("A4", "Plotting Canvas PHs", 750, 300, 1000, 800); + A4->Clear(); + A4->Divide(4, 2, 0.005, 0.005); + if (false) // the photon map canvas is gone; keep the pointer unused + A5 = new TCanvas("A5", "Plotting Canvas Photon Map", 750, 300, 1000, + 600); + if (A6 == nullptr) + A6 = new TCanvas("A6", "Plotting Canvas LvsR", 650, 250, 650, 660); + if (A7 == nullptr) + A7 = new TCanvas("A7", "ROI PH", 770, 340, 1000, 700); + A7->Clear(); // one pad: all the gate-ROI pixels in one spectrum + // The keV canvases are NOT created here: a ROOT canvas maps its window in + // the constructor, and these two must stay invisible until a gain map is + // loaded. They are created lazily by the ROOT thread in + // Plot1DHistosKev()/PlotRoiHistosKev() the first time they are needed. + + + // display clones painted by the ROOT thread (sources stay exclusive to + // the analysis thread) + disp4500 = (TH2F *)his4500->Clone("disp4500"); + dispLvsR = (TH2F *)hisLvsR->Clone("dispLvsR"); + for (int ih = 0; ih < 8; ih++) { + char nm[64]; + sprintf(nm, "dispChip%d", ih); + dispChip[ih] = (TH1I *)hchip[ih]->Clone(nm); + } + dispRoi = (TH1I *)hroi->Clone("dispRoi"); + for (int ih = 0; ih < 8; ih++) { + char nm[64]; + sprintf(nm, "dispChipKev%d", ih); + dispChipKev[ih] = (TH1I *)hchipKev[ih]->Clone(nm); + } + dispRoiKev = (TH1I *)hroiKev->Clone("dispRoiKev"); + + // start with the operator's ranges (Set Ranges dialog): the spectra open + // on the PH window, the main plot keeps its value window (fixranges) + for (int ih = 0; ih < 8; ih++) { + if (dispChip[ih]) + dispChip[ih]->GetXaxis()->SetRangeUser(float(chipxmin.load()), + float(chipxmax.load())); + } + if (dispRoi) + dispRoi->GetXaxis()->SetRangeUser(float(chipxmin.load()), + float(chipxmax.load())); + if (dispRoiKev) + dispRoiKev->GetXaxis()->SetRangeUser( + chipxmin.load() / KEV_NOMINAL_ADU, + chipxmax.load() / KEV_NOMINAL_ADU); + for (int ih = 0; ih < 8; ih++) { + if (dispChipKev[ih]) + dispChipKev[ih]->GetXaxis()->SetRangeUser( + chipxmin.load() / KEV_NOMINAL_ADU, + chipxmax.load() / KEV_NOMINAL_ADU); + } + // the photon window opens on the photon-map colour range (the ADC image + // was given the ADC range in setupUI()) + if (m_photonView) + m_photonView->setColorRange(pmmin.load(), pmmax.load()); +#ifdef JFSTRX + disp1000 = (TH2F *)his1000->Clone("disp1000"); + disp1001 = (TH2F *)his1001->Clone("disp1001"); + disp1002 = (TH2F *)his1002->Clone("disp1002"); + disp1060 = (TH2F *)his1060->Clone("disp1060"); + disp1061 = (TH2F *)his1061->Clone("disp1061"); + disp1062 = (TH2F *)his1062->Clone("disp1062"); +#endif + + printhelp(); +} + +MainWindow::~MainWindow() { stop(); } + +// --------------------------------------------------------------------------- +// UI +// --------------------------------------------------------------------------- +void MainWindow::setupUI() { + auto *central = new QWidget(this); + setCentralWidget(central); + auto *rootLayout = new QVBoxLayout(central); + auto *columns = new QHBoxLayout(); + rootLayout->addLayout(columns); + + auto *leftCol = new QVBoxLayout(); + auto *rightCol = new QVBoxLayout(); + // The connectors that cross from one column to the other are drawn by a + // strip placed by hand in the gap (not in the layout: an item here would + // stretch the column row and leave empty space above the save panel). A + // zero-height spacer reserves the width. + midCol_ = new QVBoxLayout(); + leftCol_ = leftCol; + rightCol_ = rightCol; + midSpacer_ = new QWidget(central); + midSpacer_->setFixedWidth(64); + midSpacer_->setFixedHeight(0); + midCol_->addWidget(midSpacer_); + strip_ = new FlowStrip(central); + columns->addLayout(leftCol, 1); + columns->addLayout(midCol_, 0); + columns->addLayout(rightCol, 1); + // bottom row: what the program is doing (Status) and what it has counted. + // It is added to the layout *after* the save panel (see the end of this + // function) so the save ends up above the counters. + auto *bottomRow = new QHBoxLayout(); + + // ── Status ── + auto *statusGroup = new QGroupBox("Status", central); + auto *statusLayout = new QVBoxLayout(statusGroup); + frameLabel = new QLabel("Frame: --", statusGroup); + statusLabel = new QLabel("Status: idle", statusGroup); + drawLabel = new QLabel(statusGroup); + streamVal_ = makeValueLabel("disconnected", statusGroup); + pedeVal_ = makeValueLabel("not loaded", statusGroup); + errVal_ = makeValueLabel(QString(), statusGroup); + errVal_->setWordWrap(true); + displayDropVal_ = makeValueLabel("0", statusGroup); + alignVal_ = makeValueLabel(QString(), statusGroup); + alignVal_->setWordWrap(true); + statusLayout->addWidget(frameLabel); + statusLayout->addWidget(statusLabel); + statusLayout->addWidget(drawLabel); + statusLayout->addWidget(new QLabel("Stream:", statusGroup)); + statusLayout->addWidget(streamVal_); + statusLayout->addWidget(new QLabel("Pedestal:", statusGroup)); + statusLayout->addWidget(pedeVal_); + statusLayout->addWidget(new QLabel("Display drops (analysis too slow):", + statusGroup)); + statusLayout->addWidget(displayDropVal_); + statusLayout->addWidget(new QLabel("Frame alignment:", statusGroup)); + statusLayout->addWidget(alignVal_); + statusLayout->addWidget(errVal_); + bottomRow->addWidget(statusGroup, 1); + + // ── Gate (event finder): two gates, applied in series ── + auto *gateGroup = new QGroupBox("Event finder gate", central); + auto *gateCol = new QVBoxLayout(gateGroup); + + // gate 1: storage cell. This is a header field, so it is checked first + // and frames from other cells cost no CPU at all. + auto *scGroup = new QGroupBox("1. Storage cell (header)", gateGroup); + scGroup_ = scGroup; + auto *scForm = new QFormLayout(scGroup); + scCombo_ = new QComboBox(scGroup); + scCombo_->addItem("ALL"); // index 0 == no storage-cell gating + for (int i = 0; i <= 15; i++) + scCombo_->addItem(QString::number(i)); + scCombo_->setToolTip( + "Pass only frames acquired in this storage cell (bits 8-11 of the\n" + "header's detector specific 3; 0..15, 15 = default). \"ALL\" passes\n" + "every cell. Checked before any pixel is touched, so the signal\n" + "gate only ever sees frames that survive this one."); + scLastVal_ = makeValueLabel("last frame: ?", scGroup); + scForm->addRow("Only storage cell:", scCombo_); + scForm->addRow(scLastVal_); + gateCol->addWidget(scGroup); + // Plain invisible spacer between the two gates: the path from the storage + // cell to the signal gate goes through the Pedestal and is drawn by the + // strip, so a direct arrow here would be wrong. layoutFlow() stretches + // this gap so the signal gate's middle lands on the Gain's top. + gatePad_ = new QWidget(gateGroup); + gatePad_->setFixedHeight(24); + gateCol->addWidget(gatePad_); + + // gate 2: the signal gate (the pixel counting) + auto *sigGroup = new QGroupBox("2. Signal gate (pixel counting)", gateGroup); + sigGroup_ = sigGroup; + auto *gateForm = new QFormLayout(sigGroup); + gateChk_ = new QCheckBox("Gate enabled (off = passthrough)", sigGroup); + gateChk_->setChecked(g_cfg.gateEnabled); + contigChk_ = new QCheckBox("Contiguous (N px within circle, r = N/2+1)", + sigGroup); + contigChk_->setChecked(g_cfg.contiguous); + thrSpin_ = new QDoubleSpinBox(sigGroup); + thrSpin_->setRange(0.0, 1000000.0); + thrSpin_->setDecimals(1); + thrSpin_->setSingleStep(10.0); + thrSpin_->setValue(g_cfg.threshold); + minPixSpin_ = new QSpinBox(sigGroup); + minPixSpin_->setRange(1, 10000000); + minPixSpin_->setValue(static_cast(g_cfg.minPixels)); + btnResetCounters = new QPushButton("Reset counters", gateGroup); + btnGateRoi = new QPushButton("ROI only (i)", sigGroup); + btnGateRoi->setCheckable(true); + btnGateRoi->setToolTip( + "Count only inside the ROI captured from the ADC zoom at the moment\n" + "this is pressed. The ROI is a snapshot: panning or zooming the ADC\n" + "image afterwards does not move it (it stays outlined in yellow)."); + gateRoiVal_ = makeValueLabel("full frame", sigGroup); + { + auto *gateRow = new QHBoxLayout(); + gateRow->addWidget(gateChk_); + gateRow->addWidget(btnGateRoi); + gateForm->addRow(gateRow); + } + gateForm->addRow(contigChk_); + thrLabel_ = new QLabel("Threshold (ADC, pede-subtracted):", sigGroup); + gateForm->addRow(thrLabel_, thrSpin_); + gateForm->addRow("Min pixels above threshold:", minPixSpin_); + gateForm->addRow("Gate ROI:", gateRoiVal_); + gateCol->addWidget(sigGroup); + + auto *seriesHint = new QLabel( + "Applied in series - storage cell first, so frames from other cells " + "never reach the pixel counting (all 0-15, no gating, and both " + "together are all possible).", + gateGroup); + seriesHint->setWordWrap(true); + gateCol->addWidget(seriesHint); + gateCol->addWidget(btnResetCounters); + // 1) Storage cell then 2) Signal gate: the left column *is* the gate flow. + // leftPad_ is sized at run time so that the middle of the storage-cell box + // lands on the top edge of the Pedestal box (see layoutFlow()). + // (Quit is added at the top of the right column, right aligned) + leftPad_ = new QWidget(central); + leftPad_->setFixedHeight(0); + leftCol->addWidget(leftPad_); + leftCol->addWidget(gateGroup); + + // ── Counters ── + auto *counterBox = new QGroupBox("Counters", central); + auto *grid = new QGridLayout(counterBox); + grid->setColumnStretch(1, 1); + grid->setColumnStretch(2, 1); + auto *headCum = new QLabel("Cumulative", counterBox); + auto *headCur = new QLabel("Current", counterBox); + headCum->setStyleSheet("font-weight: bold;"); + headCur->setStyleSheet("font-weight: bold;"); + grid->addWidget(new QLabel(QString(), counterBox), 0, 0); + grid->addWidget(headCum, 0, 1); + grid->addWidget(headCur, 0, 2); + + framesInCumVal_ = makeValueLabel("0", counterBox); + framesInCurVal_ = makeValueLabel("0", counterBox); + grid->addWidget(new QLabel("Frames in", counterBox), 1, 0); + grid->addWidget(framesInCumVal_, 1, 1); + grid->addWidget(framesInCurVal_, 1, 2); + + framesPassCumVal_ = makeValueLabel("0", counterBox); + framesPassCurVal_ = makeValueLabel("0", counterBox); + grid->addWidget(new QLabel("Frames passed", counterBox), 2, 0); + grid->addWidget(framesPassCumVal_, 2, 1); + grid->addWidget(framesPassCurVal_, 2, 2); + + framesDropCumVal_ = makeValueLabel("0", counterBox); + framesDropCurVal_ = makeValueLabel("0", counterBox); + grid->addWidget(new QLabel("Frames dropped", counterBox), 3, 0); + grid->addWidget(framesDropCumVal_, 3, 1); + grid->addWidget(framesDropCurVal_, 3, 2); + + scDropCumVal_ = makeValueLabel("0", counterBox); + scDropCurVal_ = makeValueLabel("0", counterBox); + grid->addWidget(new QLabel("Frames dropped (SC)", counterBox), 4, 0); + grid->addWidget(scDropCumVal_, 4, 1); + grid->addWidget(scDropCurVal_, 4, 2); + + pixelsAboveCumVal_ = makeValueLabel("0", counterBox); + pixelsAboveCurVal_ = makeValueLabel("0", counterBox); + grid->addWidget(new QLabel("Pixels above", counterBox), 5, 0); + grid->addWidget(pixelsAboveCumVal_, 5, 1); + grid->addWidget(pixelsAboveCurVal_, 5, 2); + + rateInVal_ = makeValueLabel("0 fps / 0 MB/s", counterBox); + grid->addWidget(new QLabel("Rate in (fps / MB/s)", counterBox), 6, 0); + grid->addWidget(rateInVal_, 6, 1, 1, 2); + ratePassVal_ = makeValueLabel("0 fps / 0 MB/s", counterBox); + grid->addWidget(new QLabel("Rate pass (fps / MB/s)", counterBox), 7, 0); + grid->addWidget(ratePassVal_, 7, 1, 1, 2); + dropFracVal_ = makeValueLabel("0.000", counterBox); + grid->addWidget(new QLabel("Drop fraction (cum.)", counterBox), 8, 0); + grid->addWidget(dropFracVal_, 8, 1, 1, 2); + bottomRow->addWidget(counterBox, 1); + + // ── Display controls ── + auto *ctrlGroup = new QGroupBox("Display", central); + displayGroup_ = ctrlGroup; + auto *ctrlGrid = new QGridLayout(ctrlGroup); + btnStartPause = new QPushButton("Start/Pause (s)", ctrlGroup); + btnFaster = new QPushButton("Faster (+)", ctrlGroup); + btnSlower = new QPushButton("Slower (-)", ctrlGroup); + btnGainToggle = new QPushButton("gain correction on/off (g)", ctrlGroup); + btnGainToggle->setCheckable(true); + btnGainToggle->setToolTip( + "Divide every pixel by its ADU/keV - the per-pixel value from the gain\n" + "map when one is loaded, the nominal 40 ADU/keV otherwise - to get keV.\n" + "\"keV PH\" then shows the PH spectra in keV; with this off (or keV PH\n" + "unticked) they show the raw pedestal-subtracted ADC."); + btnBWPalette = new QPushButton("BW Pal. (b)", ctrlGroup); + btnBWPalette2 = new QPushButton("BW Pal. 2 (B)", ctrlGroup); + btnSetRanges = new QPushButton("Set Ranges (S)", ctrlGroup); + btnColPal = new QPushButton("Col Pal (c)", ctrlGroup); + btnRates = new QPushButton("Update Rates", ctrlGroup); + btnResetHis = new QPushButton("Reset His (r)", ctrlGroup); + btnResetAxis = new QPushButton("Reset Axis (R)", ctrlGroup); + photonThrSpin_ = new QDoubleSpinBox(central); + photonThrSpin_->setRange(0.0, 100000.0); + photonThrSpin_->setDecimals(0); // ADU: whole numbers + photonThrSpin_->setSingleStep(10.0); + photonThrSpin_->setValue(static_cast(g_pheneThreshold)); + photonThrSpin_->setToolTip( + "Photon-map threshold (pede-subtracted ADC): only pixels above this\n" + "contribute to the map, and the number of photons a pixel counts for is\n" + "(value + threshold) / (2 x threshold). In ADU while the gain\n" + "correction is off, in keV while it is on (the value is then divided by\n" + "the pixel's ADU/keV first); both are remembered separately. It does\n" + "not affect the event-finder gate, which has its own threshold."); + // ── Virtual photon counting ── + // Left of the Display column (the Display lives in the right column at this + // height). The map itself is the same photon counting as before, but shown + // in its own Qt window: a fresh image every N analysed frames, and + // cumulative only if asked. + auto *pmapGroup = new QGroupBox("Virtual photon counting", central); + pmapGroup_ = pmapGroup; + auto *pmapGrid = new QGridLayout(pmapGroup); + pmapCumulative_ = new QCheckBox("cumulative (toggle to reset)", pmapGroup); + pmapCumulative_->setChecked(gPmapCumulative.load()); + pmapCumulative_->setToolTip( + "off: every displayed image shows only the photons counted since the\n" + "previous one; on: the map keeps adding up (the old behaviour)"); + pmapGrid->addWidget(pmapCumulative_, 0, 0, 1, 2); + pmapGrid->addWidget(new QLabel("plot every N images:", pmapGroup), 1, 0); + pmapEverySpin_ = new QSpinBox(pmapGroup); + pmapEverySpin_->setRange(1, 1000000); + pmapEverySpin_->setSingleStep(100); + pmapEverySpin_->setValue(gPmapEvery.load()); + pmapEverySpin_->setToolTip( + "how many analysed frames go into one image of the photon map\n" + "(default 2000; independent of the ADC image decimation)"); + pmapGrid->addWidget(pmapEverySpin_, 1, 1); + photonThrLabel_ = new QLabel("photon threshold (ADU):", pmapGroup); + pmapGrid->addWidget(photonThrLabel_, 2, 0); + // the photon threshold moves here from the Display panel + pmapGrid->addWidget(photonThrSpin_, 2, 1); + // under the gates, left of the Display column + leftCol->insertWidget(2, pmapGroup); + leftCol->addStretch(1); + + ctrlGrid->addWidget(btnStartPause, 0, 0); + ctrlGrid->addWidget(btnFaster, 0, 1); + ctrlGrid->addWidget(btnSlower, 0, 2); + // the three palette buttons share one row + ctrlGrid->addWidget(btnBWPalette, 1, 0); + ctrlGrid->addWidget(btnBWPalette2, 1, 1); + ctrlGrid->addWidget(btnColPal, 1, 2); + // (btnGainToggle lives in the Gain panel, below) + ctrlGrid->addWidget(btnSetRanges, 2, 0); + ctrlGrid->addWidget(btnRates, 2, 1); + ctrlGrid->addWidget(btnResetHis, 3, 0); + ctrlGrid->addWidget(btnResetAxis, 3, 1); + + // (the Display panel is added below the Gain panel, see below: it shows + // the result, so it comes after the chain that produces it) + + // ── ROI save (HDF5) ── + auto *saveGroup = new QGroupBox("Save", central); + saveGroup_ = saveGroup; + auto *saveForm = new QFormLayout(saveGroup); + saveFrames_ = new QSpinBox(saveGroup); + saveFrames_->setRange(0, 1000000000); + saveFrames_->setValue(1000); + saveFrames_->setSpecialValueText("unlimited"); + if (g_cfg.saveBudget > 0) // --save-budget: frames, or maps in photon mode + saveFrames_->setValue(static_cast(g_cfg.saveBudget)); + saveFrames_->setToolTip( + "Number of gate-passed frames to write; 0 = unlimited (stop with the\n" + "button). The run also stops by itself when this many frames are in\n" + "the file. In virtual photon counting mode it counts the maps."); + btnSaveStart = new QPushButton("Start/Stop save (n)", saveGroup); + btnSaveStart->setCheckable(true); + btnSaveStart->setToolTip( + "Write the active gate ROI of every gate-passed frame to an HDF5\n" + "file (slsReceiver layout, slice 0 = pedestal) plus a JSON sidecar."); + saveCorrected_ = new QCheckBox("Pedestal corrected", saveGroup); + saveCorrected_->setChecked(false); + saveCorrected_->setToolTip( + "Off (default): /data holds the raw 16-bit words and slice 0 is the\n" + "pedestal image.\n" + "On: /data holds pedestal-corrected float32 values with no pedestal\n" + "slice, and the pedestal image is written to the separate dataset\n" + "/pedestal data. Needs a loaded pedestal."); + saveDirEdit_ = new QLineEdit(QString::fromStdString(g_cfg.h5Dir), saveGroup); + saveDirEdit_->setToolTip("Directory the HDF5 file and JSON sidecar go to."); + auto *dirBtn = new QPushButton("...", saveGroup); + dirBtn->setFixedWidth(32); + auto *dirRow = new QHBoxLayout(); + dirRow->addWidget(saveDirEdit_); + dirRow->addWidget(dirBtn); + saveBaseEdit_ = new QLineEdit(QString::fromStdString(g_cfg.h5Base), saveGroup); + saveDefaultBase_ = saveBaseEdit_->text().trimmed(); // start-up default + saveBaseEdit_->setToolTip( + "File-name base. The file is __f0_0.h5 (and the\n" + "JSON the same with .json), so re-using a base never overwrites."); + btnSaveDest_ = new QPushButton("Previous destinations...", saveGroup); + btnSaveDest_->setToolTip( + "List of destinations used before (directory + base), most recent\n" + "first; pick one to fill the fields, or delete entries."); + saveDefaultDir_ = saveDirEdit_->text().trimmed(); // start-up default + saveVal_ = makeValueLabel("idle", saveGroup); + saveVal_->setWordWrap(true); + saveBudgetLabel_ = + new QLabel("Number of frames to be saved:", saveGroup); + saveForm->addRow(saveBudgetLabel_, saveFrames_); + // what gets written: the gate-passed frames, or the virtual photon + // counting maps (non-cumulative, one every M analysed frames) + saveModeFrames_ = new QRadioButton("save frames", saveGroup); + saveModePhoton_ = new QRadioButton("virtual photon counting", saveGroup); + saveModeFrames_->setChecked(gSaveMode.load() != 1); + saveModePhoton_->setChecked(gSaveMode.load() == 1); + saveForm->addRow(saveModeFrames_); + // the correction is a property of the *frames* being saved: the photon maps + // are always pedestal-subtracted, so the box lives under that radio and is + // greyed while the maps are selected + saveForm->addRow(saveCorrected_); + saveForm->addRow(saveModePhoton_); // (was dropped by the move above) + pmapSaveSpin_ = new QSpinBox(saveGroup); + pmapSaveSpin_->setRange(200, 1000000); // the requested minimum + pmapSaveSpin_->setSingleStep(100); + pmapSaveSpin_->setValue(gPmapSaveEvery.load()); + pmapSaveSpin_->setEnabled(gSaveMode.load() == 1); + pmapSaveSpin_->setToolTip( + "virtual photon counting: one non-cumulative map every this many\n" + "analysed frames (minimum 200), independent of the display cadence"); + saveForm->addRow("photon map every N frames:", pmapSaveSpin_); + saveForm->addRow("Directory:", dirRow); + saveForm->addRow("Base name:", saveBaseEdit_); + saveForm->addRow(btnSaveDest_); + saveForm->addRow(btnSaveStart); + saveForm->addRow("Save status:", saveVal_); + // (the ROI-save panel is added below the Gain panel, see the end of this + // function: the right column reads Display, Pedestal, Gain, Save) + + // ── Pedestal ── + auto *pedeGroup = new QGroupBox("Pedestal", central); + pedeGroup_ = pedeGroup; + auto *pedeGrid = new QGridLayout(pedeGroup); + btnLoadPede = new QPushButton("Acquire Pede (p)", pedeGroup); + btnSavePede = new QPushButton("Save Pede (O)", pedeGroup); + btnReadPede = new QPushButton("Read Pede (o)", pedeGroup); + btnLoadAllPede = new QPushButton("Load Pede File (P)", pedeGroup); + btnGainMap = new QPushButton("Gain map (keV)...", pedeGroup); + kevChk_ = new QCheckBox("keV PH", pedeGroup); + kevChk_->setToolTip( + "Show the PH spectra in keV instead of ADU. With a gain map the " + "pixel's own ADU/keV is used; without one the nominal 40 ADU/keV is " + "used and the panel says so."); + // usable without a map: the nominal 40 ADU/keV is used then + gainMapVal_ = new QLabel("no gain map loaded", pedeGroup); + btnNoPede_ = new QPushButton("Pede/Raw toggle", pedeGroup); + btnNoPede_->setCheckable(true); + btnNoPede_->setChecked(gNoPede.load()); // startup default: raw ADC + btnNoPede_->setToolTip( + "Switch between the loaded pedestal and raw ADC. Pressed = raw ADC:\n" + "the display, the photon map, the spectra, the event finder and the ROI\n" + "save then work on raw 14-bit values, and the Pedestal box is greyed\n" + "(this button stays active, it is how you switch back)."); + noPedeVal_ = new QLabel("pede subtracted", pedeGroup); + pedeGrid->addWidget(btnLoadPede, 0, 0); + pedeGrid->addWidget(btnSavePede, 0, 1); + pedeGrid->addWidget(btnReadPede, 1, 0); + pedeGrid->addWidget(btnLoadAllPede, 1, 1); + // calibration for the keV spectra (the ADC plots stay in ADU) + // (the gain map moved to the Gain panel below) + pedeTrackGroup_ = new QGroupBox("Pedestal tracking", central); + auto *trackCol = new QVBoxLayout(pedeTrackGroup_); + btnPedeTrack_ = + new QPushButton("pedestal tracking on/off", pedeTrackGroup_); + btnPedeTrack_->setCheckable(true); + btnPedeTrack_->setChecked(gPedeTracking.load()); // --pede-tracking + btnPedeTrack_->setToolTip( + "Follow the pedestal drift from the frames the signal gate drops\n" + "(the beam is off then). While the gate drops more than 90 % of the\n" + "frames reaching it, 0.2 % of every dropped frame feeds a per-pixel\n" + "running average over a 10 M sample window; the pedestal in use is\n" + "replaced when the largest difference exceeds 5 ADU, at most once\n" + "every 10 s and at least every 10 min. Needs the signal gate on."); + pedeTrackVal_ = makeValueLabel("off", pedeTrackGroup_); + pedeGrid->addWidget(btnNoPede_, 2, 0); + pedeGrid->addWidget(noPedeVal_, 2, 1); + trackCol->addWidget(btnPedeTrack_); + trackCol->addWidget(pedeTrackVal_); + gainMapVal_->setWordWrap(true); + pedePad_ = new QWidget(central); + pedePad_->setFixedHeight(0); + rightCol->addWidget(pedePad_); + rightCol->addWidget(pedeGroup); + // the tracker reads the frames the signal gate drops and feeds its result + // back into the Pedestal box above it, so the arrow points up + // the tracking feedback goes back into the pedestal, so it is pedestal path + arrTrackUp_ = makeFlowArrow(QString::fromUtf8("\u2191"), central, + FlowPedestal); + rightCol->addWidget(arrTrackUp_, 0, Qt::AlignHCenter); + rightCol->addWidget(pedeTrackGroup_); + gainPad_ = new QWidget(central); + gainPad_->setFixedHeight(0); + rightCol->addWidget(gainPad_); + + auto *gainGroup = new QGroupBox("Gain", central); + gainGroup_ = gainGroup; + auto *gainCol = new QVBoxLayout(gainGroup); + { + auto *gainRow = new QHBoxLayout(); + gainRow->addWidget(btnGainToggle); + // Read-only state indicator (the button is the only control): it shows + // at a glance whether the gain correction is applied - itself if a map + // is loaded, the nominal 40 ADU/keV otherwise. + gainStateChk_ = new QCheckBox("gain corrected", gainGroup); + gainStateChk_->setAttribute(Qt::WA_TransparentForMouseEvents, true); + gainStateChk_->setFocusPolicy(Qt::NoFocus); + gainStateChk_->setToolTip( + "Whether the gain correction is applied (read-only: switch it with\n" + "the button on the left). Ticked = pixels are divided by their\n" + "ADU/keV, so the PH spectra can be shown in keV."); + gainRow->addWidget(gainStateChk_); + gainRow->addStretch(1); + gainCol->addLayout(gainRow); + } + { + auto *gmRow = new QHBoxLayout(); + gmRow->addWidget(new QLabel("Gain map (keV PH):", gainGroup)); + gmRow->addWidget(btnGainMap); + gmRow->addWidget(kevChk_); + gmRow->addStretch(1); + gainCol->addLayout(gmRow); + } + gainCol->addWidget(gainMapVal_); + rightCol->addWidget(gainGroup); + arrGainDown_ = makeFlowArrow(QString::fromUtf8("\u2193"), central); + rightCol->addWidget(arrGainDown_, 0, Qt::AlignHCenter); + rightCol->addWidget(ctrlGroup); + + // ROI panel: selecting / loading / saving ROIs (the gate keeps its own + // "ROI only" capture and its ROI read-out next to the signal gate) + roiGroup_ = new QGroupBox("ROI", central); + { + auto *roiCol = new QVBoxLayout(roiGroup_); + btnSaveRoi = new QPushButton("Save ROI", roiGroup_); + btnLoadRoi = new QPushButton("Load ROI...", roiGroup_); + btnSaveRoi->setToolTip( + "Add the active gate ROI to the list kept in the program folder\n" + "(updates its timestamp if the same rectangle is already there)."); + btnLoadRoi->setToolTip( + "Pick a ROI from the list (most recently saved/loaded first) and\n" + "apply it to the gate. The list dialog can also delete entries."); + btnSelectRoi_ = new QPushButton("Select ROI (ADC zoom)", roiGroup_); + btnSelectRoi_->setToolTip( + "Make the region currently zoomed in the ADC image the current\n" + "ROI. This only selects it: press \"ROI only\" next to the signal\n" + "gate to actually restrict the gate to it."); + roiCol->addWidget(btnSelectRoi_); + btnUnsetRoi_ = new QPushButton("Unset ROI", roiGroup_); + btnUnsetRoi_->setToolTip( + "Forget the current ROI: the gate and the saved data go back to the\n" + "full frame (the ROI list is not touched - use \"Load ROI...\" for\n" + "that)."); + roiCol->addWidget(btnUnsetRoi_); + auto *row = new QHBoxLayout(); + row->addWidget(btnSaveRoi); + row->addWidget(btnLoadRoi); + roiCol->addLayout(row); + roiListVal_ = makeValueLabel("no ROI in the list", roiGroup_); + roiCol->addWidget(roiListVal_); + } + rightCol->addWidget(roiGroup_); + // ROI -> Save: on the data path while "save frames" is selected (the + // colour follows the save mode, see updateFlow) + arrRoiDown_ = makeFlowArrow(QString::fromUtf8("\u2193"), central, FlowData); + rightCol->addWidget(arrRoiDown_, 0, Qt::AlignHCenter); + + // the save panel spans the whole window, below the flow but above the + // Status/Counters row + rootLayout->addWidget(saveGroup); + rootLayout->addLayout(bottomRow); + + btnQuit = new QPushButton("Quit (q)", central); + // first row of the right column, right aligned: out of the gate/arrow area + rightCol->insertWidget(0, btnQuit, 0, Qt::AlignRight); + // (Quit was added at the top left, above the gates) + rightCol->addStretch(1); + + // ── Signals: gate parameters ── + connect(gateChk_, &QCheckBox::toggled, this, [this](bool on) { + pipeline_.gate().setEnabled(on); + statusLabel->setText(on ? "Status: gate enabled" + : "Status: passthrough (gate off)"); + // the tracking works off the frames the gate drops, so switching the + // gate off turns it off too + if (!on && btnPedeTrack_ && btnPedeTrack_->isChecked()) + btnPedeTrack_->setChecked(false); + }); + connect(thrSpin_, QOverload::of(&QDoubleSpinBox::valueChanged), this, + [this](double v) { + if (!confirmGateChangeDuringRun( + QString("Signal-gate threshold -> %1 ADU").arg(v))) { + const QSignalBlocker b(thrSpin_); + thrSpin_->setValue( + static_cast(pipeline_.gate().threshold())); + return; + } + pipeline_.gate().setThreshold(static_cast(v)); + pipeline_.gate().resetCounters(); + }); + connect(minPixSpin_, QOverload::of(&QSpinBox::valueChanged), this, + [this](int v) { + if (!confirmGateChangeDuringRun( + QString("Min pixels above threshold -> %1").arg(v))) { + const QSignalBlocker b(minPixSpin_); + minPixSpin_->setValue( + static_cast(pipeline_.gate().minPixels())); + return; + } + pipeline_.gate().setMinPixels(static_cast(v)); + pipeline_.gate().resetCounters(); + }); + connect(contigChk_, &QCheckBox::toggled, this, [this](bool on) { + pipeline_.gate().setContiguous(on); + pipeline_.gate().resetCounters(); + }); + connect(btnResetCounters, &QPushButton::clicked, this, + [this]() { pipeline_.gate().resetCounters(); }); + connect(scCombo_, QOverload::of(&QComboBox::currentIndexChanged), + this, [this](int idx) { + // index 0 is "ALL" (no storage-cell gating), 1..16 are SC 0..15 + const int sc = (idx <= 0) ? -1 : idx - 1; + pipeline_.gate().setScFilter(sc); + statusLabel->setText( + sc < 0 ? "Status: all storage cells" + : QString("Status: only storage cell %1").arg(sc)); + }); + connect(btnGateRoi, &QPushButton::toggled, this, + [this](bool on) { toggleGateRoi(on); }); + connect(btnSaveStart, &QPushButton::toggled, this, + [this](bool on) { toggleRoiSave(on); }); + connect(saveModePhoton_, &QRadioButton::toggled, this, [this](bool on) { + gSaveMode = on ? 1 : 0; + pmapSaveSpin_->setEnabled(on); + // in photon mode the same spinbox budgets MAPS, so say so + if (saveBudgetLabel_) + saveBudgetLabel_->setText( + on ? "Number of photon maps to be saved:" + : "Number of frames to be saved:"); + // the correction only applies to the frames mode + if (saveCorrected_) + saveCorrected_->setEnabled(!on && + !pipeline_.saver().status().running); + updateFlow(); // the data-path arrows follow the mode + printf("[save] mode: %s\n", + on ? "virtual photon counting" : "frames"); + fflush(stdout); + statusLabel->setText(on + ? "Status: saving virtual photon counting maps" + : "Status: saving gate-passed frames"); + }); + connect(pmapSaveSpin_, QOverload::of(&QSpinBox::valueChanged), this, + [this](int v) { + if (v < 200) { + pmapSaveSpin_->setValue(200); + return; + } + gPmapSaveEvery = v; + }); + connect(dirBtn, &QPushButton::clicked, this, + [this]() { browseSaveDir(); }); + connect(btnSaveDest_, &QPushButton::clicked, this, + [this]() { saveDestDialog(); }); + connect(btnSelectRoi_, &QPushButton::clicked, this, + [this]() { captureRoi(); }); + connect(btnUnsetRoi_, &QPushButton::clicked, this, + [this]() { unsetRoi(); }); + connect(btnSaveRoi, &QPushButton::clicked, this, [this]() { saveRoi(); }); + connect(btnLoadRoi, &QPushButton::clicked, this, + [this]() { loadRoiDialog(); }); + + // ── Signals: everything else ── + connect(btnStartPause, &QPushButton::clicked, this, + [this]() { toggleStartPause(); }); + connect(btnFaster, &QPushButton::clicked, this, [this]() { faster(); }); + connect(btnSlower, &QPushButton::clicked, this, [this]() { slower(); }); + connect(btnGainToggle, &QPushButton::toggled, this, + [this](bool on) { setApplyGain(on); }); + connect(btnBWPalette, &QPushButton::clicked, this, + [this]() { bwPalette1(); }); + connect(btnBWPalette2, &QPushButton::clicked, this, + [this]() { bwPalette2(); }); + connect(btnSetRanges, &QPushButton::clicked, this, + [this]() { showRangeDialog(); }); + connect(btnRates, &QPushButton::clicked, this, + [this]() { showRateDialog(); }); + connect(pmapCumulative_, &QCheckBox::toggled, this, [this](bool on) { + gPmapCumulative = on; + // "toggle to reset": either way the map starts from scratch, so the + // switch itself is the reset (no need to reach for Reset His) + resetHis(); + statusLabel->setText(on ? "Status: photon map cumulative (restarted)" + : "Status: photon map per image (restarted)"); + }); + connect(pmapEverySpin_, QOverload::of(&QSpinBox::valueChanged), this, + [this](int v) { + gPmapEvery = v > 0 ? v : 1; + statusLabel->setText( + QString("Status: photon map every %1 frames").arg(v)); + }); + connect(photonThrSpin_, QOverload::of(&QDoubleSpinBox::valueChanged), + this, [this](double v) { + // the photon map is filled by the analysis thread, which reads + // the atomic every frame; the multiplicity follows as 2 x v. + // Which one is being edited depends on the gain stage. + const bool kev = gain_flag.load(); + if (kev) + g_pheneThresholdKev = static_cast(v); + else + g_pheneThreshold = static_cast(v); + statusLabel->setText( + QString("Status: photon-map threshold %1 %2") + .arg(v, 0, 'f', kev ? 1 : 0) + .arg(kev ? "keV" : "ADU")); + }); + // "Pedestal corrected" decides whether the *saved* frames use the pedestal, + // which is what the orange route in the flow diagram shows + connect(saveCorrected_, &QCheckBox::toggled, this, [this](bool on) { + updateFlow(); + statusLabel->setText( + on ? "Status: saved frames pedestal corrected" + : "Status: saved frames raw (pedestal not subtracted)"); + }); + connect(btnResetHis, &QPushButton::clicked, this, + [this]() { resetHis(); }); + connect(btnResetAxis, &QPushButton::clicked, this, + [this]() { resetAxis(); }); + connect(btnColPal, &QPushButton::clicked, this, + [this]() { colPal(); }); + connect(btnLoadPede, &QPushButton::clicked, this, + [this]() { acquirePede(); }); + connect(btnSavePede, &QPushButton::clicked, this, + [this]() { savePede(); }); + connect(btnReadPede, &QPushButton::clicked, this, + [this]() { readPede(); }); + connect(btnLoadAllPede, &QPushButton::clicked, this, + [this]() { loadAllPede(); }); + connect(btnPedeTrack_, &QPushButton::toggled, this, [this](bool on) { + // needs the signal gate: without it "dropped" has no meaning + if (on && gateChk_ && !gateChk_->isChecked()) { + QMessageBox::information( + this, "Pedestal tracking", + "Pedestal tracking follows the frames the signal gate drops, " + "so it needs \"Gate enabled\" on."); + btnPedeTrack_->setChecked(false); + return; + } + gPedeTracking = on; + printf("[pedetrack] %s\n", on ? "on" : "off"); + fflush(stdout); + statusLabel->setText(on ? "Status: pedestal tracking on" + : "Status: pedestal tracking off"); + updateFlow(); + }); + connect(btnNoPede_, &QPushButton::toggled, this, [this](bool on) { + gNoPede = on; + updateFlow(); + statusLabel->setText(on ? "Status: pedestal NOT subtracted (raw ADC)" + : "Status: pedestal subtraction on"); + }); + connect(btnGainMap, &QPushButton::clicked, this, + [this]() { loadGainMap(); }); + connect(kevChk_, &QCheckBox::toggled, this, [this](bool on) { + kevSpectra = on; + forceRootRedraw(); + updateFlow(); // the Gain panel is in use when keV is shown + if (on && !gainMapLoaded()) { + if (gainMapVal_) + gainMapVal_->setText( + "no gain map selected - using nominal values " + "(40 ADU/keV)"); + statusLabel->setText( + "Status: PH spectra in keV (nominal 40 ADU/keV, no gain map)"); + } else { + updateGainMapLabel(); + statusLabel->setText(on ? "Status: PH spectra in keV" + : "Status: PH spectra in ADU"); + } + }); + connect(btnQuit, &QPushButton::clicked, this, [this]() { quitApp(); }); + + // ── Fast image views (separate top-level windows, as in the original) ── + m_adcView = new FastImageView(); + m_adcView->setPaletteLut(paletteLut(Palette::Color)); // match the ROOT plots + // the ADC (main) image opens on the ADC range of the Set Ranges dialog + m_adcView->setColorRange(adcmin.load(), adcmax.load()); + m_adcWin = new QWidget(nullptr, Qt::Window); + m_adcWin->setWindowTitle("ADC Image"); + m_adcWin->resize(1024 + 80, 512 + 40); + auto *adcLay = new QVBoxLayout(m_adcWin); + adcLay->setContentsMargins(0, 0, 0, 0); + adcLay->addWidget(m_adcView); + m_adcWin->show(); + + m_gainView = new FastImageView(); + m_gainView->setColorRange(0, 4); + m_gainWin = new QWidget(nullptr, Qt::Window); + m_gainWin->setWindowTitle("Gain Image"); + m_gainWin->resize(1024 + 80, 512 + 40); + auto *gainLay = new QVBoxLayout(m_gainWin); + gainLay->setContentsMargins(0, 0, 0, 0); + gainLay->addWidget(m_gainView); + m_gainWin->show(); + + m_photonView = new FastImageView(); + m_photonView->setPaletteLut(paletteLut(Palette::Color)); + m_photonView->setZeroWhite(true); // 0 photons = white, like the ROOT plot + m_photonView->setColorRange(0, 8); // photons per pixel over one image + m_photonWin = new QWidget(nullptr, Qt::Window); + m_photonWin->setWindowTitle("Photon Map (virtual photon counting)"); + m_photonWin->resize(1024 + 80, 512 + 40); + auto *photonLay = new QVBoxLayout(m_photonWin); + photonLay->setContentsMargins(0, 0, 0, 0); + photonLay->addWidget(m_photonView); + m_photonWin->show(); + +#ifdef JFSTRX + // ── Separate strixel window: the six ADC groups, arranged like the + // original ROOT ADC canvas (left column chip1 g3/g2/g1, right column + // chip6 g1/g2/g3). Display only: the gate ROI is selected on the raw + // ADC image, which stays unmapped so the coordinates are raw pixels. + m_adcPanelWin = new QWidget(nullptr, Qt::Window); + m_adcPanelWin->setWindowTitle("ADC Strixel Panels (JFSTRX)"); + m_adcPanelWin->resize(900, 900); + auto *panelRow = new QHBoxLayout(m_adcPanelWin); + panelRow->setContentsMargins(2, 2, 2, 2); + auto *panelCol0 = new QVBoxLayout(); + auto *panelCol1 = new QVBoxLayout(); + panelRow->addLayout(panelCol0); + panelRow->addLayout(panelCol1); + QVBoxLayout *panelCol[2] = {panelCol0, panelCol1}; + // panel index order: 0..2 = chip1 g1/g2/g3, 3..5 = chip6 g1/g2/g3 + static const int panelOrder[2][3] = {{2, 1, 0}, {3, 4, 5}}; // top -> bottom + static const int panelStretch[2][3] = {{5, 2, 2}, {2, 2, 5}}; + // mapped-pixel aspect X/Y: X = 75*(75/Y), Y = 25/15/18.75 um + // -> 225/25 = 9, 375/15 = 25, 300/18.75 = 16 + static const double panelAspect[kNumAdcPanels] = {9.0, 25.0, 16.0, + 9.0, 25.0, 16.0}; + static const char *panelTitle[kNumAdcPanels] = { + "chip 1 group 1 (25um)", "chip 1 group 2 (15um)", + "chip 1 group 3 (18.75um)", "chip 6 group 1 (25um)", + "chip 6 group 2 (15um)", "chip 6 group 3 (18.75um)"}; + for (int col = 0; col < 2; col++) { + for (int k = 0; k < 3; k++) { + const int p = panelOrder[col][k]; + auto *cell = new QWidget(); + auto *cv = new QVBoxLayout(cell); + cv->setContentsMargins(0, 0, 0, 0); + cv->setSpacing(0); + auto *lab = new QLabel(panelTitle[p], cell); + lab->setAlignment(Qt::AlignCenter); + cv->addWidget(lab); + auto *v = new FastImageView(); + v->setPaletteLut(paletteLut(Palette::Color)); + v->setColorRange(adcmin.load(), adcmax.load()); + v->setPixelAspectX(panelAspect[p]); + cv->addWidget(v, 1); + panelCol[col]->addWidget(cell, panelStretch[col][k]); + m_adcPanelView[p] = v; + } + } + m_adcPanelWin->show(); +#endif + + // Master-slave zoom: the ADC view is the master; the gain view follows + // immediately (same thread) and the ROOT photon map follows through a + // coalesced command, so only the newest viewport is ever redrawn. + m_adcView->onZoomChanged = [this](double x1, double y1, double x2, + double y2) { zoomTo(x1, y1, x2, y2); }; + + // ── Timers (started by start()) ── + processTimer = new QTimer(this); + connect(processTimer, &QTimer::timeout, this, + [this]() { processImages(); }); + statusTimer = new QTimer(this); + connect(statusTimer, &QTimer::timeout, this, [this]() { updateStatus(); }); + sigTimer = new QTimer(this); + connect(sigTimer, &QTimer::timeout, this, [this]() { checkSignals(); }); + + updateFlow(); + + updateDrawLabel(); +} + +void MainWindow::connectShortcuts() { + auto sc = [this](const char *key, const std::function &fn) { + QShortcut *s = new QShortcut(QKeySequence(QString::fromLatin1(key)), + this); + connect(s, &QShortcut::activated, this, fn); + }; + sc("s", [this]() { toggleStartPause(); }); + sc("+", [this]() { faster(); }); + sc("=", [this]() { faster(); }); + sc("-", [this]() { slower(); }); + sc("g", [this]() { toggleGain(); }); + sc("b", [this]() { bwPalette1(); }); + sc("Shift+B", [this]() { bwPalette2(); }); + sc("c", [this]() { colPal(); }); + sc("i", [this]() { btnGateRoi->setChecked(!btnGateRoi->isChecked()); }); + sc("e", [this]() { + // keV PH (e for energy): keeps its tick while the gain is off, see + // updateFlow. Not "k": that is already Load ROI. + if (kevChk_) + kevChk_->setChecked(!kevChk_->isChecked()); + }); + sc("n", [this]() { btnSaveStart->setChecked(!btnSaveStart->isChecked()); }); + sc("j", [this]() { saveRoi(); }); + sc("k", [this]() { loadRoiDialog(); }); + sc("r", [this]() { resetHis(); }); + sc("Shift+R", [this]() { resetAxis(); }); + sc("p", [this]() { acquirePede(); }); + sc("Shift+O", [this]() { savePede(); }); + sc("o", [this]() { readPede(); }); + sc("Shift+P", [this]() { loadAllPede(); }); + sc("q", [this]() { quitApp(); }); + sc("Shift+S", [this]() { showRangeDialog(); }); +} + +// --------------------------------------------------------------------------- +// lifecycle +// --------------------------------------------------------------------------- +void MainWindow::start() { + // Pedestals must be installed before the threads start reading frames. + std::string perr; + if (pathExists(g_cfg.pedeFile)) { + if (pedeLoadFile(g_cfg.pedeFile, perr)) { + printf("[pede] loaded %s\n", g_cfg.pedeFile.c_str()); + pedeVal_->setText(QString("loaded (%1)").arg( + QString::fromStdString(g_cfg.pedeFile))); + pipeline_.gate().setLastError(""); + } else { + fprintf(stderr, "[pede] %s\n", perr.c_str()); + pipeline_.gate().setLastError("loadPedestal: " + perr); + } + } else if (g_cfg.pedeExplicit) { + fprintf(stderr, "[pede] file not found: %s\n", g_cfg.pedeFile.c_str()); + pipeline_.gate().setLastError("loadPedestal: file not found: " + + g_cfg.pedeFile); + } else { + printf("[pede] %s not found, running on raw values\n", + g_cfg.pedeFile.c_str()); + } + + if (!g_cfg.gainMapFile.empty()) { + std::string gerr; + if (gainMapLoadFile(g_cfg.gainMapFile, gerr)) { + if (gainMapVal_) + gainMapVal_->setText(QFileInfo(QString::fromStdString( + g_cfg.gainMapFile)) + .fileName()); + if (kevChk_) + kevChk_->setChecked(true); // show what was just calibrated + updateFlow(); + } else { + fprintf(stderr, "[gain] %s\n", gerr.c_str()); + pipeline_.gate().setLastError("gainMap: " + gerr); + } + } + + std::string err; + if (!pipeline_.init(err)) { + fprintf(stderr, "FATAL: %s\n", err.c_str()); + pipeline_.gate().setLastError(err); + statusLabel->setText("Status: socket error"); + QMessageBox::critical(this, "ZMQ", QString::fromStdString(err)); + } else { + pipeline_.start(); + pipeline_.gate().setScFilter(g_cfg.scFilter); + if (scCombo_) + scCombo_->setCurrentIndex(g_cfg.scFilter < 0 ? 0 + : g_cfg.scFilter + 1); + statusLabel->setText("Status: running"); + if (g_cfg.acquirePede) + pipeline_.requestPedeAcquire(g_cfg.pedeFrames); + if (g_cfg.gateRoiX >= 0) { // --gate-roi: same path as the "ROI only" + // button, so the box and the gate agree + applyRoi(g_cfg.gateRoiX, g_cfg.gateRoiY, g_cfg.gateRoiW, + g_cfg.gateRoiH, "command line", /*restrict=*/true); + // reflect the restriction in the button, so the first press turns + // it off instead of toggling the wrong way (m_applyingRoi keeps the + // toggle handler from re-capturing or warning) + if (btnGateRoi && pipeline_.gate().roiActive() && + !btnGateRoi->isChecked()) { + m_applyingRoi = true; + btnGateRoi->setChecked(true); + m_applyingRoi = false; + } + } + } + + startRootThread(); + processTimer->start(30); + statusTimer->start(500); + sigTimer->start(200); + + // --apply-gain: same as pressing the Gain button (it also swaps the photon + // threshold to keV and relabels its row) + if (g_cfg.applyGain) + btnGainToggle->setChecked(true); + + // --start-save: press the Save button for scripted/headless runs. The + // check triggers toggleRoiSave() exactly like the GUI click. + if (g_cfg.startSave) { + printf("[save] --start-save: starting the %s writer\n", + gSaveMode.load() == 1 ? "virtual photon counting" + : "frame"); + fflush(stdout); + btnSaveStart->setChecked(true); + } + + signal(SIGINT, handleSigint); + signal(SIGTERM, handleSigint); +} + +void MainWindow::stop() { + if (processTimer) + processTimer->stop(); + if (statusTimer) + statusTimer->stop(); + if (sigTimer) + sigTimer->stop(); + { + const RoiSaver::Status st = pipeline_.saver().status(); + if (st.running) { + printf("[shutdown] flushing the ROI save (%llu frames, %s)...\n", + (unsigned long long)st.saved, st.h5Path.c_str()); + fflush(stdout); + if (statusLabel) + statusLabel->setText( + "Status: flushing the ROI save before exit..."); + } + } + pipeline_.saver().requestStop("program exit"); + pipeline_.saver().finish(); // drain the queue, close the file, write JSON + pipeline_.stop(); + stopRootThread(); +} + +// Question asked before quitting (both from the Quit button/shortcut and from +// closing the window). A running ROI save is called out explicitly, because +// quitting flushes it: the writer drains its queue, closes the HDF5 file and +// writes the JSON sidecar before the program exits. +bool MainWindow::confirmQuit() { + const RoiSaver::Status st = pipeline_.saver().status(); + QString text = "Quit the online display?"; + if (st.running) { + text += QString("\n\nA ROI save is still running: %1 frames written " + "to\n%2\n\nIt will be stopped and flushed (queued " + "frames written, file closed, JSON sidecar written) " + "before the program exits.") + .arg(static_cast(st.saved)) + .arg(QString::fromStdString(st.h5Path)); + } else if (!st.h5Path.empty()) { + text += QString("\n\nThe last ROI save is closed:\n%1\n(%2 frames)") + .arg(QString::fromStdString(st.h5Path)) + .arg(static_cast(st.saved)); + } + const QMessageBox::StandardButton answer = QMessageBox::question( + this, "Quit", text, QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); + printf("[quit] %s\n", answer == QMessageBox::Yes ? "confirmed" + : "cancelled"); + fflush(stdout); + return answer == QMessageBox::Yes; +} + +// Sizes the two empty pads that turn the panels into a flow chart: +// * the gates start low enough that the middle of "1. Storage cell" sits on +// the top edge of the Pedestal box, and +// * the Gain box starts at the middle of "2. Signal gate". +// The only thing that matters is that the two columns stay top-aligned, which +// they do, so this runs once at start-up and again if the window is resized +// wider/narrower (text wrapping can change the heights). +void MainWindow::layoutFlow() { + if (!centralWidget() || !leftPad_ || !gainPad_ || !scGroup_ || + !sigGroup_ || !pedeGroup_ || !gainGroup_) + return; + static bool inLayout = false; + if (inLayout) + return; // setFixedHeight() re-enters through resizeEvent() + inLayout = true; + flowQueued_ = false; // this call is the queued one, if it was + + // The first call comes before Qt has computed the layout, when every box + // still has a token size; solving from that produced a huge pad and, with + // it, a window minimum that stayed large for the rest of the session. + if (scGroup_->height() < 40 || sigGroup_->height() < 40 || + pedeGroup_->height() < 40 || gainGroup_->height() < 40) { + inLayout = false; + return; + } + + // Iterative, because every change alters what is being measured (boxes + // grow when their labels wrap, and the pads move the boxes). Three knobs: + // leftPad - pushes the gates down until the storage-cell middle is on + // the top edge of the Pedestal + // gatePad - the gap *between* the two gates: stretching it moves the + // signal gate down without touching the storage cell + // gainPad - fine adjustment so the Gain starts at the signal-gate middle + const int arrowMin = 10; // keep the arrows visible + leftPad_->setFixedHeight(flowPads_[0]); + pedePad_->setFixedHeight(flowPads_[1]); + gainPad_->setFixedHeight(flowPads_[2]); + if (gatePad_) + gatePad_->setFixedHeight(flowPads_[3]); + if (QLayout *l = centralWidget()->layout()) + l->activate(); + + { + const int scMid = scGroup_->mapTo(centralWidget(), + QPoint(0, scGroup_->height() / 2)) + .y(); + const int sigMid = sigGroup_->mapTo(centralWidget(), + QPoint(0, sigGroup_->height() / 2)) + .y(); + const int pedeTop = + pedeGroup_->mapTo(centralWidget(), QPoint(0, 0)).y(); + const int gainTop = + gainGroup_->mapTo(centralWidget(), QPoint(0, 0)).y(); + + // storage-cell middle must sit on the Pedestal's top edge: move + // whichever of the two has room (the Pedestal down, or the gates down) + int wantLeft = flowPads_[0], wantPede = flowPads_[1]; + const int errPede = scMid - pedeTop; // >0: the Pedestal is too high + if (errPede > 0) + wantPede += errPede; + else + wantLeft += -errPede; + if (wantLeft < 0) + wantLeft = 0; + if (wantPede < 0) + wantPede = 0; + // The Gain must start at the signal gate's middle. If it sits too + // high, push it down with gainPad; if it sits too low, push the signal + // gate down instead by stretching the arrow between the gates (the + // storage cell above is not affected by that arrow). + const int err = sigMid - gainTop; // >0: the gain is too high + int wantArrow = flowPads_[3]; + int wantGain = flowPads_[2]; + if (err > 0) { + wantGain += err; // the Gain sits above the signal middle + } else if (err < 0) { + wantArrow += -err; // below it: push the signal gate down instead + wantGain = 0; + } + // err == 0: aligned, leave both pads alone (resetting gainPad here was + // what made the solver oscillate between aligned and 50 px off) + if (wantArrow < arrowMin) + wantArrow = arrowMin; + if (wantGain < 0) + wantGain = 0; + + const bool converged = wantLeft == flowPads_[0] && + wantPede == flowPads_[1] && + wantArrow == flowPads_[3] && + wantGain == flowPads_[2]; + flowPads_[0] = wantLeft; + flowPads_[1] = wantPede; + flowPads_[2] = wantGain; + flowPads_[3] = wantArrow; + if (!converged && getenv("JF_TIMING")) + printf("[flow] pass %d: left %d pede %d gain %d gateArrow %d | " + "scMid %d sigMid %d pedeTop %d gainTop %d\n", + flowPass_, wantLeft, wantPede, wantGain, wantArrow, scMid, + sigMid, pedeTop, gainTop); + if (!converged) { + // re-measure on the next event-loop turn: within one call the + // widget positions are not guaranteed to have settled yet, and + // solving from stale numbers compounds the corrections + flowPass_++; + if (flowPass_ < 12 && !flowQueued_) { + flowQueued_ = true; + QTimer::singleShot(0, this, &MainWindow::layoutFlow); + } + } else { + flowPass_ = 0; + } + } + leftPad_->setFixedHeight(flowPads_[0]); + pedePad_->setFixedHeight(flowPads_[1]); + gainPad_->setFixedHeight(flowPads_[2]); + if (gatePad_) + gatePad_->setFixedHeight(flowPads_[3]); + if (QLayout *l = centralWidget()->layout()) + l->activate(); + + // place the strip over the gap between the two columns, spanning from the + // top of the first box to the bottom of the last one + if (strip_ && scGroup_ && sigGroup_ && pedeGroup_ && gainGroup_) { + const int x0 = scGroup_->mapTo(centralWidget(), QPoint(0, 0)).x() + + scGroup_->width() + 1; + const int x1 = + pedeGroup_->mapTo(centralWidget(), QPoint(0, 0)).x() - 1; + int top = scGroup_->mapTo(centralWidget(), QPoint(0, 0)).y(); + int bot = pedeGroup_->mapTo(centralWidget(), QPoint(0, 0)).y() + + pedeGroup_->height(); + const int gainBot = gainGroup_->mapTo(centralWidget(), QPoint(0, 0)).y() + + gainGroup_->height(); + if (gainBot > bot) + bot = gainBot; + if (roiGroup_) { + const int roiBot = + roiGroup_->mapTo(centralWidget(), QPoint(0, 0)).y() + + roiGroup_->height(); + if (roiBot > bot) + bot = roiBot; + } + if (x1 > x0 && bot > top) + strip_->setGeometry(x0, top - 6, x1 - x0, bot - top + 12); + strip_->raise(); + strip_->show(); + } + + // hand the strip its connectors, in its own coordinates + if (strip_ && scGroup_ && sigGroup_ && pedeGroup_ && gainGroup_ && + pmapGroup_ && displayGroup_) { + auto yIn = [&](QWidget *w, int y) { + return w->mapTo(centralWidget(), QPoint(0, 0)).y() + y; + }; + const int stripTop = + strip_->mapTo(centralWidget(), QPoint(0, 0)).y(); + // The signal gate does not change the data, so nothing enters or + // leaves its box: the flow runs from the previous stage to the Gain and + // simply passes the gate, which is drawn as a funnel on that line (the + // gate panel only points at the funnel - see FlowStrip::drawFunnel). + const int funnelY = + yIn(sigGroup_, sigGroup_->height() / 2) - stripTop; + FlowStrip::Path a, b, c; + // 1. storage cell (middle) -> Pedestal (middle): enter from the left + a.yFrom = yIn(scGroup_, scGroup_->height() / 2) - stripTop; + a.yTo = yIn(pedeGroup_, pedeGroup_->height() / 2) - stripTop; + a.dir = +1; + a.cls = FlowPedestal; // into the Pedestal + // Pedestal (bottom) -> the funnel, travelling leftwards (no head: it + // merges into the funnel) + b.yFrom = yIn(pedeGroup_, pedeGroup_->height()) - stripTop; + b.yTo = funnelY; + b.dir = -1; + b.x = FlowStrip::kFunnelX; + b.head = false; + // the funnel -> Gain (top): the flow continues after the gate + c.yFrom = funnelY; + c.yTo = yIn(gainGroup_, 0) - stripTop; + c.dir = +1; + c.x = FlowStrip::kFunnelX; + c.startAtJog = true; + // Gain -> ROI: a rail just left of the right column, so it does not + // have to cross the Display panel. Frames are the ROI-cropped + // product, so this is the frames-mode input of the ROI. + FlowStrip::Path d; + d.yFrom = yIn(gainGroup_, gainGroup_->height() / 2) - stripTop; + d.yTo = roiGroup_ ? (yIn(roiGroup_, roiGroup_->height() / 2) - stripTop) + : 0; + d.dir = 2; + d.on = roiGroup_ != nullptr; + d.cls = FlowData; // corrected when setSaveMode() knows the mode + // the gate block -> Pedestal tracking: the tracker lives off the frames + // the gate *drops*, so it leaves the gate block's top-right corner + FlowStrip::Path e; + e.yFrom = yIn(sigGroup_, 6) - stripTop; + e.yTo = pedeTrackGroup_ + ? (yIn(pedeTrackGroup_, pedeTrackGroup_->height() / 2) - + stripTop) + : 0; + e.dir = +1; + e.on = pedeTrackGroup_ != nullptr; + e.cls = FlowPedestal; // the tracker is the pedestal's own feedback + // Gain -> Virtual photon counting: the calibration reaches the counting + // (its threshold is keV while the gain correction is on). The map is + // built in the left column, so the connector travels leftwards. + FlowStrip::Path f; + f.yFrom = yIn(gainGroup_, gainGroup_->height() - 8) - stripTop; + f.yTo = pmapGroup_ ? (yIn(pmapGroup_, 24) - stripTop) : 0; + f.dir = -1; + f.on = pmapGroup_ != nullptr; + f.cls = FlowNormal; // flipped by setSaveMode() + // Virtual photon counting -> Display: the map is shown (in its own + // window), so this one travels rightwards into the Display. + FlowStrip::Path g; + g.yFrom = pmapGroup_ + ? (yIn(pmapGroup_, pmapGroup_->height() - 8) - stripTop) + : 0; + g.yTo = displayGroup_ + ? (yIn(displayGroup_, displayGroup_->height() / 2) - stripTop) + : 0; + g.dir = +1; + g.on = pmapGroup_ != nullptr && displayGroup_ != nullptr; + // counting -> ROI: with the maps as the saved product this is the + // input of the ROI (they are cropped to it on the way out) + FlowStrip::Path i; + i.yFrom = yIn(pmapGroup_, pmapGroup_->height() - 8) - stripTop; + i.yTo = roiGroup_ ? (yIn(roiGroup_, roiGroup_->height() / 2) - stripTop) + : 0; + i.dir = +1; + i.cls = FlowData; + i.on = roiGroup_ != nullptr; + // raw mode: the storage cell feeds the funnel directly, so the flow + // does not have to pass through the (greyed) Pedestal box. Both + // feeders are always drawn; setStages() decides which one is live. + FlowStrip::Path h; + h.yFrom = yIn(scGroup_, scGroup_->height()) - stripTop; + h.yTo = funnelY; + h.dir = +1; + h.x = FlowStrip::kFunnelX; + h.head = false; // merges into the funnel + strip_->setPaths(a, b, c, d, e, f, g, h, i, funnelY); + bool dp, sp, go, pm; + flowStageStates(dp, sp, go, pm); + strip_->setStages(dp, sp, go, pm); + } + inLayout = false; + + if (getenv("JF_TIMING")) { + printf("[flow] pads: gates %d pede %d gain %d gateArrow %d (passes " + "%d)\n", + flowPads_[0], flowPads_[1], flowPads_[2], flowPads_[3], + flowPass_); + auto rect = [&](QWidget *w) { + return w ? QString("%1,%2 %3x%4") + .arg(w->mapTo(centralWidget(), QPoint(0, 0)).x()) + .arg(w->mapTo(centralWidget(), QPoint(0, 0)).y()) + .arg(w->width()) + .arg(w->height()) + .toStdString() + : std::string("-"); + }; + printf("[rect] sc %s | signal %s | pede %s | track %s | gain %s | " + "roi %s\n", + rect(scGroup_).c_str(), rect(sigGroup_).c_str(), + rect(pedeGroup_).c_str(), rect(pedeTrackGroup_).c_str(), + rect(gainGroup_).c_str(), rect(roiGroup_).c_str()); + printf("[rect] gainToggle %s | keVchk %s | noPede %s | saveStart %s | " + "selectRoi %s | saveRoi %s | quit %s | track %s | pmap %s\n", + rect(btnGainToggle).c_str(), rect(kevChk_).c_str(), + rect(btnNoPede_).c_str(), rect(btnSaveStart).c_str(), + rect(btnSelectRoi_).c_str(), rect(btnSaveRoi).c_str(), + rect(btnQuit).c_str(), rect(btnPedeTrack_).c_str(), + rect(pmapGroup_).c_str()); + printf("[rect] unsetRoi %s | selectRoi %s | thr %s | minPix %s\n", + rect(btnUnsetRoi_).c_str(), rect(btnSelectRoi_).c_str(), + rect(thrSpin_).c_str(), rect(minPixSpin_).c_str()); + printf("[rect] saveFramesRadio %s | savePhotonRadio %s | " + "saveCorrected %s\n", + rect(saveModeFrames_).c_str(), rect(saveModePhoton_).c_str(), + rect(saveCorrected_).c_str()); + } + // The pads and the alignment moves push the panels around *after* the first + // paint; repaint the column area so the space they leave behind does not + // keep the old pixels (that is what left a piece of the "Virtual photon + // counting" title floating next to the Save box). + if (centralWidget()) + centralWidget()->update(); +} + +// Tells the overlay which boxes are in the chain right now: a stage that is +// switched off is skipped, so the arrow steps over it. +// What the ROI panel shows: the ROI the gate is using and how many are saved. +void MainWindow::updateRoiLabel() { + if (!roiListVal_) + return; + QString act = "no ROI selected"; + if (pipeline_.gate().roiSet()) { + int x0 = 0, y0 = 0, x1 = 0, y1 = 0; + pipeline_.gate().roiSnapshot(x0, y0, x1, y1); + act = QString("active %1x%2 at %3,%4") + .arg(x1 - x0) + .arg(y1 - y0) + .arg(x0) + .arg(y0); + } + roiListVal_->setText(QString("%1 | gate: %2 | %3 saved") + .arg(act) + .arg(pipeline_.gate().roiActive() ? "ROI only" + : "full frame") + .arg(roiListLoad().size())); +} + +// One place that says what the keV conversion is using. +void MainWindow::updateGainMapLabel() { + if (!gainMapVal_) + return; + if (!gain_flag.load()) + gainMapVal_->setText("gain off: PH spectra in ADU"); + else if (gainMapLoaded()) + gainMapVal_->setText(QFileInfo(QString::fromStdString(g_cfg.gainMapFile)) + .fileName()); + else if (kevChk_ && kevChk_->isChecked()) + gainMapVal_->setText( + "no gain map selected - using nominal values (40 ADU/keV)"); + else + gainMapVal_->setText("no gain map loaded : using defaults"); +} + +void MainWindow::updateFlow() { + const bool pede = PedestalStore::instance().loaded() && !gNoPede.load(); + const bool gain = gain_flag.load(); + const bool scAll = !scCombo_ || scCombo_->currentIndex() <= 0; + const bool gateOn = !gateChk_ || gateChk_->isChecked(); + // The arrows are static: the chain is always drawn the same way, and a + // stage that is not in use is greyed out instead, so the bypass is obvious + // without the arrows moving. + // the stage's own switch stays black: Pede/Raw, Gate enabled and the gain + // button are how a bypassed stage is switched back on + greyStage(pedeGroup_, !pede, {btnNoPede_}); + // the Gain panel follows the gain toggle alone; the keV checkbox keeps its + // tick for next time but is greyed while the gain is off, and the plots + // fall back to ADU + greyStage(gainGroup_, !gain, {btnGainToggle}); + if (kevChk_) + kevChk_->setEnabled(gain); + if (gainStateChk_) + gainStateChk_->setChecked(gain); // read-only indicator, no signals + // a gate that is switched off greys out exactly like a bypassed stage + // Greyed when it cannot be *selected* (the tracker needs the signal gate), + // NOT when it is merely inactive: the label under the button says whether + // tracking is on, so the box must not go grey just because it is off. + greyStage(pedeTrackGroup_, !gateOn); + greyStage(scGroup_, scAll, {scCombo_}); // storage-cell filter == ALL + greyStage(sigGroup_, !gateOn, {gateChk_}); // signal gate off == passthrough + // photon counting needs a pedestal (it thresholds the pede-subtracted + // ADC), so the whole virtual-photon-counting box is a bypassed stage when + // the pedestal is out of the chain - the display map goes empty with it + greyStage(pmapGroup_, !pede); + // pedestal tracking only makes sense with the signal gate on: enable the + // button here, but do not flip it (the gate's own handler does that when it + // is switched off, so --pede-tracking survives start-up) + if (btnPedeTrack_) + btnPedeTrack_->setEnabled(gateOn); + if (pedeTrackVal_) { + const PedeTrackStats ts = pedeTrackerStats(); + if (!ts.on) + pedeTrackVal_->setText("off"); + else + pedeTrackVal_->setText( + QString("on: %1 samples, delta %2 ADU, %3 updates") + .arg(static_cast(ts.samples)) + .arg(ts.maxDiff, 0, 'f', 1) + .arg(static_cast(ts.updates))); + } + // the gate threshold is applied to the raw 14-bit value when no pedestal + // is in play, so say so instead of claiming "pede-subtracted" + if (thrLabel_) { + const bool raw = gNoPede.load() || !PedestalStore::instance().loaded(); + thrLabel_->setText(raw ? "Threshold (raw ADC - no pede):" + : "Threshold (ADC, pede-subtracted):"); + } + // The data path (orange) follows the save mode: frames are written from the + // ROI, photon maps from the virtual photon counter. Only the colours + // change - the connectors themselves never move. + { + // only the branch that actually feeds the file is drawn, and it is + // orange - the frames come from the ROI, the maps from the counting + bool pmap = false, savePede = false; + bool dPede, gOn; + flowStageStates(dPede, savePede, gOn, pmap); + // the file is always entered through the ROI: the maps are cropped to + // it on the way out, so this arrow never moves and is always orange + if (arrRoiDown_) { + setFlowArrowClass(arrRoiDown_, FlowData); + arrRoiDown_->setVisible(true); + } + if (strip_) + strip_->setStages(dPede, savePede, gOn, pmap); + } + updateGainMapLabel(); + updateRoiLabel(); + if (noPedeVal_) { + if (gNoPede.load()) + noPedeVal_->setText("no pede: raw ADC"); + else if (PedestalStore::instance().loaded()) + noPedeVal_->setText("pede subtracted"); + else + noPedeVal_->setText("no pedestal loaded"); + } +} + +void MainWindow::showEvent(QShowEvent *event) { + QMainWindow::showEvent(event); + flowPass_ = 0; + layoutFlow(); +} + +void MainWindow::resizeEvent(QResizeEvent *event) { + QMainWindow::resizeEvent(event); + if (!flowQueued_) + flowPass_ = 0; // a fresh solve for the new geometry + layoutFlow(); +} + +void MainWindow::closeEvent(QCloseEvent *event) { + if (!confirmQuit()) { + event->ignore(); // the operator changed their mind + return; + } + stop(); + closeSideWindows(); + event->accept(); + // ...and end the program even if a side window somehow refused to close + QApplication::quit(); +} + +// --------------------------------------------------------------------------- +// periodic GUI work +// --------------------------------------------------------------------------- +void MainWindow::processImages() { + updateReceiverRoiOverlay(); // cheap when the ROI did not change + if (goout.load() == 0) + return; + std::lock_guard lock(pipeline_.images().mtx); + if (!pipeline_.images().ready) + return; + // ADC and gain are always the raw 1024x512 layout (ROI selection on them + // stays in raw data coordinates, also in a JFSTRX build) + m_adcView->setImageCopy(pipeline_.images().adc.data(), kRawWidth, + kRawHeight); + if (m_photonView && pipeline_.images().photonReady && + pipeline_.images().photonSeq != pmapSeq_ && + !pipeline_.images().photon.empty()) { + pmapSeq_ = pipeline_.images().photonSeq; + m_photonView->setImageCopy(pipeline_.images().photon.data(), kRawWidth, + 512); + m_photonView->update(); + } + m_gainView->setImageCopy(pipeline_.images().gain.data(), kRawWidth, + kRawHeight); +#ifdef JFSTRX + if (pipeline_.images().panelsReady) { + for (int p = 0; p < kNumAdcPanels; p++) { + if (m_adcPanelView[p] && !pipeline_.images().panels[p].empty()) + m_adcPanelView[p]->setImageCopy( + pipeline_.images().panels[p].data(), kAdcPanelW[p], + kAdcPanelH[p]); + } + } +#endif + pipeline_.images().ready = false; +} + +// Mirrors the receiver ROI ("rx_roi" in the stream header) onto the ADC image +// as the RED box, next to the yellow gate ROI. The ADC image is the raw +// 1024x512 layout, so this is always a plain rectangle in raw coordinates. +void MainWindow::updateReceiverRoiOverlay() { + if (!m_adcView) + return; + const int xmin = rxRoiXmin.load(); + const int xmax = rxRoiXmax.load(); + const int ymin = rxRoiYmin.load(); + const int ymax = rxRoiYmax.load(); + if (!rxRoiValid.load() || xmax <= xmin || ymax <= ymin) { + if (m_rxRoiShown) { + m_adcView->setReceiverRoiRect(0, 0, 0, 0, false); + m_rxRoiShown = false; + } + return; + } + if (m_rxRoiShown && xmin == m_rxRoiLast[0] && xmax == m_rxRoiLast[1] && + ymin == m_rxRoiLast[2] && ymax == m_rxRoiLast[3]) + return; + m_adcView->setReceiverRoiRect(xmin, ymin, xmax + 1, ymax + 1, true); + m_rxRoiShown = true; + m_rxRoiLast[0] = xmin; + m_rxRoiLast[1] = xmax; + m_rxRoiLast[2] = ymin; + m_rxRoiLast[3] = ymax; +} + +void MainWindow::updateStatus() { + const GateEngine::Counters s = pipeline_.gate().snapshot(); + const QLocale loc; + + frameLabel->setText( + QString("Frames: display %1 | in stream %2 | gap %3") + .arg(loc.toString(static_cast(pipeline_.displayFrames()))) + .arg(loc.toString(static_cast(s.framesIn))) + .arg(loc.toString(static_cast(s.framesIn - + s.framesPass)))); + + // Stream state comes from ZMQ socket monitoring: an idle-but-alive + // receiver is reported as connected, never as "stopped". + QString streamTxt; + for (size_t k = 0; k < pipeline_.portCount(); k++) { + double since = 0.0; + const Pipeline::StreamState st = + pipeline_.streamState(static_cast(k), &since); + QString one; + switch (st) { + case Pipeline::StreamState::Disconnected: + one = "disconnected"; + break; + case Pipeline::StreamState::Connecting: + one = "waiting..."; + break; + case Pipeline::StreamState::Connected: + one = "connected"; + break; + case Pipeline::StreamState::Idle: + one = QString("connected (no frames for %1 s)").arg(since, 0, 'f', 1); + break; + case Pipeline::StreamState::EndOfAcq: + one = "connected (end of acquisition)"; + break; + } + const int w = pipeline_.portWidth(static_cast(k)); + const int h = pipeline_.portHeight(static_cast(k)); + const QString geo = + (w > 0 && h > 0) ? QString(" [%1x%2]").arg(w).arg(h) : QString(); + if (k) + streamTxt += "\n"; + streamTxt += QString("%1: %2%3 (%4 frames, last #%5)") + .arg(pipeline_.portList()[k]) + .arg(one) + .arg(geo) + .arg(pipeline_.portFrames(static_cast(k))) + .arg(pipeline_.portLastFrame(static_cast(k))); + } + streamVal_->setText(streamTxt); + + // frame-index alignment: delta between the newest frames of each + // interface, plus what had to be discarded to stay aligned + if (pipeline_.portCount() > 1) { + const qlonglong f0 = static_cast( + pipeline_.portLastFrame(0)); + const qlonglong f1 = static_cast( + pipeline_.portLastFrame(1)); + alignVal_->setText( + QString("delta %1 | aligned %2 | resync drops %3 | behind drops " + "%4 | unpaired %5") + .arg(f1 - f0) + .arg(loc.toString(static_cast(s.framesIn))) + .arg(loc.toString( + static_cast(pipeline_.resyncDrops()))) + .arg(loc.toString( + static_cast(pipeline_.behindDrops()))) + .arg(loc.toString( + static_cast(pipeline_.incompleteFrames())))); + alignVal_->setVisible(true); + } else { + alignVal_->setVisible(false); + } + if (s.pedeLoaded) + pedeVal_->setText(QString("loaded (%1)%2") + .arg(QString::fromStdString(g_cfg.pedeFile)) + .arg(gPedeTracked.load() ? " [tracked]" + : "")); + else + pedeVal_->setText("not loaded"); + + displayDropVal_->setText( + loc.toString(static_cast(pipeline_.displayDropped()))); + + const QString err = QString::fromStdString(s.lastError); + errVal_->setText(err.isEmpty() ? QString() : QString("Last error: %1").arg(err)); + + framesInCumVal_->setText(loc.toString(static_cast(s.framesIn))); + framesInCurVal_->setText(loc.toString(static_cast(s.framesInCur))); + framesPassCumVal_->setText( + loc.toString(static_cast(s.framesPass))); + framesPassCurVal_->setText( + loc.toString(static_cast(s.framesPassCur))); + framesDropCumVal_->setText( + loc.toString(static_cast(s.framesDrop))); + framesDropCurVal_->setText( + loc.toString(static_cast(s.framesDropCur))); + updateFlow(); // the pedestal/gain state may have changed since last time + + scDropCumVal_->setText( + loc.toString(static_cast(s.framesScDrop))); + scDropCurVal_->setText( + loc.toString(static_cast(s.framesScDropCur))); + if (scLastVal_) + scLastVal_->setText(s.lastSc < 0 + ? QString("last frame: ? (no SC in header)") + : QString("last frame: SC %1").arg(s.lastSc)); + pixelsAboveCumVal_->setText( + loc.toString(static_cast(s.pixelsAbove))); + pixelsAboveCurVal_->setText( + loc.toString(static_cast(s.pixelsAboveCur))); + rateInVal_->setText(QString("%1 fps / %2 MB/s") + .arg(loc.toString(s.rateIn, 'f', 1)) + .arg(loc.toString(s.rateInMBs, 'f', 1))); + ratePassVal_->setText(QString("%1 fps / %2 MB/s") + .arg(loc.toString(s.ratePass, 'f', 1)) + .arg(loc.toString(s.ratePassMBs, 'f', 1))); + dropFracVal_->setText(loc.toString(s.dropFrac, 'f', 3)); + + if (pipeline_.pedeAcquireActive()) + statusLabel->setText( + QString("Status: acquiring pede... %1 frames") + .arg(pipeline_.pedeAcquireProgress())); + + // ROI save progress + auto-stop detection (budget reached) + if (pipeline_.saver().running()) { + if (m_saveWasRunning != true) { + m_saveWasRunning = true; + if (btnSaveStart) + btnSaveStart->setChecked(true); + } + updateSaveLabel(); + } else if (m_saveWasRunning) { + m_saveWasRunning = false; + if (btnSaveStart) + btnSaveStart->setChecked(false); + applySaveLock(false); // the run ended (user, budget or error) + updateFlow(); + updateSaveLabel(); + const RoiSaver::Status st = pipeline_.saver().status(); + statusLabel->setText(QString("Status: ROI save stopped (%1), %2 saved") + .arg(QString::fromStdString(st.stopReason)) + .arg(st.saved)); + } + + // gate ROI, for the headless line + char roiTxt[64]; + if (pipeline_.gate().roiActive()) { + int rx0, ry0, rx1, ry1; + pipeline_.gate().roiSnapshot(rx0, ry0, rx1, ry1); + snprintf(roiTxt, sizeof(roiTxt), "%dx%d@%d,%d", rx1 - rx0, ry1 - ry0, + rx0, ry0); + } else { + snprintf(roiTxt, sizeof(roiTxt), "off"); + } + + // ROI save state, for the headless line + char saveTxt[96]; + { + const RoiSaver::Status ss = pipeline_.saver().status(); + // the first number is whatever this mode writes: frames or maps + const unsigned long long n = + (unsigned long long)(ss.photonMode ? ss.pmaps : ss.saved); + if (ss.running && ss.photonMode) + snprintf(saveTxt, sizeof(saveTxt), "%llumap/%llu", n, + (unsigned long long)ss.dropped); + else if (ss.running) + snprintf(saveTxt, sizeof(saveTxt), "%llu/%llu", n, + (unsigned long long)ss.dropped); + else if (!ss.h5Path.empty()) + snprintf(saveTxt, sizeof(saveTxt), "done:%llu%s/%llu", n, + ss.photonMode ? "map" : "", + (unsigned long long)ss.dropped); + else + snprintf(saveTxt, sizeof(saveTxt), "off"); + } + + // optional headless monitoring: the counter line the old CLI printed + if (g_cfg.printCounters) { + static int tick = 0; + if (++tick >= 2) { // updateStatus() runs every 500 ms + tick = 0; + printf("in=%llu pass=%llu drop=%llu inrate=%.1f passrate=%.1f " + "dropfrac=%.3f px=%llu display=%llu displaydrop=%llu " + "stream=%s resync=%llu behind=%llu incomplete=%llu " + "ports=%s workers=%d roi=%s save=%s gainmap=%s sc=%s " + "scdrop=%llu pthr=%g%s kev=%s ptrack=%s shown=%llu\n", + static_cast(s.framesIn), + static_cast(s.framesPass), + static_cast(s.framesDrop), s.rateIn, + s.ratePass, s.dropFrac, + static_cast(s.pixelsAbove), + static_cast(pipeline_.displayFrames()), + static_cast(pipeline_.displayDropped()), + pipeline_.streamStateName(), + static_cast(pipeline_.resyncDrops()), + static_cast(pipeline_.behindDrops()), + static_cast( + pipeline_.incompleteFrames()), + pipeline_.streamStateSummary().c_str(), + g_cfg.analysisWorkers, roiTxt, saveTxt, + gainMapLoaded() ? "loaded" : "off", + s.scFilter < 0 ? "all" : std::to_string(s.scFilter).c_str(), + static_cast(s.framesScDrop), + static_cast(gain_flag.load() ? g_pheneThresholdKev + : g_pheneThreshold), + gain_flag.load() ? "keV" : "ADU", + (kevSpectra.load() && gain_flag.load()) + ? (gainMapLoaded() ? "loaded" : "nominal") + : "off", + gPedeTracking.load() + ? (pedeTrackerStats().updates > 0 ? "on-upd" : "on") + : "off", + static_cast(pipeline_.imagesShown())); + fflush(stdout); + } + } +} + +void MainWindow::checkSignals() { + if (g_sigCount == 0) + return; + if (g_sigCount > 1) + std::_Exit(0); + printf("[shutdown] signal received, stopping threads\n"); + fflush(stdout); + statusLabel->setText("Status: stopping..."); + stop(); + QApplication::quit(); +} + +// --------------------------------------------------------------------------- +// actions +// --------------------------------------------------------------------------- +void MainWindow::toggleStartPause() { + if (goout.load() == 0) { + goout = 1; + statusLabel->setText("Status: running"); + } else { + goout = 0; + statusLabel->setText("Status: paused"); + } +} + +void MainWindow::faster() { + int v = int(HDraw_every.load() * 0.8f); + if (v < 1) + v = 1; + HDraw_every = v; + cout << endl << "Drawing every " << HDraw_every.load() << " frames" << endl; + updateDrawLabel(); +} + +void MainWindow::slower() { + int cur = HDraw_every.load(); + int v = int(cur * 1.25f); + if (v <= cur) + v = cur + 1; // avoid 1->1 + HDraw_every = v; + cout << endl << "Drawing every " << HDraw_every.load() << " frames" << endl; + updateDrawLabel(); +} + +// "Apply gain": divide the pixels by their ADU/keV (map if loaded, otherwise +// the nominal 40) so the keV spectra are physically meaningful. The plots show +// keV only while this is pushed and "keV PH" is ticked. +void MainWindow::toggleGain() { + if (btnGainToggle) + btnGainToggle->setChecked(!btnGainToggle->isChecked()); +} + +void MainWindow::setApplyGain(bool on) { + // The photon-counting threshold is in ADU with the gain off and in keV with + // it on: keep the value that is on screen in its own unit and show the + // other one, so toggling the gain never destroys either number. + if (photonThrSpin_) { + const double shown = photonThrSpin_->value(); + if (on) + g_pheneThreshold = static_cast(shown); // it was ADU + else + g_pheneThresholdKev = static_cast(shown); // it was keV + const QSignalBlocker block(photonThrSpin_); // no handler round trip + photonThrSpin_->setDecimals(on ? 1 : 0); + photonThrSpin_->setSingleStep(on ? 0.5 : 10.0); + photonThrSpin_->setValue(static_cast( + on ? g_pheneThresholdKev : g_pheneThreshold)); + if (photonThrLabel_) + photonThrLabel_->setText(on ? "photon threshold (keV):" + : "photon threshold (ADU):"); + } + gain_flag = on; + // Toggling the gain mid-save changes the unit of the photon counting, so + // the maps written from here on use the other one while the JSON keeps the + // value it recorded at save start. Say it out loud instead of leaving a + // file with two incompatible scales. + if (gSaveMode.load() == 1 && pipeline_.saver().status().running) { + printf("[gain] warning: the photon-counting unit is now %s while a " + "photon-map save is running - maps written from here on use the " + "new unit, the JSON keeps the one from save start\n", + on ? "keV" : "ADU"); + fflush(stdout); + } + updateFlow(); // the arrows bypass the Gain box when it is off + printf("[gain] apply gain %s (ADU/keV: %s)\n", on ? "ON" : "off", + on ? (gainMapLoaded() ? "from the gain map" : "nominal 40") : "-"); + fflush(stdout); + statusLabel->setText(on ? (gainMapLoaded() + ? "Status: apply gain on (per-pixel map)" + : "Status: apply gain on (nominal 40 ADU/keV)") + : "Status: apply gain off (raw ADC)"); +} + +void MainWindow::bwPalette1() { + RootCmd c; + c.type = RootCmdType::PaletteBW1; + pushRootCmd(c); + m_adcView->setPaletteLut(paletteLut(Palette::BW1)); // same colours as ROOT + statusLabel->setText("Status: BW palette"); +} + +void MainWindow::bwPalette2() { + RootCmd c; + c.type = RootCmdType::PaletteBW2; + pushRootCmd(c); + m_adcView->setPaletteLut(paletteLut(Palette::BW2)); + statusLabel->setText("Status: BW palette 2"); +} + +void MainWindow::colPal() { + RootCmd c; + c.type = RootCmdType::PaletteColor; + pushRootCmd(c); + m_adcView->setPaletteLut(paletteLut(Palette::Color)); + statusLabel->setText("Status: colour palette"); +} + +// The ADC widget works in DISPLAY coordinates (row 0 = top of the widget), +// while every ROOT plot and the gate use DATA coordinates (row 0 = bottom, +// (0,0) = bottom-left). This is the single place where the two meet: the +// gain view follows in display coordinates, the ROOT photon map gets the +// range converted to data rows AND framed on pixel edges (the histogram has +// one bin per pixel centred on the integer coordinate, so showing display rows +// [y1,y2) means the data range [ny-y2-0.5, ny-y1-0.5]). +void MainWindow::zoomTo(double x1, double y1, double x2, double y2) { + if (x2 - x1 < 2 || y2 - y1 < 2) + return; // never push a degenerate range to the ROOT canvases + if (m_gainView) + m_gainView->setViewport(x1, y1, x2, y2); + if (m_photonView) + m_photonView->setViewport(x1, y1, x2, y2); + const double ny = + (m_adcView && m_adcView->imageHeight() > 0) + ? static_cast(m_adcView->imageHeight()) + : 512.0; + RootCmd c; + c.type = RootCmdType::Zoom; + c.a = x1 - 0.5; + c.b = ny - y2 - 0.5; + c.c = x2 - 0.5; + c.d = ny - y1 - 0.5; + pushRootCmdCoalesced(c); +} + +// The gate ROI is captured from the ADC viewport when the button is pressed +// and then stays fixed, so the operator can keep panning/zooming the image. +// The viewport is in DISPLAY coordinates (row 0 at the top) while the gate +// works in DATA rows (row 0 at the bottom), hence the vertical flip. +void MainWindow::toggleGateRoi(bool on) { + if (on && m_applyingRoi) + return; // ROI was set directly by applyRoi(); do not re-capture + + if (!on) { // stop restricting, but keep the current ROI + pipeline_.gate().setRoiOnly(false); + pipeline_.gate().resetCounters(); + printf("[roi] gate over the full frame (current ROI kept)\n"); + fflush(stdout); + statusLabel->setText( + "Status: gate over the full frame (ROI kept as the current one)"); + updateRoiLabel(); + return; + } + if (!pipeline_.gate().roiSet()) { + QMessageBox::warning( + this, "ROI only", + "No ROI selected.\n\nUse \"Select ROI (ADC zoom)\" in the ROI " + "panel (zoom the ADC image first), or pick one with " + "\"Load ROI...\", then press ROI only again."); + btnGateRoi->setChecked(false); // leave it unselected + return; + } + pipeline_.gate().setRoiOnly(true); + pipeline_.gate().resetCounters(); + int x0 = 0, y0 = 0, x1 = 0, y1 = 0; + pipeline_.gate().roiSnapshot(x0, y0, x1, y1); + printf("[roi] gate restricted to the current ROI x=%d..%d y=%d..%d\n", x0, + x1, y0, y1); + fflush(stdout); + statusLabel->setText(QString("Status: gate restricted to the current ROI " + "%1x%2 at %3,%4") + .arg(x1 - x0) + .arg(y1 - y0) + .arg(x0) + .arg(y0)); + updateRoiLabel(); +} + +// Selects the ADC zoom as the current ROI. Selection only: whether the gate +// is restricted to it is the "ROI only" button's business. +void MainWindow::captureRoi() { + if (!m_adcView || !m_adcView->hasImage()) { + QMessageBox::information( + this, "Select ROI", + "No image yet - wait for frames and zoom the ADC image first."); + return; + } + double vx = 0, vy = 0, vw = 0, vh = 0; + m_adcView->viewport(vx, vy, vw, vh); + const int imgW = m_adcView->imageWidth(); + const int imgH = m_adcView->imageHeight(); + + int x0 = static_cast(std::floor(vx)); + int x1 = static_cast(std::ceil(vx + vw)); + const int dy0 = static_cast(std::floor(vy)); + const int dy1 = static_cast(std::ceil(vy + vh)); + // display row d <-> data row (imgH - 1 - d) + int y0 = imgH - dy1; + int y1 = imgH - dy0; + if (x0 < 0) x0 = 0; + if (y0 < 0) y0 = 0; + if (x1 > imgW) x1 = imgW; + if (y1 > imgH) y1 = imgH; + + pipeline_.gate().setRoi(x0, y0, x1, y1); + if (!pipeline_.gate().roiSet()) { // too small to be useful + QMessageBox::information(this, "Select ROI", + "The selected region is too small."); + return; + } + if (m_adcView) + m_adcView->setOverlayRect(x0, y0, x1, y1, true); + if (gateRoiVal_) + gateRoiVal_->setText(QString("%1x%2 at %3,%4 (data rows)") + .arg(x1 - x0) + .arg(y1 - y0) + .arg(x0) + .arg(y0)); + printf("[roi] current ROI x=%d..%d y=%d..%d (%dx%d px, data rows, row 0 = " + "bottom) selected from the ADC zoom\n", + x0, x1, y0, y1, x1 - x0, y1 - y0); + fflush(stdout); + updateRoiLabel(); + statusLabel->setText( + QString("Status: ROI %1x%2 at %3,%4 is the current ROI%5") + .arg(x1 - x0) + .arg(y1 - y0) + .arg(x0) + .arg(y0) + .arg(pipeline_.gate().roiActive() + ? " (gate restricted to it)" + : " - press \"ROI only\" to restrict the gate")); +} + +// Adds the active gate ROI to the list file in the program folder. +void MainWindow::saveRoi() { + if (!pipeline_.gate().roiSet()) { + QMessageBox::information( + this, "Save ROI", + "No ROI selected.\n\nZoom the ADC image and press " + "\"Select ROI (ADC zoom)\" first (or load one from the list)."); + return; + } + int x0, y0, x1, y1; + pipeline_.gate().roiSnapshot(x0, y0, x1, y1); + const int w = x1 - x0, h = y1 - y0; + if (!roiListUpsert(x0, y0, w, h)) { + QMessageBox::warning(this, "Save ROI", + QString("Could not write %1") + .arg(QString::fromStdString(g_cfg.roiFile))); + return; + } + const size_t n = roiListLoad().size(); + updateRoiLabel(); + statusLabel->setText(QString("Status: ROI %1x%2 at %3,%4 saved (%5 in list)") + .arg(w).arg(h).arg(x0).arg(y0).arg(n)); +} +// Applies a ROI to the gate without capturing the ADC viewport, and makes +// sure the "ROI only" button reflects that the gate is restricted. +void MainWindow::applyRoi(int x, int y, int w, int h, const QString &src, + bool restrict) { + pipeline_.gate().setRoi(x, y, x + w, y + h); + if (!pipeline_.gate().roiSet()) { + QMessageBox::information(this, "Load ROI", + "That entry is too small to use."); + return; + } + // -1 = keep the current restriction, 0/1 = force it off/on + if (restrict) + pipeline_.gate().setRoiOnly(true); + pipeline_.gate().resetCounters(); + if (m_adcView) + m_adcView->setOverlayRect(x, y, x + w, y + h, true); + printf("[roi] current ROI x=%d..%d y=%d..%d (%dx%d px, data rows, row 0 = " + "bottom) applied from %s\n", + x, x + w, y, y + h, w, h, src.toLocal8Bit().constData()); + fflush(stdout); + if (gateRoiVal_) + gateRoiVal_->setText(QString("%1x%2 at %3,%4 (data rows)") + .arg(w).arg(h).arg(x).arg(y)); + updateRoiLabel(); + statusLabel->setText( + QString("Status: ROI %1x%2 at %3,%4 is the current ROI (from %5)%6") + .arg(w).arg(h).arg(x).arg(y).arg(src) + .arg(pipeline_.gate().roiActive() + ? " - gate restricted to it" + : " - press \"ROI only\" to restrict the gate")); +} + +// List of saved ROIs, most recently saved/loaded first. +void MainWindow::loadRoiDialog() { + std::vector list = roiListLoad(); + if (list.empty()) { + QMessageBox::information( + this, "Load ROI", + QString("The ROI list is empty.\n\nPress \"Save ROI\" with a " + "gate ROI active; entries are kept in %1.") + .arg(QString::fromStdString(g_cfg.roiFile))); + return; + } + + QDialog dialog(this); + dialog.setWindowTitle("Load gate ROI"); + dialog.resize(520, 320); + auto *lay = new QVBoxLayout(&dialog); + auto *table = new QTableWidget(static_cast(list.size()), 5, &dialog); + table->setHorizontalHeaderLabels( + QStringList() << "X" << "Y" << "width" << "height" << "last used"); + table->verticalHeader()->setVisible(false); + table->setSelectionBehavior(QAbstractItemView::SelectRows); + table->setSelectionMode(QAbstractItemView::SingleSelection); + table->setEditTriggers(QAbstractItemView::NoEditTriggers); + const QLocale loc; + for (size_t i = 0; i < list.size(); i++) { + const RoiEntry &e = list[i]; + const QString when = QDateTime::fromMSecsSinceEpoch(e.usedMs) + .toString("yyyy-MM-dd HH:mm:ss"); + const QString cells[5] = { + loc.toString(e.x), loc.toString(e.y), loc.toString(e.w), + loc.toString(e.h), when}; + for (int c = 0; c < 5; c++) + table->setItem(static_cast(i), c, + new QTableWidgetItem(cells[c])); + } + table->resizeColumnsToContents(); + table->selectRow(0); // newest first, pre-selected + lay->addWidget(table); + // the same convention as the ADC image and every ROOT plot + auto *hint = new QLabel( + "X/Y are data coordinates: x grows to the right, y grows upwards and " + "(0,0) is the bottom-left pixel.", + &dialog); + hint->setWordWrap(true); + lay->addWidget(hint); + + auto *btns = new QDialogButtonBox(&dialog); + btns->addButton("Apply", QDialogButtonBox::AcceptRole); + auto *delBtn = btns->addButton("Delete", QDialogButtonBox::DestructiveRole); + btns->addButton(QDialogButtonBox::Cancel); + lay->addWidget(btns); + connect(btns, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(btns, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + connect(table, &QTableWidget::itemDoubleClicked, &dialog, + [&dialog](QTableWidgetItem *) { dialog.accept(); }); + + bool deleted = false; + connect(delBtn, &QPushButton::clicked, &dialog, [&]() { + const int r = table->currentRow(); + if (r < 0 || r >= static_cast(list.size())) + return; + const RoiEntry &e = list[r]; + if (!roiListRemove(e.x, e.y, e.w, e.h)) { + QMessageBox::warning(this, "Load ROI", + QString("Could not write %1") + .arg(QString::fromStdString( + g_cfg.roiFile))); + return; + } + deleted = true; + table->removeRow(r); // keep the dialog open for further edits + }); + + if (dialog.exec() != QDialog::Accepted) { + if (deleted) + statusLabel->setText("Status: ROI entry deleted"); + return; + } + const int row = table->currentRow(); + if (row < 0 || row >= static_cast(list.size())) + return; + const RoiEntry e = list[row]; + // refresh the timestamp so it comes back first next time + roiListUpsert(e.x, e.y, e.w, e.h); + applyRoi(e.x, e.y, e.w, e.h, "list"); +} + +void MainWindow::resetHis() { + requestHistoReset(); + statusLabel->setText("Status: histograms reset"); +} + +void MainWindow::resetAxis() { + RootCmd c; + c.type = RootCmdType::ResetAxes; + pushRootCmd(c); + // The ADC view is the master of the zoom, so unzooming only the ROOT + // clones would leave the images (and, through the next drag, the photon + // map) still zoomed: reset the widget viewports as well. + if (m_adcView) + m_adcView->resetView(); // propagates to the gain view + photon map + statusLabel->setText("Status: axes reset"); +} + +// Starts/stops writing the gate-passed ROI frames to HDF5. +void MainWindow::toggleRoiSave(bool on) { + RoiSaver &sv = pipeline_.saver(); + if (!on) { + if (sv.running()) { + sv.requestStop("user"); + statusLabel->setText("Status: finishing ROI save (flushing)..."); + applySaveLock(false); // the run is over, settings are free again + updateFlow(); + updateSaveLabel(); + } + return; + } + if (sv.running()) { + btnSaveStart->setChecked(false); + return; + } + // No gate ROI is fine: roiSnapshot() then reports the full frame, so the + // save writes the whole image instead of refusing to start. + + RoiSaver::StartInfo info; + int x0, y0, x1, y1; + pipeline_.gate().roiSnapshot(x0, y0, x1, y1); + info.x = x0; + info.y = y0; + info.w = x1 - x0; + info.h = y1 - y0; + info.maxFrames = static_cast(saveFrames_->value()); + info.photonMode = saveModePhoton_ && saveModePhoton_->isChecked(); + info.photonEvery = pmapSaveSpin_ ? pmapSaveSpin_->value() : 2000; + info.photonThreshold = gain_flag.load() ? g_pheneThresholdKev + : g_pheneThreshold; + info.photonKev = gain_flag.load(); + gGateChangedDuringSave.store(false); // fresh run, fresh note + pmapSaveReset(); // start the first saved map from scratch + info.dir = saveDirEdit_->text().trimmed().toStdString(); + if (info.dir.empty()) + info.dir = "."; + info.baseName = saveBaseEdit_->text().trimmed().toStdString(); + if (info.baseName.empty()) + info.baseName = "jfroi"; + + // pedestal in the same ROI (gain 0): slice 0 in raw mode, the + // "/pedestal data" dataset in corrected mode + std::shared_ptr peds = PedestalStore::instance().get(); + info.corrected = saveCorrected_->isChecked() && !gNoPede.load(); + if (info.corrected && !peds) { + QMessageBox::information( + this, "ROI save", + "Pedestal-corrected saving needs a pedestal.\n\nRead or acquire " + "one first, or switch the option off to save raw data."); + btnSaveStart->setChecked(false); + return; + } + // the correction itself needs the full-frame pedestal set + info.peds = peds; + // Virtual photon counting thresholds the PEDESTAL-SUBTRACTED ADC, so a map + // without a pedestal would be empty: refuse the save instead of writing + // empty maps (same policy as "Pedestal corrected" above). + if (info.photonMode && (!peds || gNoPede.load())) { + const char *why = peds ? "\"No pede\" is on" : "no pedestal is loaded"; + printf("[save] refusing virtual photon counting: %s, and the photon " + "map thresholds the pedestal-subtracted ADC\n", why); + fflush(stdout); + statusLabel->setText( + QString("Status: virtual photon counting needs a pedestal (%1)") + .arg(QString::fromLatin1(why))); + // a scripted start (--start-save) must not wait on a modal dialog + if (!g_cfg.startSave) { + QMessageBox::warning( + this, "Virtual photon counting", + QString("Virtual photon counting thresholds the " + "pedestal-subtracted ADC, but %1.\n\nLoad or acquire " + "a pedestal and clear \"No pede\" first (or save " + "frames instead).") + .arg(QString::fromLatin1(why))); + } + btnSaveStart->setChecked(false); + updateSaveLabel(); + return; + } + info.useGain = gain_flag.load(); + info.g1off = G1Poffset.load(); + info.g2off = G2Poffset.load(); + if (peds) { + // the ROI is in raw data coordinates (the ADC image is unmapped), and + // g0 is the raw 1024x512 pedestal even in a JFSTRX build + info.pedestal.resize(static_cast(info.w) * info.h, 0.0f); + for (int y = 0; y < info.h; y++) { + const int sy = info.y + y; + for (int x = 0; x < info.w; x++) { + const int sx = info.x + x; + if (sy >= 0 && sy < kRawHeight && sx >= 0 && sx < kRawWidth) + info.pedestal[static_cast(y) * info.w + x] = + peds->g0[static_cast(sy) * kRawWidth + sx]; + } + } + } + + // gate criteria in force at the start + const GateEngine::Counters g0 = pipeline_.gate().snapshot(); + // read from the engine: the operator can change these while running, and + // g_cfg keeps the startup snapshot + info.gate.enabled = pipeline_.gate().enabled(); + info.gate.threshold = pipeline_.gate().threshold(); + info.gate.minPixels = pipeline_.gate().minPixels(); + info.gate.contiguous = pipeline_.gate().contiguous(); + info.gate.roiActive = true; + info.gate.noPede = gNoPede.load(); + info.gate.roiX = x0; + info.gate.roiY = y0; + info.gate.roiW = info.w; + info.gate.roiH = info.h; + info.gateAtEnd = g0; + { + char buf[256]; + snprintf(buf, sizeof(buf), + "{\"enabled\":%s,\"threshold\":%.3f,\"min_pixels\":%llu," + "\"contiguous\":%s,\"storage_cell\":%d,\"no_pede\":%s}", + g_cfg.gateEnabled ? "true" : "false", + static_cast(g_cfg.threshold), + static_cast(g_cfg.minPixels), + g_cfg.contiguous ? "true" : "false", + pipeline_.gate().scFilter(), + gNoPede.load() ? "true" : "false"); + info.gateJson = buf; + } + // exact end-of-run statistics, refreshed by the writer when it finishes + info.statsProvider = [this](GateEngine::Counters &g, RunStats &st, + std::vector &ports) { + g = pipeline_.gate().snapshot(); + st.resync = pipeline_.resyncDrops(); + st.behind = pipeline_.behindDrops(); + st.unpaired = pipeline_.incompleteFrames(); + st.displayFrames = pipeline_.displayFrames(); + st.displayDropped = pipeline_.displayDropped(); + ports.clear(); + for (size_t i = 0; i < pipeline_.portCount(); i++) { + StreamInfo si; + si.port = pipeline_.portList()[i]; + si.state = pipeline_.streamStateName( + pipeline_.streamState(static_cast(i))); + si.frames = pipeline_.portFrames(static_cast(i)); + si.lastFrame = pipeline_.portLastFrame(static_cast(i)); + ports.push_back(si); + } + }; + + std::string err; + if (!sv.start(info, err)) { + QMessageBox::warning(this, "ROI save", + QString("Could not start: %1") + .arg(QString::fromStdString(err))); + btnSaveStart->setChecked(false); + return; + } + // remember the destination, unless it is still the start-up default: that + // one is not a "previous" destination and would only clutter the list + if (info.dir == saveDefaultDir_.toStdString() && + info.baseName == saveDefaultBase_.toStdString()) { + printf("[save] %s/%s is the default destination, not added to the " + "previous-destinations list\n", + info.dir.c_str(), info.baseName.c_str()); + fflush(stdout); + } else { + saveDestListUpsert(info.dir, info.baseName); + } + // keep the CLI defaults in sync so later runs start from the last choice + g_cfg.h5Dir = info.dir; + g_cfg.h5Base = info.baseName; + const RoiSaver::Status st = sv.status(); + statusLabel->setText(QString("Status: saving to %1") + .arg(QString::fromStdString(st.h5Path))); + applySaveLock(true); // nothing that changes the file from here on + updateSaveLabel(); +} + +void MainWindow::browseSaveDir() { + const QString dir = QFileDialog::getExistingDirectory( + this, "Directory for the ROI save", + saveDirEdit_->text().isEmpty() ? QDir::currentPath() + : saveDirEdit_->text()); + if (!dir.isEmpty()) + saveDirEdit_->setText(dir); +} + +// List of destinations used before, most recent first. +void MainWindow::saveDestDialog() { + std::vector list = saveDestListLoad(); + if (list.empty()) { + QMessageBox::information( + this, "Previous destinations", + QString("No destination has been used yet.\n\nThey are recorded " + "when a save starts; the list lives in %1.") + .arg(QString::fromStdString(g_cfg.saveListFile))); + return; + } + + QDialog dialog(this); + dialog.setWindowTitle("Previous save destinations"); + dialog.resize(560, 300); + auto *lay = new QVBoxLayout(&dialog); + auto *table = new QTableWidget(static_cast(list.size()), 3, &dialog); + table->setHorizontalHeaderLabels( + QStringList() << "directory" << "base name" << "last used"); + table->verticalHeader()->setVisible(false); + table->setSelectionBehavior(QAbstractItemView::SelectRows); + table->setSelectionMode(QAbstractItemView::SingleSelection); + table->setEditTriggers(QAbstractItemView::NoEditTriggers); + const QLocale loc; + for (size_t i = 0; i < list.size(); i++) { + const QString when = QDateTime::fromMSecsSinceEpoch(list[i].usedMs) + .toString("yyyy-MM-dd HH:mm:ss"); + table->setItem(static_cast(i), 0, + new QTableWidgetItem( + QString::fromStdString(list[i].dir))); + table->setItem(static_cast(i), 1, + new QTableWidgetItem( + QString::fromStdString(list[i].base))); + table->setItem(static_cast(i), 2, new QTableWidgetItem(when)); + } + table->resizeColumnsToContents(); + table->selectRow(0); + lay->addWidget(table); + + auto *btns = new QDialogButtonBox(&dialog); + btns->addButton("Use", QDialogButtonBox::AcceptRole); + auto *delBtn = btns->addButton("Delete", QDialogButtonBox::DestructiveRole); + btns->addButton(QDialogButtonBox::Cancel); + lay->addWidget(btns); + connect(btns, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(btns, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + connect(table, &QTableWidget::itemDoubleClicked, &dialog, + [&dialog](QTableWidgetItem *) { dialog.accept(); }); + bool deleted = false; + connect(delBtn, &QPushButton::clicked, &dialog, [&]() { + const int r = table->currentRow(); + if (r < 0 || r >= static_cast(list.size())) + return; + saveDestListRemove(list[r].dir, list[r].base); + deleted = true; + table->removeRow(r); + }); + + if (dialog.exec() != QDialog::Accepted) { + if (deleted) + statusLabel->setText("Status: destination removed from the list"); + return; + } + const int row = table->currentRow(); + if (row < 0 || row >= static_cast(list.size())) + return; + saveDirEdit_->setText(QString::fromStdString(list[row].dir)); + saveBaseEdit_->setText(QString::fromStdString(list[row].base)); + statusLabel->setText( + QString("Status: destination set to %1 / %2") + .arg(QString::fromStdString(list[row].dir)) + .arg(QString::fromStdString(list[row].base))); +} + +void MainWindow::updateSaveLabel() { + if (!saveVal_) + return; + const RoiSaver::Status st = pipeline_.saver().status(); + // in photon-map mode the frames counter never moves, so report the maps + const qulonglong n = st.photonMode ? st.pmaps : st.saved; + const QString what = st.photonMode ? (n == 1 ? "map" : "maps") + : (n == 1 ? "frame" : "frames"); + // only mention the queue when something was actually dropped: "0 dropped + // (queue full)" claimed a problem that is not there + const QString dropNote = st.dropped + ? QString(" (writer behind: queue full)") + : QString(" (nothing lost)"); + if (st.running) { + saveVal_->setText(QString("%1\n%2 %3 written, %4 dropped%5") + .arg(QString::fromStdString(st.h5Path)) + .arg(n) + .arg(what) + .arg(st.dropped) + .arg(dropNote)); + } else if (st.h5Path.empty()) { + saveVal_->setText("idle"); + } else { + saveVal_->setText( + QString("%1\n%2 %3 written, %4 dropped - stopped (%5)") + .arg(QString::fromStdString(st.h5Path)) + .arg(n) + .arg(what) + .arg(st.dropped) + .arg(QString::fromStdString(st.stopReason))); + } +} + +void MainWindow::acquirePede() { + if (!pipeline_.running()) { + QMessageBox::information(this, "Acquire pedestal", + "The acquisition pipeline is not running."); + return; + } + if (pipeline_.pedeAcquireActive()) + return; + pipeline_.requestPedeAcquire(g_cfg.pedeFrames); + statusLabel->setText("Status: acquiring pede..."); +} + +void MainWindow::savePede() { + std::string werr; + if (pedeSaveFile(g_cfg.pedeFile, werr)) { + printf("[pede] saved %s\n", g_cfg.pedeFile.c_str()); + statusLabel->setText(QString("Status: pede saved to %1") + .arg(QString::fromStdString(g_cfg.pedeFile))); + } else { + fprintf(stderr, "[pede] save failed: %s\n", werr.c_str()); + pipeline_.gate().setLastError("savePedestal: " + werr); + QMessageBox::warning(this, "Save Pedestal", + QString::fromStdString(werr)); + } +} + +void MainWindow::readPede() { + std::string perr; + if (pedeLoadFile(g_cfg.pedeFile, perr)) { + printf("[pede] read %s\n", g_cfg.pedeFile.c_str()); + pipeline_.gate().setLastError(""); + statusLabel->setText("Status: pede read"); + } else { + fprintf(stderr, "[pede] read failed: %s\n", perr.c_str()); + pipeline_.gate().setLastError("loadPedestal: " + perr); + QMessageBox::warning(this, "Read Pedestal", + QString::fromStdString(perr)); + } +} + +void MainWindow::loadAllPede() { + // The original only printed "not implemented". It is genuinely useful in + // the merged program (it is the only way to load a full G0/G1/G2 set from + // an arbitrary path), so it opens a file chooser now. + const QString path = QFileDialog::getOpenFileName( + this, "Load pedestal file", QDir::currentPath(), + "Pedestal files (*.dat *.txt);;All files (*)"); + if (path.isEmpty()) + return; + std::string perr; + if (pedeLoadFile(path.toStdString(), perr)) { + cout << "[pede] loaded " << path.toStdString() << endl; + pipeline_.gate().setLastError(""); + statusLabel->setText(QString("Status: pede loaded from %1").arg(path)); + } else { + fprintf(stderr, "[pede] load failed: %s\n", perr.c_str()); + pipeline_.gate().setLastError("loadPedestal: " + perr); + QMessageBox::warning(this, "Load Pedestal", + QString::fromStdString(perr)); + } +} + +// The live value range of the main plot: what its colour scale is using right +// now, whatever set it (the defaults, this dialog, or a drag on the ROOT +// canvas). Read from the displayed histogram, so the dialog always opens on +// the values the operator is actually looking at. +// The ADC image (the "main plot") keeps its own colour scale, which the +// operator can also drag on the colour bar: read what the widget is using. +void MainWindow::liveAdcImageRange(int &lo, int &hi) const { + if (m_adcView) { + double a = 0, b = 0; + m_adcView->colorRange(a, b); + lo = static_cast(std::floor(a)); + hi = static_cast(std::ceil(b)); + if (lo != hi) + return; + } + lo = adcmin.load(); + hi = adcmax.load(); +} + +// The ROOT 2D map (canvas A5) keeps its colour scale in min/max. +void MainWindow::liveMapRange(int &lo, int &hi) const { + if (m_photonView) { + double a = 0, b = 0; + m_photonView->colorRange(a, b); + lo = static_cast(a); + hi = static_cast(b); + if (lo != hi) + return; + } + lo = pmmin.load(); + hi = pmmax.load(); +} + +// Same for the shared x window of the spectra. +void MainWindow::liveChipRange(int &lo, int &hi) const { + if (dispChip[0] && dispChip[0]->GetXaxis()) { + TAxis *ax = dispChip[0]->GetXaxis(); + lo = static_cast(std::floor(ax->GetBinLowEdge(ax->GetFirst()))); + hi = static_cast(std::ceil(ax->GetBinUpEdge(ax->GetLast()))); + if (lo != hi) + return; + } + lo = chipxmin.load(); + hi = chipxmax.load(); +} + +// Calibration popup: pick (or change) the gain-map file used for the keV +// spectra. Everything else keeps working in ADU. +void MainWindow::loadGainMap() { + const QString start = + g_cfg.gainMapFile.empty() ? QDir::homePath() + : QString::fromStdString(g_cfg.gainMapFile); + const QString path = QFileDialog::getOpenFileName( + this, "Select gain map (g0,g1,g2,hg0,hg1,hg2 - 1024x512 doubles)", start, + "Gain maps (*.bin *.dat *.raw);;All files (*)"); + if (path.isEmpty()) + return; + std::string err; + if (!gainMapLoadFile(path.toStdString(), err)) { + fprintf(stderr, "[gain] %s\n", err.c_str()); + QMessageBox::warning(this, "Gain map", QString::fromStdString(err)); + return; + } + g_cfg.gainMapFile = path.toStdString(); + const QString base = QFileInfo(path).fileName(); + if (kevChk_) + kevChk_->setChecked(true); // show what was just calibrated + updateFlow(); + statusLabel->setText( + QString("Status: gain map %1 loaded - PH spectra also in keV") + .arg(base)); +} + +void MainWindow::showRangeDialog() { + QDialog dialog(this); + dialog.setWindowTitle("Set Ranges"); + auto *form = new QFormLayout(&dialog); + + int imgLo = 0, imgHi = 0, mapLo = 0, mapHi = 0, chipLo = 0, chipHi = 0; + liveAdcImageRange(imgLo, imgHi); + liveMapRange(mapLo, mapHi); + liveChipRange(chipLo, chipHi); + + auto *adcMinEdit = new QSpinBox(&dialog); + adcMinEdit->setRange(-100000, 100000); + adcMinEdit->setValue(imgLo); + auto *adcMaxEdit = new QSpinBox(&dialog); + adcMaxEdit->setRange(-100000, 100000); + adcMaxEdit->setValue(imgHi); + auto *pmMinEdit = new QSpinBox(&dialog); + pmMinEdit->setRange(-100000, 100000); + pmMinEdit->setValue(mapLo); + auto *pmMaxEdit = new QSpinBox(&dialog); + pmMaxEdit->setRange(-100000, 100000); + pmMaxEdit->setValue(mapHi); + auto *chMinEdit = new QSpinBox(&dialog); + chMinEdit->setRange(-100000, 100000); + chMinEdit->setValue(chipLo); + auto *chMaxEdit = new QSpinBox(&dialog); + chMaxEdit->setRange(-100000, 100000); + chMaxEdit->setValue(chipHi); + + form->addRow("ADC min (ADC image):", adcMinEdit); + form->addRow("ADC max (ADC image):", adcMaxEdit); + form->addRow("P.Map min (ROOT map):", pmMinEdit); + form->addRow("P.Map max (ROOT map):", pmMaxEdit); + form->addRow("PH min (spectra):", chMinEdit); + form->addRow("PH max (spectra):", chMaxEdit); + auto *hint = new QLabel( + "Each field opens on the scale that display is using now; nothing " + "changes unless you edit it.", + &dialog); + hint->setWordWrap(true); + form->addRow(hint); + + auto *buttons = new QDialogButtonBox( + QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); + form->addRow(buttons); + connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + + if (dialog.exec() == QDialog::Accepted) { + setRanges(adcMinEdit->value(), adcMaxEdit->value(), pmMinEdit->value(), + pmMaxEdit->value(), chMinEdit->value(), chMaxEdit->value()); + } +} + +void MainWindow::setRanges(int amin, int amax, int pmin, int pmax, int cmin, + int cmax) { + int imgLo = 0, imgHi = 0, mapLo = 0, mapHi = 0, chipLo = 0, chipHi = 0; + liveAdcImageRange(imgLo, imgHi); + liveMapRange(mapLo, mapHi); + liveChipRange(chipLo, chipHi); + + // the ADC pair is the colour scale of the ADC (main) image - a Qt widget, + // so it is applied straight away - and the P.Map pair the ROOT 2D map, + // which must be changed on the ROOT thread + adcmin = amin; + adcmax = amax; + pmmin = pmin; + pmmax = pmax; + chipxmin = cmin; + chipxmax = cmax; + if ((amin != imgLo || amax != imgHi) && m_adcView) { + m_adcView->setColorRange(amin, amax); + cout << "Ranges set: ADC image [" << amin << "," << amax << "]" + << endl; + } + if (pmin != mapLo || pmax != mapHi) { + fixranges = true; + RootCmd c; + c.type = RootCmdType::SetMapRange; + c.a = pmin; + c.b = pmax; + pushRootCmd(c); + cout << "Ranges set: ROOT map z [" << pmin << "," << pmax << "]" + << endl; + } + if (cmin != chipLo || cmax != chipHi) { + RootCmd c; + c.type = RootCmdType::SetChipRange; + c.a = cmin; + c.b = cmax; + pushRootCmd(c); + cout << "Ranges set: PH x [" << cmin << "," << cmax << "]" << endl; + } + forceRootRedraw(); +} + +void MainWindow::showRateDialog() { + QDialog dialog(this); + dialog.setWindowTitle("Update Rates"); + auto *form = new QFormLayout(&dialog); + + auto *mainRateSpin = new QSpinBox(&dialog); + mainRateSpin->setRange(1, 10000); + mainRateSpin->setValue(HDraw_every.load()); + form->addRow("Main image (frames):", mainRateSpin); + + auto *rootMsSpin = new QSpinBox(&dialog); + rootMsSpin->setRange(50, 60000); + rootMsSpin->setSingleStep(100); + rootMsSpin->setValue(rootUpdateMs.load()); + form->addRow("ROOT update (ms):", rootMsSpin); + + auto *chipSpin = new QSpinBox(&dialog); + chipSpin->setRange(1, 100); + chipSpin->setValue(chipUpdateEvery.load()); + chipSpin->setToolTip( + "Redraw the chip-spectrum canvas (A4) every N-th ROOT redraw. It is\n" + "the most expensive canvas, so a larger N keeps the other canvases\n" + "responsive."); + form->addRow("Chip spectra every N redraws:", chipSpin); + + auto *effLabel = new QLabel( + QString("ROOT redraws every %1 ms when frames arrived") + .arg(rootUpdateMs.load()), + &dialog); + effLabel->setWordWrap(true); + form->addRow(effLabel); + connect(rootMsSpin, QOverload::of(&QSpinBox::valueChanged), + [effLabel](int ms) { + effLabel->setText( + QString("ROOT redraws every %1 ms when frames arrived") + .arg(ms)); + }); + + auto *buttons = new QDialogButtonBox( + QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); + form->addRow(buttons); + connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + + if (dialog.exec() == QDialog::Accepted) { + HDraw_every = mainRateSpin->value(); + rootUpdateMs = rootMsSpin->value(); + chipUpdateEvery = chipSpin->value(); + updateDrawLabel(); + cout << "Rates: main=" << HDraw_every.load() + << " rootMs=" << rootUpdateMs.load() << endl; + } +} + +void MainWindow::updateDrawLabel() { + if (drawLabel) + drawLabel->setText(QString("Main: every %1 frames | ROOT: every %2 ms") + .arg(HDraw_every.load()) + .arg(rootUpdateMs.load())); +} + +// The ADC / gain / photon (and, under JFSTRX, the strixel) windows are separate +// top-level widgets, and the ROOT canvases live in the ROOT thread: closing the +// control window used to leave them on screen, and with them the process alive +// (Qt only quits on the *last* window). Close them all, then leave the loop. +// The display chain and the saved data do not have to agree about the +// pedestal: the display follows the Pede/Raw toggle, while the file is +// corrected only when "Pedestal corrected" is ticked - and the photon maps +// always need the pedestal (their save is refused without it). +void MainWindow::flowStageStates(bool &displayPede, bool &savePede, bool &gateOn, + bool &pmap) const { + displayPede = PedestalStore::instance().loaded() && !gNoPede.load(); + pmap = (gSaveMode.load() == 1); + savePede = pmap || ((saveCorrected_ && saveCorrected_->isChecked()) && + !gNoPede.load()); + gateOn = !gateChk_ || gateChk_->isChecked(); +} + +void MainWindow::closeSideWindows() { + QWidget *wins[] = {m_adcWin, m_gainWin, m_photonWin, +#ifdef JFSTRX + m_adcPanelWin, +#endif + nullptr}; + for (QWidget *w : wins) { + if (!w) + continue; + w->hide(); + w->close(); + } +} + +// While a save is running everything that would change what lands in the file is +// locked: its JSON describes the settings at save start, so a quiet change +// halfway would leave a file nobody can interpret. Display-only controls (and +// the pedestal tracker, which is meant to follow drift during a run) stay live; +// the gate threshold and min-pixels stay live but ask first. +void MainWindow::applySaveLock(bool saving) { + auto lockGroup = [&](QGroupBox *g, std::initializer_list keep) { + if (!g) + return; + for (QObject *c : g->children()) { + auto *w = qobject_cast(c); + if (!w) + continue; + bool kept = false; + for (QWidget *k : keep) + if (k == w) + kept = true; + w->setEnabled(!saving || kept); + } + }; + lockGroup(saveGroup_, {btnSaveStart, saveVal_}); // Start/Stop must stay live + lockGroup(pedeGroup_, {}); // manual pede work changes the saved data + lockGroup(gainGroup_, {kevChk_, gainStateChk_}); // keV PH is display only + lockGroup(scGroup_, {}); // the cell filter changes which frames pass + lockGroup(sigGroup_, {thrSpin_, minPixSpin_}); // the two that ask first + lockGroup(pmapGroup_, {pmapCumulative_, pmapEverySpin_}); // display-only pair + if (btnResetCounters) + btnResetCounters->setEnabled(!saving); + for (QWidget *w : {btnSelectRoi_, btnUnsetRoi_, btnLoadRoi}) + if (w) + w->setEnabled(!saving); // they move the ROI, i.e. the saved crop + // the photon threshold scales the counts that are written in photon-map + // mode, so it is locked there and stays live while frames are saved + if (photonThrSpin_) + photonThrSpin_->setEnabled(!(saving && gSaveMode.load() == 1)); + if (saveCorrected_) // only meaningful for the frames mode + saveCorrected_->setEnabled(!(gSaveMode.load() == 1) && !saving); +} + +bool MainWindow::confirmGateChangeDuringRun(const QString &what) { + if (!pipeline_.saver().status().running) + return true; + // With the gate off (passthrough) the threshold and min-pixels decide + // nothing about the saved frames, so there is nothing to confirm. + if (!pipeline_.gate().enabled()) + return true; + const QMessageBox::StandardButton a = QMessageBox::question( + this, "Gate setting", + QString("Changing settings during a run, are you sure?\n\n%1\n\nThe " + "file being written keeps the gate criterion it started with; a " + "note is added to its JSON sidecar.") + .arg(what), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (a != QMessageBox::Yes) { + printf("[save] gate change refused during the run (%s)\n", + what.toLocal8Bit().constData()); + fflush(stdout); + return false; + } + gGateChangedDuringSave.store(true); + printf("[save] gate setting changed during the run: %s (noted in the JSON)\n", + what.toLocal8Bit().constData()); + fflush(stdout); + return true; +} + +// "Unset ROI": drop the current rectangle, so the gate, the ROI spectra and the +// saved data are back to the full frame. The ROI list on disk is untouched. +void MainWindow::unsetRoi() { + if (!pipeline_.gate().roiSet()) { + statusLabel->setText("Status: no ROI set (already the full frame)"); + return; + } + // release the restriction first, so its own message stays truthful + if (btnGateRoi && btnGateRoi->isChecked()) + btnGateRoi->setChecked(false); + pipeline_.gate().clearRoi(); + pipeline_.gate().resetCounters(); + if (m_adcView) + m_adcView->setOverlayRect(0, 0, 0, 0, false); + if (gateRoiVal_) + gateRoiVal_->setText("full frame"); + printf("[roi] ROI unset: full frame\n"); + fflush(stdout); + updateRoiLabel(); + statusLabel->setText("Status: ROI unset (full frame)"); +} + +void MainWindow::quitApp() { + if (!confirmQuit()) + return; + stop(); + closeSideWindows(); + QApplication::quit(); +} + +// --------------------------------------------------------------------------- +// command line +// --------------------------------------------------------------------------- +void printUsage(const char *prog) { + printf("Usage: %s [rx_port] [gate_threshold] [min_pixels]\n" + " [pede_file] [--pub PORT] [--contig] [--no-gate]\n" + " [--pede-frames N]\n" + "\n" + " rx_host slsReceiver host\n" + " rx_port slsReceiver zmq port, or a '+' list for the\n" + " multi-interface (N UDP interfaces) mode, ordered\n" + " bottom -> top: 30001+30002 = (bot,top)\n" + " (default %d)\n" + " gate_threshold event-finder gate threshold, pede-subtracted ADC\n" + " (default %d)\n" + " min_pixels gate: pass when >= N pixels are above threshold\n" + " (default 5)\n" + " pede_file pedestal file (default %s)\n" + "\n" + " The photon-map threshold (ADU, default %d) is a control in the\n" + " Display panel, not an argument; --raw-map shows raw ADC there\n" + " instead of photon counts.\n" + "\n" + " --pub PORT additionally republish gated frames on PORT\n" + " --contig enable the contiguity proxy (r = N/2+1)\n" + " --gate enable the gate (it is OFF by default)\n" + " --no-gate disable the gate (same as the default)\n" + " --raw-map show raw ADC in the main map instead of\n" + " photon counts (threshold unused then)\n" + " --save-budget N stop after N saved frames, or N saved photon\n" + " maps in virtual photon counting mode\n" + " --apply-gain start with the gain correction on (the photon\n" + " threshold then means keV, default 4 keV)\n" + " --start-save start saving at startup (same as pressing\n" + " the Save button); combine with --save-photon\n" + " --save-photon write the virtual photon counting maps instead\n" + " of the frames (see the Save panel)\n" + " --pmap-every N display cadence of the photon map (default 2000)\n" + " --pmap-save-every N\n" + " saved map cadence, minimum 200 (default 2000)\n" + " --pede-tracking follow the pedestal drift from the frames the\n" + " signal gate drops (see the Pedestal panel)\n" + " --no-pede ignore the pedestal everywhere (same as the\n" + " No pede button); this is the STARTUP DEFAULT,\n" + " so the gate threshold applies to raw 14-bit ADC\n" + " --use-pede start with pedestal subtraction enabled\n" + " --stats print the counter line on stdout every second\n" + " --acquire-pede average --pede-frames frames into a live\n" + " pedestal as soon as acquisition starts\n" + " --pede-frames N frames averaged by \"Acquire Pede\" (default %d)\n" + " --analysis-workers N\n" + " threads for the per-pixel analysis loops\n" + " (default: auto = cores/4, capped at 8; 1 =\n" + " single threaded). Scales across cores with no\n" + " extra memory and bit-identical results.\n" + " --h5-dir DIR directory for ROI-save HDF5 + JSON (default .)\n" + " --h5-base NAME file-name base, file is __f0_0.h5\n" + " (default jfroi)\n" + " --gainmap PATH gain-map file (6 x 1024x512 doubles: g0 g1 g2\n" + " hg0 hg1 hg2). g0 is ADU/keV and is used to\n" + " fill a second set of PH spectra in keV; the ADC\n" + " image, the photon map and the HDF5 writer stay\n" + " in ADU. Can also be loaded from the GUI.\n" + " --roi-file PATH gate-ROI list file (default ./roi_list.dat)\n" + " --sc N pass only frames from storage cell N (0..15,\n" + " bits 8-11 of detSpec3; 'all' = no SC gating).\n" + " Checked before the signal gate, so other cells\n" + " cost no CPU. The GUI has the same selector.\n" + " --gate-roi X Y W H\n" + " start with a gate ROI (data rows: x to the\n" + " right, y upwards, (0,0) = bottom-left; same\n" + " convention as the ROI list and the ROOT plots)\n" + " --rx-hwm N receive high-water mark per interface (default\n" + " %d); smaller = lower latency, more drops\n" + "\n" + "Run chain: slsReceiver -> %s (no external event finder, no\n" + "intermediate zmq hop: the gate and the display share one process)\n", + prog, DEFAULT_RX_PORT, + static_cast(PHOTON_THRESHOLD_DEFAULT), PEDEFNAME, + static_cast(PHOTON_THRESHOLD_DEFAULT), DEFAULT_N_PEDE_FRAMES, + 1000, prog); +} + +bool parseCommandLine(int argc, char **argv, Config &cfg, bool &help, + std::string &error) { + help = false; + std::vector pos; + for (int i = 1; i < argc; i++) { + const std::string a = argv[i]; + if (a == "-h" || a == "--help") { + help = true; + return true; + } else if (a == "--pub") { + if (i + 1 >= argc) { + error = "--pub needs a port"; + return false; + } + cfg.pubPort = std::atoi(argv[++i]); + } else if (a == "--contig") { + cfg.contiguous = true; + } else if (a == "--pmap-every") { + if (i + 1 >= argc) { + error = "--pmap-every needs a count"; + return false; + } + const int v = std::atoi(argv[++i]); + gPmapEvery = v > 0 ? v : 1; + } else if (a == "--pmap-save-every") { + if (i + 1 >= argc) { + error = "--pmap-save-every needs a count (>= 200)"; + return false; + } + const int v = std::atoi(argv[++i]); + gPmapSaveEvery = v < 200 ? 200 : v; + } else if (a == "--save-photon") { + gSaveMode = 1; // same as the Save panel's radio button + } else if (a == "--start-save") { + cfg.startSave = true; // start saving as soon as we are up + } else if (a == "--apply-gain") { + // the gain stage is the only one without a CLI mirror; needed for + // scripted keV runs (and it makes the keV photon threshold testable) + cfg.applyGain = true; + } else if (a == "--save-budget") { + if (i + 1 >= argc) { + error = "--save-budget needs a count"; + return false; + } + cfg.saveBudget = static_cast(std::atoll(argv[++i])); + } else if (a == "--pede-tracking") { + gPedeTracking = true; // same as the Pedestal panel button + } else if (a == "--no-pede") { + // same as the "Pede/Raw toggle" button: no pedestal subtraction + // anywhere, so the gate threshold applies to the raw 14-bit value + gNoPede = true; + } else if (a == "--use-pede") { + // re-enable pedestal subtraction (the startup default is --no-pede) + gNoPede = false; + } else if (a == "--raw-map") { + cfg.rawMap = true; // his3000 shows raw ADC instead of counts + } else if (a == "--no-gate") { + cfg.gateEnabled = false; + } else if (a == "--gate") { + cfg.gateEnabled = true; // the gate is off by default + } else if (a == "--stats") { + cfg.printCounters = true; + } else if (a == "--acquire-pede") { + cfg.acquirePede = true; + } else if (a == "--h5-base") { + if (i + 1 >= argc) { + error = "--h5-base needs a name"; + return false; + } + cfg.h5Base = argv[++i]; + } else if (a == "--h5-dir") { + if (i + 1 >= argc) { + error = "--h5-dir needs a path"; + return false; + } + cfg.h5Dir = argv[++i]; + } else if (a == "--gainmap") { + if (i + 1 >= argc) { + error = "--gainmap needs a file"; + return false; + } + cfg.gainMapFile = argv[++i]; + cfg.gainMapExplicit = true; + } else if (a == "--roi-file") { + if (i + 1 >= argc) { + error = "--roi-file needs a path"; + return false; + } + cfg.roiFile = argv[++i]; + } else if (a == "--sc") { + if (i + 1 >= argc) { + error = "--sc needs a value (0..15, or all)"; + return false; + } + const std::string v = argv[++i]; + if (v == "all" || v == "ALL") { + cfg.scFilter = -1; + } else { + const int n = std::atoi(v.c_str()); + if (n < 0 || n > 15) { + error = "--sc must be 0..15 or all"; + return false; + } + cfg.scFilter = n; + } + } else if (a == "--gate-roi") { + // X Y W H in data rows (row 0 = bottom), same as the ROI list + if (i + 4 >= argc) { + error = "--gate-roi needs X Y W H"; + return false; + } + cfg.gateRoiX = std::atoi(argv[++i]); + cfg.gateRoiY = std::atoi(argv[++i]); + cfg.gateRoiW = std::atoi(argv[++i]); + cfg.gateRoiH = std::atoi(argv[++i]); + if (cfg.gateRoiW < 1 || cfg.gateRoiH < 1) { + error = "--gate-roi needs a positive width and height"; + return false; + } + } else if (a == "--analysis-workers") { + if (i + 1 >= argc) { + error = "--analysis-workers needs a count"; + return false; + } + const int n = std::atoi(argv[++i]); + if (n < 1 || n > 32) { + error = "--analysis-workers must be 1..32"; + return false; + } + cfg.analysisWorkers = n; + } else if (a == "--rx-hwm") { + if (i + 1 >= argc) { + error = "--rx-hwm needs a value"; + return false; + } + const int n = std::atoi(argv[++i]); + if (n < 1) { + error = "--rx-hwm must be >= 1"; + return false; + } + cfg.rxHwm = n; + } else if (a == "--pede-frames") { + if (i + 1 >= argc) { + error = "--pede-frames needs a count"; + return false; + } + const int n = std::atoi(argv[++i]); + if (n < 1) { + error = "--pede-frames must be >= 1"; + return false; + } + cfg.pedeFrames = n; + } else if (a == "--pede") { + if (i + 1 >= argc) { + error = "--pede needs a file"; + return false; + } + cfg.pedeFile = argv[++i]; + cfg.pedeExplicit = true; + } else if (!a.empty() && a[0] == '-' && a.size() > 1 && + !isdigit(static_cast(a[1]))) { + error = "unknown option: " + a; + return false; + } else { + pos.push_back(a); + } + } + + if (pos.empty()) { + error = "need at least "; + return false; + } + cfg.rxHost = pos[0]; + // the photon-map threshold is a Display control (ADU, default 150); the + // raw-ADC map is still available with --raw-map + cfg.dophotonmap = !cfg.rawMap; + cfg.threshold = static_cast(cfg.phene / 2); // default gate threshold + if (pos.size() >= 2) { + // "30001" (single interface) or "30001+30002" (multi-interface, + // ordered BOTTOM -> TOP in the assembled image) + cfg.rxPorts.clear(); + std::string spec = pos[1]; + size_t start = 0; + while (start <= spec.size()) { + const size_t plus = spec.find('+', start); + const std::string tok = + spec.substr(start, (plus == std::string::npos) + ? std::string::npos + : plus - start); + if (tok.empty()) { + error = "bad rx_port list '" + spec + "'"; + return false; + } + const int port = std::atoi(tok.c_str()); + if (port <= 0 || port > 65535) { + error = "bad rx_port '" + tok + "' in '" + spec + "'"; + return false; + } + cfg.rxPorts.push_back(port); + if (plus == std::string::npos) + break; + start = plus + 1; + } + if (cfg.rxPorts.empty() || + cfg.rxPorts.size() > static_cast(JF_MAX_PORTS)) { + error = "rx_port list must have 1.." + + std::to_string(JF_MAX_PORTS) + " entries"; + return false; + } + } + if (pos.size() >= 3) + cfg.threshold = static_cast(std::atof(pos[2].c_str())); + if (pos.size() >= 4) + cfg.minPixels = std::strtoull(pos[3].c_str(), nullptr, 10); + if (pos.size() >= 5) { + cfg.pedeFile = pos[4]; + cfg.pedeExplicit = true; + } + if (cfg.rxHost.empty()) { + error = "rx_host is empty"; + return false; + } + if (cfg.analysisWorkers == 0) { // auto + unsigned hw = std::thread::hardware_concurrency(); + if (hw == 0) + hw = 4; + cfg.analysisWorkers = + static_cast(std::min(8u, std::max(1u, hw / 4))); + } + return true; +} + +// --------------------------------------------------------------------------- +int main(int argc, char *argv[]) { + Config cfg; + bool help = false; + std::string err; + if (!parseCommandLine(argc, argv, cfg, help, err)) { + if (!err.empty()) + fprintf(stderr, "error: %s\n", err.c_str()); + printUsage(argv[0]); + return help ? 0 : 2; + } + if (help) { + printUsage(argv[0]); + return 0; + } + + QApplication app(argc, argv); + g_cfg = cfg; + g_pheneThreshold = PHOTON_THRESHOLD_DEFAULT; // photon counting, ADU + g_pheneThresholdKev = PHOTON_THRESHOLD_KEV_DEFAULT; // same, keV (gain on) + cfg.phene = static_cast(2 * PHOTON_THRESHOLD_DEFAULT); + dophotonmap.store(cfg.dophotonmap); + + printf("=== Online JF display + event finder (single process) ===\n"); + { + std::string ports; + for (size_t k = 0; k < cfg.rxPorts.size(); k++) { + if (k) + ports += "+"; + ports += std::to_string(cfg.rxPorts[k]); + } + printf("input : %s:%s (subscriber)%s\n", cfg.rxHost.c_str(), + ports.c_str(), + cfg.rxPorts.size() > 1 + ? " [multi-interface: slabs stacked bottom -> top]" + : ""); + } + printf("photon map: threshold %.0f ADU, %.1f keV while the gain correction " + "is on (Virtual photon counting panel)%s\n", + static_cast(g_pheneThreshold), + static_cast(PHOTON_THRESHOLD_KEV_DEFAULT), + cfg.dophotonmap ? "" : " [raw ADC map]"); + printf("gate : %s, threshold %.1f, min_pixels %llu%s\n", + cfg.gateEnabled ? "enabled" : "PASSTHROUGH", cfg.threshold, + static_cast(cfg.minPixels), + cfg.contiguous ? ", contiguous" : ""); + printf("pede : %s\n", cfg.pedeFile.c_str()); + printf("threads: analysis workers %d\n", cfg.analysisWorkers); + if (cfg.pubPort > 0) + printf("pub : gated frames republished on port %d\n", cfg.pubPort); + + MainWindow window; + window.show(); + window.start(); + return app.exec(); +} diff --git a/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/online_jf_h5.cpp b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/online_jf_h5.cpp new file mode 100644 index 000000000..40ed52bc9 --- /dev/null +++ b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/online_jf_h5.cpp @@ -0,0 +1,1081 @@ +// SPDX-License-Identifier: LGPL-3.0-or-other +// =========================================================================== +// online_jf_h5.cpp -- decimated gate-passed ROI frames to HDF5 +// +// The file layout mirrors slsReceiverSoftware/src/HDF5DataFile.cpp so that +// existing readers of receiver files work unchanged: +// +// /_f0_0.h5 +// attribute "version" = HDF5 writer version (double) +// /data rank 3, U16LE, dims {nframes, h, w}, +// max {UNLIMITED, h, w}, chunk {1, h, w} +// /frame number rank 1, U64, one value per frame +// /exp length or sub exposure time U32 +// /packets caught U32 +// /detector specific 1 U64 +// /timestamp U64 +// /mod id U16 +// /row U16 +// /column U16 +// /detector specific 2 U16 +// /detector specific 3 U32 +// /detector specific 4 U16 +// /detector type U8 +// /detector header version U8 +// /packets caught bit mask fixed size string (all zero here) +// +// Differences from a receiver file, by design: +// * /data holds the ROI crop of the module, not the whole module; +// * only frames that passed the gate are written, but each keeps its +// ORIGINAL frame number in /frame number (so it is obvious which +// acquisition frame each slice came from); +// * raw mode (default): slice 0 of every dataset is the PEDESTAL in the +// same ROI (frame number 0, marked by "pedestal_in_frame_0"); +// * corrected mode (option): /data holds pedestal-corrected float32 values +// and has NO pedestal slice; the pedestal image lives in the separate +// "/pedestal data" dataset instead (that image is the pedestal at save +// start; each frame is corrected with the pedestal in use when it was +// captured, so live drift tracking is followed like the display, and the +// "/pedestal tracking" series is the authoritative history); +// * /data slices are the raw 16-bit words as received (ADC in bits 0-13, +// gain in bits 14-15), exactly like the receiver; slice 0 is the gain-0 +// pedestal in ADC units; +// * a few extra root attributes describe the ROI and the gate. +// +// A bounded queue decouples the acq thread (which must never block on I/O) +// from the writer thread; frames that do not fit are counted as dropped and +// reported in the JSON sidecar and in the GUI. +// =========================================================================== +#include "online_jf.h" + +#include "H5Cpp.h" +#include "sls/sls_detector_defs.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace { + +// same value the receiver writes into the "version" attribute +const double kHdf5WriterVersion = 7.0; +// queue budget between the acq thread and the writer thread +constexpr size_t kQueueBudgetBytes = 256u * 1024u * 1024u; +constexpr size_t kQueueMaxItems = 4096; +// frames added per dataset extension (metadata operations are expensive) +constexpr hsize_t kExtendBlock = 128; + +bool makeDir(const std::string &dir) { + if (dir.empty() || dir == ".") + return true; + struct stat st; + if (stat(dir.c_str(), &st) == 0) + return S_ISDIR(st.st_mode); + return mkdir(dir.c_str(), 0777) == 0; +} + +std::string timestampName() { + const std::time_t t = std::time(nullptr); + struct tm tmv; + localtime_r(&t, &tmv); + char buf[32]; + strftime(buf, sizeof(buf), "%Y%m%d_%H%M%S", &tmv); + return std::string(buf); +} + +std::string jsonEscape(const std::string &s) { + std::string out; + for (char c : s) { + switch (c) { + case '"': + out += "\\\""; + break; + case '\\': + out += "\\\\"; + break; + case '\n': + out += "\\n"; + break; + case '\r': + out += "\\r"; + break; + case '\t': + out += "\\t"; + break; + default: + if (static_cast(c) < 0x20) { + char b[8]; + snprintf(b, sizeof(b), "\\u%04x", c); + out += b; + } else { + out += c; + } + } + } + return out; +} + +std::string isoNow() { + const std::time_t t = std::time(nullptr); + struct tm tmv; + localtime_r(&t, &tmv); + char buf[32]; + strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", &tmv); + return std::string(buf); +} + +// fsync the directory holding a file so a rename() survives a crash. Best +// effort: a failure just means the rename may not be durable, never fatal. +void syncDirOf(const std::string &path) { + const size_t slash = path.find_last_of('/'); + const std::string dir = + (slash == std::string::npos) ? std::string(".") : path.substr(0, slash); + const int dfd = ::open(dir.c_str(), O_RDONLY | O_DIRECTORY); + if (dfd >= 0) { + ::fsync(dfd); + ::close(dfd); + } +} + +} // namespace + +// --------------------------------------------------------------------------- +// RoiSaver +// --------------------------------------------------------------------------- +struct RoiSaver::Impl { + // ---- configuration of the running save ------------------------------- + StartInfo info; + std::string h5Path, jsonPath; + size_t roiPixels = 0; // w*h + + // ---- queue (acq thread -> writer thread) ----------------------------- + struct Item { + std::vector pixels; // roiPixels: raw words or corrected values + SavedFrameHeader hdr; + }; + // one virtual-photon-counting map on its way to the file + struct PmapItem { + std::vector map; // roiPixels, photons per pixel + uint64_t frames = 0; // analysed frames counted into it + uint64_t firstFrame = 0, firstStamp = 0; // provenance of the window + }; + std::mutex qMtx; + std::condition_variable qCv; + std::deque queue; + std::vector freeList; // recycled buffers + size_t queuedBytes = 0; + + // ---- writer ---------------------------------------------------------- + std::thread thread; + std::atomic stopRequested{false}; + uint64_t pmapPending = 0; // photon maps offered + std::atomic pmapsInFile{0}; // maps actually written + std::deque pmapQueue; + std::atomic running{false}; + std::atomic saved{0}; + std::atomic dropped{0}; + std::atomic firstFrame{0}; + std::atomic lastFrame{0}; + + mutable std::mutex sMtx; + std::string stopReason, lastError; + + void setStop(const std::string &r) { + std::lock_guard lk(sMtx); + if (stopReason.empty()) + stopReason = r; + } + void setError(const std::string &e) { + std::lock_guard lk(sMtx); + lastError = e; + } + + void writerLoop(); +}; + +RoiSaver::RoiSaver() : impl_(new Impl) {} + +RoiSaver::~RoiSaver() { + requestStop("destructor"); + if (impl_ && impl_->thread.joinable()) + impl_->thread.join(); +} + +bool RoiSaver::start(const StartInfo &info, std::string &error) { + if (impl_->running.load()) { + error = "a save is already running"; + return false; + } + // A save that ended on its own - frame budget reached, "user" stop, or an + // I/O error - leaves a JOINABLE std::thread behind, because only the + // destructor and finish() joined it. Assigning a new std::thread to a + // joinable one calls std::terminate, which is what killed the program the + // second time a file was started ("terminate called without an active + // exception"). running == false means the writer loop is done (it is the + // last thing it does), so reap it here and the saver is restartable. + if (impl_->thread.joinable()) + impl_->thread.join(); + if (info.w < 4 || info.h < 4 || info.w > 1024 || info.h > 512) { + error = "invalid ROI size"; + return false; + } + if (!makeDir(info.dir)) { + error = "cannot create directory '" + info.dir + "'"; + return false; + } + // receiver-style name: __f0_0.h5, with the timestamp so + // re-using a base never overwrites an earlier run + std::string base = info.baseName.empty() ? std::string("jfroi") + : info.baseName; + for (char &c : base) { // keep it a file name + if (c == '/' || c == '\t' || c == '\n' || c == '\r') + c = '_'; + } + const std::string stem = info.dir + "/" + base + "_" + timestampName() + + "_f0_0"; + impl_->info = info; + impl_->h5Path = stem + ".h5"; + impl_->jsonPath = stem + ".json"; + impl_->roiPixels = static_cast(info.w) * static_cast(info.h); + impl_->queuedBytes = 0; + { + std::lock_guard lk(impl_->qMtx); + impl_->queue.clear(); + impl_->freeList.clear(); + } + impl_->saved.store(0); + impl_->dropped.store(0); + impl_->firstFrame.store(0); + impl_->lastFrame.store(0); + impl_->stopRequested.store(false); + { + std::lock_guard lk(impl_->sMtx); + impl_->stopReason.clear(); + impl_->lastError.clear(); + } + impl_->running.store(true); + impl_->thread = std::thread(&RoiSaver::Impl::writerLoop, impl_.get()); + return true; +} + +// Virtual photon counting: the map is queued like a frame and written by the +// writer thread. Not reachable yet: gSaveMode is still 0 everywhere, the Save +// panel control and the dataset writer come next (see NOTES.md). +void RoiSaver::offerPhotonMap(const std::vector &map, uint64_t frames, + uint64_t firstFrame, uint64_t firstStamp) { + if (!impl_->running.load() || !impl_->info.photonMode || map.empty()) + return; + Impl::PmapItem it; + it.map = map; + it.frames = frames; + it.firstFrame = firstFrame; + it.firstStamp = firstStamp; + { + std::lock_guard lk(impl_->qMtx); + impl_->pmapPending++; + impl_->pmapQueue.push_back(std::move(it)); + } + impl_->qCv.notify_one(); +} + +void RoiSaver::offer(const short *frame, int frameW, int frameH, + const SavedFrameHeader &hdr) { + // nothing new is queued once a stop (user, budget, error) was requested: + // otherwise a gate faster than the disk keeps the queue non-empty and the + // writer, which drains before it exits, never stops + if (!impl_->running.load() || !impl_->info.w || + impl_->stopRequested.load()) + return; + Impl::Item item; + { + std::lock_guard lk(impl_->qMtx); + if (!impl_->freeList.empty()) { + item = std::move(impl_->freeList.back()); + impl_->freeList.pop_back(); + } else { + item.pixels.resize(impl_->roiPixels); + } + if (impl_->queue.size() >= kQueueMaxItems || + impl_->queuedBytes + impl_->roiPixels * sizeof(float) > + kQueueBudgetBytes) { + // keep the buffer for reuse and count the loss + impl_->dropped.fetch_add(1); + impl_->freeList.push_back(std::move(item)); + return; + } + } + // copy the ROI out of the assembled frame (strided by the frame width), + // either as the raw 16-bit word or pedestal-corrected exactly like the + // display and the gate do it + const int w = impl_->info.w, h = impl_->info.h; + const int cx0 = std::max(0, impl_->info.x); + const int cy0 = std::max(0, impl_->info.y); + const int cx1 = std::min(frameW, impl_->info.x + w); + const int cy1 = std::min(frameH, impl_->info.y + h); + const int span = (cx1 > cx0) ? (cx1 - cx0) : 0; + std::fill(item.pixels.begin(), item.pixels.end(), 0.0f); + // Use the pedestal actually in force for THIS frame, exactly as the display + // and the gate do. The drift tracker (and a manual Read/Load/Acquire Pede) + // swaps in a new immutable PedeSet while a save runs, so the snapshot the + // GUI took at Start Save would silently keep correcting with a stale + // pedestal for the whole night. The shared_ptr keeps this frame's snapshot + // alive for the copy below. With "No pede" on, corrected mode falls back + // to raw words in the float dataset, mirroring the display's raw path. + std::shared_ptr livePeds; + if (impl_->info.corrected) + livePeds = gNoPede.load() ? nullptr : PedestalStore::instance().get(); + const PedeSet *peds = livePeds.get(); + const bool hasAll = peds && peds->hasAll; + const float *g0 = peds ? peds->g0.data() : nullptr; + const float *g1 = hasAll ? peds->g1.data() : nullptr; + const float *g2 = hasAll ? peds->g2.data() : nullptr; + const int G1 = impl_->info.g1off, G2 = impl_->info.g2off; + + for (int sy = cy0; sy < cy1; sy++) { + const short *src = frame + static_cast(sy) * frameW + cx0; + float *dst = item.pixels.data() + + static_cast(sy - impl_->info.y) * w + + (cx0 - impl_->info.x); + if (!peds) { // raw 16-bit word (including the gain bits) + for (int k = 0; k < span; k++) + dst[k] = static_cast( + static_cast(src[k])); + continue; + } + for (int k = 0; k < span; k++) { + const size_t fi = + static_cast(sy) * frameW + cx0 + k; // full-frame index + const int raw = src[k] & 0x3fff; + const int g = (src[k] & 0xc000) ? ((src[k] >> 14) & 0x3) : 0; + // pedestal-subtracted ADC with the gain-bit conversion, exactly as + // the analysis loop computes it ("Apply gain" is a display choice, + // not part of the saved values) + float v; + if (g == 1 && hasAll) + v = (g1[fi] + G1 - raw) * 30.0f; + else if (g == 3 && hasAll) + v = (g2[fi] + G2 - raw) * 340.0f; + else + v = static_cast(raw) - g0[fi]; + dst[k] = v; + } + } + item.hdr = hdr; + { + std::lock_guard lk(impl_->qMtx); + if (impl_->stopRequested.load()) { // stop arrived during the copy + impl_->freeList.push_back(std::move(item)); + return; + } + impl_->queuedBytes += item.pixels.size() * sizeof(float); + impl_->queue.push_back(std::move(item)); + } + impl_->qCv.notify_one(); +} + +void RoiSaver::requestStop(const std::string &reason) { + impl_->setStop(reason); + impl_->stopRequested.store(true); + impl_->qCv.notify_all(); +} + +bool RoiSaver::running() const { return impl_->running.load(); } + +RoiSaver::Status RoiSaver::status() const { + Status s; + s.running = impl_->running.load(); + s.saved = impl_->saved.load(); + s.dropped = impl_->dropped.load(); + s.pmaps = impl_->pmapsInFile.load(); + s.photonMode = impl_->info.photonMode; + s.h5Path = impl_->h5Path; + s.jsonPath = impl_->jsonPath; + { + std::lock_guard lk(impl_->sMtx); + s.stopReason = impl_->stopReason; + s.lastError = impl_->lastError; + } + return s; +} + +void RoiSaver::finish() { + if (impl_->thread.joinable()) + impl_->thread.join(); + impl_->running.store(false); +} + +// --------------------------------------------------------------------------- +// Writer thread: creates the file, writes slice 0 (pedestal) and every queued +// gate-passed frame, then the JSON sidecar. +// --------------------------------------------------------------------------- +void RoiSaver::Impl::writerLoop() { + using namespace H5; + const int w = info.w, h = info.h; + const uint64_t maxFrames = info.maxFrames; + const size_t bitsetBytes = sizeof(sls::defs::bitset_storage); + + std::unique_ptr file; + std::unique_ptr data; + std::vector> para; + std::unique_ptr dataSpace, paraSpace; + std::vector paraNames; + hsize_t framesAllocated = 0; + uint64_t framesWritten = 0; + // in "virtual photon counting" the budget counts maps, not frames + uint64_t pmapsWritten = 0; + auto wallStart = std::chrono::steady_clock::now(); + const std::string startedAt = isoNow(); + + // Rewrite the JSON sidecar in one atomic step: fill .json.part, + // fsync it, rename it over .json. A reader therefore always sees a + // complete JSON (the previous one until the rename), and a crash during the + // write leaves only a stale .part behind. Called periodically and once at + // the end; writer thread only. + auto writeJsonSidecar = [&](bool complete) { + // refresh the live statistics just before reporting them + if (info.statsProvider) { + try { + info.statsProvider(info.gateAtEnd, info.stats, info.streams); + } catch (...) { + } + } + std::string stopR, errR; + { + std::lock_guard lk(sMtx); + stopR = stopReason; + errR = lastError; + } + const double wall = + std::chrono::duration(std::chrono::steady_clock::now() - + wallStart) + .count(); + const std::string partPath = jsonPath + ".part"; + FILE *f = fopen(partPath.c_str(), "w"); + if (!f) + return; + const StartInfo &in = info; + fprintf(f, "{\n"); + fprintf(f, " \"complete\": %s,\n", complete ? "true" : "false"); + fprintf(f, " \"hdf5_format\": \"v110 (SWMR)\",\n"); + fprintf(f, " \"save_mode\": \"%s\",\n", + in.photonMode ? "virtual photon counting" : "frames"); + fprintf(f, " \"photon_map_every\": %d,\n", in.photonEvery); + fprintf(f, " \"photon_threshold\": %.3f,\n", in.photonThreshold); + fprintf(f, " \"photon_threshold_unit\": \"%s\",\n", + in.photonKev ? "keV" : "ADU"); + fprintf(f, " \"file\": \"%s\",\n", jsonEscape(h5Path).c_str()); + fprintf(f, " \"started\": \"%s\",\n", startedAt.c_str()); + fprintf(f, " \"duration_s\": %.3f,\n", wall); + fprintf(f, " \"format\": \"slsReceiver HDF5DataFile (writer " + "version %.1f), ROI crop, gate-passed frames only\",\n", + kHdf5WriterVersion); + fprintf(f, " \"roi\": {\"x\": %d, \"y\": %d, \"w\": %d, " + "\"h\": %d, \"coords\": \"data rows, row 0 at bottom\"},\n", + in.x, in.y, in.w, in.h); + fprintf(f, " \"frame_0_is_pedestal\": %s,\n", + in.corrected ? "false" : "true"); + fprintf(f, " \"pedestal_available\": %s,\n", + in.pedestal.size() == roiPixels ? "true" : "false"); + fprintf(f, " \"pedestal_corrected\": %s,\n", + in.corrected ? "true" : "false"); + fprintf(f, " \"data_dtype\": \"%s\",\n", + in.corrected ? "float32" : "uint16"); + if (in.corrected) + fprintf(f, " \"data_encoding\": \"pedestal corrected, " + "gain normalised (g0: raw-pede, g1: x30, g3: x340), " + "float32; pedestal image in /pedestal data\",\n"); + else + fprintf(f, " \"data_encoding\": \"raw 16-bit word: bits 0-13 " + "= ADC, bits 14-15 = gain; slice 0 = gain-0 pedestal " + "in ADC units\",\n"); + fprintf(f, " \"gate\": {\"enabled\": %s, \"threshold\": %.3f, " + "\"min_pixels\": %llu, \"contiguous\": %s, " + "\"no_pede\": %s},\n", + in.gate.enabled ? "true" : "false", + static_cast(in.gate.threshold), + static_cast(in.gate.minPixels), + in.gate.contiguous ? "true" : "false", + in.gate.noPede ? "true" : "false"); + fprintf(f, " \"gate_roi\": {\"active\": %s, \"x\": %d, \"y\": %d, " + "\"w\": %d, \"h\": %d},\n", + in.gate.roiActive ? "true" : "false", in.gate.roiX, + in.gate.roiY, in.gate.roiW, in.gate.roiH); + if (gGateChangedDuringSave.load()) + fprintf(f, " \"gate_settings_modified_during_run\": true,\n" + " \"gate_settings_note\": \"the signal-gate threshold " + "and/or min-pixels were changed while this file was being " + "written; the \\\"gate\\\" block above is the criterion " + "in force at save start\",\n"); + fprintf(f, " \"stop_reason\": \"%s\",\n", + jsonEscape(stopR).c_str()); + fprintf(f, " \"error\": \"%s\",\n", jsonEscape(errR).c_str()); + fprintf(f, " \"frames\": {\"saved\": %llu, \"dropped_queue_full\": " + "%llu, \"budget\": %llu, \"first_frame_number\": %llu, " + "\"last_frame_number\": %llu},\n", + static_cast( + in.photonMode ? pmapsWritten : saved.load()), + static_cast(dropped.load()), + static_cast(maxFrames), + static_cast(firstFrame.load()), + static_cast(lastFrame.load())); + if (in.photonMode) + fprintf(f, " \"photon_maps\": {\"saved\": %llu, \"every\": %d, " + "\"threshold\": %.3f, \"threshold_unit\": \"%s\", " + "\"cumulative\": false, \"pedestal_active\": %s, " + "\"dataset\": \"/virtual photon counting\"},\n", + static_cast(pmapsWritten), + in.photonEvery, in.photonThreshold, + in.photonKev ? "keV" : "ADU", + in.peds ? "true" : "false"); + const GateEngine::Counters g = in.gateAtEnd; + fprintf(f, " \"gate_statistics\": {\"frames_in\": %llu, " + "\"frames_passed\": %llu, \"frames_dropped\": %llu, " + "\"pixels_above\": %llu, \"drop_fraction\": %.6f, " + "\"rate_in_fps\": %.3f, \"rate_pass_fps\": %.3f, " + "\"rate_in_MBps\": %.3f, \"rate_pass_MBps\": %.3f},\n", + static_cast(g.framesIn), + static_cast(g.framesPass), + static_cast(g.framesDrop), + static_cast(g.pixelsAbove), g.dropFrac, + g.rateIn, g.ratePass, g.rateInMBs, g.ratePassMBs); + fprintf(f, " \"alignment\": {\"resync_drops\": %llu, " + "\"behind_drops\": %llu, \"unpaired\": %llu, " + "\"display_frames\": %llu, \"display_dropped\": %llu},\n", + static_cast(in.stats.resync), + static_cast(in.stats.behind), + static_cast(in.stats.unpaired), + static_cast(in.stats.displayFrames), + static_cast(in.stats.displayDropped)); + fprintf(f, " \"streams\": ["); + for (size_t i = 0; i < in.streams.size(); i++) { + fprintf(f, "%s{\"port\": %d, \"state\": \"%s\", \"frames\": " + "%llu, \"last_frame\": %llu}", + i ? ", " : "", in.streams[i].port, + jsonEscape(in.streams[i].state).c_str(), + static_cast(in.streams[i].frames), + static_cast(in.streams[i].lastFrame)); + } + fprintf(f, "]\n}\n"); + // make the .part content durable, then publish it with one rename + fflush(f); + ::fsync(::fileno(f)); + fclose(f); + if (::rename(partPath.c_str(), jsonPath.c_str()) == 0) + syncDirOf(jsonPath); + }; + + // Periodic clean checkpoint: shrink the over-allocated datasets back to the + // frames actually written, flush the HDF5 to disk and refresh the JSON. + // At every checkpoint boundary the pair on disk is self-consistent, so a + // copy/open then sees exactly the committed frames and a crash loses at + // most one interval. Writer thread only. + auto h5Checkpoint = [&]() { + if (!file) + return; + if (framesWritten > 0 && framesWritten != framesAllocated) { + hsize_t nd[3] = {framesWritten, hsize_t(h), hsize_t(w)}; + data->extend(nd); + for (auto &p : para) { + hsize_t np[1] = {framesWritten}; + p->extend(np); + } + framesAllocated = framesWritten; + dataSpace.reset(new DataSpace(data->getSpace())); + paraSpace.reset(new DataSpace(para[0]->getSpace())); + } + H5Fflush(file->getId(), H5F_SCOPE_GLOBAL); + writeJsonSidecar(false); + }; + + try { + Exception::dontPrint(); + + // ---- file + version attribute (as the receiver writes it) -------- + // Create in the latest (v110) file format: SWMR requires it, and it is + // what makes the file guaranteed readable while it is being written and + // after a writer crash. Readers need HDF5 >= 1.10. + // Built with the C++ constructor so H5File owns the only reference: + // wrapping a raw H5Fcreate id (H5File(fid)) adds a second one, and + // close() then left the file open until the process exited. + { + FileAccPropList fapl; + fapl.setLibverBounds(H5F_LIBVER_LATEST, H5F_LIBVER_LATEST); + file.reset(new H5File(h5Path, H5F_ACC_TRUNC, + FileCreatPropList::DEFAULT, fapl)); + } + { + DataSpace scalar(H5S_SCALAR); + Attribute a = + file->createAttribute("version", PredType::NATIVE_DOUBLE, + scalar); + double v = kHdf5WriterVersion; + a.write(PredType::NATIVE_DOUBLE, &v); + } + // ---- extra self-describing attributes (ignored by receiver readers) + { + DataSpace scalar(H5S_SCALAR); + Attribute ax = file->createAttribute("roi_x", PredType::NATIVE_UINT32, + scalar); + uint32_t v = static_cast(info.x); + ax.write(PredType::NATIVE_UINT32, &v); + Attribute ay = file->createAttribute("roi_y", PredType::NATIVE_UINT32, + scalar); + v = static_cast(info.y); + ay.write(PredType::NATIVE_UINT32, &v); + Attribute aw = file->createAttribute("roi_w", PredType::NATIVE_UINT32, + scalar); + v = static_cast(w); + aw.write(PredType::NATIVE_UINT32, &v); + Attribute ah = file->createAttribute("roi_h", PredType::NATIVE_UINT32, + scalar); + v = static_cast(h); + ah.write(PredType::NATIVE_UINT32, &v); + Attribute ap = file->createAttribute( + "pedestal_in_frame_0", PredType::NATIVE_UINT8, scalar); + uint8_t one = info.corrected ? 0 : 1; + ap.write(PredType::NATIVE_UINT8, &one); + Attribute ac = file->createAttribute( + "pedestal_corrected", PredType::NATIVE_UINT8, scalar); + uint8_t corr = info.corrected ? 1 : 0; + ac.write(PredType::NATIVE_UINT8, &corr); + StrType st(PredType::C_S1, H5T_VARIABLE); + Attribute ag = file->createAttribute( + "gate", st, scalar); + std::string g = info.gateJson; + const char *gp = g.c_str(); + ag.write(st, &gp); + } + + // ---- /data ------------------------------------------------------- + paraNames = {"frame number", + "exp length or sub exposure time", + "packets caught", + "detector specific 1", + "timestamp", + "mod id", + "row", + "column", + "detector specific 2", + "detector specific 3", + "detector specific 4", + "detector type", + "detector header version", + "packets caught bit mask"}; + + // raw mode starts with one slice (the pedestal), corrected mode with + // none: /data then contains exactly the gate-passed frames + const hsize_t n0 = info.corrected ? 0 : 1; + const DataType dataType = info.corrected ? DataType(PredType::IEEE_F32LE) + : DataType(PredType::STD_U16LE); + hsize_t dims[3] = {n0, hsize_t(h), hsize_t(w)}; + hsize_t dimsMax[3] = {H5S_UNLIMITED, hsize_t(h), hsize_t(w)}; + hsize_t chunk[3] = {1, hsize_t(h), hsize_t(w)}; + DataSpace ds(3, dims, dimsMax); + DSetCreatPropList plist; + plist.setChunk(3, chunk); + data.reset(new DataSet( + file->createDataSet("/data", dataType, ds, plist))); + framesAllocated = n0; + + // corrected mode: the pedestal image as its own single-image dataset + std::unique_ptr pedSet; + if (info.corrected) { + hsize_t pd[3] = {1, hsize_t(h), hsize_t(w)}; + hsize_t pdMax[3] = {1, hsize_t(h), hsize_t(w)}; + DataSpace pds3(3, pd, pdMax); + DSetCreatPropList ppl; + ppl.setChunk(3, chunk); + pedSet.reset(new DataSet(file->createDataSet( + "/pedestal data", PredType::IEEE_F32LE, pds3, ppl))); + std::vector pv(roiPixels, 0.0f); + if (info.pedestal.size() == roiPixels) + pv = info.pedestal; + const hsize_t pc[3] = {1, hsize_t(h), hsize_t(w)}; + DataSpace mspace(3, pc); // writes the whole 1-image dataset + pedSet->write(pv.data(), PredType::IEEE_F32LE, mspace); + mspace.close(); + } + + // ---- virtual photon counting: one map per publish ---------------- + // Created before SWMR starts, like every other object. + std::unique_ptr pmapSet, pmapFrameSet, pmapStampSet; + hsize_t pmapWritten = 0; + if (info.photonMode) { + hsize_t md[3] = {0, hsize_t(h), hsize_t(w)}; + hsize_t mdMax[3] = {H5S_UNLIMITED, hsize_t(h), hsize_t(w)}; + DataSpace mds(3, md, mdMax); + DSetCreatPropList mpl; + mpl.setChunk(3, chunk); + pmapSet.reset(new DataSet(file->createDataSet( + "/virtual photon counting", PredType::IEEE_F32LE, mds, mpl))); + const char *names[2] = {"/virtual photon counting frame", + "/virtual photon counting timestamp"}; + std::unique_ptr *sets[2] = {&pmapFrameSet, &pmapStampSet}; + for (int k = 0; k < 2; k++) { + hsize_t d1[1] = {0}; + hsize_t d1Max[1] = {H5S_UNLIMITED}; + hsize_t c1[1] = {1}; + DataSpace s1(1, d1, d1Max); + DSetCreatPropList p1; + p1.setChunk(1, c1); + sets[k]->reset(new DataSet( + file->createDataSet(names[k], PredType::STD_U64LE, s1, p1))); + } + } + auto writePhotonMap = [&](const Impl::PmapItem &it) { + if (!pmapSet) + return; + // the map is accumulated full-frame (1024x512 data layout); the + // saved map is the gate ROI, cropped exactly like /data (same + // x/y/w/h, same data rows) + const int cx0 = std::max(0, info.x); + const int cy0 = std::max(0, info.y); + const int cx1 = std::min(kRawWidth, info.x + w); + const int cy1 = std::min(kRawHeight, info.y + h); + const int cw = (cx1 > cx0) ? (cx1 - cx0) : 0; + std::vector crop(static_cast(w) * h, 0.0f); + for (int y = 0; cy0 + y < cy1; y++) + memcpy(&crop[static_cast(y) * w], + &it.map[static_cast(cy0 + y) * kRawWidth + cx0], + static_cast(cw) * sizeof(float)); + hsize_t nd[3] = {pmapWritten + 1, hsize_t(h), hsize_t(w)}; + pmapSet->extend(nd); + const hsize_t count[3] = {1, hsize_t(h), hsize_t(w)}; + const hsize_t start[3] = {pmapWritten, 0, 0}; + DataSpace ds3 = pmapSet->getSpace(); + ds3.selectHyperslab(H5S_SELECT_SET, count, start); + const hsize_t memDims[2] = {hsize_t(h), hsize_t(w)}; + DataSpace mem(2, memDims); + pmapSet->write(crop.data(), PredType::IEEE_F32LE, mem, ds3); + mem.close(); + ds3.close(); + const uint64_t prov[2] = {it.firstFrame, it.firstStamp}; + std::unique_ptr *sets[2] = {&pmapFrameSet, &pmapStampSet}; + for (int k = 0; k < 2; k++) { + hsize_t fnd[1] = {pmapWritten + 1}; + (*sets[k])->extend(fnd); + hsize_t fc[1] = {1}; + hsize_t fs[1] = {pmapWritten}; + DataSpace fds = (*sets[k])->getSpace(); + fds.selectHyperslab(H5S_SELECT_SET, fc, fs); + const hsize_t fm[1] = {1}; + DataSpace fms(1, fm); + (*sets[k])->write(&prov[k], PredType::STD_U64LE, fms, fds); + fms.close(); + fds.close(); + } + pmapWritten++; + pmapsInFile.store(pmapWritten); // Impl member, no shadowing + printf("[pmap] map %llu written (%llu frames, first frame %llu)\n", + (unsigned long long)pmapWritten, + (unsigned long long)it.frames, + (unsigned long long)it.firstFrame); + fflush(stdout); + }; + + // ---- pedestal tracking: one image per PEDE_TRACK_H5_PERIOD_MS ---- + // Created up front (empty) because SWMR mode cannot create objects after + // it starts. The series is still written only while tracking is on + // (plus one baseline at the start), so a run without tracking carries + // two extra empty datasets. + std::unique_ptr trackSet, trackFrameSet; + { + hsize_t td[3] = {0, hsize_t(h), hsize_t(w)}; + hsize_t tdMax[3] = {H5S_UNLIMITED, hsize_t(h), hsize_t(w)}; + DataSpace tds(3, td, tdMax); + DSetCreatPropList tpl; + tpl.setChunk(3, chunk); + trackSet.reset(new DataSet(file->createDataSet( + "/pedestal tracking", PredType::IEEE_F32LE, tds, tpl))); + hsize_t fd[1] = {0}; + hsize_t fdMax[1] = {H5S_UNLIMITED}; + hsize_t fchunk[1] = {1}; + DataSpace fds(1, fd, fdMax); + DSetCreatPropList fpl; + fpl.setChunk(1, fchunk); + trackFrameSet.reset(new DataSet(file->createDataSet( + "/pedestal tracking frame", PredType::STD_U64LE, fds, fpl))); + } + hsize_t trackWritten = 0; + int64_t lastTrackMs = 0; + auto writeTrackedPedestal = [&](uint64_t frameNo, bool force) { + if (!gPedeTracking.load() && !force) + return; + const int64_t now = + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); + if (trackWritten > 0 && !force && + now - lastTrackMs < PEDE_TRACK_H5_PERIOD_MS) + return; + std::shared_ptr ps = PedestalStore::instance().get(); + if (!ps) + return; + std::vector img(static_cast(h) * w, 0.0f); + for (int ry = 0; ry < h; ry++) + for (int rx = 0; rx < w; rx++) { + const size_t fi = static_cast(info.y + ry) * 1024 + + info.x + rx; + if (fi < ps->g0.size()) + img[static_cast(ry) * w + rx] = ps->g0[fi]; + } + { + hsize_t nd[3] = {trackWritten + 1, hsize_t(h), hsize_t(w)}; + trackSet->extend(nd); + hsize_t count[3] = {1, hsize_t(h), hsize_t(w)}; + hsize_t start[3] = {trackWritten, 0, 0}; + DataSpace ds3 = trackSet->getSpace(); + ds3.selectHyperslab(H5S_SELECT_SET, count, start); + const hsize_t memDims[2] = {hsize_t(h), hsize_t(w)}; + DataSpace mem(2, memDims); + trackSet->write(img.data(), PredType::IEEE_F32LE, mem, ds3); + mem.close(); + ds3.close(); + hsize_t fnd[1] = {trackWritten + 1}; + trackFrameSet->extend(fnd); + hsize_t fcount[1] = {1}; + hsize_t fstart[1] = {trackWritten}; + DataSpace fds = trackFrameSet->getSpace(); + fds.selectHyperslab(H5S_SELECT_SET, fcount, fstart); + const hsize_t fmem[1] = {1}; + DataSpace fms(1, fmem); + trackFrameSet->write(&frameNo, PredType::STD_U64LE, fms, fds); + fms.close(); + fds.close(); + } + trackWritten++; + lastTrackMs = now; + printf("[pedetrack] pedestal image %llu written to %s (frame %llu)\n", + (unsigned long long)trackWritten, h5Path.c_str(), + (unsigned long long)frameNo); + fflush(stdout); + }; + + // ---- parameter datasets (same names/types as the receiver) -------- + hsize_t pdims[1] = {n0}; + hsize_t pdimsMax[1] = {H5S_UNLIMITED}; + hsize_t pchunk[1] = {1}; + DataSpace pds(1, pdims, pdimsMax); + DSetCreatPropList pplist; + pplist.setChunk(1, pchunk); + StrType strType(PredType::C_S1, bitsetBytes); + const DataType types[14] = { + PredType::STD_U64LE, PredType::STD_U32LE, PredType::STD_U32LE, + PredType::STD_U64LE, PredType::STD_U64LE, PredType::STD_U16LE, + PredType::STD_U16LE, PredType::STD_U16LE, PredType::STD_U16LE, + PredType::STD_U32LE, PredType::STD_U16LE, PredType::STD_U8LE, + PredType::STD_U8LE, strType}; + for (int i = 0; i < 14; i++) + para.emplace_back(new DataSet(file->createDataSet( + "/" + paraNames[i], types[i], pds, pplist))); + dataSpace.reset(new DataSpace(data->getSpace())); + paraSpace.reset(new DataSpace(para[0]->getSpace())); + + // ---- switch to SWMR ---------------------------------------------- + // Every object now exists (attributes, /data, /pedestal data, + // /pedestal tracking, the parameter datasets); SWMR forbids creating + // new ones afterwards. From here on the H5Fflush at each checkpoint + // publishes a consistent view to concurrent SWMR readers, and the file + // stays valid even if this process dies without closing it. + if (H5Fstart_swmr_write(file->getId()) < 0) + throw std::runtime_error("H5Fstart_swmr_write failed for " + h5Path); + writeTrackedPedestal(0, /*force=*/true); // baseline at the start + + // ---- write one frame (image + parameters) ------------------------ + auto writeFrame = [&](const SavedFrameHeader &hd, + const void *pixels) { + writeTrackedPedestal(hd.frameNumber, /*force=*/false); + if (framesWritten + 1 > framesAllocated) { + hsize_t nd[3] = {framesAllocated + kExtendBlock, hsize_t(h), + hsize_t(w)}; + data->extend(nd); + for (auto &p : para) { + hsize_t np[1] = {nd[0]}; + p->extend(np); + } + framesAllocated = nd[0]; + dataSpace.reset(new DataSpace(data->getSpace())); + paraSpace.reset(new DataSpace(para[0]->getSpace())); + } + const hsize_t count[3] = {1, hsize_t(h), hsize_t(w)}; + const hsize_t start[3] = {framesWritten, 0, 0}; + const hsize_t memDims[2] = {hsize_t(h), hsize_t(w)}; + dataSpace->selectHyperslab(H5S_SELECT_SET, count, start); + DataSpace mem(2, memDims); + data->write(pixels, dataType, mem, *dataSpace); + mem.close(); + + const hsize_t pcount[1] = {1}; + const hsize_t pstart[1] = {framesWritten}; + paraSpace->selectHyperslab(H5S_SELECT_SET, pcount, pstart); + DataSpace pscalar(H5S_SCALAR); + unsigned char empty[sizeof(sls::defs::bitset_storage)]; + memset(empty, 0, sizeof(empty)); + para[0]->write(&hd.frameNumber, types[0], pscalar, *paraSpace); + para[1]->write(&hd.expLength, types[1], pscalar, *paraSpace); + para[2]->write(&hd.packetNumber, types[2], pscalar, *paraSpace); + para[3]->write(&hd.detSpec1, types[3], pscalar, *paraSpace); + para[4]->write(&hd.timestamp, types[4], pscalar, *paraSpace); + para[5]->write(&hd.modId, types[5], pscalar, *paraSpace); + para[6]->write(&hd.row, types[6], pscalar, *paraSpace); + para[7]->write(&hd.column, types[7], pscalar, *paraSpace); + para[8]->write(&hd.detSpec2, types[8], pscalar, *paraSpace); + para[9]->write(&hd.detSpec3, types[9], pscalar, *paraSpace); + para[10]->write(&hd.detSpec4, types[10], pscalar, *paraSpace); + para[11]->write(&hd.detType, types[11], pscalar, *paraSpace); + para[12]->write(&hd.version, types[12], pscalar, *paraSpace); + para[13]->write(empty, types[13], pscalar, *paraSpace); + pscalar.close(); + framesWritten++; + }; + + // ---- raw mode: slice 0 = the pedestal in the same ROI ------------- + if (!info.corrected) { + std::vector ped(roiPixels, 0); + if (info.pedestal.size() == roiPixels) { + for (size_t i = 0; i < roiPixels; i++) { + const float v = info.pedestal[i]; + ped[i] = (v <= 0.0f) + ? 0 + : (v >= 65535.0f + ? 65535 + : static_cast(v + 0.5f)); + } + } + SavedFrameHeader hd; // frame number 0 marks the pedestal slice + writeFrame(hd, ped.data()); + } + + // ---- drain the queue --------------------------------------------- + const int checkpointMs = info.checkpointMs; + auto steadyMs = []() { + return std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); + }; + int64_t lastCheckpointMs = steadyMs(); + for (;;) { + Impl::Item item; + bool have = false; + { + std::unique_lock lk(qMtx); + qCv.wait_for(lk, std::chrono::milliseconds(100), [&] { + return !queue.empty() || stopRequested.load(); + }); + if (!queue.empty()) { + item = std::move(queue.front()); + queue.pop_front(); + queuedBytes -= item.pixels.size() * sizeof(float); + have = true; + } + } + // periodic clean checkpoint (HDF5 exact+flushed, JSON renamed) + if (checkpointMs > 0) { + const int64_t nowMs = steadyMs(); + if (nowMs - lastCheckpointMs >= checkpointMs) { + h5Checkpoint(); + lastCheckpointMs = nowMs; + } + } + for (;;) { // virtual photon counting maps + Impl::PmapItem it; + { + std::lock_guard lk(qMtx); + if (pmapQueue.empty()) + break; + it = std::move(pmapQueue.front()); + pmapQueue.pop_front(); + } + writePhotonMap(it); + pmapsWritten++; + if (maxFrames > 0 && pmapsWritten >= maxFrames) { + setStop("photon-map budget reached"); + stopRequested.store(true); + break; + } + } + if (stopRequested.load()) + break; + if (!have) { + // idle tick: the tracked pedestal is written every + // PEDE_TRACK_H5_PERIOD_MS of wall clock while the save runs, + // even if no frame passes the gate + writeTrackedPedestal(lastFrame.load(), /*force=*/false); + } + if (have) { + if (info.corrected) { + writeFrame(item.hdr, item.pixels.data()); + } else { + static std::vector raw; + raw.resize(item.pixels.size()); + for (size_t k = 0; k < item.pixels.size(); k++) { + const float v = item.pixels[k]; + raw[k] = (v <= 0.0f) + ? 0 + : (v >= 65535.0f + ? 65535 + : static_cast(v + 0.5f)); + } + writeFrame(item.hdr, raw.data()); + } + if (firstFrame.load() == 0) + firstFrame.store(item.hdr.frameNumber); + lastFrame.store(item.hdr.frameNumber); + const uint64_t n = saved.fetch_add(1) + 1; + { + std::lock_guard lk(qMtx); + freeList.push_back(std::move(item)); + } + if (maxFrames > 0 && n >= maxFrames) { + setStop("frame budget reached"); + stopRequested.store(true); + break; // frames still queued are beyond the budget + } + continue; + } + if (stopRequested.load()) + break; + } + + // ---- shrink to what we actually wrote, then close ----------------- + { + const hsize_t finalFrames = framesWritten ? framesWritten : 1; + hsize_t nd[3] = {finalFrames, hsize_t(h), hsize_t(w)}; + data->extend(nd); + for (auto &p : para) { + hsize_t np[1] = {finalFrames}; + p->extend(np); + } + } + dataSpace.reset(); + paraSpace.reset(); + data.reset(); + para.clear(); + pedSet.reset(); + trackSet.reset(); + trackFrameSet.reset(); + file->close(); + file.reset(); + } catch (const H5::Exception &e) { + setError(std::string("HDF5: ") + e.getDetailMsg()); + setStop("error"); + stopRequested.store(true); + } catch (const std::exception &e) { + setError(std::string("save failed: ") + e.what()); + setStop("error"); + stopRequested.store(true); + } + + // ---- final JSON sidecar (atomic .json.part -> rename) -------- + // Periodic snapshots have already refreshed it during the run. + writeJsonSidecar(true); + running.store(false); +} diff --git a/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/online_jf_root.cpp b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/online_jf_root.cpp new file mode 100644 index 000000000..50c5d5341 --- /dev/null +++ b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/online_jf_root.cpp @@ -0,0 +1,1393 @@ +// SPDX-License-Identifier: LGPL-3.0-or-other +// =========================================================================== +// online_jf_root.cpp -- ROOT canvases, the ROOT redraw thread and the +// command queue. +// +// ROOT is not thread safe: canvas painting and gSystem->ProcessEvents() may +// only run on ONE thread. That thread is the only one allowed to touch the +// display clones; the source histograms are filled by the analysis thread. +// +// Split out of onlinedisp_zmq.cpp (Plot1D/2DHistos, historeset, axisreset, +// palettes, the ROOT half of MainWindow::rootDrawLoop). +// =========================================================================== +#include "online_jf.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +// --------------------------------------------------------------------------- +// ROOT objects +// --------------------------------------------------------------------------- +TApplication *rootapp = nullptr; +TCanvas *A2 = nullptr, *A3 = nullptr, *A4 = nullptr, *A5 = nullptr, + *A6 = nullptr, *A7 = nullptr; +TPad *p1 = nullptr, *p2 = nullptr, *p3 = nullptr, *p4 = nullptr, *p5 = nullptr, + *p6 = nullptr; +TH1I **hchip = nullptr; +TH1I *hroi = nullptr; // one spectrum over the gate-ROI pixels only +// the same spectra in keV (canvases A8/A9); filled only when a gain map is +// loaded, and drawn only then too +TH1I **hchipKev = nullptr; +TH1I *hroiKev = nullptr; +TH1I *hproj = nullptr, *hchcum = nullptr; +TH2F *his1000 = nullptr, *his1001 = nullptr, *his1002 = nullptr; +TH2F *his1060 = nullptr, *his1061 = nullptr, *his1062 = nullptr; +TH2F *his2000 = nullptr, *his3000 = nullptr, *his4500 = nullptr, + *hisLvsR = nullptr; +TBox *box = nullptr; + +// display clones painted by the ROOT thread +TH2F *disp3000 = nullptr; +TH2F *disp4500 = nullptr; +TH2F *dispLvsR = nullptr; +TH1I *dispChip[8] = {nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr}; +TH1I *dispRoi = nullptr; +TH1I *dispChipKev[8] = {nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr}; +TH1I *dispRoiKev = nullptr; +// photon-map helpers (defined further down, ROOT thread only) +void plotPhotonMap(); +void zoomPhotonMap(double x0, double x1, double y0, double y1); +void setSpectrumTitles(bool kev); + +#ifdef JFSTRX +TH2F *disp1000 = nullptr, *disp1001 = nullptr, *disp1002 = nullptr; +TH2F *disp1060 = nullptr, *disp1061 = nullptr, *disp1062 = nullptr; +#endif + +namespace { +// TEMP instrumentation: where the ROOT thread spends its time +double g_rtCopy = 0, g_rtPlot2 = 0, g_rtPlot1 = 0, g_rtPlotRoi = 0, + g_rtPlotKev = 0, g_rtPlotRoiKev = 0, g_rtPe = 0, g_rtCmd = 0; +uint64_t g_rtRedraws = 0, g_rtLoops = 0; +uint64_t g_rtNPlot1 = 0, g_rtNPlotRoi = 0, g_rtNPlotKev = 0, + g_rtNPlotRoiKev = 0; +bool g_rootTiming() { return getenv("JF_TIMING") != nullptr; } + +std::mutex gCmdMtx; +std::condition_variable gCmdCv; // wakes the ROOT thread for a prompt command +std::vector gCmds; + +std::thread *gRootThread = nullptr; +std::atomic gRootRun{false}; + +// copies bin contents + entry count from a source accumulator to its display +// clone (same geometry). ROOT-thread only. +void copyBins(TH1 *dst, TH1 *src) { + if (!dst || !src) + return; + const int bx = dst->GetNbinsX(); + const int by = dst->GetNbinsY(); + dst->Reset(); + if (by <= 1) { + for (int b = 1; b <= bx; b++) + dst->SetBinContent(b, src->GetBinContent(b)); + } else { + for (int y = 1; y <= by; y++) + for (int x = 1; x <= bx; x++) + dst->SetBinContent(x, y, src->GetBinContent(x, y)); + } + dst->SetEntries(src->GetEntries()); +} + +void rootLoop(); +} // namespace + +// --------------------------------------------------------------------------- +// Command queue (any thread -> ROOT thread) +// --------------------------------------------------------------------------- +void pushRootCmd(const RootCmd &cmd) { + { + std::lock_guard lock(gCmdMtx); + gCmds.push_back(cmd); + } + gCmdCv.notify_one(); // service it now, not at the next scheduled redraw +} + +void pushRootCmdCoalesced(const RootCmd &cmd) { + { + std::lock_guard lock(gCmdMtx); + for (auto &c : gCmds) { + if (c.type == cmd.type) { + c = cmd; // keep only the newest viewport + gCmdCv.notify_one(); + return; + } + } + gCmds.push_back(cmd); + } + gCmdCv.notify_one(); +} + +void requestHistoReset() { + // Wakes the ROOT thread. execRootCmd() applies the reset if the analysis + // thread is between frames, otherwise it leaves the flag for the analysis + // thread to apply at the top of its next frame. The ROOT thread must never + // BLOCK on gHistoMtx: under a heavy stream it can starve, which is what + // made the reset appear to take tens of seconds. + RootCmd c; + c.type = RootCmdType::ResetHis; + pushRootCmd(c); +} + +void forceRootRedraw() { + gRedrawRequest.store(true); + gCmdCv.notify_one(); +} + +void startRootThread() { + if (gRootThread) + return; + gRootRun.store(true); + gRootThread = new std::thread(rootLoop); +} + +void stopRootThread() { + gRootRun.store(false); + // only the first call prints (a signal stop and the normal shutdown can + // both come through here; the second one has nothing left to join) + if (gRootThread && g_rootTiming()) { + printf("[root] loops=%llu redraws=%llu | per-redraw us: copy=%.0f " + "plot2d=%.0f plot1d=%.0f plotroi=%.0f plotkev=%.0f " + "plotrokev=%.0f | per-loop us: cmd=%.0f pe=%.0f\n", + (unsigned long long)g_rtLoops, + (unsigned long long)g_rtRedraws, + g_rtRedraws ? g_rtCopy / g_rtRedraws : 0.0, + g_rtRedraws ? g_rtPlot2 / g_rtRedraws : 0.0, + g_rtRedraws ? g_rtPlot1 / g_rtRedraws : 0.0, + g_rtRedraws ? g_rtPlotRoi / g_rtRedraws : 0.0, + g_rtRedraws ? g_rtPlotKev / g_rtRedraws : 0.0, + g_rtRedraws ? g_rtPlotRoiKev / g_rtRedraws : 0.0, + g_rtLoops ? g_rtCmd / g_rtLoops : 0.0, + g_rtLoops ? g_rtPe / g_rtLoops : 0.0); + printf("[root] spectrum paints: chipsADU=%llu roiADU=%llu " + "chipsKeV=%llu roiKeV=%llu | per paint us: %.0f %.0f %.0f %.0f\n", + (unsigned long long)g_rtNPlot1, (unsigned long long)g_rtNPlotRoi, + (unsigned long long)g_rtNPlotKev, + (unsigned long long)g_rtNPlotRoiKev, + g_rtNPlot1 ? g_rtPlot1 / g_rtNPlot1 : 0.0, + g_rtNPlotRoi ? g_rtPlotRoi / g_rtNPlotRoi : 0.0, + g_rtNPlotKev ? g_rtPlotKev / g_rtNPlotKev : 0.0, + g_rtNPlotRoiKev ? g_rtPlotRoiKev / g_rtNPlotRoiKev : 0.0); + // spectrum totals (the ROI ones must be a subset of the chip ones); + // read under the histogram lock, the analysis thread may still be + // alive when this thread is stopped + { + std::lock_guard lock(gHistoMtx); + char chipTxt[8 * 20] = ""; + double chipTot = 0; + for (int k = 0; k < 8; k++) { + char one[24]; + const double c = hchip && hchip[k] ? hchip[k]->GetEntries() : 0; + chipTot += c; + snprintf(one, sizeof(one), "%.0f ", c); + strncat(chipTxt, one, sizeof(chipTxt) - strlen(chipTxt) - 1); + } + const double roiTot = hroi ? hroi->GetEntries() : 0; + double kevTot = 0; + if (hchipKev) { + for (int k = 0; k < 8; k++) + if (hchipKev[k]) + kevTot += hchipKev[k]->GetEntries(); + } + const double roiKevTot = hroiKev ? hroiKev->GetEntries() : 0; + printf("[root] spectra entries: hchip=[%s] total=%.0f | hroi=%.0f " + "(roi/chip=%.4f)\n", + chipTxt, chipTot, roiTot, + chipTot > 0 ? roiTot / chipTot : 0.0); + printf("[root] keV entries: hchipKev=%.0f hroiKev=%.0f " + "(kev/adu=%.4f, gainmap=%s)\n", + kevTot, roiKevTot, + chipTot > 0 ? kevTot / chipTot : 0.0, + gainMapLoaded() ? "loaded" + : ((kevSpectra.load() && gain_flag.load()) + ? "nominal" + : "none")); + // the conversion is right when keV_mean == ADU_mean / (ADU per + // keV): with a g0 of 40 that ratio is exactly 40 + if (hroiKev && roiKevTot > 0) { + const TAxis *kx = hroiKev->GetXaxis(); + const int kmax = hroiKev->GetMaximumBin(); + printf("[root] hroi_keV axis %.4f..%.4f nbins %d window " + "bins %d..%d | maxbin %d (%.4f) content %.0f | " + "mean %.4f | under %0.f over %.0f\n", + kx->GetXmin(), kx->GetXmax(), kx->GetNbins(), + kx->GetFirst(), kx->GetLast(), kmax, + kx->GetBinCenter(kmax), hroiKev->GetBinContent(kmax), + hroiKev->GetMean(), hroiKev->GetBinContent(0), + hroiKev->GetBinContent(kx->GetNbins() + 1)); + } + if (hroiKev && roiTot > 0 && roiKevTot > 0) { + const double aduMean = hroi->GetMean(); + const double kevMean = hroiKev->GetMean(); + printf("[root] mean of the ROI spectra: ADU %.4f -> keV %.4f " + "(ADU/keV = %.4f)\n", + aduMean, kevMean, + kevMean != 0 ? aduMean / kevMean : 0.0); + } + // the axis windows actually in force + auto lo = [](TAxis *a) { return a->GetBinLowEdge(a->GetFirst()); }; + auto hi = [](TAxis *a) { return a->GetBinUpEdge(a->GetLast()); }; + if (dispChip[0]) + printf("[root] ranges: PH x %.0f..%.0f", + lo(dispChip[0]->GetXaxis()), hi(dispChip[0]->GetXaxis())); + if (dispRoi) + printf(" | ROI x %.0f..%.0f", lo(dispRoi->GetXaxis()), + hi(dispRoi->GetXaxis())); + printf(" | PH set %s", + (kevSpectra.load() && gain_flag.load()) ? "keV" : "ADU"); + if (disp3000) // the colour scale lives in min/max, not in a range + printf(" | main z %.0f..%.0f (%s)", disp3000->GetMinimum(), + disp3000->GetMaximum(), + dophotonmap.load() ? "photon counts" : "raw ADC"); + printf("\n"); + } + fflush(stdout); + } + if (gRootThread) { + if (gRootThread->joinable()) + gRootThread->join(); + delete gRootThread; + gRootThread = nullptr; + } +} + +// --------------------------------------------------------------------------- +// Command execution (ROOT thread only) +// --------------------------------------------------------------------------- +namespace { + +void execRootCmd(const RootCmd &cmd) { + switch (cmd.type) { + case RootCmdType::ResetHis: + gResetHisPending.store(true); + gRedrawRequest.store(true); + // If the analysis thread is between frames it does not hold gHistoMtx, + // so apply the reset here (makes a reset while idle/paused immediate). + // Otherwise leave the flag; the analysis thread performs it at the top + // of its next frame, where the lock is already held. try_lock never + // blocks, so the ROOT thread can never starve on gHistoMtx. + if (gHistoMtx.try_lock()) { + if (gResetHisPending.exchange(false)) + historesetSourcesLocked(); + gHistoMtx.unlock(); + } + break; + case RootCmdType::ResetAxes: + axisreset(); + break; + case RootCmdType::PaletteColor: + loadRootPalette(Palette::Color); + Plot2DHistos(); + Plot1DHistos(); + break; + case RootCmdType::PaletteBW1: + loadRootPalette(Palette::BW1); + Plot2DHistos(); + Plot1DHistos(); + break; + case RootCmdType::PaletteBW2: + loadRootPalette(Palette::BW2); + Plot2DHistos(); + Plot1DHistos(); + break; + case RootCmdType::Zoom: + if (disp3000 && A5) { + // a drag produces a stream of viewports; only repaint the last one + // (the ranges are applied below, so nothing is lost by skipping) + { + bool newer = false; + std::lock_guard lock(gCmdMtx); + for (const RootCmd &c : gCmds) { + if (c.type == RootCmdType::Zoom) { + newer = true; + break; + } + } + if (newer) + break; + } + zoomPhotonMap(cmd.a, cmd.c, cmd.b, cmd.d); + } + break; + case RootCmdType::ShowPlots: { + // TCanvas has no Hide() and unmapping its widget does not hide the + // TRootCanvas frame, so the frames are parked off-screen instead and + // their geometry is restored when they are shown again. The canvases, + // their histograms and their contents stay alive either way. + static std::map> parked; + TIter next(gROOT->GetListOfCanvases()); + while (TObject *o = next()) { + TCanvas *c = static_cast(o); + if (!c || !c->GetCanvasImp()) + continue; + if (cmd.a > 0.5) { + auto it = parked.find(c); + if (it != parked.end()) { + c->GetCanvasImp()->SetWindowPosition(it->second.first, + it->second.second); + parked.erase(it); + } + c->Modified(); + c->Update(); + } else { + Int_t x = 0, y = 0; + UInt_t w = 0, h = 0; + c->GetCanvasImp()->GetWindowGeometry(x, y, w, h); + if (x > -1000) // only remember a real on-screen position + parked[c] = std::make_pair(x, y); + c->GetCanvasImp()->SetWindowPosition(-4000, -4000); + } + } + break; + } + case RootCmdType::SetMapRange: + // colour scale of the ROOT 2D map: applied once, on the ROOT thread, + // so a scale the operator drags on the canvas is NOT reset by the + // periodic redraw (TH1::Reset() does not touch it either) + if (disp3000) { + disp3000->GetZaxis()->SetRangeUser(cmd.a, cmd.b); + Plot2DHistos(); + } + break; + case RootCmdType::SetChipRange: + for (int ih = 0; ih < 8; ih++) { + if (dispChip[ih]) + dispChip[ih]->GetXaxis()->SetRangeUser(cmd.a, cmd.b); + // the ROI and keV spectra share A4's x window; the keV axis is + // the ADU axis / 40, so the same bin indices are selected + if (dispRoi) + dispRoi->GetXaxis()->SetRangeUser(cmd.a, cmd.b); + if (dispRoiKev) + dispRoiKev->GetXaxis()->SetRangeUser(cmd.a / KEV_NOMINAL_ADU, + cmd.b / KEV_NOMINAL_ADU); + for (int ik = 0; ik < 8; ik++) { + if (dispChipKev[ik]) + dispChipKev[ik]->GetXaxis()->SetRangeUser( + cmd.a / KEV_NOMINAL_ADU, cmd.b / KEV_NOMINAL_ADU); + } + } + Plot1DHistos(); + break; + default: + break; + } +} + +// --------------------------------------------------------------------------- +// ROOT redraw loop: the single owner of ROOT graphics +// --------------------------------------------------------------------------- +void rootLoop() { + auto next = steady_clock::now(); + while (gRootRun.load()) { + try { // an escaping exception would abort the process + // Wait for the scheduled redraw, but wake immediately for a queued + // command or a forced redraw: with ROOT update set to tens of seconds + // a button must not wait for the next tick. + { + std::unique_lock lk(gCmdMtx); + gCmdCv.wait_until(lk, next, [&] { + return !gCmds.empty() || gRedrawRequest.load() || + !gRootRun.load(); + }); + } + if (!gRootRun.load()) + break; + + g_rtLoops++; + const auto tLoop0 = steady_clock::now(); + // 1) execute queued user commands (posted by the Qt thread) + for (;;) { + RootCmd cmd; + { + std::lock_guard lock(gCmdMtx); + if (gCmds.empty()) + break; + cmd = gCmds.front(); + gCmds.erase(gCmds.begin()); + } + execRootCmd(cmd); + } + + g_rtCmd += duration(steady_clock::now() - tLoop0) + .count(); + // 2) ROOT GUI event processing lives HERE (single owner thread), + // keeping the canvases interactive (zoom etc.) + { + const auto t0 = steady_clock::now(); + gSystem->ProcessEvents(); + g_rtPe += duration(steady_clock::now() - t0) + .count(); + } + + // 3) periodic plot + int ms = rootUpdateMs.load(); + if (ms < 50) + ms = 50; + next = steady_clock::now() + std::chrono::milliseconds(ms); + + const bool forced = gRedrawRequest.exchange(false); + if (gFramesForRoot.exchange(0) == 0 && !forced) + continue; + + // snapshot the source accumulators into the display clones that the + // canvases paint (never paint the objects being filled) + { + const auto t0 = steady_clock::now(); + copyBins(disp3000, his3000); + copyBins(disp4500, his4500); + copyBins(dispLvsR, hisLvsR); + for (int ih = 0; ih < 8; ih++) + copyBins(dispChip[ih], hchip ? hchip[ih] : nullptr); + copyBins(dispRoi, hroi); + for (int ih = 0; ih < 8; ih++) + copyBins(dispChipKev[ih], hchipKev ? hchipKev[ih] : nullptr); + copyBins(dispRoiKev, hroiKev); + g_rtCopy += duration(steady_clock::now() - t0) + .count(); + } +#ifdef JFSTRX + copyBins(disp1000, his1000); + copyBins(disp1001, his1001); + copyBins(disp1002, his1002); + copyBins(disp1060, his1060); + copyBins(disp1061, his1061); + copyBins(disp1062, his1062); +#endif + + { + const auto t0 = steady_clock::now(); + Plot2DHistos(); + g_rtPlot2 += duration(steady_clock::now() - t0) + .count(); + } + serviceRootEvents(); // the 2D canvases are painted: service input now + + // The spectrum canvases are the most expensive ones, so they run at a + // slower cadence than the images, one per tick in round-robin: a + // single redraw never pays for both. Only ONE set is on screen - the + // ADU pair or the keV pair, selected by the "keV PH" checkbox. + int every = chipUpdateEvery.load(); + if (every < 1) + every = 1; + static int chipTick = 0; + static int rot = 0; + if ((chipTick++ % every) == 0) { + // the keV view follows the gain stage; no map -> nominal 40 ADU/keV + const bool kev = kevSpectra.load() && gain_flag.load(); + setSpectrumTitles(kev); + const auto t0 = steady_clock::now(); + if (kev) { + if ((rot % 2) == 0) + Plot1DHistosKev(); + else + PlotRoiHistosKev(); + } else { + if ((rot % 2) == 0) + Plot1DHistos(); + else + PlotRoiHistos(); + } + const double us = + duration(steady_clock::now() - t0).count(); + if (kev && (rot % 2) == 0) { + g_rtPlotKev += us; + g_rtNPlotKev++; + } else if (kev) { + g_rtPlotRoiKev += us; + g_rtNPlotRoiKev++; + } else if ((rot % 2) == 0) { + g_rtPlot1 += us; + g_rtNPlot1++; + } else { + g_rtPlotRoi += us; + g_rtNPlotRoi++; + } + rot++; + serviceRootEvents(); + } + g_rtRedraws++; + // keep the TvsB/LvsR scatter windows fresh: the analysis thread + // resets them before the first fill of the next period (the photon + // map stays cumulative) + gSoftResetPending.store(true); + } catch (const std::exception &e) { + fprintf(stderr, "[root] exception: %s (ROOT drawing stopped)\n", + e.what()); + fflush(stderr); + break; + } catch (...) { + fprintf(stderr, "[root] unknown exception (ROOT drawing stopped)\n"); + fflush(stderr); + break; + } + } +} + +} // namespace + +// --------------------------------------------------------------------------- +// Plotting helpers (ROOT thread only, except where noted) +// --------------------------------------------------------------------------- +void serviceRootEvents() { + if (gSystem) + gSystem->ProcessEvents(); +} + +// --------------------------------------------------------------------------- +// Palettes: a single definition per palette feeds both ROOT (gradient colour +// table) and the fast image widgets (256-entry RGB lookup table), so the +// photon map and the ADC image always look the same. +// --------------------------------------------------------------------------- +namespace { + +struct PaletteDef { + // piecewise-linear colour stops (used when nstops > 0) + int nstops = 0; + double stops[24] = {0, 0, 0, 0, 0}; + double r[24] = {0, 0, 0, 0, 0}; + double g[24] = {0, 0, 0, 0, 0}; + double b[24] = {0, 0, 0, 0, 0}; + // gray ramp exponent (used when nstops == 0) + double gamma = 1.0; +}; + +const PaletteDef &paletteDef(Palette p) { + // "False" colour table, as used by the original display + static const PaletteDef color = [] { + PaletteDef d; + d.nstops = 5; + d.stops[0] = 0.00; d.stops[1] = 0.34; d.stops[2] = 0.61; + d.stops[3] = 0.84; d.stops[4] = 1.00; + d.r[0] = 0.00; d.r[1] = 0.00; d.r[2] = 0.87; d.r[3] = 1.00; + d.r[4] = 0.51; + d.g[0] = 0.00; d.g[1] = 0.81; d.g[2] = 1.00; d.g[3] = 0.20; + d.g[4] = 0.00; + d.b[0] = 0.51; d.b[1] = 1.00; d.b[2] = 0.12; d.b[3] = 0.00; + d.b[4] = 0.00; + return d; + }(); + static const PaletteDef bw1 = [] { + PaletteDef d; + d.gamma = 1.1111; // same exponent the original 'b' button used + return d; + }(); + static const PaletteDef bw2 = [] { + PaletteDef d; + d.gamma = 0.9; // same exponent the original 'B' button used + return d; + }(); + static const PaletteDef turbo = [] { + PaletteDef d; + d.nstops = 15; + d.stops[0] = 0.0000; + d.stops[1] = 0.0714; + d.stops[2] = 0.1429; + d.stops[3] = 0.2143; + d.stops[4] = 0.2857; + d.stops[5] = 0.3571; + d.stops[6] = 0.4286; + d.stops[7] = 0.5000; + d.stops[8] = 0.5714; + d.stops[9] = 0.6429; + d.stops[10] = 0.7143; + d.stops[11] = 0.7857; + d.stops[12] = 0.8571; + d.stops[13] = 0.9286; + d.stops[14] = 1.0000; + d.r[0] = 0.1882; d.r[1] = 0.2549; d.r[2] = 0.2745; d.r[3] = 0.2235; + d.r[4] = 0.1059; d.r[5] = 0.1412; d.r[6] = 0.3804; d.r[7] = 0.6431; + d.r[8] = 0.8196; d.r[9] = 0.9529; d.r[10] = 0.9961; d.r[11] = 0.9529; + d.r[12] = 0.8510; d.r[13] = 0.6941; d.r[14] = 0.4784; + d.g[0] = 0.0706; d.g[1] = 0.2706; d.g[2] = 0.4588; d.g[3] = 0.6353; + d.g[4] = 0.8118; d.g[5] = 0.9255; d.g[6] = 0.9882; d.g[7] = 0.9882; + d.g[8] = 0.9098; d.g[9] = 0.7765; d.g[10] = 0.6078; d.g[11] = 0.3882; + d.g[12] = 0.2196; d.g[13] = 0.0980; d.g[14] = 0.0157; + d.b[0] = 0.2314; d.b[1] = 0.6706; d.b[2] = 0.9294; d.b[3] = 0.9882; + d.b[4] = 0.8314; d.b[5] = 0.6510; d.b[6] = 0.4235; d.b[7] = 0.2314; + d.b[8] = 0.2039; d.b[9] = 0.2275; d.b[10] = 0.1765; d.b[11] = 0.0824; + d.b[12] = 0.0235; d.b[13] = 0.0039; d.b[14] = 0.0078; + return d; + }(); + static const PaletteDef viridis = [] { + PaletteDef d; + d.nstops = 10; + d.stops[0] = 0.0000; + d.stops[1] = 0.1111; + d.stops[2] = 0.2222; + d.stops[3] = 0.3333; + d.stops[4] = 0.4444; + d.stops[5] = 0.5556; + d.stops[6] = 0.6667; + d.stops[7] = 0.7778; + d.stops[8] = 0.8889; + d.stops[9] = 1.0000; + d.r[0] = 0.2667; d.r[1] = 0.2824; d.r[2] = 0.2431; d.r[3] = 0.1922; + d.r[4] = 0.1490; d.r[5] = 0.1216; d.r[6] = 0.2078; d.r[7] = 0.4314; + d.r[8] = 0.7098; d.r[9] = 0.9922; + d.g[0] = 0.0039; d.g[1] = 0.1569; d.g[2] = 0.2863; d.g[3] = 0.4078; + d.g[4] = 0.5098; d.g[5] = 0.6196; d.g[6] = 0.7176; d.g[7] = 0.8078; + d.g[8] = 0.8706; d.g[9] = 0.9059; + d.b[0] = 0.3294; d.b[1] = 0.4706; d.b[2] = 0.5373; d.b[3] = 0.5569; + d.b[4] = 0.5569; d.b[5] = 0.5373; d.b[6] = 0.4745; d.b[7] = 0.3451; + d.b[8] = 0.1686; d.b[9] = 0.1451; + return d; + }(); + static const PaletteDef plasma = [] { + PaletteDef d; + d.nstops = 10; + d.stops[0] = 0.0000; + d.stops[1] = 0.1111; + d.stops[2] = 0.2222; + d.stops[3] = 0.3333; + d.stops[4] = 0.4444; + d.stops[5] = 0.5556; + d.stops[6] = 0.6667; + d.stops[7] = 0.7778; + d.stops[8] = 0.8889; + d.stops[9] = 1.0000; + d.r[0] = 0.0510; d.r[1] = 0.2745; d.r[2] = 0.4471; d.r[3] = 0.6118; + d.r[4] = 0.7412; d.r[5] = 0.8471; d.r[6] = 0.9294; d.r[7] = 0.9843; + d.r[8] = 0.9922; d.r[9] = 0.9412; + d.g[0] = 0.0314; d.g[1] = 0.0118; d.g[2] = 0.0039; d.g[3] = 0.0902; + d.g[4] = 0.2157; d.g[5] = 0.3412; d.g[6] = 0.4745; d.g[7] = 0.6235; + d.g[8] = 0.7922; d.g[9] = 0.9765; + d.b[0] = 0.5294; d.b[1] = 0.6235; d.b[2] = 0.6588; d.b[3] = 0.6196; + d.b[4] = 0.5255; d.b[5] = 0.4196; d.b[6] = 0.3255; d.b[7] = 0.2275; + d.b[8] = 0.1490; d.b[9] = 0.1294; + return d; + }(); + static const PaletteDef inferno = [] { + PaletteDef d; + d.nstops = 10; + d.stops[0] = 0.0000; + d.stops[1] = 0.1111; + d.stops[2] = 0.2222; + d.stops[3] = 0.3333; + d.stops[4] = 0.4444; + d.stops[5] = 0.5556; + d.stops[6] = 0.6667; + d.stops[7] = 0.7778; + d.stops[8] = 0.8889; + d.stops[9] = 1.0000; + d.r[0] = 0.0000; d.r[1] = 0.1059; d.r[2] = 0.2902; d.r[3] = 0.4706; + d.r[4] = 0.6471; d.r[5] = 0.8118; d.r[6] = 0.9294; d.r[7] = 0.9843; + d.r[8] = 0.9686; d.r[9] = 0.9882; + d.g[0] = 0.0000; d.g[1] = 0.0471; d.g[2] = 0.0471; d.g[3] = 0.1098; + d.g[4] = 0.1725; d.g[5] = 0.2667; d.g[6] = 0.4118; d.g[7] = 0.6078; + d.g[8] = 0.8196; d.g[9] = 1.0000; + d.b[0] = 0.0157; d.b[1] = 0.2549; d.b[2] = 0.4196; d.b[3] = 0.4275; + d.b[4] = 0.3765; d.b[5] = 0.2745; d.b[6] = 0.1451; d.b[7] = 0.0235; + d.b[8] = 0.2392; d.b[9] = 0.6431; + return d; + }(); + static const PaletteDef magma = [] { + PaletteDef d; + d.nstops = 10; + d.stops[0] = 0.0000; + d.stops[1] = 0.1111; + d.stops[2] = 0.2222; + d.stops[3] = 0.3333; + d.stops[4] = 0.4444; + d.stops[5] = 0.5556; + d.stops[6] = 0.6667; + d.stops[7] = 0.7778; + d.stops[8] = 0.8889; + d.stops[9] = 1.0000; + d.r[0] = 0.0000; d.r[1] = 0.0941; d.r[2] = 0.2667; d.r[3] = 0.4471; + d.r[4] = 0.6196; d.r[5] = 0.8039; d.r[6] = 0.9451; d.r[7] = 0.9922; + d.r[8] = 0.9961; d.r[9] = 0.9882; + d.g[0] = 0.0000; d.g[1] = 0.0588; d.g[2] = 0.0588; d.g[3] = 0.1216; + d.g[4] = 0.1843; d.g[5] = 0.2510; d.g[6] = 0.3765; d.g[7] = 0.5882; + d.g[8] = 0.7922; d.g[9] = 0.9922; + d.b[0] = 0.0157; d.b[1] = 0.2392; d.b[2] = 0.4627; d.b[3] = 0.5059; + d.b[4] = 0.4980; d.b[5] = 0.4431; d.b[6] = 0.3647; d.b[7] = 0.4078; + d.b[8] = 0.5529; d.b[9] = 0.7490; + return d; + }(); + static const PaletteDef cividis = [] { + PaletteDef d; + d.nstops = 10; + d.stops[0] = 0.0000; + d.stops[1] = 0.1111; + d.stops[2] = 0.2222; + d.stops[3] = 0.3333; + d.stops[4] = 0.4444; + d.stops[5] = 0.5556; + d.stops[6] = 0.6667; + d.stops[7] = 0.7778; + d.stops[8] = 0.8889; + d.stops[9] = 1.0000; + d.r[0] = 0.0000; d.r[1] = 0.0706; d.r[2] = 0.2314; d.r[3] = 0.3412; + d.r[4] = 0.4392; d.r[5] = 0.5412; d.r[6] = 0.6471; d.r[7] = 0.7647; + d.r[8] = 0.8824; d.r[9] = 0.9961; + d.g[0] = 0.1333; d.g[1] = 0.2078; d.g[2] = 0.2863; d.g[3] = 0.3647; + d.g[4] = 0.4431; d.g[5] = 0.5255; d.g[6] = 0.6118; d.g[7] = 0.7020; + d.g[8] = 0.8000; d.g[9] = 0.9098; + d.b[0] = 0.3059; d.b[1] = 0.4392; d.b[2] = 0.4235; d.b[3] = 0.4275; + d.b[4] = 0.4510; d.b[5] = 0.4706; d.b[6] = 0.4549; d.b[7] = 0.4118; + d.b[8] = 0.3333; d.b[9] = 0.2196; + return d; + }(); + static const PaletteDef hot = [] { + PaletteDef d; + d.nstops = 4; + d.stops[0] = 0.0000; + d.stops[1] = 0.3650; + d.stops[2] = 0.7460; + d.stops[3] = 1.0000; + d.r[0] = 0.0000; d.r[1] = 1.0000; d.r[2] = 1.0000; d.r[3] = 1.0000; + d.g[0] = 0.0000; d.g[1] = 0.0000; d.g[2] = 1.0000; d.g[3] = 1.0000; + d.b[0] = 0.0000; d.b[1] = 0.0000; d.b[2] = 0.0000; d.b[3] = 1.0000; + return d; + }(); + static const PaletteDef gray = [] { + PaletteDef d; + d.nstops = 2; + d.stops[0] = 0.0000; + d.stops[1] = 1.0000; + d.r[0] = 0.0000; d.r[1] = 1.0000; + d.g[0] = 0.0000; d.g[1] = 1.0000; + d.b[0] = 0.0000; d.b[1] = 1.0000; + return d; + }(); + static const PaletteDef gray_r = [] { + PaletteDef d; + d.nstops = 2; + d.stops[0] = 0.0000; + d.stops[1] = 1.0000; + d.r[0] = 1.0000; d.r[1] = 0.0000; + d.g[0] = 1.0000; d.g[1] = 0.0000; + d.b[0] = 1.0000; d.b[1] = 0.0000; + return d; + }(); + static const PaletteDef coolwarm = [] { + PaletteDef d; + d.nstops = 3; + d.stops[0] = 0.0000; + d.stops[1] = 0.5000; + d.stops[2] = 1.0000; + d.r[0] = 0.2314; d.r[1] = 0.8667; d.r[2] = 0.7059; + d.g[0] = 0.2980; d.g[1] = 0.8667; d.g[2] = 0.0157; + d.b[0] = 0.7529; d.b[1] = 0.8667; d.b[2] = 0.1490; + return d; + }(); + static const PaletteDef bwr = [] { + PaletteDef d; + d.nstops = 3; + d.stops[0] = 0.0000; + d.stops[1] = 0.5000; + d.stops[2] = 1.0000; + d.r[0] = 0.0000; d.r[1] = 1.0000; d.r[2] = 1.0000; + d.g[0] = 0.0000; d.g[1] = 1.0000; d.g[2] = 0.0000; + d.b[0] = 1.0000; d.b[1] = 1.0000; d.b[2] = 0.0000; + return d; + }(); + static const PaletteDef seismic = [] { + PaletteDef d; + d.nstops = 5; + d.stops[0] = 0.0000; + d.stops[1] = 0.2500; + d.stops[2] = 0.5000; + d.stops[3] = 0.7500; + d.stops[4] = 1.0000; + d.r[0] = 0.0000; d.r[1] = 0.0000; d.r[2] = 1.0000; d.r[3] = 1.0000; + d.r[4] = 0.5020; + d.g[0] = 0.0000; d.g[1] = 0.0000; d.g[2] = 1.0000; d.g[3] = 0.0000; + d.g[4] = 0.0000; + d.b[0] = 0.3020; d.b[1] = 1.0000; d.b[2] = 1.0000; d.b[3] = 0.0000; + d.b[4] = 0.0000; + return d; + }(); + static const PaletteDef rdbu = [] { + PaletteDef d; + d.nstops = 9; + d.stops[0] = 0.0000; + d.stops[1] = 0.1250; + d.stops[2] = 0.2500; + d.stops[3] = 0.3750; + d.stops[4] = 0.5000; + d.stops[5] = 0.6250; + d.stops[6] = 0.7500; + d.stops[7] = 0.8750; + d.stops[8] = 1.0000; + d.r[0] = 0.6980; d.r[1] = 0.8392; d.r[2] = 0.9569; d.r[3] = 0.9922; + d.r[4] = 0.9686; d.r[5] = 0.8196; d.r[6] = 0.5725; d.r[7] = 0.2627; + d.r[8] = 0.1294; + d.g[0] = 0.0941; d.g[1] = 0.3765; d.g[2] = 0.6471; d.g[3] = 0.8588; + d.g[4] = 0.9686; d.g[5] = 0.8980; d.g[6] = 0.7725; d.g[7] = 0.5765; + d.g[8] = 0.4000; + d.b[0] = 0.1686; d.b[1] = 0.3020; d.b[2] = 0.5098; d.b[3] = 0.7804; + d.b[4] = 0.9686; d.b[5] = 0.9412; d.b[6] = 0.8706; d.b[7] = 0.7647; + d.b[8] = 0.6745; + return d; + }(); + static const PaletteDef ylgnbu = [] { + PaletteDef d; + d.nstops = 9; + d.stops[0] = 0.0000; + d.stops[1] = 0.1250; + d.stops[2] = 0.2500; + d.stops[3] = 0.3750; + d.stops[4] = 0.5000; + d.stops[5] = 0.6250; + d.stops[6] = 0.7500; + d.stops[7] = 0.8750; + d.stops[8] = 1.0000; + d.r[0] = 0.0314; d.r[1] = 0.1451; d.r[2] = 0.1333; d.r[3] = 0.1137; + d.r[4] = 0.2549; d.r[5] = 0.4980; d.r[6] = 0.7804; d.r[7] = 0.9294; + d.r[8] = 1.0000; + d.g[0] = 0.1137; d.g[1] = 0.2039; d.g[2] = 0.3686; d.g[3] = 0.5686; + d.g[4] = 0.7137; d.g[5] = 0.8039; d.g[6] = 0.9137; d.g[7] = 0.9725; + d.g[8] = 1.0000; + d.b[0] = 0.3451; d.b[1] = 0.5804; d.b[2] = 0.6588; d.b[3] = 0.7529; + d.b[4] = 0.7686; d.b[5] = 0.7333; d.b[6] = 0.7059; d.b[7] = 0.6941; + d.b[8] = 0.8510; + return d; + }(); + static const PaletteDef sunset = [] { + PaletteDef d; + d.nstops = 11; + d.stops[0] = 0.0000; + d.stops[1] = 0.1000; + d.stops[2] = 0.2000; + d.stops[3] = 0.3000; + d.stops[4] = 0.4000; + d.stops[5] = 0.5000; + d.stops[6] = 0.6000; + d.stops[7] = 0.7000; + d.stops[8] = 0.8000; + d.stops[9] = 0.9000; + d.stops[10] = 1.0000; + d.r[0] = 0.2118; d.r[1] = 0.2902; d.r[2] = 0.4314; d.r[3] = 0.5961; + d.r[4] = 0.7608; d.r[5] = 0.9176; d.r[6] = 0.9961; d.r[7] = 0.9922; + d.r[8] = 0.9647; d.r[9] = 0.8667; d.r[10] = 0.6471; + d.g[0] = 0.2941; d.g[1] = 0.4824; d.g[2] = 0.6510; d.g[3] = 0.7922; + d.g[4] = 0.8941; d.g[5] = 0.9255; d.g[6] = 0.8549; d.g[7] = 0.7020; + d.g[8] = 0.4941; d.g[9] = 0.2392; d.g[10] = 0.0000; + d.b[0] = 0.6039; d.b[1] = 0.7176; d.b[2] = 0.8039; d.b[3] = 0.8824; + d.b[4] = 0.9373; d.b[5] = 0.8000; d.b[6] = 0.5451; d.b[7] = 0.4000; + d.b[8] = 0.2941; d.b[9] = 0.1765; d.b[10] = 0.1490; + return d; + }(); + + // The gain image holds one of four numbers (the Jungfrau gain bits). These + // are deliberately desaturated so a large flat map does not glare. + static const PaletteDef gain4 = [] { + PaletteDef d; + d.nstops = 4; + d.stops[0] = 0.0000; d.stops[1] = 0.3333; d.stops[2] = 0.6667; + d.stops[3] = 1.0000; + d.r[0] = 0.3725; d.r[1] = 0.4353; d.r[2] = 0.7961; d.r[3] = 0.6902; + d.g[0] = 0.5020; d.g[1] = 0.6275; d.g[2] = 0.7412; d.g[3] = 0.4157; + d.b[0] = 0.6588; d.b[1] = 0.4353; d.b[2] = 0.4157; d.b[3] = 0.3882; + return d; + }(); + + switch (p) { + case Palette::BW1: + return bw1; + case Palette::BW2: + return bw2; + case Palette::Turbo: + return turbo; + case Palette::Viridis: + return viridis; + case Palette::Plasma: + return plasma; + case Palette::Inferno: + return inferno; + case Palette::Magma: + return magma; + case Palette::Cividis: + return cividis; + case Palette::Hot: + return hot; + case Palette::Gray: + return gray; + case Palette::Gray_r: + return gray_r; + case Palette::Coolwarm: + return coolwarm; + case Palette::Bwr: + return bwr; + case Palette::Seismic: + return seismic; + case Palette::RdBu: + return rdbu; + case Palette::YlGnBu: + return ylgnbu; + case Palette::Sunset: + return sunset; + case Palette::Gain4: + return gain4; + case Palette::Color: + default: + return color; + } +} + + +// 256-entry lookup table sampled from exactly the same definition +void buildLut(const PaletteDef &d, QRgb *out) { + for (int i = 0; i < 256; i++) { + const double v = i / 255.0; + double rr, gg, bb; + if (d.nstops <= 0) { + const double c = std::pow(v, d.gamma); + rr = gg = bb = c; + } else { + int k = 0; + while (k < d.nstops - 2 && v > d.stops[k + 1]) + k++; + const double span = d.stops[k + 1] - d.stops[k]; + const double t = (span > 0) ? (v - d.stops[k]) / span : 0.0; + const double tc = (t < 0) ? 0.0 : (t > 1 ? 1.0 : t); + rr = d.r[k] + (d.r[k + 1] - d.r[k]) * tc; + gg = d.g[k] + (d.g[k + 1] - d.g[k]) * tc; + bb = d.b[k] + (d.b[k + 1] - d.b[k]) * tc; + } + out[i] = qRgb(static_cast(rr * 255.0 + 0.5), + static_cast(gg * 255.0 + 0.5), + static_cast(bb * 255.0 + 0.5)); + } +} + +// one 256-entry table per palette, all built the same way from paletteDef() +constexpr int kNumPalettes = int(Palette::Gain4) + 1; +QRgb g_luts[kNumPalettes][256]; +struct LutInit { + LutInit() { + for (int i = 0; i < kNumPalettes; i++) + buildLut(paletteDef(static_cast(i)), g_luts[i]); + } +} g_lutInit; + +} // namespace + +// Labels for the scale panel's palette chooser (same order as the enum). +const char *paletteName(Palette p) { + switch (p) { + case Palette::Gain4: + return "gain 0..3 (fixed)"; + case Palette::Color: + return "Col (jet)"; + case Palette::BW1: + return "BW grey 1"; + case Palette::BW2: + return "BW grey 2"; + case Palette::Turbo: + return "Turbo"; + case Palette::Viridis: + return "Viridis"; + case Palette::Plasma: + return "Plasma"; + case Palette::Inferno: + return "Inferno"; + case Palette::Magma: + return "Magma"; + case Palette::Cividis: + return "Cividis (colorblind)"; + case Palette::Hot: + return "Hot"; + case Palette::Gray: + return "Grey (linear)"; + case Palette::Gray_r: + return "Grey reversed"; + case Palette::Coolwarm: + return "Coolwarm (diverging)"; + case Palette::Bwr: + return "BWR (diverging)"; + case Palette::Seismic: + return "Seismic (diverging)"; + case Palette::RdBu: + return "RdBu (diverging)"; + case Palette::YlGnBu: + return "YlGnBu (Brewer)"; + case Palette::Sunset: + return "Sunset (Tol)"; + } + return "?"; +} + +const QRgb *paletteLut(Palette p) { + const int i = int(p); + if (i < 0 || i >= kNumPalettes) + return g_luts[int(Palette::Color)]; + return g_luts[i]; +} + +// ROOT-thread only: rebuild the ROOT gradient colour table +void loadRootPalette(Palette p) { + const PaletteDef &d = paletteDef(p); + const Int_t NCont = 90; + Double_t stops[99], red[99], green[99], blue[99]; + Int_t n; + if (d.nstops <= 0) { + n = 99; + for (int i = 0; i < n; i++) { + const double s = static_cast(i) / (n - 1); + stops[i] = s; + const double c = std::pow(s, d.gamma); + red[i] = green[i] = blue[i] = c; + } + } else { + n = d.nstops; + for (int i = 0; i < n; i++) { + stops[i] = d.stops[i]; + red[i] = d.r[i]; + green[i] = d.g[i]; + blue[i] = d.b[i]; + } + } + TColor::CreateGradientColorTable(n, stops, red, green, blue, NCont); + gStyle->SetNumberContours(NCont); +} + +// The 8 chip spectra live in a 4x2 pad grid whose TOP row is the TOP half of +// the detector (chips 4..7 = data rows 256..511). Both spectrum canvases must +// use the same mapping so they can be compared pad by pad. +int chipPadToHist(int ipad) { return ipad % 4 + (1 - int(ipad / 4)) * 4; } + +// shares the x axis of every spectrum of one canvas with its first pad (the +// operator zooms one pad and all of them follow, as in the original display). +// Each canvas keeps its own state so the two cannot interact. +struct ChipXSync { + int first = -1, last = -1; + bool have = false; +}; +ChipXSync g_aduXSync, g_kevXSync; +void syncChipXRange(TH1I **h, ChipXSync &st) { + if (!h || !h[0]) + return; + if (!st.have || h[0]->GetXaxis()->GetLast() != st.last) { + st.first = h[0]->GetXaxis()->GetFirst(); + st.last = h[0]->GetXaxis()->GetLast(); + st.have = true; + for (int ipad = 1; ipad < 8; ipad++) { + if (h[ipad]) + h[ipad]->GetXaxis()->SetRange(st.first, st.last); + } + } +} + +// the ROI spectrum is read against the chip spectra, so it always mirrors the +// x range of the chip canvas (and only A4's: the two sync states must not +// interact) +void mirrorChipXRangeToRoi() { + if (!dispChip[0]) + return; + const int first = dispChip[0]->GetXaxis()->GetFirst(); + const int last = dispChip[0]->GetXaxis()->GetLast(); + // the keV canvases share the same window: their axes are the ADU axes + // divided by KEV_NOMINAL_ADU, so the bin indices are identical + TH1I *dst[10] = {dispRoi, dispRoiKev, dispChipKev[0], + dispChipKev[1], dispChipKev[2], dispChipKev[3], + dispChipKev[4], dispChipKev[5], dispChipKev[6], + dispChipKev[7]}; + for (TH1I *h : dst) { + if (!h) + continue; + if (h->GetXaxis()->GetFirst() != first || + h->GetXaxis()->GetLast() != last) + h->GetXaxis()->SetRange(first, last); + } +} + +// paints one 4x2 canvas of 8 chip spectra (canvas A4 in ADU, A8 in keV) +void plotChipCanvas(TH1I **h, TCanvas *c, ChipXSync &st) { + if (!h || !h[0] || !c) + return; + syncChipXRange(h, st); // all pads follow the first one + gStyle->SetOptStat(1); + for (int ipad = 0; ipad < 8; ipad++) { + c->cd(ipad + 1); + if (gPad && !gPad->GetLogy()) + gPad->SetLogy(); + // "L" draws one polyline through the bin contents. The default + // (markers + error bars) is ~2000 X11 operations per pad, which costs + // hundreds of ms per redraw and starves the event loop. + h[chipPadToHist(ipad)]->Draw("L"); + if (ipad == 3) + serviceRootEvents(); // keep the canvases interactive mid-redraw + } + c->cd(); + c->Update(); +} + +// paints the canvas holding the single ROI spectrum (A7 in ADU, A9 in keV). +// The window title carries the ROI rectangle it was filled with. +void plotRoiCanvas(TH1I *h, TCanvas *c, const char *what) { + if (!h || !c) + return; + static int lx0 = -1, ly0 = -1, lx1 = -1, ly1 = -1; + static bool lastOn = false, have = false, titled = false; + const bool on = ROIactive.load(); + const int x0 = ROIxmin.load(), y0 = ROIymin.load(); + const int x1 = ROIxmax.load(), y1 = ROIymax.load(); + if (!have || !titled || on != lastOn || x0 != lx0 || y0 != ly0 || + x1 != lx1 || y1 != ly1) { + have = true; + titled = true; + lastOn = on; + lx0 = x0; + ly0 = y0; + lx1 = x1; + ly1 = y1; + char t[160]; + if (on) + snprintf(t, sizeof(t), + "%s - gate ROI x %d..%d, y %d..%d (%dx%d px, data rows, " + "row 0 = bottom)", + what, x0, x1, y0, y1, x1 - x0, y1 - y0); + else + snprintf(t, sizeof(t), "%s - no gate ROI (use \"ROI only\")", + what); + c->SetTitle(t); // TCanvas::SetTitle also renames the window + } + gStyle->SetOptStat(1); + c->cd(); + if (gPad && !gPad->GetLogy()) + gPad->SetLogy(); + h->Draw(); // normal histogram rendering (markers + error bars) + c->cd(); + c->Update(); +} + +// the operator's PH window is shared by every spectrum canvas +void Plot1DHistos(void) { + mirrorChipXRangeToRoi(); + plotChipCanvas(dispChip, A4, g_aduXSync); +} + +// title of the shared canvases, so it is obvious which set is on screen +void setSpectrumTitles(bool kev) { + static int shown = -1; + if (shown == (kev ? 1 : 0)) + return; + shown = kev ? 1 : 0; + if (A4) + A4->SetTitle(kev ? "PH spectra [keV]" : "PH spectra [ADU]"); + if (A7) + A7->SetTitle(kev ? "ROI PH [keV]" : "ROI PH [ADU]"); +} + +// ONE spectrum of the gate-ROI pixels (canvas A7): same binning and same +// border trim as the chip spectra, but summed over the whole selection, so it +// answers "what do the pixels I selected look like?" and can be compared with +// the sum of the eight pads of A4. +void PlotRoiHistos(void) { + if (!dispRoi || !A7) + return; + plotRoiCanvas(dispRoi, A7, "ROI PH"); +} + +// the same two canvases in keV, filled and drawn only when a gain map is +// loaded (canvases A8/A9) +// the same canvas, filled from the keV histograms instead of the ADU ones +void Plot1DHistosKev(void) { + if (!kevSpectra.load() || !gain_flag.load()) + return; + // the ROI canvas mirrors the chips, exactly as in the ADU set + if (dispChipKev[0] && dispRoiKev) { + const int f = dispChipKev[0]->GetXaxis()->GetFirst(); + const int l = dispChipKev[0]->GetXaxis()->GetLast(); + if (dispRoiKev->GetXaxis()->GetFirst() != f || + dispRoiKev->GetXaxis()->GetLast() != l) + dispRoiKev->GetXaxis()->SetRange(f, l); + } + plotChipCanvas(dispChipKev, A4, g_kevXSync); +} + +void PlotRoiHistosKev(void) { + if (!kevSpectra.load() || !gain_flag.load()) + return; + plotRoiCanvas(dispRoiKev, A7, "ROI PH [keV]"); +} + +// ROOT maps an explicit axis range onto its pad frame, so a range whose shape +// differs from the frame is stretched: a narrow/tall ROI came out 5x wider than +// tall. Widen the shorter side of the range to the frame's shape - centred on +// the ROI and shifted (never resized) to stay on the sensor - so the region +// keeps the shape of the ADC window, the ROI box keeps its shape and the +// pixels stay square. The extra margin around the ROI is visible, so the box +// can be seen sitting inside the plot. +void zoomPhotonMap(double x0, double x1, double y0, double y1) { + if (!A5 || !disp3000) + return; + double dx = x1 - x0, dy = y1 - y0; + if (dx <= 0 || dy <= 0) + return; + double W = A5->GetWw(), H = A5->GetWh(); + if (W < 10 || H < 10) { // not mapped yet: use the booked size + W = 1000; + H = 600; + } + // frame area in pixels (the z palette lives in the right margin) + const double fw = + 1.0 - gStyle->GetPadLeftMargin() - gStyle->GetPadRightMargin(); + const double fh = + 1.0 - gStyle->GetPadBottomMargin() - gStyle->GetPadTopMargin(); + const double fx = W * fw, fy = H * fh; + if (fx / dx > fy / dy) + dx = dy * fx / fy; // x is magnified more: widen it + else + dy = dx * fy / fx; + // centre on the requested window, then shift it inside the sensors range + const double cx = 0.5 * (x0 + x1), cy = 0.5 * (y0 + y1); + const double ax0 = disp3000->GetXaxis()->GetXmin(); + const double ax1 = disp3000->GetXaxis()->GetXmax(); + const double ay0 = disp3000->GetYaxis()->GetXmin(); + const double ay1 = disp3000->GetYaxis()->GetXmax(); + double nx = cx - dx / 2.0, ny = cy - dy / 2.0; + if (nx < ax0) + nx = ax0; + if (nx + dx > ax1) + nx = ax1 - dx; + if (nx < ax0) + nx = ax0; // wider than the sensor: ROOT clamps the selection + if (ny < ay0) + ny = ay0; + if (ny + dy > ay1) + ny = ay1 - dy; + if (ny < ay0) + ny = ay0; + disp3000->GetXaxis()->SetRangeUser(nx, nx + dx); + disp3000->GetYaxis()->SetRangeUser(ny, ny + dy); +} + +// redraws the photon map (used by Plot2DHistos and by the zoom command) +void plotPhotonMap() { + if (!A5 || !disp3000) + return; + A5->cd(); + disp3000->Draw("colz"); + drawRoiBox(); + A5->Update(); +} + +void drawRoiBox() { + if (box) { + if (gPad && gPad->GetListOfPrimitives()) + gPad->GetListOfPrimitives()->Remove(box); + delete box; + box = nullptr; + } + if (!ROIactive.load()) + return; // no ROI: nothing to outline + const double x1 = ROIxmin.load(); + const double x2 = ROIxmax.load(); + const double y1 = ROIymin.load(); + const double y2 = ROIymax.load(); + if (x2 <= x1 || y2 <= y1) + return; + box = new TBox(x1, y1, x2, y2); + box->SetLineColor(kYellow); // same colour as the ADC overlay + box->SetFillStyle(0); + box->SetLineWidth(2); + box->Draw(); +} + +void Plot2DHistos(void) { + gStyle->SetOptStat(0); +#ifdef JFSTRX + if (p5 && disp1000) { + p5->cd(); + disp1000->Draw("colz"); + } + if (p3 && disp1001) { + p3->cd(); + disp1001->Draw("colz"); + } + if (p1 && disp1002) { + p1->cd(); + disp1002->Draw("colz"); + } + if (p2 && disp1060) { + p2->cd(); + disp1060->Draw("colz"); + } + if (p4 && disp1061) { + p4->cd(); + disp1061->Draw("colz"); + } + if (p6 && disp1062) { + p6->cd(); + disp1062->Draw("colz"); + } +#endif + if (A5 && disp3000) + plotPhotonMap(); + serviceRootEvents(); // the photon map is painted: service input + if (A6 && disp4500 && dispLvsR) { + A6->cd(); + if (!A6->GetListOfPrimitives() || + !A6->GetListOfPrimitives()->GetSize()) { + A6->Divide(1, 2); + } + A6->cd(1); + disp4500->Draw("colz"); + A6->cd(2); + dispLvsR->Draw("colz"); + A6->Update(); + } +} + +void axisreset() { // ROOT-thread only (executed via the command queue) + fixranges.store(false); +#ifdef JFSTRX + TH2F *jf[6] = {disp1000, disp1001, disp1002, disp1060, disp1061, disp1062}; + for (int k = 0; k < 6; k++) { + if (!jf[k]) + continue; + jf[k]->GetXaxis()->UnZoom(); + jf[k]->GetYaxis()->UnZoom(); + jf[k]->GetZaxis()->UnZoom(); + } +#endif + if (disp3000) { + disp3000->GetXaxis()->UnZoom(); + disp3000->GetYaxis()->UnZoom(); + disp3000->GetZaxis()->UnZoom(); + } + for (int i = 0; i < 8; i++) { + if (dispChip[i]) { + dispChip[i]->GetXaxis()->UnZoom(); + dispChip[i]->GetYaxis()->UnZoom(); + } + } + if (dispRoi) { + dispRoi->GetXaxis()->UnZoom(); + dispRoi->GetYaxis()->UnZoom(); + } + if (dispRoiKev) { + dispRoiKev->GetXaxis()->UnZoom(); + dispRoiKev->GetYaxis()->UnZoom(); + } + for (int i = 0; i < 8; i++) { + if (dispChipKev[i]) { + dispChipKev[i]->GetXaxis()->UnZoom(); + dispChipKev[i]->GetYaxis()->UnZoom(); + } + } + if (disp4500) { + disp4500->GetXaxis()->UnZoom(); + disp4500->GetYaxis()->UnZoom(); + disp4500->GetZaxis()->UnZoom(); + } + if (dispLvsR) { + dispLvsR->GetXaxis()->UnZoom(); + dispLvsR->GetYaxis()->UnZoom(); + dispLvsR->GetZaxis()->UnZoom(); + } + Plot2DHistos(); + Plot1DHistos(); +} diff --git a/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/test/gate_contig_test.cpp b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/test/gate_contig_test.cpp new file mode 100644 index 000000000..34558948b --- /dev/null +++ b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/test/gate_contig_test.cpp @@ -0,0 +1,184 @@ +// Unit test for the gate's contiguity proxy ("Contiguous (N px within circle, +// r = N/2+1)"). +// +// The rule must be: pass when SOME circle of radius r contains at least N +// above-threshold pixels. It used to require every above-threshold pixel to be +// within r of the centroid, which rejected perfectly valid events (two +// separated clusters) - the bug this test pins down. +// +// Build: make gate_contig_test Run: ./test/gate_contig_test +#include "online_jf.h" + +#include +#include +#include +#include + +namespace { + +int gFail = 0; + +void expect(const char *what, bool got, bool want) { + const bool ok = (got == want); + if (!ok) + gFail++; + std::printf("%-58s %s (got %s, want %s)\n", what, ok ? "ok " : "FAIL", + got ? "pass" : "drop", want ? "pass" : "drop"); +} + +struct Frame { + std::vector px = std::vector(NCH, 0); + + void hit(int x, int y, short v = 2000) { + px[static_cast(y) * kRawWidth + x] = v; + } + void clear() { std::fill(px.begin(), px.end(), 0); } +}; + +// Runs the real GateEngine over the frame, raw ADC values, no pedestals. +bool passes(GateEngine &g, Frame &f) { + uint64_t above = 0; + bool scDropped = false; + return g.evaluate(f.px.data(), NCH, 0, nullptr, 0, 0, above, &scDropped); +} + +} // namespace + +int main() { + GateEngine g; + g.setEnabled(true); + g.setThreshold(1000.0f); // the frame's hits are 2000 -> above threshold + g.setMinPixels(5); + g.setContiguous(true); + + Frame f; + + // 1. one tight cluster of exactly N hits: must pass + f.clear(); + for (int i = 0; i < 5; i++) + f.hit(100 + i, 100); + expect("5 hits in a row (spread 4 px <= 2r)", passes(g, f), true); + + // 2. a compact blob: must pass + f.clear(); + f.hit(300, 200); + f.hit(301, 200); + f.hit(302, 200); + f.hit(301, 201); + f.hit(301, 199); + expect("5 hits in a compact cross", passes(g, f), true); + + // 3. N hits far apart, no cluster: must be rejected + f.clear(); + for (int i = 0; i < 5; i++) + f.hit(100 + 100 * i, 300); + expect("5 isolated hits 100 px apart", passes(g, f), false); + + // 4. fewer than N hits, however tight: must be rejected + f.clear(); + for (int i = 0; i < 4; i++) + f.hit(500 + i, 400); + expect("4 hits (below min pixels)", passes(g, f), false); + + // 5. THE REPORTED BUG: two separated clusters, each with N hits. + // The centroid falls between them, which used to reject the frame. + f.clear(); + for (int i = 0; i < 5; i++) + f.hit(100 + i, 50); // cluster A + for (int i = 0; i < 5; i++) + f.hit(700 + i, 450); // cluster B + expect("two clusters of 5, 700 px apart (must pass)", passes(g, f), true); + + // 6. two groups of 4: no circle holds 5 -> reject + f.clear(); + for (int i = 0; i < 4; i++) + f.hit(100 + i, 60); + for (int i = 0; i < 4; i++) + f.hit(600 + i, 440); + expect("two clusters of 4 (no circle holds 5)", passes(g, f), false); + + // 7. a spread-out event with one dense core: pass + f.clear(); + for (int i = 0; i < 5; i++) + f.hit(400 + i, 300); + f.hit(800, 100); + f.hit(900, 200); + expect("dense core plus 2 stragglers", passes(g, f), true); + + // 8. N follows min_pixels: 3 hits pass when min_pixels is 3 + g.setMinPixels(3); + f.clear(); + for (int i = 0; i < 3; i++) + f.hit(200 + i, 200); + expect("3 hits with min_pixels = 3", passes(g, f), true); + f.clear(); + for (int i = 0; i < 2; i++) + f.hit(200 + i, 200); + expect("2 hits with min_pixels = 3", passes(g, f), false); + + // 9. contiguity off: the plain count decides (2 hits < 3 -> reject) + g.setContiguous(false); + f.clear(); + f.hit(10, 10); + f.hit(900, 400); + expect("contiguity off, 2 hits with min_pixels = 3", passes(g, f), false); + f.clear(); + for (int i = 0; i < 3; i++) + f.hit(10 + i, 10); + expect("contiguity off, 3 hits with min_pixels = 3", passes(g, f), true); + + // ---- hot pixel mask taken from a photon map --------------------------- + { + GateEngine h; + h.setEnabled(true); + h.setThreshold(1000.0f); + h.setMinPixels(5); + h.setContiguous(false); + h.setHotSuppress(true); + + // The map has two very bright pixels: those are the hot ones. + std::vector map(NCH, 0.0f); + map[10 * kRawWidth + 10] = 400.0f; + map[10 * kRawWidth + 11] = 900.0f; + hotSetMaskFromMap(map.data(), map.size(), 100.0); + std::printf("mask from map > 100: %llu px (want 2)\n", + static_cast(hotPixelCount())); + if (hotPixelCount() != 2) + gFail++; + + Frame f3; + // 5 hits, two of them on the masked pixels -> only 3 count -> drop + f3.clear(); + for (int i = 0; i < 5; i++) + f3.hit(10 + i, 10); + expect("5 hits, 2 on hot pixels (min 5)", passes(h, f3), false); + + // a real cluster away from the mask still passes + f3.clear(); + for (int i = 0; i < 5; i++) + f3.hit(300 + i, 300); + expect("real cluster of 5 with the mask active", passes(h, f3), true); + + // cleared mask: the hot pixels count again + hotClearMask(); + expect("after hotClearMask, 5 hits count", passes(h, f3), true); + f3.clear(); + for (int i = 0; i < 5; i++) + f3.hit(10 + i, 10); + expect("after hotClearMask, the 5 hits pass", passes(h, f3), true); + + // suppression off: the mask is ignored by the gate + hotSetMaskFromMap(map.data(), map.size(), 100.0); + h.setHotSuppress(false); + f3.clear(); + for (int i = 0; i < 5; i++) + f3.hit(10 + i, 10); + expect("suppression off, masked pixels count again", passes(h, f3), + true); + hotClearMask(); + } + + std::printf("\n%s (%d failure(s))\n", gFail ? "FAILED" : "all tests passed", + gFail); + return gFail ? 1 : 0; +} diff --git a/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/test/jf_feeder.cpp b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/test/jf_feeder.cpp new file mode 100644 index 000000000..e430209cd --- /dev/null +++ b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/test/jf_feeder.cpp @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: LGPL-3.0-or-other +// =========================================================================== +// test/jf_feeder.cpp -- synthetic slsReceiver stand-in. +// +// Publishes the same ZMQ message pair that +// slsReceiver sends, so jf_online_display_gui can be exercised without hardware. +// +// Every 3rd frame carries NO signal; the other frames carry a 40-pixel +// cluster at ADC 300 on a baseline of 100. With +// ./jf_online_display_gui 400 127.0.0.1 30777 --stats --pede /nonexistent +// the gate threshold is 200, so exactly 2/3 of the frames must pass. +// +// make feeder +// ./test/jf_feeder tcp://*:30777 200 20 & +// ./jf_online_display_gui 400 127.0.0.1 30777 --stats --pede /nonexistent +// +// Subscriber mode (counts the frames republished by --pub): +// ./test/jf_feeder sub tcp://127.0.0.1:30778 8 +// +// 4th positional arg = initial silent period in seconds (idle-receiver test). +// =========================================================================== +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +int runSubscriber(const char *addr, int seconds) { + void *ctx = zmq_ctx_new(); + void *sub = zmq_socket(ctx, ZMQ_SUB); + int rcvtimeo = 200; + zmq_setsockopt(sub, ZMQ_RCVTIMEO, &rcvtimeo, sizeof(rcvtimeo)); + zmq_setsockopt(sub, ZMQ_SUBSCRIBE, "", 0); + if (zmq_connect(sub, addr) != 0) { + fprintf(stderr, "connect %s failed: %s\n", addr, + zmq_strerror(zmq_errno())); + return 1; + } + printf("subscriber: connected to %s for %d s\n", addr, seconds); + fflush(stdout); + + std::vector data(2 * 1024 * 1024); + char hdr[16384]; + int frames = 0, endMarkers = 0; + int dumped = 0; + const auto deadline = + std::chrono::steady_clock::now() + std::chrono::seconds(seconds); + while (std::chrono::steady_clock::now() < deadline) { + const int hn = zmq_recv(sub, hdr, sizeof(hdr) - 1, 0); + if (hn < 0) + continue; + hdr[hn] = '\0'; + const bool isData = (strstr(hdr, "\"data\":1") != nullptr); + if (isData) + frames++; + else if (strstr(hdr, "\"data\":0") != nullptr) + endMarkers++; + int more = 0; + size_t moreSize = sizeof(more); + zmq_getsockopt(sub, ZMQ_RCVMORE, &more, &moreSize); + int dn = -1; + if (more) + dn = zmq_recv(sub, data.data(), data.size(), 0); + if (dumped < 3) { + dumped++; + char head[400]; + snprintf(head, sizeof(head), "%.399s", hdr); + printf("subscriber: hdr_len=%d data_len=%d more=%d head=%s\n", hn, + dn, more, head); + fflush(stdout); + } + } + printf("subscriber: received %d data frames, %d end markers\n", frames, + endMarkers); + fflush(stdout); + zmq_close(sub); + zmq_ctx_term(ctx); + return frames > 0 ? 0 : 1; +} + +// Dual-interface stand-in: two PUB sockets on base and base+1, each sending +// one 1024x256 Y-slab of the same frame (the slsReceiver multi-UDP-interface +// layout). The bottom slab is filled with botVal, the top slab with topVal, +// so the assembled image can be verified from the gate counters. +// ./test/jf_feeder dual [delaySec] [botVal] +// [topVal] [topLagFrames] +int runDual(const char *baseStr, int nframes, int fps, int delayMs, int botVal, + int topVal, int topLag) { + const int base = atoi(baseStr); + const int W = 1024, H = 256; + const int bytes = W * H * 2; + + void *ctx = zmq_ctx_new(); + void *bot = zmq_socket(ctx, ZMQ_PUB); + void *top = zmq_socket(ctx, ZMQ_PUB); + int hwm = 1000; + zmq_setsockopt(bot, ZMQ_SNDHWM, &hwm, sizeof(hwm)); + zmq_setsockopt(top, ZMQ_SNDHWM, &hwm, sizeof(hwm)); + const std::string a0 = "tcp://*:" + std::to_string(base); + const std::string a1 = "tcp://*:" + std::to_string(base + 1); + if (zmq_bind(bot, a0.c_str()) != 0 || zmq_bind(top, a1.c_str()) != 0) { + fprintf(stderr, "dual bind failed: %s\n", zmq_strerror(zmq_errno())); + return 1; + } + printf("feeder: dual mode on %s and %s, %d frames at %d fps " + "(bot=%d top=%d topLag=%d)\n", + a0.c_str(), a1.c_str(), nframes, fps, botVal, topVal, topLag); + fflush(stdout); + + std::vector slab(W * H); + std::this_thread::sleep_for(std::chrono::milliseconds(delayMs)); + + auto sendSlab = [&](void *sock, int value, int frameNo) { + for (size_t i = 0; i < slab.size(); i++) + slab[i] = static_cast(value); + char hdr[512]; + // detSpec3 bits 8-11 carry the storage cell; cycle 0..15 so the SC + // filter can be exercised end to end + const unsigned detSpec3 = ((unsigned)(frameNo % 16) & 0xF) << 8; + const int hn = snprintf( + hdr, sizeof(hdr), + "{\"jsonversion\":5,\"data\":1,\"size\":%d," + "\"shape\":[%d,%d],\"frameNumber\":%d,\"frameIndex\":%d," + "\"detSpec1\":0,\"detSpec3\":%u}", + bytes, W, H, frameNo, frameNo, detSpec3); + zmq_send(sock, hdr, hn, ZMQ_SNDMORE); + zmq_send(sock, slab.data(), bytes, 0); + }; + + for (int f = 0; f < nframes; f++) { + sendSlab(bot, botVal, f); + const int topFrame = (f - topLag >= 0) ? f - topLag : f; + sendSlab(top, topVal, topFrame); + std::this_thread::sleep_for( + std::chrono::milliseconds(1000 / (fps > 0 ? fps : 1))); + } + const char *endhdr = "{\"jsonversion\":5,\"data\":0}"; + zmq_send(bot, endhdr, strlen(endhdr), 0); + zmq_send(top, endhdr, strlen(endhdr), 0); + printf("feeder: dual done\n"); + fflush(stdout); + zmq_close(bot); + zmq_close(top); + zmq_ctx_term(ctx); + return 0; +} + +} // namespace + +int main(int argc, char **argv) { + if (argc > 1 && std::string(argv[1]) == "sub") { + const char *addr = (argc > 2) ? argv[2] : "tcp://127.0.0.1:30778"; + const int seconds = (argc > 3) ? atoi(argv[3]) : 8; + return runSubscriber(addr, seconds); + } + if (argc > 1 && std::string(argv[1]) == "dual") { + return runDual(argc > 2 ? argv[2] : "30777", argc > 3 ? atoi(argv[3]) : 60, + argc > 4 ? atoi(argv[4]) : 25, + argc > 5 ? atoi(argv[5]) * 1000 : 800, + argc > 6 ? atoi(argv[6]) : 100, + argc > 7 ? atoi(argv[7]) : 150, + argc > 8 ? atoi(argv[8]) : 0); + } + + const char *addr = (argc > 1) ? argv[1] : "tcp://*:30777"; + const int nframes = (argc > 2) ? atoi(argv[2]) : 200; + const int fps = (argc > 3) ? atoi(argv[3]) : 20; + // optional 4th arg: stay bound and connected but SILENT for N seconds + // first (checks that an idle-but-alive receiver is not called "stopped") + const int delayMs = (argc > 4) ? atoi(argv[4]) * 1000 : 800; + const int NCH = 524288; + + void *ctx = zmq_ctx_new(); + void *pub = zmq_socket(ctx, ZMQ_PUB); + int hwm = 1000; + zmq_setsockopt(pub, ZMQ_SNDHWM, &hwm, sizeof(hwm)); + if (zmq_bind(pub, addr) != 0) { + fprintf(stderr, "bind %s failed: %s\n", addr, + zmq_strerror(zmq_errno())); + return 1; + } + printf("feeder: bound %s, sending %d frames at %d fps\n", addr, nframes, + fps); + fflush(stdout); + + std::vector frame(NCH); + std::this_thread::sleep_for(std::chrono::milliseconds(delayMs)); + + // detSpec1 packs the receiver ROI as 4 x uint16: xmin|xmax|ymin|ymax + const unsigned long long detSpec1 = ((unsigned long long)10u << 48) | + ((unsigned long long)400u << 32) | + ((unsigned long long)20u << 16) | 300u; + + for (int f = 0; f < nframes; f++) { + const bool signal = (f % 3) != 0; + for (int i = 0; i < NCH; i++) + frame[i] = 100; // baseline, gain 0 + if (signal) + for (int k = 0; k < 40; k++) + frame[1000 + k] = 300; // 40 px above the 200 threshold + + char hdr[512]; + const int hn = snprintf( + hdr, sizeof(hdr), + "{\"jsonversion\":5,\"data\":1,\"size\":%d,\"detSpec1\":%llu," + "\"frameIndex\":%d,\"bitmode\":1,\"fileIndex\":0," + "\"detshape\":[1,1],\"shape\":[1024,512],\"acqIndex\":%d," + "\"frameNumber\":%d,\"expLength\":0,\"packetNumber\":0," + "\"timestamp\":0,\"modId\":0,\"row\":0,\"column\":0,\"detSpec2\":0," + "\"detSpec3\":%u,\"detSpec4\":0,\"detType\":0,\"version\":0," + "\"flipRows\":0,\"quad\":0,\"completeImage\":1}", + NCH * 2, detSpec1, f, f, f, + ((unsigned)(f % 16) & 0xF) << 8); + zmq_send(pub, hdr, hn, ZMQ_SNDMORE); + zmq_send(pub, frame.data(), NCH * 2, 0); + std::this_thread::sleep_for( + std::chrono::milliseconds(1000 / (fps > 0 ? fps : 1))); + } + + const char *endhdr = "{\"jsonversion\":5,\"data\":0}"; + zmq_send(pub, endhdr, strlen(endhdr), 0); + printf("feeder: done\n"); + fflush(stdout); + zmq_close(pub); + zmq_ctx_term(ctx); + return 0; +} diff --git a/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/test/panel_map_test.cpp b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/test/panel_map_test.cpp new file mode 100644 index 000000000..ec35fc488 --- /dev/null +++ b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/test/panel_map_test.cpp @@ -0,0 +1,65 @@ +// Standalone JFSTRX check that the six-panel fill geometry is consistent with +// the strixel decoder: every real (non-dummy) mapped pixel must land inside a +// valid panel at an in-range local coordinate, and each panel must be fully +// covered by the decoder map (so no panel stays empty / has holes). +#include "online_jf.h" + +#include "jungfrauLGADStrixelsData_new.h" +#include "sls/sls_detector_defs.h" +#include "slsDetectorData.h" + +#include + +using header = sls::defs::sls_receiver_header; + +int main() { + initGeometry(); + jungfrauLGADStrixelsData *dec = new jungfrauLGADStrixelsData(); + const int g1_nrows = 165, g2_nrows = 320, g3_nrows = 476; + const int mc6g3_start = g1_nrows + g2_nrows + g3_nrows; + static const int panelStartY[kNumAdcPanels] = {0, 165, 485, + 1757, 1437, 961}; + long perPanel[kNumAdcPanels] = {0}; + long real = 0; + long dropped = 0; // real pixels outside the booked panel rectangles + bool ok = true; + for (int ipx = 0; ipx < nx; ++ipx) { + for (int ipy = 0; ipy < ny; ++ipy) { + if (dec->dataMap[ipy][ipx] == int(sizeof(header))) + continue; // dummy strixel + ++real; + const int group = dec->groupmap[ipy][ipx]; + if (group < 0 || group > 2) { + ok = false; + continue; + } + const int base = (ipy >= mc6g3_start) ? 3 : 0; + const int p = base + group; + const int lx = ipx; + const int ly = ipy - panelStartY[p]; + if (lx < 0 || lx >= kAdcPanelW[p] || ly < 0 || ly >= kAdcPanelH[p]) { + // the decoder maps a couple of extra right-edge columns for + // chip1 g2/g3 that the ROOT histograms (and these panels) do + // not book; dropping them matches the original display + ++dropped; + continue; + } + ++perPanel[p]; + } + } + delete dec; + + std::printf("mapped %d x %d, real pixels %ld, dropped edge pixels %ld\n", + nx, ny, real, dropped); + for (int p = 0; p < kNumAdcPanels; ++p) { + const long need = long(kAdcPanelW[p]) * kAdcPanelH[p]; + std::printf("panel %d: %dx%d filled %ld/%ld%s\n", p, kAdcPanelW[p], + kAdcPanelH[p], perPanel[p], need, + perPanel[p] == need ? "" : " <-- NOT FULL"); + if (perPanel[p] != need) + ok = false; + } + std::printf("%s\n", ok ? "PASS: six-panel geometry" + : "FAIL: six-panel geometry"); + return ok ? 0 : 1; +} diff --git a/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/test/pede_live_test.cpp b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/test/pede_live_test.cpp new file mode 100644 index 000000000..1ce04cf0f --- /dev/null +++ b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/test/pede_live_test.cpp @@ -0,0 +1,171 @@ +// Standalone check for the ROI writer (no hardware, no GUI): +// +// 1. a Pedestal corrected save follows the LIVE pedestal, not the snapshot +// taken at Start Save (one frame before, one after a mid-run swap); +// 2. the periodic checkpoint leaves a CLEAN pair on disk while the save is +// still running: the JSON sidecar is published atomically (.part -> +// rename) and the HDF5 is shrunk to exactly the frames written, so a +// copy taken at a checkpoint boundary is self-consistent. +// +// Not part of the build; compile by hand, see README "Testing without +// hardware". +#include "online_jf.h" + +#include "H5Cpp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace H5; + +static bool fileExists(const std::string &p) { + std::ifstream f(p); + return f.good(); +} + +static std::string readFile(const std::string &p) { + std::ifstream f(p, std::ios::binary); + std::ostringstream ss; + ss << f.rdbuf(); + return ss.str(); +} + +// Read the /data frame count and the first pixel of one slice from the LIVE +// file with the SWMR read flag - exactly what an external checker does while +// the save runs. +static bool readSlice(const std::string &path, hsize_t slice, hsize_t &frames, + float &px) { + hid_t f = H5Fopen(path.c_str(), H5F_ACC_RDONLY | H5F_ACC_SWMR_READ, + H5P_DEFAULT); + if (f < 0) + return false; + hid_t d = H5Dopen2(f, "/data", H5P_DEFAULT); + if (d < 0) { + H5Fclose(f); + return false; + } + hid_t s = H5Dget_space(d); + hsize_t dims[3] = {0, 0, 0}; + H5Sget_simple_extent_dims(s, dims, nullptr); + frames = dims[0]; + bool ok = false; + if (dims[0] > slice) { + hsize_t off[3] = {slice, 0, 0}; + hsize_t cnt[3] = {1, 1, 1}; + H5Sselect_hyperslab(s, H5S_SELECT_SET, off, nullptr, cnt, nullptr); + hid_t m = H5Screate_simple(3, cnt, nullptr); + ok = H5Dread(d, H5T_NATIVE_FLOAT, m, s, H5P_DEFAULT, &px) >= 0; + H5Sclose(m); + } + H5Sclose(s); + H5Dclose(d); + H5Fclose(f); + return ok; +} + +// wait until the periodic JSON says at least `saved` frames are committed +static std::string waitJson(const std::string &jsonPath, uint64_t saved) { + const std::string needle = "\"saved\": " + std::to_string(saved); + for (int i = 0; i < 100; i++) { + if (fileExists(jsonPath)) { + const std::string t = readFile(jsonPath); + if (t.find(needle) != std::string::npos) + return t; + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + return {}; +} + +int main() { + const int W = 1024, H = 4; + const hsize_t h = 4, w = 8; + + RoiSaver saver; + RoiSaver::StartInfo info; + info.x = 0; + info.y = 0; + info.w = static_cast(w); + info.h = static_cast(h); + info.dir = "."; + info.baseName = "pede_live_test"; + info.corrected = true; + info.checkpointMs = 300; // fast checkpoints for the test + + auto setA = std::make_shared(); + std::fill(setA->g0.begin(), setA->g0.end(), 100.0f); + PedestalStore::instance().install(setA); + info.peds = setA; // the snapshot the GUI would have taken at Start Save + + std::string err; + if (!saver.start(info, err)) { + std::printf("FAIL start: %s\n", err.c_str()); + return 1; + } + const std::string h5 = saver.status().h5Path; + const std::string js = saver.status().jsonPath; + + std::vector frame(static_cast(W) * H, 1000); + SavedFrameHeader h1; + h1.frameNumber = 1; + saver.offer(frame.data(), W, H, h1); + + // --- checkpoint 1: exactly one frame, corrected with pedestal A ------- + const std::string j1 = waitJson(js, 1); + if (j1.empty()) { + std::printf("FAIL: no periodic JSON with 1 frame\n"); + return 2; + } + const bool liveMid = j1.find("\"complete\": false") != std::string::npos; + hsize_t n1 = 0; + float p1 = -1.0f; + const bool live1 = readSlice(h5, 0, n1, p1); // live file, SWMR read + std::printf("checkpoint1: periodic json (complete:false)=%s live swmr " + "read=%s h5 extent=%llu px=%.1f (expect true, true, 1, 900)\n", + liveMid ? "true" : "false", live1 ? "true" : "false", + (unsigned long long)n1, p1); + + // --- the tracker swaps the pedestal mid-run --------------------------- + auto setB = std::make_shared(); + std::fill(setB->g0.begin(), setB->g0.end(), 150.0f); + PedestalStore::instance().install(setB); + SavedFrameHeader h2; + h2.frameNumber = 2; + saver.offer(frame.data(), W, H, h2); + + const std::string j2 = waitJson(js, 2); + if (j2.empty()) { + std::printf("FAIL: no periodic JSON with 2 frames\n"); + return 3; + } + hsize_t n2 = 0; + float p2 = -1.0f; + const bool live2 = readSlice(h5, 1, n2, p2); // live file, SWMR read + std::printf("checkpoint2: live swmr read=%s h5 extent=%llu slice1 px=%.1f " + "(expect true, 2, 850)\n", + live2 ? "true" : "false", (unsigned long long)n2, p2); + + const bool noPart = !fileExists(js + ".part"); + + saver.requestStop("test"); + saver.finish(); + const std::string jf = readFile(js); + const bool finalComplete = + jf.find("\"complete\": true") != std::string::npos; + + const bool ok = liveMid && live1 && (n1 == 1) && (p1 == 900.0f) && live2 && + (n2 == 2) && (p2 == 850.0f) && noPart && finalComplete; + std::printf("final: complete=%s no .part left=%s\n", + finalComplete ? "true" : "false", noPart ? "true" : "false"); + std::printf("%s\n", + ok ? "PASS: live pedestal + periodic clean checkpoint" + : "FAIL"); + return ok ? 0 : 4; +} diff --git a/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/test/xclick.c b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/test/xclick.c new file mode 100644 index 000000000..62c4053f3 --- /dev/null +++ b/slsDetectorCalibration/jungfrauExecutables/jf_online_display_gui/test/xclick.c @@ -0,0 +1,69 @@ +// Minimal XTest driver: xclick click | key +#include +#include +#include +#include +#include +extern int XTestFakeMotionEvent(Display *, int, int, int, unsigned long); +extern int XTestFakeButtonEvent(Display *, unsigned int, int, unsigned long); +extern int XTestFakeKeyEvent(Display *, unsigned int, int, unsigned long); +int main(int argc, char **argv) { + Display *d = XOpenDisplay(argc > 1 ? argv[1] : NULL); + if (!d) { fprintf(stderr, "no display\n"); return 1; } + if (argc > 3 && strcmp(argv[2], "click") == 0) { + int x = atoi(argv[3]), y = atoi(argv[4]); + XTestFakeMotionEvent(d, -1, x, y, 0); + XTestFakeButtonEvent(d, 1, True, 0); + XTestFakeButtonEvent(d, 1, False, 0); + } else if (argc > 3 && strcmp(argv[2], "press") == 0) { + XTestFakeMotionEvent(d, -1, atoi(argv[3]), atoi(argv[4]), 0); + XTestFakeButtonEvent(d, 1, True, 0); + } else if (argc > 3 && strcmp(argv[2], "move") == 0) { + XTestFakeMotionEvent(d, -1, atoi(argv[3]), atoi(argv[4]), 0); + } else if (argc > 2 && strcmp(argv[2], "release") == 0) { + XTestFakeButtonEvent(d, 1, False, 0); + } else if (argc > 2 && strcmp(argv[2], "raise") == 0) { + const char *want = argc > 3 ? argv[3] : ""; + Window root = DefaultRootWindow(d), r2, par, *kids = NULL; + unsigned n = 0; + if (XQueryTree(d, root, &r2, &par, &kids, &n)) { + for (unsigned i = 0; i < n; i++) { + char *nm = NULL; + if (XFetchName(d, kids[i], &nm) && nm) { + if (strstr(nm, want)) { + XRaiseWindow(d, kids[i]); + XSetInputFocus(d, kids[i], RevertToParent, CurrentTime); + printf("raised: %s\n", nm); + } + XFree(nm); + } + } + if (kids) XFree(kids); + } + } else if (argc > 4 && strcmp(argv[2], "movewin") == 0) { + const char *want = argv[3]; + Window root = DefaultRootWindow(d), r2, par, *kids = NULL; + unsigned n = 0; + if (XQueryTree(d, root, &r2, &par, &kids, &n)) { + for (unsigned i = 0; i < n; i++) { + char *nm = NULL; + if (XFetchName(d, kids[i], &nm) && nm) { + if (strstr(nm, want)) { + XMoveWindow(d, kids[i], atoi(argv[4]), atoi(argv[5])); + printf("moved: %s\n", nm); + } + XFree(nm); + } + } + if (kids) XFree(kids); + } + } else if (argc > 2 && strcmp(argv[2], "key") == 0) { + KeySym ks = XStringToKeysym(argc > 3 ? argv[3] : "Return"); + KeyCode kc = XKeysymToKeycode(d, ks); + XTestFakeKeyEvent(d, kc, True, 0); + XTestFakeKeyEvent(d, kc, False, 0); + } + XFlush(d); + XCloseDisplay(d); + return 0; +}