49constexpr const char* default_calling_ae =
"WORKLIST_SCU";
70 std::string called_ae{
"ANY-SCP"};
71 std::string calling_ae{default_calling_ae};
76 std::string scheduled_date;
77 std::string scheduled_time;
80 std::string physician;
90 std::vector<query_key> custom_keys;
94 std::string output_file;
97 size_t max_results{0};
103std::string get_today_date() {
104 auto now = std::time(
nullptr);
105 auto* tm = std::localtime(&now);
106 std::ostringstream oss;
107 oss << std::put_time(tm,
"%Y%m%d");
114std::optional<query_key> parse_query_key(
const std::string& key_str) {
116 auto eq_pos = key_str.find(
'=');
117 if (eq_pos == std::string::npos) {
121 std::string tag_part = key_str.substr(0, eq_pos);
122 std::string value_part = key_str.substr(eq_pos + 1);
125 auto comma_pos = tag_part.find(
',');
126 if (comma_pos == std::string::npos) {
131 uint16_t group =
static_cast<uint16_t
>(
132 std::stoul(tag_part.substr(0, comma_pos),
nullptr, 16));
133 uint16_t element =
static_cast<uint16_t
>(
134 std::stoul(tag_part.substr(comma_pos + 1),
nullptr, 16));
135 return query_key{group, element, value_part};
136 }
catch (
const std::exception&) {
144void print_usage(
const char* program_name) {
146Worklist SCU - Modality Worklist Query Client
148Usage: )" << program_name << R"( [options] <peer> <port>
151 peer Remote host address (IP or hostname)
152 port Remote port number (typically 104 or 11112)
155 -aet, --aetitle <ae> Calling AE Title (default: WLSCU)
156 -aec, --call <ae> Called AE Title (default: ANY-SCP)
157 -to, --timeout <sec> Connection timeout in seconds (default: 30)
160 -k, --key <tag=value> Query key (e.g., "0008,0060=CT")
161 Can be specified multiple times
163Common Filters (convenience options):
164 --modality <mod> Filter by modality (CT, MR, US, XR, NM, etc.)
165 --date <date> Scheduled date (YYYYMMDD or range YYYYMMDD-YYYYMMDD)
166 Use "today" for current date
167 --time <time> Scheduled time (HHMMSS or range)
168 --station <name> Scheduled Station AE Title
169 --station-name <name> Scheduled Station Name
170 --physician <name> Scheduled Performing Physician Name
171 --patient-name <name> Patient name (wildcards: * ?)
172 --patient-id <id> Patient ID
173 --accession <num> Accession number
176 -o, --output <format> Output format: text, json, csv, xml (default: text)
177 --output-file <file> Write results to file
178 --max-results <n> Maximum results to display (default: unlimited)
181 -v, --verbose Verbose output mode
182 -d, --debug Debug output mode
183 -h, --help Show this help message
186 )" << program_name << R"( 192.168.1.100 11112 --modality CT
187 )" << program_name << R"( 192.168.1.100 11112 --modality MR --date today
188 )" << program_name << R"( -aec RIS_SCP --date 20241215 --station CT_SCANNER_01 192.168.1.100 11112
189 )" << program_name << R"( -k "0008,0060=CT" -k "0040,0002=20241215" 192.168.1.100 11112
190 )" << program_name << R"( --modality CT -o json --output-file worklist.json 192.168.1.100 11112
193 0 Success - Query completed with results
194 1 Success - Query completed with no results
195 2 Error - Invalid arguments or connection failure
205bool parse_arguments(
int argc,
char* argv[], options& opts) {
211 std::vector<std::string> positional_args;
213 for (
int i = 1; i < argc; ++i) {
214 std::string arg = argv[i];
217 if (arg ==
"--help" || arg ==
"-h") {
221 if (arg ==
"--verbose" || arg ==
"-v") {
225 if (arg ==
"--debug" || arg ==
"-d") {
232 if ((arg ==
"-aet" || arg ==
"--aetitle") && i + 1 < argc) {
233 opts.calling_ae = argv[++i];
234 if (opts.calling_ae.length() > 16) {
235 std::cerr <<
"Error: Calling AE title exceeds 16 characters\n";
240 if ((arg ==
"-aec" || arg ==
"--call") && i + 1 < argc) {
241 opts.called_ae = argv[++i];
242 if (opts.called_ae.length() > 16) {
243 std::cerr <<
"Error: Called AE title exceeds 16 characters\n";
248 if ((arg ==
"-to" || arg ==
"--timeout") && i + 1 < argc) {
250 int timeout_sec = std::stoi(argv[++i]);
251 if (timeout_sec < 1) {
252 std::cerr <<
"Error: Timeout must be positive\n";
255 opts.timeout = std::chrono::milliseconds{timeout_sec * 1000};
256 }
catch (
const std::exception&) {
257 std::cerr <<
"Error: Invalid timeout value\n";
264 if ((arg ==
"-k" || arg ==
"--key") && i + 1 < argc) {
265 auto key = parse_query_key(argv[++i]);
267 std::cerr <<
"Error: Invalid query key format. Use 'GGGG,EEEE=value'\n";
270 opts.custom_keys.push_back(*key);
275 if (arg ==
"--modality" && i + 1 < argc) {
276 opts.modality = argv[++i];
279 if (arg ==
"--date" && i + 1 < argc) {
280 std::string date_arg = argv[++i];
281 if (date_arg ==
"today") {
282 opts.scheduled_date = get_today_date();
284 opts.scheduled_date = date_arg;
288 if (arg ==
"--time" && i + 1 < argc) {
289 opts.scheduled_time = argv[++i];
292 if (arg ==
"--station" && i + 1 < argc) {
293 opts.station_ae = argv[++i];
296 if (arg ==
"--station-name" && i + 1 < argc) {
297 opts.station_name = argv[++i];
300 if (arg ==
"--physician" && i + 1 < argc) {
301 opts.physician = argv[++i];
304 if (arg ==
"--patient-name" && i + 1 < argc) {
305 opts.patient_name = argv[++i];
308 if (arg ==
"--patient-id" && i + 1 < argc) {
309 opts.patient_id = argv[++i];
312 if (arg ==
"--accession" && i + 1 < argc) {
313 opts.accession_number = argv[++i];
318 if ((arg ==
"-o" || arg ==
"--output" || arg ==
"--format") && i + 1 < argc) {
322 if (arg ==
"--output-file" && i + 1 < argc) {
323 opts.output_file = argv[++i];
326 if (arg ==
"--max-results" && i + 1 < argc) {
328 opts.max_results =
static_cast<size_t>(std::stoul(argv[++i]));
329 }
catch (
const std::exception&) {
330 std::cerr <<
"Error: Invalid max-results value\n";
337 if (arg ==
"--calling-ae" && i + 1 < argc) {
338 opts.calling_ae = argv[++i];
339 if (opts.calling_ae.length() > 16) {
340 std::cerr <<
"Error: Calling AE title exceeds 16 characters\n";
348 std::cerr <<
"Error: Unknown option '" << arg <<
"'\n";
353 positional_args.push_back(arg);
357 if (positional_args.size() < 2) {
358 std::cerr <<
"Error: Missing required arguments <peer> <port>\n";
361 if (positional_args.size() > 2) {
362 std::cerr <<
"Error: Too many positional arguments\n";
366 opts.host = positional_args[0];
370 int port_int = std::stoi(positional_args[1]);
371 if (port_int < 1 || port_int > 65535) {
372 std::cerr <<
"Error: Port must be between 1 and 65535\n";
375 opts.port =
static_cast<uint16_t
>(port_int);
376 }
catch (
const std::exception&) {
377 std::cerr <<
"Error: Invalid port number '" << positional_args[1] <<
"'\n";
387int perform_query(
const options& opts) {
392 std::cout <<
"Connecting to " << opts.host <<
":" << opts.port <<
"...\n";
393 std::cout <<
" Calling AE: " << opts.calling_ae <<
"\n";
394 std::cout <<
" Called AE: " << opts.called_ae <<
"\n";
395 std::cout <<
" Query Type: Modality Worklist\n";
396 if (!opts.modality.empty()) {
397 std::cout <<
" Modality: " << opts.modality <<
"\n";
399 if (!opts.scheduled_date.empty()) {
400 std::cout <<
" Sched Date: " << opts.scheduled_date <<
"\n";
402 if (!opts.station_ae.empty()) {
403 std::cout <<
" Station AE: " << opts.station_ae <<
"\n";
418 std::string(worklist_find_sop_class_uid),
420 "1.2.840.10008.1.2.1",
426 auto start_time = std::chrono::steady_clock::now();
427 auto connect_result = association::connect(opts.host, opts.port, config, opts.timeout);
429 if (connect_result.is_err()) {
430 std::cerr <<
"Failed to establish association: "
431 << connect_result.error().message <<
"\n";
435 auto& assoc = connect_result.value();
436 auto connect_time = std::chrono::steady_clock::now();
439 auto connect_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
440 connect_time - start_time);
441 std::cout <<
"Association established in " << connect_duration.count() <<
" ms\n";
445 if (!assoc.has_accepted_context(worklist_find_sop_class_uid)) {
446 std::cerr <<
"Error: Modality Worklist SOP Class not accepted by remote SCP\n";
464 scu_config.
timeout = opts.timeout;
471 std::cout <<
"Sending C-FIND request...\n";
477 if (opts.custom_keys.empty()) {
479 auto result = scu.query(assoc, keys);
480 if (result.is_err()) {
481 std::cerr <<
"Query failed: " << result.error().message <<
"\n";
485 query_result = std::move(result.value());
488 auto query_ds = scu.query(assoc, keys);
489 if (query_ds.is_err()) {
490 std::cerr <<
"Query failed: " << query_ds.error().message <<
"\n";
494 query_result = std::move(query_ds.value());
500 for (
const auto& key : opts.custom_keys) {
501 std::cout <<
" Custom key: (" << std::hex << std::setw(4)
502 << std::setfill(
'0') << key.group <<
","
503 << std::setw(4) << key.element << std::dec
504 <<
") = \"" << key.value <<
"\"\n";
510 if (query_result.is_success()) {
511 std::cout <<
"Query completed successfully.\n";
512 }
else if (query_result.is_cancelled()) {
513 std::cerr <<
"Query was cancelled.\n";
515 std::cerr <<
"Query completed with status: 0x"
516 << std::hex << query_result.status << std::dec <<
"\n";
522 std::cout <<
"Releasing association...\n";
525 auto release_result = assoc.release(opts.timeout);
526 if (release_result.is_err() && opts.verbose) {
527 std::cerr <<
"Warning: Release failed: " << release_result.error().message <<
"\n";
530 auto end_time = std::chrono::steady_clock::now();
531 auto total_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
532 end_time - start_time);
535 std::vector<kcenon::pacs::core::dicom_dataset> result_datasets;
536 result_datasets.reserve(query_result.items.size());
537 for (
const auto& item : query_result.items) {
538 result_datasets.push_back(
item.dataset);
543 std::string formatted_output = formatter.format(result_datasets);
546 if (!opts.output_file.empty()) {
547 std::ofstream outfile(opts.output_file);
549 std::cerr <<
"Error: Cannot open output file '" << opts.output_file <<
"'\n";
552 outfile << formatted_output;
555 std::cout <<
"Results written to: " << opts.output_file <<
"\n";
558 std::cout << formatted_output;
563 std::cout <<
"\n========================================\n";
564 std::cout <<
" Summary\n";
565 std::cout <<
"========================================\n";
566 std::cout <<
" Total items: " << query_result.items.size();
567 if (opts.max_results > 0 && query_result.total_pending > opts.max_results) {
568 std::cout <<
" (limited from " << query_result.total_pending <<
")";
571 std::cout <<
" Query time: " << total_duration.count() <<
" ms\n";
572 std::cout <<
" Library stats: " << scu.queries_performed() <<
" queries, "
573 << scu.total_items() <<
" items\n";
574 std::cout <<
"========================================\n";
577 return query_result.items.empty() ? 1 : 0;
582int main(
int argc,
char* argv[]) {
584 bool show_banner =
true;
585 for (
int i = 1; i < argc; ++i) {
586 std::string arg = argv[i];
587 if ((arg ==
"--format" || arg ==
"--output" || arg ==
"-o") && i + 1 < argc) {
588 std::string fmt = argv[i + 1];
589 if (fmt ==
"json" || fmt ==
"csv" || fmt ==
"xml") {
598 __ __ _ _ _ _ ____ ____ _ _
599 \ \ / /__ _ __| | _| (_)___| |_ / ___| / ___| | | |
600 \ \ /\ / / _ \| '__| |/ / | / __| __| \___ \| | | | | |
601 \ V V / (_) | | | <| | \__ \ |_ ___) | |___| |_| |
602 \_/\_/ \___/|_| |_|\_\_|_|___/\__| |____/ \____|\___/
604 Modality Worklist Query Client
610 if (!parse_arguments(argc, argv, opts)) {
611 print_usage(argv[0]);
615 return perform_query(opts);
DICOM Association management per PS3.8.
Compile-time constants for commonly used DICOM tags.
@ station_ae
(0008,1010) Station Name or calling AE
std::chrono::milliseconds default_timeout()
Default timeout for test operations (5s normal, 30s CI)
constexpr int timeout
Lock timeout exceeded.
output_format
Output format enumeration.
@ table
Human-readable table format (alias: text)
output_format parse_output_format(std::string_view format_str)
Parse output format from string.
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
Typed query keys for Modality Worklist queries.
std::string scheduled_station_ae
Scheduled Station AE Title (0040,0001)
std::string scheduled_date
Scheduled Procedure Step Start Date (0040,0002) - YYYYMMDD or range.
std::string modality
Modality (0008,0060) - e.g., CT, MR, US, XR.
std::string scheduled_physician
Scheduled Performing Physician's Name (0040,0006)
std::string accession_number
Accession Number (0008,0050)
std::string patient_name
Patient's Name (0010,0010) - supports wildcards (* ?)
std::string patient_id
Patient ID (0010,0020)
std::string scheduled_time
Scheduled Procedure Step Start Time (0040,0003) - HHMMSS or range.
Result of a Modality Worklist query operation.
Configuration for Worklist SCU service.
std::chrono::milliseconds timeout
Timeout for receiving query responses (milliseconds)
size_t max_results
Maximum number of results to return (0 = unlimited)
DICOM Modality Worklist SCP service (MWL C-FIND handler)
DICOM Modality Worklist SCU service (MWL C-FIND sender)