PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
retrieve_scp.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
11
17
18#include <kcenon/common/patterns/event_bus.h>
19
20namespace kcenon::pacs::services {
21
22// =============================================================================
23// Construction
24// =============================================================================
25
26retrieve_scp::retrieve_scp(std::shared_ptr<di::ILogger> logger)
27 : scp_service(std::move(logger)) {}
28
29// =============================================================================
30// Configuration
31// =============================================================================
32
34 retrieve_handler_ = std::move(handler);
35}
36
40
44
48
49// =============================================================================
50// scp_service Interface Implementation
51// =============================================================================
52
53std::vector<std::string> retrieve_scp::supported_sop_classes() const {
54 return {
59 };
60}
61
64 uint8_t context_id,
65 const network::dimse::dimse_message& request) {
66
67 using namespace network::dimse;
68
69 // Route to appropriate handler based on command type
70 switch (request.command()) {
71 case command_field::c_move_rq:
72 return handle_c_move(assoc, context_id, request);
73
74 case command_field::c_get_rq:
75 return handle_c_get(assoc, context_id, request);
76
77 default:
80 "Expected C-MOVE-RQ or C-GET-RQ but received " +
81 std::string(to_string(request.command())));
82 }
83}
84
85std::string_view retrieve_scp::service_name() const noexcept {
86 return "Retrieve SCP";
87}
88
89// =============================================================================
90// Statistics
91// =============================================================================
92
93size_t retrieve_scp::move_operations() const noexcept {
94 return move_operations_.load();
95}
96
97size_t retrieve_scp::get_operations() const noexcept {
98 return get_operations_.load();
99}
100
101size_t retrieve_scp::images_transferred() const noexcept {
102 return images_transferred_.load();
103}
104
110
111// =============================================================================
112// Private Implementation - C-MOVE
113// =============================================================================
114
117 uint8_t context_id,
118 const network::dimse::dimse_message& request) {
119
120 using namespace network::dimse;
121
122 // Verify we have a retrieve handler
123 if (!retrieve_handler_) {
126 "No retrieve handler configured");
127 }
128
129 // Verify we have a destination resolver for C-MOVE
133 "No destination resolver configured for C-MOVE");
134 }
135
136 auto sop_class_uid = request.affected_sop_class_uid();
137 auto message_id = request.message_id();
138
139 // Verify we have a dataset (query keys)
140 if (!request.has_dataset()) {
142 return send_final_response(
143 assoc, context_id, message_id,
144 sop_class_uid, true, stats, false);
145 }
146
147 // Get move destination AE title
148 auto dest_ae = get_move_destination(request);
149 if (dest_ae.empty()) {
150 // Missing Move Destination - return error status
152
153 dimse_message response{command_field::c_move_rsp, 0};
154 response.set_affected_sop_class_uid(sop_class_uid);
155 response.set_message_id_responded_to(message_id);
156 response.set_status(status_refused_move_destination_unknown);
157
158 return assoc.send_dimse(context_id, response);
159 }
160
161 // Resolve destination to network address
162 auto dest_addr = destination_resolver_(dest_ae);
163 if (!dest_addr.has_value()) {
164 // Unknown destination AE - return error status
166
167 dimse_message response{command_field::c_move_rsp, 0};
168 response.set_affected_sop_class_uid(sop_class_uid);
169 response.set_message_id_responded_to(message_id);
170 response.set_status(status_refused_move_destination_unknown);
171
172 return assoc.send_dimse(context_id, response);
173 }
174
175 // Get calling AE for move originator information
176 std::string calling_ae{assoc.calling_ae()};
177
178 // Retrieve matching files
179 const auto& query_keys = request.dataset().value().get();
180 auto files = retrieve_handler_(query_keys);
181 auto start_time = std::chrono::steady_clock::now();
182
183 // Get study UID for event
184 std::string study_uid = query_keys.get_string(core::tags::study_instance_uid);
185
186 // Publish retrieve started event
187 kcenon::common::get_event_bus().publish(
190 calling_ae,
191 dest_ae,
192 study_uid,
193 static_cast<uint16_t>(files.size())
194 }
195 );
196
197 // Initialize sub-operation statistics
199 stats.remaining = static_cast<uint16_t>(files.size());
200 bool was_cancelled = false;
201
202 // Process each file (C-STORE sub-operations)
203 for (const auto& file : files) {
204 // Check for cancel request
205 if (cancel_check_ && cancel_check_()) {
206 was_cancelled = true;
207 break;
208 }
209
210 // Send pending response with current progress
211 auto pending_result = send_pending_response(
212 assoc, context_id, message_id,
213 sop_class_uid, true, stats);
214
215 if (pending_result.is_err()) {
216 return pending_result;
217 }
218
219 // Perform C-STORE sub-operation
220 // Note: In a full implementation, this would establish a sub-association
221 // to the destination and perform C-STORE. For now, we simulate success
222 // or use the provided store handler.
223 status_code store_status = status_success;
224
225 if (store_handler_) {
226 // Use custom store handler
227 // The handler should establish connection to dest_addr and perform C-STORE
228 // For now, we pass the current association (which isn't correct for C-MOVE)
229 // A real implementation would create a separate association to destination
230 store_status = store_handler_(
231 assoc, context_id, file,
232 calling_ae, message_id);
233 }
234
235 // Update statistics based on store result
236 stats.remaining--;
237 if (is_success(store_status)) {
238 stats.completed++;
240 } else if (is_warning(store_status)) {
241 stats.warning++;
243 } else {
244 stats.failed++;
245 }
246 }
247
248 // Update operation count
250
251 // Calculate duration and publish completed event
252 auto end_time = std::chrono::steady_clock::now();
253 auto duration_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
254 end_time - start_time).count();
255
256 kcenon::common::get_event_bus().publish(
259 calling_ae,
260 dest_ae,
261 stats.completed,
262 stats.failed,
263 stats.warning,
264 static_cast<uint64_t>(duration_ms)
265 }
266 );
267
268 // Send final response
269 return send_final_response(
270 assoc, context_id, message_id,
271 sop_class_uid, true, stats, was_cancelled);
272}
273
274// =============================================================================
275// Private Implementation - C-GET
276// =============================================================================
277
280 uint8_t context_id,
281 const network::dimse::dimse_message& request) {
282
283 using namespace network::dimse;
284
285 // Verify we have a retrieve handler
286 if (!retrieve_handler_) {
289 "No retrieve handler configured");
290 }
291
292 auto sop_class_uid = request.affected_sop_class_uid();
293 auto message_id = request.message_id();
294
295 // Verify we have a dataset (query keys)
296 if (!request.has_dataset()) {
298 return send_final_response(
299 assoc, context_id, message_id,
300 sop_class_uid, false, stats, false);
301 }
302
303 // Get calling AE for logging
304 std::string calling_ae{assoc.calling_ae()};
305
306 // Retrieve matching files
307 const auto& query_keys = request.dataset().value().get();
308 auto files = retrieve_handler_(query_keys);
309 auto start_time = std::chrono::steady_clock::now();
310
311 // Get study UID for event
312 std::string study_uid = query_keys.get_string(core::tags::study_instance_uid);
313
314 // Publish retrieve started event (C-GET has no destination AE)
315 kcenon::common::get_event_bus().publish(
318 calling_ae,
319 "", // No destination for C-GET
320 study_uid,
321 static_cast<uint16_t>(files.size())
322 }
323 );
324
325 // Initialize sub-operation statistics
327 stats.remaining = static_cast<uint16_t>(files.size());
328 bool was_cancelled = false;
329
330 // Process each file (C-STORE sub-operations on same association)
331 for (const auto& file : files) {
332 // Check for cancel request
333 if (cancel_check_ && cancel_check_()) {
334 was_cancelled = true;
335 break;
336 }
337
338 // Send pending response with current progress
339 auto pending_result = send_pending_response(
340 assoc, context_id, message_id,
341 sop_class_uid, false, stats);
342
343 if (pending_result.is_err()) {
344 return pending_result;
345 }
346
347 // Perform C-STORE sub-operation on the same association
348 // For C-GET, images are sent back on the same association
349 status_code store_status = status_success;
350
351 if (store_handler_) {
352 // Use custom store handler
353 store_status = store_handler_(
354 assoc, context_id, file,
355 calling_ae, message_id);
356 } else {
357 // Default implementation: build and send C-STORE-RQ
358 // Get the SOP Class and Instance UIDs from the file
359 auto file_sop_class = file.sop_class_uid();
360 auto file_sop_instance = file.sop_instance_uid();
361
362 // Create C-STORE request
363 dimse_message store_rq{command_field::c_store_rq, message_id};
364 store_rq.set_affected_sop_class_uid(file_sop_class);
365 store_rq.set_affected_sop_instance_uid(file_sop_instance);
366 store_rq.set_priority(priority_medium);
367
368 // For C-GET, include Move Originator information
369 // (0000,1030) Move Originator Application Entity Title
370 // (0000,1031) Move Originator Message ID
371 store_rq.command_set().set_string(
372 tag_move_originator_aet,
374 calling_ae);
375 store_rq.command_set().set_numeric<uint16_t>(
376 tag_move_originator_message_id,
378 message_id);
379
380 // Attach the dataset
381 store_rq.set_dataset(file.dataset());
382
383 // Find the presentation context for the SOP Class
384 auto store_context_id = assoc.accepted_context_id(file_sop_class);
385 if (!store_context_id.has_value()) {
386 // No accepted context for this SOP Class
387 stats.remaining--;
388 stats.failed++;
389 continue;
390 }
391
392 // Send the C-STORE request
393 auto send_result = assoc.send_dimse(store_context_id.value(), store_rq);
394 if (send_result.is_err()) {
395 stats.remaining--;
396 stats.failed++;
397 continue;
398 }
399
400 // Wait for C-STORE response
401 auto recv_result = assoc.receive_dimse();
402 if (recv_result.is_err()) {
403 stats.remaining--;
404 stats.failed++;
405 continue;
406 }
407
408 auto& [recv_ctx, store_rsp] = recv_result.value();
409 store_status = store_rsp.status();
410 }
411
412 // Update statistics based on store result
413 stats.remaining--;
414 if (is_success(store_status)) {
415 stats.completed++;
417 } else if (is_warning(store_status)) {
418 stats.warning++;
420 } else {
421 stats.failed++;
422 }
423 }
424
425 // Update operation count
427
428 // Calculate duration and publish completed event
429 auto end_time = std::chrono::steady_clock::now();
430 auto duration_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
431 end_time - start_time).count();
432
433 kcenon::common::get_event_bus().publish(
436 calling_ae,
437 "", // No destination for C-GET
438 stats.completed,
439 stats.failed,
440 stats.warning,
441 static_cast<uint64_t>(duration_ms)
442 }
443 );
444
445 // Send final response
446 return send_final_response(
447 assoc, context_id, message_id,
448 sop_class_uid, false, stats, was_cancelled);
449}
450
451// =============================================================================
452// Private Implementation - Response Helpers
453// =============================================================================
454
457 uint8_t context_id,
458 uint16_t message_id,
459 std::string_view sop_class_uid,
460 bool is_move,
461 const sub_operation_stats& stats) {
462
463 using namespace network::dimse;
464
465 // Create response message
466 auto cmd = is_move ? command_field::c_move_rsp : command_field::c_get_rsp;
467 dimse_message response{cmd, 0};
468
469 response.set_affected_sop_class_uid(sop_class_uid);
470 response.set_message_id_responded_to(message_id);
471 response.set_status(status_pending);
472
473 // Set sub-operation counts
474 response.set_remaining_subops(stats.remaining);
475 response.set_completed_subops(stats.completed);
476 response.set_failed_subops(stats.failed);
477 response.set_warning_subops(stats.warning);
478
479 // Send the response
480 return assoc.send_dimse(context_id, response);
481}
482
485 uint8_t context_id,
486 uint16_t message_id,
487 std::string_view sop_class_uid,
488 bool is_move,
489 const sub_operation_stats& stats,
490 bool was_cancelled) {
491
492 using namespace network::dimse;
493
494 // Create response message
495 auto cmd = is_move ? command_field::c_move_rsp : command_field::c_get_rsp;
496 dimse_message response{cmd, 0};
497
498 response.set_affected_sop_class_uid(sop_class_uid);
499 response.set_message_id_responded_to(message_id);
500
501 // Determine final status
502 status_code final_status;
503 if (was_cancelled) {
504 final_status = status_cancel;
505 } else if (stats.failed > 0 && stats.completed == 0 && stats.warning == 0) {
506 // All sub-operations failed
507 final_status = status_refused_out_of_resources_subops;
508 } else if (stats.failed > 0 || stats.warning > 0) {
509 // Some sub-operations had issues
510 final_status = status_warning_subops_complete_failures;
511 } else {
512 // All successful
513 final_status = status_success;
514 }
515
516 response.set_status(final_status);
517
518 // Set final sub-operation counts
519 response.set_remaining_subops(stats.remaining);
520 response.set_completed_subops(stats.completed);
521 response.set_failed_subops(stats.failed);
522 response.set_warning_subops(stats.warning);
523
524 // Send the response
525 return assoc.send_dimse(context_id, response);
526}
527
529 const network::dimse::dimse_message& request) const {
530
531 // Move Destination is in the command set at tag (0000,0600)
532 return request.command_set().get_string(network::dimse::tag_move_destination);
533}
534
535} // namespace kcenon::pacs::services
Result< std::monostate > send_dimse(uint8_t context_id, const dimse::dimse_message &msg)
Send a DIMSE message.
std::string_view calling_ae() const noexcept
Get calling AE title.
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.
auto message_id() const noexcept -> uint16_t
Get the message ID.
auto affected_sop_class_uid() const -> std::string
Get the Affected SOP Class UID.
auto command_set() noexcept -> core::dicom_dataset &
Get mutable reference to the command set.
auto has_dataset() const noexcept -> bool
Check if the message has an associated data set.
auto dataset() -> kcenon::pacs::Result< std::reference_wrapper< core::dicom_dataset > >
Get mutable reference to the data set.
auto command() const noexcept -> command_field
Get the command field.
std::string_view service_name() const noexcept override
Get the service name.
network::Result< std::monostate > handle_message(network::association &assoc, uint8_t context_id, const network::dimse::dimse_message &request) override
Handle an incoming DIMSE message (C-MOVE-RQ or C-GET-RQ)
std::string get_move_destination(const network::dimse::dimse_message &request) const
Get the Move Destination AE title from the request.
void set_destination_resolver(destination_resolver resolver)
Set the destination resolver function.
network::Result< std::monostate > send_pending_response(network::association &assoc, uint8_t context_id, uint16_t message_id, std::string_view sop_class_uid, bool is_move, const sub_operation_stats &stats)
Send a pending response with progress information.
std::atomic< size_t > get_operations_
size_t get_operations() const noexcept
Get total number of C-GET operations processed.
void reset_statistics() noexcept
Reset statistics counters.
size_t images_transferred() const noexcept
Get total number of images transferred.
void set_cancel_check(retrieve_cancel_check check)
Set the cancel check function.
retrieve_cancel_check cancel_check_
std::atomic< size_t > images_transferred_
std::vector< std::string > supported_sop_classes() const override
Get supported SOP Class UIDs.
network::Result< std::monostate > handle_c_get(network::association &assoc, uint8_t context_id, const network::dimse::dimse_message &request)
Handle a C-GET request.
network::Result< std::monostate > send_final_response(network::association &assoc, uint8_t context_id, uint16_t message_id, std::string_view sop_class_uid, bool is_move, const sub_operation_stats &stats, bool was_cancelled)
Send a final response with completion status.
network::Result< std::monostate > handle_c_move(network::association &assoc, uint8_t context_id, const network::dimse::dimse_message &request)
Handle a C-MOVE request.
destination_resolver destination_resolver_
void set_retrieve_handler(retrieve_handler handler)
Set the retrieve handler function.
std::atomic< size_t > move_operations_
size_t move_operations() const noexcept
Get total number of C-MOVE operations processed.
void set_store_sub_operation(store_sub_operation handler)
Set the store sub-operation handler.
retrieve_scp(std::shared_ptr< di::ILogger > logger=nullptr)
Construct a Retrieve SCP with optional logger.
DIMSE command field enumeration.
Compile-time constants for commonly used DICOM tags.
DICOM event definitions for event-based communication.
constexpr dicom_tag study_instance_uid
Study Instance UID.
@ US
Unsigned Short (2 bytes)
@ AE
Application Entity (16 chars max)
constexpr int retrieve_handler_not_set
Definition result.h:161
constexpr int retrieve_missing_destination
Definition result.h:162
constexpr int retrieve_unexpected_command
Definition result.h:165
constexpr core::dicom_tag tag_move_destination
Move Destination (0000,0600) - AE.
constexpr bool is_success(storage_status status) noexcept
Check if the status indicates success.
std::function< std::optional< std::pair< std::string, uint16_t > >( const std::string &ae_title)> destination_resolver
Destination resolver function type.
std::function< network::dimse::status_code( network::association &assoc, uint8_t context_id, const core::dicom_file &file, const std::string &move_originator_ae, uint16_t move_originator_msg_id)> store_sub_operation
Store sub-operation function type.
std::function< std::vector< core::dicom_file >( const core::dicom_dataset &query_keys)> retrieve_handler
Retrieve handler function type.
std::function< bool()> retrieve_cancel_check
Cancel check function type.
constexpr bool is_warning(storage_status status) noexcept
Check if the status indicates a warning.
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.
VoidResult pacs_void_error(int code, const std::string &message, const std::string &details="")
Create a PACS void error result.
Definition result.h:249
Result<T> type aliases and helpers for PACS system.
DICOM Retrieve SCP service (C-MOVE/C-GET handler)
DIMSE status codes.
Event published when a retrieve operation completes.
Definition events.h:271
Event published when a retrieve operation (C-MOVE/C-GET) starts.
Definition events.h:247
Statistics for C-MOVE/C-GET sub-operations.