Files
Jungfraujoch/broker/JFJochStateMachine.cpp
T
leonarski_fandClaude Opus 5 5f838bdb26 Broker: refuse a run the writer cannot write before arming the detector
A writer refusing to overwrite an existing file was reported badly, and the
earlier fix only covered half of it. The commit message of a6a715703 states the
assumption that broke: "SendStartMessage precedes every std::async in the
receiver constructor". That is true of JFJochReceiverFPGA, where the start
message goes out from the constructor body, and false of JFJochReceiverLite,
where it goes out from MeasurementThread - launched by std::async from the
constructor, which waits only on data_analysis_started. On the DECTRIS path the
writer's refusal therefore cannot reach services.Start(); it arrives from
services.Stop(), i.e. the second try-block of the state machine's
MeasurementThread, which never touched start_exception. /wait_till_done answered
200 for a run that never wrote anything.

Two changes, in front of the problem and behind it.

In front: a PREFLIGHT frame (TCP protocol version 3 -> 4), sent to every writer
before the receiver is built and before the detector is armed. It carries the
same CBOR start message a START does and asks one question: could this run be
written? The writer runs FileWriter::Preflight - the first three statements of
the FileWriter constructor, stopping short of the only one that creates
anything - and answers with an ACK. Shared code, so a pre-flight that passes
cannot be contradicted by the start behind it. A refusal is not fatal: nothing
was started, the connection stays usable, and above all no series is left armed
waiting for a trigger that will not come. Both ends already reject a frame of
another protocol version, so the bump costs no compatibility.

The check now covers the data files, not only the master. A stale
_data_000001.h5 used to sail through and fail when the file was renamed into
place at the end of the run, after the whole dataset had been collected. The
writer holding the master file checks for all of them - they share a directory
by construction, the master linking them by relative name - so the siblings do
not race each other statting files the others are creating.

Behind: /wait_till_done maps an Idle state to 200 or 500 by the severity of the
end-of-run message, so a collection that lost packets, or whose writer could not
write, is no longer indistinguishable from a good one to a script. A cancelled
collection is not affected - MeasurementThread checks the cancel before it looks
at the completeness of the data, and reports it with Info severity - and a
warning is still a 200. start_exception is untouched.

The PREFLIGHT payload is built from FillMessage alone, which is what keeps it
small: the per-pixel arrays are added by SendStartMessage and are not in it.
Measured on a JUNGFRAU 9M, 1.4 kB against 543 kB for the start message. It is
deliberately the whole of FillMessage rather than the fields the check reads
today - a hand-picked subset would answer for a different set of file names the
day the check learns to read one more field, and would do it silently.

Three defects found on the way:

TCPImagePuller republished a PREFLIGHT as a real start message, announcing to
downstream consumers a run that was not going to happen.

TCPStreamPusher::run_number was a plain uint64_t written by the control thread
and read by every ACK thread; Preflight adds a second writer to it.

StreamWriter::CollectImages reprocessed the last frame whenever WaitForImage
returned false - run is only tested at the top of the loop - so an abort wrote
the last image a second time. Pre-existing; the new branch made it visible.

Verified against a running broker on the local configuration: a colliding master
file and a colliding data file are both refused by /start, /wait_until_running
and /wait_till_done with the writer's own message, and the log shows the receiver
was never started for either; a cancelled collection still answers 200.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WmryXe8ASbNi632sUMfsa
2026-08-27 12:20:29 +02:00

1223 lines
47 KiB
C++

