PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
retrieve_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
25retrieve_scu::retrieve_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// C-MOVE Operations
34// =============================================================================
35
38 const core::dicom_dataset& query_keys,
39 std::string_view destination_ae,
41
42 return perform_move(assoc, query_keys, destination_ae, progress);
43}
44
47 const core::dicom_dataset& query_keys,
48 std::string_view destination_ae,
50
51 using namespace network::dimse;
52
53 auto start_time = std::chrono::steady_clock::now();
54 auto message_id = next_message_id();
55
56 // Verify association is established
57 if (!assoc.is_established()) {
60 "Association not established");
61 }
62
63 // Validate move destination
64 if (destination_ae.empty()) {
67 "Move destination AE title is required");
68 }
69
70 // Get SOP Class UID
71 auto sop_class_uid = get_move_sop_class_uid();
72
73 // Get accepted presentation context for this SOP class
74 auto context_id = assoc.accepted_context_id(sop_class_uid);
75 if (!context_id) {
78 "No accepted presentation context for SOP Class: " +
79 std::string(sop_class_uid));
80 }
81
82 // Build C-MOVE-RQ message
83 dimse_message request{command_field::c_move_rq, message_id};
84 request.set_affected_sop_class_uid(sop_class_uid);
85 request.set_priority(config_.priority);
86
87 // Set Move Destination
88 request.command_set().set_string(
89 tag_move_destination,
91 std::string(destination_ae));
92
93 request.set_dataset(query_keys);
94
95 logger_->debug_fmt("Sending C-MOVE request (message_id={}, dest={}, sop_class={})",
96 message_id, destination_ae, sop_class_uid);
97
98 // Send the request
99 auto send_result = assoc.send_dimse(*context_id, request);
100 if (send_result.is_err()) {
101 return send_result.error();
102 }
103
104 // Initialize progress tracking
106 prog.start_time = start_time;
107
108 // Receive responses
109 retrieve_result result;
110 bool move_complete = false;
111
112 while (!move_complete) {
113 auto recv_result = assoc.receive_dimse(config_.timeout);
114 if (recv_result.is_err()) {
115 return recv_result.error();
116 }
117
118 const auto& [recv_context_id, response] = recv_result.value();
119
120 // Verify it's a C-MOVE response
121 if (response.command() != command_field::c_move_rsp) {
124 "Expected C-MOVE-RSP but received " +
125 std::string(to_string(response.command())));
126 }
127
128 auto status = response.status();
129
130 // Extract sub-operation counts
131 if (auto remaining = response.remaining_subops()) {
132 prog.remaining = *remaining;
133 }
134 if (auto completed = response.completed_subops()) {
135 prog.completed = *completed;
136 result.completed = *completed;
137 }
138 if (auto failed = response.failed_subops()) {
139 prog.failed = *failed;
140 result.failed = *failed;
141 }
142 if (auto warning = response.warning_subops()) {
143 prog.warning = *warning;
144 result.warning = *warning;
145 }
146
147 // Call progress callback
148 if (progress) {
149 progress(prog);
150 }
151
152 // Check completion status
153 if (status == status_success) {
154 move_complete = true;
155 result.final_status = static_cast<uint16_t>(status);
156 } else if (status == status_cancel) {
157 move_complete = true;
158 result.final_status = static_cast<uint16_t>(status);
159 } else if ((status & 0xF000) == 0xA000 ||
160 (status & 0xF000) == 0xC000) {
161 // Error status (Refused or Failed)
162 move_complete = true;
163 result.final_status = static_cast<uint16_t>(status);
164 }
165 // status_pending (0xFF00) continues the loop
166 }
167
168 auto end_time = std::chrono::steady_clock::now();
169 result.elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
170 end_time - start_time);
171
172 // Update statistics
173 retrieves_performed_.fetch_add(1, std::memory_order_relaxed);
174 instances_retrieved_.fetch_add(result.completed, std::memory_order_relaxed);
175
176 logger_->debug_fmt("C-MOVE completed: {} completed, {} failed in {} ms",
177 result.completed, result.failed, result.elapsed.count());
178
179 return result;
180}
181
182// =============================================================================
183// C-GET Operations
184// =============================================================================
185
188 const core::dicom_dataset& query_keys,
190
191 return perform_get(assoc, query_keys, progress);
192}
193
196 const core::dicom_dataset& query_keys,
198
199 using namespace network::dimse;
200
201 auto start_time = std::chrono::steady_clock::now();
202 auto message_id = next_message_id();
203
204 // Verify association is established
205 if (!assoc.is_established()) {
208 "Association not established");
209 }
210
211 // Get SOP Class UID
212 auto sop_class_uid = get_get_sop_class_uid();
213
214 // Get accepted presentation context for this SOP class
215 auto context_id = assoc.accepted_context_id(sop_class_uid);
216 if (!context_id) {
219 "No accepted presentation context for SOP Class: " +
220 std::string(sop_class_uid));
221 }
222
223 // Build C-GET-RQ message
224 dimse_message request{command_field::c_get_rq, message_id};
225 request.set_affected_sop_class_uid(sop_class_uid);
226 request.set_priority(config_.priority);
227 request.set_dataset(query_keys);
228
229 logger_->debug_fmt("Sending C-GET request (message_id={}, sop_class={})",
230 message_id, sop_class_uid);
231
232 // Send the request
233 auto send_result = assoc.send_dimse(*context_id, request);
234 if (send_result.is_err()) {
235 return send_result.error();
236 }
237
238 // Initialize progress tracking
240 prog.start_time = start_time;
241
242 // Receive responses (C-GET-RSP and C-STORE-RQ interleaved)
243 retrieve_result result;
244 bool retrieve_complete = false;
245
246 while (!retrieve_complete) {
247 auto recv_result = assoc.receive_dimse(config_.timeout);
248 if (recv_result.is_err()) {
249 return recv_result.error();
250 }
251
252 auto& [recv_context_id, msg] = recv_result.value();
253 auto cmd = msg.command();
254
255 if (cmd == command_field::c_get_rsp) {
256 auto status = msg.status();
257
258 // Extract sub-operation counts
259 if (auto remaining = msg.remaining_subops()) {
260 prog.remaining = *remaining;
261 }
262 if (auto completed = msg.completed_subops()) {
263 prog.completed = *completed;
264 result.completed = *completed;
265 }
266 if (auto failed = msg.failed_subops()) {
267 prog.failed = *failed;
268 result.failed = *failed;
269 }
270 if (auto warning = msg.warning_subops()) {
271 prog.warning = *warning;
272 result.warning = *warning;
273 }
274
275 // Call progress callback
276 if (progress) {
277 progress(prog);
278 }
279
280 // Check completion status
281 if (status == status_success) {
282 retrieve_complete = true;
283 result.final_status = static_cast<uint16_t>(status);
284 } else if (status == status_cancel) {
285 retrieve_complete = true;
286 result.final_status = static_cast<uint16_t>(status);
287 } else if ((status & 0xF000) == 0xA000 ||
288 (status & 0xF000) == 0xC000) {
289 // Error status
290 retrieve_complete = true;
291 result.final_status = static_cast<uint16_t>(status);
292 }
293
294 } else if (cmd == command_field::c_store_rq) {
295 // Incoming C-STORE sub-operation - store the received instance
296 if (msg.has_dataset()) {
297 auto dataset_result = msg.dataset();
298 if (dataset_result.is_ok()) {
299 result.received_instances.push_back(dataset_result.value().get());
300
301 // Update bytes transferred (approximate)
302 bytes_retrieved_.fetch_add(1024, std::memory_order_relaxed);
303 }
304 }
305
306 // Send C-STORE response
307 auto sop_class = msg.affected_sop_class_uid();
308 auto sop_instance = msg.affected_sop_instance_uid();
309
310 auto store_rsp = make_c_store_rsp(
311 msg.message_id(),
312 sop_class,
313 sop_instance,
314 status_success
315 );
316
317 auto send_rsp_result = assoc.send_dimse(recv_context_id, store_rsp);
318 if (send_rsp_result.is_err()) {
319 logger_->warn_fmt("Failed to send C-STORE response: {}",
320 send_rsp_result.error().message);
321 }
322 }
323 }
324
325 auto end_time = std::chrono::steady_clock::now();
326 result.elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
327 end_time - start_time);
328
329 // Update statistics
330 retrieves_performed_.fetch_add(1, std::memory_order_relaxed);
331 instances_retrieved_.fetch_add(result.completed, std::memory_order_relaxed);
332
333 logger_->debug_fmt("C-GET completed: {} completed, {} failed, {} received in {} ms",
334 result.completed, result.failed,
335 result.received_instances.size(), result.elapsed.count());
336
337 return result;
338}
339
340// =============================================================================
341// Convenience Methods
342// =============================================================================
343
346 std::string_view study_uid,
348
349 auto query_ds = build_study_query(study_uid);
350
352 if (config_.move_destination.empty()) {
355 "Move destination is required for C-MOVE mode");
356 }
357 return move(assoc, query_ds, config_.move_destination, progress);
358 } else {
359 return get(assoc, query_ds, progress);
360 }
361}
362
365 std::string_view series_uid,
367
368 auto query_ds = build_series_query(series_uid);
369
371 if (config_.move_destination.empty()) {
374 "Move destination is required for C-MOVE mode");
375 }
376 return move(assoc, query_ds, config_.move_destination, progress);
377 } else {
378 return get(assoc, query_ds, progress);
379 }
380}
381
384 std::string_view sop_instance_uid,
386
387 auto query_ds = build_instance_query(sop_instance_uid);
388
390 if (config_.move_destination.empty()) {
393 "Move destination is required for C-MOVE mode");
394 }
395 return move(assoc, query_ds, config_.move_destination, progress);
396 } else {
397 return get(assoc, query_ds, progress);
398 }
399}
400
401// =============================================================================
402// C-CANCEL Support
403// =============================================================================
404
407 uint16_t message_id) {
408
409 using namespace network::dimse;
410
411 auto sop_class_uid = (config_.mode == retrieve_mode::c_move)
414
415 auto context_id = assoc.accepted_context_id(sop_class_uid);
416 if (!context_id) {
419 "No accepted presentation context for cancel");
420 }
421
422 // Build C-CANCEL-RQ message
423 dimse_message cancel_rq{command_field::c_cancel_rq, message_id};
424 return assoc.send_dimse(*context_id, cancel_rq);
425}
426
427// =============================================================================
428// Configuration
429// =============================================================================
430
434
435void retrieve_scu::set_move_destination(std::string_view ae_title) {
436 config_.move_destination = std::string(ae_title);
437}
438
440 return config_;
441}
442
443// =============================================================================
444// Statistics
445// =============================================================================
446
447size_t retrieve_scu::retrieves_performed() const noexcept {
448 return retrieves_performed_.load(std::memory_order_relaxed);
449}
450
451size_t retrieve_scu::instances_retrieved() const noexcept {
452 return instances_retrieved_.load(std::memory_order_relaxed);
453}
454
455size_t retrieve_scu::bytes_retrieved() const noexcept {
456 return bytes_retrieved_.load(std::memory_order_relaxed);
457}
458
460 retrieves_performed_.store(0, std::memory_order_relaxed);
461 instances_retrieved_.store(0, std::memory_order_relaxed);
462 bytes_retrieved_.store(0, std::memory_order_relaxed);
463}
464
465// =============================================================================
466// Private Implementation
467// =============================================================================
468
469uint16_t retrieve_scu::next_message_id() noexcept {
470 uint16_t id = message_id_counter_.fetch_add(1, std::memory_order_relaxed);
471 // Wrap around at 0xFFFF, skip 0 (reserved)
472 if (id == 0) {
473 id = message_id_counter_.fetch_add(1, std::memory_order_relaxed);
474 }
475 return id;
476}
477
478std::string_view retrieve_scu::get_move_sop_class_uid() const noexcept {
479 switch (config_.model) {
484 default:
486 }
487}
488
489std::string_view retrieve_scu::get_get_sop_class_uid() const noexcept {
490 switch (config_.model) {
495 default:
497 }
498}
499
501 std::string_view study_uid) const {
502
503 using namespace core;
504 using namespace encoding;
505
506 dicom_dataset ds;
507 ds.set_string(tags::query_retrieve_level, vr_type::CS, "STUDY");
508 ds.set_string(tags::study_instance_uid, vr_type::UI, std::string(study_uid));
509
510 return ds;
511}
512
514 std::string_view series_uid) const {
515
516 using namespace core;
517 using namespace encoding;
518
519 dicom_dataset ds;
520 ds.set_string(tags::query_retrieve_level, vr_type::CS, "SERIES");
521 ds.set_string(tags::series_instance_uid, vr_type::UI, std::string(series_uid));
522
523 return ds;
524}
525
527 std::string_view sop_instance_uid) const {
528
529 using namespace core;
530 using namespace encoding;
531
532 dicom_dataset ds;
533 ds.set_string(tags::query_retrieve_level, vr_type::CS, "IMAGE");
534 ds.set_string(tags::sop_instance_uid, vr_type::UI, std::string(sop_instance_uid));
535
536 return ds;
537}
538
539} // 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.
network::Result< retrieve_result > retrieve_instance(network::association &assoc, std::string_view sop_instance_uid, retrieve_progress_callback progress=nullptr)
Retrieve a single instance by SOP Instance UID.
network::Result< std::monostate > cancel(network::association &assoc, uint16_t message_id)
Send a C-CANCEL request to stop an ongoing retrieve.
std::atomic< size_t > instances_retrieved_
Statistics: total number of instances retrieved.
network::Result< retrieve_result > retrieve_series(network::association &assoc, std::string_view series_uid, retrieve_progress_callback progress=nullptr)
Retrieve a series by Series Instance UID.
retrieve_scu_config config_
Configuration.
network::Result< retrieve_result > perform_get(network::association &assoc, const core::dicom_dataset &query_keys, retrieve_progress_callback progress)
Internal C-GET implementation.
const retrieve_scu_config & config() const noexcept
Get the current configuration.
size_t retrieves_performed() const noexcept
Get the number of retrieves performed since construction.
core::dicom_dataset build_instance_query(std::string_view sop_instance_uid) const
Build query dataset for instance retrieval.
network::Result< retrieve_result > retrieve_study(network::association &assoc, std::string_view study_uid, retrieve_progress_callback progress=nullptr)
Retrieve a study by Study Instance UID.
void set_move_destination(std::string_view ae_title)
Set the move destination AE title.
network::Result< retrieve_result > get(network::association &assoc, const core::dicom_dataset &query_keys, retrieve_progress_callback progress=nullptr)
Perform a C-GET operation with raw dataset.
void reset_statistics() noexcept
Reset statistics counters to zero.
std::string_view get_get_sop_class_uid() const noexcept
Get GET SOP Class UID based on current configuration.
std::atomic< size_t > retrieves_performed_
Statistics: number of retrieves performed.
size_t instances_retrieved() const noexcept
Get the total number of instances retrieved since construction.
uint16_t next_message_id() noexcept
Get the next message ID for DIMSE operations.
std::atomic< uint16_t > message_id_counter_
Message ID counter.
std::atomic< size_t > bytes_retrieved_
Statistics: total bytes retrieved.
void set_config(const retrieve_scu_config &config)
Update the SCU configuration.
network::Result< retrieve_result > move(network::association &assoc, const core::dicom_dataset &query_keys, std::string_view destination_ae, retrieve_progress_callback progress=nullptr)
Perform a C-MOVE operation with raw dataset.
network::Result< retrieve_result > perform_move(network::association &assoc, const core::dicom_dataset &query_keys, std::string_view destination_ae, retrieve_progress_callback progress)
Internal C-MOVE implementation.
std::string_view get_move_sop_class_uid() const noexcept
Get MOVE SOP Class UID based on current configuration.
std::shared_ptr< di::ILogger > logger_
Logger instance for service logging.
size_t bytes_retrieved() const noexcept
Get the total bytes retrieved since construction (C-GET only)
core::dicom_dataset build_study_query(std::string_view study_uid) const
Build query dataset for study retrieval.
core::dicom_dataset build_series_query(std::string_view series_uid) const
Build query dataset for series retrieval.
retrieve_scu(std::shared_ptr< di::ILogger > logger=nullptr)
Construct a Retrieve SCU with default configuration.
DIMSE command field enumeration.
Compile-time constants for commonly used DICOM tags.
constexpr dicom_tag sop_instance_uid
SOP Instance UID.
constexpr dicom_tag query_retrieve_level
Query/Retrieve Level.
constexpr dicom_tag study_instance_uid
Study Instance UID.
constexpr dicom_tag series_instance_uid
Series Instance UID.
@ AE
Application Entity (16 chars max)
constexpr int no_acceptable_context
Definition result.h:103
constexpr int retrieve_missing_destination
Definition result.h:162
constexpr int retrieve_unexpected_command
Definition result.h:165
constexpr int association_not_established
Definition result.h:202
@ warning
Printer has a non-critical issue.
@ c_move
Request SCP to send to third party (requires move destination)
@ completed
Procedure completed successfully.
std::function< void(const retrieve_progress &)> retrieve_progress_callback
Callback type for retrieve progress updates.
constexpr std::string_view study_root_move_sop_class_uid
Study Root Query/Retrieve Information Model - MOVE.
constexpr std::string_view study_root_get_sop_class_uid
Study Root Query/Retrieve Information Model - GET.
auto to_string(mpps_status status) -> std::string_view
Convert mpps_status to DICOM string representation.
Definition mpps_scp.h:60
constexpr std::string_view patient_root_move_sop_class_uid
Patient Root Query/Retrieve Information Model - MOVE.
constexpr std::string_view patient_root_get_sop_class_uid
Patient Root Query/Retrieve Information Model - GET.
@ study_root
Study Root Query/Retrieve Information Model.
@ patient_root
Patient Root Query/Retrieve Information Model.
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
Result<T> type aliases and helpers for PACS system.
DICOM Retrieve SCU service (C-MOVE/C-GET sender)
DIMSE status codes.
Progress information for a retrieve operation.
uint16_t failed
Number of failed sub-operations.
uint16_t remaining
Number of remaining sub-operations.
uint16_t completed
Number of completed sub-operations.
uint16_t warning
Number of sub-operations with warnings.
std::chrono::steady_clock::time_point start_time
Result of a retrieve operation (C-MOVE or C-GET)
uint16_t warning
Number of sub-operations with warnings.
uint16_t completed
Number of successfully completed sub-operations.
uint16_t failed
Number of failed sub-operations.
uint16_t final_status
Final DIMSE status code.
std::vector< core::dicom_dataset > received_instances
Received instances (for C-GET mode only)
std::chrono::milliseconds elapsed
Retrieve execution time.
Configuration for Retrieve SCU service.
std::chrono::milliseconds timeout
Timeout for receiving responses (milliseconds)
query_model model
Query information model (Patient Root or Study Root)
retrieve_mode mode
Retrieve mode (C-MOVE or C-GET)
uint16_t priority
Priority for DIMSE operations (0=medium, 1=high, 2=low)
std::string move_destination
Move destination AE title (required for C-MOVE mode)