PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1
27
28#include <atomic>
29#include <chrono>
30#include <csignal>
31#include <cstdlib>
32#include <filesystem>
33#include <fstream>
34#include <iomanip>
35#include <iostream>
36#include <map>
37#include <mutex>
38#include <sstream>
39#include <string>
40#include <variant>
41#include <vector>
42
43namespace {
44
45// =============================================================================
46// Global State for Signal Handling
47// =============================================================================
48
50std::atomic<kcenon::pacs::network::dicom_server*> g_server{nullptr};
51
53std::atomic<bool> g_running{true};
54
59void signal_handler(int signal) {
60 std::cout << "\nReceived signal " << signal << ", shutting down...\n";
61 g_running = false;
62
63 auto* server = g_server.load();
64 if (server) {
65 server->stop();
66 }
67}
68
72void install_signal_handlers() {
73 std::signal(SIGINT, signal_handler);
74 std::signal(SIGTERM, signal_handler);
75#ifndef _WIN32
76 std::signal(SIGHUP, signal_handler);
77#endif
78}
79
80// =============================================================================
81// Minimal JSON Parser
82// =============================================================================
83
84struct json_value;
85using json_object = std::map<std::string, json_value>;
86using json_array = std::vector<json_value>;
87
88struct json_value {
89 std::variant<std::nullptr_t, bool, double, std::string, json_array, json_object> data;
90
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); }
97
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); }
103
104 bool has(const std::string& key) const {
105 if (!is_object()) return false;
106 return as_object().count(key) > 0;
107 }
108
109 const json_value& operator[](const std::string& key) const {
110 return as_object().at(key);
111 }
112
113 const json_value& operator[](size_t idx) const {
114 return as_array().at(idx);
115 }
116
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();
124 return oss.str();
125 }
126 return default_val;
127 }
128};
129
133class json_parser {
134public:
135 explicit json_parser(const std::string& input) : input_(input), pos_(0) {}
136
137 json_value parse() {
138 skip_whitespace();
139 return parse_value();
140 }
141
142private:
143 const std::string& input_;
144 size_t pos_;
145
146 char peek() const {
147 return pos_ < input_.size() ? input_[pos_] : '\0';
148 }
149
150 char get() {
151 return pos_ < input_.size() ? input_[pos_++] : '\0';
152 }
153
154 void skip_whitespace() {
155 while (pos_ < input_.size() && std::isspace(input_[pos_])) {
156 ++pos_;
157 }
158 }
159
160 json_value parse_value() {
161 skip_whitespace();
162 char c = peek();
163
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();
170
171 throw std::runtime_error("Invalid JSON at position " + std::to_string(pos_));
172 }
173
174 json_value parse_string() {
175 get(); // consume '"'
176 std::string result;
177 while (peek() != '"') {
178 if (peek() == '\\') {
179 get(); // consume backslash
180 char escaped = get();
181 switch (escaped) {
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;
188 case 'u': {
189 std::string hex;
190 for (int i = 0; i < 4; ++i) hex += get();
191 result += static_cast<char>(std::stoul(hex, nullptr, 16));
192 break;
193 }
194 default: result += escaped; break;
195 }
196 } else {
197 result += get();
198 }
199 }
200 get(); // consume closing '"'
201 return json_value{result};
202 }
203
204 json_value parse_object() {
205 get(); // consume '{'
206 json_object obj;
207 skip_whitespace();
208
209 if (peek() == '}') {
210 get();
211 return json_value{obj};
212 }
213
214 while (true) {
215 skip_whitespace();
216 auto key = parse_string().as_string();
217 skip_whitespace();
218 get(); // consume ':'
219 skip_whitespace();
220 obj[key] = parse_value();
221 skip_whitespace();
222
223 if (peek() == '}') {
224 get();
225 break;
226 }
227 get(); // consume ','
228 }
229
230 return json_value{obj};
231 }
232
233 json_value parse_array() {
234 get(); // consume '['
235 json_array arr;
236 skip_whitespace();
237
238 if (peek() == ']') {
239 get();
240 return json_value{arr};
241 }
242
243 while (true) {
244 skip_whitespace();
245 arr.push_back(parse_value());
246 skip_whitespace();
247
248 if (peek() == ']') {
249 get();
250 break;
251 }
252 get(); // consume ','
253 }
254
255 return json_value{arr};
256 }
257
258 json_value parse_bool() {
259 if (input_.substr(pos_, 4) == "true") {
260 pos_ += 4;
261 return json_value{true};
262 }
263 if (input_.substr(pos_, 5) == "false") {
264 pos_ += 5;
265 return json_value{false};
266 }
267 throw std::runtime_error("Invalid boolean at position " + std::to_string(pos_));
268 }
269
270 json_value parse_null() {
271 if (input_.substr(pos_, 4) == "null") {
272 pos_ += 4;
273 return json_value{nullptr};
274 }
275 throw std::runtime_error("Invalid null at position " + std::to_string(pos_));
276 }
277
278 json_value parse_number() {
279 size_t start = pos_;
280 if (peek() == '-') get();
281 while (std::isdigit(peek())) get();
282 if (peek() == '.') {
283 get();
284 while (std::isdigit(peek())) get();
285 }
286 if (peek() == 'e' || peek() == 'E') {
287 get();
288 if (peek() == '+' || peek() == '-') get();
289 while (std::isdigit(peek())) get();
290 }
291 return json_value{std::stod(input_.substr(start, pos_ - start))};
292 }
293};
294
295// =============================================================================
296// Command Line Parsing
297// =============================================================================
298
303void print_usage(const char* program_name) {
304 std::cout << R"(
305Modality Worklist SCP - DICOM Worklist Server
306
307Usage: )" << program_name << R"( <port> <ae_title> [options]
308
309Arguments:
310 port Port number to listen on (typically 104 or 11112)
311 ae_title Application Entity Title for this server (max 16 chars)
312
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
316
317Optional Options:
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
323
324Examples:
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
328
329JSON Worklist File Format:
330 [
331 {
332 "patientId": "12345",
333 "patientName": "DOE^JOHN",
334 "patientBirthDate": "19800101",
335 "patientSex": "M",
336 "studyInstanceUid": "1.2.3.4.5...",
337 "accessionNumber": "ACC001",
338 "scheduledStationAeTitle": "CT_01",
339 "scheduledProcedureStepStartDate": "20241220",
340 "scheduledProcedureStepStartTime": "100000",
341 "modality": "CT",
342 "scheduledProcedureStepId": "SPS001",
343 "scheduledProcedureStepDescription": "CT Abdomen"
344 }
345 ]
346
347Notes:
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
351
352Exit Codes:
353 0 Normal termination
354 1 Error - Failed to start server or invalid arguments
355)";
356}
357
361struct worklist_scp_args {
362 uint16_t port = 0;
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;
370};
371
379bool parse_arguments(int argc, char* argv[], worklist_scp_args& args) {
380 if (argc < 3) {
381 return false;
382 }
383
384 // Check for help flag
385 for (int i = 1; i < argc; ++i) {
386 if (std::string(argv[i]) == "--help" || std::string(argv[i]) == "-h") {
387 return false;
388 }
389 }
390
391 // Parse port
392 try {
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";
396 return false;
397 }
398 args.port = static_cast<uint16_t>(port_int);
399 } catch (const std::exception&) {
400 std::cerr << "Error: Invalid port number '" << argv[1] << "'\n";
401 return false;
402 }
403
404 // Parse AE title
405 args.ae_title = argv[2];
406 if (args.ae_title.length() > 16) {
407 std::cerr << "Error: AE title exceeds 16 characters\n";
408 return false;
409 }
410
411 // Parse optional arguments
412 for (int i = 3; i < argc; ++i) {
413 std::string arg = argv[i];
414
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) {
420 try {
421 int val = std::stoi(argv[++i]);
422 if (val < 1) {
423 std::cerr << "Error: max-assoc must be positive\n";
424 return false;
425 }
426 args.max_associations = static_cast<size_t>(val);
427 } catch (const std::exception&) {
428 std::cerr << "Error: Invalid max-assoc value\n";
429 return false;
430 }
431 } else if (arg == "--timeout" && i + 1 < argc) {
432 try {
433 int val = std::stoi(argv[++i]);
434 if (val < 0) {
435 std::cerr << "Error: timeout cannot be negative\n";
436 return false;
437 }
438 args.idle_timeout = static_cast<uint32_t>(val);
439 } catch (const std::exception&) {
440 std::cerr << "Error: Invalid timeout value\n";
441 return false;
442 }
443 } else if (arg == "--max-results" && i + 1 < argc) {
444 try {
445 int val = std::stoi(argv[++i]);
446 if (val < 0) {
447 std::cerr << "Error: max-results cannot be negative\n";
448 return false;
449 }
450 args.max_results = static_cast<size_t>(val);
451 } catch (const std::exception&) {
452 std::cerr << "Error: Invalid max-results value\n";
453 return false;
454 }
455 } else if (arg == "--reload") {
456 args.auto_reload = true;
457 } else {
458 std::cerr << "Error: Unknown option '" << arg << "'\n";
459 return false;
460 }
461 }
462
463 // Validate required arguments
464 if (args.worklist_file.empty() && args.worklist_dir.empty()) {
465 std::cerr << "Error: --worklist-file or --worklist-dir is required\n";
466 return false;
467 }
468
469 return true;
470}
471
472// =============================================================================
473// Utility Functions
474// =============================================================================
475
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;
485
486 std::tm tm_buf{};
487#ifdef _WIN32
488 localtime_s(&tm_buf, &time_t_now);
489#else
490 localtime_r(&time_t_now, &tm_buf);
491#endif
492
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();
496 return oss.str();
497}
498
499// =============================================================================
500// Worklist Data Management
501// =============================================================================
502
506struct worklist_item {
507 // Patient demographics
508 std::string patient_id;
509 std::string patient_name;
510 std::string patient_birth_date;
511 std::string patient_sex;
512
513 // Study information
514 std::string study_instance_uid;
515 std::string accession_number;
516 std::string referring_physician;
517 std::string study_description;
518
519 // Scheduled Procedure Step
520 std::string scheduled_station_ae_title;
523 std::string modality;
524 std::string scheduled_performing_physician;
526 std::string scheduled_procedure_step_id;
528
529 // Requested Procedure
530 std::string requested_procedure_id;
532};
533
537worklist_item parse_worklist_item(const json_value& json) {
538 worklist_item item;
539
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");
544
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");
549
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");
558
559 item.requested_procedure_id = json.get_string("requestedProcedureId");
560 item.requested_procedure_description = json.get_string("requestedProcedureDescription");
561
562 return item;
563}
564
568std::vector<worklist_item> load_worklist_file(const std::filesystem::path& path) {
569 std::vector<worklist_item> items;
570
571 std::ifstream file(path);
572 if (!file) {
573 std::cerr << "Warning: Could not open worklist file: " << path << "\n";
574 return items;
575 }
576
577 std::string content((std::istreambuf_iterator<char>(file)),
578 std::istreambuf_iterator<char>());
579
580 try {
581 json_parser parser(content);
582 auto json = parser.parse();
583
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]));
587 }
588 } else if (json.is_object()) {
589 // Single item
590 items.push_back(parse_worklist_item(json));
591 }
592 } catch (const std::exception& e) {
593 std::cerr << "Warning: Failed to parse worklist file " << path
594 << ": " << e.what() << "\n";
595 }
596
597 return items;
598}
599
603std::vector<worklist_item> load_worklist_directory(const std::filesystem::path& dir) {
604 std::vector<worklist_item> items;
605 namespace fs = std::filesystem;
606
607 if (!fs::exists(dir) || !fs::is_directory(dir)) {
608 std::cerr << "Warning: Worklist directory does not exist: " << dir << "\n";
609 return items;
610 }
611
612 for (const auto& entry : fs::recursive_directory_iterator(dir)) {
613 if (!entry.is_regular_file()) continue;
614
615 auto ext = entry.path().extension().string();
616 if (ext != ".json" && ext != ".JSON") continue;
617
618 auto file_items = load_worklist_file(entry.path());
619 items.insert(items.end(), file_items.begin(), file_items.end());
620 }
621
622 return items;
623}
624
628class worklist_repository {
629public:
630 void load(const worklist_scp_args& args) {
631 std::lock_guard<std::mutex> lock(mutex_);
632 items_.clear();
633
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());
637 }
638
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());
642 }
643
644 std::cout << "Loaded " << items_.size() << " worklist item(s)\n";
645 }
646
647 std::vector<kcenon::pacs::core::dicom_dataset> query(
648 const kcenon::pacs::core::dicom_dataset& query_keys,
649 [[maybe_unused]] const std::string& calling_ae) const {
650
651 using namespace kcenon::pacs::core;
652 using namespace kcenon::pacs::encoding;
653 namespace tags = kcenon::pacs::core::tags;
654
655 std::lock_guard<std::mutex> lock(mutex_);
656 std::vector<dicom_dataset> results;
657
658 // Extract query filters
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, "");
662
663 // Get Scheduled Procedure Step filters (flat structure - no sequence support)
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, "");
667
668 for (const auto& item : items_) {
669 // Apply filters (empty filter matches all)
670 if (!query_patient_id.empty() && !matches_wildcard(item.patient_id, query_patient_id)) {
671 continue;
672 }
673 if (!query_patient_name.empty() && !matches_wildcard(item.patient_name, query_patient_name)) {
674 continue;
675 }
676 if (!query_accession.empty() && !matches_wildcard(item.accession_number, query_accession)) {
677 continue;
678 }
679 if (!query_station_ae.empty() && !matches_wildcard(item.scheduled_station_ae_title, query_station_ae)) {
680 continue;
681 }
682 if (!query_start_date.empty() && !matches_date_range(item.scheduled_procedure_step_start_date, query_start_date)) {
683 continue;
684 }
685 if (!query_modality.empty() && !matches_wildcard(item.modality, query_modality)) {
686 continue;
687 }
688
689 // Build response dataset
690 dicom_dataset ds;
691
692 // Patient demographics
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);
697
698 // Study information
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);
703
704 // Requested Procedure
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);
707
708 // Scheduled Procedure Step attributes (flat structure - no sequence support)
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);
717
718 results.push_back(std::move(ds));
719 }
720
721 return results;
722 }
723
724 size_t size() const {
725 std::lock_guard<std::mutex> lock(mutex_);
726 return items_.size();
727 }
728
729private:
730 mutable std::mutex mutex_;
731 std::vector<worklist_item> items_;
732
737 static bool matches_wildcard(const std::string& value, const std::string& pattern) {
738 if (pattern.empty() || pattern == "*") return true;
739
740 size_t v = 0, p = 0;
741 size_t v_star = std::string::npos, p_star = std::string::npos;
742
743 while (v < value.size()) {
744 if (p < pattern.size() && (pattern[p] == '?' || std::toupper(pattern[p]) == std::toupper(value[v]))) {
745 ++v;
746 ++p;
747 } else if (p < pattern.size() && pattern[p] == '*') {
748 v_star = v;
749 p_star = p++;
750 } else if (p_star != std::string::npos) {
751 p = p_star + 1;
752 v = ++v_star;
753 } else {
754 return false;
755 }
756 }
757
758 while (p < pattern.size() && pattern[p] == '*') ++p;
759 return p == pattern.size();
760 }
761
766 static bool matches_date_range(const std::string& value, const std::string& pattern) {
767 if (pattern.empty()) return true;
768
769 auto dash_pos = pattern.find('-');
770 if (dash_pos == std::string::npos) {
771 // Single date match
772 return value == pattern;
773 }
774
775 // Date range
776 std::string start_date = pattern.substr(0, dash_pos);
777 std::string end_date = pattern.substr(dash_pos + 1);
778
779 if (start_date.empty()) start_date = "00000000";
780 if (end_date.empty()) end_date = "99999999";
781
782 return value >= start_date && value <= end_date;
783 }
784};
785
786// =============================================================================
787// Server Implementation
788// =============================================================================
789
795bool run_server(const worklist_scp_args& args) {
796 using namespace kcenon::pacs::network;
797 using namespace kcenon::pacs::services;
798
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";
804 }
805 if (!args.worklist_dir.empty()) {
806 std::cout << " Worklist Directory: " << args.worklist_dir << "\n";
807 }
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";
812 }
813 std::cout << "\n";
814
815 // Load worklist data
816 worklist_repository repository;
817 repository.load(args);
818
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";
822 }
823
824 // Configure server
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";
832
833 // Create server
834 dicom_server server{config};
835 g_server = &server;
836
837 // Register Verification service (C-ECHO)
838 server.register_service(std::make_shared<verification_scp>());
839
840 // Configure Worklist SCP
841 auto worklist_service = std::make_shared<worklist_scp>();
842 worklist_service->set_handler(
843 [&repository](const kcenon::pacs::core::dicom_dataset& keys, const std::string& ae) {
844 return repository.query(keys, ae);
845 });
846
847 if (args.max_results > 0) {
848 worklist_service->set_max_results(args.max_results);
849 }
850
851 server.register_service(worklist_service);
852
853 // Set up callbacks for logging
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";
858 });
859
860 server.on_association_released([](const association& assoc) {
861 std::cout << "[" << current_timestamp() << "] "
862 << "Association released: " << assoc.calling_ae() << "\n";
863 });
864
865 server.on_error([](const std::string& error) {
866 std::cerr << "[" << current_timestamp() << "] "
867 << "Error: " << error << "\n";
868 });
869
870 // Start server
871 auto result = server.start();
872 if (result.is_err()) {
873 std::cerr << "Failed to start server: " << result.error().message << "\n";
874 g_server = nullptr;
875 return false;
876 }
877
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";
883
884 // Wait for shutdown
885 server.wait_for_shutdown();
886
887 // Print final statistics
888 auto server_stats = server.get_statistics();
889
890 std::cout << "\n";
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";
901
902 g_server = nullptr;
903 return true;
904}
905
906} // namespace
907
908int main(int argc, char* argv[]) {
909 std::cout << R"(
910 __ __ _ _ _ _ ____ ____ ____
911 \ \ / /__ _ __| | _| (_)___| |_ / ___| / ___| _ \
912 \ \ /\ / / _ \| '__| |/ / | / __| __| | \___ \| | | |_) |
913 \ V V / (_) | | | <| | \__ \ |_ ___) | |___| __/
914 \_/\_/ \___/|_| |_|\_\_|_|___/\__| |____/ \____|_|
915
916 DICOM Modality Worklist Server
917)" << "\n";
918
919 worklist_scp_args args;
920
921 if (!parse_arguments(argc, argv, args)) {
922 print_usage(argv[0]);
923 return 1;
924 }
925
926 // Install signal handlers
927 install_signal_handlers();
928
929 bool success = run_server(args);
930
931 std::cout << "\nModality Worklist SCP terminated\n";
932 return success ? 0 : 1;
933}
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.
size_t pos_
int main()
Definition main.cpp:84
@ error
Node returned an error.
@ referring_physician
(0008,0090) Referring Physician's Name
constexpr dicom_tag scheduled_procedure_step_description
Scheduled Procedure Step Description.
constexpr dicom_tag study_description
Study Description.
constexpr dicom_tag requested_procedure_description
Requested Procedure Description.
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag scheduled_procedure_step_start_date
Scheduled Procedure Step Start Date.
constexpr dicom_tag scheduled_procedure_step_location
Scheduled Procedure Step Location.
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 scheduled_station_ae_title
Scheduled Station AE Title.
constexpr dicom_tag patient_sex
Patient's Sex.
constexpr dicom_tag study_instance_uid
Study Instance UID.
constexpr dicom_tag item
Item.
constexpr dicom_tag requested_procedure_id
Requested Procedure ID.
constexpr dicom_tag patient_name
Patient's Name.
constexpr dicom_tag scheduled_procedure_step_start_time
Scheduled Procedure Step Start Time.
constexpr dicom_tag scheduled_procedure_step_id
Scheduled Procedure Step ID.
@ 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)