PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
ups_push_scu.cpp
Go to the documentation of this file.
1
7
13
14#include <chrono>
15#include <random>
16#include <sstream>
17
18namespace kcenon::pacs::services {
19
20// =============================================================================
21// Local Helper Functions
22// =============================================================================
23
24namespace {
25
27constexpr const char* uid_root = "1.2.826.0.1.3680043.2.1545.3";
28
30constexpr core::dicom_tag tag_requested_sop_instance_uid{0x0000, 0x1001};
31
35network::dimse::dimse_message make_ups_n_create_rq(
36 uint16_t message_id,
37 std::string_view sop_instance_uid,
38 core::dicom_dataset dataset) {
39
40 using namespace network::dimse;
41
42 dimse_message msg{command_field::n_create_rq, message_id};
43 msg.set_affected_sop_class_uid(ups_push_sop_class_uid);
44 msg.set_affected_sop_instance_uid(sop_instance_uid);
45 msg.set_dataset(std::move(dataset));
46
47 return msg;
48}
49
53network::dimse::dimse_message make_ups_n_set_rq(
54 uint16_t message_id,
55 std::string_view sop_instance_uid,
56 core::dicom_dataset modifications) {
57
58 using namespace network::dimse;
59 using namespace encoding;
60
61 dimse_message msg{command_field::n_set_rq, message_id};
62 msg.set_affected_sop_class_uid(ups_push_sop_class_uid);
63 msg.command_set().set_string(
64 tag_requested_sop_instance_uid,
65 vr_type::UI,
66 std::string(sop_instance_uid));
67 msg.set_dataset(std::move(modifications));
68
69 return msg;
70}
71
72} // namespace
73
74// =============================================================================
75// Construction
76// =============================================================================
77
78ups_push_scu::ups_push_scu(std::shared_ptr<di::ILogger> logger)
79 : logger_(logger ? std::move(logger) : di::null_logger()) {}
80
82 std::shared_ptr<di::ILogger> logger)
83 : logger_(logger ? std::move(logger) : di::null_logger()),
84 config_(config) {}
85
86// =============================================================================
87// N-CREATE Operation
88// =============================================================================
89
92 const ups_create_data& data) {
93
94 using namespace network::dimse;
95
96 auto start_time = std::chrono::steady_clock::now();
97
98 // Verify association is established
99 if (!assoc.is_established()) {
102 "Association not established");
103 }
104
105 // Get accepted presentation context for UPS Push
106 auto context_id = assoc.accepted_context_id(ups_push_sop_class_uid);
107 if (!context_id) {
110 "No accepted presentation context for UPS Push SOP Class");
111 }
112
113 // Generate or use provided workitem UID
114 std::string uid = data.workitem_uid;
115 if (uid.empty() && config_.auto_generate_uid) {
117 }
118
119 if (uid.empty()) {
122 "Workitem UID is required");
123 }
124
125 // Build the creation dataset
126 auto dataset = build_create_dataset(data);
127
128 // Create the N-CREATE request
129 auto request = make_ups_n_create_rq(next_message_id(), uid, std::move(dataset));
130
131 logger_->debug("Sending N-CREATE request for UPS workitem: " + uid);
132
133 // Send the request
134 auto send_result = assoc.send_dimse(*context_id, request);
135 if (send_result.is_err()) {
136 logger_->error("Failed to send N-CREATE: " + send_result.error().message);
137 return send_result.error();
138 }
139
140 // Receive the response
141 auto recv_result = assoc.receive_dimse(config_.timeout);
142 if (recv_result.is_err()) {
143 logger_->error("Failed to receive N-CREATE response: " +
144 recv_result.error().message);
145 return recv_result.error();
146 }
147
148 const auto& [recv_context_id, response] = recv_result.value();
149
150 // Verify it's an N-CREATE response
151 if (response.command() != command_field::n_create_rsp) {
154 "Expected N-CREATE-RSP but received " +
155 std::string(to_string(response.command())));
156 }
157
158 auto end_time = std::chrono::steady_clock::now();
159 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
160 end_time - start_time);
161
162 // Build result
163 ups_result result;
164 result.workitem_uid = uid;
165 result.status = static_cast<uint16_t>(response.status());
166 result.elapsed = elapsed;
167
168 if (response.command_set().contains(tag_error_comment)) {
169 result.error_comment = response.command_set().get_string(tag_error_comment);
170 }
171
172 creates_performed_.fetch_add(1, std::memory_order_relaxed);
173
174 if (result.is_success()) {
175 logger_->info("N-CREATE successful for UPS workitem: " + uid);
176 } else {
177 logger_->warn("N-CREATE returned status 0x" +
178 std::to_string(result.status) +
179 " for UPS workitem: " + uid);
180 }
181
182 return result;
183}
184
185// =============================================================================
186// N-SET Operation
187// =============================================================================
188
191 const ups_set_data& data) {
192
193 using namespace network::dimse;
194
195 auto start_time = std::chrono::steady_clock::now();
196
197 if (!assoc.is_established()) {
200 "Association not established");
201 }
202
203 if (data.workitem_uid.empty()) {
206 "Workitem UID is required for N-SET");
207 }
208
209 auto context_id = assoc.accepted_context_id(ups_push_sop_class_uid);
210 if (!context_id) {
213 "No accepted presentation context for UPS Push SOP Class");
214 }
215
216 // Create the N-SET request with modification dataset
217 auto request = make_ups_n_set_rq(
219 data.workitem_uid,
221
222 logger_->debug("Sending N-SET request for UPS workitem: " + data.workitem_uid);
223
224 auto send_result = assoc.send_dimse(*context_id, request);
225 if (send_result.is_err()) {
226 logger_->error("Failed to send N-SET: " + send_result.error().message);
227 return send_result.error();
228 }
229
230 auto recv_result = assoc.receive_dimse(config_.timeout);
231 if (recv_result.is_err()) {
232 logger_->error("Failed to receive N-SET response: " +
233 recv_result.error().message);
234 return recv_result.error();
235 }
236
237 const auto& [recv_context_id, response] = recv_result.value();
238
239 if (response.command() != command_field::n_set_rsp) {
242 "Expected N-SET-RSP but received " +
243 std::string(to_string(response.command())));
244 }
245
246 auto end_time = std::chrono::steady_clock::now();
247 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
248 end_time - start_time);
249
250 ups_result result;
251 result.workitem_uid = data.workitem_uid;
252 result.status = static_cast<uint16_t>(response.status());
253 result.elapsed = elapsed;
254
255 if (response.command_set().contains(tag_error_comment)) {
256 result.error_comment = response.command_set().get_string(tag_error_comment);
257 }
258
259 sets_performed_.fetch_add(1, std::memory_order_relaxed);
260
261 if (result.is_success()) {
262 logger_->info("N-SET successful for UPS workitem: " + data.workitem_uid);
263 } else {
264 logger_->warn("N-SET returned status 0x" +
265 std::to_string(result.status) +
266 " for UPS workitem: " + data.workitem_uid);
267 }
268
269 return result;
270}
271
272// =============================================================================
273// N-GET Operation
274// =============================================================================
275
278 const ups_get_data& data) {
279
280 using namespace network::dimse;
281
282 auto start_time = std::chrono::steady_clock::now();
283
284 if (!assoc.is_established()) {
287 "Association not established");
288 }
289
290 if (data.workitem_uid.empty()) {
293 "Workitem UID is required for N-GET");
294 }
295
296 auto context_id = assoc.accepted_context_id(ups_push_sop_class_uid);
297 if (!context_id) {
300 "No accepted presentation context for UPS Push SOP Class");
301 }
302
303 // Use the global factory function for N-GET
304 auto request = make_n_get_rq(
307 data.workitem_uid,
308 data.attribute_tags);
309
310 logger_->debug("Sending N-GET request for UPS workitem: " + data.workitem_uid +
311 " (attributes: " +
312 (data.attribute_tags.empty() ? "all" :
313 std::to_string(data.attribute_tags.size())) + ")");
314
315 auto send_result = assoc.send_dimse(*context_id, request);
316 if (send_result.is_err()) {
317 logger_->error("Failed to send N-GET: " + send_result.error().message);
318 return send_result.error();
319 }
320
321 auto recv_result = assoc.receive_dimse(config_.timeout);
322 if (recv_result.is_err()) {
323 logger_->error("Failed to receive N-GET response: " +
324 recv_result.error().message);
325 return recv_result.error();
326 }
327
328 const auto& [recv_context_id, response] = recv_result.value();
329
330 if (response.command() != command_field::n_get_rsp) {
333 "Expected N-GET-RSP but received " +
334 std::string(to_string(response.command())));
335 }
336
337 auto end_time = std::chrono::steady_clock::now();
338 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
339 end_time - start_time);
340
341 ups_result result;
342 result.workitem_uid = data.workitem_uid;
343 result.status = static_cast<uint16_t>(response.status());
344 result.elapsed = elapsed;
345
346 if (response.command_set().contains(tag_error_comment)) {
347 result.error_comment = response.command_set().get_string(tag_error_comment);
348 }
349
350 // Extract returned attributes from response dataset
351 if (response.has_dataset()) {
352 auto dataset_result = response.dataset();
353 if (dataset_result.is_ok()) {
354 result.attributes = dataset_result.value().get();
355 }
356 }
357
358 gets_performed_.fetch_add(1, std::memory_order_relaxed);
359
360 if (result.is_success()) {
361 logger_->info("N-GET successful for UPS workitem: " + data.workitem_uid);
362 } else {
363 logger_->warn("N-GET returned status 0x" +
364 std::to_string(result.status) +
365 " for UPS workitem: " + data.workitem_uid);
366 }
367
368 return result;
369}
370
371// =============================================================================
372// N-ACTION Operations
373// =============================================================================
374
377 const ups_change_state_data& data) {
378
379 using namespace network::dimse;
380
381 auto start_time = std::chrono::steady_clock::now();
382
383 if (!assoc.is_established()) {
386 "Association not established");
387 }
388
389 if (data.workitem_uid.empty()) {
392 "Workitem UID is required for state change");
393 }
394
395 if (data.transaction_uid.empty()) {
398 "Transaction UID is required for state change");
399 }
400
401 auto context_id = assoc.accepted_context_id(ups_push_sop_class_uid);
402 if (!context_id) {
405 "No accepted presentation context for UPS Push SOP Class");
406 }
407
408 // Build the N-ACTION request with action dataset
409 auto request = make_n_action_rq(
412 data.workitem_uid,
414
415 // Attach dataset with state and transaction UID
416 auto action_dataset = build_change_state_dataset(data);
417 request.set_dataset(std::move(action_dataset));
418
419 logger_->debug("Sending N-ACTION (Change State) for UPS workitem: " +
420 data.workitem_uid + " -> " + data.requested_state);
421
422 auto send_result = assoc.send_dimse(*context_id, request);
423 if (send_result.is_err()) {
424 logger_->error("Failed to send N-ACTION: " + send_result.error().message);
425 return send_result.error();
426 }
427
428 auto recv_result = assoc.receive_dimse(config_.timeout);
429 if (recv_result.is_err()) {
430 logger_->error("Failed to receive N-ACTION response: " +
431 recv_result.error().message);
432 return recv_result.error();
433 }
434
435 const auto& [recv_context_id, response] = recv_result.value();
436
437 if (response.command() != command_field::n_action_rsp) {
440 "Expected N-ACTION-RSP but received " +
441 std::string(to_string(response.command())));
442 }
443
444 auto end_time = std::chrono::steady_clock::now();
445 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
446 end_time - start_time);
447
448 ups_result result;
449 result.workitem_uid = data.workitem_uid;
450 result.status = static_cast<uint16_t>(response.status());
451 result.elapsed = elapsed;
452
453 if (response.command_set().contains(tag_error_comment)) {
454 result.error_comment = response.command_set().get_string(tag_error_comment);
455 }
456
457 actions_performed_.fetch_add(1, std::memory_order_relaxed);
458
459 if (result.is_success()) {
460 logger_->info("N-ACTION (Change State) successful for UPS workitem: " +
461 data.workitem_uid + " -> " + data.requested_state);
462 } else {
463 logger_->warn("N-ACTION returned status 0x" +
464 std::to_string(result.status) +
465 " for UPS workitem: " + data.workitem_uid);
466 }
467
468 return result;
469}
470
473 const ups_request_cancel_data& data) {
474
475 using namespace network::dimse;
476
477 auto start_time = std::chrono::steady_clock::now();
478
479 if (!assoc.is_established()) {
482 "Association not established");
483 }
484
485 if (data.workitem_uid.empty()) {
488 "Workitem UID is required for cancel request");
489 }
490
491 auto context_id = assoc.accepted_context_id(ups_push_sop_class_uid);
492 if (!context_id) {
495 "No accepted presentation context for UPS Push SOP Class");
496 }
497
498 // Build the N-ACTION request for cancel
499 auto request = make_n_action_rq(
502 data.workitem_uid,
504
505 // Attach dataset with cancellation reason if provided
506 if (!data.reason.empty()) {
507 auto cancel_dataset = build_request_cancel_dataset(data);
508 request.set_dataset(std::move(cancel_dataset));
509 }
510
511 logger_->debug("Sending N-ACTION (Request Cancel) for UPS workitem: " +
512 data.workitem_uid);
513
514 auto send_result = assoc.send_dimse(*context_id, request);
515 if (send_result.is_err()) {
516 logger_->error("Failed to send N-ACTION: " + send_result.error().message);
517 return send_result.error();
518 }
519
520 auto recv_result = assoc.receive_dimse(config_.timeout);
521 if (recv_result.is_err()) {
522 logger_->error("Failed to receive N-ACTION response: " +
523 recv_result.error().message);
524 return recv_result.error();
525 }
526
527 const auto& [recv_context_id, response] = recv_result.value();
528
529 if (response.command() != command_field::n_action_rsp) {
532 "Expected N-ACTION-RSP but received " +
533 std::string(to_string(response.command())));
534 }
535
536 auto end_time = std::chrono::steady_clock::now();
537 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
538 end_time - start_time);
539
540 ups_result result;
541 result.workitem_uid = data.workitem_uid;
542 result.status = static_cast<uint16_t>(response.status());
543 result.elapsed = elapsed;
544
545 if (response.command_set().contains(tag_error_comment)) {
546 result.error_comment = response.command_set().get_string(tag_error_comment);
547 }
548
549 actions_performed_.fetch_add(1, std::memory_order_relaxed);
550
551 if (result.is_success()) {
552 logger_->info("N-ACTION (Request Cancel) successful for UPS workitem: " +
553 data.workitem_uid);
554 } else {
555 logger_->warn("N-ACTION (Request Cancel) returned status 0x" +
556 std::to_string(result.status) +
557 " for UPS workitem: " + data.workitem_uid);
558 }
559
560 return result;
561}
562
563// =============================================================================
564// Statistics
565// =============================================================================
566
567size_t ups_push_scu::creates_performed() const noexcept {
568 return creates_performed_.load(std::memory_order_relaxed);
569}
570
571size_t ups_push_scu::sets_performed() const noexcept {
572 return sets_performed_.load(std::memory_order_relaxed);
573}
574
575size_t ups_push_scu::gets_performed() const noexcept {
576 return gets_performed_.load(std::memory_order_relaxed);
577}
578
579size_t ups_push_scu::actions_performed() const noexcept {
580 return actions_performed_.load(std::memory_order_relaxed);
581}
582
584 creates_performed_.store(0, std::memory_order_relaxed);
585 sets_performed_.store(0, std::memory_order_relaxed);
586 gets_performed_.store(0, std::memory_order_relaxed);
587 actions_performed_.store(0, std::memory_order_relaxed);
588}
589
590// =============================================================================
591// Private Implementation - Dataset Building
592// =============================================================================
593
595 const ups_create_data& data) const {
596
597 using namespace core;
598 using namespace encoding;
599
600 dicom_dataset ds;
601
602 // Procedure Step State - always SCHEDULED for N-CREATE
603 ds.set_string(ups_tags::procedure_step_state, vr_type::CS, "SCHEDULED");
604
605 // Procedure Step Label (required)
606 if (!data.procedure_step_label.empty()) {
607 ds.set_string(ups_tags::procedure_step_label, vr_type::LO,
609 }
610
611 // Worklist Label
612 if (!data.worklist_label.empty()) {
613 ds.set_string(ups_tags::worklist_label, vr_type::LO,
614 data.worklist_label);
615 }
616
617 // Priority
618 if (!data.priority.empty()) {
619 ds.set_string(ups_tags::scheduled_procedure_step_priority, vr_type::CS,
620 data.priority);
621 }
622
623 // Scheduled start datetime
624 if (!data.scheduled_start_datetime.empty()) {
625 ds.set_string(
626 dicom_tag{0x0040, 0x4005}, vr_type::DT,
628 }
629
630 // Expected completion datetime
631 if (!data.expected_completion_datetime.empty()) {
632 ds.set_string(
633 dicom_tag{0x0040, 0x4011}, vr_type::DT,
635 }
636
637 // Scheduled Station Name
638 if (!data.scheduled_station_name.empty()) {
639 ds.set_string(
640 dicom_tag{0x0040, 0x4001}, vr_type::CS,
642 }
643
644 return ds;
645}
646
648 const ups_change_state_data& data) const {
649
650 using namespace core;
651 using namespace encoding;
652
653 dicom_dataset ds;
654
655 // Requested state
656 ds.set_string(ups_tags::procedure_step_state, vr_type::CS,
657 data.requested_state);
658
659 // Transaction UID
660 ds.set_string(ups_tags::transaction_uid, vr_type::UI,
661 data.transaction_uid);
662
663 return ds;
664}
665
667 const ups_request_cancel_data& data) const {
668
669 using namespace core;
670 using namespace encoding;
671
672 dicom_dataset ds;
673
674 if (!data.reason.empty()) {
675 ds.set_string(ups_tags::reason_for_cancellation, vr_type::LT,
676 data.reason);
677 }
678
679 return ds;
680}
681
682// =============================================================================
683// Private Implementation - Utility Functions
684// =============================================================================
685
687 static std::mt19937_64 gen{std::random_device{}()};
688 static std::uniform_int_distribution<uint64_t> dist;
689
690 auto now = std::chrono::system_clock::now();
691 auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
692 now.time_since_epoch()).count();
693
694 return std::string(uid_root) + "." + std::to_string(timestamp) +
695 "." + std::to_string(dist(gen) % 100000);
696}
697
698uint16_t ups_push_scu::next_message_id() noexcept {
699 uint16_t id = message_id_counter_.fetch_add(1, std::memory_order_relaxed);
700 if (id == 0) {
701 id = message_id_counter_.fetch_add(1, std::memory_order_relaxed);
702 }
703 return id;
704}
705
706} // namespace kcenon::pacs::services
auto get(dicom_tag tag) noexcept -> dicom_element *
Get a pointer to the element with the given tag.
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.
size_t creates_performed() const noexcept
network::Result< ups_result > request_cancel(network::association &assoc, const ups_request_cancel_data &data)
Request cancellation of a UPS workitem (N-ACTION Type 3)
std::atomic< size_t > gets_performed_
std::atomic< size_t > sets_performed_
core::dicom_dataset build_request_cancel_dataset(const ups_request_cancel_data &data) const
network::Result< ups_result > set(network::association &assoc, const ups_set_data &data)
Modify an existing UPS workitem (N-SET)
size_t sets_performed() const noexcept
size_t gets_performed() const noexcept
network::Result< ups_result > change_state(network::association &assoc, const ups_change_state_data &data)
Change UPS workitem state (N-ACTION Type 1)
std::atomic< size_t > actions_performed_
size_t actions_performed() const noexcept
ups_push_scu(std::shared_ptr< di::ILogger > logger=nullptr)
Construct UPS Push SCU with default configuration.
std::shared_ptr< di::ILogger > logger_
network::Result< ups_result > get(network::association &assoc, const ups_get_data &data)
Retrieve UPS workitem attributes from remote SCP (N-GET)
core::dicom_dataset build_create_dataset(const ups_create_data &data) const
core::dicom_dataset build_change_state_dataset(const ups_change_state_data &data) const
std::atomic< size_t > creates_performed_
std::atomic< uint16_t > message_id_counter_
network::Result< ups_result > create(network::association &assoc, const ups_create_data &data)
Create a new UPS workitem on the remote SCP (N-CREATE)
DIMSE command field enumeration.
Compile-time constants for commonly used DICOM tags.
constexpr dicom_tag message_id
Message ID.
constexpr int ups_unexpected_command
Definition result.h:210
constexpr int ups_context_not_accepted
Definition result.h:216
constexpr int ups_missing_transaction_uid
Definition result.h:215
constexpr int ups_missing_uid
Definition result.h:212
constexpr int association_not_established
Definition result.h:202
constexpr core::dicom_tag tag_requested_sop_instance_uid
Requested SOP Instance UID (0000,1001) - UI.
constexpr core::dicom_tag procedure_step_label
Procedure Step Label (0074,1204)
constexpr core::dicom_tag transaction_uid
Transaction UID (0008,1195)
constexpr core::dicom_tag procedure_step_state
Procedure Step State (0074,1000)
constexpr core::dicom_tag worklist_label
Worklist Label (0074,1202)
constexpr core::dicom_tag reason_for_cancellation
Reason for Cancellation (0074,1238)
constexpr core::dicom_tag scheduled_procedure_step_priority
Scheduled Procedure Step Priority (0074,1200)
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 ups_push_sop_class_uid
UPS Push SOP Class UID (PS3.4 Table CC.2-1)
constexpr uint16_t ups_action_change_state
N-ACTION Type 1: Change UPS State (PS3.4 CC.2.4)
constexpr uint16_t ups_action_request_cancel
N-ACTION Type 3: Request Cancellation (PS3.4 CC.2.5)
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.
DIMSE status codes.
Data for N-ACTION Type 1 (change UPS state)
std::string requested_state
Requested new state: "IN PROGRESS", "COMPLETED", "CANCELED".
std::string workitem_uid
Workitem SOP Instance UID (required)
std::string transaction_uid
Transaction UID (required for claiming/completing/canceling)
Data for N-CREATE operation (create new workitem)
std::string priority
Priority: LOW, MEDIUM, HIGH.
std::string procedure_step_label
Procedure Step Label (required)
std::string scheduled_station_name
Scheduled Station Name AE.
std::string expected_completion_datetime
Expected completion date/time (DICOM DT format)
std::string workitem_uid
Workitem SOP Instance UID (generated if empty)
std::string worklist_label
Worklist Label.
std::string scheduled_start_datetime
Scheduled start date/time (DICOM DT format)
Data for N-GET operation (retrieve workitem)
std::string workitem_uid
Workitem SOP Instance UID (required)
std::vector< core::dicom_tag > attribute_tags
Specific attribute tags to retrieve (empty = all)
Configuration for UPS Push SCU service.
std::chrono::milliseconds timeout
Timeout for receiving DIMSE response.
bool auto_generate_uid
Auto-generate workitem UID if not provided.
Data for N-ACTION Type 3 (request cancellation)
std::string reason
Reason for cancellation request (optional)
std::string workitem_uid
Workitem SOP Instance UID (required)
Result of a UPS SCU operation.
std::chrono::milliseconds elapsed
Time taken for the operation.
bool is_success() const noexcept
Check if the operation was successful.
std::string workitem_uid
Workitem SOP Instance UID.
uint16_t status
DIMSE status code (0x0000 = success)
core::dicom_dataset attributes
Response dataset (for N-GET operations)
std::string error_comment
Error comment from the SCP (if any)
Data for N-SET operation (modify workitem attributes)
core::dicom_dataset modifications
Modification dataset.
std::string workitem_uid
Workitem SOP Instance UID (required)
std::string_view uid
DICOM UPS (Unified Procedure Step) Push SCU service.