// SPDX-FileCopyrightText: 2024 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <thread>
#include "JFJochStateMachine.h"
#include "../preview/JFJochTIFF.h"
#include "../common/CUDAWrapper.h"
#include "../common/GitInfo.h"
#include "../common/JFJochException.h"
JFJochStateMachine::JFJochStateMachine(const DiffractionExperiment& in_experiment,
JFJochServices &in_services,
Logger &in_logger,
const SpotFindingSettings &spot_finding_settings)
: logger(in_logger),
services(in_services),
experiment(in_experiment),
pixel_mask(experiment),
current_detector_setup(0),
data_processing_settings(spot_finding_settings),
pixel_mask_statistics({0, 0, 0}),
gpu_count(get_gpu_count()) {
#ifndef JFJOCH_USE_FFTW
indexing_possible = (get_gpu_count() > 0);
if (!indexing_possible)
data_processing_settings.indexing = false;
#else
data_processing_settings.indexing = true;
#endif
SuppressTIFFErrors();
}
bool JFJochStateMachine::ImportPedestalG0(const JFJochReceiverOutput &receiver_output) {
if (receiver_output.pedestal_result.empty())
return false;
if (receiver_output.pedestal_result.size() != experiment.GetModulesNum() * experiment.GetStorageCellNumber())
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Mismatch in pedestal output");
size_t gain_level = experiment.IsFixedGainG1() ? 1 : 0;
for (int s = 0; s < experiment.GetStorageCellNumber(); s++) {
for (int module = 0; module < experiment.GetModulesNum(); module++)
calibration->Pedestal(module, gain_level, s)
= receiver_output.pedestal_result[module + s * experiment.GetModulesNum()];
}
SetCalibrationStatistics(calibration->GetModuleStatistics());
return true;
}
bool JFJochStateMachine::ImportPedestalG1G2(const JFJochReceiverOutput &receiver_output, size_t gain_level,
size_t storage_cell) {
if (receiver_output.pedestal_result.empty())
return false;
if (receiver_output.pedestal_result.size() != experiment.GetModulesNum())
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Mismatch in pedestal output");
for (int i = 0; i < receiver_output.pedestal_result.size(); i++)
calibration->Pedestal(i, gain_level, storage_cell) = receiver_output.pedestal_result[i];
SetCalibrationStatistics(calibration->GetModuleStatistics());
return true;
}
bool JFJochStateMachine::CalibrateJUNGFRAU(std::unique_lock<std::mutex> &ul) {
if (!gain_calibration.empty()) {
if (gain_calibration.size() != experiment.GetModulesNum())
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Mismatch in gain files number");
for (int i = 0; i < gain_calibration.size(); i++)
calibration->GainCalibration(i) = gain_calibration[i];
}
// Abandon the sequence on the first failure. Collecting G1 on top of a G0 that was never
// measured only produces a calibration that looks complete.
if (!TakePedestalInternalG0(ul))
return false;
if (!experiment.IsFixedGainG1()) {
for (int i = 0; i < experiment.GetStorageCellNumber(); i++) {
if (!TakePedestalInternalG1(ul, i))
return false;
if (!TakePedestalInternalG2(ul, i))
return false;
}
}
pixel_mask.LoadDetectorBadPixelMask(experiment, calibration.get());
return true;
}
void JFJochStateMachine::CalibrateDetector(std::unique_lock<std::mutex> ul) {
cancel_sequence = false;
pixel_mask = PixelMask(experiment);
UpdatePixelMaskStatistics(pixel_mask.GetStatistics());
logger.Info("Calibration sequence started");
bool calibrated;
try {
if (experiment.GetDetectorType() == DetectorType::EIGER) {
// PSI EIGER - only reset calibration
calibration.reset();
calibrated = true;
} else if (experiment.GetDetectorType() == DetectorType::DECTRIS) {
// DECTRIS - take dark data for mask
calibration.reset();
calibrated = TakeDarkMaskInternal(ul);
} else {
// PSI JUNGFRAU - take pedestal
calibration = std::make_unique<JFCalibration>(experiment);
calibrated = CalibrateJUNGFRAU(ul);
}
// Update pixel mask statistics
UpdatePixelMaskStatistics(pixel_mask.GetStatistics());
// configure detector for standard operation - only worth doing if there is a calibration to
// operate with, and a cancelled sequence has left the detector mid-abort anyway
if (calibrated)
services.ConfigureDetector(experiment);
} catch (const std::exception &e) {
logger.Error("Calibration sequence error {}", e.what());
// The calibration is in an undefined state, so the detector has to be initialized again.
SetState(JFJochState::Inactive, e.what(), BrokerStatus::MessageSeverity::Error);
c.notify_all(); // ul unlocks on the way out
throw;
}
// The steps above report a cancellation or a failure through SetState and return false; that
// must not be overwritten with success here.
if (calibrated) {
SetState(JFJochState::Idle, "Calibration sequence done", BrokerStatus::MessageSeverity::Success);
logger.Info("Calibration sequence done");
}
ul.unlock(); // Notify all outside of mutex
c.notify_all();
}
bool JFJochStateMachine::TakeDarkMaskInternal(std::unique_lock<std::mutex> &ul) {
if (cancel_sequence) {
SetState(JFJochState::Inactive,
"Mask sequence cancelled",
BrokerStatus::MessageSeverity::Error);
return false;
}
services.LoadDetectorPixelMask(pixel_mask);
if (experiment.GetDarkMaskNumberOfFrames() == 0)
return true;
DiffractionExperiment local_experiment(experiment);
local_experiment.Mode(DetectorMode::DarkMask);
SetState(JFJochState::Calibration, "Dark sequence for mask calculation", BrokerStatus::MessageSeverity::Info);
services.ConfigureDetector(local_experiment);
services.Start(local_experiment, pixel_mask, nullptr);
services.Trigger();
ul.unlock();
// Allow to cancel/abort during the mask data collection
auto mask_output = services.Stop();
ul.lock();
if (mask_output.receiver_output.dark_mask_result.size() != local_experiment.GetPixelsNum()) {
SetState(JFJochState::Inactive, "Mask not collected properly", BrokerStatus::MessageSeverity::Error);
return false;
}
pixel_mask.LoadDarkBadPixelMask(local_experiment, mask_output.receiver_output.dark_mask_result);
SetState(JFJochState::Idle);
return true;
}
bool JFJochStateMachine::TakePedestalInternalG0(std::unique_lock<std::mutex> &ul) {
DiffractionExperiment local_experiment(experiment);
std::string message;
if (local_experiment.IsFixedGainG1()) {
local_experiment.Mode(DetectorMode::PedestalG1);
message = "Pedestal G1";
} else {
local_experiment.Mode(DetectorMode::PedestalG0);
message = "Pedestal G0";
}
if (local_experiment.GetStorageCellNumber() == 1)
local_experiment.StorageCellStart(15);
else
local_experiment.StorageCellStart(0);
if (cancel_sequence) {
SetState(JFJochState::Inactive,
"Pedestal sequence cancelled",
BrokerStatus::MessageSeverity::Error);
return false;
}
if (local_experiment.GetPedestalG0Frames() == 0)
return true;
SetState(JFJochState::Calibration, message, BrokerStatus::MessageSeverity::Info);
services.ConfigureDetector(local_experiment);
services.Start(local_experiment, pixel_mask, calibration.get());
services.Trigger();
ul.unlock();
// Allow to cancel/abort during the pedestal data collection
// Must ensure that while state is Pedestal, nothing can take lock for longer time, to avoid deadlock
auto pedestal_output = services.Stop();
ul.lock();
if (!ImportPedestalG0(pedestal_output.receiver_output)) {
SetState(JFJochState::Inactive,
"Pedestal not collected properly",
BrokerStatus::MessageSeverity::Error);
return false;
}
SetState(JFJochState::Idle);
return true;
}
bool JFJochStateMachine::TakePedestalInternalG1(std::unique_lock<std::mutex> &ul, int32_t storage_cell) {
DiffractionExperiment local_experiment(experiment);
local_experiment.Mode(DetectorMode::PedestalG1);
if (local_experiment.GetStorageCellNumber() == 2)
local_experiment.StorageCellStart((storage_cell + 15) % 16); // one previous
else
local_experiment.StorageCellStart(15);
if (cancel_sequence) {
SetState(JFJochState::Inactive,
"Pedestal sequence cancelled",
BrokerStatus::MessageSeverity::Error);
return false;
}
if (local_experiment.GetPedestalG1Frames() == 0)
return true;
SetState(JFJochState::Calibration,
"Pedestal G1 SC" + std::to_string(storage_cell),
BrokerStatus::MessageSeverity::Info);
services.ConfigureDetector(local_experiment);
services.Start(local_experiment, pixel_mask, calibration.get());
services.Trigger();
ul.unlock();
// Allow to cancel/abort during the pedestal data collection
// Must ensure that while state is Pedestal, nothing can take lock for longer time, to avoid deadlock
auto pedestal_output = services.Stop();
ul.lock();
if (!ImportPedestalG1G2(pedestal_output.receiver_output, 1, storage_cell)) {
SetState(JFJochState::Inactive,
"Pedestal not collected properly",
BrokerStatus::MessageSeverity::Error);
return false;
}
return true;
}
bool JFJochStateMachine::TakePedestalInternalG2(std::unique_lock<std::mutex> &ul, int32_t storage_cell) {
DiffractionExperiment local_experiment(experiment);
local_experiment.Mode(DetectorMode::PedestalG2);
if (local_experiment.GetStorageCellNumber() == 2)
local_experiment.StorageCellStart((storage_cell + 15) % 16); // one previous
else
local_experiment.StorageCellStart(15);
if (cancel_sequence) {
SetState(JFJochState::Inactive,
"Pedestal sequence cancelled",
BrokerStatus::MessageSeverity::Error);
return false;
}
if (local_experiment.GetPedestalG2Frames() == 0)
return true;
SetState(JFJochState::Calibration,
"Pedestal G2 SC" + std::to_string(storage_cell),
BrokerStatus::MessageSeverity::Info);
services.ConfigureDetector(local_experiment);
services.Start(local_experiment, pixel_mask, calibration.get());
services.Trigger();
ul.unlock();
// Allow to cancel/abort during the pedestal data collection
// Must ensure that while state is Pedestal, nothing can take lock for longer time, to avoid deadlock
auto pedestal_output = services.Stop();
ul.lock();
if (!ImportPedestalG1G2(pedestal_output.receiver_output, 2, storage_cell)) {
SetState(JFJochState::Inactive,
"Pedestal not collected properly",
BrokerStatus::MessageSeverity::Error);
return false;
}
return true;
}
void JFJochStateMachine::Initialize() {
std::unique_lock ul(m);
if (IsRunning())
throw WrongDAQStateException("Cannot initialize during measurement");
if (detector_setup.empty())
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Detector information not provided");
ResetError(); // Clear error, we don't care what was it
start_exception = nullptr; // Re-initialising discards a pending start failure
logger.Info("Initialize");
SetState(JFJochState::Busy, "Configuring indexing threads", BrokerStatus::MessageSeverity::Info);
try {
services.SetupIndexing(experiment.GetIndexingSettings());
} catch (const JFJochException &e) {
SetState(JFJochState::Error, e.what(), BrokerStatus::MessageSeverity::Error);
throw;
}
SetState(JFJochState::Busy, "Configuring detector", BrokerStatus::MessageSeverity::Info);
scan_result = {}; // Clear scan result
measurement = std::async(std::launch::async, &JFJochStateMachine::InitializeThread, this, std::move(ul));
}
void JFJochStateMachine::Pedestal() {
std::unique_lock ul(m);
if (state != JFJochState::Idle)
throw WrongDAQStateException("Must be idle to take pedestal");
start_exception = nullptr; // A new operation supersedes a pending start failure
SetState(JFJochState::Busy, "Updating calibration", BrokerStatus::MessageSeverity::Info);
measurement = std::async(std::launch::async, &JFJochStateMachine::CalibrateDetector, this, std::move(ul));
}
void JFJochStateMachine::InitializeThread(std::unique_lock<std::mutex> ul) {
try {
// services.On can potentially take a lot of time, so better to unlock main mutex
// Since On might modify the experiment (reads DECTRIS configuration), one has to have a local copy for unlocked part
DiffractionExperiment local_experiment(experiment);
if (state != JFJochState::Busy)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"State must be busy for safe operation");
ul.unlock();
services.On(local_experiment);
ul.lock();
experiment = local_experiment;
detector_setup[current_detector_setup] = experiment.GetDetectorSetup();
} catch (const std::exception &e) {
logger.Error("Initialize error {}", e.what());
SetState(JFJochState::Error, e.what(), BrokerStatus::MessageSeverity::Error);
// Wake anyone in WaitTillNotBusy/WaitTillMeasurementDone - the state has left Busy, and
// without this they sleep out their whole timeout before noticing.
c.notify_all(); // ul unlocks on the way out
throw;
}
CalibrateDetector(std::move(ul));
}
void JFJochStateMachine::Trigger() {
services.Trigger();
}
void JFJochStateMachine::Start(const DatasetSettings &settings, bool async) {
std::unique_lock ul(m);
if (state != JFJochState::Idle)
throw WrongDAQStateException("Must be idle to start measurement");
if (measurement.valid())
measurement.get(); // In case measurement was running - clear thread
// Clear before ImportDatasetSettings, which can throw: a rejected /start must not leave the
// previous run's failure behind for the next wait call to report.
start_exception = nullptr;
experiment.ImportDatasetSettings(settings);
cancel_sequence = false;
if (experiment.GetStorageCellNumber() == 1)
experiment.StorageCellStart(15);
else
experiment.StorageCellStart(0);
experiment.IncrementRunNumber();
SetState(JFJochState::Busy, "Preparing measurement", BrokerStatus::MessageSeverity::Info);
measurement = std::async(std::launch::async, &JFJochStateMachine::MeasurementThread, this);
if (!async) {
c.wait(ul, [&]() { return state != JFJochState::Busy; });
// A synchronous start propagates the failure to the caller. The state has already been set
// by MeasurementThread (Idle for an ordinary failure, Error for a critical detector fault).
// start_exception is left in place - the next Start() or Initialize() clears it - so that a
// wait call made afterwards reports the same failure instead of an apparent timeout.
if (start_exception)
std::rethrow_exception(start_exception);
}
}
BrokerStatus JFJochStateMachine::WaitTillNotBusy(std::chrono::milliseconds timeout) {
std::unique_lock ul(m);
c.wait_for(ul, timeout, [&]() { return state != JFJochState::Busy; });
// An asynchronous start reports its failure here, since /start itself returned before the
// measurement thread ran. Without this the state is plain Idle and the caller cannot tell a
// failed start from a timeout. rethrow_exception does not consume the exception_ptr, so
// repeated calls all report the same failure.
if (start_exception)
std::rethrow_exception(start_exception);
return GetStatus();
}
void JFJochStateMachine::UpdatePixelMaskStatistics(const PixelMaskStatistics &input) {
std::unique_lock ul(pixel_mask_statistics_mutex);
pixel_mask_statistics = input;
}
PixelMaskStatistics JFJochStateMachine::GetPixelMaskStatistics() const {
std::unique_lock ul(pixel_mask_statistics_mutex);
return pixel_mask_statistics;
}
void JFJochStateMachine::MeasurementThread() {
try {
// Before anything is started: ask the writer whether it could write this run. A run that
// would be refused for a file already there, or a directory that cannot be made, is
// refused now - with no receiver built and, above all, no detector armed, so there is
// nothing to tear down and no series left waiting for a trigger that will not come.
services.Preflight(experiment);
services.SetSpotFindingSettings(GetSpotFindingSettings());
services.Start(experiment, pixel_mask, calibration.get());
{
std::unique_lock ul(m);
SetState(JFJochState::Measuring, "Measuring ...", BrokerStatus::MessageSeverity::Info);
}
c.notify_all();
} catch (const JFJochCriticalException &e) {
// Detector left in an undefined state - force re-initialisation via the Error state.
logger.Error("Critical error starting measurement: {}", e.what());
{
std::unique_lock ul(m);
SetState(JFJochState::Error, e.what(), BrokerStatus::MessageSeverity::Error);
start_exception = std::current_exception();
}
c.notify_all();
return;
} catch (const std::exception &e) {
// Ordinary acquisition failure - the detector is still configured/calibrated, so return to
// Idle and let the user retry without re-initialising. services.Start has already stopped the
// receiver it launched.
logger.Error("Error starting measurement: {}", e.what());
{
std::unique_lock ul(m);
SetState(JFJochState::Idle, e.what(), BrokerStatus::MessageSeverity::Error);
start_exception = std::current_exception();
}
c.notify_all();
return;
}
try {
auto tmp_output = services.Stop();
{
std::unique_lock ul(m);
scan_result = tmp_output.receiver_output.scan_result;
auto image_mean_time = tmp_output.receiver_output.processing_time;
logger.Info("Per-image mean processing time (microseconds): compression {:.0f} preprocess {:.0f} azint {:.0f} spot finding {:.0f} indexing {:.0f} refinement {:.0f} indexing analysis {:.0f} prediction {:.0f} integration {:.0f} total {:.0f}",
image_mean_time.compression * 1e6,
image_mean_time.preprocessing * 1e6,
image_mean_time.azint * 1e6,
image_mean_time.spot_finding * 1e6,
image_mean_time.indexing * 1e6,
image_mean_time.refinement * 1e6,
image_mean_time.indexing_analysis * 1e6,
image_mean_time.bragg_prediction * 1e6,
image_mean_time.integration * 1e6,
image_mean_time.processing * 1e6);
// Priority order matters. A cancel is checked first (it legitimately leaves efficiency < 1),
// then the hard errors (missing packets, truncated writer output). The queue-full warning is
// only the primary status when the run otherwise succeeded - it must not mask a real error
// by downgrading an incomplete/truncated dataset to a "reduce frame rate" warning.
if (tmp_output.receiver_output.status.cancelled)
SetState(JFJochState::Idle,
"Data collection cancelled",
BrokerStatus::MessageSeverity::Info);
else if (tmp_output.receiver_output.efficiency != 1.0)
SetState(JFJochState::Idle,
"Missing packets in data collection; reduce frame rate",
BrokerStatus::MessageSeverity::Error);
else if (!tmp_output.receiver_output.writer_err.empty())
SetState(JFJochState::Idle,
"Writer error, written data may be incomplete: " + tmp_output.receiver_output.writer_err,
BrokerStatus::MessageSeverity::Error);
else if (tmp_output.receiver_output.writer_queue_full_warning)
SetState(JFJochState::Idle,
"Stream receiver (writer or downstream analysis) cannot cope with data; reduce frame rate",
BrokerStatus::MessageSeverity::Warning);
else
SetState(JFJochState::Idle,
"Data collection without problems",
BrokerStatus::MessageSeverity::Success);
}
} catch (const JFJochCriticalException &e) {
// Detector faulted during the run - it needs re-initialisation, so go to the Error state.
logger.Error("Critical error finishing measurement: {}", e.what());
std::unique_lock ul(m);
SetState(JFJochState::Error, e.what(), BrokerStatus::MessageSeverity::Error);
} catch (const std::exception &e) {
// Receiver/writer problem - the data may be incomplete, but the detector is still usable, so
// return to Idle rather than forcing re-initialisation.
logger.Error("Error finishing measurement: {}", e.what());
std::unique_lock ul(m);
SetState(JFJochState::Idle, e.what(), BrokerStatus::MessageSeverity::Error);
}
c.notify_all();
}
void JFJochStateMachine::Cancel() {
// This is inconsistency in naming - need to solve later
std::unique_lock ul(m);
if ((state == JFJochState::Calibration) || (state == JFJochState::Measuring)) {
services.Cancel();
cancel_sequence = true;
}
}
void JFJochStateMachine::DebugOnly_SetState(JFJochState in_state,
const std::optional<std::string> &message,
BrokerStatus::MessageSeverity message_severity) {
std::unique_lock ul(m);
SetState(in_state, message, message_severity);
}
void JFJochStateMachine::Deactivate() {
std::unique_lock ul(m);
// Powering the detector off holds m for the whole sequence, so it must not be started while a
// measurement, calibration or initialisation thread is still live - those re-acquire m to
// finish, and waiting for them here would deadlock the whole control plane.
if (IsRunning())
throw WrongDAQStateException("Cannot deactivate while the detector is busy");
// Reap the finished thread, but do not let a failure it stored stop the power-off: the state is
// Error precisely because that run failed, and leaving the detector powered is worse than
// losing an error message that was already reported when it happened.
if (measurement.valid()) {
try {
measurement.get();
} catch (const std::exception &e) {
logger.Warning("Deactivating after an earlier failure: {}", e.what());
}
}
try {
services.Off();
SetState(JFJochState::Inactive,
"Detector safe to turn off",
BrokerStatus::MessageSeverity::Info);
} catch (const std::exception &e) {
SetState(JFJochState::Error,
e.what(),
BrokerStatus::MessageSeverity::Error);
throw;
}
}
JFJochStateMachine::~JFJochStateMachine() {
ResetError();
}
std::optional<MeasurementStatistics> JFJochStateMachine::GetMeasurementStatistics() const {
MeasurementStatistics tmp{};
tmp.file_prefix = experiment.GetFilePrefix();
tmp.run_number = experiment.GetRunNumber();
tmp.experiment_group = experiment.GetExperimentGroup();
tmp.detector_width = experiment.GetXPixelsNum();
tmp.detector_height = experiment.GetYPixelsNum();
tmp.detector_pixel_depth = experiment.GetByteDepthImage();
tmp.images_expected = experiment.GetImageNum();
tmp.unit_cell = experiment.GetUnitCellString();
auto rcv_status = services.GetReceiverStatus();
if (rcv_status) {
tmp.compression_ratio = rcv_status->compressed_ratio;
tmp.images_collected = rcv_status->images_collected;
tmp.images_sent = rcv_status->images_sent;
tmp.images_skipped = rcv_status->images_skipped;
tmp.cancelled = rcv_status->cancelled;
tmp.max_image_number_sent = rcv_status->max_image_number_sent;
tmp.max_receive_delay = rcv_status->max_receive_delay;
tmp.indexing_rate = rcv_status->indexing_rate;
tmp.bkg_estimate = rcv_status->bkg_estimate;
tmp.collection_efficiency = rcv_status->efficiency;
tmp.error_pixels = rcv_status->error_pixels;
tmp.saturated_pixels = rcv_status->saturated_pixels;
tmp.roi_beam_sum = rcv_status->roi_beam_sum;
tmp.roi_beam_npixel = rcv_status->roi_beam_npixel;
tmp.images_written = rcv_status->images_written;
}
return tmp;
}
std::vector<JFCalibrationModuleStatistics> JFJochStateMachine::GetCalibrationStatistics() const {
std::unique_lock ul(calibration_statistics_mutex);
return calibration_statistics;
}
void JFJochStateMachine::SetCalibrationStatistics(const std::vector<JFCalibrationModuleStatistics> &input) {
std::unique_lock ul(calibration_statistics_mutex);
calibration_statistics = input;
}
DetectorSettings JFJochStateMachine::GetDetectorSettings() const {
std::unique_lock ul(experiment_detector_settings_mutex);
return experiment.GetDetectorSettings();
}
bool JFJochStateMachine::ImportDetectorSettings(const DetectorSettings &input) {
std::unique_lock ul(experiment_detector_settings_mutex);
// For JUNGFRAU detector, if detector settings changes key parameters
// need to recalibrate the detector
bool recalib = input.NeedsJUNGFRAURecalibration(experiment.GetDetectorSettings())
&& experiment.GetDetectorType() == DetectorType::JUNGFRAU;
experiment.ImportDetectorSettings(input);
return recalib;
}
void JFJochStateMachine::LoadDetectorSettings(const DetectorSettings &settings) {
std::unique_lock ul(m);
switch (state) {
case JFJochState::Inactive:
case JFJochState::Error:
ImportDetectorSettings(settings);
break;
case JFJochState::Idle:
if (ImportDetectorSettings(settings)) {
start_exception = nullptr; // A new operation supersedes a pending start failure
SetState(JFJochState::Busy, "Loading settings", BrokerStatus::MessageSeverity::Info);
measurement = std::async(std::launch::async, &JFJochStateMachine::CalibrateDetector, this, std::move(ul));
} else {
try {
SetState(JFJochState::Busy, "Configure detector", BrokerStatus::MessageSeverity::Info);
services.ConfigureDetector(experiment);
SetState(JFJochState::Idle, "Detector configured", BrokerStatus::MessageSeverity::Info);
} catch (const std::exception &e) {
logger.Error("Detector configuration error {}", e.what());
SetState(JFJochState::Error, e.what(), BrokerStatus::MessageSeverity::Error);
}
}
break;
case JFJochState::Measuring:
case JFJochState::Busy:
case JFJochState::Calibration:
throw WrongDAQStateException("Cannot change detector settings during data collection");
}
}
DiffractionExperiment JFJochStateMachine::Experiment() {
return experiment;
}
BrokerStatus JFJochStateMachine::GetStatus() const {
std::unique_lock ul(broker_status_mutex);
BrokerStatus ret = broker_status;
ret.progress = services.GetReceiverProgress();
ret.gpu_count = gpu_count;
ret.broker_version = jfjoch_version();
return ret;
}
void JFJochStateMachine::SetState(JFJochState curr_state,
const std::optional<std::string> &message,
BrokerStatus::MessageSeverity message_severity) {
std::unique_lock ul(broker_status_mutex);
state = curr_state;
broker_status = BrokerStatus{
.state = curr_state,
.message = message,
.message_severity = message_severity
};
}
MultiLinePlot JFJochStateMachine::GetPlots(const PlotRequest &request) const {
return services.GetPlots(request);
}
void JFJochStateMachine::GetPlotRaw(std::vector<float> &v, PlotType type, const std::string &roi) const {
services.GetPlotRaw(v, type, roi);
}
void JFJochStateMachine::SetSpotFindingSettings(const SpotFindingSettings &settings) {
std::unique_lock ul(data_processing_settings_mutex);
DiffractionExperiment::CheckDataProcessingSettings(settings);
// The adaptive threshold is a property of the software spot finder, which only the DECTRIS
// (SIMPLON) workflow runs - JUNGFRAU and EIGER find spots on the FPGA, at its own fixed
// threshold. Refuse it there rather than accept it and do nothing: a silently ignored detection
// setting is indistinguishable from one that had no effect on the data.
if (settings.adaptive_threshold && experiment.GetDetectorType() != DetectorType::DECTRIS)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Adaptive spot-finding threshold is not available on this detector: spots "
"are found on the FPGA, which applies its own fixed threshold");
data_processing_settings = settings;
// If there is no capability to use the features, make sure these are disabled
if (!indexing_possible)
data_processing_settings.indexing = false;
services.SetSpotFindingSettings(data_processing_settings);
}
SpotFindingSettings JFJochStateMachine::GetSpotFindingSettings() const {
std::unique_lock ul(data_processing_settings_mutex);
return data_processing_settings;
}
void JFJochStateMachine::AddDetectorSetup(const DetectorSetup &setup) {
// Not thread safe, only during setup
if (detector_setup.empty()) {
experiment.Detector(setup);
UpdateROIDefinition();
gain_calibration = setup.GetGainCalibration();
current_detector_setup = 0;
pixel_mask = PixelMask(experiment);
}
detector_setup.emplace_back(setup);
}
DetectorList JFJochStateMachine::GetDetectorsList() const {
DetectorList ret;
for (const auto &i: detector_setup) {
DetectorListElement tmp;
tmp.description = i.GetDescription();
tmp.nmodules = i.GetModulesNum();
tmp.width = i.GetGeometry().GetWidth(true);
tmp.height = i.GetGeometry().GetHeight(true);
tmp.serial_number = i.GetSerialNumber();
tmp.base_ipv4_addr = i.GetBaseIPv4Addr();
tmp.udp_interface_count = i.GetUDPInterfaceCount();
tmp.min_frame_time = i.GetMinFrameTime();
tmp.min_count_time = i.GetMinCountTime();
tmp.readout_time = i.GetReadOutTime();
tmp.detector_type = i.GetDetectorType();
tmp.pixel_size_mm = i.GetPixelSize_mm();
ret.detector.emplace_back(std::move(tmp));
}
ret.current_id = current_detector_setup;
return ret;
}
std::optional<DetectorStatus> JFJochStateMachine::GetDetectorStatus() const {
return services.GetDetectorStatus();
}
void JFJochStateMachine::SelectDetector(int64_t id) {
std::unique_lock ul(m);
if ((id < 0) || (id >= detector_setup.size()))
throw JFJochException(JFJochExceptionCategory::ArrayOutOfBounds, "Detector doesn't exist");
if (IsRunning())
throw WrongDAQStateException("Cannot change detector during data collection");
// Do nothing if this is the same detector as currently used
if (id == current_detector_setup)
return;
// Try to deactivate current detector (if actually running)
if (state != JFJochState::Inactive) {
try {
SetState(JFJochState::Busy, "Deactivating existing detector");
ul.unlock();
services.Off();
ul.lock();
} catch (const std::exception &e) {
logger.ErrorException(e);
logger.Warning("Cannot turn off existing detector - proceeding anyway");
}
}
try {
experiment.Detector(detector_setup[id]);
UpdateROIDefinition();
gain_calibration = detector_setup[id].GetGainCalibration();
pixel_mask = PixelMask(experiment);
SetState(JFJochState::Inactive, detector_setup[id].GetDescription() + " selected; please initialize");
current_detector_setup = id;
} catch (const JFJochException &e) {
logger.ErrorException(e);
SetState(JFJochState::Error, e.what(), BrokerStatus::MessageSeverity::Error);
throw; // re-throw the exception, so it is populated to caller
}
}
void JFJochStateMachine::SetRadialIntegrationSettings(const AzimuthalIntegrationSettings &settings) {
std::unique_lock ul(m);
if (IsRunning())
throw WrongDAQStateException("Cannot change radial integration settings during data collection");
{
std::unique_lock ul2(experiment_azimuthal_integration_settings_mutex);
experiment.ImportAzimuthalIntegrationSettings(settings);
}
}
AzimuthalIntegrationSettings JFJochStateMachine::GetRadialIntegrationSettings() const {
std::unique_lock ul(experiment_azimuthal_integration_settings_mutex);
return experiment.GetAzimuthalIntegrationSettings();
}
bool JFJochStateMachine::IsRunning() const {
switch (state) {
case JFJochState::Inactive:
case JFJochState::Error:
case JFJochState::Idle:
return false;
case JFJochState::Measuring:
case JFJochState::Busy:
case JFJochState::Calibration:
return true;
default:
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "State unknown");
}
}
BrokerStatus JFJochStateMachine::WaitTillMeasurementDone() {
std::unique_lock ul(m);
c.wait(ul, [&] { return !IsRunning(); });
// A start that failed asynchronously never reached Measuring, so the state is Idle and would
// otherwise be reported as a successfully finished collection.
if (start_exception)
std::rethrow_exception(start_exception);
return GetStatus();
}
BrokerStatus JFJochStateMachine::WaitTillMeasurementDone(std::chrono::milliseconds timeout) {
std::unique_lock ul(m);
c.wait_for(ul, timeout, [&] { return !IsRunning(); });
if (start_exception)
std::rethrow_exception(start_exception);
return GetStatus();
}
void JFJochStateMachine::ResetError() noexcept {
try {
if (measurement.valid())
measurement.get();
} catch (...) {
}
}
std::string JFJochStateMachine::GetPreviewJPEG(const PreviewImageSettings &settings, int64_t image_number) const {
return services.GetPreviewJPEG(settings, image_number);
}
std::string JFJochStateMachine::GetPreviewTIFF(int64_t image_number) const {
return services.GetPreviewTIFF(image_number);
}
std::string JFJochStateMachine::GetPedestalTIFF(size_t gain_level, size_t sc) const {
std::unique_lock ul(m);
if (state != JFJochState::Idle)
throw WrongDAQStateException("Pedestal can be only retrieved in Idle state");
if ((experiment.GetDetectorSetup().GetDetectorType() == DetectorType::JUNGFRAU) && calibration) {
auto tmp = calibration->GetPedestal(gain_level, sc);
CompressedImage image(tmp, RAW_MODULE_COLS, RAW_MODULE_LINES * experiment.GetModulesNum());
return WriteTIFFToString(image);
} else
return {};
}
void JFJochStateMachine::LoadInternalGeneratorImage(const void *data, size_t size, uint64_t image_number) {
std::unique_lock ul(m);
if (state != JFJochState::Idle)
throw WrongDAQStateException("Can change internal generator image only when detector in Idle state");
if ((size != experiment.GetPixelsNum() * sizeof(uint16_t))
&& (size != experiment.GetModulesNum() * RAW_MODULE_SIZE * sizeof(uint16_t)))
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Image size doesn't match current detector");
if (image_number >= experiment.GetInternalPacketGeneratorImages())
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Image for internal generator out of bounds");
std::vector<uint16_t> image(size / sizeof(uint16_t));
memcpy(image.data(), data, size);
services.LoadInternalGeneratorImage(experiment, image, image_number);
}
void JFJochStateMachine::LoadInternalGeneratorImageTIFF(const std::string &s, uint64_t image_number) {
std::unique_lock ul(m);
if (state != JFJochState::Idle)
throw WrongDAQStateException("Can change internal generator image only when detector in Idle state");
uint32_t cols, lines;
auto v = ReadTIFFFromString16(s, cols, lines);
if (((cols == experiment.GetXPixelsNum()) && (lines == experiment.GetYPixelsNum()))
|| ((cols == RAW_MODULE_COLS) && (lines == RAW_MODULE_LINES * experiment.GetModulesNum())))
services.LoadInternalGeneratorImage(experiment, v, image_number);
else
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Image size doesn't match current detector");
}
void JFJochStateMachine::UpdateROIDefinition() {
std::unique_lock ul(roi_mutex);
roi = experiment.ROI().GetROIDefinition();
}
void JFJochStateMachine::SetROIDefinition(const ROIDefinition &input) {
std::unique_lock ul(m);
if (IsRunning())
throw WrongDAQStateException("ROI can be modified only when detector is not running");
experiment.ROI().SetROI(input);
UpdateROIDefinition();
}
ROIDefinition JFJochStateMachine::GetROIDefintion() const {
std::unique_lock ul(roi_mutex);
return roi;
}
std::vector<uint64_t> JFJochStateMachine::GetXFELPulseID() const {
std::vector<uint64_t> ret;
services.GetXFELPulseID(ret);
return ret;
}
std::vector<uint64_t> JFJochStateMachine::GetXFELEventCode() const {
std::vector<uint64_t> ret;
services.GetXFELEventCode(ret);
return ret;
}
std::string JFJochStateMachine::GetFullPixelMaskTIFF() const {
std::unique_lock ul(m);
if (state == JFJochState::Inactive)
return {};
std::vector v = pixel_mask.GetMask(experiment);
CompressedImage mask_image(v, experiment.GetXPixelsNum(), experiment.GetYPixelsNum());
return WriteTIFFToString(mask_image);
}
std::string JFJochStateMachine::GetUserPixelMaskTIFF() const {
std::unique_lock ul(m);
if (state == JFJochState::Inactive)
return {};
std::vector v = pixel_mask.GetUserMask(experiment);
CompressedImage mask_image(v, experiment.GetXPixelsNum(), experiment.GetYPixelsNum());
return WriteTIFFToString(mask_image);
}
std::vector<uint32_t> JFJochStateMachine::GetFullPixelMask() const {
std::unique_lock ul(m);
if (state == JFJochState::Inactive)
return {};
return pixel_mask.GetMask(experiment);
}
std::vector<uint32_t> JFJochStateMachine::GetUserPixelMask() const {
std::unique_lock ul(m);
if (state == JFJochState::Inactive)
return {};
return pixel_mask.GetUserMask(experiment);
}
void JFJochStateMachine::SetUserPixelMask(const std::vector<uint32_t> &v) {
std::unique_lock ul(m);
if (state != JFJochState::Idle)
throw WrongDAQStateException("User mask can be only modified in Idle state");
try {
pixel_mask.LoadUserMask(experiment, v);
UpdatePixelMaskStatistics(pixel_mask.GetStatistics());
} catch (const JFJochException &e) {
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Problem handling user mask " + std::string(e.what()));
}
}
void JFJochStateMachine::SetUserPixelMask(const CompressedImage &image) {
std::unique_lock ul(m);
if (state != JFJochState::Idle)
throw WrongDAQStateException("User mask can be only modified in Idle state");
try {
pixel_mask.LoadUserMask(experiment, image);
UpdatePixelMaskStatistics(pixel_mask.GetStatistics());
} catch (const JFJochException &e) {
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Problem handling user mask " + std::string(e.what()));
}
}
InstrumentMetadata JFJochStateMachine::GetInstrumentMetadata() const {
std::unique_lock ul(experiment_instrument_metadata_mutex);
return experiment.GetInstrumentMetadata();
}
void JFJochStateMachine::LoadInstrumentMetadata(const InstrumentMetadata &settings) {
std::unique_lock ul(m);
if (IsRunning())
throw WrongDAQStateException("Cannot change instrument metadata during data collection");
{
std::unique_lock ul2(experiment_instrument_metadata_mutex);
experiment.ImportInstrumentMetadata(settings);
}
}
ImageFormatSettings JFJochStateMachine::GetImageFormatSettings() const {
std::unique_lock ul(experiment_image_format_settings_mutex);
return experiment.GetImageFormatSettings();
}
void JFJochStateMachine::LoadImageFormatSettings(const ImageFormatSettings &settings) {
std::unique_lock ul(m);
if (IsRunning())
throw WrongDAQStateException("Cannot change image format settings during data collection");
bool recalc_mask = (experiment.GetPedestalG0RMSLimit() != settings.GetPedestalG0RMSLimit());
{
std::unique_lock ul2(experiment_image_format_settings_mutex);
experiment.ImportImageFormatSettings(settings);
}
if (recalc_mask)
pixel_mask.LoadDetectorBadPixelMask(experiment, calibration.get());
else
pixel_mask.CalcEdgePixels(experiment);
UpdatePixelMaskStatistics(pixel_mask.GetStatistics());
}
void JFJochStateMachine::RawImageFormatSettings() {
std::unique_lock ul(m);
if (IsRunning())
throw WrongDAQStateException("Cannot change instrument metadata during data collection");
experiment.Raw();
}
void JFJochStateMachine::ConvImageFormatSettings() {
std::unique_lock ul(m);
if (IsRunning())
throw WrongDAQStateException("Cannot change instrument metadata during data collection");
experiment.Conversion();
}
std::vector<DeviceStatus> JFJochStateMachine::GetDeviceStatus() const {
return services.GetDeviceStatus();
}
void JFJochStateMachine::SetPreviewSocketSettings(const ZMQPreviewSettings &input) {
services.SetPreviewSocketSettings(input);
}
ZMQPreviewSettings JFJochStateMachine::GetPreviewSocketSettings() {
return services.GetPreviewSocketSettings();
}
void JFJochStateMachine::SetMetadataSocketSettings(const ZMQMetadataSettings &input) {
services.SetMetadataSocketSettings(input);
}
ZMQMetadataSettings JFJochStateMachine::GetMetadataSocketSettings() {
return services.GetMetadataSocketSettings();
}
void JFJochStateMachine::GetStartMessageFromBuffer(std::vector<uint8_t> &v) {
return services.GetStartMessageFromBuffer(v);
}
void JFJochStateMachine::GetImageFromBuffer(std::vector<uint8_t> &v, int64_t image_number) {
services.GetImageFromBuffer(v, image_number);
}
ImageBufferStatus JFJochStateMachine::GetImageBufferStatus() const {
return services.GetImageBufferStatus();
}
void JFJochStateMachine::ClearImageBuffer() const {
std::unique_lock ul(m);
if (IsRunning())
throw WrongDAQStateException("Cannot clear image buffer during data collection");
services.ClearImageBuffer();
}
FileWriterSettings JFJochStateMachine::GetFileWriterSettings() const {
std::unique_lock ul(experiment_file_writer_settings_mutex);
return experiment.GetFileWriterSettings();
}
void JFJochStateMachine::LoadFileWriterSettings(const FileWriterSettings &settings) {
std::unique_lock ul(m);
if (IsRunning())
throw WrongDAQStateException("Cannot change instrument metadata during data collection");
{
std::unique_lock ul2(experiment_file_writer_settings_mutex);
experiment.ImportFileWriterSettings(settings);
}
}
IndexingSettings JFJochStateMachine::GetIndexingSettings() const {
std::unique_lock ul(experiment_indexing_settings_mutex);
return experiment.GetIndexingSettings();
}
void JFJochStateMachine::SetIndexingSettings(const IndexingSettings &input) {
std::unique_lock ul(m);
if (IsRunning())
throw WrongDAQStateException("Cannot change instrument metadata during data collection");
{
std::unique_lock ul2(experiment_indexing_settings_mutex);
experiment.ImportIndexingSettings(input);
try {
services.SetupIndexing(input);
} catch (const JFJochException &e) {
logger.ErrorException(e);
SetState(JFJochState::Error,
e.what(),
BrokerStatus::MessageSeverity::Error);
throw;
}
}
}
BraggIntegrationSettings JFJochStateMachine::GetBraggIntegrationSettings() const {
std::unique_lock ul(experiment_indexing_settings_mutex);
return experiment.GetBraggIntegrationSettings();
}
void JFJochStateMachine::SetBraggIntegrationSettings(const BraggIntegrationSettings &input) {
std::unique_lock ul(m);
if (IsRunning())
throw WrongDAQStateException("Cannot change Bragg integration settings during data collection");
// The analysis engines read the integrator mode off the experiment when they are built at the start
// of the next run, so importing it here is all that is needed.
std::unique_lock ul2(experiment_indexing_settings_mutex);
experiment.ImportBraggIntegrationSettings(input);
}
std::optional<ScanResult> JFJochStateMachine::GetScanResult() const {
std::unique_lock ul(m);
if (IsRunning())
throw WrongDAQStateException("Cannot check scan result, when running");
return scan_result;
}
DarkMaskSettings JFJochStateMachine::GetDarkMaskSettings() const {
std::unique_lock ul(experiment_dark_mask_settings_mutex);
return experiment.GetDarkMaskSettings();
}
void JFJochStateMachine::SetDarkMaskSettings(const DarkMaskSettings &settings) {
std::unique_lock ul(m);
if (IsRunning())
throw WrongDAQStateException("Cannot change dark mask calculation settings during data collection");
{
// Setting dark mask settings in experiment requires BOTH mutexes
std::unique_lock ul2(experiment_dark_mask_settings_mutex);
experiment.ImportDarkMaskSettings(settings);
}
if ((experiment.GetDetectorType() == DetectorType::DECTRIS) && (state == JFJochState::Idle)) {
// Need to redo the calibration
start_exception = nullptr; // A new operation supersedes a pending start failure
SetState(JFJochState::Busy, "Loading settings", BrokerStatus::MessageSeverity::Info);
measurement = std::async(std::launch::async, &JFJochStateMachine::CalibrateDetector, this, std::move(ul));
}
}
ImagePusherStatus JFJochStateMachine::GetImagePusherStatus() const {
return services.GetImagePusherStatus();
}