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 <chrono>
29#include <cstdlib>
30#include <iomanip>
31#include <iostream>
32#include <numeric>
33#include <sstream>
34#include <string>
35#include <thread>
36#include <vector>
37
38namespace {
39
40// =============================================================================
41// Constants
42// =============================================================================
43
45constexpr const char* version_string = "1.0.0";
46
48constexpr const char* default_calling_ae = "ECHOSCU";
49
51constexpr const char* default_called_ae = "ANY-SCP";
52
54constexpr auto default_connection_timeout = std::chrono::seconds{30};
55
57constexpr auto default_acse_timeout = std::chrono::seconds{30};
58
60constexpr auto default_dimse_timeout = std::chrono::seconds{0};
61
63constexpr size_t max_ae_title_length = 16;
64
65// =============================================================================
66// Output Modes
67// =============================================================================
68
72enum class verbosity_level {
73 quiet,
74 normal,
75 verbose,
76 debug
77};
78
79// =============================================================================
80// Command Line Options
81// =============================================================================
82
86struct options {
87 // Network options
88 std::string peer_host;
89 uint16_t peer_port{0};
90 std::string calling_ae_title{default_calling_ae};
91 std::string called_ae_title{default_called_ae};
92
93 // Timeout options
94 std::chrono::seconds connection_timeout{default_connection_timeout};
95 std::chrono::seconds acse_timeout{default_acse_timeout};
96 std::chrono::seconds dimse_timeout{default_dimse_timeout};
97
98 // Repeat options
99 int repeat_count{1};
100 std::chrono::milliseconds repeat_delay{0};
101
102 // Output options
103 verbosity_level verbosity{verbosity_level::normal};
104
105 // TLS options (for future extension)
106 bool use_tls{false};
107 std::string tls_cert_file;
108 std::string tls_key_file;
109 std::string tls_ca_file;
110
111 // Help/version flags
112 bool show_help{false};
113 bool show_version{false};
114};
115
119struct echo_result {
120 bool success{false};
121 uint16_t status_code{0};
122 std::chrono::milliseconds association_time{0};
123 std::chrono::milliseconds echo_time{0};
124 std::chrono::milliseconds total_time{0};
125 std::string error_message;
126};
127
131struct echo_statistics {
132 int total_attempts{0};
133 int successful{0};
134 int failed{0};
135 std::vector<std::chrono::milliseconds> response_times;
136
137 [[nodiscard]] double success_rate() const {
138 return total_attempts > 0
139 ? (static_cast<double>(successful) / total_attempts) * 100.0
140 : 0.0;
141 }
142
143 [[nodiscard]] std::chrono::milliseconds min_time() const {
144 if (response_times.empty()) return std::chrono::milliseconds{0};
145 return *std::min_element(response_times.begin(), response_times.end());
146 }
147
148 [[nodiscard]] std::chrono::milliseconds max_time() const {
149 if (response_times.empty()) return std::chrono::milliseconds{0};
150 return *std::max_element(response_times.begin(), response_times.end());
151 }
152
153 [[nodiscard]] std::chrono::milliseconds avg_time() const {
154 if (response_times.empty()) return std::chrono::milliseconds{0};
155 auto sum = std::accumulate(
156 response_times.begin(), response_times.end(),
157 std::chrono::milliseconds{0});
158 return sum / static_cast<int>(response_times.size());
159 }
160};
161
162// =============================================================================
163// Output Functions
164// =============================================================================
165
169void print_banner() {
170 std::cout << R"(
171 _____ ____ _ _ ___ ____ ____ _ _
172 | ____/ ___| | | |/ _ \ / ___| / ___| | | |
173 | _|| | | |_| | | | | \___ \| | | | | |
174 | |__| |___| _ | |_| | ___) | |___| |_| |
175 |_____\____|_| |_|\___/ |____/ \____|\___/
176
177 DICOM Connectivity Test Client v)" << version_string << R"(
178)" << "\n";
179}
180
185void print_usage(const char* program_name) {
186 std::cout << "Usage: " << program_name << R"( [options] <peer> <port>
187
188Arguments:
189 peer Remote host address (IP or hostname)
190 port Remote port number (typically 104 or 11112)
191
192Options:
193 -h, --help Show this help message and exit
194 -v, --verbose Verbose output mode
195 -d, --debug Debug output mode (more details than verbose)
196 -q, --quiet Quiet mode (minimal output)
197 --version Show version information
198
199Network Options:
200 -aet, --aetitle <aetitle> Calling AE Title (default: ECHOSCU)
201 -aec, --call <aetitle> Called AE Title (default: ANY-SCP)
202 -to, --timeout <seconds> Connection timeout (default: 30)
203 -ta, --acse-timeout <seconds> ACSE timeout (default: 30)
204 -td, --dimse-timeout <seconds> DIMSE timeout (default: 0=infinite)
205
206Repeat Options:
207 -r, --repeat <count> Repeat echo request n times (default: 1)
208 --repeat-delay <ms> Delay between repeats in milliseconds (default: 0)
209
210TLS Options (not yet implemented):
211 --tls Enable TLS connection
212 --tls-cert <file> TLS certificate file
213 --tls-key <file> TLS private key file
214 --tls-ca <file> TLS CA certificate file
215
216Examples:
217 # Basic echo test
218 )" << program_name << R"( localhost 11112
219
220 # With custom AE Titles
221 )" << program_name << R"( -aet MYSCU -aec PACS localhost 11112
222
223 # Repeat test for connectivity monitoring
224 )" << program_name << R"( -r 10 --repeat-delay 1000 localhost 11112
225
226 # Verbose output with timeout
227 )" << program_name << R"( -v -to 60 192.168.1.100 104
228
229Exit Codes:
230 0 Success - All echo responses received
231 1 Error - Echo failed or partial failure
232 2 Error - Invalid arguments
233)";
234}
235
239void print_version() {
240 std::cout << "echo_scu version " << version_string << "\n";
241 std::cout << "PACS System DICOM Utilities\n";
242 std::cout << "Copyright (c) 2024\n";
243}
244
245// =============================================================================
246// Argument Parsing
247// =============================================================================
248
256bool parse_timeout(const std::string& value, std::chrono::seconds& result,
257 const std::string& option_name) {
258 try {
259 int seconds = std::stoi(value);
260 if (seconds < 0) {
261 std::cerr << "Error: " << option_name << " must be non-negative\n";
262 return false;
263 }
264 result = std::chrono::seconds{seconds};
265 return true;
266 } catch (const std::exception&) {
267 std::cerr << "Error: Invalid value for " << option_name << ": '"
268 << value << "'\n";
269 return false;
270 }
271}
272
281bool parse_int(const std::string& value, int& result,
282 const std::string& option_name, int min_value = 0) {
283 try {
284 result = std::stoi(value);
285 if (result < min_value) {
286 std::cerr << "Error: " << option_name << " must be at least "
287 << min_value << "\n";
288 return false;
289 }
290 return true;
291 } catch (const std::exception&) {
292 std::cerr << "Error: Invalid value for " << option_name << ": '"
293 << value << "'\n";
294 return false;
295 }
296}
297
304bool validate_ae_title(const std::string& ae_title,
305 const std::string& option_name) {
306 if (ae_title.empty()) {
307 std::cerr << "Error: " << option_name << " cannot be empty\n";
308 return false;
309 }
310 if (ae_title.length() > max_ae_title_length) {
311 std::cerr << "Error: " << option_name << " exceeds "
312 << max_ae_title_length << " characters\n";
313 return false;
314 }
315 return true;
316}
317
325bool parse_arguments(int argc, char* argv[], options& opts) {
326 std::vector<std::string> positional_args;
327
328 for (int i = 1; i < argc; ++i) {
329 std::string arg = argv[i];
330
331 // Help options
332 if (arg == "-h" || arg == "--help") {
333 opts.show_help = true;
334 return true;
335 }
336 if (arg == "--version") {
337 opts.show_version = true;
338 return true;
339 }
340
341 // Verbosity options
342 if (arg == "-v" || arg == "--verbose") {
343 opts.verbosity = verbosity_level::verbose;
344 continue;
345 }
346 if (arg == "-d" || arg == "--debug") {
347 opts.verbosity = verbosity_level::debug;
348 continue;
349 }
350 if (arg == "-q" || arg == "--quiet") {
351 opts.verbosity = verbosity_level::quiet;
352 continue;
353 }
354
355 // Network options with values
356 if ((arg == "-aet" || arg == "--aetitle") && i + 1 < argc) {
357 opts.calling_ae_title = argv[++i];
358 if (!validate_ae_title(opts.calling_ae_title, "Calling AE Title")) {
359 return false;
360 }
361 continue;
362 }
363 if ((arg == "-aec" || arg == "--call") && i + 1 < argc) {
364 opts.called_ae_title = argv[++i];
365 if (!validate_ae_title(opts.called_ae_title, "Called AE Title")) {
366 return false;
367 }
368 continue;
369 }
370
371 // Timeout options
372 if ((arg == "-to" || arg == "--timeout") && i + 1 < argc) {
373 if (!parse_timeout(argv[++i], opts.connection_timeout,
374 "Connection timeout")) {
375 return false;
376 }
377 continue;
378 }
379 if ((arg == "-ta" || arg == "--acse-timeout") && i + 1 < argc) {
380 if (!parse_timeout(argv[++i], opts.acse_timeout, "ACSE timeout")) {
381 return false;
382 }
383 continue;
384 }
385 if ((arg == "-td" || arg == "--dimse-timeout") && i + 1 < argc) {
386 if (!parse_timeout(argv[++i], opts.dimse_timeout, "DIMSE timeout")) {
387 return false;
388 }
389 continue;
390 }
391
392 // Repeat options
393 if ((arg == "-r" || arg == "--repeat") && i + 1 < argc) {
394 if (!parse_int(argv[++i], opts.repeat_count, "Repeat count", 1)) {
395 return false;
396 }
397 continue;
398 }
399 if (arg == "--repeat-delay" && i + 1 < argc) {
400 int delay_ms = 0;
401 if (!parse_int(argv[++i], delay_ms, "Repeat delay", 0)) {
402 return false;
403 }
404 opts.repeat_delay = std::chrono::milliseconds{delay_ms};
405 continue;
406 }
407
408 // TLS options
409 if (arg == "--tls") {
410 opts.use_tls = true;
411 continue;
412 }
413 if (arg == "--tls-cert" && i + 1 < argc) {
414 opts.tls_cert_file = argv[++i];
415 continue;
416 }
417 if (arg == "--tls-key" && i + 1 < argc) {
418 opts.tls_key_file = argv[++i];
419 continue;
420 }
421 if (arg == "--tls-ca" && i + 1 < argc) {
422 opts.tls_ca_file = argv[++i];
423 continue;
424 }
425
426 // Check for unknown options
427 if (arg.starts_with("-")) {
428 std::cerr << "Error: Unknown option '" << arg << "'\n";
429 return false;
430 }
431
432 // Positional arguments
433 positional_args.push_back(arg);
434 }
435
436 // Validate positional arguments
437 if (positional_args.size() != 2) {
438 std::cerr << "Error: Expected <peer> <port> arguments\n";
439 return false;
440 }
441
442 opts.peer_host = positional_args[0];
443
444 // Parse port
445 try {
446 int port_int = std::stoi(positional_args[1]);
447 if (port_int < 1 || port_int > 65535) {
448 std::cerr << "Error: Port must be between 1 and 65535\n";
449 return false;
450 }
451 opts.peer_port = static_cast<uint16_t>(port_int);
452 } catch (const std::exception&) {
453 std::cerr << "Error: Invalid port number '" << positional_args[1]
454 << "'\n";
455 return false;
456 }
457
458 // TLS validation
459 if (opts.use_tls) {
460 std::cerr << "Warning: TLS support is not yet implemented\n";
461 }
462
463 return true;
464}
465
466// =============================================================================
467// Echo Implementation
468// =============================================================================
469
475echo_result perform_single_echo(const options& opts) {
476 using namespace kcenon::pacs::network;
477 using namespace kcenon::pacs::network::dimse;
478 using namespace kcenon::pacs::services;
479
480 echo_result result;
481 auto start_time = std::chrono::steady_clock::now();
482
483 // Configure association
484 association_config config;
485 config.calling_ae_title = opts.calling_ae_title;
486 config.called_ae_title = opts.called_ae_title;
487 config.implementation_class_uid = "1.2.826.0.1.3680043.2.1545.1";
488 config.implementation_version_name = "ECHO_SCU_100";
489
490 // Propose Verification SOP Class
491 config.proposed_contexts.push_back({
492 1, // Context ID (must be odd: 1, 3, 5, ...)
493 std::string(verification_sop_class_uid),
494 {
495 "1.2.840.10008.1.2.1", // Explicit VR Little Endian
496 "1.2.840.10008.1.2" // Implicit VR Little Endian
497 }
498 });
499
500 // Establish association
501 auto timeout = std::chrono::duration_cast<std::chrono::milliseconds>(
502 opts.connection_timeout);
503 auto connect_result = association::connect(
504 opts.peer_host, opts.peer_port, config, timeout);
505
506 if (connect_result.is_err()) {
507 result.error_message = "Connection failed: " +
508 std::string(connect_result.error().message);
509 return result;
510 }
511
512 auto& assoc = connect_result.value();
513 auto connect_time = std::chrono::steady_clock::now();
514 result.association_time = std::chrono::duration_cast<std::chrono::milliseconds>(
515 connect_time - start_time);
516
517 // Verify we have an accepted context for Verification
518 if (!assoc.has_accepted_context(verification_sop_class_uid)) {
519 result.error_message = "Verification SOP Class not accepted by remote SCP";
520 assoc.abort();
521 return result;
522 }
523
524 // Get the accepted context ID
525 auto context_id_opt = assoc.accepted_context_id(verification_sop_class_uid);
526 if (!context_id_opt) {
527 result.error_message = "Could not get presentation context ID";
528 assoc.abort();
529 return result;
530 }
531 uint8_t context_id = *context_id_opt;
532
533 // Create and send C-ECHO request
534 auto echo_rq = make_c_echo_rq(1, verification_sop_class_uid);
535
536 auto send_result = assoc.send_dimse(context_id, echo_rq);
537 if (send_result.is_err()) {
538 result.error_message = "Send failed: " +
539 std::string(send_result.error().message);
540 assoc.abort();
541 return result;
542 }
543
544 // Receive C-ECHO response
545 auto dimse_timeout = opts.dimse_timeout.count() > 0
546 ? std::chrono::duration_cast<std::chrono::milliseconds>(opts.dimse_timeout)
547 : std::chrono::milliseconds{30000}; // Default if 0
548
549 auto recv_result = assoc.receive_dimse(dimse_timeout);
550 if (recv_result.is_err()) {
551 result.error_message = "Receive failed: " +
552 std::string(recv_result.error().message);
553 assoc.abort();
554 return result;
555 }
556
557 auto echo_time = std::chrono::steady_clock::now();
558 result.echo_time = std::chrono::duration_cast<std::chrono::milliseconds>(
559 echo_time - connect_time);
560
561 auto& [recv_context_id, echo_rsp] = recv_result.value();
562
563 // Check response
564 if (echo_rsp.command() != command_field::c_echo_rsp) {
565 result.error_message = "Unexpected response (expected C-ECHO-RSP)";
566 assoc.abort();
567 return result;
568 }
569
570 result.status_code = static_cast<uint16_t>(echo_rsp.status());
571
572 if (echo_rsp.status() != status_success) {
573 std::ostringstream oss;
574 oss << "C-ECHO failed with status: 0x" << std::hex << result.status_code;
575 result.error_message = oss.str();
576 (void)assoc.release();
577 return result;
578 }
579
580 // Release association gracefully
581 (void)assoc.release(timeout);
582
583 auto end_time = std::chrono::steady_clock::now();
584 result.total_time = std::chrono::duration_cast<std::chrono::milliseconds>(
585 end_time - start_time);
586
587 result.success = true;
588 return result;
589}
590
596int perform_echo(const options& opts) {
597 echo_statistics stats;
598 bool is_quiet = opts.verbosity == verbosity_level::quiet;
599 bool is_verbose = opts.verbosity == verbosity_level::verbose ||
600 opts.verbosity == verbosity_level::debug;
601
602 // Print connection info
603 if (!is_quiet) {
604 std::cout << "Requesting Association with "
605 << opts.peer_host << ":" << opts.peer_port << "\n";
606 std::cout << " Calling AE Title: " << opts.calling_ae_title << "\n";
607 std::cout << " Called AE Title: " << opts.called_ae_title << "\n";
608
609 if (is_verbose) {
610 std::cout << " Connection Timeout: "
611 << opts.connection_timeout.count() << "s\n";
612 std::cout << " ACSE Timeout: "
613 << opts.acse_timeout.count() << "s\n";
614 std::cout << " DIMSE Timeout: "
615 << (opts.dimse_timeout.count() == 0
616 ? "infinite"
617 : std::to_string(opts.dimse_timeout.count()) + "s")
618 << "\n";
619 }
620
621 if (opts.repeat_count > 1) {
622 std::cout << " Repeat Count: " << opts.repeat_count << "\n";
623 std::cout << " Repeat Delay: "
624 << opts.repeat_delay.count() << " ms\n";
625 }
626 std::cout << "\n";
627 }
628
629 // Perform echo operations
630 for (int i = 0; i < opts.repeat_count; ++i) {
631 stats.total_attempts++;
632
633 if (!is_quiet && opts.repeat_count > 1) {
634 std::cout << "Echo " << (i + 1) << "/" << opts.repeat_count << ": ";
635 std::cout.flush();
636 }
637
638 auto result = perform_single_echo(opts);
639
640 if (result.success) {
641 stats.successful++;
642 stats.response_times.push_back(result.echo_time);
643
644 if (!is_quiet) {
645 if (opts.repeat_count > 1) {
646 std::cout << "Success (";
647 std::cout << result.echo_time.count() << " ms)\n";
648 } else {
649 std::cout << "Association Accepted\n";
650 std::cout << "Sending Echo Request (Message ID: 1)\n";
651 std::cout << "Received Echo Response (Status: Success)\n";
652 std::cout << "Releasing Association\n";
653 std::cout << "Echo Successful\n";
654 }
655
656 if (is_verbose && opts.repeat_count == 1) {
657 std::cout << "\nStatistics:\n";
658 std::cout << " Association Time: "
659 << result.association_time.count() << " ms\n";
660 std::cout << " Echo Response Time: "
661 << result.echo_time.count() << " ms\n";
662 std::cout << " Total Time: "
663 << result.total_time.count() << " ms\n";
664 }
665 }
666 } else {
667 stats.failed++;
668
669 if (!is_quiet) {
670 if (opts.repeat_count > 1) {
671 std::cout << "Failed: " << result.error_message << "\n";
672 } else {
673 std::cerr << "Echo Failed: " << result.error_message << "\n";
674 }
675 }
676 }
677
678 // Delay between repeats
679 if (i < opts.repeat_count - 1 && opts.repeat_delay.count() > 0) {
680 std::this_thread::sleep_for(opts.repeat_delay);
681 }
682 }
683
684 // Print summary for multiple echo operations
685 if (!is_quiet && opts.repeat_count > 1) {
686 std::cout << "\n";
687 std::cout << "========================================\n";
688 std::cout << " Summary\n";
689 std::cout << "========================================\n";
690 std::cout << " Total Attempts: " << stats.total_attempts << "\n";
691 std::cout << " Successful: " << stats.successful << "\n";
692 std::cout << " Failed: " << stats.failed << "\n";
693 std::cout << std::fixed << std::setprecision(1);
694 std::cout << " Success Rate: " << stats.success_rate() << "%\n";
695
696 if (stats.successful > 0) {
697 std::cout << "\nResponse Times:\n";
698 std::cout << " Min: " << stats.min_time().count() << " ms\n";
699 std::cout << " Max: " << stats.max_time().count() << " ms\n";
700 std::cout << " Avg: " << stats.avg_time().count() << " ms\n";
701 }
702 std::cout << "========================================\n";
703 }
704
705 // Return appropriate exit code
706 if (stats.failed == 0) {
707 if (!is_quiet) {
708 std::cout << "Status: SUCCESS\n";
709 }
710 return 0;
711 } else if (stats.successful > 0) {
712 if (!is_quiet) {
713 std::cout << "Status: PARTIAL FAILURE\n";
714 }
715 return 1;
716 } else {
717 if (!is_quiet) {
718 std::cout << "Status: FAILURE\n";
719 }
720 return 1;
721 }
722}
723
724} // namespace
725
726// =============================================================================
727// Main Entry Point
728// =============================================================================
729
730int main(int argc, char* argv[]) {
731 options opts;
732
733 if (!parse_arguments(argc, argv, opts)) {
734 if (!opts.show_help && !opts.show_version) {
735 std::cerr << "\nUse --help for usage information.\n";
736 return 2;
737 }
738 }
739
740 if (opts.show_version) {
741 print_version();
742 return 0;
743 }
744
745 if (opts.show_help) {
746 print_banner();
747 print_usage(argv[0]);
748 return 0;
749 }
750
751 // Print banner unless quiet mode
752 if (opts.verbosity != verbosity_level::quiet) {
753 print_banner();
754 }
755
756 return perform_echo(opts);
757}
DICOM Association management per PS3.8.
DIMSE message encoding and decoding.
int main()
Definition main.cpp:84
constexpr int connection_timeout
Definition result.h:95
constexpr core::dicom_tag echo_time
Echo Time (0018,0081) - Type 2 Time in ms between the middle of the excitation pulse and peak of echo...
constexpr int timeout
Lock timeout exceeded.
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
DICOM Verification SCP service (C-ECHO handler)