50std::atomic<kcenon::pacs::network::dicom_server*> g_server{
nullptr};
53std::atomic<bool> g_running{
true};
59void signal_handler(
int signal) {
60 std::cout <<
"\nReceived signal " << signal <<
", shutting down...\n";
63 auto* server = g_server.load();
72void install_signal_handlers() {
73 std::signal(SIGINT, signal_handler);
74 std::signal(SIGTERM, signal_handler);
76 std::signal(SIGHUP, signal_handler);
85using json_object = std::map<std::string, json_value>;
86using json_array = std::vector<json_value>;
89 std::variant<std::nullptr_t, bool, double, std::string, json_array, json_object> data;
91 bool is_null()
const {
return std::holds_alternative<std::nullptr_t>(data); }
92 bool is_bool()
const {
return std::holds_alternative<bool>(data); }
93 bool is_number()
const {
return std::holds_alternative<double>(data); }
94 bool is_string()
const {
return std::holds_alternative<std::string>(data); }
95 bool is_array()
const {
return std::holds_alternative<json_array>(data); }
96 bool is_object()
const {
return std::holds_alternative<json_object>(data); }
98 bool as_bool()
const {
return std::get<bool>(data); }
99 double as_number()
const {
return std::get<double>(data); }
100 const std::string& as_string()
const {
return std::get<std::string>(data); }
101 const json_array& as_array()
const {
return std::get<json_array>(data); }
102 const json_object& as_object()
const {
return std::get<json_object>(data); }
104 bool has(
const std::string& key)
const {
105 if (!is_object())
return false;
106 return as_object().count(key) > 0;
109 const json_value& operator[](
const std::string& key)
const {
110 return as_object().at(key);
113 const json_value& operator[](
size_t idx)
const {
114 return as_array().at(idx);
117 std::string get_string(
const std::string& key,
const std::string& default_val =
"")
const {
118 if (!is_object() || !has(key))
return default_val;
119 const auto& val = (*this)[key];
120 if (val.is_string())
return val.as_string();
121 if (val.is_number()) {
122 std::ostringstream oss;
123 oss << std::fixed << std::setprecision(0) << val.as_number();
135 explicit json_parser(
const std::string& input) : input_(input),
pos_(0) {}
139 return parse_value();
143 const std::string& input_;
147 return pos_ < input_.size() ? input_[
pos_] :
'\0';
151 return pos_ < input_.size() ? input_[
pos_++] :
'\0';
154 void skip_whitespace() {
155 while (pos_ < input_.size() && std::isspace(input_[pos_])) {
160 json_value parse_value() {
164 if (c ==
'"')
return parse_string();
165 if (c ==
'{')
return parse_object();
166 if (c ==
'[')
return parse_array();
167 if (c ==
't' || c ==
'f')
return parse_bool();
168 if (c ==
'n')
return parse_null();
169 if (c ==
'-' || std::isdigit(c))
return parse_number();
171 throw std::runtime_error(
"Invalid JSON at position " + std::to_string(pos_));
174 json_value parse_string() {
177 while (peek() !=
'"') {
178 if (peek() ==
'\\') {
180 char escaped =
get();
182 case 'n': result +=
'\n';
break;
183 case 't': result +=
'\t';
break;
184 case 'r': result +=
'\r';
break;
185 case '"': result +=
'"';
break;
186 case '\\': result +=
'\\';
break;
187 case '/': result +=
'/';
break;
190 for (
int i = 0; i < 4; ++i) hex +=
get();
191 result +=
static_cast<char>(std::stoul(hex,
nullptr, 16));
194 default: result += escaped;
break;
201 return json_value{result};
204 json_value parse_object() {
211 return json_value{obj};
216 auto key = parse_string().as_string();
220 obj[key] = parse_value();
230 return json_value{obj};
233 json_value parse_array() {
240 return json_value{arr};
245 arr.push_back(parse_value());
255 return json_value{arr};
258 json_value parse_bool() {
259 if (input_.substr(pos_, 4) ==
"true") {
261 return json_value{
true};
263 if (input_.substr(pos_, 5) ==
"false") {
265 return json_value{
false};
267 throw std::runtime_error(
"Invalid boolean at position " + std::to_string(pos_));
270 json_value parse_null() {
271 if (input_.substr(pos_, 4) ==
"null") {
273 return json_value{
nullptr};
275 throw std::runtime_error(
"Invalid null at position " + std::to_string(pos_));
278 json_value parse_number() {
280 if (peek() ==
'-')
get();
281 while (std::isdigit(peek()))
get();
284 while (std::isdigit(peek()))
get();
286 if (peek() ==
'e' || peek() ==
'E') {
288 if (peek() ==
'+' || peek() ==
'-')
get();
289 while (std::isdigit(peek()))
get();
291 return json_value{std::stod(input_.substr(start, pos_ - start))};
303void print_usage(
const char* program_name) {
305Modality Worklist SCP - DICOM Worklist Server
307Usage: )" << program_name << R"( <port> <ae_title> [options]
310 port Port number to listen on (typically 104 or 11112)
311 ae_title Application Entity Title for this server (max 16 chars)
313Data Source Options (at least one required):
314 --worklist-file <path> JSON file containing worklist items
315 --worklist-dir <path> Directory containing worklist JSON files
318 --max-assoc <n> Maximum concurrent associations (default: 10)
319 --timeout <sec> Idle timeout in seconds (default: 300)
320 --max-results <n> Maximum results per query (default: unlimited)
321 --reload Enable auto-reload of worklist files
322 --help Show this help message
325 )" << program_name << R"( 11112 MY_WORKLIST --worklist-file ./worklist.json
326 )" << program_name << R"( 11112 MY_WORKLIST --worklist-dir ./worklist_data
327 )" << program_name << R"( 11112 MY_WORKLIST --worklist-file ./worklist.json --max-results 100
329JSON Worklist File Format:
332 "patientId": "12345",
333 "patientName": "DOE^JOHN",
334 "patientBirthDate": "19800101",
336 "studyInstanceUid": "1.2.3.4.5...",
337 "accessionNumber": "ACC001",
338 "scheduledStationAeTitle": "CT_01",
339 "scheduledProcedureStepStartDate": "20241220",
340 "scheduledProcedureStepStartTime": "100000",
342 "scheduledProcedureStepId": "SPS001",
343 "scheduledProcedureStepDescription": "CT Abdomen"
348 - Press Ctrl+C to stop the server gracefully
349 - Worklist items are loaded on startup
350 - With --reload, files are re-read when changed
354 1 Error - Failed to start server or invalid arguments
361struct worklist_scp_args {
363 std::string ae_title;
364 std::filesystem::path worklist_file;
365 std::filesystem::path worklist_dir;
366 size_t max_associations = 10;
367 uint32_t idle_timeout = 300;
368 size_t max_results = 0;
369 bool auto_reload =
false;
379bool parse_arguments(
int argc,
char* argv[], worklist_scp_args& args) {
385 for (
int i = 1; i < argc; ++i) {
386 if (std::string(argv[i]) ==
"--help" || std::string(argv[i]) ==
"-h") {
393 int port_int = std::stoi(argv[1]);
394 if (port_int < 1 || port_int > 65535) {
395 std::cerr <<
"Error: Port must be between 1 and 65535\n";
398 args.port =
static_cast<uint16_t
>(port_int);
399 }
catch (
const std::exception&) {
400 std::cerr <<
"Error: Invalid port number '" << argv[1] <<
"'\n";
405 args.ae_title = argv[2];
406 if (args.ae_title.length() > 16) {
407 std::cerr <<
"Error: AE title exceeds 16 characters\n";
412 for (
int i = 3; i < argc; ++i) {
413 std::string arg = argv[i];
415 if (arg ==
"--worklist-file" && i + 1 < argc) {
416 args.worklist_file = argv[++i];
417 }
else if (arg ==
"--worklist-dir" && i + 1 < argc) {
418 args.worklist_dir = argv[++i];
419 }
else if (arg ==
"--max-assoc" && i + 1 < argc) {
421 int val = std::stoi(argv[++i]);
423 std::cerr <<
"Error: max-assoc must be positive\n";
426 args.max_associations =
static_cast<size_t>(val);
427 }
catch (
const std::exception&) {
428 std::cerr <<
"Error: Invalid max-assoc value\n";
431 }
else if (arg ==
"--timeout" && i + 1 < argc) {
433 int val = std::stoi(argv[++i]);
435 std::cerr <<
"Error: timeout cannot be negative\n";
438 args.idle_timeout =
static_cast<uint32_t
>(val);
439 }
catch (
const std::exception&) {
440 std::cerr <<
"Error: Invalid timeout value\n";
443 }
else if (arg ==
"--max-results" && i + 1 < argc) {
445 int val = std::stoi(argv[++i]);
447 std::cerr <<
"Error: max-results cannot be negative\n";
450 args.max_results =
static_cast<size_t>(val);
451 }
catch (
const std::exception&) {
452 std::cerr <<
"Error: Invalid max-results value\n";
455 }
else if (arg ==
"--reload") {
456 args.auto_reload =
true;
458 std::cerr <<
"Error: Unknown option '" << arg <<
"'\n";
464 if (args.worklist_file.empty() && args.worklist_dir.empty()) {
465 std::cerr <<
"Error: --worklist-file or --worklist-dir is required\n";
480std::string current_timestamp() {
481 auto now = std::chrono::system_clock::now();
482 auto time_t_now = std::chrono::system_clock::to_time_t(now);
483 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
484 now.time_since_epoch()) % 1000;
488 localtime_s(&tm_buf, &time_t_now);
490 localtime_r(&time_t_now, &tm_buf);
493 std::ostringstream oss;
494 oss << std::put_time(&tm_buf,
"%Y-%m-%d %H:%M:%S");
495 oss <<
'.' << std::setfill(
'0') << std::setw(3) << ms.count();
506struct worklist_item {
524 std::string scheduled_performing_physician;
537worklist_item parse_worklist_item(
const json_value& json) {
540 item.patient_id = json.get_string(
"patientId");
541 item.patient_name = json.get_string(
"patientName");
542 item.patient_birth_date = json.get_string(
"patientBirthDate");
543 item.patient_sex = json.get_string(
"patientSex");
545 item.study_instance_uid = json.get_string(
"studyInstanceUid");
546 item.accession_number = json.get_string(
"accessionNumber");
547 item.referring_physician = json.get_string(
"referringPhysician");
548 item.study_description = json.get_string(
"studyDescription");
550 item.scheduled_station_ae_title = json.get_string(
"scheduledStationAeTitle");
551 item.scheduled_procedure_step_start_date = json.get_string(
"scheduledProcedureStepStartDate");
552 item.scheduled_procedure_step_start_time = json.get_string(
"scheduledProcedureStepStartTime");
553 item.modality = json.get_string(
"modality");
554 item.scheduled_performing_physician = json.get_string(
"scheduledPerformingPhysician");
555 item.scheduled_procedure_step_description = json.get_string(
"scheduledProcedureStepDescription");
556 item.scheduled_procedure_step_id = json.get_string(
"scheduledProcedureStepId");
557 item.scheduled_procedure_step_location = json.get_string(
"scheduledProcedureStepLocation");
559 item.requested_procedure_id = json.get_string(
"requestedProcedureId");
560 item.requested_procedure_description = json.get_string(
"requestedProcedureDescription");
568std::vector<worklist_item> load_worklist_file(
const std::filesystem::path& path) {
569 std::vector<worklist_item> items;
571 std::ifstream file(path);
573 std::cerr <<
"Warning: Could not open worklist file: " << path <<
"\n";
577 std::string content((std::istreambuf_iterator<char>(file)),
578 std::istreambuf_iterator<char>());
581 json_parser parser(content);
582 auto json = parser.parse();
584 if (json.is_array()) {
585 for (
size_t i = 0; i < json.as_array().size(); ++i) {
586 items.push_back(parse_worklist_item(json[i]));
588 }
else if (json.is_object()) {
590 items.push_back(parse_worklist_item(json));
592 }
catch (
const std::exception& e) {
593 std::cerr <<
"Warning: Failed to parse worklist file " << path
594 <<
": " << e.what() <<
"\n";
603std::vector<worklist_item> load_worklist_directory(
const std::filesystem::path& dir) {
604 std::vector<worklist_item> items;
605 namespace fs = std::filesystem;
607 if (!fs::exists(dir) || !fs::is_directory(dir)) {
608 std::cerr <<
"Warning: Worklist directory does not exist: " << dir <<
"\n";
612 for (
const auto& entry : fs::recursive_directory_iterator(dir)) {
613 if (!entry.is_regular_file())
continue;
615 auto ext = entry.path().extension().string();
616 if (ext !=
".json" && ext !=
".JSON")
continue;
618 auto file_items = load_worklist_file(entry.path());
619 items.insert(items.end(), file_items.begin(), file_items.end());
628class worklist_repository {
630 void load(
const worklist_scp_args& args) {
631 std::lock_guard<std::mutex> lock(mutex_);
634 if (!args.worklist_file.empty()) {
635 auto file_items = load_worklist_file(args.worklist_file);
636 items_.insert(items_.end(), file_items.begin(), file_items.end());
639 if (!args.worklist_dir.empty()) {
640 auto dir_items = load_worklist_directory(args.worklist_dir);
641 items_.insert(items_.end(), dir_items.begin(), dir_items.end());
644 std::cout <<
"Loaded " << items_.size() <<
" worklist item(s)\n";
647 std::vector<kcenon::pacs::core::dicom_dataset>
query(
649 [[maybe_unused]]
const std::string& calling_ae)
const {
655 std::lock_guard<std::mutex> lock(mutex_);
656 std::vector<dicom_dataset> results;
659 std::string query_patient_id = query_keys.
get_string(tags::patient_id,
"");
660 std::string query_patient_name = query_keys.
get_string(tags::patient_name,
"");
661 std::string query_accession = query_keys.
get_string(tags::accession_number,
"");
664 std::string query_station_ae = query_keys.
get_string(tags::scheduled_station_ae_title,
"");
665 std::string query_start_date = query_keys.
get_string(tags::scheduled_procedure_step_start_date,
"");
666 std::string query_modality = query_keys.
get_string(tags::modality,
"");
668 for (
const auto& item : items_) {
670 if (!query_patient_id.empty() && !matches_wildcard(
item.patient_id, query_patient_id)) {
673 if (!query_patient_name.empty() && !matches_wildcard(
item.patient_name, query_patient_name)) {
676 if (!query_accession.empty() && !matches_wildcard(
item.accession_number, query_accession)) {
679 if (!query_station_ae.empty() && !matches_wildcard(
item.scheduled_station_ae_title, query_station_ae)) {
682 if (!query_start_date.empty() && !matches_date_range(
item.scheduled_procedure_step_start_date, query_start_date)) {
685 if (!query_modality.empty() && !matches_wildcard(
item.modality, query_modality)) {
693 ds.set_string(tags::patient_id, vr_type::LO,
item.patient_id);
694 ds.set_string(tags::patient_name, vr_type::PN,
item.patient_name);
695 ds.set_string(tags::patient_birth_date, vr_type::DA,
item.patient_birth_date);
696 ds.set_string(tags::patient_sex, vr_type::CS,
item.patient_sex);
699 ds.set_string(tags::study_instance_uid, vr_type::UI,
item.study_instance_uid);
700 ds.set_string(tags::accession_number, vr_type::SH,
item.accession_number);
701 ds.set_string(tags::referring_physician_name, vr_type::PN,
item.referring_physician);
702 ds.set_string(tags::study_description, vr_type::LO,
item.study_description);
705 ds.set_string(tags::requested_procedure_id, vr_type::SH,
item.requested_procedure_id);
706 ds.set_string(tags::requested_procedure_description, vr_type::LO,
item.requested_procedure_description);
709 ds.set_string(tags::scheduled_station_ae_title, vr_type::AE,
item.scheduled_station_ae_title);
710 ds.set_string(tags::scheduled_procedure_step_start_date, vr_type::DA,
item.scheduled_procedure_step_start_date);
711 ds.set_string(tags::scheduled_procedure_step_start_time, vr_type::TM,
item.scheduled_procedure_step_start_time);
712 ds.set_string(tags::modality, vr_type::CS,
item.modality);
713 ds.set_string(tags::scheduled_performing_physician_name, vr_type::PN,
item.scheduled_performing_physician);
714 ds.set_string(tags::scheduled_procedure_step_description, vr_type::LO,
item.scheduled_procedure_step_description);
715 ds.set_string(tags::scheduled_procedure_step_id, vr_type::SH,
item.scheduled_procedure_step_id);
716 ds.set_string(tags::scheduled_procedure_step_location, vr_type::SH,
item.scheduled_procedure_step_location);
718 results.push_back(std::move(ds));
724 size_t size()
const {
725 std::lock_guard<std::mutex> lock(mutex_);
726 return items_.size();
730 mutable std::mutex mutex_;
731 std::vector<worklist_item> items_;
737 static bool matches_wildcard(
const std::string& value,
const std::string& pattern) {
738 if (pattern.empty() || pattern ==
"*")
return true;
741 size_t v_star = std::string::npos, p_star = std::string::npos;
743 while (v < value.size()) {
744 if (p < pattern.size() && (pattern[p] ==
'?' || std::toupper(pattern[p]) == std::toupper(value[v]))) {
747 }
else if (p < pattern.size() && pattern[p] ==
'*') {
750 }
else if (p_star != std::string::npos) {
758 while (p < pattern.size() && pattern[p] ==
'*') ++p;
759 return p == pattern.size();
766 static bool matches_date_range(
const std::string& value,
const std::string& pattern) {
767 if (pattern.empty())
return true;
769 auto dash_pos = pattern.find(
'-');
770 if (dash_pos == std::string::npos) {
772 return value == pattern;
776 std::string start_date = pattern.substr(0, dash_pos);
777 std::string end_date = pattern.substr(dash_pos + 1);
779 if (start_date.empty()) start_date =
"00000000";
780 if (end_date.empty()) end_date =
"99999999";
782 return value >= start_date && value <= end_date;
795bool run_server(
const worklist_scp_args& args) {
799 std::cout <<
"\nStarting Modality Worklist SCP...\n";
800 std::cout <<
" AE Title: " << args.ae_title <<
"\n";
801 std::cout <<
" Port: " << args.port <<
"\n";
802 if (!args.worklist_file.empty()) {
803 std::cout <<
" Worklist File: " << args.worklist_file <<
"\n";
805 if (!args.worklist_dir.empty()) {
806 std::cout <<
" Worklist Directory: " << args.worklist_dir <<
"\n";
808 std::cout <<
" Max Associations: " << args.max_associations <<
"\n";
809 std::cout <<
" Idle Timeout: " << args.idle_timeout <<
" seconds\n";
810 if (args.max_results > 0) {
811 std::cout <<
" Max Results: " << args.max_results <<
"\n";
816 worklist_repository repository;
817 repository.load(args);
819 if (repository.size() == 0) {
820 std::cout <<
"\nWarning: No worklist items loaded.\n";
821 std::cout <<
" Server will start but queries will return no results.\n\n";
825 server_config config;
826 config.ae_title = args.ae_title;
827 config.port = args.port;
828 config.max_associations = args.max_associations;
829 config.idle_timeout = std::chrono::seconds{args.idle_timeout};
830 config.implementation_class_uid =
"1.2.826.0.1.3680043.2.1545.2";
831 config.implementation_version_name =
"WL_SCP_001";
834 dicom_server server{config};
838 server.register_service(std::make_shared<verification_scp>());
841 auto worklist_service = std::make_shared<worklist_scp>();
842 worklist_service->set_handler(
844 return repository.query(keys, ae);
847 if (args.max_results > 0) {
848 worklist_service->set_max_results(args.max_results);
851 server.register_service(worklist_service);
854 server.on_association_established([](
const association& assoc) {
855 std::cout <<
"[" << current_timestamp() <<
"] "
856 <<
"Association established from: " << assoc.calling_ae()
857 <<
" -> " << assoc.called_ae() <<
"\n";
860 server.on_association_released([](
const association& assoc) {
861 std::cout <<
"[" << current_timestamp() <<
"] "
862 <<
"Association released: " << assoc.calling_ae() <<
"\n";
865 server.on_error([](
const std::string& error) {
866 std::cerr <<
"[" << current_timestamp() <<
"] "
867 <<
"Error: " <<
error <<
"\n";
871 auto result = server.start();
872 if (result.is_err()) {
873 std::cerr <<
"Failed to start server: " << result.error().message <<
"\n";
878 std::cout <<
"=================================================\n";
879 std::cout <<
" Modality Worklist SCP is running on port " << args.port <<
"\n";
880 std::cout <<
" Worklist Items: " << repository.size() <<
"\n";
881 std::cout <<
" Press Ctrl+C to stop\n";
882 std::cout <<
"=================================================\n\n";
885 server.wait_for_shutdown();
888 auto server_stats = server.get_statistics();
891 std::cout <<
"=================================================\n";
892 std::cout <<
" Server Statistics\n";
893 std::cout <<
"=================================================\n";
894 std::cout <<
" Total Associations: " << server_stats.total_associations <<
"\n";
895 std::cout <<
" Rejected Associations: " << server_stats.rejected_associations <<
"\n";
896 std::cout <<
" Messages Processed: " << server_stats.messages_processed <<
"\n";
897 std::cout <<
" Worklist Queries: " << worklist_service->queries_processed() <<
"\n";
898 std::cout <<
" Items Returned: " << worklist_service->items_returned() <<
"\n";
899 std::cout <<
" Uptime: " << server_stats.uptime().count() <<
" seconds\n";
900 std::cout <<
"=================================================\n";
908int main(
int argc,
char* argv[]) {
910 __ __ _ _ _ _ ____ ____ ____
911 \ \ / /__ _ __| | _| (_)___| |_ / ___| / ___| _ \
912 \ \ /\ / / _ \| '__| |/ / | / __| __| | \___ \| | | |_) |
913 \ V V / (_) | | | <| | \__ \ |_ ___) | |___| __/
914 \_/\_/ \___/|_| |_|\_\_|_|___/\__| |____/ \____|_|
916 DICOM Modality Worklist Server
919 worklist_scp_args args;
921 if (!parse_arguments(argc, argv, args)) {
922 print_usage(argv[0]);
927 install_signal_handlers();
929 bool success = run_server(args);
931 std::cout <<
"\nModality Worklist SCP terminated\n";
932 return success ? 0 : 1;
auto get_string(dicom_tag tag, std::string_view default_value="") const -> std::string
Get the string value of an element.
DICOM Dataset - ordered collection of Data Elements.
Multi-threaded DICOM server for handling multiple associations.
Compile-time constants for commonly used DICOM tags.
@ error
Node returned an error.
@ referring_physician
(0008,0090) Referring Physician's Name
@ get
C-GET retrieve request/response.
const atna_coded_value query
Query (110112)
DICOM Server configuration structures.
DICOM Verification SCP service (C-ECHO handler)
DICOM Modality Worklist SCP service (MWL C-FIND handler)