26 : logger_(logger ? std::move(logger) : di::null_logger()) {}
29 std::shared_ptr<di::ILogger> logger)
30 : logger_(logger ? std::move(logger) : di::null_logger()), config_(config) {}
46 uint16_t message_id) {
48 using namespace network::dimse;
50 auto start_time = std::chrono::steady_clock::now();
56 "Association not established");
67 "No accepted presentation context for SOP Class: " +
68 std::string(sop_class_uid));
72 auto request = make_c_find_rq(message_id, sop_class_uid);
73 request.set_dataset(query_keys);
75 logger_->debug_fmt(
"Sending C-FIND request (message_id={}, sop_class={})",
76 message_id, sop_class_uid);
79 auto send_result = assoc.
send_dimse(*context_id, request);
80 if (send_result.is_err()) {
81 return send_result.error();
86 bool query_complete =
false;
88 while (!query_complete) {
90 if (recv_result.is_err()) {
91 return recv_result.error();
94 const auto& [recv_context_id, response] = recv_result.value();
97 if (response.command() != command_field::c_find_rsp) {
100 "Expected C-FIND-RSP but received " +
101 std::string(
to_string(response.command())));
104 auto status = response.status();
106 if (status == status_pending || status == status_pending_warning) {
109 if (response.has_dataset()) {
114 auto dataset_result = response.dataset();
115 if (dataset_result.is_ok()) {
116 result.
matches.push_back(dataset_result.value().get());
126 "Max results ({}) reached, sending C-CANCEL",
130 auto cancel_result =
cancel(assoc, message_id);
131 if (cancel_result.is_err()) {
132 logger_->warn_fmt(
"Failed to send C-CANCEL: {}",
133 cancel_result.error().message);
137 }
else if (status == status_success) {
138 query_complete =
true;
139 result.
status =
static_cast<uint16_t
>(status);
140 }
else if (status == status_cancel) {
141 query_complete =
true;
142 result.
status =
static_cast<uint16_t
>(status);
145 query_complete =
true;
146 result.
status =
static_cast<uint16_t
>(status);
150 auto end_time = std::chrono::steady_clock::now();
151 result.
elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
152 end_time - start_time);
158 logger_->debug_fmt(
"C-FIND completed: {} matches in {} ms",
169 using namespace network::dimse;
172 auto start_time = std::chrono::steady_clock::now();
178 "Association not established");
189 "No accepted presentation context for SOP Class: " +
190 std::string(sop_class_uid));
194 auto request = make_c_find_rq(message_id, sop_class_uid);
195 request.set_dataset(query_keys);
197 logger_->debug_fmt(
"Sending streaming C-FIND request (message_id={})",
201 auto send_result = assoc.
send_dimse(*context_id, request);
202 if (send_result.is_err()) {
203 return send_result.error();
208 bool query_complete =
false;
209 bool should_cancel =
false;
211 while (!query_complete) {
213 if (recv_result.is_err()) {
214 return recv_result.error();
217 const auto& [recv_context_id, response] = recv_result.value();
220 if (response.command() != command_field::c_find_rsp) {
223 "Expected C-FIND-RSP but received " +
224 std::string(
to_string(response.command())));
227 auto status = response.status();
229 if (status == status_pending || status == status_pending_warning) {
230 if (response.has_dataset()) {
231 auto dataset_result = response.dataset();
232 if (dataset_result.is_ok()) {
237 if (!callback(dataset_result.value().get())) {
238 should_cancel =
true;
248 logger_->debug(
"Cancelling streaming query");
249 auto cancel_result =
cancel(assoc, message_id);
250 if (cancel_result.is_err()) {
251 logger_->warn_fmt(
"Failed to send C-CANCEL: {}",
252 cancel_result.error().message);
254 should_cancel =
false;
259 query_complete =
true;
267 auto end_time = std::chrono::steady_clock::now();
268 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
269 end_time - start_time);
271 logger_->debug_fmt(
"Streaming C-FIND completed: {} results in {} ms",
272 count, elapsed.count());
289 auto result =
find(assoc, query_ds);
303 auto result =
find(assoc, query_ds);
317 auto result =
find(assoc, query_ds);
331 auto result =
find(assoc, query_ds);
343 uint16_t message_id) {
345 using namespace network::dimse;
353 "No accepted presentation context for cancel");
357 dimse_message cancel_rq{command_field::c_cancel_rq, message_id};
358 return assoc.
send_dimse(*context_id, cancel_rq);
406 using namespace core;
407 using namespace encoding;
431 using namespace core;
432 using namespace encoding;
462 using namespace core;
463 using namespace encoding;
488 using namespace core;
489 using namespace encoding;
bool is_established() const noexcept
Check if association is established and ready for DIMSE.
Result< std::monostate > send_dimse(uint8_t context_id, const dimse::dimse_message &msg)
Send a DIMSE message.
Result< std::pair< uint8_t, dimse::dimse_message > > receive_dimse(duration timeout=default_timeout)
Receive a DIMSE message.
std::optional< uint8_t > accepted_context_id(std::string_view abstract_syntax) const
Get the presentation context ID for an abstract syntax.
std::string_view get_sop_class_uid() const noexcept
Get SOP Class UID based on current configuration.
query_scu_config config_
Configuration.
network::Result< size_t > find_streaming(network::association &assoc, const core::dicom_dataset &query_keys, query_streaming_callback callback)
Perform a streaming C-FIND query for large result sets.
std::shared_ptr< di::ILogger > logger_
Logger instance for service logging.
void set_config(const query_scu_config &config)
Update the SCU configuration.
std::atomic< size_t > queries_performed_
Statistics: number of queries performed.
std::atomic< size_t > total_matches_
Statistics: total number of matches received.
network::Result< query_result > find_patients(network::association &assoc, const patient_query_keys &keys)
Query for patients.
network::Result< query_result > find_instances(network::association &assoc, const instance_query_keys &keys)
Query for instances within a series.
size_t queries_performed() const noexcept
Get the number of queries performed since construction.
network::Result< query_result > find_studies(network::association &assoc, const study_query_keys &keys)
Query for studies.
void reset_statistics() noexcept
Reset statistics counters to zero.
network::Result< std::monostate > cancel(network::association &assoc, uint16_t message_id)
Send a C-CANCEL request to stop an ongoing query.
std::atomic< uint16_t > message_id_counter_
Message ID counter.
uint16_t next_message_id() noexcept
Get the next message ID for DIMSE operations.
const query_scu_config & config() const noexcept
Get the current configuration.
network::Result< query_result > find_impl(network::association &assoc, const core::dicom_dataset &query_keys, uint16_t message_id)
Internal query implementation.
query_scu(std::shared_ptr< di::ILogger > logger=nullptr)
Construct a Query SCU with default configuration.
core::dicom_dataset build_query_dataset(const patient_query_keys &keys) const
Build query dataset from patient keys.
network::Result< query_result > find_series(network::association &assoc, const series_query_keys &keys)
Query for series within a study.
size_t total_matches() const noexcept
Get the total number of matches received since construction.
network::Result< query_result > find(network::association &assoc, const core::dicom_dataset &query_keys)
Perform a C-FIND query with raw dataset.
DIMSE command field enumeration.
Compile-time constants for commonly used DICOM tags.
constexpr int no_acceptable_context
constexpr int find_unexpected_command
constexpr int association_not_established
std::function< bool(const core::dicom_dataset &)> query_streaming_callback
Callback type for streaming query results.
constexpr std::string_view get_find_sop_class_uid(query_model model) noexcept
Get the FIND SOP Class UID for a query model.
auto to_string(mpps_status status) -> std::string_view
Convert mpps_status to DICOM string representation.
@ study
Study level - query study information.
@ image
Image (Instance) level - query instance information.
@ patient
Patient level - query patient demographics.
@ series
Series level - query series information.
Result< T > pacs_error(int code, const std::string &message, const std::string &details="")
Create a PACS error result with module context.
DICOM Query SCU service (C-FIND sender)
Result<T> type aliases and helpers for PACS system.
Query keys for IMAGE (Instance) level queries.
std::string sop_instance_uid
SOP Instance UID (0008,0018)
std::string series_uid
Series Instance UID (0020,000E) - Required.
std::string instance_number
Instance Number (0020,0013)
Query keys for PATIENT level queries.
std::string birth_date
Patient's Birth Date (0010,0030)
std::string sex
Patient's Sex (0010,0040)
std::string patient_id
Patient ID (0010,0020)
std::string patient_name
Patient's Name (0010,0010)
Result of a C-FIND query operation.
std::chrono::milliseconds elapsed
Query execution time.
size_t total_pending
Number of pending responses received (may differ from matches.size() if max_results was enforced)
uint16_t status
Final DIMSE status code (0x0000 = success)
std::vector< core::dicom_dataset > matches
Matching datasets returned by the SCP.
Configuration for Query SCU service.
std::chrono::milliseconds timeout
Timeout for receiving query responses (milliseconds)
query_model model
Query information model (Patient Root or Study Root)
bool cancel_on_max
Send C-CANCEL when max_results is reached.
size_t max_results
Maximum number of results to return (0 = unlimited)
query_level level
Query level (Patient, Study, Series, or Image)
Query keys for SERIES level queries.
std::string series_number
Series Number (0020,0011)
std::string series_uid
Series Instance UID (0020,000E)
std::string study_uid
Study Instance UID (0020,000D) - Required.
std::string modality
Modality (0008,0060)
Query keys for STUDY level queries.
std::string accession_number
Accession Number (0008,0050)
std::string study_uid
Study Instance UID (0020,000D)
std::string study_description
Study Description (0008,1030)
std::string modality
Modalities in Study (0008,0061)
std::string patient_id
Patient ID (0010,0020) - for filtering.
std::string study_date
Study Date (0008,0020) - YYYYMMDD or range.