PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1
31
32#include <atomic>
33#include <chrono>
34#include <csignal>
35#include <cstdlib>
36#include <filesystem>
37#include <iomanip>
38#include <iostream>
39#include <map>
40#include <sstream>
41#include <string>
42#include <vector>
43
44namespace {
45
47std::atomic<kcenon::pacs::network::dicom_server*> g_server{nullptr};
48
50std::atomic<bool> g_running{true};
51
56void signal_handler(int signal) {
57 std::cout << "\nReceived signal " << signal << ", shutting down...\n";
58 g_running = false;
59
60 auto* server = g_server.load();
61 if (server) {
62 server->stop();
63 }
64}
65
69void install_signal_handlers() {
70 std::signal(SIGINT, signal_handler);
71 std::signal(SIGTERM, signal_handler);
72#ifndef _WIN32
73 std::signal(SIGHUP, signal_handler);
74#endif
75}
76
81void print_usage(const char* program_name) {
82 std::cout << R"(
83Query/Retrieve SCP - DICOM Query/Retrieve Server
84
85Usage: )" << program_name << R"( <port> <ae_title> [options]
86
87Arguments:
88 port Port number to listen on (typically 104 or 11112)
89 ae_title Application Entity Title for this server (max 16 chars)
90
91Required Options:
92 --storage-dir <path> Directory containing DICOM files to serve
93
94Optional Options:
95 --index-db <path> SQLite database for indexing (default: in-memory)
96 --peer <spec> Known peer for C-MOVE (format: AE:host:port)
97 Can be specified multiple times
98 --max-assoc <n> Maximum concurrent associations (default: 10)
99 --timeout <sec> Idle timeout in seconds (default: 300)
100 --scan-only Scan storage and exit (for indexing)
101 --help Show this help message
102
103Examples:
104 )" << program_name << R"( 11112 MY_PACS --storage-dir ./dicom
105 )" << program_name << R"( 11112 MY_PACS --storage-dir ./dicom --index-db ./pacs.db
106 )" << program_name << R"( 11112 MY_PACS --storage-dir ./dicom --peer VIEWER:192.168.1.10:11113
107 )" << program_name << R"( 11112 MY_PACS --storage-dir ./dicom --peer WS1:10.0.0.1:104 --peer WS2:10.0.0.2:104
108
109Notes:
110 - Press Ctrl+C to stop the server gracefully
111 - Files are indexed on startup from the storage directory
112 - C-FIND supports Patient Root and Study Root queries
113 - C-MOVE requires known peers to be configured with --peer
114 - C-GET sends files directly to the requesting SCU
115
116Exit Codes:
117 0 Normal termination
118 1 Error - Failed to start server or invalid arguments
119)";
120}
121
125struct peer_config {
126 std::string ae_title;
127 std::string host;
128 uint16_t port;
129};
130
134struct qr_scp_args {
135 uint16_t port = 0;
136 std::string ae_title;
137 std::filesystem::path storage_dir;
138 std::filesystem::path index_db;
139 std::vector<peer_config> peers;
140 size_t max_associations = 10;
141 uint32_t idle_timeout = 300;
142 bool scan_only = false;
143};
144
151bool parse_peer(const std::string& spec, peer_config& peer) {
152 size_t first_colon = spec.find(':');
153 if (first_colon == std::string::npos) {
154 return false;
155 }
156
157 size_t last_colon = spec.rfind(':');
158 if (last_colon == first_colon || last_colon == spec.length() - 1) {
159 return false;
160 }
161
162 peer.ae_title = spec.substr(0, first_colon);
163 peer.host = spec.substr(first_colon + 1, last_colon - first_colon - 1);
164
165 try {
166 int port_int = std::stoi(spec.substr(last_colon + 1));
167 if (port_int < 1 || port_int > 65535) {
168 return false;
169 }
170 peer.port = static_cast<uint16_t>(port_int);
171 } catch (const std::exception&) {
172 return false;
173 }
174
175 if (peer.ae_title.empty() || peer.ae_title.length() > 16 || peer.host.empty()) {
176 return false;
177 }
178
179 return true;
180}
181
189bool parse_arguments(int argc, char* argv[], qr_scp_args& args) {
190 if (argc < 3) {
191 return false;
192 }
193
194 // Check for help flag
195 for (int i = 1; i < argc; ++i) {
196 if (std::string(argv[i]) == "--help" || std::string(argv[i]) == "-h") {
197 return false;
198 }
199 }
200
201 // Parse port
202 try {
203 int port_int = std::stoi(argv[1]);
204 if (port_int < 1 || port_int > 65535) {
205 std::cerr << "Error: Port must be between 1 and 65535\n";
206 return false;
207 }
208 args.port = static_cast<uint16_t>(port_int);
209 } catch (const std::exception&) {
210 std::cerr << "Error: Invalid port number '" << argv[1] << "'\n";
211 return false;
212 }
213
214 // Parse AE title
215 args.ae_title = argv[2];
216 if (args.ae_title.length() > 16) {
217 std::cerr << "Error: AE title exceeds 16 characters\n";
218 return false;
219 }
220
221 // Parse optional arguments
222 for (int i = 3; i < argc; ++i) {
223 std::string arg = argv[i];
224
225 if (arg == "--storage-dir" && i + 1 < argc) {
226 args.storage_dir = argv[++i];
227 } else if (arg == "--index-db" && i + 1 < argc) {
228 args.index_db = argv[++i];
229 } else if (arg == "--peer" && i + 1 < argc) {
230 peer_config peer;
231 if (!parse_peer(argv[++i], peer)) {
232 std::cerr << "Error: Invalid peer format. Use AE:host:port\n";
233 return false;
234 }
235 args.peers.push_back(peer);
236 } else if (arg == "--max-assoc" && i + 1 < argc) {
237 try {
238 int val = std::stoi(argv[++i]);
239 if (val < 1) {
240 std::cerr << "Error: max-assoc must be positive\n";
241 return false;
242 }
243 args.max_associations = static_cast<size_t>(val);
244 } catch (const std::exception&) {
245 std::cerr << "Error: Invalid max-assoc value\n";
246 return false;
247 }
248 } else if (arg == "--timeout" && i + 1 < argc) {
249 try {
250 int val = std::stoi(argv[++i]);
251 if (val < 0) {
252 std::cerr << "Error: timeout cannot be negative\n";
253 return false;
254 }
255 args.idle_timeout = static_cast<uint32_t>(val);
256 } catch (const std::exception&) {
257 std::cerr << "Error: Invalid timeout value\n";
258 return false;
259 }
260 } else if (arg == "--scan-only") {
261 args.scan_only = true;
262 } else {
263 std::cerr << "Error: Unknown option '" << arg << "'\n";
264 return false;
265 }
266 }
267
268 // Validate required arguments
269 if (args.storage_dir.empty()) {
270 std::cerr << "Error: --storage-dir is required\n";
271 return false;
272 }
273
274 return true;
275}
276
283std::string current_timestamp() {
284 auto now = std::chrono::system_clock::now();
285 auto time_t_now = std::chrono::system_clock::to_time_t(now);
286 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
287 now.time_since_epoch()) % 1000;
288
289 std::tm tm_buf{};
290#ifdef _WIN32
291 localtime_s(&tm_buf, &time_t_now);
292#else
293 localtime_r(&time_t_now, &tm_buf);
294#endif
295
296 std::ostringstream oss;
297 oss << std::put_time(&tm_buf, "%Y-%m-%d %H:%M:%S");
298 oss << '.' << std::setfill('0') << std::setw(3) << ms.count();
299 return oss.str();
300}
301
305std::string format_bytes(size_t bytes) {
306 const char* units[] = {"B", "KB", "MB", "GB", "TB"};
307 int unit_index = 0;
308 double size = static_cast<double>(bytes);
309
310 while (size >= 1024.0 && unit_index < 4) {
311 size /= 1024.0;
312 ++unit_index;
313 }
314
315 std::ostringstream oss;
316 if (unit_index == 0) {
317 oss << bytes << " " << units[unit_index];
318 } else {
319 oss << std::fixed << std::setprecision(2) << size << " " << units[unit_index];
320 }
321 return oss.str();
322}
323
330size_t scan_storage(
331 const std::filesystem::path& storage_dir,
333
334 using namespace kcenon::pacs::core;
335 namespace fs = std::filesystem;
336
337 size_t count = 0;
338 size_t errors = 0;
339
340 std::cout << "Scanning " << storage_dir << "...\n";
341
342 for (const auto& entry : fs::recursive_directory_iterator(storage_dir)) {
343 if (!entry.is_regular_file()) {
344 continue;
345 }
346
347 // Try common DICOM extensions
348 auto ext = entry.path().extension().string();
349 bool is_dcm = ext == ".dcm" || ext == ".DCM" || ext.empty();
350 if (!is_dcm) {
351 continue;
352 }
353
354 try {
355 auto file = dicom_file::open(entry.path());
356 if (file.is_err()) {
357 ++errors;
358 continue;
359 }
360
361 const auto& dataset = file.value().dataset();
362
363 // Extract patient info
364 auto patient_id = dataset.get_string(tags::patient_id, "");
365 auto patient_name = dataset.get_string(tags::patient_name, "");
366 auto birth_date = dataset.get_string(tags::patient_birth_date, "");
367 auto sex = dataset.get_string(tags::patient_sex, "");
368
369 // Insert/update patient
370 auto patient_pk = db.upsert_patient(patient_id, patient_name, birth_date, sex);
371 if (patient_pk.is_err()) {
372 ++errors;
373 continue;
374 }
375
376 // Extract study info
377 auto study_uid = dataset.get_string(tags::study_instance_uid, "");
378 auto study_id = dataset.get_string(tags::study_id, "");
379 auto study_date = dataset.get_string(tags::study_date, "");
380 auto study_time = dataset.get_string(tags::study_time, "");
381 auto accession = dataset.get_string(tags::accession_number, "");
382 auto ref_phys = dataset.get_string(tags::referring_physician_name, "");
383 auto study_desc = dataset.get_string(tags::study_description, "");
384
385 // Insert/update study
386 auto study_pk = db.upsert_study(
387 patient_pk.value(), study_uid, study_id, study_date, study_time,
388 accession, ref_phys, study_desc);
389 if (study_pk.is_err()) {
390 ++errors;
391 continue;
392 }
393
394 // Extract series info
395 auto series_uid = dataset.get_string(tags::series_instance_uid, "");
396 auto modality = dataset.get_string(tags::modality, "");
397 auto series_num_str = dataset.get_string(tags::series_number, "");
398 auto series_desc = dataset.get_string(tags::series_description, "");
399 auto body_part = std::string{}; // body_part_examined not in tag constants
400 auto station = dataset.get_string(tags::station_name, "");
401
402 std::optional<int> series_num;
403 if (!series_num_str.empty()) {
404 try {
405 series_num = std::stoi(series_num_str);
406 } catch (...) {}
407 }
408
409 // Insert/update series
410 auto series_pk = db.upsert_series(
411 study_pk.value(), series_uid, modality, series_num,
412 series_desc, body_part, station);
413 if (series_pk.is_err()) {
414 ++errors;
415 continue;
416 }
417
418 // Extract instance info
419 auto sop_uid = dataset.get_string(tags::sop_instance_uid, "");
420 auto sop_class = dataset.get_string(tags::sop_class_uid, "");
421 auto inst_num_str = dataset.get_string(tags::instance_number, "");
422 auto transfer_syntax_uid = file.value().transfer_syntax().uid();
423
424 std::optional<int> inst_num;
425 if (!inst_num_str.empty()) {
426 try {
427 inst_num = std::stoi(inst_num_str);
428 } catch (...) {}
429 }
430
431 auto file_size = static_cast<int64_t>(fs::file_size(entry.path()));
432
433 // Insert/update instance
434 auto instance_pk = db.upsert_instance(
435 series_pk.value(), sop_uid, sop_class,
436 entry.path().string(), file_size, transfer_syntax_uid, inst_num);
437
438 if (instance_pk.is_ok()) {
439 ++count;
440 if (count % 100 == 0) {
441 std::cout << " Indexed " << count << " files...\n";
442 }
443 } else {
444 ++errors;
445 }
446 } catch (const std::exception& e) {
447 ++errors;
448 }
449 }
450
451 std::cout << "Scan complete: " << count << " files indexed";
452 if (errors > 0) {
453 std::cout << " (" << errors << " errors)";
454 }
455 std::cout << "\n";
456
457 return count;
458}
459
463std::vector<kcenon::pacs::core::dicom_dataset> handle_query(
465 const kcenon::pacs::core::dicom_dataset& query_keys,
466 [[maybe_unused]] const std::string& calling_ae,
468
469 using namespace kcenon::pacs::core;
470 using namespace kcenon::pacs::storage;
471 using namespace kcenon::pacs::services;
472 using namespace kcenon::pacs::encoding;
473
474 std::vector<dicom_dataset> results;
475
476 try {
477 switch (level) {
478 case query_level::patient: {
479 patient_query pq;
480 pq.patient_id = query_keys.get_string(tags::patient_id, "");
481 pq.patient_name = query_keys.get_string(tags::patient_name, "");
482
483 auto patients = db.search_patients(pq);
484 if (patients.is_ok()) {
485 for (const auto& p : patients.value()) {
486 dicom_dataset ds;
487 ds.set_string(tags::query_retrieve_level, vr_type::CS, "PATIENT");
488 ds.set_string(tags::patient_id, vr_type::LO, p.patient_id);
489 ds.set_string(tags::patient_name, vr_type::PN, p.patient_name);
490 ds.set_string(tags::patient_birth_date, vr_type::DA, p.birth_date);
491 ds.set_string(tags::patient_sex, vr_type::CS, p.sex);
492 results.push_back(std::move(ds));
493 }
494 }
495 break;
496 }
497
498 case query_level::study: {
499 study_query sq;
500 sq.patient_id = query_keys.get_string(tags::patient_id, "");
501 sq.patient_name = query_keys.get_string(tags::patient_name, "");
502 sq.study_uid = query_keys.get_string(tags::study_instance_uid, "");
503 sq.study_date = query_keys.get_string(tags::study_date, "");
504 sq.accession_number = query_keys.get_string(tags::accession_number, "");
505 sq.study_description = query_keys.get_string(tags::study_description, "");
506
507 auto studies = db.search_studies(sq);
508 if (studies.is_ok()) {
509 for (const auto& s : studies.value()) {
510 // Get patient info
511 auto patient = db.find_patient_by_pk(s.patient_pk);
512
513 dicom_dataset ds;
514 ds.set_string(tags::query_retrieve_level, vr_type::CS, "STUDY");
515 if (patient) {
516 ds.set_string(tags::patient_id, vr_type::LO, patient->patient_id);
517 ds.set_string(tags::patient_name, vr_type::PN, patient->patient_name);
518 ds.set_string(tags::patient_birth_date, vr_type::DA, patient->birth_date);
519 ds.set_string(tags::patient_sex, vr_type::CS, patient->sex);
520 }
521 ds.set_string(tags::study_instance_uid, vr_type::UI, s.study_uid);
522 ds.set_string(tags::study_id, vr_type::SH, s.study_id);
523 ds.set_string(tags::study_date, vr_type::DA, s.study_date);
524 ds.set_string(tags::study_time, vr_type::TM, s.study_time);
525 ds.set_string(tags::accession_number, vr_type::SH, s.accession_number);
526 ds.set_string(tags::referring_physician_name, vr_type::PN, s.referring_physician);
527 ds.set_string(tags::study_description, vr_type::LO, s.study_description);
528 ds.set_string(tags::modalities_in_study, vr_type::CS, s.modalities_in_study);
529 results.push_back(std::move(ds));
530 }
531 }
532 break;
533 }
534
535 case query_level::series: {
536 series_query serq;
537 serq.study_uid = query_keys.get_string(tags::study_instance_uid, "");
538 serq.series_uid = query_keys.get_string(tags::series_instance_uid, "");
539 serq.modality = query_keys.get_string(tags::modality, "");
540 serq.series_description = query_keys.get_string(tags::series_description, "");
541
542 auto series_list = db.search_series(serq);
543 if (series_list.is_ok()) {
544 for (const auto& ser : series_list.value()) {
545 // Get study info
546 auto study = db.find_study_by_pk(ser.study_pk);
547
548 dicom_dataset ds;
549 ds.set_string(tags::query_retrieve_level, vr_type::CS, "SERIES");
550 if (study) {
551 ds.set_string(tags::study_instance_uid, vr_type::UI, study->study_uid);
552 }
553 ds.set_string(tags::series_instance_uid, vr_type::UI, ser.series_uid);
554 ds.set_string(tags::modality, vr_type::CS, ser.modality);
555 if (ser.series_number.has_value()) {
556 ds.set_string(tags::series_number, vr_type::IS,
557 std::to_string(ser.series_number.value()));
558 }
559 ds.set_string(tags::series_description, vr_type::LO, ser.series_description);
560 // body_part_examined tag not in constants, skip
561 results.push_back(std::move(ds));
562 }
563 }
564 break;
565 }
566
567 case query_level::image: {
569 iq.series_uid = query_keys.get_string(tags::series_instance_uid, "");
570 iq.sop_uid = query_keys.get_string(tags::sop_instance_uid, "");
571 iq.sop_class_uid = query_keys.get_string(tags::sop_class_uid, "");
572
573 auto instances = db.search_instances(iq);
574 if (instances.is_ok()) {
575 for (const auto& inst : instances.value()) {
576 // Get series info
577 auto series = db.find_series_by_pk(inst.series_pk);
578
579 dicom_dataset ds;
580 ds.set_string(tags::query_retrieve_level, vr_type::CS, "IMAGE");
581 if (series) {
582 ds.set_string(tags::series_instance_uid, vr_type::UI, series->series_uid);
583
584 // Get study info
585 auto study = db.find_study_by_pk(series->study_pk);
586 if (study) {
587 ds.set_string(tags::study_instance_uid, vr_type::UI, study->study_uid);
588 }
589 }
590 ds.set_string(tags::sop_instance_uid, vr_type::UI, inst.sop_uid);
591 ds.set_string(tags::sop_class_uid, vr_type::UI, inst.sop_class_uid);
592 if (inst.instance_number.has_value()) {
593 ds.set_string(tags::instance_number, vr_type::IS,
594 std::to_string(inst.instance_number.value()));
595 }
596 results.push_back(std::move(ds));
597 }
598 }
599 break;
600 }
601 }
602 } catch (const std::exception& e) {
603 std::cerr << "[" << current_timestamp() << "] Query error: " << e.what() << "\n";
604 }
605
606 return results;
607}
608
612std::vector<kcenon::pacs::core::dicom_file> handle_retrieve(
613 const kcenon::pacs::core::dicom_dataset& query_keys,
615
616 using namespace kcenon::pacs::core;
617 using namespace kcenon::pacs::storage;
618
619 std::vector<dicom_file> files;
620
621 try {
622 // Determine query level from keys
623 auto sop_uid = query_keys.get_string(tags::sop_instance_uid, "");
624 auto series_uid = query_keys.get_string(tags::series_instance_uid, "");
625 auto study_uid = query_keys.get_string(tags::study_instance_uid, "");
626 auto patient_id = query_keys.get_string(tags::patient_id, "");
627
628 std::vector<instance_record> instances;
629
630 if (!sop_uid.empty()) {
631 // Instance level retrieve
632 auto inst = db.find_instance(sop_uid);
633 if (inst) {
634 instances.push_back(*inst);
635 }
636 } else if (!series_uid.empty()) {
637 // Series level retrieve
638 auto result = db.list_instances(series_uid);
639 if (result.is_ok()) {
640 instances = std::move(result.value());
641 }
642 } else if (!study_uid.empty()) {
643 // Study level retrieve - get all series first
644 auto series_result = db.list_series(study_uid);
645 if (series_result.is_ok()) {
646 for (const auto& ser : series_result.value()) {
647 auto inst_result = db.list_instances(ser.series_uid);
648 if (inst_result.is_ok()) {
649 for (auto& inst : inst_result.value()) {
650 instances.push_back(std::move(inst));
651 }
652 }
653 }
654 }
655 } else if (!patient_id.empty()) {
656 // Patient level retrieve - get all studies first
657 auto studies_result = db.list_studies(patient_id);
658 if (studies_result.is_ok()) {
659 for (const auto& study : studies_result.value()) {
660 auto series_result = db.list_series(study.study_uid);
661 if (series_result.is_ok()) {
662 for (const auto& ser : series_result.value()) {
663 auto inst_result = db.list_instances(ser.series_uid);
664 if (inst_result.is_ok()) {
665 for (auto& inst : inst_result.value()) {
666 instances.push_back(std::move(inst));
667 }
668 }
669 }
670 }
671 }
672 }
673 }
674
675 // Load files
676 for (const auto& inst : instances) {
677 auto file = dicom_file::open(inst.file_path);
678 if (file.is_ok()) {
679 files.push_back(std::move(file.value()));
680 }
681 }
682 } catch (const std::exception& e) {
683 std::cerr << "[" << current_timestamp() << "] Retrieve error: " << e.what() << "\n";
684 }
685
686 return files;
687}
688
694bool run_server(const qr_scp_args& args) {
695 using namespace kcenon::pacs::network;
696 using namespace kcenon::pacs::services;
697 using namespace kcenon::pacs::storage;
698 using namespace kcenon::pacs::core;
699
700 std::cout << "\nStarting Query/Retrieve SCP...\n";
701 std::cout << " AE Title: " << args.ae_title << "\n";
702 std::cout << " Port: " << args.port << "\n";
703 std::cout << " Storage Directory: " << args.storage_dir << "\n";
704 if (!args.index_db.empty()) {
705 std::cout << " Index Database: " << args.index_db << "\n";
706 } else {
707 std::cout << " Index Database: (in-memory)\n";
708 }
709 std::cout << " Max Associations: " << args.max_associations << "\n";
710 std::cout << " Idle Timeout: " << args.idle_timeout << " seconds\n";
711 if (!args.peers.empty()) {
712 std::cout << " Known Peers:\n";
713 for (const auto& peer : args.peers) {
714 std::cout << " - " << peer.ae_title << " -> "
715 << peer.host << ":" << peer.port << "\n";
716 }
717 }
718 std::cout << "\n";
719
720 // Verify storage directory exists
721 std::error_code ec;
722 if (!std::filesystem::exists(args.storage_dir, ec)) {
723 std::cerr << "Error: Storage directory does not exist: " << args.storage_dir << "\n";
724 return false;
725 }
726
727 // Open index database
728 std::string db_path = args.index_db.empty() ? ":memory:" : args.index_db.string();
729 auto db_result = index_database::open(db_path);
730 if (db_result.is_err()) {
731 std::cerr << "Failed to open database: " << db_result.error().message << "\n";
732 return false;
733 }
734 auto db = std::move(db_result.value());
735
736 // Scan storage and build index
737 auto indexed = scan_storage(args.storage_dir, *db);
738
739 if (args.scan_only) {
740 std::cout << "\nScan complete. Exiting.\n";
741 return true;
742 }
743
744 if (indexed == 0) {
745 std::cout << "\nWarning: No DICOM files found in storage directory.\n";
746 std::cout << " Server will start but queries will return no results.\n\n";
747 }
748
749 // Build peer map for destination resolution
750 std::map<std::string, std::pair<std::string, uint16_t>> peer_map;
751 for (const auto& peer : args.peers) {
752 peer_map[peer.ae_title] = {peer.host, peer.port};
753 }
754
755 // Configure server
756 server_config config;
757 config.ae_title = args.ae_title;
758 config.port = args.port;
759 config.max_associations = args.max_associations;
760 config.idle_timeout = std::chrono::seconds{args.idle_timeout};
761 config.implementation_class_uid = "1.2.826.0.1.3680043.2.1545.1";
762 config.implementation_version_name = "QR_SCP_001";
763
764 // Create server
765 dicom_server server{config};
766 g_server = &server;
767
768 // Register Verification service (C-ECHO)
769 server.register_service(std::make_shared<verification_scp>());
770
771 // Configure Query SCP
772 auto query_service = std::make_shared<query_scp>();
773 query_service->set_handler(
774 [&db](query_level level, const dicom_dataset& keys, const std::string& ae) {
775 return handle_query(level, keys, ae, *db);
776 });
777
778 server.register_service(query_service);
779
780 // Configure Retrieve SCP
781 auto retrieve_service = std::make_shared<retrieve_scp>();
782 retrieve_service->set_retrieve_handler(
783 [&db](const dicom_dataset& keys) {
784 return handle_retrieve(keys, *db);
785 });
786
787 // Set destination resolver for C-MOVE
788 retrieve_service->set_destination_resolver(
789 [&peer_map](const std::string& ae_title)
790 -> std::optional<std::pair<std::string, uint16_t>> {
791 auto it = peer_map.find(ae_title);
792 if (it != peer_map.end()) {
793 return it->second;
794 }
795 return std::nullopt;
796 });
797
798 server.register_service(retrieve_service);
799
800 // Set up callbacks for logging
801 server.on_association_established([](const association& assoc) {
802 std::cout << "[" << current_timestamp() << "] "
803 << "Association established from: " << assoc.calling_ae()
804 << " -> " << assoc.called_ae() << "\n";
805 });
806
807 server.on_association_released([](const association& assoc) {
808 std::cout << "[" << current_timestamp() << "] "
809 << "Association released: " << assoc.calling_ae() << "\n";
810 });
811
812 server.on_error([](const std::string& error) {
813 std::cerr << "[" << current_timestamp() << "] "
814 << "Error: " << error << "\n";
815 });
816
817 // Start server
818 auto result = server.start();
819 if (result.is_err()) {
820 std::cerr << "Failed to start server: " << result.error().message << "\n";
821 g_server = nullptr;
822 return false;
823 }
824
825 std::cout << "=================================================\n";
826 std::cout << " Query/Retrieve SCP is running on port " << args.port << "\n";
827 std::cout << " Storage: " << args.storage_dir << "\n";
828 std::cout << " Indexed: " << indexed << " DICOM files\n";
829 std::cout << " Press Ctrl+C to stop\n";
830 std::cout << "=================================================\n\n";
831
832 // Wait for shutdown
833 server.wait_for_shutdown();
834
835 // Print final statistics
836 auto server_stats = server.get_statistics();
837
838 std::cout << "\n";
839 std::cout << "=================================================\n";
840 std::cout << " Server Statistics\n";
841 std::cout << "=================================================\n";
842 std::cout << " Total Associations: " << server_stats.total_associations << "\n";
843 std::cout << " Rejected Associations: " << server_stats.rejected_associations << "\n";
844 std::cout << " Messages Processed: " << server_stats.messages_processed << "\n";
845 std::cout << " Queries Processed: " << query_service->queries_processed() << "\n";
846 std::cout << " C-MOVE Operations: " << retrieve_service->move_operations() << "\n";
847 std::cout << " C-GET Operations: " << retrieve_service->get_operations() << "\n";
848 std::cout << " Images Transferred: " << retrieve_service->images_transferred() << "\n";
849 std::cout << " Bytes Received: " << format_bytes(server_stats.bytes_received) << "\n";
850 std::cout << " Bytes Sent: " << format_bytes(server_stats.bytes_sent) << "\n";
851 std::cout << " Uptime: " << server_stats.uptime().count() << " seconds\n";
852 std::cout << "=================================================\n";
853
854 g_server = nullptr;
855 return true;
856}
857
858} // namespace
859
860int main(int argc, char* argv[]) {
861 std::cout << R"(
862 ___ ____ ____ ____ ____
863 / _ \| _ \ / ___| / ___| _ \
864 | | | | |_) | \___ \| | | |_) |
865 | |_| | _ < ___) | |___| __/
866 \__\_\_| \_\ |____/ \____|_|
867
868 DICOM Query/Retrieve Server
869)" << "\n";
870
871 qr_scp_args args;
872
873 if (!parse_arguments(argc, argv, args)) {
874 print_usage(argv[0]);
875 return 1;
876 }
877
878 // Install signal handlers
879 install_signal_handlers();
880
881 bool success = run_server(args);
882
883 std::cout << "\nQuery/Retrieve SCP terminated\n";
884 return success ? 0 : 1;
885}
auto get_string(dicom_tag tag, std::string_view default_value="") const -> std::string
Get the string value of an element.
auto find_patient_by_pk(int64_t pk) const -> std::optional< patient_record >
Find a patient by primary key.
auto find_study_by_pk(int64_t pk) const -> std::optional< study_record >
Find a study by primary key.
auto list_instances(std::string_view series_uid) const -> Result< std::vector< instance_record > >
List all instances for a series.
auto list_series(std::string_view study_uid) const -> Result< std::vector< series_record > >
List all series for a study.
auto find_series_by_pk(int64_t pk) const -> std::optional< series_record >
Find a series by primary key.
auto upsert_instance(int64_t series_pk, std::string_view sop_uid, std::string_view sop_class_uid, std::string_view file_path, int64_t file_size, std::string_view transfer_syntax="", std::optional< int > instance_number=std::nullopt) -> Result< int64_t >
Insert or update an instance record.
auto search_studies(const study_query &query) const -> Result< std::vector< study_record > >
Search studies with query criteria.
auto upsert_patient(std::string_view patient_id, std::string_view patient_name="", std::string_view birth_date="", std::string_view sex="") -> Result< int64_t >
Insert or update a patient record.
auto upsert_series(int64_t study_pk, std::string_view series_uid, std::string_view modality="", std::optional< int > series_number=std::nullopt, std::string_view series_description="", std::string_view body_part_examined="", std::string_view station_name="") -> Result< int64_t >
Insert or update a series record.
auto list_studies(std::string_view patient_id) const -> Result< std::vector< study_record > >
List all studies for a patient.
auto search_instances(const instance_query &query) const -> Result< std::vector< instance_record > >
Search instances with query criteria.
auto search_series(const series_query &query) const -> Result< std::vector< series_record > >
Search series with query criteria.
auto find_instance(std::string_view sop_uid) const -> std::optional< instance_record >
Find an instance by SOP Instance UID.
auto search_patients(const patient_query &query) const -> Result< std::vector< patient_record > >
Search patients with query criteria.
auto upsert_study(int64_t patient_pk, std::string_view study_uid, std::string_view study_id="", std::string_view study_date="", std::string_view study_time="", std::string_view accession_number="", std::string_view referring_physician="", std::string_view study_description="") -> Result< int64_t >
Insert or update a study record.
DICOM Dataset - ordered collection of Data Elements.
DICOM Part 10 file handling for reading/writing DICOM files.
Multi-threaded DICOM server for handling multiple associations.
Compile-time constants for commonly used DICOM tags.
Filesystem-based DICOM storage with hierarchical organization.
PACS index database for metadata storage and retrieval.
int main()
Definition main.cpp:84
@ error
Node returned an error.
@ body_part
(0018,0015) Body Part Examined
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag modality
Modality.
constexpr dicom_tag study_time
Study Time.
constexpr dicom_tag transfer_syntax_uid
Transfer Syntax UID.
constexpr dicom_tag study_id
Study ID.
constexpr dicom_tag patient_name
Patient's Name.
constexpr dicom_tag study_date
Study Date.
query_level
DICOM Query/Retrieve level enumeration.
Definition query_scp.h:63
DICOM Query SCP service (C-FIND handler)
DICOM Retrieve SCP service (C-MOVE/C-GET handler)
DICOM Server configuration structures.
std::optional< std::string > series_uid
Series Instance UID for filtering by series (exact match)
std::optional< std::string > sop_uid
SOP Instance UID (exact match)
std::optional< std::string > sop_class_uid
SOP Class UID filter (exact match)
std::optional< std::string > patient_name
Patient name pattern (supports * wildcard)
std::optional< std::string > patient_id
Patient ID pattern (supports * wildcard)
std::optional< std::string > series_description
Series description pattern (supports * wildcard)
std::optional< std::string > study_uid
Study Instance UID for filtering by study (exact match)
std::optional< std::string > series_uid
Series Instance UID (exact match)
std::optional< std::string > modality
Modality filter (exact match, e.g., "CT", "MR")
std::optional< std::string > study_uid
Study Instance UID (exact match)
std::optional< std::string > accession_number
Accession number pattern (supports * wildcard)
std::optional< std::string > study_date
Study date (exact match, format: YYYYMMDD)
std::optional< std::string > patient_name
Patient name pattern (supports * wildcard)
std::optional< std::string > study_description
Study description pattern (supports * wildcard)
std::optional< std::string > patient_id
Patient ID for filtering by patient (exact match or wildcard)
DICOM Verification SCP service (C-ECHO handler)