PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1
28
29#include <atomic>
30#include <chrono>
31#include <csignal>
32#include <cstdlib>
33#include <filesystem>
34#include <iomanip>
35#include <iostream>
36#include <map>
37#include <sstream>
38#include <string>
39#include <vector>
40
41namespace {
42
44std::atomic<kcenon::pacs::network::dicom_server*> g_server{nullptr};
45
47std::atomic<bool> g_running{true};
48
50std::atomic<kcenon::pacs::storage::file_storage*> g_file_storage{nullptr};
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"(
83Storage SCP - DICOM Image Receiver
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 to store received DICOM files
93
94Optional Options:
95 --index-db <path> SQLite database for indexing (optional)
96 --accept <modalities> Comma-separated list of accepted modalities
97 (CT,MR,US,XR,CR,DX,NM,PT,SC,SR)
98 --naming <scheme> File naming scheme: hierarchical (default),
99 date, flat
100 --duplicate <policy> Duplicate handling: reject (default), replace, ignore
101 --max-assoc <n> Maximum concurrent associations (default: 10)
102 --timeout <sec> Idle timeout in seconds (default: 300)
103 --help Show this help message
104
105Examples:
106 )" << program_name << R"( 11112 MY_PACS --storage-dir ./received
107 )" << program_name << R"( 11112 MY_PACS --storage-dir ./received --index-db ./pacs.db
108 )" << program_name << R"( 11112 MY_PACS --storage-dir ./archive --accept "CT,MR"
109
110Notes:
111 - Press Ctrl+C to stop the server gracefully
112 - Files are stored in hierarchical structure: StudyUID/SeriesUID/SOPUID.dcm
113 - Without --accept, all standard storage SOP classes are accepted
114
115Exit Codes:
116 0 Normal termination
117 1 Error - Failed to start server or invalid arguments
118)";
119}
120
124struct store_scp_args {
125 uint16_t port = 0;
126 std::string ae_title;
127 std::filesystem::path storage_dir;
128 std::filesystem::path index_db;
129 std::vector<std::string> accepted_modalities;
132 size_t max_associations = 10;
133 uint32_t idle_timeout = 300;
134};
135
139std::vector<std::string> modality_to_sop_classes(const std::string& modality) {
140 static const std::map<std::string, std::vector<std::string>> modality_map = {
141 {"CT", {"1.2.840.10008.5.1.4.1.1.2", "1.2.840.10008.5.1.4.1.1.2.1"}},
142 {"MR", {"1.2.840.10008.5.1.4.1.1.4", "1.2.840.10008.5.1.4.1.1.4.1"}},
143 {"US", {"1.2.840.10008.5.1.4.1.1.6.1"}},
144 {"CR", {"1.2.840.10008.5.1.4.1.1.1"}},
145 {"DX", {"1.2.840.10008.5.1.4.1.1.1.1", "1.2.840.10008.5.1.4.1.1.1.1.1"}},
146 {"XR", {"1.2.840.10008.5.1.4.1.1.12.1", "1.2.840.10008.5.1.4.1.1.12.2"}},
147 {"NM", {"1.2.840.10008.5.1.4.1.1.20"}},
148 {"PT", {"1.2.840.10008.5.1.4.1.1.128", "1.2.840.10008.5.1.4.1.1.130"}},
149 {"SC", {"1.2.840.10008.5.1.4.1.1.7"}},
150 {"SR", {"1.2.840.10008.5.1.4.1.1.88.11", "1.2.840.10008.5.1.4.1.1.88.22",
151 "1.2.840.10008.5.1.4.1.1.88.33"}}
152 };
153
154 auto it = modality_map.find(modality);
155 if (it != modality_map.end()) {
156 return it->second;
157 }
158 return {};
159}
160
164std::vector<std::string> parse_modalities(const std::string& input) {
165 std::vector<std::string> sop_classes;
166 std::istringstream ss(input);
167 std::string modality;
168
169 while (std::getline(ss, modality, ',')) {
170 // Trim whitespace
171 modality.erase(0, modality.find_first_not_of(" \t"));
172 modality.erase(modality.find_last_not_of(" \t") + 1);
173
174 // Convert to uppercase
175 for (auto& c : modality) {
176 c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
177 }
178
179 auto classes = modality_to_sop_classes(modality);
180 sop_classes.insert(sop_classes.end(), classes.begin(), classes.end());
181 }
182
183 return sop_classes;
184}
185
193bool parse_arguments(int argc, char* argv[], store_scp_args& args) {
194 if (argc < 3) {
195 return false;
196 }
197
198 // Check for help flag
199 for (int i = 1; i < argc; ++i) {
200 if (std::string(argv[i]) == "--help" || std::string(argv[i]) == "-h") {
201 return false;
202 }
203 }
204
205 // Parse port
206 try {
207 int port_int = std::stoi(argv[1]);
208 if (port_int < 1 || port_int > 65535) {
209 std::cerr << "Error: Port must be between 1 and 65535\n";
210 return false;
211 }
212 args.port = static_cast<uint16_t>(port_int);
213 } catch (const std::exception&) {
214 std::cerr << "Error: Invalid port number '" << argv[1] << "'\n";
215 return false;
216 }
217
218 // Parse AE title
219 args.ae_title = argv[2];
220 if (args.ae_title.length() > 16) {
221 std::cerr << "Error: AE title exceeds 16 characters\n";
222 return false;
223 }
224
225 // Parse optional arguments
226 for (int i = 3; i < argc; ++i) {
227 std::string arg = argv[i];
228
229 if (arg == "--storage-dir" && i + 1 < argc) {
230 args.storage_dir = argv[++i];
231 } else if (arg == "--index-db" && i + 1 < argc) {
232 args.index_db = argv[++i];
233 } else if (arg == "--accept" && i + 1 < argc) {
234 args.accepted_modalities = parse_modalities(argv[++i]);
235 } else if (arg == "--naming" && i + 1 < argc) {
236 std::string scheme = argv[++i];
237 if (scheme == "hierarchical") {
239 } else if (scheme == "date") {
241 } else if (scheme == "flat") {
243 } else {
244 std::cerr << "Error: Unknown naming scheme '" << scheme << "'\n";
245 return false;
246 }
247 } else if (arg == "--duplicate" && i + 1 < argc) {
248 std::string policy = argv[++i];
249 if (policy == "reject") {
251 } else if (policy == "replace") {
253 } else if (policy == "ignore") {
255 } else {
256 std::cerr << "Error: Unknown duplicate policy '" << policy << "'\n";
257 return false;
258 }
259 } else if (arg == "--max-assoc" && i + 1 < argc) {
260 try {
261 int val = std::stoi(argv[++i]);
262 if (val < 1) {
263 std::cerr << "Error: max-assoc must be positive\n";
264 return false;
265 }
266 args.max_associations = static_cast<size_t>(val);
267 } catch (const std::exception&) {
268 std::cerr << "Error: Invalid max-assoc value\n";
269 return false;
270 }
271 } else if (arg == "--timeout" && i + 1 < argc) {
272 try {
273 int val = std::stoi(argv[++i]);
274 if (val < 0) {
275 std::cerr << "Error: timeout cannot be negative\n";
276 return false;
277 }
278 args.idle_timeout = static_cast<uint32_t>(val);
279 } catch (const std::exception&) {
280 std::cerr << "Error: Invalid timeout value\n";
281 return false;
282 }
283 } else {
284 std::cerr << "Error: Unknown option '" << arg << "'\n";
285 return false;
286 }
287 }
288
289 // Validate required arguments
290 if (args.storage_dir.empty()) {
291 std::cerr << "Error: --storage-dir is required\n";
292 return false;
293 }
294
295 return true;
296}
297
302std::string current_timestamp() {
303 auto now = std::chrono::system_clock::now();
304 auto time_t_now = std::chrono::system_clock::to_time_t(now);
305 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
306 now.time_since_epoch()) % 1000;
307
308 std::ostringstream oss;
309 oss << std::put_time(std::localtime(&time_t_now), "%Y-%m-%d %H:%M:%S");
310 oss << '.' << std::setfill('0') << std::setw(3) << ms.count();
311 return oss.str();
312}
313
317std::string format_bytes(size_t bytes) {
318 const char* units[] = {"B", "KB", "MB", "GB", "TB"};
319 int unit_index = 0;
320 double size = static_cast<double>(bytes);
321
322 while (size >= 1024.0 && unit_index < 4) {
323 size /= 1024.0;
324 ++unit_index;
325 }
326
327 std::ostringstream oss;
328 if (unit_index == 0) {
329 oss << bytes << " " << units[unit_index];
330 } else {
331 oss << std::fixed << std::setprecision(2) << size << " " << units[unit_index];
332 }
333 return oss.str();
334}
335
341bool run_server(const store_scp_args& args) {
342 using namespace kcenon::pacs::network;
343 using namespace kcenon::pacs::services;
344 using namespace kcenon::pacs::storage;
345 using namespace kcenon::pacs::core;
346
347 std::cout << "\nStarting Storage SCP...\n";
348 std::cout << " AE Title: " << args.ae_title << "\n";
349 std::cout << " Port: " << args.port << "\n";
350 std::cout << " Storage Directory: " << args.storage_dir << "\n";
351 if (!args.index_db.empty()) {
352 std::cout << " Index Database: " << args.index_db << "\n";
353 }
354 std::cout << " Max Associations: " << args.max_associations << "\n";
355 std::cout << " Idle Timeout: " << args.idle_timeout << " seconds\n";
356 if (!args.accepted_modalities.empty()) {
357 std::cout << " Accepted Classes: " << args.accepted_modalities.size()
358 << " SOP class(es)\n";
359 } else {
360 std::cout << " Accepted Classes: All standard storage classes\n";
361 }
362 std::cout << "\n";
363
364 // Create storage directory if it doesn't exist
365 std::error_code ec;
366 if (!std::filesystem::exists(args.storage_dir)) {
367 if (!std::filesystem::create_directories(args.storage_dir, ec)) {
368 std::cerr << "Failed to create storage directory: " << ec.message() << "\n";
369 return false;
370 }
371 std::cout << "Created storage directory: " << args.storage_dir << "\n";
372 }
373
374 // Configure file storage
375 file_storage_config storage_config;
376 storage_config.root_path = args.storage_dir;
377 storage_config.naming = args.naming;
378 storage_config.duplicate = args.duplicate;
379 storage_config.create_directories = true;
380
381 // Create file storage
382 file_storage storage{storage_config};
383 g_file_storage = &storage;
384
385 // Configure server
386 server_config config;
387 config.ae_title = args.ae_title;
388 config.port = args.port;
389 config.max_associations = args.max_associations;
390 config.idle_timeout = std::chrono::seconds{args.idle_timeout};
391 config.implementation_class_uid = "1.2.826.0.1.3680043.2.1545.1";
392 config.implementation_version_name = "STORE_SCP_001";
393
394 // Create server
395 dicom_server server{config};
396 g_server = &server;
397
398 // Configure Storage SCP service
399 storage_scp_config scp_config;
400 if (!args.accepted_modalities.empty()) {
401 scp_config.accepted_sop_classes = args.accepted_modalities;
402 }
403 scp_config.dup_policy =
407
408 auto storage_service = std::make_shared<storage_scp>(scp_config);
409
410 // Set storage handler
411 storage_service->set_handler(
412 [&storage](const dicom_dataset& dataset,
413 const std::string& calling_ae,
414 [[maybe_unused]] const std::string& sop_class_uid,
415 [[maybe_unused]] const std::string& sop_instance_uid) -> storage_status {
416
417 // Log incoming image
418 auto patient_name = dataset.get_string(tags::patient_name, "Unknown");
419 auto study_desc = dataset.get_string(tags::study_description, "");
420 auto modality = dataset.get_string(tags::modality, "??");
421
422 std::cout << "[" << current_timestamp() << "] "
423 << "C-STORE from " << calling_ae << ": "
424 << modality << " - " << patient_name;
425 if (!study_desc.empty()) {
426 std::cout << " (" << study_desc << ")";
427 }
428 std::cout << "\n";
429
430 // Store the dataset
431 auto result = storage.store(dataset);
432 if (result.is_err()) {
433 std::cerr << "[" << current_timestamp() << "] "
434 << "Storage failed: " << result.error().message << "\n";
435 return storage_status::out_of_resources;
436 }
437
438 return storage_status::success;
439 });
440
441 // Optional: pre-store validation
442 storage_service->set_pre_store_handler([](const dicom_dataset& dataset) {
443 // Validate required attributes
444 if (!dataset.contains(tags::study_instance_uid) ||
445 !dataset.contains(tags::series_instance_uid) ||
446 !dataset.contains(tags::sop_instance_uid)) {
447 std::cerr << "[" << current_timestamp() << "] "
448 << "Rejected: Missing required UID attributes\n";
449 return false;
450 }
451 return true;
452 });
453
454 // Register storage service
455 server.register_service(storage_service);
456
457 // Set up callbacks for logging
458 server.on_association_established([](const association& assoc) {
459 std::cout << "[" << current_timestamp() << "] "
460 << "Association established from: " << assoc.calling_ae()
461 << " -> " << assoc.called_ae() << "\n";
462 });
463
464 server.on_association_released([](const association& assoc) {
465 std::cout << "[" << current_timestamp() << "] "
466 << "Association released: " << assoc.calling_ae() << "\n";
467 });
468
469 server.on_error([](const std::string& error) {
470 std::cerr << "[" << current_timestamp() << "] "
471 << "Error: " << error << "\n";
472 });
473
474 // Start server
475 auto result = server.start();
476 if (result.is_err()) {
477 std::cerr << "Failed to start server: " << result.error().message << "\n";
478 g_server = nullptr;
479 g_file_storage = nullptr;
480 return false;
481 }
482
483 std::cout << "=================================================\n";
484 std::cout << " Storage SCP is running on port " << args.port << "\n";
485 std::cout << " Storage: " << args.storage_dir << "\n";
486 std::cout << " Press Ctrl+C to stop\n";
487 std::cout << "=================================================\n\n";
488
489 // Wait for shutdown
490 server.wait_for_shutdown();
491
492 // Print final statistics
493 auto server_stats = server.get_statistics();
494 auto storage_stats = storage.get_statistics();
495
496 std::cout << "\n";
497 std::cout << "=================================================\n";
498 std::cout << " Server Statistics\n";
499 std::cout << "=================================================\n";
500 std::cout << " Total Associations: " << server_stats.total_associations << "\n";
501 std::cout << " Rejected Associations: " << server_stats.rejected_associations << "\n";
502 std::cout << " Messages Processed: " << server_stats.messages_processed << "\n";
503 std::cout << " Images Received: " << storage_service->images_received() << "\n";
504 std::cout << " Bytes Received: " << format_bytes(storage_service->bytes_received()) << "\n";
505 std::cout << " Uptime: " << server_stats.uptime().count() << " seconds\n";
506 std::cout << "=================================================\n";
507 std::cout << " Storage Statistics\n";
508 std::cout << "=================================================\n";
509 std::cout << " Total Instances: " << storage_stats.total_instances << "\n";
510 std::cout << " Total Size: " << format_bytes(storage_stats.total_bytes) << "\n";
511 std::cout << "=================================================\n";
512
513 g_server = nullptr;
514 g_file_storage = nullptr;
515 return true;
516}
517
518} // namespace
519
520int main(int argc, char* argv[]) {
521 std::cout << R"(
522 ____ _____ ___ ____ _____ ____ ____ ____
523 / ___|_ _/ _ \| _ \| ____| / ___| / ___| _ \
524 \___ \ | || | | | |_) | _| \___ \| | | |_) |
525 ___) || || |_| | _ <| |___ ___) | |___| __/
526 |____/ |_| \___/|_| \_\_____| |____/ \____|_|
527
528 DICOM Image Receiver Server
529)" << "\n";
530
531 store_scp_args args;
532
533 if (!parse_arguments(argc, argv, args)) {
534 print_usage(argv[0]);
535 return 1;
536 }
537
538 // Install signal handlers
539 install_signal_handlers();
540
541 bool success = run_server(args);
542
543 std::cout << "\nStorage SCP terminated\n";
544 return success ? 0 : 1;
545}
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.
int main()
Definition main.cpp:84
@ error
Node returned an error.
constexpr dicom_tag modality
Modality.
constexpr dicom_tag patient_name
Patient's Name.
@ ignore
Silently accept duplicate (return success)
@ reject
Reject duplicates with error status.
@ replace
Replace existing instance with new one.
naming_scheme
Naming scheme for DICOM file organization.
@ flat
{SOPUID}.dcm (flat structure)
@ date_hierarchical
YYYY/MM/DD/{StudyUID}/{SOPUID}.dcm.
@ uid_hierarchical
{StudyUID}/{SeriesUID}/{SOPUID}.dcm
duplicate_policy
Policy for handling duplicate SOP Instance UIDs.
@ ignore
Skip silently if instance exists.
@ reject
Return error if instance already exists.
@ replace
Overwrite existing instance.
DICOM Server configuration structures.
DICOM Storage SCP service (C-STORE handler)
Configuration for SCP to accept associations.
Configuration for file_storage.
bool create_directories
Create directories automatically if they don't exist.
duplicate_policy duplicate
How to handle duplicate instances.
std::filesystem::path root_path
Root directory for storage.
naming_scheme naming
File organization scheme.