PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1
27
33
34#include <chrono>
35#include <cstdlib>
36#include <ctime>
37#include <fstream>
38#include <iomanip>
39#include <iostream>
40#include <optional>
41#include <sstream>
42#include <string>
43#include <utility>
44#include <vector>
45
46namespace {
47
49constexpr const char* default_calling_ae = "WORKLIST_SCU";
50
52constexpr auto default_timeout = std::chrono::milliseconds{30000};
53
57struct query_key {
58 uint16_t group;
59 uint16_t element;
60 std::string value;
61};
62
66struct options {
67 // Connection
68 std::string host;
69 uint16_t port{0};
70 std::string called_ae{"ANY-SCP"};
71 std::string calling_ae{default_calling_ae};
72 std::chrono::milliseconds timeout{default_timeout};
73
74 // Scheduled Procedure Step criteria
75 std::string modality;
76 std::string scheduled_date;
77 std::string scheduled_time;
78 std::string station_ae;
79 std::string station_name;
80 std::string physician;
81
82 // Patient criteria
83 std::string patient_name;
84 std::string patient_id;
85
86 // Study/Request criteria
87 std::string accession_number;
88
89 // Custom query keys (-k option)
90 std::vector<query_key> custom_keys;
91
92 // Output options
94 std::string output_file;
95 bool verbose{false};
96 bool debug{false};
97 size_t max_results{0}; // 0 = unlimited
98};
99
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");
108 return oss.str();
109}
110
114std::optional<query_key> parse_query_key(const std::string& key_str) {
115 // Find '=' separator
116 auto eq_pos = key_str.find('=');
117 if (eq_pos == std::string::npos) {
118 return std::nullopt;
119 }
120
121 std::string tag_part = key_str.substr(0, eq_pos);
122 std::string value_part = key_str.substr(eq_pos + 1);
123
124 // Find ',' separator in tag
125 auto comma_pos = tag_part.find(',');
126 if (comma_pos == std::string::npos) {
127 return std::nullopt;
128 }
129
130 try {
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&) {
137 return std::nullopt;
138 }
139}
140
144void print_usage(const char* program_name) {
145 std::cout << R"(
146Worklist SCU - Modality Worklist Query Client
147
148Usage: )" << program_name << R"( [options] <peer> <port>
149
150Arguments:
151 peer Remote host address (IP or hostname)
152 port Remote port number (typically 104 or 11112)
153
154Network Options:
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)
158
159Query Keys:
160 -k, --key <tag=value> Query key (e.g., "0008,0060=CT")
161 Can be specified multiple times
162
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
174
175Output Options:
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)
179
180General Options:
181 -v, --verbose Verbose output mode
182 -d, --debug Debug output mode
183 -h, --help Show this help message
184
185Examples:
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
191
192Exit Codes:
193 0 Success - Query completed with results
194 1 Success - Query completed with no results
195 2 Error - Invalid arguments or connection failure
196)";
197}
198
205bool parse_arguments(int argc, char* argv[], options& opts) {
206 if (argc < 3) {
207 return false;
208 }
209
210 // Collect positional arguments (host and port at the end)
211 std::vector<std::string> positional_args;
212
213 for (int i = 1; i < argc; ++i) {
214 std::string arg = argv[i];
215
216 // Help option
217 if (arg == "--help" || arg == "-h") {
218 return false;
219 }
220 // Verbose/Debug options
221 if (arg == "--verbose" || arg == "-v") {
222 opts.verbose = true;
223 continue;
224 }
225 if (arg == "--debug" || arg == "-d") {
226 opts.debug = true;
227 opts.verbose = true;
228 continue;
229 }
230
231 // Network options
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";
236 return false;
237 }
238 continue;
239 }
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";
244 return false;
245 }
246 continue;
247 }
248 if ((arg == "-to" || arg == "--timeout") && i + 1 < argc) {
249 try {
250 int timeout_sec = std::stoi(argv[++i]);
251 if (timeout_sec < 1) {
252 std::cerr << "Error: Timeout must be positive\n";
253 return false;
254 }
255 opts.timeout = std::chrono::milliseconds{timeout_sec * 1000};
256 } catch (const std::exception&) {
257 std::cerr << "Error: Invalid timeout value\n";
258 return false;
259 }
260 continue;
261 }
262
263 // Query key option
264 if ((arg == "-k" || arg == "--key") && i + 1 < argc) {
265 auto key = parse_query_key(argv[++i]);
266 if (!key) {
267 std::cerr << "Error: Invalid query key format. Use 'GGGG,EEEE=value'\n";
268 return false;
269 }
270 opts.custom_keys.push_back(*key);
271 continue;
272 }
273
274 // Common filter options
275 if (arg == "--modality" && i + 1 < argc) {
276 opts.modality = argv[++i];
277 continue;
278 }
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();
283 } else {
284 opts.scheduled_date = date_arg;
285 }
286 continue;
287 }
288 if (arg == "--time" && i + 1 < argc) {
289 opts.scheduled_time = argv[++i];
290 continue;
291 }
292 if (arg == "--station" && i + 1 < argc) {
293 opts.station_ae = argv[++i];
294 continue;
295 }
296 if (arg == "--station-name" && i + 1 < argc) {
297 opts.station_name = argv[++i];
298 continue;
299 }
300 if (arg == "--physician" && i + 1 < argc) {
301 opts.physician = argv[++i];
302 continue;
303 }
304 if (arg == "--patient-name" && i + 1 < argc) {
305 opts.patient_name = argv[++i];
306 continue;
307 }
308 if (arg == "--patient-id" && i + 1 < argc) {
309 opts.patient_id = argv[++i];
310 continue;
311 }
312 if (arg == "--accession" && i + 1 < argc) {
313 opts.accession_number = argv[++i];
314 continue;
315 }
316
317 // Output options
318 if ((arg == "-o" || arg == "--output" || arg == "--format") && i + 1 < argc) {
319 opts.format = worklist_cli::parse_output_format(argv[++i]);
320 continue;
321 }
322 if (arg == "--output-file" && i + 1 < argc) {
323 opts.output_file = argv[++i];
324 continue;
325 }
326 if (arg == "--max-results" && i + 1 < argc) {
327 try {
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";
331 return false;
332 }
333 continue;
334 }
335
336 // Legacy option for backward compatibility
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";
341 return false;
342 }
343 continue;
344 }
345
346 // Check if it looks like an unknown option
347 if (arg[0] == '-') {
348 std::cerr << "Error: Unknown option '" << arg << "'\n";
349 return false;
350 }
351
352 // Positional argument
353 positional_args.push_back(arg);
354 }
355
356 // Need exactly 2 positional arguments: host and port
357 if (positional_args.size() < 2) {
358 std::cerr << "Error: Missing required arguments <peer> <port>\n";
359 return false;
360 }
361 if (positional_args.size() > 2) {
362 std::cerr << "Error: Too many positional arguments\n";
363 return false;
364 }
365
366 opts.host = positional_args[0];
367
368 // Parse port
369 try {
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";
373 return false;
374 }
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";
378 return false;
379 }
380
381 return true;
382}
383
387int perform_query(const options& opts) {
388 using namespace kcenon::pacs::network;
389 using namespace kcenon::pacs::services;
390
391 if (opts.verbose) {
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";
398 }
399 if (!opts.scheduled_date.empty()) {
400 std::cout << " Sched Date: " << opts.scheduled_date << "\n";
401 }
402 if (!opts.station_ae.empty()) {
403 std::cout << " Station AE: " << opts.station_ae << "\n";
404 }
405 std::cout << "\n";
406 }
407
408 // Configure association
409 association_config config;
410 config.calling_ae_title = opts.calling_ae;
411 config.called_ae_title = opts.called_ae;
412 config.implementation_class_uid = "1.2.826.0.1.3680043.2.1545.1";
413 config.implementation_version_name = "WORKLIST_SCU_001";
414
415 // Propose Modality Worklist SOP Class
416 config.proposed_contexts.push_back({
417 1, // Context ID
418 std::string(worklist_find_sop_class_uid),
419 {
420 "1.2.840.10008.1.2.1", // Explicit VR Little Endian
421 "1.2.840.10008.1.2" // Implicit VR Little Endian
422 }
423 });
424
425 // Establish association
426 auto start_time = std::chrono::steady_clock::now();
427 auto connect_result = association::connect(opts.host, opts.port, config, opts.timeout);
428
429 if (connect_result.is_err()) {
430 std::cerr << "Failed to establish association: "
431 << connect_result.error().message << "\n";
432 return 2;
433 }
434
435 auto& assoc = connect_result.value();
436 auto connect_time = std::chrono::steady_clock::now();
437
438 if (opts.verbose) {
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";
442 }
443
444 // Check if context was accepted
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";
447 assoc.abort();
448 return 2;
449 }
450
451 // Build query keys using library structure
453 keys.patient_name = opts.patient_name;
454 keys.patient_id = opts.patient_id;
455 keys.modality = opts.modality;
456 keys.scheduled_date = opts.scheduled_date;
457 keys.scheduled_time = opts.scheduled_time;
458 keys.scheduled_station_ae = opts.station_ae;
459 keys.scheduled_physician = opts.physician;
460 keys.accession_number = opts.accession_number;
461
462 // Configure worklist_scu
463 worklist_scu_config scu_config;
464 scu_config.timeout = opts.timeout;
465 scu_config.max_results = opts.max_results;
466
467 // Create worklist_scu instance
469
470 if (opts.verbose) {
471 std::cout << "Sending C-FIND request...\n";
472 }
473
474 // Perform query using library
476
477 if (opts.custom_keys.empty()) {
478 // Use typed query keys
479 auto result = scu.query(assoc, keys);
480 if (result.is_err()) {
481 std::cerr << "Query failed: " << result.error().message << "\n";
482 assoc.abort();
483 return 2;
484 }
485 query_result = std::move(result.value());
486 } else {
487 // Build raw dataset for custom keys
488 auto query_ds = scu.query(assoc, keys);
489 if (query_ds.is_err()) {
490 std::cerr << "Query failed: " << query_ds.error().message << "\n";
491 assoc.abort();
492 return 2;
493 }
494 query_result = std::move(query_ds.value());
495
496 // Note: Custom keys are applied by building a raw dataset
497 // For now, we use typed keys and note that custom keys require
498 // building a custom dataset directly
499 if (opts.debug) {
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";
505 }
506 }
507 }
508
509 if (opts.verbose) {
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";
514 } else {
515 std::cerr << "Query completed with status: 0x"
516 << std::hex << query_result.status << std::dec << "\n";
517 }
518 }
519
520 // Release association gracefully
521 if (opts.verbose) {
522 std::cout << "Releasing association...\n";
523 }
524
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";
528 }
529
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);
533
534 // Convert worklist_items to dicom_datasets for formatter
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);
539 }
540
541 // Format results
542 worklist_cli::worklist_result_formatter formatter(opts.format);
543 std::string formatted_output = formatter.format(result_datasets);
544
545 // Output to file or stdout
546 if (!opts.output_file.empty()) {
547 std::ofstream outfile(opts.output_file);
548 if (!outfile) {
549 std::cerr << "Error: Cannot open output file '" << opts.output_file << "'\n";
550 return 2;
551 }
552 outfile << formatted_output;
553 outfile.close();
554 if (opts.verbose) {
555 std::cout << "Results written to: " << opts.output_file << "\n";
556 }
557 } else {
558 std::cout << formatted_output;
559 }
560
561 // Print summary for table format
562 if (opts.format == worklist_cli::output_format::table && opts.verbose) {
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 << ")";
569 }
570 std::cout << "\n";
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";
575 }
576
577 return query_result.items.empty() ? 1 : 0;
578}
579
580} // namespace
581
582int main(int argc, char* argv[]) {
583 // Only show banner for table/text format (to not corrupt json/csv/xml output)
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") {
590 show_banner = false;
591 }
592 break;
593 }
594 }
595
596 if (show_banner) {
597 std::cout << R"(
598 __ __ _ _ _ _ ____ ____ _ _
599 \ \ / /__ _ __| | _| (_)___| |_ / ___| / ___| | | |
600 \ \ /\ / / _ \| '__| |/ / | / __| __| \___ \| | | | | |
601 \ V V / (_) | | | <| | \__ \ |_ ___) | |___| |_| |
602 \_/\_/ \___/|_| |_|\_\_|_|___/\__| |____/ \____|\___/
603
604 Modality Worklist Query Client
605)" << "\n";
606 }
607
608 options opts;
609
610 if (!parse_arguments(argc, argv, opts)) {
611 print_usage(argv[0]);
612 return 2;
613 }
614
615 return perform_query(opts);
616}
DICOM Association management per PS3.8.
Result formatter for worklist query results.
Compile-time constants for commonly used DICOM tags.
int main()
Definition main.cpp:84
@ station_ae
(0008,1010) Station Name or calling AE
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag accession_number
Accession Number.
constexpr dicom_tag modality
Modality.
constexpr dicom_tag station_name
Station Name.
constexpr dicom_tag item
Item.
constexpr dicom_tag patient_name
Patient's Name.
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::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)
Worklist Result Formatting Utilities.
DICOM Modality Worklist SCP service (MWL C-FIND handler)
DICOM Modality Worklist SCU service (MWL C-FIND sender)