PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1
21#include "query_builder.h"
22#include "result_formatter.h"
23
27
28#include <chrono>
29#include <cstdlib>
30#include <iostream>
31#include <string>
32#include <vector>
33
34namespace {
35
37constexpr const char* default_calling_ae = "QUERY_SCU";
38
40constexpr auto default_timeout = std::chrono::milliseconds{30000};
41
45struct options {
46 // Connection
47 std::string host;
48 uint16_t port{0};
49 std::string called_ae;
50 std::string calling_ae{default_calling_ae};
51
52 // Query parameters
54 std::string query_model{"study"}; // "patient" or "study" root
55
56 // Search criteria
57 std::string patient_name;
58 std::string patient_id;
59 std::string patient_birth_date;
60 std::string patient_sex;
61 std::string study_date;
62 std::string study_time;
63 std::string accession_number;
64 std::string study_uid;
65 std::string study_id;
66 std::string study_description;
67 std::string modality;
68 std::string series_uid;
69 std::string sop_instance_uid;
70
71 // Output options
73 bool verbose{false};
74 size_t max_results{0}; // 0 = unlimited
75};
76
80void print_usage(const char* program_name) {
81 std::cout << R"(
82Query SCU - DICOM C-FIND Client
83
84Usage: )" << program_name << R"( <host> <port> <called_ae> [options]
85
86Arguments:
87 host Remote host address (IP or hostname)
88 port Remote port number (typically 104 or 11112)
89 called_ae Called AE Title (remote SCP's AE title)
90
91Query Options:
92 --level <level> Query level: PATIENT, STUDY, SERIES, IMAGE (default: STUDY)
93 --model <model> Query model: patient, study (default: study)
94
95Search Criteria:
96 --patient-name <name> Patient name (wildcards: * ?)
97 --patient-id <id> Patient ID
98 --patient-birth-date <date> Patient birth date (YYYYMMDD)
99 --patient-sex <sex> Patient sex (M, F, O)
100 --study-date <date> Study date (YYYYMMDD or range YYYYMMDD-YYYYMMDD)
101 --study-time <time> Study time (HHMMSS or range)
102 --accession-number <num> Accession number
103 --study-uid <uid> Study Instance UID
104 --study-id <id> Study ID
105 --study-description <desc> Study description
106 --modality <mod> Modality (CT, MR, US, XR, etc.)
107 --series-uid <uid> Series Instance UID
108 --sop-instance-uid <uid> SOP Instance UID
109
110Output Options:
111 --format <fmt> Output format: table, json, csv (default: table)
112 --max-results <n> Maximum results to display (default: unlimited)
113 --calling-ae <ae> Calling AE Title (default: QUERY_SCU)
114 --verbose, -v Show detailed progress
115 --help, -h Show this help message
116
117Examples:
118 )" << program_name << R"( localhost 11112 PACS_SCP --level PATIENT --patient-name "Smith*"
119 )" << program_name << R"( localhost 11112 PACS_SCP --level STUDY --patient-id "12345" --study-date "20240101-20241231"
120 )" << program_name << R"( localhost 11112 PACS_SCP --level SERIES --study-uid "1.2.3.4.5" --format json
121 )" << program_name << R"( localhost 11112 PACS_SCP --modality CT --format csv > results.csv
122
123Exit Codes:
124 0 Success - Query completed
125 1 Error - Query failed or no results
126 2 Error - Invalid arguments or connection failure
127)";
128}
129
133std::optional<kcenon::pacs::services::query_level> parse_level(std::string_view level_str) {
134 if (level_str == "PATIENT" || level_str == "patient") {
136 }
137 if (level_str == "STUDY" || level_str == "study") {
139 }
140 if (level_str == "SERIES" || level_str == "series") {
142 }
143 if (level_str == "IMAGE" || level_str == "image" ||
144 level_str == "INSTANCE" || level_str == "instance") {
146 }
147 return std::nullopt;
148}
149
153bool parse_arguments(int argc, char* argv[], options& opts) {
154 if (argc < 4) {
155 return false;
156 }
157
158 opts.host = argv[1];
159
160 // Parse port
161 try {
162 int port_int = std::stoi(argv[2]);
163 if (port_int < 1 || port_int > 65535) {
164 std::cerr << "Error: Port must be between 1 and 65535\n";
165 return false;
166 }
167 opts.port = static_cast<uint16_t>(port_int);
168 } catch (const std::exception&) {
169 std::cerr << "Error: Invalid port number '" << argv[2] << "'\n";
170 return false;
171 }
172
173 opts.called_ae = argv[3];
174 if (opts.called_ae.length() > 16) {
175 std::cerr << "Error: Called AE title exceeds 16 characters\n";
176 return false;
177 }
178
179 // Parse optional arguments
180 for (int i = 4; i < argc; ++i) {
181 std::string arg = argv[i];
182
183 if ((arg == "--help" || arg == "-h")) {
184 return false;
185 }
186 if (arg == "--verbose" || arg == "-v") {
187 opts.verbose = true;
188 } else if (arg == "--level" && i + 1 < argc) {
189 auto level = parse_level(argv[++i]);
190 if (!level) {
191 std::cerr << "Error: Invalid query level '" << argv[i] << "'\n";
192 return false;
193 }
194 opts.level = *level;
195 } else if (arg == "--model" && i + 1 < argc) {
196 opts.query_model = argv[++i];
197 if (opts.query_model != "patient" && opts.query_model != "study") {
198 std::cerr << "Error: Invalid query model (use 'patient' or 'study')\n";
199 return false;
200 }
201 } else if (arg == "--patient-name" && i + 1 < argc) {
202 opts.patient_name = argv[++i];
203 } else if (arg == "--patient-id" && i + 1 < argc) {
204 opts.patient_id = argv[++i];
205 } else if (arg == "--patient-birth-date" && i + 1 < argc) {
206 opts.patient_birth_date = argv[++i];
207 } else if (arg == "--patient-sex" && i + 1 < argc) {
208 opts.patient_sex = argv[++i];
209 } else if (arg == "--study-date" && i + 1 < argc) {
210 opts.study_date = argv[++i];
211 } else if (arg == "--study-time" && i + 1 < argc) {
212 opts.study_time = argv[++i];
213 } else if (arg == "--accession-number" && i + 1 < argc) {
214 opts.accession_number = argv[++i];
215 } else if (arg == "--study-uid" && i + 1 < argc) {
216 opts.study_uid = argv[++i];
217 } else if (arg == "--study-id" && i + 1 < argc) {
218 opts.study_id = argv[++i];
219 } else if (arg == "--study-description" && i + 1 < argc) {
220 opts.study_description = argv[++i];
221 } else if (arg == "--modality" && i + 1 < argc) {
222 opts.modality = argv[++i];
223 } else if (arg == "--series-uid" && i + 1 < argc) {
224 opts.series_uid = argv[++i];
225 } else if (arg == "--sop-instance-uid" && i + 1 < argc) {
226 opts.sop_instance_uid = argv[++i];
227 } else if (arg == "--format" && i + 1 < argc) {
228 opts.format = query_scu::parse_output_format(argv[++i]);
229 } else if (arg == "--max-results" && i + 1 < argc) {
230 try {
231 opts.max_results = static_cast<size_t>(std::stoul(argv[++i]));
232 } catch (const std::exception&) {
233 std::cerr << "Error: Invalid max-results value\n";
234 return false;
235 }
236 } else if (arg == "--calling-ae" && i + 1 < argc) {
237 opts.calling_ae = argv[++i];
238 if (opts.calling_ae.length() > 16) {
239 std::cerr << "Error: Calling AE title exceeds 16 characters\n";
240 return false;
241 }
242 } else {
243 std::cerr << "Error: Unknown option '" << arg << "'\n";
244 return false;
245 }
246 }
247
248 return true;
249}
250
254std::string_view get_find_sop_class_uid(const std::string& model) {
255 if (model == "patient") {
257 }
259}
260
264int perform_query(const options& opts) {
265 using namespace kcenon::pacs::network;
266 using namespace kcenon::pacs::network::dimse;
267 using namespace kcenon::pacs::services;
268
269 auto sop_class_uid = get_find_sop_class_uid(opts.query_model);
270
271 if (opts.verbose) {
272 std::cout << "Connecting to " << opts.host << ":" << opts.port << "...\n";
273 std::cout << " Calling AE: " << opts.calling_ae << "\n";
274 std::cout << " Called AE: " << opts.called_ae << "\n";
275 std::cout << " Query Model: " << opts.query_model << " root\n";
276 std::cout << " Query Level: " << to_string(opts.level) << "\n\n";
277 }
278
279 // Configure association
280 association_config config;
281 config.calling_ae_title = opts.calling_ae;
282 config.called_ae_title = opts.called_ae;
283 config.implementation_class_uid = "1.2.826.0.1.3680043.2.1545.1";
284 config.implementation_version_name = "QUERY_SCU_001";
285
286 // Propose Query/Retrieve SOP Class
287 config.proposed_contexts.push_back({
288 1, // Context ID
289 std::string(sop_class_uid),
290 {
291 "1.2.840.10008.1.2.1", // Explicit VR Little Endian
292 "1.2.840.10008.1.2" // Implicit VR Little Endian
293 }
294 });
295
296 // Establish association
297 auto start_time = std::chrono::steady_clock::now();
298 auto connect_result = association::connect(opts.host, opts.port, config, default_timeout);
299
300 if (connect_result.is_err()) {
301 std::cerr << "Failed to establish association: "
302 << connect_result.error().message << "\n";
303 return 2;
304 }
305
306 auto& assoc = connect_result.value();
307 auto connect_time = std::chrono::steady_clock::now();
308
309 if (opts.verbose) {
310 auto connect_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
311 connect_time - start_time);
312 std::cout << "Association established in " << connect_duration.count() << " ms\n";
313 }
314
315 // Check if context was accepted
316 if (!assoc.has_accepted_context(sop_class_uid)) {
317 std::cerr << "Error: Query SOP Class not accepted by remote SCP\n";
318 assoc.abort();
319 return 2;
320 }
321
322 auto context_id_opt = assoc.accepted_context_id(sop_class_uid);
323 if (!context_id_opt) {
324 std::cerr << "Error: Could not get presentation context ID\n";
325 assoc.abort();
326 return 2;
327 }
328 uint8_t context_id = *context_id_opt;
329
330 // Build query dataset
331 auto query_ds = query_scu::query_builder()
332 .level(opts.level)
333 .patient_name(opts.patient_name)
334 .patient_id(opts.patient_id)
335 .patient_birth_date(opts.patient_birth_date)
336 .patient_sex(opts.patient_sex)
337 .study_date(opts.study_date)
338 .study_time(opts.study_time)
339 .accession_number(opts.accession_number)
340 .study_instance_uid(opts.study_uid)
341 .study_id(opts.study_id)
342 .study_description(opts.study_description)
343 .modality(opts.modality)
344 .series_instance_uid(opts.series_uid)
345 .sop_instance_uid(opts.sop_instance_uid)
346 .build();
347
348 // Create C-FIND request
349 auto find_rq = make_c_find_rq(1, sop_class_uid);
350 find_rq.set_dataset(std::move(query_ds));
351
352 if (opts.verbose) {
353 std::cout << "Sending C-FIND request...\n";
354 }
355
356 // Send C-FIND request
357 auto send_result = assoc.send_dimse(context_id, find_rq);
358 if (send_result.is_err()) {
359 std::cerr << "Failed to send C-FIND: " << send_result.error().message << "\n";
360 assoc.abort();
361 return 2;
362 }
363
364 // Receive responses
365 std::vector<kcenon::pacs::core::dicom_dataset> results;
366 bool query_complete = false;
367 size_t pending_count = 0;
368
369 while (!query_complete) {
370 auto recv_result = assoc.receive_dimse(default_timeout);
371 if (recv_result.is_err()) {
372 std::cerr << "Failed to receive C-FIND response: "
373 << recv_result.error().message << "\n";
374 assoc.abort();
375 return 2;
376 }
377
378 auto& [recv_context_id, find_rsp] = recv_result.value();
379
380 if (find_rsp.command() != command_field::c_find_rsp) {
381 std::cerr << "Error: Unexpected response (expected C-FIND-RSP)\n";
382 assoc.abort();
383 return 2;
384 }
385
386 auto status = find_rsp.status();
387
388 // Check for pending status (more results coming)
389 if (status == status_pending || status == status_pending_warning) {
390 ++pending_count;
391
392 if (find_rsp.has_dataset()) {
393 // Check max results limit
394 if (opts.max_results == 0 || results.size() < opts.max_results) {
395 auto dataset_result = find_rsp.dataset();
396 if (dataset_result.is_ok()) {
397 results.push_back(dataset_result.value().get());
398 }
399 }
400 }
401
402 if (opts.verbose && pending_count % 10 == 0) {
403 std::cout << "\rReceived " << pending_count << " results..." << std::flush;
404 }
405 } else if (status == status_success) {
406 // Query complete
407 query_complete = true;
408 if (opts.verbose) {
409 std::cout << "\rQuery completed successfully.\n";
410 }
411 } else if (status == status_cancel) {
412 query_complete = true;
413 std::cerr << "Query was cancelled.\n";
414 } else {
415 // Error status
416 query_complete = true;
417 std::cerr << "Query failed with status: 0x"
418 << std::hex << status << std::dec << "\n";
419 }
420 }
421
422 // Release association gracefully
423 if (opts.verbose) {
424 std::cout << "Releasing association...\n";
425 }
426
427 auto release_result = assoc.release(default_timeout);
428 if (release_result.is_err() && opts.verbose) {
429 std::cerr << "Warning: Release failed: " << release_result.error().message << "\n";
430 }
431
432 auto end_time = std::chrono::steady_clock::now();
433 auto total_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
434 end_time - start_time);
435
436 // Format and display results
437 query_scu::result_formatter formatter(opts.format, opts.level);
438 std::cout << formatter.format(results);
439
440 // Print summary for table format
441 if (opts.format == query_scu::output_format::table && opts.verbose) {
442 std::cout << "\n========================================\n";
443 std::cout << " Summary\n";
444 std::cout << "========================================\n";
445 std::cout << " Query level: " << to_string(opts.level) << "\n";
446 std::cout << " Total results: " << results.size();
447 if (opts.max_results > 0 && pending_count > opts.max_results) {
448 std::cout << " (limited from " << pending_count << ")";
449 }
450 std::cout << "\n";
451 std::cout << " Query time: " << total_duration.count() << " ms\n";
452 std::cout << "========================================\n";
453 }
454
455 return results.empty() ? 1 : 0;
456}
457
458} // namespace
459
460int main(int argc, char* argv[]) {
461 // Only show banner for table format (to not corrupt json/csv output)
462 bool show_banner = true;
463 for (int i = 1; i < argc; ++i) {
464 if (std::string(argv[i]) == "--format" && i + 1 < argc) {
465 std::string fmt = argv[i + 1];
466 if (fmt == "json" || fmt == "csv") {
467 show_banner = false;
468 }
469 break;
470 }
471 }
472
473 if (show_banner) {
474 std::cout << R"(
475 ___ _ _ _____ ______ __ ____ ____ _ _
476 / _ \| | | | ____| _ \ \ / / / ___| / ___| | | |
477 | | | | | | | _| | |_) \ V / \___ \| | | | | |
478 | |_| | |_| | |___| _ < | | ___) | |___| |_| |
479 \__\_\\___/|_____|_| \_\|_| |____/ \____|\___/
480
481 DICOM C-FIND Client
482)" << "\n";
483 }
484
485 options opts;
486
487 if (!parse_arguments(argc, argv, opts)) {
488 print_usage(argv[0]);
489 return 2;
490 }
491
492 return perform_query(opts);
493}
DICOM Association management per PS3.8.
query_builder & modality(std::string_view mod)
Set modality criteria.
query_builder & patient_birth_date(std::string_view date)
Set patient birth date criteria.
query_builder & study_date(std::string_view date)
Set study date criteria (supports ranges)
query_builder & study_instance_uid(std::string_view uid)
Set study instance UID criteria.
query_builder & study_id(std::string_view id)
Set study ID criteria.
query_builder & sop_instance_uid(std::string_view uid)
Set SOP instance UID criteria.
query_builder & patient_sex(std::string_view sex)
Set patient sex criteria.
query_builder & patient_name(std::string_view name)
Set patient name search criteria (supports wildcards)
query_builder & series_instance_uid(std::string_view uid)
Set series instance UID criteria.
query_builder & accession_number(std::string_view accession)
Set accession number criteria.
query_builder & patient_id(std::string_view id)
Set patient ID search criteria.
kcenon::pacs::core::dicom_dataset build() const
Build the query dataset.
query_builder & study_description(std::string_view desc)
Set study description criteria.
query_builder & study_time(std::string_view time)
Set study time criteria.
query_builder & level(query_level lvl)
Set the query/retrieve level.
Result formatter for query results.
DIMSE message encoding and decoding.
int main()
Definition main.cpp:84
constexpr dicom_tag study_description
Study Description.
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag sop_instance_uid
SOP Instance UID.
constexpr dicom_tag status
Status.
constexpr dicom_tag patient_birth_date
Patient's Birth Date.
constexpr dicom_tag accession_number
Accession Number.
constexpr dicom_tag modality
Modality.
constexpr dicom_tag study_time
Study Time.
constexpr dicom_tag patient_sex
Patient's Sex.
constexpr dicom_tag sop_class_uid
SOP Class UID.
constexpr dicom_tag study_id
Study ID.
constexpr dicom_tag patient_name
Patient's Name.
constexpr dicom_tag study_date
Study Date.
std::chrono::milliseconds default_timeout()
Default timeout for test operations (5s normal, 30s CI)
constexpr std::string_view study_root_find_sop_class_uid
Study Root Query/Retrieve Information Model - FIND.
Definition query_scp.h:42
constexpr std::string_view get_find_sop_class_uid(query_model model) noexcept
Get the FIND SOP Class UID for a query model.
Definition query_scu.h:70
constexpr std::string_view patient_root_find_sop_class_uid
Patient Root Query/Retrieve Information Model - FIND.
Definition query_scp.h:38
query_level
DICOM Query/Retrieve level enumeration.
Definition query_scp.h:63
@ study
Study level - query study information.
@ image
Image (Instance) level - query instance information.
@ patient
Patient level - query patient demographics.
@ series
Series level - query series information.
output_format
Output format enumeration.
@ table
Human-readable table format.
output_format parse_output_format(std::string_view format_str)
Parse output format from string.
DICOM Query Dataset Builder.
DICOM Query SCP service (C-FIND handler)
Query Result Formatting Utilities.
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