Broker: log why a DECTRIS run ends in a detector error, reuse one SIMPLON connection

"Error in detector operation" was raised whenever the wait-for-idle loop in
JFJochServices::Stop() ended in any state other than idle. For DECTRIS that
includes a single failed status request (connect/read timeout, non-200), which
GetState() silently turned into NOT_CONNECTED, and a cancel whose disarm
failed - none of them distinguishable in the log.

- DectrisDetectorWrapper::GetState() logs the failure reason before returning
  NOT_CONNECTED.
- Stop() logs the state that ended the loop, whether the stop command failed,
  and what the detector reports (DetectorWrapper::GetErrorDescription(); for
  DECTRIS the SIMPLON state and the detector status "error" list).
- Cancel() logs why the stop command failed.
- A non-200 GET now reports the HTTP status and body (it printed "Success").
- DectrisSimplonClient keeps one keep-alive httplib::Client, serialised by a
  mutex, instead of opening a new TCP connection per request.
- The wait-for-idle loop polls every 100 ms instead of every 5 ms.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-08 22:45:42 +02:00
co-authored by Claude Opus 5.5
parent eac0833c88
commit 9807bc40df
8 changed files with 152 additions and 23 deletions
+66
View File
@@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <catch2/catch_all.hpp>
#include <httplib.h>
#include <nlohmann/json.hpp>
#include <mutex>
#include <set>
#include <thread>
#include "../detector_control/DectrisSimplonClient.h"
// A minimal SIMPLON server: the detector state and error list, and a disarm command that fails.
TEST_CASE("DectrisSimplonClient_MockServer", "[detector]") {
httplib::Server server;
std::mutex m;
std::set<int> client_ports;
std::string state = "idle";
server.Get("/detector/api/1.8.0/status/state", [&](const httplib::Request &req, httplib::Response &res) {
std::lock_guard lg(m);
client_ports.insert(req.remote_port);
if (state == "unavailable") {
res.status = 503;
res.set_content("DCU busy", "text/plain");
} else
res.set_content(nlohmann::json{{"value", state}}.dump(), "application/json");
});
server.Get("/detector/api/1.8.0/status/error", [&](const httplib::Request &, httplib::Response &res) {
res.set_content(nlohmann::json{{"value", {"overheating"}}}.dump(), "application/json");
});
server.Post("/detector/api/1.8.0/command/disarm", [&](const httplib::Request &, httplib::Response &res) {
res.status = 500;
res.set_content("disarm refused", "text/plain");
});
const int port = server.bind_to_any_port("127.0.0.1");
REQUIRE(port > 0);
struct ServerThread {
httplib::Server &server;
std::thread thread;
explicit ServerThread(httplib::Server &s) : server(s), thread([&s] { s.listen_after_bind(); }) {}
~ServerThread() { server.stop(); thread.join(); }
} server_thread(server);
server.wait_until_ready();
DectrisSimplonClient client("127.0.0.1", port);
// Repeated polling reuses one connection instead of opening a new one per request
for (int i = 0; i < 10; i++)
REQUIRE(client.GetState() == SimplonState::Idle);
CHECK(client_ports.size() == 1);
CHECK(client.GetErrorStatus() == R"(["overheating"])");
// Failures carry the HTTP status and the body the DCU sent
{
std::lock_guard lg(m);
state = "unavailable";
}
REQUIRE_THROWS_WITH(client.GetState(), Catch::Matchers::ContainsSubstring("HTTP503")
&& Catch::Matchers::ContainsSubstring("DCU busy"));
REQUIRE_THROWS_WITH(client.EndAcquisitionFinished(), Catch::Matchers::ContainsSubstring("HTTP500")
&& Catch::Matchers::ContainsSubstring("disarm refused"));
}