PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1
29
30#include <atomic>
31#include <chrono>
32#include <csignal>
33#include <cstdlib>
34#include <filesystem>
35#include <fstream>
36#include <iomanip>
37#include <iostream>
38#include <map>
39#include <mutex>
40#include <sstream>
41#include <string>
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// Command Line Parsing
82// =============================================================================
83
88void print_usage(const char* program_name) {
89 std::cout << R"(
90MPPS SCP - DICOM Modality Performed Procedure Step Server
91
92Usage: )" << program_name << R"( <port> <ae_title> [options]
93
94Arguments:
95 port Port number to listen on (typically 104 or 11112)
96 ae_title Application Entity Title for this server (max 16 chars)
97
98Output Options (optional):
99 --output-dir <path> Directory to store MPPS records as JSON files
100 --output-file <path> Single JSON file to append MPPS records
101
102Server Options:
103 --max-assoc <n> Maximum concurrent associations (default: 10)
104 --timeout <sec> Idle timeout in seconds (default: 300)
105 --help Show this help message
106
107Examples:
108 )" << program_name << R"( 11112 MY_MPPS
109 )" << program_name << R"( 11112 MY_MPPS --output-dir ./mpps_records
110 )" << program_name << R"( 11112 MY_MPPS --output-file ./mpps.json --max-assoc 20
111
112MPPS Protocol:
113 - N-CREATE: Modality starts a procedure (status = IN PROGRESS)
114 - N-SET: Modality completes or discontinues a procedure
115 (status = COMPLETED or DISCONTINUED)
116
117Notes:
118 - Press Ctrl+C to stop the server gracefully
119 - Without output options, MPPS records are logged to console only
120 - Each MPPS instance is identified by its SOP Instance UID
121
122Exit Codes:
123 0 Normal termination
124 1 Error - Failed to start server or invalid arguments
125)";
126}
127
131struct mpps_scp_args {
132 uint16_t port = 0;
133 std::string ae_title;
134 std::filesystem::path output_dir;
135 std::filesystem::path output_file;
136 size_t max_associations = 10;
137 uint32_t idle_timeout = 300;
138};
139
147bool parse_arguments(int argc, char* argv[], mpps_scp_args& args) {
148 if (argc < 3) {
149 return false;
150 }
151
152 // Check for help flag
153 for (int i = 1; i < argc; ++i) {
154 if (std::string(argv[i]) == "--help" || std::string(argv[i]) == "-h") {
155 return false;
156 }
157 }
158
159 // Parse port
160 try {
161 int port_int = std::stoi(argv[1]);
162 if (port_int < 1 || port_int > 65535) {
163 std::cerr << "Error: Port must be between 1 and 65535\n";
164 return false;
165 }
166 args.port = static_cast<uint16_t>(port_int);
167 } catch (const std::exception&) {
168 std::cerr << "Error: Invalid port number '" << argv[1] << "'\n";
169 return false;
170 }
171
172 // Parse AE title
173 args.ae_title = argv[2];
174 if (args.ae_title.length() > 16) {
175 std::cerr << "Error: AE title exceeds 16 characters\n";
176 return false;
177 }
178
179 // Parse optional arguments
180 for (int i = 3; i < argc; ++i) {
181 std::string arg = argv[i];
182
183 if (arg == "--output-dir" && i + 1 < argc) {
184 args.output_dir = argv[++i];
185 } else if (arg == "--output-file" && i + 1 < argc) {
186 args.output_file = argv[++i];
187 } else if (arg == "--max-assoc" && i + 1 < argc) {
188 try {
189 int val = std::stoi(argv[++i]);
190 if (val < 1) {
191 std::cerr << "Error: max-assoc must be positive\n";
192 return false;
193 }
194 args.max_associations = static_cast<size_t>(val);
195 } catch (const std::exception&) {
196 std::cerr << "Error: Invalid max-assoc value\n";
197 return false;
198 }
199 } else if (arg == "--timeout" && i + 1 < argc) {
200 try {
201 int val = std::stoi(argv[++i]);
202 if (val < 0) {
203 std::cerr << "Error: timeout cannot be negative\n";
204 return false;
205 }
206 args.idle_timeout = static_cast<uint32_t>(val);
207 } catch (const std::exception&) {
208 std::cerr << "Error: Invalid timeout value\n";
209 return false;
210 }
211 } else {
212 std::cerr << "Error: Unknown option '" << arg << "'\n";
213 return false;
214 }
215 }
216
217 return true;
218}
219
220// =============================================================================
221// Utility Functions
222// =============================================================================
223
228std::string current_timestamp() {
229 auto now = std::chrono::system_clock::now();
230 auto time_t_now = std::chrono::system_clock::to_time_t(now);
231 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
232 now.time_since_epoch()) % 1000;
233
234 std::tm tm_buf{};
235#ifdef _WIN32
236 localtime_s(&tm_buf, &time_t_now);
237#else
238 localtime_r(&time_t_now, &tm_buf);
239#endif
240
241 std::ostringstream oss;
242 oss << std::put_time(&tm_buf, "%Y-%m-%d %H:%M:%S");
243 oss << '.' << std::setfill('0') << std::setw(3) << ms.count();
244 return oss.str();
245}
246
250std::string json_escape(const std::string& str) {
251 std::string result;
252 result.reserve(str.size());
253 for (char c : str) {
254 switch (c) {
255 case '"': result += "\\\""; break;
256 case '\\': result += "\\\\"; break;
257 case '\n': result += "\\n"; break;
258 case '\r': result += "\\r"; break;
259 case '\t': result += "\\t"; break;
260 default: result += c; break;
261 }
262 }
263 return result;
264}
265
266// =============================================================================
267// MPPS Record Storage
268// =============================================================================
269
273struct mpps_record {
274 std::string sop_instance_uid;
275 std::string status;
276 std::string station_ae;
277 std::string patient_id;
278 std::string patient_name;
279 std::string modality;
280 std::string procedure_step_id;
281 std::string start_date;
282 std::string start_time;
283 std::string end_date;
284 std::string end_time;
285 std::string created_at;
286 std::string updated_at;
287};
288
292std::string to_json(const mpps_record& record, bool pretty = true) {
293 std::ostringstream oss;
294 std::string indent = pretty ? " " : "";
295 std::string nl = pretty ? "\n" : "";
296
297 oss << "{" << nl;
298 oss << indent << "\"sopInstanceUid\": \"" << json_escape(record.sop_instance_uid) << "\"," << nl;
299 oss << indent << "\"status\": \"" << json_escape(record.status) << "\"," << nl;
300 oss << indent << "\"stationAeTitle\": \"" << json_escape(record.station_ae) << "\"," << nl;
301 oss << indent << "\"patientId\": \"" << json_escape(record.patient_id) << "\"," << nl;
302 oss << indent << "\"patientName\": \"" << json_escape(record.patient_name) << "\"," << nl;
303 oss << indent << "\"modality\": \"" << json_escape(record.modality) << "\"," << nl;
304 oss << indent << "\"procedureStepId\": \"" << json_escape(record.procedure_step_id) << "\"," << nl;
305 oss << indent << "\"startDate\": \"" << json_escape(record.start_date) << "\"," << nl;
306 oss << indent << "\"startTime\": \"" << json_escape(record.start_time) << "\"," << nl;
307 oss << indent << "\"endDate\": \"" << json_escape(record.end_date) << "\"," << nl;
308 oss << indent << "\"endTime\": \"" << json_escape(record.end_time) << "\"," << nl;
309 oss << indent << "\"createdAt\": \"" << json_escape(record.created_at) << "\"," << nl;
310 oss << indent << "\"updatedAt\": \"" << json_escape(record.updated_at) << "\"" << nl;
311 oss << "}";
312
313 return oss.str();
314}
315
319class mpps_repository {
320public:
321 explicit mpps_repository(const mpps_scp_args& args)
322 : output_dir_(args.output_dir)
323 , output_file_(args.output_file) {
324
325 // Create output directory if specified
326 if (!output_dir_.empty()) {
327 std::error_code ec;
328 std::filesystem::create_directories(output_dir_, ec);
329 if (ec) {
330 std::cerr << "Warning: Could not create output directory: "
331 << output_dir_ << " - " << ec.message() << "\n";
332 }
333 }
334 }
335
341
342 std::lock_guard<std::mutex> lock(mutex_);
343
344 // Extract data from the MPPS instance
345 mpps_record record;
346 record.sop_instance_uid = instance.sop_instance_uid;
347 record.status = std::string(kcenon::pacs::services::to_string(instance.status));
348 record.station_ae = instance.station_ae;
349 record.created_at = current_timestamp();
350 record.updated_at = record.created_at;
351
352 // Extract patient information from dataset
353 namespace tags = kcenon::pacs::core::tags;
354 record.patient_id = instance.data.get_string(tags::patient_id, "");
355 record.patient_name = instance.data.get_string(tags::patient_name, "");
356 record.modality = instance.data.get_string(tags::modality, "");
357 record.start_date = instance.data.get_string(
358 tags::performed_procedure_step_start_date, "");
359 record.start_time = instance.data.get_string(
360 tags::performed_procedure_step_start_time, "");
361
362 // Extract procedure step ID from MPPS-specific tags
363 record.procedure_step_id = instance.data.get_string(
365
366 // Store record
367 records_[record.sop_instance_uid] = record;
368
369 // Log the event
370 std::cout << "[" << current_timestamp() << "] "
371 << "N-CREATE: MPPS instance created\n"
372 << " UID: " << record.sop_instance_uid << "\n"
373 << " Status: " << record.status << "\n"
374 << " Station: " << record.station_ae << "\n"
375 << " Patient: " << record.patient_id << " / " << record.patient_name << "\n"
376 << " Modality: " << record.modality << "\n";
377
378 // Save to file if configured
379 save_record(record);
380
382 }
383
388 const std::string& sop_instance_uid,
389 const kcenon::pacs::core::dicom_dataset& modifications,
391
392 std::lock_guard<std::mutex> lock(mutex_);
393
394 // Find existing record
395 auto it = records_.find(sop_instance_uid);
396 if (it == records_.end()) {
397 // If not found, create a minimal record
398 mpps_record record;
399 record.sop_instance_uid = sop_instance_uid;
400 record.created_at = current_timestamp();
401 records_[sop_instance_uid] = record;
402 it = records_.find(sop_instance_uid);
403 }
404
405 auto& record = it->second;
406
407 // Check if already in final state
408 if (record.status == "COMPLETED" || record.status == "DISCONTINUED") {
409 std::cerr << "[" << current_timestamp() << "] "
410 << "Warning: Cannot modify MPPS in final state: "
411 << record.sop_instance_uid << "\n";
414 "Cannot modify MPPS in final state");
415 }
416
417 // Update status
418 record.status = std::string(kcenon::pacs::services::to_string(new_status));
419 record.updated_at = current_timestamp();
420
421 // Extract end date/time from modifications
422 record.end_date = modifications.get_string(
424 record.end_time = modifications.get_string(
426
427 // Log the event
428 std::cout << "[" << current_timestamp() << "] "
429 << "N-SET: MPPS instance updated\n"
430 << " UID: " << record.sop_instance_uid << "\n"
431 << " New Status: " << record.status << "\n";
432
433 if (!record.end_date.empty() || !record.end_time.empty()) {
434 std::cout << " End Time: " << record.end_date << " " << record.end_time << "\n";
435 }
436
437 // Save updated record
438 save_record(record);
439
441 }
442
446 size_t size() const {
447 std::lock_guard<std::mutex> lock(mutex_);
448 return records_.size();
449 }
450
454 std::map<std::string, size_t> count_by_status() const {
455 std::lock_guard<std::mutex> lock(mutex_);
456 std::map<std::string, size_t> counts;
457 for (const auto& [uid, record] : records_) {
458 counts[record.status]++;
459 }
460 return counts;
461 }
462
463private:
464 mutable std::mutex mutex_;
465 std::map<std::string, mpps_record> records_;
466 std::filesystem::path output_dir_;
467 std::filesystem::path output_file_;
468
472 void save_record(const mpps_record& record) {
473 // Save to individual file in output directory
474 if (!output_dir_.empty()) {
475 auto filename = output_dir_ / (record.sop_instance_uid + ".json");
476 std::ofstream file(filename);
477 if (file) {
478 file << to_json(record, true);
479 file.close();
480 } else {
481 std::cerr << "Warning: Could not write to " << filename << "\n";
482 }
483 }
484
485 // Append to output file
486 if (!output_file_.empty()) {
487 std::ofstream file(output_file_, std::ios::app);
488 if (file) {
489 file << to_json(record, false) << "\n";
490 file.close();
491 } else {
492 std::cerr << "Warning: Could not write to " << output_file_ << "\n";
493 }
494 }
495 }
496};
497
498// =============================================================================
499// Server Implementation
500// =============================================================================
501
507bool run_server(const mpps_scp_args& args) {
508 using namespace kcenon::pacs::network;
509 using namespace kcenon::pacs::services;
510
511 std::cout << "\nStarting MPPS SCP...\n";
512 std::cout << " AE Title: " << args.ae_title << "\n";
513 std::cout << " Port: " << args.port << "\n";
514 if (!args.output_dir.empty()) {
515 std::cout << " Output Directory: " << args.output_dir << "\n";
516 }
517 if (!args.output_file.empty()) {
518 std::cout << " Output File: " << args.output_file << "\n";
519 }
520 std::cout << " Max Associations: " << args.max_associations << "\n";
521 std::cout << " Idle Timeout: " << args.idle_timeout << " seconds\n";
522 std::cout << "\n";
523
524 // Create MPPS repository
525 mpps_repository repository(args);
526
527 // Configure server
528 server_config config;
529 config.ae_title = args.ae_title;
530 config.port = args.port;
531 config.max_associations = args.max_associations;
532 config.idle_timeout = std::chrono::seconds{args.idle_timeout};
533 config.implementation_class_uid = "1.2.826.0.1.3680043.2.1545.3";
534 config.implementation_version_name = "MPPS_SCP_001";
535
536 // Create server
537 dicom_server server{config};
538 g_server = &server;
539
540 // Register Verification service (C-ECHO)
541 server.register_service(std::make_shared<verification_scp>());
542
543 // Configure MPPS SCP
544 auto mpps_service = std::make_shared<mpps_scp>();
545
546 // Set N-CREATE handler
547 mpps_service->set_create_handler(
548 [&repository](const mpps_instance& instance) {
549 return repository.on_create(instance);
550 });
551
552 // Set N-SET handler
553 mpps_service->set_set_handler(
554 [&repository](const std::string& uid,
556 mpps_status status) {
557 return repository.on_set(uid, mods, status);
558 });
559
560 server.register_service(mpps_service);
561
562 // Set up callbacks for logging
563 server.on_association_established([](const association& assoc) {
564 std::cout << "[" << current_timestamp() << "] "
565 << "Association established from: " << assoc.calling_ae()
566 << " -> " << assoc.called_ae() << "\n";
567 });
568
569 server.on_association_released([](const association& assoc) {
570 std::cout << "[" << current_timestamp() << "] "
571 << "Association released: " << assoc.calling_ae() << "\n";
572 });
573
574 server.on_error([](const std::string& error) {
575 std::cerr << "[" << current_timestamp() << "] "
576 << "Error: " << error << "\n";
577 });
578
579 // Start server
580 auto result = server.start();
581 if (result.is_err()) {
582 std::cerr << "Failed to start server: " << result.error().message << "\n";
583 g_server = nullptr;
584 return false;
585 }
586
587 std::cout << "=================================================\n";
588 std::cout << " MPPS SCP is running on port " << args.port << "\n";
589 std::cout << " Waiting for MPPS requests...\n";
590 std::cout << " Press Ctrl+C to stop\n";
591 std::cout << "=================================================\n\n";
592
593 // Wait for shutdown
594 server.wait_for_shutdown();
595
596 // Print final statistics
597 auto server_stats = server.get_statistics();
598 auto status_counts = repository.count_by_status();
599
600 std::cout << "\n";
601 std::cout << "=================================================\n";
602 std::cout << " Server Statistics\n";
603 std::cout << "=================================================\n";
604 std::cout << " Total Associations: " << server_stats.total_associations << "\n";
605 std::cout << " Rejected Associations: " << server_stats.rejected_associations << "\n";
606 std::cout << " Messages Processed: " << server_stats.messages_processed << "\n";
607 std::cout << " N-CREATE Processed: " << mpps_service->creates_processed() << "\n";
608 std::cout << " N-SET Processed: " << mpps_service->sets_processed() << "\n";
609 std::cout << " MPPS Completed: " << mpps_service->mpps_completed() << "\n";
610 std::cout << " MPPS Discontinued: " << mpps_service->mpps_discontinued() << "\n";
611 std::cout << " Total MPPS Records: " << repository.size() << "\n";
612
613 if (!status_counts.empty()) {
614 std::cout << " Records by Status:\n";
615 for (const auto& [status, count] : status_counts) {
616 std::cout << " - " << status << ": " << count << "\n";
617 }
618 }
619
620 std::cout << " Uptime: " << server_stats.uptime().count() << " seconds\n";
621 std::cout << "=================================================\n";
622
623 g_server = nullptr;
624 return true;
625}
626
627} // namespace
628
629int main(int argc, char* argv[]) {
630 std::cout << R"(
631 __ __ ____ ____ ____ ____ ____ ____
632 | \/ | _ \| _ \/ ___| / ___| / ___| _ \
633 | |\/| | |_) | |_) \___ \ \___ \| | | |_) |
634 | | | | __/| __/ ___) | ___) | |___| __/
635 |_| |_|_| |_| |____/ |____/ \____|_|
636
637 DICOM Modality Performed Procedure Step Server
638)" << "\n";
639
640 mpps_scp_args args;
641
642 if (!parse_arguments(argc, argv, args)) {
643 print_usage(argv[0]);
644 return 1;
645 }
646
647 // Install signal handlers
648 install_signal_handlers();
649
650 bool success = run_server(args);
651
652 std::cout << "\nMPPS SCP terminated\n";
653 return success ? 0 : 1;
654}
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.
int main()
Definition main.cpp:84
DICOM MPPS (Modality Performed Procedure Step) SCP service.
@ error
Node returned an error.
@ station_ae
(0008,1010) Station Name or calling AE
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag sop_instance_uid
SOP Instance UID.
constexpr dicom_tag status
Status.
constexpr dicom_tag modality
Modality.
constexpr dicom_tag patient_name
Patient's Name.
constexpr int mpps_invalid_state
Definition result.h:173
constexpr core::dicom_tag performed_procedure_step_end_date
Performed Procedure Step End Date (0040,0250)
Definition mpps_scp.h:438
constexpr core::dicom_tag performed_procedure_step_id
Performed Procedure Step ID (0040,0253)
Definition mpps_scp.h:447
constexpr core::dicom_tag performed_procedure_step_end_time
Performed Procedure Step End Time (0040,0251)
Definition mpps_scp.h:441
mpps_status
MPPS status enumeration.
Definition mpps_scp.h:48
auto to_string(mpps_status status) -> std::string_view
Convert mpps_status to DICOM string representation.
Definition mpps_scp.h:60
Result< T > pacs_error(int code, const std::string &message, const std::string &details="")
Create a PACS error result with module context.
Definition result.h:234
Result<T> type aliases and helpers for PACS system.
DICOM Server configuration structures.
MPPS instance data structure.
Definition mpps_scp.h:98
mpps_status status
Current status (always IN PROGRESS for N-CREATE)
Definition mpps_scp.h:103
std::string sop_instance_uid
SOP Instance UID - unique identifier for this MPPS.
Definition mpps_scp.h:100
std::string station_ae
Performing station AE Title.
Definition mpps_scp.h:106
core::dicom_dataset data
Complete MPPS dataset from the request.
Definition mpps_scp.h:109
std::string_view uid
DICOM Verification SCP service (C-ECHO handler)