37constexpr const char* default_calling_ae =
"QUERY_SCU";
49 std::string called_ae;
50 std::string calling_ae{default_calling_ae};
54 std::string query_model{
"study"};
64 std::string study_uid;
68 std::string series_uid;
74 size_t max_results{0};
80void print_usage(
const char* program_name) {
82Query SCU - DICOM C-FIND Client
84Usage: )" << program_name << R"( <host> <port> <called_ae> [options]
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)
92 --level <level> Query level: PATIENT, STUDY, SERIES, IMAGE (default: STUDY)
93 --model <model> Query model: patient, study (default: study)
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
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
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
124 0 Success - Query completed
125 1 Error - Query failed or no results
126 2 Error - Invalid arguments or connection failure
133std::optional<kcenon::pacs::services::query_level> parse_level(std::string_view level_str) {
134 if (level_str ==
"PATIENT" || level_str ==
"patient") {
137 if (level_str ==
"STUDY" || level_str ==
"study") {
140 if (level_str ==
"SERIES" || level_str ==
"series") {
143 if (level_str ==
"IMAGE" || level_str ==
"image" ||
144 level_str ==
"INSTANCE" || level_str ==
"instance") {
153bool parse_arguments(
int argc,
char* argv[], options& opts) {
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";
167 opts.port =
static_cast<uint16_t
>(port_int);
168 }
catch (
const std::exception&) {
169 std::cerr <<
"Error: Invalid port number '" << argv[2] <<
"'\n";
173 opts.called_ae = argv[3];
174 if (opts.called_ae.length() > 16) {
175 std::cerr <<
"Error: Called AE title exceeds 16 characters\n";
180 for (
int i = 4; i < argc; ++i) {
181 std::string arg = argv[i];
183 if ((arg ==
"--help" || arg ==
"-h")) {
186 if (arg ==
"--verbose" || arg ==
"-v") {
188 }
else if (arg ==
"--level" && i + 1 < argc) {
189 auto level = parse_level(argv[++i]);
191 std::cerr <<
"Error: Invalid query level '" << argv[i] <<
"'\n";
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";
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) {
229 }
else if (arg ==
"--max-results" && i + 1 < argc) {
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";
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";
243 std::cerr <<
"Error: Unknown option '" << arg <<
"'\n";
255 if (model ==
"patient") {
264int perform_query(
const options& opts) {
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";
289 std::string(sop_class_uid),
291 "1.2.840.10008.1.2.1",
297 auto start_time = std::chrono::steady_clock::now();
298 auto connect_result = association::connect(opts.host, opts.port, config, default_timeout);
300 if (connect_result.is_err()) {
301 std::cerr <<
"Failed to establish association: "
302 << connect_result.error().message <<
"\n";
306 auto& assoc = connect_result.value();
307 auto connect_time = std::chrono::steady_clock::now();
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";
316 if (!assoc.has_accepted_context(sop_class_uid)) {
317 std::cerr <<
"Error: Query SOP Class not accepted by remote SCP\n";
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";
328 uint8_t context_id = *context_id_opt;
349 auto find_rq = make_c_find_rq(1, sop_class_uid);
350 find_rq.set_dataset(std::move(query_ds));
353 std::cout <<
"Sending C-FIND request...\n";
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";
365 std::vector<kcenon::pacs::core::dicom_dataset> results;
366 bool query_complete =
false;
367 size_t pending_count = 0;
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";
378 auto& [recv_context_id, find_rsp] = recv_result.value();
380 if (find_rsp.command() != command_field::c_find_rsp) {
381 std::cerr <<
"Error: Unexpected response (expected C-FIND-RSP)\n";
386 auto status = find_rsp.status();
389 if (status == status_pending || status == status_pending_warning) {
392 if (find_rsp.has_dataset()) {
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());
402 if (opts.verbose && pending_count % 10 == 0) {
403 std::cout <<
"\rReceived " << pending_count <<
" results..." << std::flush;
405 }
else if (status == status_success) {
407 query_complete =
true;
409 std::cout <<
"\rQuery completed successfully.\n";
411 }
else if (status == status_cancel) {
412 query_complete =
true;
413 std::cerr <<
"Query was cancelled.\n";
416 query_complete =
true;
417 std::cerr <<
"Query failed with status: 0x"
418 << std::hex <<
status << std::dec <<
"\n";
424 std::cout <<
"Releasing association...\n";
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";
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);
438 std::cout << formatter.format(results);
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 <<
")";
451 std::cout <<
" Query time: " << total_duration.count() <<
" ms\n";
452 std::cout <<
"========================================\n";
455 return results.empty() ? 1 : 0;
460int main(
int argc,
char* argv[]) {
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") {
475 ___ _ _ _____ ______ __ ____ ____ _ _
476 / _ \| | | | ____| _ \ \ / / / ___| / ___| | | |
477 | | | | | | | _| | |_) \ V / \___ \| | | | | |
478 | |_| | |_| | |___| _ < | | ___) | |___| |_| |
479 \__\_\\___/|_____|_| \_\|_| |____/ \____|\___/
487 if (!parse_arguments(argc, argv, opts)) {
488 print_usage(argv[0]);
492 return perform_query(opts);
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.
DIMSE message encoding and decoding.
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.
constexpr std::string_view get_find_sop_class_uid(query_model model) noexcept
Get the FIND SOP Class UID for a query model.
constexpr std::string_view patient_root_find_sop_class_uid
Patient Root Query/Retrieve Information Model - FIND.
query_level
DICOM Query/Retrieve level enumeration.
@ 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)
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