NXmx has no field for the depth of the stored image - only bit_depth_readout, "how many bits the electronics record per pixel". The two diverge exactly when summation is used: the readout keeps the detector's native width while the summed image must be wider to hold the sum. Every NXmx reader nonetheless takes bit_depth_readout as the width of the stored pixel. dxtbx ignores the non-standard bit_depth_image entirely for a generic NXmx file, derives its masking markers from bit_depth_readout, and raises "Unsupported integer dtype uint32" for a 32-bit image when the field is absent. Reporting the electronic value there would mislead precisely where it differs. So report the image depth in both fields, and drop the machinery that existed to carry the electronic one for a DECTRIS detector: the SIMPLON read, the DetectorSetup setter, and the receiver-side propagation of a key that the DECTRIS stream2 protocol does not even define. JUNGFRAU and PSI EIGER keep their readout depth, which the FPGA acquisition genuinely needs. Also write NXmx underload_value, the lowest valid value. Without it a reader takes the trusted minimum to be -0x7FFFFFFF, so the error-pixel marker sits inside the trusted range and is consumed as an intensity. Measured with DIALS 3.27 on a written file: trusted_range goes from (-2147483647, 32766) to (-32767, 32766), so the INT16_MIN gap pixels are now masked. Third fix in the same area: JFJochReceiverLite::Configure took the image width from the incoming stream but not the sign, while the image itself is forwarded byte-for-byte. A detector sending int32 was re-declared uint32, and the VDS master was typed unsigned over signed data files. Take pixel_signed from the stream too - it and the width are both carried by the one image_dtype key. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
342 lines
13 KiB
C++
342 lines
13 KiB
C++
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include <base64/Base64.h>
|
|
#include <httplib.h>
|
|
|
|
#include "DectrisSimplonClient.h"
|
|
|
|
std::string to_string(SimplonState state) {
|
|
switch (state) {
|
|
case SimplonState::Na: return "Na";
|
|
case SimplonState::Ready: return "Ready";
|
|
case SimplonState::Initialize: return "Initialize";
|
|
case SimplonState::Configure: return "Configure";
|
|
case SimplonState::Acquire: return "Acquire";
|
|
case SimplonState::Idle: return "Idle";
|
|
case SimplonState::Test: return "Test";
|
|
case SimplonState::Error: return "Error";
|
|
default: return "Unknown";
|
|
}
|
|
}
|
|
|
|
DectrisSimplonClient::DectrisSimplonClient(const std::string &addr, uint16_t in_port)
|
|
: hostname(addr), port(in_port) {
|
|
|
|
}
|
|
|
|
void DectrisSimplonClient::SendDetectorCommand(SimplonDetectorCommand cmd) {
|
|
std::string key;
|
|
switch (cmd) {
|
|
case SimplonDetectorCommand::Initialize:
|
|
key = "initialize";
|
|
break;
|
|
case SimplonDetectorCommand::Arm:
|
|
key = "arm";
|
|
break;
|
|
case SimplonDetectorCommand::Disarm:
|
|
key = "disarm";
|
|
break;
|
|
case SimplonDetectorCommand::Trigger:
|
|
key = "trigger";
|
|
break;
|
|
case SimplonDetectorCommand::Cancel:
|
|
key = "cancel";
|
|
break;
|
|
}
|
|
auto addr = GenAddr(SimplonModule::Detector, SimplonTask::Command, key);
|
|
|
|
httplib::Client cli_cmd(hostname, port);
|
|
cli_cmd.set_connection_timeout(std::chrono::milliseconds(500));
|
|
cli_cmd.set_read_timeout(std::chrono::minutes(20)); // Initialize or disarm might take a lot of time
|
|
cli_cmd.set_write_timeout(std::chrono::minutes(20)); // Initialize or disarm might take a lot of time
|
|
|
|
auto res = cli_cmd.Post(addr);
|
|
|
|
if (!res || res->status != httplib::StatusCode::OK_200) {
|
|
std::string err_msg = "Command failed " + key;
|
|
if (res) {
|
|
err_msg += "; HTTP" + std::to_string(res->status);
|
|
if (!res->body.empty())
|
|
err_msg += "; " + res->body;
|
|
}
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, err_msg);
|
|
}
|
|
}
|
|
|
|
void DectrisSimplonClient::SetConfig(SimplonModule element, const std::string &key, const nlohmann::json &value) {
|
|
std::string addr = GenAddr(element, SimplonTask::Config, key);
|
|
|
|
nlohmann::json j;
|
|
j["value"] = value;
|
|
|
|
httplib::Client cli_cmd(hostname, port);
|
|
cli_cmd.set_connection_timeout(std::chrono::seconds(1));
|
|
cli_cmd.set_read_timeout(std::chrono::minutes(20)); // Initialize or disarm might take a lot of time
|
|
cli_cmd.set_write_timeout(std::chrono::minutes(20)); // Initialize or disarm might take a lot of time
|
|
|
|
auto res = cli_cmd.Post(addr, j.dump(), "application/json");
|
|
|
|
if (!res || res->status != httplib::StatusCode::OK_200) {
|
|
std::string err_msg = "Configure failed " + key;
|
|
if (res) {
|
|
err_msg += "; HTTP" + std::to_string(res->status);
|
|
if (!res->body.empty())
|
|
err_msg += "; " + res->body;
|
|
}
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, err_msg);
|
|
}
|
|
}
|
|
|
|
void DectrisSimplonClient::SetConfigIfDifferent(SimplonModule element, const std::string &key, float value, float tolerance) {
|
|
auto curr_val = GetConfig(element, key).val.get<float>();
|
|
|
|
if (std::abs(curr_val - value) > tolerance)
|
|
SetConfig(element, key, value);
|
|
}
|
|
|
|
void DectrisSimplonClient::SetConfigIfDifferent(SimplonModule element, const std::string &key, int64_t value) {
|
|
auto curr_val = GetConfig(element, key).val.get<int64_t>();
|
|
if (curr_val != value)
|
|
SetConfig(element, key, value);
|
|
}
|
|
|
|
std::string DectrisSimplonClient::GenAddr(SimplonModule element, SimplonTask task, const std::string& key) {
|
|
std::string addr;
|
|
switch (element) {
|
|
case SimplonModule::Detector:
|
|
addr += "/detector/";
|
|
break;
|
|
case SimplonModule::Stream:
|
|
addr += "/stream/";
|
|
break;
|
|
case SimplonModule::Filewriter:
|
|
addr += "/filewriter/";
|
|
break;
|
|
case SimplonModule::Monitor:
|
|
addr += "/monitor/";
|
|
break;
|
|
}
|
|
addr += "api/";
|
|
addr += api_version;
|
|
switch (task) {
|
|
case SimplonTask::Config:
|
|
addr += "/config/";
|
|
break;
|
|
case SimplonTask::Command:
|
|
addr += "/command/";
|
|
break;
|
|
case SimplonTask::Status:
|
|
addr += "/status/";
|
|
break;
|
|
}
|
|
addr += key;
|
|
return addr;
|
|
}
|
|
|
|
SimplonConfig
|
|
DectrisSimplonClient::GetConfig(SimplonModule element, SimplonTask task, const std::string &key) {
|
|
httplib::Client cli_simple(hostname, port);
|
|
cli_simple.set_connection_timeout(std::chrono::milliseconds(500));
|
|
cli_simple.set_read_timeout(std::chrono::seconds(1));
|
|
cli_simple.set_write_timeout(std::chrono::seconds(1));
|
|
auto res = cli_simple.Get(GenAddr(element, task, key));
|
|
|
|
if (!res) {
|
|
throw JFJochException(JFJochExceptionCategory::SimplonError,
|
|
"Get key " + key + " timeout/error: " + to_string(res.error()));
|
|
}
|
|
if (res->status != httplib::StatusCode::OK_200) {
|
|
throw JFJochException(JFJochExceptionCategory::SimplonError,
|
|
"Get key " + key + " failed: " + to_string(res.error()));
|
|
}
|
|
|
|
try {
|
|
auto j = nlohmann::json::parse(res->body);
|
|
SimplonConfig ret{};
|
|
ret.val = j["value"];
|
|
if (j.contains("unit"))
|
|
ret.unit = j["unit"];
|
|
if (j.contains("min"))
|
|
ret.min = j["min"];
|
|
if (j.contains("max"))
|
|
ret.max = j["max"];
|
|
return ret;
|
|
} catch (const std::exception &e) {
|
|
throw JFJochException(JFJochExceptionCategory::JSON, e.what());
|
|
}
|
|
}
|
|
|
|
SimplonConfig DectrisSimplonClient::GetConfig(SimplonModule element, const std::string &key) {
|
|
return GetConfig(element, SimplonTask::Config, key);
|
|
}
|
|
|
|
SimplonConfig DectrisSimplonClient::GetDetCfg(const std::string &key) {
|
|
return GetConfig(SimplonModule::Detector, key);
|
|
}
|
|
|
|
nlohmann::json DectrisSimplonClient::GetStatus(SimplonModule element, const std::string &key) {
|
|
return GetConfig(element, SimplonTask::Status, key).val;
|
|
}
|
|
|
|
template <class T>
|
|
T float2time(float time_in_seconds) {
|
|
return std::chrono::duration_cast<T>(std::chrono::duration<float>(time_in_seconds));
|
|
}
|
|
|
|
void DectrisSimplonClient::ReadDetectorConfig(DetectorSetup &setup) {
|
|
// Need to initialize to read configuration information
|
|
|
|
if ((GetState() == SimplonState::Na)
|
|
|| (GetState() == SimplonState::Error))
|
|
throw JFJochException(JFJochExceptionCategory::Detector,
|
|
"Detector state Na or Error - need to reinitialize before reading configuration");
|
|
|
|
setup.Description(GetDetCfg( "description").val);
|
|
setup.PixelSize_um(std::round(GetDetCfg( "x_pixel_size").val.get<float>() * 1e6f));
|
|
setup.SerialNumber(GetDetCfg( "detector_number").val);
|
|
setup.SensorMaterial(GetDetCfg( "sensor_material").val);
|
|
setup.SensorThickness_um(std::round(GetDetCfg( "sensor_thickness").val.get<float>() * 1e6f));
|
|
|
|
// Only the image depth is read: it sizes the image buffer before the first frame arrives, and
|
|
// is replaced from the stream2 image_dtype when the detector arms. The electronic readout depth
|
|
// is deliberately not read - nothing consumes it.
|
|
setup.BitDepthImage(GetDetCfg("bit_depth_image").val.get<int64_t>());
|
|
setup.MinFrameTime(float2time<std::chrono::nanoseconds>(GetDetCfg("frame_time").min.get<float>()));
|
|
setup.MinCountTime(float2time<std::chrono::nanoseconds>(GetDetCfg("count_time").min.get<float>()));
|
|
setup.ReadOutTime(float2time<std::chrono::nanoseconds>(GetDetCfg("detector_readout_time").val.get<float>()));
|
|
|
|
nlohmann::json min_threshold_energy = GetDetCfg("threshold_energy").min;
|
|
if (min_threshold_energy.is_number())
|
|
setup.MinThreshold_keV(min_threshold_energy.get<float>() / 1000.0f);
|
|
else
|
|
setup.MinThreshold_keV(2.7f);
|
|
|
|
int64_t width = GetDetCfg("x_pixels_in_detector").val.get<int64_t>();
|
|
int64_t height = GetDetCfg( "y_pixels_in_detector").val.get<int64_t>();
|
|
setup.Geometry(DetectorGeometryFixed(width, height));
|
|
}
|
|
|
|
void DectrisSimplonClient::InitializeDetector(DetectorSetup& setup) {
|
|
auto state = GetState();
|
|
|
|
if ((state == SimplonState::Na) || (state == SimplonState::Error))
|
|
SendDetectorCommand(SimplonDetectorCommand::Initialize);
|
|
|
|
if (GetState() != SimplonState::Idle)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Initialize unsuccessful");
|
|
|
|
SetConfig(SimplonModule::Detector, "roi_mode", setup.GetDECTRISROI());
|
|
// Configure streaming, disable file writer and monitor
|
|
SetConfig(SimplonModule::Stream, "format", "cbor");
|
|
SetConfig(SimplonModule::Stream, "mode", "enabled");
|
|
SetConfig(SimplonModule::Filewriter, "mode", "disabled");
|
|
SetConfig(SimplonModule::Monitor, "mode", "disabled");
|
|
ReadDetectorConfig(setup);
|
|
}
|
|
|
|
void DectrisSimplonClient::TriggerAcquisition() {
|
|
SendDetectorCommand(SimplonDetectorCommand::Trigger);
|
|
}
|
|
|
|
void DectrisSimplonClient::CancelAcquisition() {
|
|
SendDetectorCommand(SimplonDetectorCommand::Cancel);
|
|
}
|
|
|
|
void DectrisSimplonClient::EndAcquisitionFinished() {
|
|
SendDetectorCommand(SimplonDetectorCommand::Disarm);
|
|
}
|
|
|
|
void DectrisSimplonClient::StartAcquisition(const DiffractionExperiment& experiment) {
|
|
SetConfigIfDifferent(SimplonModule::Detector, "photon_energy", experiment.GetIncidentEnergy_keV() * 1e3f, 0.01f);
|
|
auto thr = experiment.GetEigerThreshold_keV();
|
|
SetConfigIfDifferent(SimplonModule::Detector, "threshold_energy", thr * 1e3f, 0.01);
|
|
|
|
SetConfigIfDifferent(SimplonModule::Detector, "count_time", std::chrono::duration<float>(experiment.GetFrameCountTime()).count(), 1e-9);
|
|
SetConfigIfDifferent(SimplonModule::Detector, "frame_time", std::chrono::duration<float>(experiment.GetFrameTime()).count(), 1e-9);
|
|
SetConfigIfDifferent(SimplonModule::Detector, "nimages", experiment.GetFrameNumPerTrigger());
|
|
SetConfigIfDifferent(SimplonModule::Detector, "ntrigger", experiment.GetNumTriggers());
|
|
|
|
SendDetectorCommand(SimplonDetectorCommand::Arm);
|
|
}
|
|
|
|
void DectrisSimplonClient::ConfigureDetector(const DiffractionExperiment &experiment) {
|
|
// TODO: Check if counting_mode retrigger is available
|
|
if (experiment.GetDetectorMode() == DetectorMode::DarkMask)
|
|
SetConfig(SimplonModule::Detector, "counting_mode", "normal");
|
|
else
|
|
SetConfig(SimplonModule::Detector, "counting_mode", "retrigger");
|
|
|
|
SetConfigIfDifferent(SimplonModule::Detector, "photon_energy", experiment.GetIncidentEnergy_keV() * 1e3f, 0.01f);
|
|
auto thr = experiment.GetEigerThreshold_keV();
|
|
SetConfigIfDifferent(SimplonModule::Detector, "threshold_energy", thr * 1e3f, 0.01);
|
|
|
|
switch (experiment.GetDetectorTiming()) {
|
|
case DetectorTiming::Auto:
|
|
SetConfig(SimplonModule::Detector, "trigger_mode", "ints");
|
|
break;
|
|
case DetectorTiming::Trigger:
|
|
SetConfig(SimplonModule::Detector, "trigger_mode", "exts");
|
|
break;
|
|
case DetectorTiming::Gated:
|
|
SetConfig(SimplonModule::Detector, "trigger_mode", "extg");
|
|
break;
|
|
case DetectorTiming::Burst:
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Burst mode not supported with DECTRIS sytems");
|
|
}
|
|
|
|
SetConfig(SimplonModule::Detector, "trigger_start_delay", std::chrono::duration<float>(experiment.GetDetectorDelay()).count());
|
|
}
|
|
|
|
SimplonState DectrisSimplonClient::GetState() {
|
|
auto c = GetStatus(SimplonModule::Detector, "state");
|
|
if (!c.is_string())
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"value must be string");
|
|
|
|
std::string res = c.get<std::string>();
|
|
|
|
if (res == "na")
|
|
return SimplonState::Na;
|
|
else if (res == "ready")
|
|
return SimplonState::Ready;
|
|
else if (res == "idle")
|
|
return SimplonState::Idle;
|
|
else if (res == "acquire")
|
|
return SimplonState::Acquire;
|
|
else if (res == "error")
|
|
return SimplonState::Error;
|
|
else if (res == "initialize")
|
|
return SimplonState::Initialize;
|
|
else if (res == "configure")
|
|
return SimplonState::Configure;
|
|
else if (res == "test")
|
|
return SimplonState::Test;
|
|
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"State unknown " + res);
|
|
}
|
|
|
|
float DectrisSimplonClient::GetHumidity() {
|
|
return GetStatus(SimplonModule::Detector, "humidity");
|
|
}
|
|
|
|
float DectrisSimplonClient::GetTemperature() {
|
|
return GetStatus(SimplonModule::Detector, "temperature");
|
|
}
|
|
|
|
|
|
std::vector<uint32_t> DectrisSimplonClient::GetPixelMask() {
|
|
auto mask = GetDetCfg("pixel_mask");
|
|
if (!mask.val.is_object() || !mask.val.contains("shape") || !mask.val.contains("data"))
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "No pixel mask");
|
|
|
|
auto shape = mask.val["shape"];
|
|
|
|
std::vector<uint32_t> pixel_mask(shape[0].get<int64_t>() * shape[1].get<int64_t>());
|
|
macaron::Decode(mask.val["data"], pixel_mask);
|
|
|
|
return pixel_mask;
|
|
} |