PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1
33
34#include <algorithm>
35#include <atomic>
36#include <chrono>
37#include <cstdlib>
38#include <filesystem>
39#include <fstream>
40#include <iomanip>
41#include <iostream>
42#include <regex>
43#include <sstream>
44#include <string>
45#include <thread>
46#include <vector>
47
48namespace {
49
50// =============================================================================
51// Constants
52// =============================================================================
53
54constexpr const char* version_string = "1.0.0";
55constexpr const char* default_calling_ae = "MOVESCU";
56constexpr const char* default_called_ae = "ANY-SCP";
57constexpr auto default_timeout = std::chrono::seconds{60};
58constexpr size_t max_ae_title_length = 16;
59constexpr int progress_bar_width = 40;
60
61// =============================================================================
62// Query Model and Level
63// =============================================================================
64
65enum class query_model {
68};
69
70enum class query_level {
71 patient,
72 study,
73 series,
74 image
75};
76
77// =============================================================================
78// Query Key
79// =============================================================================
80
81struct query_key {
83 std::string value;
84};
85
86// =============================================================================
87// Command Line Options
88// =============================================================================
89
90struct options {
91 // Network options
92 std::string peer_host;
93 uint16_t peer_port{0};
94 std::string calling_ae_title{default_calling_ae};
95 std::string called_ae_title{default_called_ae};
96 std::string move_destination; // Required for C-MOVE
97
98 // Timeout options
99 std::chrono::seconds connection_timeout{default_timeout};
100 std::chrono::seconds acse_timeout{default_timeout};
101 std::chrono::seconds dimse_timeout{0}; // 0 = infinite
102
103 // Query model and level
104 query_model model{query_model::patient_root};
105 query_level level{query_level::study};
106
107 // Query keys
108 std::vector<query_key> keys;
109 std::string query_file;
110
111 // Output options (when receiving locally)
112 std::filesystem::path output_dir{"./downloads"};
113 uint16_t receive_port{0}; // 0 = auto
114
115 // Progress options
116 bool show_progress{true};
117 bool ignore_pending{false};
118
119 // Verbosity
120 bool verbose{false};
121 bool debug{false};
122 bool quiet{false};
123
124 // Help/version flags
125 bool show_help{false};
126 bool show_version{false};
127};
128
129// =============================================================================
130// Progress Tracking
131// =============================================================================
132
133struct move_progress {
134 std::atomic<uint16_t> remaining{0};
135 std::atomic<uint16_t> completed{0};
136 std::atomic<uint16_t> failed{0};
137 std::atomic<uint16_t> warning{0};
138 std::chrono::steady_clock::time_point start_time;
139
140 void reset() {
141 remaining = 0;
142 completed = 0;
143 failed = 0;
144 warning = 0;
145 start_time = std::chrono::steady_clock::now();
146 }
147
148 [[nodiscard]] uint16_t total() const {
149 return remaining + completed + failed + warning;
150 }
151};
152
153// =============================================================================
154// Utility Functions
155// =============================================================================
156
157std::string_view query_model_to_string(query_model model) {
158 switch (model) {
159 case query_model::patient_root: return "Patient Root";
160 case query_model::study_root: return "Study Root";
161 default: return "Unknown";
162 }
163}
164
165std::string_view query_level_to_string(query_level level) {
166 switch (level) {
167 case query_level::patient: return "PATIENT";
168 case query_level::study: return "STUDY";
169 case query_level::series: return "SERIES";
170 case query_level::image: return "IMAGE";
171 default: return "UNKNOWN";
172 }
173}
174
175std::string_view get_move_sop_class_uid(query_model model) {
176 switch (model) {
177 case query_model::patient_root:
179 case query_model::study_root:
181 default:
183 }
184}
185
186// =============================================================================
187// Output Functions
188// =============================================================================
189
190void print_banner() {
191 std::cout << R"(
192 __ __ _____ _ _ _____ ____ ____ _ _
193 | \/ |/ _ \ \| | / / ____| / ___| / ___| | | |
194 | |\/| | | | \ V / | _| \___ \| | | | | |
195 | | | | |_| |\ /| | |___ ___) | |___| |_| |
196 |_| |_|\___/ \_/ |_____| |____/ \____|\___/
197
198 DICOM C-MOVE Client v)" << version_string << R"(
199)" << "\n";
200}
201
202void print_usage(const char* program_name) {
203 std::cout << "Usage: " << program_name << R"( [options] <peer> <port>
204
205Arguments:
206 peer Remote host address (IP or hostname)
207 port Remote port number (typically 104 or 11112)
208
209Options:
210 -h, --help Show this help message and exit
211 -v, --verbose Verbose output mode
212 -d, --debug Debug output mode
213 -q, --quiet Quiet mode (minimal output)
214 --version Show version information
215
216Network Options:
217 -aet, --aetitle <aetitle> Calling AE Title (default: MOVESCU)
218 -aec, --call <aetitle> Called AE Title (default: ANY-SCP)
219 -aem, --move-dest <aetitle> Move destination AE Title (REQUIRED)
220 -to, --timeout <seconds> Connection timeout (default: 60)
221 -ta, --acse-timeout <seconds> ACSE timeout (default: 60)
222 -td, --dimse-timeout <seconds> DIMSE timeout (default: 0=infinite)
223
224Query Model:
225 -P, --patient-root Patient Root Query Model (default)
226 -S, --study-root Study Root Query Model
227
228Query Level:
229 -L, --level <level> Retrieve level (PATIENT|STUDY|SERIES|IMAGE)
230
231Query Keys:
232 -k, --key <tag=value> Query key for retrieval
233 -f, --query-file <file> Read query keys from file
234
235Output Options (when receiving locally):
236 -od, --output-dir <dir> Output directory (default: ./downloads)
237 --port <port> Port for receiving files (default: auto)
238
239Progress Options:
240 -p, --progress Show progress information (default)
241 --no-progress Disable progress display
242 --ignore-pending Ignore pending status
243
244Examples:
245 # Move study to third party
246 )" << program_name << R"( -aem WORKSTATION \
247 -L STUDY \
248 -k "0020,000D=1.2.840..." \
249 pacs.example.com 104
250
251 # Move series to self
252 )" << program_name << R"( -aem MOVESCU \
253 --port 11113 \
254 -od ./received/ \
255 -L SERIES \
256 -k "0020,000E=1.2.840..." \
257 localhost 11112
258
259 # Move patient data with progress
260 )" << program_name << R"( -aem ARCHIVE \
261 --progress \
262 -L PATIENT \
263 -k "0010,0020=12345" \
264 pacs.example.com 104
265
266Exit Codes:
267 0 Success - Move completed
268 1 Partial success - Some sub-operations failed
269 2 Error - Move failed or invalid arguments
270)";
271}
272
273void print_version() {
274 std::cout << "move_scu version " << version_string << "\n";
275 std::cout << "PACS System DICOM Utilities\n";
276 std::cout << "Copyright (c) 2024\n";
277}
278
279void display_progress(const move_progress& progress, bool verbose) {
280 auto total = progress.total();
281 if (total == 0) return;
282
283 uint16_t done = progress.completed + progress.failed + progress.warning;
284 float pct = static_cast<float>(done) / total;
285
286 auto elapsed = std::chrono::steady_clock::now() - progress.start_time;
287 auto elapsed_sec = std::chrono::duration<double>(elapsed).count();
288
289 std::cout << "\r[";
290 int filled = static_cast<int>(pct * progress_bar_width);
291 for (int i = 0; i < progress_bar_width; ++i) {
292 if (i < filled) std::cout << "=";
293 else if (i == filled) std::cout << ">";
294 else std::cout << " ";
295 }
296 std::cout << "] ";
297
298 std::cout << std::fixed << std::setprecision(1) << (pct * 100) << "% ";
299 std::cout << "(" << done << "/" << total << ") ";
300
301 if (verbose) {
302 std::cout << std::setprecision(1) << elapsed_sec << "s ";
303 if (progress.failed > 0) {
304 std::cout << "[" << progress.failed.load() << " failed] ";
305 }
306 }
307
308 std::cout << std::flush;
309}
310
311// =============================================================================
312// Argument Parsing
313// =============================================================================
314
315bool parse_timeout(const std::string& value, std::chrono::seconds& result,
316 const std::string& option_name) {
317 try {
318 int seconds = std::stoi(value);
319 if (seconds < 0) {
320 std::cerr << "Error: " << option_name << " must be non-negative\n";
321 return false;
322 }
323 result = std::chrono::seconds{seconds};
324 return true;
325 } catch (const std::exception&) {
326 std::cerr << "Error: Invalid value for " << option_name << ": '"
327 << value << "'\n";
328 return false;
329 }
330}
331
332bool validate_ae_title(const std::string& ae_title,
333 const std::string& option_name) {
334 if (ae_title.empty()) {
335 std::cerr << "Error: " << option_name << " cannot be empty\n";
336 return false;
337 }
338 if (ae_title.length() > max_ae_title_length) {
339 std::cerr << "Error: " << option_name << " exceeds "
340 << max_ae_title_length << " characters\n";
341 return false;
342 }
343 return true;
344}
345
346std::optional<query_level> parse_level(const std::string& level_str) {
347 std::string upper = level_str;
348 std::transform(upper.begin(), upper.end(), upper.begin(), ::toupper);
349
350 if (upper == "PATIENT") return query_level::patient;
351 if (upper == "STUDY") return query_level::study;
352 if (upper == "SERIES") return query_level::series;
353 if (upper == "IMAGE" || upper == "INSTANCE") return query_level::image;
354 return std::nullopt;
355}
356
357bool parse_query_key(const std::string& key_str, query_key& key) {
358 std::regex key_regex(R"(\‍(?([0-9A-Fa-f]{4}),([0-9A-Fa-f]{4})\)?=?(.*))");
359 std::smatch match;
360
361 if (!std::regex_match(key_str, match, key_regex)) {
362 std::cerr << "Error: Invalid query key format: '" << key_str << "'\n";
363 return false;
364 }
365
366 uint16_t group = static_cast<uint16_t>(std::stoul(match[1].str(), nullptr, 16));
367 uint16_t element = static_cast<uint16_t>(std::stoul(match[2].str(), nullptr, 16));
368
369 key.tag = kcenon::pacs::core::dicom_tag{group, element};
370 key.value = match[3].str();
371
372 return true;
373}
374
375bool load_query_file(const std::string& filename, std::vector<query_key>& keys) {
376 std::ifstream file(filename);
377 if (!file.is_open()) {
378 std::cerr << "Error: Cannot open query file: " << filename << "\n";
379 return false;
380 }
381
382 std::string line;
383 while (std::getline(file, line)) {
384 auto pos = line.find_first_not_of(" \t");
385 if (pos == std::string::npos || line[pos] == '#') {
386 continue;
387 }
388
389 query_key key;
390 if (!parse_query_key(line, key)) {
391 return false;
392 }
393 keys.push_back(key);
394 }
395
396 return true;
397}
398
399bool parse_arguments(int argc, char* argv[], options& opts) {
400 std::vector<std::string> positional_args;
401
402 for (int i = 1; i < argc; ++i) {
403 std::string arg = argv[i];
404
405 if (arg == "-h" || arg == "--help") {
406 opts.show_help = true;
407 return true;
408 }
409 if (arg == "--version") {
410 opts.show_version = true;
411 return true;
412 }
413
414 // Verbosity options
415 if (arg == "-v" || arg == "--verbose") {
416 opts.verbose = true;
417 continue;
418 }
419 if (arg == "-d" || arg == "--debug") {
420 opts.debug = true;
421 opts.verbose = true;
422 continue;
423 }
424 if (arg == "-q" || arg == "--quiet") {
425 opts.quiet = true;
426 continue;
427 }
428
429 // Network options
430 if ((arg == "-aet" || arg == "--aetitle") && i + 1 < argc) {
431 opts.calling_ae_title = argv[++i];
432 if (!validate_ae_title(opts.calling_ae_title, "Calling AE Title")) {
433 return false;
434 }
435 continue;
436 }
437 if ((arg == "-aec" || arg == "--call") && i + 1 < argc) {
438 opts.called_ae_title = argv[++i];
439 if (!validate_ae_title(opts.called_ae_title, "Called AE Title")) {
440 return false;
441 }
442 continue;
443 }
444 if ((arg == "-aem" || arg == "--move-dest") && i + 1 < argc) {
445 opts.move_destination = argv[++i];
446 if (!validate_ae_title(opts.move_destination, "Move Destination")) {
447 return false;
448 }
449 continue;
450 }
451
452 // Timeout options
453 if ((arg == "-to" || arg == "--timeout") && i + 1 < argc) {
454 if (!parse_timeout(argv[++i], opts.connection_timeout, "timeout")) {
455 return false;
456 }
457 continue;
458 }
459 if ((arg == "-ta" || arg == "--acse-timeout") && i + 1 < argc) {
460 if (!parse_timeout(argv[++i], opts.acse_timeout, "ACSE timeout")) {
461 return false;
462 }
463 continue;
464 }
465 if ((arg == "-td" || arg == "--dimse-timeout") && i + 1 < argc) {
466 if (!parse_timeout(argv[++i], opts.dimse_timeout, "DIMSE timeout")) {
467 return false;
468 }
469 continue;
470 }
471
472 // Query model
473 if (arg == "-P" || arg == "--patient-root") {
474 opts.model = query_model::patient_root;
475 continue;
476 }
477 if (arg == "-S" || arg == "--study-root") {
478 opts.model = query_model::study_root;
479 continue;
480 }
481
482 // Query level
483 if ((arg == "-L" || arg == "--level") && i + 1 < argc) {
484 auto level = parse_level(argv[++i]);
485 if (!level) {
486 std::cerr << "Error: Invalid query level: '" << argv[i] << "'\n";
487 return false;
488 }
489 opts.level = *level;
490 continue;
491 }
492
493 // Query keys
494 if ((arg == "-k" || arg == "--key") && i + 1 < argc) {
495 query_key key;
496 if (!parse_query_key(argv[++i], key)) {
497 return false;
498 }
499 opts.keys.push_back(key);
500 continue;
501 }
502 if ((arg == "-f" || arg == "--query-file") && i + 1 < argc) {
503 opts.query_file = argv[++i];
504 continue;
505 }
506
507 // Output options
508 if ((arg == "-od" || arg == "--output-dir") && i + 1 < argc) {
509 opts.output_dir = argv[++i];
510 continue;
511 }
512 if (arg == "--port" && i + 1 < argc) {
513 try {
514 int port = std::stoi(argv[++i]);
515 if (port < 1 || port > 65535) {
516 std::cerr << "Error: Port must be between 1 and 65535\n";
517 return false;
518 }
519 opts.receive_port = static_cast<uint16_t>(port);
520 } catch (...) {
521 std::cerr << "Error: Invalid port number\n";
522 return false;
523 }
524 continue;
525 }
526
527 // Progress options
528 if (arg == "-p" || arg == "--progress") {
529 opts.show_progress = true;
530 continue;
531 }
532 if (arg == "--no-progress") {
533 opts.show_progress = false;
534 continue;
535 }
536 if (arg == "--ignore-pending") {
537 opts.ignore_pending = true;
538 continue;
539 }
540
541 if (arg.starts_with("-")) {
542 std::cerr << "Error: Unknown option '" << arg << "'\n";
543 return false;
544 }
545
546 positional_args.push_back(arg);
547 }
548
549 if (positional_args.size() != 2) {
550 std::cerr << "Error: Expected <peer> <port> arguments\n";
551 return false;
552 }
553
554 opts.peer_host = positional_args[0];
555
556 try {
557 int port_int = std::stoi(positional_args[1]);
558 if (port_int < 1 || port_int > 65535) {
559 std::cerr << "Error: Port must be between 1 and 65535\n";
560 return false;
561 }
562 opts.peer_port = static_cast<uint16_t>(port_int);
563 } catch (...) {
564 std::cerr << "Error: Invalid port number '" << positional_args[1] << "'\n";
565 return false;
566 }
567
568 // Validate move destination
569 if (opts.move_destination.empty()) {
570 std::cerr << "Error: Move destination (-aem) is required\n";
571 return false;
572 }
573
574 // Load query file if specified
575 if (!opts.query_file.empty()) {
576 if (!load_query_file(opts.query_file, opts.keys)) {
577 return false;
578 }
579 }
580
581 // Validate at least one key
582 if (opts.keys.empty()) {
583 std::cerr << "Error: At least one query key (-k) is required\n";
584 return false;
585 }
586
587 return true;
588}
589
590// =============================================================================
591// Query Dataset Building
592// =============================================================================
593
594kcenon::pacs::core::dicom_dataset build_query_dataset(const options& opts) {
595 using namespace kcenon::pacs::core;
596 using namespace kcenon::pacs::encoding;
597
598 dicom_dataset ds;
599
600 std::string level_str{query_level_to_string(opts.level)};
601 ds.set_string(tags::query_retrieve_level, vr_type::CS, level_str);
602
603 for (const auto& key : opts.keys) {
604 ds.set_string(key.tag, vr_type::UN, key.value);
605 }
606
607 return ds;
608}
609
610// =============================================================================
611// Move Implementation
612// =============================================================================
613
615 uint16_t message_id,
616 std::string_view sop_class_uid,
617 std::string_view move_destination) {
618
619 using namespace kcenon::pacs::network::dimse;
620
621 dimse_message msg{command_field::c_move_rq, message_id};
622 msg.set_affected_sop_class_uid(sop_class_uid);
623 msg.set_priority(priority_medium);
624
625 msg.command_set().set_string(
626 tag_move_destination,
628 std::string(move_destination));
629
630 return msg;
631}
632
633int perform_move(const options& opts) {
634 using namespace kcenon::pacs::network;
635 using namespace kcenon::pacs::network::dimse;
636 using namespace kcenon::pacs::services;
637
638 auto sop_class_uid = get_move_sop_class_uid(opts.model);
639
640 if (!opts.quiet) {
641 std::cout << "Requesting Association\n";
642 if (opts.verbose) {
643 std::cout << " Peer: " << opts.peer_host << ":"
644 << opts.peer_port << "\n";
645 std::cout << " Calling AE: " << opts.calling_ae_title << "\n";
646 std::cout << " Called AE: " << opts.called_ae_title << "\n";
647 std::cout << " Move Dest: " << opts.move_destination << "\n";
648 std::cout << " Query Model: " << query_model_to_string(opts.model)
649 << "\n";
650 std::cout << " Query Level: " << query_level_to_string(opts.level)
651 << "\n\n";
652 }
653 }
654
655 // Configure association
656 association_config config;
657 config.calling_ae_title = opts.calling_ae_title;
658 config.called_ae_title = opts.called_ae_title;
659 config.implementation_class_uid = "1.2.826.0.1.3680043.2.1545.1";
660 config.implementation_version_name = "MOVE_SCU_100";
661
662 config.proposed_contexts.push_back({
663 1,
664 std::string(sop_class_uid),
665 {
666 "1.2.840.10008.1.2.1",
667 "1.2.840.10008.1.2"
668 }
669 });
670
671 // Establish association
672 auto start_time = std::chrono::steady_clock::now();
673 auto timeout = std::chrono::duration_cast<std::chrono::milliseconds>(
674 opts.connection_timeout);
675 auto connect_result = association::connect(opts.peer_host, opts.peer_port,
676 config, timeout);
677
678 if (connect_result.is_err()) {
679 std::cerr << "Association Failed: " << connect_result.error().message
680 << "\n";
681 return 2;
682 }
683
684 auto& assoc = connect_result.value();
685
686 if (!opts.quiet) {
687 std::cout << "Association Accepted\n";
688 }
689
690 if (!assoc.has_accepted_context(sop_class_uid)) {
691 std::cerr << "Error: C-MOVE SOP Class not accepted by remote SCP\n";
692 assoc.abort();
693 return 2;
694 }
695
696 auto context_id_opt = assoc.accepted_context_id(sop_class_uid);
697 if (!context_id_opt) {
698 std::cerr << "Error: Could not get presentation context ID\n";
699 assoc.abort();
700 return 2;
701 }
702 uint8_t context_id = *context_id_opt;
703
704 auto query_ds = build_query_dataset(opts);
705
706 auto move_rq = make_c_move_rq(1, sop_class_uid, opts.move_destination);
707 move_rq.set_dataset(std::move(query_ds));
708
709 if (!opts.quiet) {
710 std::cout << "Initiating C-MOVE to " << opts.move_destination << "...\n";
711 }
712
713 auto send_result = assoc.send_dimse(context_id, move_rq);
714 if (send_result.is_err()) {
715 std::cerr << "Send Failed: " << send_result.error().message << "\n";
716 assoc.abort();
717 return 2;
718 }
719
720 // Progress tracking
721 move_progress progress;
722 progress.reset();
723
724 bool move_complete = false;
725 uint16_t final_completed = 0;
726 uint16_t final_failed = 0;
727 uint16_t final_warning = 0;
728
729 auto dimse_timeout = opts.dimse_timeout.count() > 0
730 ? std::chrono::duration_cast<std::chrono::milliseconds>(opts.dimse_timeout)
731 : std::chrono::milliseconds{60000};
732
733 while (!move_complete) {
734 auto recv_result = assoc.receive_dimse(dimse_timeout);
735 if (recv_result.is_err()) {
736 std::cerr << "\nReceive Failed: " << recv_result.error().message
737 << "\n";
738 assoc.abort();
739 return 2;
740 }
741
742 auto& [recv_context_id, msg] = recv_result.value();
743
744 if (msg.command() != command_field::c_move_rsp) {
745 std::cerr << "\nError: Unexpected response (expected C-MOVE-RSP)\n";
746 assoc.abort();
747 return 2;
748 }
749
750 auto status = msg.status();
751
752 if (auto remaining = msg.remaining_subops()) {
753 progress.remaining = *remaining;
754 }
755 if (auto completed = msg.completed_subops()) {
756 progress.completed = *completed;
757 final_completed = *completed;
758 }
759 if (auto failed = msg.failed_subops()) {
760 progress.failed = *failed;
761 final_failed = *failed;
762 }
763 if (auto warning = msg.warning_subops()) {
764 progress.warning = *warning;
765 final_warning = *warning;
766 }
767
768 if (opts.show_progress && !opts.quiet) {
769 display_progress(progress, opts.verbose);
770 }
771
772 if (status == status_success ||
773 status == status_cancel ||
774 (status & 0xF000) == 0xA000 ||
775 (status & 0xF000) == 0xC000) {
776
777 move_complete = true;
778
779 if (status != status_success && status != status_cancel &&
780 !opts.quiet) {
781 std::cerr << "\nC-MOVE failed with status: 0x" << std::hex
782 << status << std::dec << "\n";
783 }
784 }
785 }
786
787 if (opts.show_progress && !opts.quiet) {
788 std::cout << "\n";
789 }
790
791 if (!opts.quiet && opts.verbose) {
792 std::cout << "Releasing Association\n";
793 }
794
795 auto release_result = assoc.release(timeout);
796 if (release_result.is_err() && opts.verbose) {
797 std::cerr << "Warning: Release failed: "
798 << release_result.error().message << "\n";
799 }
800
801 auto end_time = std::chrono::steady_clock::now();
802 auto total_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
803 end_time - start_time);
804
805 // Print summary
806 if (!opts.quiet) {
807 std::cout << "\n========================================\n";
808 std::cout << " Move Summary\n";
809 std::cout << "========================================\n";
810 std::cout << " Destination: " << opts.move_destination << "\n";
811 std::cout << " Level: " << query_level_to_string(opts.level)
812 << "\n";
813 std::cout << " ----------------------------------------\n";
814 std::cout << " Completed: " << final_completed << "\n";
815 if (final_warning > 0) {
816 std::cout << " Warnings: " << final_warning << "\n";
817 }
818 if (final_failed > 0) {
819 std::cout << " Failed: " << final_failed << "\n";
820 }
821 std::cout << " Total Time: " << total_duration.count() << " ms\n";
822 std::cout << "========================================\n";
823 }
824
825 if (final_failed > 0 && final_completed == 0) {
826 return 2;
827 } else if (final_failed > 0) {
828 return 1;
829 }
830 return 0;
831}
832
833} // namespace
834
835// =============================================================================
836// Main Entry Point
837// =============================================================================
838
839int main(int argc, char* argv[]) {
840 options opts;
841
842 if (!parse_arguments(argc, argv, opts)) {
843 if (!opts.show_help && !opts.show_version) {
844 std::cerr << "\nUse --help for usage information.\n";
845 return 2;
846 }
847 }
848
849 if (opts.show_version) {
850 print_version();
851 return 0;
852 }
853
854 if (opts.show_help) {
855 print_banner();
856 print_usage(argv[0]);
857 return 0;
858 }
859
860 if (!opts.quiet) {
861 print_banner();
862 }
863
864 return perform_move(opts);
865}
DICOM Association management per PS3.8.
DICOM Dataset - ordered collection of Data Elements.
DICOM Part 10 file handling for reading/writing DICOM files.
DICOM Tag representation (Group, Element pairs)
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 message_id
Message ID.
constexpr dicom_tag status
Status.
constexpr dicom_tag sop_class_uid
SOP Class UID.
constexpr dicom_tag move_destination
Move Destination.
@ AE
Application Entity (16 chars max)
constexpr int connection_timeout
Definition result.h:95
auto query_level_to_string(query_level level) -> std::string
Convert query level to string.
Definition events.h:176
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 patient_root_move_sop_class_uid
Patient Root Query/Retrieve Information Model - MOVE.
@ study_root
Study Root Query/Retrieve Information Model.
@ patient_root
Patient Root Query/Retrieve Information Model.
constexpr int timeout
Lock timeout exceeded.
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