45constexpr const char* version_string =
"1.0.0";
48constexpr const char* default_calling_ae =
"ECHOSCU";
51constexpr const char* default_called_ae =
"ANY-SCP";
54constexpr auto default_connection_timeout = std::chrono::seconds{30};
57constexpr auto default_acse_timeout = std::chrono::seconds{30};
60constexpr auto default_dimse_timeout = std::chrono::seconds{0};
63constexpr size_t max_ae_title_length = 16;
72enum class verbosity_level {
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};
95 std::chrono::seconds acse_timeout{default_acse_timeout};
96 std::chrono::seconds dimse_timeout{default_dimse_timeout};
100 std::chrono::milliseconds repeat_delay{0};
103 verbosity_level verbosity{verbosity_level::normal};
107 std::string tls_cert_file;
108 std::string tls_key_file;
109 std::string tls_ca_file;
112 bool show_help{
false};
113 bool show_version{
false};
121 uint16_t status_code{0};
122 std::chrono::milliseconds association_time{0};
124 std::chrono::milliseconds total_time{0};
125 std::string error_message;
131struct echo_statistics {
132 int total_attempts{0};
135 std::vector<std::chrono::milliseconds> response_times;
137 [[nodiscard]]
double success_rate()
const {
138 return total_attempts > 0
139 ? (
static_cast<double>(successful) / total_attempts) * 100.0
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());
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());
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());
171 _____ ____ _ _ ___ ____ ____ _ _
172 | ____/ ___| | | |/ _ \ / ___| / ___| | | |
173 | _|| | | |_| | | | | \___ \| | | | | |
174 | |__| |___| _ | |_| | ___) | |___| |_| |
175 |_____\____|_| |_|\___/ |____/ \____|\___/
177 DICOM Connectivity Test Client v)" << version_string << R"(
185void print_usage(
const char* program_name) {
186 std::cout <<
"Usage: " << program_name << R
"( [options] <peer> <port>
189 peer Remote host address (IP or hostname)
190 port Remote port number (typically 104 or 11112)
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
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)
207 -r, --repeat <count> Repeat echo request n times (default: 1)
208 --repeat-delay <ms> Delay between repeats in milliseconds (default: 0)
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
218 )" << program_name << R"( localhost 11112
220 # With custom AE Titles
221 )" << program_name << R"( -aet MYSCU -aec PACS localhost 11112
223 # Repeat test for connectivity monitoring
224 )" << program_name << R"( -r 10 --repeat-delay 1000 localhost 11112
226 # Verbose output with timeout
227 )" << program_name << R"( -v -to 60 192.168.1.100 104
230 0 Success - All echo responses received
231 1 Error - Echo failed or partial failure
232 2 Error - Invalid arguments
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";
256bool parse_timeout(
const std::string& value, std::chrono::seconds& result,
257 const std::string& option_name) {
259 int seconds = std::stoi(value);
261 std::cerr <<
"Error: " << option_name <<
" must be non-negative\n";
264 result = std::chrono::seconds{seconds};
266 }
catch (
const std::exception&) {
267 std::cerr <<
"Error: Invalid value for " << option_name <<
": '"
281bool parse_int(
const std::string& value,
int& result,
282 const std::string& option_name,
int min_value = 0) {
284 result = std::stoi(value);
285 if (result < min_value) {
286 std::cerr <<
"Error: " << option_name <<
" must be at least "
287 << min_value <<
"\n";
291 }
catch (
const std::exception&) {
292 std::cerr <<
"Error: Invalid value for " << option_name <<
": '"
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";
310 if (ae_title.length() > max_ae_title_length) {
311 std::cerr <<
"Error: " << option_name <<
" exceeds "
312 << max_ae_title_length <<
" characters\n";
325bool parse_arguments(
int argc,
char* argv[], options& opts) {
326 std::vector<std::string> positional_args;
328 for (
int i = 1; i < argc; ++i) {
329 std::string arg = argv[i];
332 if (arg ==
"-h" || arg ==
"--help") {
333 opts.show_help =
true;
336 if (arg ==
"--version") {
337 opts.show_version =
true;
342 if (arg ==
"-v" || arg ==
"--verbose") {
343 opts.verbosity = verbosity_level::verbose;
346 if (arg ==
"-d" || arg ==
"--debug") {
347 opts.verbosity = verbosity_level::debug;
350 if (arg ==
"-q" || arg ==
"--quiet") {
351 opts.verbosity = verbosity_level::quiet;
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")) {
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")) {
372 if ((arg ==
"-to" || arg ==
"--timeout") && i + 1 < argc) {
373 if (!parse_timeout(argv[++i], opts.connection_timeout,
374 "Connection timeout")) {
379 if ((arg ==
"-ta" || arg ==
"--acse-timeout") && i + 1 < argc) {
380 if (!parse_timeout(argv[++i], opts.acse_timeout,
"ACSE timeout")) {
385 if ((arg ==
"-td" || arg ==
"--dimse-timeout") && i + 1 < argc) {
386 if (!parse_timeout(argv[++i], opts.dimse_timeout,
"DIMSE timeout")) {
393 if ((arg ==
"-r" || arg ==
"--repeat") && i + 1 < argc) {
394 if (!parse_int(argv[++i], opts.repeat_count,
"Repeat count", 1)) {
399 if (arg ==
"--repeat-delay" && i + 1 < argc) {
401 if (!parse_int(argv[++i], delay_ms,
"Repeat delay", 0)) {
404 opts.repeat_delay = std::chrono::milliseconds{delay_ms};
409 if (arg ==
"--tls") {
413 if (arg ==
"--tls-cert" && i + 1 < argc) {
414 opts.tls_cert_file = argv[++i];
417 if (arg ==
"--tls-key" && i + 1 < argc) {
418 opts.tls_key_file = argv[++i];
421 if (arg ==
"--tls-ca" && i + 1 < argc) {
422 opts.tls_ca_file = argv[++i];
427 if (arg.starts_with(
"-")) {
428 std::cerr <<
"Error: Unknown option '" << arg <<
"'\n";
433 positional_args.push_back(arg);
437 if (positional_args.size() != 2) {
438 std::cerr <<
"Error: Expected <peer> <port> arguments\n";
442 opts.peer_host = positional_args[0];
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";
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]
460 std::cerr <<
"Warning: TLS support is not yet implemented\n";
475echo_result perform_single_echo(
const options& opts) {
481 auto start_time = std::chrono::steady_clock::now();
493 std::string(verification_sop_class_uid),
495 "1.2.840.10008.1.2.1",
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);
506 if (connect_result.is_err()) {
507 result.error_message =
"Connection failed: " +
508 std::string(connect_result.error().message);
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);
518 if (!assoc.has_accepted_context(verification_sop_class_uid)) {
519 result.error_message =
"Verification SOP Class not accepted by remote SCP";
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";
531 uint8_t context_id = *context_id_opt;
534 auto echo_rq = make_c_echo_rq(1, verification_sop_class_uid);
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);
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};
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);
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);
561 auto& [recv_context_id, echo_rsp] = recv_result.value();
564 if (echo_rsp.command() != command_field::c_echo_rsp) {
565 result.error_message =
"Unexpected response (expected C-ECHO-RSP)";
570 result.status_code =
static_cast<uint16_t
>(echo_rsp.status());
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();
581 (void)assoc.release(timeout);
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);
587 result.success =
true;
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;
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";
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
617 : std::to_string(opts.dimse_timeout.count()) +
"s")
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";
630 for (
int i = 0; i < opts.repeat_count; ++i) {
631 stats.total_attempts++;
633 if (!is_quiet && opts.repeat_count > 1) {
634 std::cout <<
"Echo " << (i + 1) <<
"/" << opts.repeat_count <<
": ";
638 auto result = perform_single_echo(opts);
640 if (result.success) {
642 stats.response_times.push_back(result.echo_time);
645 if (opts.repeat_count > 1) {
646 std::cout <<
"Success (";
647 std::cout << result.echo_time.count() <<
" ms)\n";
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";
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";
670 if (opts.repeat_count > 1) {
671 std::cout <<
"Failed: " << result.error_message <<
"\n";
673 std::cerr <<
"Echo Failed: " << result.error_message <<
"\n";
679 if (i < opts.repeat_count - 1 && opts.repeat_delay.count() > 0) {
680 std::this_thread::sleep_for(opts.repeat_delay);
685 if (!is_quiet && opts.repeat_count > 1) {
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";
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";
702 std::cout <<
"========================================\n";
706 if (stats.failed == 0) {
708 std::cout <<
"Status: SUCCESS\n";
711 }
else if (stats.successful > 0) {
713 std::cout <<
"Status: PARTIAL FAILURE\n";
718 std::cout <<
"Status: FAILURE\n";
730int main(
int argc,
char* argv[]) {
733 if (!parse_arguments(argc, argv, opts)) {
734 if (!opts.show_help && !opts.show_version) {
735 std::cerr <<
"\nUse --help for usage information.\n";
740 if (opts.show_version) {
745 if (opts.show_help) {
747 print_usage(argv[0]);
752 if (opts.verbosity != verbosity_level::quiet) {
756 return perform_echo(opts);
DICOM Association management per PS3.8.
DIMSE message encoding and decoding.
@ failed
Job failed with error.
@ normal
Standard priority.
constexpr int connection_timeout
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::string implementation_class_uid
std::string implementation_version_name
std::vector< proposed_presentation_context > proposed_contexts
DICOM Verification SCP service (C-ECHO handler)