PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
query_scu.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
18
19namespace kcenon::pacs::services {
20
21// =============================================================================
22// Construction
23// =============================================================================
24
25query_scu::query_scu(std::shared_ptr<di::ILogger> logger)
26 : logger_(logger ? std::move(logger) : di::null_logger()) {}
27
29 std::shared_ptr<di::ILogger> logger)
30 : logger_(logger ? std::move(logger) : di::null_logger()), config_(config) {}
31
32// =============================================================================
33// Generic Query Operations
34// =============================================================================
35
38 const core::dicom_dataset& query_keys) {
39
40 return find_impl(assoc, query_keys, next_message_id());
41}
42
45 const core::dicom_dataset& query_keys,
46 uint16_t message_id) {
47
48 using namespace network::dimse;
49
50 auto start_time = std::chrono::steady_clock::now();
51
52 // Verify association is established
53 if (!assoc.is_established()) {
56 "Association not established");
57 }
58
59 // Get SOP Class UID
60 auto sop_class_uid = get_sop_class_uid();
61
62 // Get accepted presentation context for this SOP class
63 auto context_id = assoc.accepted_context_id(sop_class_uid);
64 if (!context_id) {
67 "No accepted presentation context for SOP Class: " +
68 std::string(sop_class_uid));
69 }
70
71 // Build C-FIND-RQ message
72 auto request = make_c_find_rq(message_id, sop_class_uid);
73 request.set_dataset(query_keys);
74
75 logger_->debug_fmt("Sending C-FIND request (message_id={}, sop_class={})",
76 message_id, sop_class_uid);
77
78 // Send the request
79 auto send_result = assoc.send_dimse(*context_id, request);
80 if (send_result.is_err()) {
81 return send_result.error();
82 }
83
84 // Receive responses
85 query_result result;
86 bool query_complete = false;
87
88 while (!query_complete) {
89 auto recv_result = assoc.receive_dimse(config_.timeout);
90 if (recv_result.is_err()) {
91 return recv_result.error();
92 }
93
94 const auto& [recv_context_id, response] = recv_result.value();
95
96 // Verify it's a C-FIND response
97 if (response.command() != command_field::c_find_rsp) {
100 "Expected C-FIND-RSP but received " +
101 std::string(to_string(response.command())));
102 }
103
104 auto status = response.status();
105
106 if (status == status_pending || status == status_pending_warning) {
107 ++result.total_pending;
108
109 if (response.has_dataset()) {
110 // Check if we should collect this result
111 if (config_.max_results == 0 ||
112 result.matches.size() < config_.max_results) {
113
114 auto dataset_result = response.dataset();
115 if (dataset_result.is_ok()) {
116 result.matches.push_back(dataset_result.value().get());
117 }
118 }
119
120 // Check if we should cancel due to max_results
121 if (config_.max_results > 0 &&
122 result.matches.size() >= config_.max_results &&
124
125 logger_->debug_fmt(
126 "Max results ({}) reached, sending C-CANCEL",
128
129 // Send 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);
134 }
135 }
136 }
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);
143 } else {
144 // Error status
145 query_complete = true;
146 result.status = static_cast<uint16_t>(status);
147 }
148 }
149
150 auto end_time = std::chrono::steady_clock::now();
151 result.elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
152 end_time - start_time);
153
154 // Update statistics
155 queries_performed_.fetch_add(1, std::memory_order_relaxed);
156 total_matches_.fetch_add(result.matches.size(), std::memory_order_relaxed);
157
158 logger_->debug_fmt("C-FIND completed: {} matches in {} ms",
159 result.matches.size(), result.elapsed.count());
160
161 return result;
162}
163
166 const core::dicom_dataset& query_keys,
167 query_streaming_callback callback) {
168
169 using namespace network::dimse;
170
171 auto message_id = next_message_id();
172 auto start_time = std::chrono::steady_clock::now();
173
174 // Verify association is established
175 if (!assoc.is_established()) {
178 "Association not established");
179 }
180
181 // Get SOP Class UID
182 auto sop_class_uid = get_sop_class_uid();
183
184 // Get accepted presentation context for this SOP class
185 auto context_id = assoc.accepted_context_id(sop_class_uid);
186 if (!context_id) {
189 "No accepted presentation context for SOP Class: " +
190 std::string(sop_class_uid));
191 }
192
193 // Build C-FIND-RQ message
194 auto request = make_c_find_rq(message_id, sop_class_uid);
195 request.set_dataset(query_keys);
196
197 logger_->debug_fmt("Sending streaming C-FIND request (message_id={})",
198 message_id);
199
200 // Send the request
201 auto send_result = assoc.send_dimse(*context_id, request);
202 if (send_result.is_err()) {
203 return send_result.error();
204 }
205
206 // Receive responses
207 size_t count = 0;
208 bool query_complete = false;
209 bool should_cancel = false;
210
211 while (!query_complete) {
212 auto recv_result = assoc.receive_dimse(config_.timeout);
213 if (recv_result.is_err()) {
214 return recv_result.error();
215 }
216
217 const auto& [recv_context_id, response] = recv_result.value();
218
219 // Verify it's a C-FIND response
220 if (response.command() != command_field::c_find_rsp) {
223 "Expected C-FIND-RSP but received " +
224 std::string(to_string(response.command())));
225 }
226
227 auto status = response.status();
228
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()) {
233 ++count;
234
235 // Call the callback
236 if (callback) {
237 if (!callback(dataset_result.value().get())) {
238 should_cancel = true;
239 }
240 }
241
242 // Check if we need to cancel
243 if (should_cancel ||
244 (config_.max_results > 0 &&
245 count >= config_.max_results &&
247
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);
253 }
254 should_cancel = false; // Already sent cancel
255 }
256 }
257 }
258 } else {
259 query_complete = true;
260 }
261 }
262
263 // Update statistics
264 queries_performed_.fetch_add(1, std::memory_order_relaxed);
265 total_matches_.fetch_add(count, std::memory_order_relaxed);
266
267 auto end_time = std::chrono::steady_clock::now();
268 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
269 end_time - start_time);
270
271 logger_->debug_fmt("Streaming C-FIND completed: {} results in {} ms",
272 count, elapsed.count());
273
274 return count;
275}
276
277// =============================================================================
278// Typed Convenience Methods
279// =============================================================================
280
283 const patient_query_keys& keys) {
284
285 auto saved_level = config_.level;
287
288 auto query_ds = build_query_dataset(keys);
289 auto result = find(assoc, query_ds);
290
291 config_.level = saved_level;
292 return result;
293}
294
297 const study_query_keys& keys) {
298
299 auto saved_level = config_.level;
301
302 auto query_ds = build_query_dataset(keys);
303 auto result = find(assoc, query_ds);
304
305 config_.level = saved_level;
306 return result;
307}
308
311 const series_query_keys& keys) {
312
313 auto saved_level = config_.level;
315
316 auto query_ds = build_query_dataset(keys);
317 auto result = find(assoc, query_ds);
318
319 config_.level = saved_level;
320 return result;
321}
322
325 const instance_query_keys& keys) {
326
327 auto saved_level = config_.level;
329
330 auto query_ds = build_query_dataset(keys);
331 auto result = find(assoc, query_ds);
332
333 config_.level = saved_level;
334 return result;
335}
336
337// =============================================================================
338// C-CANCEL Support
339// =============================================================================
340
343 uint16_t message_id) {
344
345 using namespace network::dimse;
346
347 auto sop_class_uid = get_sop_class_uid();
348
349 auto context_id = assoc.accepted_context_id(sop_class_uid);
350 if (!context_id) {
353 "No accepted presentation context for cancel");
354 }
355
356 // Build C-CANCEL-RQ manually since there's no factory function
357 dimse_message cancel_rq{command_field::c_cancel_rq, message_id};
358 return assoc.send_dimse(*context_id, cancel_rq);
359}
360
361// =============================================================================
362// Configuration
363// =============================================================================
364
366 config_ = config;
367}
368
369const query_scu_config& query_scu::config() const noexcept {
370 return config_;
371}
372
373// =============================================================================
374// Statistics
375// =============================================================================
376
377size_t query_scu::queries_performed() const noexcept {
378 return queries_performed_.load(std::memory_order_relaxed);
379}
380
381size_t query_scu::total_matches() const noexcept {
382 return total_matches_.load(std::memory_order_relaxed);
383}
384
386 queries_performed_.store(0, std::memory_order_relaxed);
387 total_matches_.store(0, std::memory_order_relaxed);
388}
389
390// =============================================================================
391// Private Implementation
392// =============================================================================
393
394uint16_t query_scu::next_message_id() noexcept {
395 uint16_t id = message_id_counter_.fetch_add(1, std::memory_order_relaxed);
396 // Wrap around at 0xFFFF, skip 0 (reserved)
397 if (id == 0) {
398 id = message_id_counter_.fetch_add(1, std::memory_order_relaxed);
399 }
400 return id;
401}
402
404 const patient_query_keys& keys) const {
405
406 using namespace core;
407 using namespace encoding;
408
409 dicom_dataset ds;
410
411 // Set Query/Retrieve Level
412 ds.set_string(tags::query_retrieve_level, vr_type::CS, "PATIENT");
413
414 // Set query keys (empty string = return key, non-empty = match key)
415 ds.set_string(tags::patient_name, vr_type::PN, keys.patient_name);
416 ds.set_string(tags::patient_id, vr_type::LO, keys.patient_id);
417 ds.set_string(tags::patient_birth_date, vr_type::DA, keys.birth_date);
418 ds.set_string(tags::patient_sex, vr_type::CS, keys.sex);
419
420 // Add standard return keys
421 ds.set_string(tags::number_of_patient_related_studies, vr_type::IS, "");
422 ds.set_string(tags::number_of_patient_related_series, vr_type::IS, "");
423 ds.set_string(tags::number_of_patient_related_instances, vr_type::IS, "");
424
425 return ds;
426}
427
429 const study_query_keys& keys) const {
430
431 using namespace core;
432 using namespace encoding;
433
434 dicom_dataset ds;
435
436 // Set Query/Retrieve Level
437 ds.set_string(tags::query_retrieve_level, vr_type::CS, "STUDY");
438
439 // Patient info (return keys)
440 ds.set_string(tags::patient_name, vr_type::PN, "");
441 ds.set_string(tags::patient_id, vr_type::LO, keys.patient_id);
442
443 // Study keys
444 ds.set_string(tags::study_instance_uid, vr_type::UI, keys.study_uid);
445 ds.set_string(tags::study_date, vr_type::DA, keys.study_date);
446 ds.set_string(tags::study_time, vr_type::TM, "");
447 ds.set_string(tags::accession_number, vr_type::SH, keys.accession_number);
448 ds.set_string(tags::study_id, vr_type::SH, "");
449 ds.set_string(tags::study_description, vr_type::LO, keys.study_description);
450 ds.set_string(tags::modalities_in_study, vr_type::CS, keys.modality);
451
452 // Standard return keys
453 ds.set_string(tags::number_of_study_related_series, vr_type::IS, "");
454 ds.set_string(tags::number_of_study_related_instances, vr_type::IS, "");
455
456 return ds;
457}
458
460 const series_query_keys& keys) const {
461
462 using namespace core;
463 using namespace encoding;
464
465 dicom_dataset ds;
466
467 // Set Query/Retrieve Level
468 ds.set_string(tags::query_retrieve_level, vr_type::CS, "SERIES");
469
470 // Study UID (required for series query)
471 ds.set_string(tags::study_instance_uid, vr_type::UI, keys.study_uid);
472
473 // Series keys
474 ds.set_string(tags::series_instance_uid, vr_type::UI, keys.series_uid);
475 ds.set_string(tags::modality, vr_type::CS, keys.modality);
476 ds.set_string(tags::series_number, vr_type::IS, keys.series_number);
477 ds.set_string(tags::series_description, vr_type::LO, "");
478
479 // Standard return keys
480 ds.set_string(tags::number_of_series_related_instances, vr_type::IS, "");
481
482 return ds;
483}
484
486 const instance_query_keys& keys) const {
487
488 using namespace core;
489 using namespace encoding;
490
491 dicom_dataset ds;
492
493 // Set Query/Retrieve Level
494 ds.set_string(tags::query_retrieve_level, vr_type::CS, "IMAGE");
495
496 // Series UID (required for instance query)
497 ds.set_string(tags::series_instance_uid, vr_type::UI, keys.series_uid);
498
499 // Instance keys
500 ds.set_string(tags::sop_instance_uid, vr_type::UI, keys.sop_instance_uid);
501 ds.set_string(tags::sop_class_uid, vr_type::UI, "");
502 ds.set_string(tags::instance_number, vr_type::IS, keys.instance_number);
503
504 return ds;
505}
506
507std::string_view query_scu::get_sop_class_uid() const noexcept {
509}
510
511} // namespace kcenon::pacs::services
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.
Definition query_scu.h:515
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.
Definition query_scu.h:512
void set_config(const query_scu_config &config)
Update the SCU configuration.
std::atomic< size_t > queries_performed_
Statistics: number of queries performed.
Definition query_scu.h:521
std::atomic< size_t > total_matches_
Statistics: total number of matches received.
Definition query_scu.h:524
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.
Definition query_scu.h:518
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.
Definition query_scu.cpp:43
query_scu(std::shared_ptr< di::ILogger > logger=nullptr)
Construct a Query SCU with default configuration.
Definition query_scu.cpp:25
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.
Definition query_scu.cpp:36
DIMSE command field enumeration.
Compile-time constants for commonly used DICOM tags.
constexpr dicom_tag number_of_study_related_series
Number of Study Related Series.
constexpr dicom_tag modalities_in_study
Modalities in Study.
constexpr dicom_tag number_of_patient_related_studies
Number of Patient Related Studies.
constexpr dicom_tag study_description
Study Description.
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag number_of_patient_related_instances
Number of Patient Related Instances.
constexpr dicom_tag number_of_study_related_instances
Number of Study Related Instances.
constexpr dicom_tag sop_instance_uid
SOP Instance UID.
constexpr dicom_tag patient_birth_date
Patient's Birth Date.
constexpr dicom_tag query_retrieve_level
Query/Retrieve Level.
constexpr dicom_tag number_of_patient_related_series
Number of Patient Related Series.
constexpr dicom_tag accession_number
Accession Number.
constexpr dicom_tag modality
Modality.
constexpr dicom_tag study_time
Study Time.
constexpr dicom_tag patient_sex
Patient's Sex.
constexpr dicom_tag study_instance_uid
Study Instance UID.
constexpr dicom_tag series_number
Series Number.
constexpr dicom_tag number_of_series_related_instances
Number of Series Related Instances.
constexpr dicom_tag series_description
Series Description.
constexpr dicom_tag sop_class_uid
SOP Class UID.
constexpr dicom_tag study_id
Study ID.
constexpr dicom_tag patient_name
Patient's Name.
constexpr dicom_tag study_date
Study Date.
constexpr dicom_tag series_instance_uid
Series Instance UID.
constexpr dicom_tag instance_number
Instance Number.
constexpr int no_acceptable_context
Definition result.h:103
constexpr int find_unexpected_command
Definition result.h:157
constexpr int association_not_established
Definition result.h:202
std::function< bool(const core::dicom_dataset &)> query_streaming_callback
Callback type for streaming query results.
Definition query_scu.h:197
constexpr std::string_view get_find_sop_class_uid(query_model model) noexcept
Get the FIND SOP Class UID for a query model.
Definition query_scu.h:70
auto to_string(mpps_status status) -> std::string_view
Convert mpps_status to DICOM string representation.
Definition mpps_scp.h:60
@ 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.
Definition result.h:234
DICOM Query SCU service (C-FIND sender)
Result<T> type aliases and helpers for PACS system.
DIMSE status codes.
Query keys for IMAGE (Instance) level queries.
Definition query_scu.h:155
std::string sop_instance_uid
SOP Instance UID (0008,0018)
Definition query_scu.h:157
std::string series_uid
Series Instance UID (0020,000E) - Required.
Definition query_scu.h:156
std::string instance_number
Instance Number (0020,0013)
Definition query_scu.h:158
Query keys for PATIENT level queries.
Definition query_scu.h:123
std::string birth_date
Patient's Birth Date (0010,0030)
Definition query_scu.h:126
std::string sex
Patient's Sex (0010,0040)
Definition query_scu.h:127
std::string patient_id
Patient ID (0010,0020)
Definition query_scu.h:125
std::string patient_name
Patient's Name (0010,0010)
Definition query_scu.h:124
Result of a C-FIND query operation.
Definition query_scu.h:91
std::chrono::milliseconds elapsed
Query execution time.
Definition query_scu.h:99
size_t total_pending
Number of pending responses received (may differ from matches.size() if max_results was enforced)
Definition query_scu.h:103
uint16_t status
Final DIMSE status code (0x0000 = success)
Definition query_scu.h:96
std::vector< core::dicom_dataset > matches
Matching datasets returned by the SCP.
Definition query_scu.h:93
Configuration for Query SCU service.
Definition query_scu.h:168
std::chrono::milliseconds timeout
Timeout for receiving query responses (milliseconds)
Definition query_scu.h:176
query_model model
Query information model (Patient Root or Study Root)
Definition query_scu.h:170
bool cancel_on_max
Send C-CANCEL when max_results is reached.
Definition query_scu.h:182
size_t max_results
Maximum number of results to return (0 = unlimited)
Definition query_scu.h:179
query_level level
Query level (Patient, Study, Series, or Image)
Definition query_scu.h:173
Query keys for SERIES level queries.
Definition query_scu.h:145
std::string series_number
Series Number (0020,0011)
Definition query_scu.h:149
std::string series_uid
Series Instance UID (0020,000E)
Definition query_scu.h:147
std::string study_uid
Study Instance UID (0020,000D) - Required.
Definition query_scu.h:146
std::string modality
Modality (0008,0060)
Definition query_scu.h:148
Query keys for STUDY level queries.
Definition query_scu.h:133
std::string accession_number
Accession Number (0008,0050)
Definition query_scu.h:137
std::string study_uid
Study Instance UID (0020,000D)
Definition query_scu.h:135
std::string study_description
Study Description (0008,1030)
Definition query_scu.h:139
std::string modality
Modalities in Study (0008,0061)
Definition query_scu.h:138
std::string patient_id
Patient ID (0010,0020) - for filtering.
Definition query_scu.h:134
std::string study_date
Study Date (0008,0020) - YYYYMMDD or range.
Definition query_scu.h:136