A command-line utility for querying scheduled procedures from a Modality Worklist SCP. Supports filtering by modality, date, station, and multiple output formats (table, JSON, CSV, XML).
This example demonstrates how to use the worklist_scu library class for Modality Worklist queries.
#include <chrono>
#include <cstdlib>
#include <ctime>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <optional>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
namespace {
constexpr const char* default_calling_ae = "WORKLIST_SCU";
constexpr auto default_timeout = std::chrono::milliseconds{30000};
struct query_key {
uint16_t group;
uint16_t element;
std::string value;
};
struct options {
std::string host;
uint16_t port{0};
std::string called_ae{"ANY-SCP"};
std::string calling_ae{default_calling_ae};
std::chrono::milliseconds timeout{default_timeout};
std::string modality;
std::string scheduled_date;
std::string scheduled_time;
std::string station_ae;
std::string station_name;
std::string physician;
std::string patient_name;
std::string patient_id;
std::string accession_number;
std::vector<query_key> custom_keys;
std::string output_file;
bool verbose{false};
bool debug{false};
size_t max_results{0};
};
std::string get_today_date() {
auto now = std::time(nullptr);
auto* tm = std::localtime(&now);
std::ostringstream oss;
oss << std::put_time(tm, "%Y%m%d");
return oss.str();
}
std::optional<query_key> parse_query_key(const std::string& key_str) {
auto eq_pos = key_str.find('=');
if (eq_pos == std::string::npos) {
return std::nullopt;
}
std::string tag_part = key_str.substr(0, eq_pos);
std::string value_part = key_str.substr(eq_pos + 1);
auto comma_pos = tag_part.find(',');
if (comma_pos == std::string::npos) {
return std::nullopt;
}
try {
uint16_t group = static_cast<uint16_t>(
std::stoul(tag_part.substr(0, comma_pos), nullptr, 16));
uint16_t element = static_cast<uint16_t>(
std::stoul(tag_part.substr(comma_pos + 1), nullptr, 16));
return query_key{group, element, value_part};
} catch (const std::exception&) {
return std::nullopt;
}
}
void print_usage(const char* program_name) {
std::cout << R"(
Worklist SCU - Modality Worklist Query Client
Usage: )" << program_name << R"( [options] <peer> <port>
Arguments:
peer Remote host address (IP or hostname)
port Remote port number (typically 104 or 11112)
Network Options:
-aet, --aetitle <ae> Calling AE Title (default: WLSCU)
-aec, --call <ae> Called AE Title (default: ANY-SCP)
-to, --timeout <sec> Connection timeout in seconds (default: 30)
Query Keys:
-k, --key <tag=value> Query key (e.g., "0008,0060=CT")
Can be specified multiple times
Common Filters (convenience options):
--modality <mod> Filter by modality (CT, MR, US, XR, NM, etc.)
--date <date> Scheduled date (YYYYMMDD or range YYYYMMDD-YYYYMMDD)
Use "today" for current date
--time <time> Scheduled time (HHMMSS or range)
--station <name> Scheduled Station AE Title
--station-name <name> Scheduled Station Name
--physician <name> Scheduled Performing Physician Name
--patient-name <name> Patient name (wildcards: * ?)
--patient-id <id> Patient ID
--accession <num> Accession number
Output Options:
-o, --output <format> Output format: text, json, csv, xml (default: text)
--output-file <file> Write results to file
--max-results <n> Maximum results to display (default: unlimited)
General Options:
-v, --verbose Verbose output mode
-d, --debug Debug output mode
-h, --help Show this help message
Examples:
)" << program_name << R"( 192.168.1.100 11112 --modality CT
)" << program_name << R"( 192.168.1.100 11112 --modality MR --date today
)" << program_name << R"( -aec RIS_SCP --date 20241215 --station CT_SCANNER_01 192.168.1.100 11112
)" << program_name << R"( -k "0008,0060=CT" -k "0040,0002=20241215" 192.168.1.100 11112
)" << program_name << R"( --modality CT -o json --output-file worklist.json 192.168.1.100 11112
Exit Codes:
0 Success - Query completed with results
1 Success - Query completed with no results
2 Error - Invalid arguments or connection failure
)";
}
bool parse_arguments(int argc, char* argv[], options& opts) {
if (argc < 3) {
return false;
}
std::vector<std::string> positional_args;
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "--help" || arg == "-h") {
return false;
}
if (arg == "--verbose" || arg == "-v") {
continue;
}
if (arg == "--debug" || arg == "-d") {
opts.debug = true;
opts.verbose = true;
continue;
}
if ((arg == "-aet" || arg == "--aetitle") && i + 1 < argc) {
opts.calling_ae = argv[++i];
if (opts.calling_ae.length() > 16) {
std::cerr << "Error: Calling AE title exceeds 16 characters\n";
return false;
}
continue;
}
if ((arg == "-aec" || arg == "--call") && i + 1 < argc) {
opts.called_ae = argv[++i];
if (opts.called_ae.length() > 16) {
std::cerr << "Error: Called AE title exceeds 16 characters\n";
return false;
}
continue;
}
if ((arg == "-to" || arg == "--timeout") && i + 1 < argc) {
try {
int timeout_sec = std::stoi(argv[++i]);
if (timeout_sec < 1) {
std::cerr << "Error: Timeout must be positive\n";
return false;
}
opts.timeout = std::chrono::milliseconds{timeout_sec * 1000};
} catch (const std::exception&) {
std::cerr << "Error: Invalid timeout value\n";
return false;
}
continue;
}
if ((arg == "-k" || arg == "--key") && i + 1 < argc) {
auto key = parse_query_key(argv[++i]);
if (!key) {
std::cerr << "Error: Invalid query key format. Use 'GGGG,EEEE=value'\n";
return false;
}
opts.custom_keys.push_back(*key);
continue;
}
if (arg == "--modality" && i + 1 < argc) {
opts.
modality = argv[++i];
continue;
}
if (arg == "--date" && i + 1 < argc) {
std::string date_arg = argv[++i];
if (date_arg == "today") {
opts.scheduled_date = get_today_date();
} else {
opts.scheduled_date = date_arg;
}
continue;
}
if (arg == "--time" && i + 1 < argc) {
opts.scheduled_time = argv[++i];
continue;
}
if (arg == "--station" && i + 1 < argc) {
opts.station_ae = argv[++i];
continue;
}
if (arg == "--station-name" && i + 1 < argc) {
opts.station_name = argv[++i];
continue;
}
if (arg == "--physician" && i + 1 < argc) {
opts.physician = argv[++i];
continue;
}
if (arg == "--patient-name" && i + 1 < argc) {
opts.
patient_name = argv[++i];
continue;
}
if (arg == "--patient-id" && i + 1 < argc) {
opts.
patient_id = argv[++i];
continue;
}
if (arg == "--accession" && i + 1 < argc) {
opts.accession_number = argv[++i];
continue;
}
if ((arg == "-o" || arg == "--output" || arg == "--format") && i + 1 < argc) {
continue;
}
if (arg == "--output-file" && i + 1 < argc) {
opts.output_file = argv[++i];
continue;
}
if (arg == "--max-results" && i + 1 < argc) {
try {
opts.max_results = static_cast<size_t>(std::stoul(argv[++i]));
} catch (const std::exception&) {
std::cerr << "Error: Invalid max-results value\n";
return false;
}
continue;
}
if (arg == "--calling-ae" && i + 1 < argc) {
opts.calling_ae = argv[++i];
if (opts.calling_ae.length() > 16) {
std::cerr << "Error: Calling AE title exceeds 16 characters\n";
return false;
}
continue;
}
if (arg[0] == '-') {
std::cerr << "Error: Unknown option '" << arg << "'\n";
return false;
}
positional_args.push_back(arg);
}
if (positional_args.size() < 2) {
std::cerr << "Error: Missing required arguments <peer> <port>\n";
return false;
}
if (positional_args.size() > 2) {
std::cerr << "Error: Too many positional arguments\n";
return false;
}
opts.host = positional_args[0];
try {
int port_int = std::stoi(positional_args[1]);
if (port_int < 1 || port_int > 65535) {
std::cerr << "Error: Port must be between 1 and 65535\n";
return false;
}
opts.port = static_cast<uint16_t>(port_int);
} catch (const std::exception&) {
std::cerr << "Error: Invalid port number '" << positional_args[1] << "'\n";
return false;
}
return true;
}
int perform_query(const options& opts) {
if (opts.verbose) {
std::cout << "Connecting to " << opts.host << ":" << opts.port << "...\n";
std::cout << " Calling AE: " << opts.calling_ae << "\n";
std::cout << " Called AE: " << opts.called_ae << "\n";
std::cout << " Query Type: Modality Worklist\n";
if (!opts.modality.empty()) {
std::cout << " Modality: " << opts.modality << "\n";
}
if (!opts.scheduled_date.empty()) {
std::cout << " Sched Date: " << opts.scheduled_date << "\n";
}
if (!opts.station_ae.empty()) {
std::cout << " Station AE: " << opts.station_ae << "\n";
}
std::cout << "\n";
}
1,
std::string(worklist_find_sop_class_uid),
{
"1.2.840.10008.1.2.1",
"1.2.840.10008.1.2"
}
});
auto start_time = std::chrono::steady_clock::now();
auto connect_result = association::connect(opts.host, opts.port, config, opts.timeout);
if (connect_result.is_err()) {
std::cerr << "Failed to establish association: "
<< connect_result.error().message << "\n";
return 2;
}
auto& assoc = connect_result.value();
auto connect_time = std::chrono::steady_clock::now();
if (opts.verbose) {
auto connect_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
connect_time - start_time);
std::cout << "Association established in " << connect_duration.count() << " ms\n";
}
if (!assoc.has_accepted_context(worklist_find_sop_class_uid)) {
std::cerr << "Error: Modality Worklist SOP Class not accepted by remote SCP\n";
assoc.abort();
return 2;
}
if (opts.verbose) {
std::cout << "Sending C-FIND request...\n";
}
if (opts.custom_keys.empty()) {
auto result = scu.
query(assoc, keys);
if (result.is_err()) {
std::cerr << "Query failed: " << result.error().message << "\n";
assoc.abort();
return 2;
}
query_result = std::move(result.value());
} else {
auto query_ds = scu.
query(assoc, keys);
if (query_ds.is_err()) {
std::cerr << "Query failed: " << query_ds.error().message << "\n";
assoc.abort();
return 2;
}
query_result = std::move(query_ds.value());
if (opts.debug) {
for (const auto& key : opts.custom_keys) {
std::cout << " Custom key: (" << std::hex << std::setw(4)
<< std::setfill('0') << key.group << ","
<< std::setw(4) << key.element << std::dec
<< ") = \"" << key.value << "\"\n";
}
}
}
if (opts.verbose) {
if (query_result.is_success()) {
std::cout << "Query completed successfully.\n";
} else if (query_result.is_cancelled()) {
std::cerr << "Query was cancelled.\n";
} else {
std::cerr << "Query completed with status: 0x"
<< std::hex << query_result.status << std::dec << "\n";
}
}
if (opts.verbose) {
std::cout << "Releasing association...\n";
}
auto release_result = assoc.release(opts.timeout);
if (release_result.is_err() && opts.verbose) {
std::cerr << "Warning: Release failed: " << release_result.error().message << "\n";
}
auto end_time = std::chrono::steady_clock::now();
auto total_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
end_time - start_time);
std::vector<kcenon::pacs::core::dicom_dataset> result_datasets;
result_datasets.reserve(query_result.items.size());
for (const auto& item : query_result.items) {
result_datasets.push_back(item.dataset);
}
std::string formatted_output = formatter.
format(result_datasets);
if (!opts.output_file.empty()) {
std::ofstream outfile(opts.output_file);
if (!outfile) {
std::cerr << "Error: Cannot open output file '" << opts.output_file << "'\n";
return 2;
}
outfile << formatted_output;
outfile.close();
if (opts.verbose) {
std::cout << "Results written to: " << opts.output_file << "\n";
}
} else {
std::cout << formatted_output;
}
std::cout << "\n========================================\n";
std::cout << " Summary\n";
std::cout << "========================================\n";
std::cout << " Total items: " << query_result.items.size();
if (opts.max_results > 0 && query_result.total_pending > opts.max_results) {
std::cout << " (limited from " << query_result.total_pending << ")";
}
std::cout << "\n";
std::cout << " Query time: " << total_duration.count() << " ms\n";
std::cout << "========================================\n";
}
return query_result.items.empty() ? 1 : 0;
}
}
int main(
int argc,
char* argv[]) {
bool show_banner = true;
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if ((arg == "--format" || arg == "--output" || arg == "-o") && i + 1 < argc) {
std::string fmt = argv[i + 1];
if (fmt == "json" || fmt == "csv" || fmt == "xml") {
show_banner = false;
}
break;
}
}
if (show_banner) {
std::cout << R"(
__ __ _ _ _ _ ____ ____ _ _
\ \ / /__ _ __| | _| (_)___| |_ / ___| / ___| | | |
\ \ /\ / / _ \| '__| |/ / | / __| __| \___ \| | | | | |
\ V V / (_) | | | <| | \__ \ |_ ___) | |___| |_| |
\_/\_/ \___/|_| |_|\_\_|_|___/\__| |____/ \____|\___/
Modality Worklist Query Client
)" << "\n";
}
options opts;
if (!parse_arguments(argc, argv, opts)) {
print_usage(argv[0]);
return 2;
}
return perform_query(opts);
}
DICOM Association management per PS3.8.
size_t queries_performed() const noexcept
Get the number of queries performed since construction.
network::Result< worklist_result > query(network::association &assoc, const worklist_query_keys &keys)
Perform a MWL C-FIND query with typed keys.
size_t total_items() const noexcept
Get the total number of items received since construction.
Compile-time constants for commonly used DICOM tags.
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)