PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1
27
28#include <atomic>
29#include <chrono>
30#include <cstdlib>
31#include <filesystem>
32#include <iomanip>
33#include <iostream>
34#include <string>
35#include <thread>
36#include <vector>
37
38namespace {
39
41constexpr const char* default_calling_ae = "RETRIEVE_SCU";
42
44constexpr auto default_timeout = std::chrono::milliseconds{60000};
45
47constexpr int progress_bar_width = 40;
48
52enum class retrieve_mode {
53 c_move,
54 c_get
55};
56
60enum class retrieve_level {
61 patient,
62 study,
63 series,
64 image
65};
66
70enum class storage_structure {
71 hierarchical,
72 flat
73};
74
78struct options {
79 // Connection
80 std::string host;
81 uint16_t port{0};
82 std::string called_ae;
83 std::string calling_ae{default_calling_ae};
84
85 // Retrieve mode
86 retrieve_mode mode{retrieve_mode::c_get};
87 std::string query_model{"study"}; // "patient" or "study" root
88
89 // C-MOVE specific
90 std::string move_destination; // Destination AE for C-MOVE
91 uint16_t local_storage_port{0}; // Port for local Storage SCP (C-MOVE)
92
93 // Retrieve identifiers
94 retrieve_level level{retrieve_level::study};
95 std::string patient_id;
96 std::string study_uid;
97 std::string series_uid;
98 std::string sop_instance_uid;
99
100 // Output options
101 std::filesystem::path output_dir{"./downloads"};
102 storage_structure structure{storage_structure::hierarchical};
103 bool overwrite{false};
104 bool show_progress{true};
105 bool verbose{false};
106};
107
111void print_usage(const char* program_name) {
112 std::cout << R"(
113Retrieve SCU - DICOM C-MOVE/C-GET Client
114
115Usage: )" << program_name << R"( <host> <port> <called_ae> [options]
116
117Arguments:
118 host Remote host address (IP or hostname)
119 port Remote port number (typically 104 or 11112)
120 called_ae Called AE Title (remote SCP's AE title)
121
122Retrieve Mode:
123 --mode <mode> Retrieve mode: move, get (default: get)
124 move: Transfer to destination AE (requires --dest-ae)
125 get: Direct retrieval to local machine
126
127 --dest-ae <ae> Destination AE Title (for C-MOVE mode)
128 --local-port <port> Local Storage SCP port (for C-MOVE, default: auto)
129
130Query Model:
131 --model <model> Query model: patient, study (default: study)
132
133Retrieve Level and Identifiers:
134 --level <level> Retrieve level: PATIENT, STUDY, SERIES, IMAGE
135 (default: STUDY)
136 --patient-id <id> Patient ID (for PATIENT level)
137 --study-uid <uid> Study Instance UID
138 --series-uid <uid> Series Instance UID
139 --sop-instance-uid <uid> SOP Instance UID (for IMAGE level)
140
141Output Options:
142 --output, -o <dir> Output directory (default: ./downloads)
143 --structure <type> Storage structure: hierarchical, flat (default: hierarchical)
144 --overwrite Overwrite existing files (default: skip)
145 --no-progress Disable progress display
146
147General Options:
148 --calling-ae <ae> Calling AE Title (default: RETRIEVE_SCU)
149 --verbose, -v Show detailed progress
150 --help, -h Show this help message
151
152Examples:
153 # C-GET: Retrieve study directly
154 )" << program_name << R"( localhost 11112 PACS_SCP --mode get --study-uid "1.2.3.4.5" -o ./data
155
156 # C-MOVE: Transfer study to another PACS
157 )" << program_name << R"( localhost 11112 PACS_SCP --mode move --dest-ae LOCAL_SCP --study-uid "1.2.3.4.5"
158
159 # Retrieve specific series
160 )" << program_name << R"( localhost 11112 PACS_SCP --level SERIES --series-uid "1.2.3.4.5.6"
161
162 # Retrieve all studies for a patient
163 )" << program_name << R"( localhost 11112 PACS_SCP --level PATIENT --patient-id "12345"
164
165Exit Codes:
166 0 Success - Retrieval completed
167 1 Partial success - Some images failed
168 2 Error - Retrieval failed or invalid arguments
169)";
170}
171
175std::optional<retrieve_mode> parse_mode(std::string_view mode_str) {
176 if (mode_str == "move" || mode_str == "MOVE" || mode_str == "c-move") {
177 return retrieve_mode::c_move;
178 }
179 if (mode_str == "get" || mode_str == "GET" || mode_str == "c-get") {
180 return retrieve_mode::c_get;
181 }
182 return std::nullopt;
183}
184
188std::optional<retrieve_level> parse_level(std::string_view level_str) {
189 if (level_str == "PATIENT" || level_str == "patient") {
190 return retrieve_level::patient;
191 }
192 if (level_str == "STUDY" || level_str == "study") {
193 return retrieve_level::study;
194 }
195 if (level_str == "SERIES" || level_str == "series") {
196 return retrieve_level::series;
197 }
198 if (level_str == "IMAGE" || level_str == "image" ||
199 level_str == "INSTANCE" || level_str == "instance") {
200 return retrieve_level::image;
201 }
202 return std::nullopt;
203}
204
208std::string_view to_string(retrieve_level level) {
209 switch (level) {
210 case retrieve_level::patient: return "PATIENT";
211 case retrieve_level::study: return "STUDY";
212 case retrieve_level::series: return "SERIES";
213 case retrieve_level::image: return "IMAGE";
214 default: return "UNKNOWN";
215 }
216}
217
221bool parse_arguments(int argc, char* argv[], options& opts) {
222 if (argc < 4) {
223 return false;
224 }
225
226 opts.host = argv[1];
227
228 // Parse port
229 try {
230 int port_int = std::stoi(argv[2]);
231 if (port_int < 1 || port_int > 65535) {
232 std::cerr << "Error: Port must be between 1 and 65535\n";
233 return false;
234 }
235 opts.port = static_cast<uint16_t>(port_int);
236 } catch (const std::exception&) {
237 std::cerr << "Error: Invalid port number '" << argv[2] << "'\n";
238 return false;
239 }
240
241 opts.called_ae = argv[3];
242 if (opts.called_ae.length() > 16) {
243 std::cerr << "Error: Called AE title exceeds 16 characters\n";
244 return false;
245 }
246
247 // Parse optional arguments
248 for (int i = 4; i < argc; ++i) {
249 std::string arg = argv[i];
250
251 if (arg == "--help" || arg == "-h") {
252 return false;
253 }
254 if (arg == "--verbose" || arg == "-v") {
255 opts.verbose = true;
256 } else if (arg == "--mode" && i + 1 < argc) {
257 auto mode = parse_mode(argv[++i]);
258 if (!mode) {
259 std::cerr << "Error: Invalid mode '" << argv[i] << "'\n";
260 return false;
261 }
262 opts.mode = *mode;
263 } else if (arg == "--model" && i + 1 < argc) {
264 opts.query_model = argv[++i];
265 if (opts.query_model != "patient" && opts.query_model != "study") {
266 std::cerr << "Error: Invalid query model (use 'patient' or 'study')\n";
267 return false;
268 }
269 } else if (arg == "--dest-ae" && i + 1 < argc) {
270 opts.move_destination = argv[++i];
271 if (opts.move_destination.length() > 16) {
272 std::cerr << "Error: Destination AE title exceeds 16 characters\n";
273 return false;
274 }
275 } else if (arg == "--local-port" && i + 1 < argc) {
276 try {
277 int port_int = std::stoi(argv[++i]);
278 if (port_int < 1 || port_int > 65535) {
279 std::cerr << "Error: Local port must be between 1 and 65535\n";
280 return false;
281 }
282 opts.local_storage_port = static_cast<uint16_t>(port_int);
283 } catch (const std::exception&) {
284 std::cerr << "Error: Invalid local port number\n";
285 return false;
286 }
287 } else if (arg == "--level" && i + 1 < argc) {
288 auto level = parse_level(argv[++i]);
289 if (!level) {
290 std::cerr << "Error: Invalid retrieve level '" << argv[i] << "'\n";
291 return false;
292 }
293 opts.level = *level;
294 } else if (arg == "--patient-id" && i + 1 < argc) {
295 opts.patient_id = argv[++i];
296 } else if (arg == "--study-uid" && i + 1 < argc) {
297 opts.study_uid = argv[++i];
298 } else if (arg == "--series-uid" && i + 1 < argc) {
299 opts.series_uid = argv[++i];
300 } else if (arg == "--sop-instance-uid" && i + 1 < argc) {
301 opts.sop_instance_uid = argv[++i];
302 } else if ((arg == "--output" || arg == "-o") && i + 1 < argc) {
303 opts.output_dir = argv[++i];
304 } else if (arg == "--structure" && i + 1 < argc) {
305 std::string struct_str = argv[++i];
306 if (struct_str == "hierarchical") {
307 opts.structure = storage_structure::hierarchical;
308 } else if (struct_str == "flat") {
309 opts.structure = storage_structure::flat;
310 } else {
311 std::cerr << "Error: Invalid structure (use 'hierarchical' or 'flat')\n";
312 return false;
313 }
314 } else if (arg == "--overwrite") {
315 opts.overwrite = true;
316 } else if (arg == "--no-progress") {
317 opts.show_progress = false;
318 } else if (arg == "--calling-ae" && i + 1 < argc) {
319 opts.calling_ae = argv[++i];
320 if (opts.calling_ae.length() > 16) {
321 std::cerr << "Error: Calling AE title exceeds 16 characters\n";
322 return false;
323 }
324 } else {
325 std::cerr << "Error: Unknown option '" << arg << "'\n";
326 return false;
327 }
328 }
329
330 return true;
331}
332
336bool validate_options(const options& opts) {
337 // C-MOVE requires destination AE
338 if (opts.mode == retrieve_mode::c_move && opts.move_destination.empty()) {
339 std::cerr << "Error: C-MOVE mode requires --dest-ae option\n";
340 return false;
341 }
342
343 // At least one identifier required
344 bool has_identifier = !opts.patient_id.empty() ||
345 !opts.study_uid.empty() ||
346 !opts.series_uid.empty() ||
347 !opts.sop_instance_uid.empty();
348
349 if (!has_identifier) {
350 std::cerr << "Error: At least one identifier is required "
351 << "(--patient-id, --study-uid, --series-uid, or --sop-instance-uid)\n";
352 return false;
353 }
354
355 // Validate level matches identifiers
356 switch (opts.level) {
357 case retrieve_level::patient:
358 if (opts.patient_id.empty()) {
359 std::cerr << "Error: PATIENT level requires --patient-id\n";
360 return false;
361 }
362 break;
363 case retrieve_level::study:
364 if (opts.study_uid.empty()) {
365 std::cerr << "Error: STUDY level requires --study-uid\n";
366 return false;
367 }
368 break;
369 case retrieve_level::series:
370 if (opts.series_uid.empty()) {
371 std::cerr << "Error: SERIES level requires --series-uid\n";
372 return false;
373 }
374 break;
375 case retrieve_level::image:
376 if (opts.sop_instance_uid.empty()) {
377 std::cerr << "Error: IMAGE level requires --sop-instance-uid\n";
378 return false;
379 }
380 break;
381 }
382
383 return true;
384}
385
389std::string_view get_retrieve_sop_class_uid(const options& opts) {
390 if (opts.mode == retrieve_mode::c_move) {
391 if (opts.query_model == "patient") {
393 }
395 } else {
396 if (opts.query_model == "patient") {
398 }
400 }
401}
402
406kcenon::pacs::core::dicom_dataset build_query_dataset(const options& opts) {
407 using namespace kcenon::pacs::core;
408
409 dicom_dataset ds;
410
411 // Set Query/Retrieve Level
412 std::string level_str{to_string(opts.level)};
413 ds.set_string(tags::query_retrieve_level, kcenon::pacs::encoding::vr_type::CS, level_str);
414
415 // Set identifiers based on level
416 if (!opts.patient_id.empty()) {
417 ds.set_string(tags::patient_id, kcenon::pacs::encoding::vr_type::LO, opts.patient_id);
418 }
419
420 if (!opts.study_uid.empty()) {
421 ds.set_string(tags::study_instance_uid, kcenon::pacs::encoding::vr_type::UI, opts.study_uid);
422 }
423
424 if (!opts.series_uid.empty()) {
425 ds.set_string(tags::series_instance_uid, kcenon::pacs::encoding::vr_type::UI, opts.series_uid);
426 }
427
428 if (!opts.sop_instance_uid.empty()) {
429 ds.set_string(tags::sop_instance_uid, kcenon::pacs::encoding::vr_type::UI, opts.sop_instance_uid);
430 }
431
432 return ds;
433}
434
438struct progress_state {
439 std::atomic<uint16_t> remaining{0};
440 std::atomic<uint16_t> completed{0};
441 std::atomic<uint16_t> failed{0};
442 std::atomic<uint16_t> warning{0};
443 std::atomic<size_t> bytes_received{0};
444 std::chrono::steady_clock::time_point start_time;
445
446 void reset() {
447 remaining = 0;
448 completed = 0;
449 failed = 0;
450 warning = 0;
451 bytes_received = 0;
452 start_time = std::chrono::steady_clock::now();
453 }
454
455 [[nodiscard]] uint16_t total() const {
456 return remaining + completed + failed + warning;
457 }
458};
459
463void display_progress(const progress_state& state, bool verbose) {
464 auto total = state.total();
465 if (total == 0) return;
466
467 uint16_t done = state.completed + state.failed + state.warning;
468 float progress = static_cast<float>(done) / total;
469
470 // Calculate elapsed time and speed
471 auto elapsed = std::chrono::steady_clock::now() - state.start_time;
472 auto elapsed_sec = std::chrono::duration<double>(elapsed).count();
473 double speed = elapsed_sec > 0 ? state.bytes_received / elapsed_sec / 1024.0 : 0;
474
475 // Clear line and print progress
476 std::cout << "\r";
477
478 // Progress bar
479 std::cout << "[";
480 int filled = static_cast<int>(progress * progress_bar_width);
481 for (int i = 0; i < progress_bar_width; ++i) {
482 if (i < filled) std::cout << "=";
483 else if (i == filled) std::cout << ">";
484 else std::cout << " ";
485 }
486 std::cout << "] ";
487
488 // Percentage and counts
489 std::cout << std::fixed << std::setprecision(1) << (progress * 100) << "% ";
490 std::cout << "(" << done << "/" << total << ") ";
491
492 if (verbose) {
493 std::cout << std::setprecision(1) << speed << " KB/s ";
494 if (state.failed > 0) {
495 std::cout << "[" << state.failed << " failed] ";
496 }
497 }
498
499 std::cout << std::flush;
500}
501
505std::filesystem::path generate_file_path(
506 const options& opts,
507 const kcenon::pacs::core::dicom_dataset& dataset) {
508
509 using namespace kcenon::pacs::core;
510
511 std::filesystem::path path = opts.output_dir;
512
513 if (opts.structure == storage_structure::hierarchical) {
514 // Get identifiers for path (using default_value parameter)
515 auto patient_id = dataset.get_string(tags::patient_id, "UNKNOWN");
516 auto study_uid = dataset.get_string(tags::study_instance_uid, "UNKNOWN");
517 auto series_uid = dataset.get_string(tags::series_instance_uid, "UNKNOWN");
518 auto sop_uid = dataset.get_string(tags::sop_instance_uid, "UNKNOWN");
519
520 // Build hierarchical path
521 path /= patient_id;
522 path /= study_uid;
523 path /= series_uid;
524 path /= sop_uid + ".dcm";
525 } else {
526 // Flat structure - just use SOP Instance UID
527 auto sop_uid = dataset.get_string(tags::sop_instance_uid, "UNKNOWN");
528 path /= sop_uid + ".dcm";
529 }
530
531 return path;
532}
533
537bool save_dicom_file(
538 const std::filesystem::path& path,
540 bool overwrite) {
541
542 // Check if file exists
543 if (std::filesystem::exists(path) && !overwrite) {
544 return true; // Skip existing file (not an error)
545 }
546
547 // Create parent directories
548 std::filesystem::create_directories(path.parent_path());
549
550 // Create DICOM file using static factory method
552 dataset,
554
555 auto result = file.save(path);
556 return result.is_ok();
557}
558
563 uint16_t message_id,
564 std::string_view sop_class_uid,
565 std::string_view move_destination) {
566
567 using namespace kcenon::pacs::network::dimse;
568
569 dimse_message msg{command_field::c_move_rq, message_id};
570 msg.set_affected_sop_class_uid(sop_class_uid);
571 msg.set_priority(priority_medium);
572
573 // Set Move Destination AE
574 msg.command_set().set_string(
575 tag_move_destination,
577 std::string(move_destination));
578
579 return msg;
580}
581
586 uint16_t message_id,
587 std::string_view sop_class_uid) {
588
589 using namespace kcenon::pacs::network::dimse;
590
591 dimse_message msg{command_field::c_get_rq, message_id};
592 msg.set_affected_sop_class_uid(sop_class_uid);
593 msg.set_priority(priority_medium);
594
595 return msg;
596}
597
601int perform_c_get(const options& opts) {
602 using namespace kcenon::pacs::network;
603 using namespace kcenon::pacs::network::dimse;
604
605 auto sop_class_uid = get_retrieve_sop_class_uid(opts);
606
607 if (opts.verbose) {
608 std::cout << "Performing C-GET retrieval\n";
609 std::cout << " Host: " << opts.host << ":" << opts.port << "\n";
610 std::cout << " Calling AE: " << opts.calling_ae << "\n";
611 std::cout << " Called AE: " << opts.called_ae << "\n";
612 std::cout << " Query Model: " << opts.query_model << " root\n";
613 std::cout << " Level: " << to_string(opts.level) << "\n";
614 std::cout << " Output: " << opts.output_dir << "\n\n";
615 }
616
617 // Create output directory
618 std::filesystem::create_directories(opts.output_dir);
619
620 // Configure association
621 association_config config;
622 config.calling_ae_title = opts.calling_ae;
623 config.called_ae_title = opts.called_ae;
624 config.implementation_class_uid = "1.2.826.0.1.3680043.2.1545.1";
625 config.implementation_version_name = "RETRIEVE_SCU_01";
626
627 // Propose C-GET SOP Class
628 config.proposed_contexts.push_back({
629 1, // Context ID
630 std::string(sop_class_uid),
631 {
632 "1.2.840.10008.1.2.1", // Explicit VR Little Endian
633 "1.2.840.10008.1.2" // Implicit VR Little Endian
634 }
635 });
636
637 // For C-GET, we need to propose storage SOP classes as SCP role
638 // to receive the C-STORE sub-operations
639 // Common storage SOP classes
640 static const std::vector<std::string_view> storage_sop_classes = {
641 "1.2.840.10008.5.1.4.1.1.2", // CT Image Storage
642 "1.2.840.10008.5.1.4.1.1.4", // MR Image Storage
643 "1.2.840.10008.5.1.4.1.1.7", // Secondary Capture Image Storage
644 "1.2.840.10008.5.1.4.1.1.1", // CR Image Storage
645 "1.2.840.10008.5.1.4.1.1.1.1", // Digital X-Ray Image Storage
646 "1.2.840.10008.5.1.4.1.1.12.1", // X-Ray Angiographic Image Storage
647 "1.2.840.10008.5.1.4.1.1.6.1", // US Image Storage
648 "1.2.840.10008.5.1.4.1.1.88.11", // Basic Text SR
649 "1.2.840.10008.5.1.4.1.1.88.22", // Enhanced SR
650 };
651
652 uint8_t context_id = 3; // Start from 3 (odd numbers for proposed contexts)
653 for (auto sop_class : storage_sop_classes) {
654 config.proposed_contexts.push_back({
655 context_id,
656 std::string(sop_class),
657 {
658 "1.2.840.10008.1.2.1", // Explicit VR Little Endian
659 "1.2.840.10008.1.2" // Implicit VR Little Endian
660 }
661 });
662 context_id += 2;
663 }
664
665 // Establish association
666 auto start_time = std::chrono::steady_clock::now();
667 auto connect_result = association::connect(opts.host, opts.port, config, default_timeout);
668
669 if (connect_result.is_err()) {
670 std::cerr << "Failed to establish association: "
671 << connect_result.error().message << "\n";
672 return 2;
673 }
674
675 auto& assoc = connect_result.value();
676
677 if (opts.verbose) {
678 auto connect_time = std::chrono::steady_clock::now();
679 auto connect_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
680 connect_time - start_time);
681 std::cout << "Association established in " << connect_duration.count() << " ms\n";
682 }
683
684 // Check if C-GET context was accepted
685 if (!assoc.has_accepted_context(sop_class_uid)) {
686 std::cerr << "Error: C-GET SOP Class not accepted by remote SCP\n";
687 assoc.abort();
688 return 2;
689 }
690
691 auto context_id_opt = assoc.accepted_context_id(sop_class_uid);
692 if (!context_id_opt) {
693 std::cerr << "Error: Could not get presentation context ID\n";
694 assoc.abort();
695 return 2;
696 }
697 uint8_t get_context_id = *context_id_opt;
698
699 // Build query dataset
700 auto query_ds = build_query_dataset(opts);
701
702 // Create C-GET request
703 auto get_rq = make_c_get_rq(1, sop_class_uid);
704 get_rq.set_dataset(std::move(query_ds));
705
706 if (opts.verbose) {
707 std::cout << "Sending C-GET request...\n";
708 }
709
710 // Send C-GET request
711 auto send_result = assoc.send_dimse(get_context_id, get_rq);
712 if (send_result.is_err()) {
713 std::cerr << "Failed to send C-GET: " << send_result.error().message << "\n";
714 assoc.abort();
715 return 2;
716 }
717
718 // Progress tracking
719 progress_state progress;
720 progress.reset();
721
722 // Process responses and C-STORE sub-operations
723 bool retrieve_complete = false;
724 uint16_t total_completed = 0;
725 uint16_t total_failed = 0;
726 uint16_t total_warning = 0;
727
728 while (!retrieve_complete) {
729 auto recv_result = assoc.receive_dimse(default_timeout);
730 if (recv_result.is_err()) {
731 std::cerr << "\nFailed to receive response: "
732 << recv_result.error().message << "\n";
733 assoc.abort();
734 return 2;
735 }
736
737 auto& [recv_context_id, msg] = recv_result.value();
738 auto cmd = msg.command();
739
740 if (cmd == command_field::c_get_rsp) {
741 // C-GET response
742 auto status = msg.status();
743
744 // Update progress from response
745 if (auto remaining = msg.remaining_subops()) {
746 progress.remaining = *remaining;
747 }
748 if (auto completed = msg.completed_subops()) {
749 progress.completed = *completed;
750 total_completed = *completed;
751 }
752 if (auto failed = msg.failed_subops()) {
753 progress.failed = *failed;
754 total_failed = *failed;
755 }
756 if (auto warning = msg.warning_subops()) {
757 progress.warning = *warning;
758 total_warning = *warning;
759 }
760
761 if (opts.show_progress) {
762 display_progress(progress, opts.verbose);
763 }
764
765 // Check if retrieval is complete
766 if (status == status_success ||
767 status == status_cancel ||
768 (status & 0xF000) == 0xA000 || // Failure
769 (status & 0xF000) == 0xC000) { // Unable to process
770
771 retrieve_complete = true;
772
773 if (status != status_success && status != status_cancel) {
774 std::cerr << "\nC-GET failed with status: 0x"
775 << std::hex << status << std::dec << "\n";
776 }
777 }
778
779 } else if (cmd == command_field::c_store_rq) {
780 // Incoming C-STORE sub-operation
781 if (msg.has_dataset()) {
782 auto dataset_result = msg.dataset();
783 if (dataset_result.is_err()) {
784 if (opts.verbose) {
785 std::cerr << "\nWarning: Failed to get dataset\n";
786 }
787 continue;
788 }
789 const auto& dataset = dataset_result.value().get();
790
791 // Generate file path
792 auto file_path = generate_file_path(opts, dataset);
793
794 // Save file
795 bool saved = save_dicom_file(file_path, dataset, opts.overwrite);
796
797 // Update bytes received
798 progress.bytes_received += 1024; // Approximate
799
800 // Send C-STORE response
801 auto sop_class = msg.affected_sop_class_uid();
802 auto sop_instance = msg.affected_sop_instance_uid();
803
804 auto store_rsp = make_c_store_rsp(
805 msg.message_id(),
806 sop_class,
807 sop_instance,
808 saved ? status_success : 0xA700 // Out of resources
809 );
810
811 auto send_rsp_result = assoc.send_dimse(recv_context_id, store_rsp);
812 if (send_rsp_result.is_err() && opts.verbose) {
813 std::cerr << "\nWarning: Failed to send C-STORE response\n";
814 }
815
816 if (opts.verbose && !saved) {
817 std::cerr << "\nWarning: Failed to save " << file_path << "\n";
818 }
819 }
820 }
821 }
822
823 // Clear progress line
824 if (opts.show_progress) {
825 std::cout << "\n";
826 }
827
828 // Release association gracefully
829 if (opts.verbose) {
830 std::cout << "Releasing association...\n";
831 }
832
833 auto release_result = assoc.release(default_timeout);
834 if (release_result.is_err() && opts.verbose) {
835 std::cerr << "Warning: Release failed: " << release_result.error().message << "\n";
836 }
837
838 // Print summary
839 auto end_time = std::chrono::steady_clock::now();
840 auto total_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
841 end_time - start_time);
842
843 std::cout << "\n========================================\n";
844 std::cout << " Retrieve Summary\n";
845 std::cout << "========================================\n";
846 std::cout << " Mode: C-GET\n";
847 std::cout << " Level: " << to_string(opts.level) << "\n";
848 std::cout << " Output: " << opts.output_dir << "\n";
849 std::cout << " ----------------------------------------\n";
850 std::cout << " Completed: " << total_completed << "\n";
851 if (total_warning > 0) {
852 std::cout << " Warnings: " << total_warning << "\n";
853 }
854 if (total_failed > 0) {
855 std::cout << " Failed: " << total_failed << "\n";
856 }
857 std::cout << " Total time: " << total_duration.count() << " ms\n";
858 std::cout << "========================================\n";
859
860 // Return appropriate exit code
861 if (total_failed > 0 && total_completed == 0) {
862 return 2; // Complete failure
863 } else if (total_failed > 0) {
864 return 1; // Partial failure
865 }
866 return 0; // Success
867}
868
872int perform_c_move(const options& opts) {
873 using namespace kcenon::pacs::network;
874 using namespace kcenon::pacs::network::dimse;
875
876 auto sop_class_uid = get_retrieve_sop_class_uid(opts);
877
878 if (opts.verbose) {
879 std::cout << "Performing C-MOVE retrieval\n";
880 std::cout << " Host: " << opts.host << ":" << opts.port << "\n";
881 std::cout << " Calling AE: " << opts.calling_ae << "\n";
882 std::cout << " Called AE: " << opts.called_ae << "\n";
883 std::cout << " Destination: " << opts.move_destination << "\n";
884 std::cout << " Query Model: " << opts.query_model << " root\n";
885 std::cout << " Level: " << to_string(opts.level) << "\n\n";
886 }
887
888 // Configure association
889 association_config config;
890 config.calling_ae_title = opts.calling_ae;
891 config.called_ae_title = opts.called_ae;
892 config.implementation_class_uid = "1.2.826.0.1.3680043.2.1545.1";
893 config.implementation_version_name = "RETRIEVE_SCU_01";
894
895 // Propose C-MOVE SOP Class
896 config.proposed_contexts.push_back({
897 1, // Context ID
898 std::string(sop_class_uid),
899 {
900 "1.2.840.10008.1.2.1", // Explicit VR Little Endian
901 "1.2.840.10008.1.2" // Implicit VR Little Endian
902 }
903 });
904
905 // Establish association
906 auto start_time = std::chrono::steady_clock::now();
907 auto connect_result = association::connect(opts.host, opts.port, config, default_timeout);
908
909 if (connect_result.is_err()) {
910 std::cerr << "Failed to establish association: "
911 << connect_result.error().message << "\n";
912 return 2;
913 }
914
915 auto& assoc = connect_result.value();
916
917 if (opts.verbose) {
918 auto connect_time = std::chrono::steady_clock::now();
919 auto connect_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
920 connect_time - start_time);
921 std::cout << "Association established in " << connect_duration.count() << " ms\n";
922 }
923
924 // Check if C-MOVE context was accepted
925 if (!assoc.has_accepted_context(sop_class_uid)) {
926 std::cerr << "Error: C-MOVE SOP Class not accepted by remote SCP\n";
927 assoc.abort();
928 return 2;
929 }
930
931 auto context_id_opt = assoc.accepted_context_id(sop_class_uid);
932 if (!context_id_opt) {
933 std::cerr << "Error: Could not get presentation context ID\n";
934 assoc.abort();
935 return 2;
936 }
937 uint8_t move_context_id = *context_id_opt;
938
939 // Build query dataset
940 auto query_ds = build_query_dataset(opts);
941
942 // Create C-MOVE request
943 auto move_rq = make_c_move_rq(1, sop_class_uid, opts.move_destination);
944 move_rq.set_dataset(std::move(query_ds));
945
946 if (opts.verbose) {
947 std::cout << "Sending C-MOVE request to move images to " << opts.move_destination << "...\n";
948 }
949
950 // Send C-MOVE request
951 auto send_result = assoc.send_dimse(move_context_id, move_rq);
952 if (send_result.is_err()) {
953 std::cerr << "Failed to send C-MOVE: " << send_result.error().message << "\n";
954 assoc.abort();
955 return 2;
956 }
957
958 // Progress tracking
959 progress_state progress;
960 progress.reset();
961
962 // Process C-MOVE responses
963 bool move_complete = false;
964 uint16_t total_completed = 0;
965 uint16_t total_failed = 0;
966 uint16_t total_warning = 0;
967
968 while (!move_complete) {
969 auto recv_result = assoc.receive_dimse(default_timeout);
970 if (recv_result.is_err()) {
971 std::cerr << "\nFailed to receive C-MOVE response: "
972 << recv_result.error().message << "\n";
973 assoc.abort();
974 return 2;
975 }
976
977 auto& [recv_context_id, msg] = recv_result.value();
978
979 if (msg.command() != command_field::c_move_rsp) {
980 std::cerr << "\nError: Unexpected response (expected C-MOVE-RSP)\n";
981 assoc.abort();
982 return 2;
983 }
984
985 auto status = msg.status();
986
987 // Update progress from response
988 if (auto remaining = msg.remaining_subops()) {
989 progress.remaining = *remaining;
990 }
991 if (auto completed = msg.completed_subops()) {
992 progress.completed = *completed;
993 total_completed = *completed;
994 }
995 if (auto failed = msg.failed_subops()) {
996 progress.failed = *failed;
997 total_failed = *failed;
998 }
999 if (auto warning = msg.warning_subops()) {
1000 progress.warning = *warning;
1001 total_warning = *warning;
1002 }
1003
1004 if (opts.show_progress) {
1005 display_progress(progress, opts.verbose);
1006 }
1007
1008 // Check if C-MOVE is complete
1009 if (status == status_success ||
1010 status == status_cancel ||
1011 (status & 0xF000) == 0xA000 || // Failure
1012 (status & 0xF000) == 0xC000) { // Unable to process
1013
1014 move_complete = true;
1015
1016 if (status != status_success && status != status_cancel) {
1017 std::cerr << "\nC-MOVE failed with status: 0x"
1018 << std::hex << status << std::dec << "\n";
1019 }
1020 }
1021 }
1022
1023 // Clear progress line
1024 if (opts.show_progress) {
1025 std::cout << "\n";
1026 }
1027
1028 // Release association gracefully
1029 if (opts.verbose) {
1030 std::cout << "Releasing association...\n";
1031 }
1032
1033 auto release_result = assoc.release(default_timeout);
1034 if (release_result.is_err() && opts.verbose) {
1035 std::cerr << "Warning: Release failed: " << release_result.error().message << "\n";
1036 }
1037
1038 // Print summary
1039 auto end_time = std::chrono::steady_clock::now();
1040 auto total_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
1041 end_time - start_time);
1042
1043 std::cout << "\n========================================\n";
1044 std::cout << " Retrieve Summary\n";
1045 std::cout << "========================================\n";
1046 std::cout << " Mode: C-MOVE\n";
1047 std::cout << " Destination: " << opts.move_destination << "\n";
1048 std::cout << " Level: " << to_string(opts.level) << "\n";
1049 std::cout << " ----------------------------------------\n";
1050 std::cout << " Completed: " << total_completed << "\n";
1051 if (total_warning > 0) {
1052 std::cout << " Warnings: " << total_warning << "\n";
1053 }
1054 if (total_failed > 0) {
1055 std::cout << " Failed: " << total_failed << "\n";
1056 }
1057 std::cout << " Total time: " << total_duration.count() << " ms\n";
1058 std::cout << "========================================\n";
1059
1060 // Return appropriate exit code
1061 if (total_failed > 0 && total_completed == 0) {
1062 return 2; // Complete failure
1063 } else if (total_failed > 0) {
1064 return 1; // Partial failure
1065 }
1066 return 0; // Success
1067}
1068
1069} // namespace
1070
1071int main(int argc, char* argv[]) {
1072 std::cout << R"(
1073 ____ _____ _____ ____ ___ _______ _______ ____ ____ _ _
1074 | _ \| ____|_ _| _ \|_ _| ____\ \ / / ____| / ___| / ___| | | |
1075 | |_) | _| | | | |_) || || _| \ \ / /| _| \___ \| | | | | |
1076 | _ <| |___ | | | _ < | || |___ \ V / | |___ ___) | |___| |_| |
1077 |_| \_\_____| |_| |_| \_\___|_____| \_/ |_____| |____/ \____|\___/
1078
1079 DICOM C-MOVE/C-GET Client
1080)" << "\n";
1081
1082 options opts;
1083
1084 if (!parse_arguments(argc, argv, opts)) {
1085 print_usage(argv[0]);
1086 return 2;
1087 }
1088
1089 if (!validate_options(opts)) {
1090 return 2;
1091 }
1092
1093 // Perform retrieval based on mode
1094 if (opts.mode == retrieve_mode::c_move) {
1095 return perform_c_move(opts);
1096 } else {
1097 return perform_c_get(opts);
1098 }
1099}
DICOM Association management per PS3.8.
auto get(dicom_tag tag) noexcept -> dicom_element *
Get a pointer to the element with the given tag.
auto get_string(dicom_tag tag, std::string_view default_value="") const -> std::string
Get the string value of an element.
static auto create(dicom_dataset dataset, const encoding::transfer_syntax &ts) -> dicom_file
Create a new DICOM file from a dataset.
static const transfer_syntax explicit_vr_little_endian
Explicit VR Little Endian (1.2.840.10008.1.2.1)
DICOM Part 10 file handling for reading/writing DICOM files.
Compile-time constants for commonly used DICOM tags.
DIMSE message encoding and decoding.
int main()
Definition main.cpp:84
@ completed
Job completed successfully.
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag message_id
Message ID.
constexpr dicom_tag sop_instance_uid
SOP Instance UID.
constexpr dicom_tag status
Status.
constexpr dicom_tag sop_class_uid
SOP Class UID.
constexpr dicom_tag move_destination
Move Destination.
@ LO
Long String (64 chars max)
@ UI
Unique Identifier (64 chars max)
@ CS
Code String (16 chars max, uppercase + digits + space + underscore)
@ AE
Application Entity (16 chars max)
std::chrono::milliseconds default_timeout()
Default timeout for test operations (5s normal, 30s CI)
@ done
Job completed successfully.
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.
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.
@ flat
{SOPUID}.dcm (flat structure)
DICOM Retrieve SCP service (C-MOVE/C-GET handler)
DICOM Storage SCP service (C-STORE handler)
Configuration for SCU association request.
std::string called_ae_title
Remote AE Title (16 chars max)
std::string calling_ae_title
Our AE Title (16 chars max)
std::vector< proposed_presentation_context > proposed_contexts