PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1
21#include "console_utils.h"
22#include "signal_handler.h"
23
32
33#include <atomic>
34#include <chrono>
35#include <cstdlib>
36#include <filesystem>
37#include <iomanip>
38#include <iostream>
39#include <memory>
40#include <string>
41
42namespace net = kcenon::pacs::network;
43namespace svc = kcenon::pacs::services;
44namespace stg = kcenon::pacs::storage;
45namespace core = kcenon::pacs::core;
46namespace tags = kcenon::pacs::core::tags;
47
48namespace {
49
54std::string current_timestamp() {
55 auto now = std::chrono::system_clock::now();
56 auto time_t_now = std::chrono::system_clock::to_time_t(now);
57 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
58 now.time_since_epoch()) % 1000;
59
60 std::ostringstream oss;
61 oss << std::put_time(std::localtime(&time_t_now), "%H:%M:%S");
62 oss << '.' << std::setfill('0') << std::setw(3) << ms.count();
63 return oss.str();
64}
65
80bool update_index(stg::index_database& db,
81 const core::dicom_dataset& ds,
82 const std::filesystem::path& file_path) {
83 // Patient information
84 auto patient_result = db.upsert_patient(
85 ds.get_string(tags::patient_id),
86 ds.get_string(tags::patient_name),
87 ds.get_string(tags::patient_birth_date),
88 ds.get_string(tags::patient_sex)
89 );
90
91 if (patient_result.is_err()) {
92 std::cerr << " Database error (patient): "
93 << patient_result.error().message << "\n";
94 return false;
95 }
96 int64_t patient_pk = patient_result.value();
97
98 // Study information
99 auto study_result = db.upsert_study(
100 patient_pk,
101 ds.get_string(tags::study_instance_uid),
102 ds.get_string(tags::study_id),
103 ds.get_string(tags::study_date),
104 ds.get_string(tags::study_time),
105 ds.get_string(tags::accession_number),
106 ds.get_string(tags::referring_physician_name),
107 ds.get_string(tags::study_description)
108 );
109
110 if (study_result.is_err()) {
111 std::cerr << " Database error (study): "
112 << study_result.error().message << "\n";
113 return false;
114 }
115 int64_t study_pk = study_result.value();
116
117 // Series information
118 // Note: body_part_examined tag not defined in current core library,
119 // pass empty string as placeholder
120 auto series_result = db.upsert_series(
121 study_pk,
122 ds.get_string(tags::series_instance_uid),
123 ds.get_string(tags::modality),
124 ds.get_numeric<int>(tags::series_number),
125 ds.get_string(tags::series_description),
126 "", // body_part_examined - not available in current tag definitions
127 ds.get_string(tags::station_name)
128 );
129
130 if (series_result.is_err()) {
131 std::cerr << " Database error (series): "
132 << series_result.error().message << "\n";
133 return false;
134 }
135 int64_t series_pk = series_result.value();
136
137 // Instance information
138 auto file_size = std::filesystem::exists(file_path)
139 ? static_cast<int64_t>(std::filesystem::file_size(file_path))
140 : 0;
141
142 auto instance_result = db.upsert_instance(
143 series_pk,
144 ds.get_string(tags::sop_instance_uid),
145 ds.get_string(tags::sop_class_uid),
146 file_path.string(),
147 file_size,
148 ds.get_string(tags::transfer_syntax_uid),
149 ds.get_numeric<int>(tags::instance_number)
150 );
151
152 if (instance_result.is_err()) {
153 std::cerr << " Database error (instance): "
154 << instance_result.error().message << "\n";
155 return false;
156 }
157
158 return true;
159}
160
161} // namespace
162
163int main(int argc, char* argv[]) {
164 kcenon::pacs::samples::print_header("Storage Server - Level 3 Sample");
165
166 // ===========================================================================
167 // Part 1: Storage Configuration
168 // ===========================================================================
169 // DICOM storage systems need several components:
170 // - Root directory for storing DICOM files
171 // - Naming scheme for file organization (UID-based hierarchy is common)
172 // - Duplicate handling policy (reject, replace, or ignore)
173 // - File extension for saved files
174
175 kcenon::pacs::samples::print_section("Part 1: Storage Configuration");
176
177 std::cout << "DICOM storage systems organize files hierarchically:\n";
178 std::cout << " - UID-based: {root}/{StudyUID}/{SeriesUID}/{SOPUID}.dcm\n";
179 std::cout << " - Date-based: {root}/YYYY/MM/DD/{StudyUID}/{SOPUID}.dcm\n";
180 std::cout << " - Flat: {root}/{SOPUID}.dcm\n\n";
181
182 // Create storage root directory
183 const std::filesystem::path storage_root = "./dicom_archive";
184 std::filesystem::create_directories(storage_root);
185
186 // Configure file storage
187 stg::file_storage_config fs_config;
188 fs_config.root_path = storage_root;
189 fs_config.naming = stg::naming_scheme::uid_hierarchical;
190 fs_config.duplicate = stg::duplicate_policy::replace;
191 fs_config.create_directories = true;
192 fs_config.file_extension = ".dcm";
193
194 kcenon::pacs::samples::print_table("File Storage Configuration", {
195 {"Root Path", storage_root.string()},
196 {"Naming Scheme", "uid_hierarchical"},
197 {"Duplicate Policy", "replace"},
198 {"File Extension", ".dcm"}
199 });
200
201 // Create file storage backend
202 auto file_storage = std::make_shared<stg::file_storage>(fs_config);
203
204 kcenon::pacs::samples::print_success("Part 1 complete - File storage configured!");
205
206 // ===========================================================================
207 // Part 2: Index Database
208 // ===========================================================================
209 // The index database enables fast queries without reading every DICOM file.
210 // It stores:
211 // - Patient demographics (name, ID, birth date, sex)
212 // - Study metadata (date, description, accession number)
213 // - Series metadata (modality, series number, description)
214 // - Instance references (SOP UID, file path, file size)
215
216 kcenon::pacs::samples::print_section("Part 2: Index Database");
217
218 std::cout << "Index database stores metadata for fast queries:\n";
219 std::cout << " - Patient demographics\n";
220 std::cout << " - Study/Series/Instance metadata\n";
221 std::cout << " - File paths for retrieval\n\n";
222
223 const auto db_path = storage_root / "index.db";
224
225 auto db_result = stg::index_database::open(db_path.string());
226 if (db_result.is_err()) {
227 kcenon::pacs::samples::print_error("Failed to open database: " +
228 db_result.error().message);
229 return 1;
230 }
231 auto index_db = std::move(db_result.value());
232
233 kcenon::pacs::samples::print_table("Database Configuration", {
234 {"Database Path", db_path.string()},
235 {"Storage Engine", "SQLite"},
236 {"Mode", "WAL (Write-Ahead Logging)"}
237 });
238
239 kcenon::pacs::samples::print_success("Part 2 complete - Database initialized!");
240
241 // ===========================================================================
242 // Part 3: Storage SCP Setup
243 // ===========================================================================
244 // Storage SCP (Service Class Provider) handles incoming C-STORE requests.
245 // Key configuration options:
246 // - Accepted SOP Classes: Which image types to accept (CT, MR, US, etc.)
247 // - Duplicate Policy: How to handle images that already exist
248 // - Handler callbacks: Custom logic for storage, validation, post-processing
249
250 kcenon::pacs::samples::print_section("Part 3: Storage SCP Setup");
251
252 std::cout << "Storage SCP handles C-STORE requests from modalities:\n";
253 std::cout << " 1. Pre-store handler: Validate incoming data\n";
254 std::cout << " 2. Storage handler: Store to filesystem + index\n";
255 std::cout << " 3. Post-store handler: Trigger workflows\n\n";
256
257 // Configure Storage SCP (empty = accept all standard storage SOP classes)
258 svc::storage_scp_config scp_config;
259 // scp_config.accepted_sop_classes = { ... }; // Leave empty to accept all
260
261 auto storage_scp = std::make_shared<svc::storage_scp>(scp_config);
262
263 // Statistics tracking
264 std::atomic<int> store_count{0};
265 std::atomic<int> reject_count{0};
266
267 // Main storage handler - called for each received image
268 storage_scp->set_handler(
269 [&](const core::dicom_dataset& dataset,
270 const std::string& calling_ae,
271 const std::string& sop_class_uid,
272 const std::string& sop_instance_uid) -> svc::storage_status {
273
274 std::cout << "\n[" << current_timestamp() << "] "
276 << kcenon::pacs::samples::colors::reset << " From: " << calling_ae << "\n";
277
278 // Extract key metadata for logging
279 std::cout << " Patient: " << dataset.get_string(tags::patient_name) << "\n";
280 std::cout << " Study: " << dataset.get_string(tags::study_description) << "\n";
281 std::cout << " Modality: " << dataset.get_string(tags::modality) << "\n";
282 std::cout << " SOP Class: " << sop_class_uid << "\n";
283
284 // Store to filesystem
285 auto store_result = file_storage->store(dataset);
286 if (store_result.is_err()) {
287 std::cout << " " << kcenon::pacs::samples::colors::red << "Storage failed: "
289 << store_result.error().message << "\n";
290 reject_count++;
291 return svc::storage_status::storage_error;
292 }
293
294 // Get the stored file path for database indexing
295 auto file_path = file_storage->get_file_path(sop_instance_uid);
296
297 // Update index database
298 if (!update_index(*index_db, dataset, file_path)) {
299 reject_count++;
300 return svc::storage_status::storage_error;
301 }
302
303 store_count++;
304 std::cout << " " << kcenon::pacs::samples::colors::green << "Stored"
305 << kcenon::pacs::samples::colors::reset << " (#" << store_count
306 << ") -> " << file_path.filename().string() << "\n";
307
308 return svc::storage_status::success;
309 }
310 );
311
312 // Pre-store validation handler (optional)
313 // Return false to reject the storage request
314 storage_scp->set_pre_store_handler(
315 [&reject_count](const core::dicom_dataset& dataset) -> bool {
316 // Example validation: Require Patient ID
317 if (dataset.get_string(tags::patient_id).empty()) {
318 std::cout << " " << kcenon::pacs::samples::colors::yellow << "[REJECTED]"
320 << " Missing Patient ID\n";
321 reject_count++;
322 return false;
323 }
324
325 // Example validation: Require Study Instance UID
326 if (dataset.get_string(tags::study_instance_uid).empty()) {
327 std::cout << " " << kcenon::pacs::samples::colors::yellow << "[REJECTED]"
329 << " Missing Study Instance UID\n";
330 reject_count++;
331 return false;
332 }
333
334 return true; // Accept the image
335 }
336 );
337
338 // Post-store notification handler (optional)
339 // Called after successful storage for workflow integration
340 storage_scp->set_post_store_handler(
341 [](const core::dicom_dataset& /*dataset*/,
342 const std::string& /*patient_id*/,
343 const std::string& /*study_uid*/,
344 const std::string& /*series_uid*/,
345 const std::string& /*sop_instance_uid*/) {
346 // Example: Trigger downstream processing
347 // - Send to AI analysis service
348 // - Notify worklist system
349 // - Queue for auto-routing
350 // For this sample, we just acknowledge the callback
351 }
352 );
353
354 kcenon::pacs::samples::print_table("Storage SCP Configuration", {
355 {"SOP Classes", "All standard storage classes"},
356 {"Duplicate Policy", "Replace existing"},
357 {"Pre-store Handler", "Validate Patient ID and Study UID"},
358 {"Post-store Handler", "Workflow notification"}
359 });
360
361 kcenon::pacs::samples::print_success("Part 3 complete - Storage SCP configured!");
362
363 // ===========================================================================
364 // Part 4: Server Startup
365 // ===========================================================================
366 // Create the DICOM server and register services:
367 // - Verification SCP (C-ECHO) for connectivity testing
368 // - Storage SCP (C-STORE) for receiving images
369
370 kcenon::pacs::samples::print_section("Part 4: Running Server");
371
372 // Parse optional port argument
373 uint16_t port = 11112;
374 if (argc > 1) {
375 try {
376 int port_arg = std::stoi(argv[1]);
377 if (port_arg > 0 && port_arg < 65536) {
378 port = static_cast<uint16_t>(port_arg);
379 }
380 } catch (const std::exception&) {
381 // Keep default port
382 }
383 }
384
385 // Configure server
386 net::server_config server_config;
387 server_config.ae_title = "STORE_SCP";
388 server_config.port = port;
389 server_config.max_associations = 20;
390 server_config.idle_timeout = std::chrono::seconds(60);
391 server_config.max_pdu_size = 65536; // 64KB for efficient image transfer
392 server_config.implementation_class_uid = "1.2.410.200001.1.1";
393 server_config.implementation_version_name = "PACS_SAMPLE_3.0";
394
395 // Create server and register services
396 auto server = std::make_unique<net::dicom_server>(server_config);
397 server->register_service(std::make_shared<svc::verification_scp>());
398 server->register_service(storage_scp);
399
400 // Connection tracking
401 std::atomic<int> active_connections{0};
402
403 server->on_association_established(
404 [&active_connections](const net::association& assoc) {
405 active_connections++;
406 std::cout << "[" << current_timestamp() << "] "
409 << assoc.calling_ae() << " -> " << assoc.called_ae()
410 << " (active: " << active_connections << ")\n";
411 }
412 );
413
414 server->on_association_released(
415 [&active_connections](const net::association& assoc) {
416 active_connections--;
417 std::cout << "[" << current_timestamp() << "] "
420 << assoc.calling_ae() << " disconnected"
421 << " (active: " << active_connections << ")\n";
422 }
423 );
424
425 server->on_error(
426 [](const std::string& error_msg) {
427 std::cerr << "[" << current_timestamp() << "] "
430 << error_msg << "\n";
431 }
432 );
433
434 // Install signal handler for graceful shutdown
435 kcenon::pacs::samples::scoped_signal_handler sig_handler([&server]() {
436 std::cout << "\n" << kcenon::pacs::samples::colors::yellow
437 << "Shutdown signal received..."
439 server->stop();
440 });
441
442 // Start the server
443 auto start_result = server->start();
444 if (start_result.is_err()) {
445 kcenon::pacs::samples::print_error("Failed to start server: " +
446 start_result.error().message);
447 return 1;
448 }
449
450 // Print server running banner
452 "Storage Server Running",
453 "",
454 "Test with DCMTK:",
455 " storescu -aec STORE_SCP localhost " + std::to_string(port) + " image.dcm",
456 "",
457 "Generate test data (from Level 1):",
458 " ./hello_dicom # Creates hello_dicom_output.dcm",
459 " storescu -aec STORE_SCP localhost " + std::to_string(port) +
460 " hello_dicom_output.dcm",
461 "",
462 "Verify connectivity:",
463 " echoscu -aec STORE_SCP localhost " + std::to_string(port),
464 "",
465 "Press Ctrl+C to stop"
466 });
467
468 // Wait for shutdown signal
469 sig_handler.wait();
470
471 // ===========================================================================
472 // Part 5: Statistics and Cleanup
473 // ===========================================================================
474 // Display final statistics and verify database contents.
475
476 kcenon::pacs::samples::print_section("Final Statistics");
477
478 // Server statistics
479 auto stats = server->get_statistics();
480 kcenon::pacs::samples::print_table("Server Statistics", {
481 {"Total Associations", std::to_string(stats.total_associations)},
482 {"Messages Processed", std::to_string(stats.messages_processed)},
483 {"Bytes Received", std::to_string(stats.bytes_received)},
484 {"Uptime", std::to_string(stats.uptime().count()) + " seconds"}
485 });
486
487 // Storage statistics
488 kcenon::pacs::samples::print_table("Storage Statistics", {
489 {"Images Stored", std::to_string(store_count.load())},
490 {"Images Rejected", std::to_string(reject_count.load())},
491 {"Storage Path", storage_root.string()}
492 });
493
494 // Database statistics
495 auto db_stats_result = index_db->get_storage_stats();
496 if (db_stats_result.is_ok()) {
497 auto db_stats = db_stats_result.value();
498 kcenon::pacs::samples::print_table("Database Statistics", {
499 {"Patients", std::to_string(db_stats.total_patients)},
500 {"Studies", std::to_string(db_stats.total_studies)},
501 {"Series", std::to_string(db_stats.total_series)},
502 {"Instances", std::to_string(db_stats.total_instances)},
503 {"Total File Size", std::to_string(db_stats.total_file_size) + " bytes"}
504 });
505 }
506
507 // Summary
509 "Congratulations! You have learned:",
510 "",
511 "1. File Storage - Hierarchical DICOM file organization",
512 "2. Index Database - SQLite-based metadata indexing",
513 "3. Storage SCP - Receiving images via C-STORE",
514 "4. Validation - Pre-store data validation hooks",
515 "5. Workflow - Post-store notification handlers",
516 "",
517 "Query the database:",
518 " sqlite3 " + db_path.string() + " \"SELECT * FROM patients;\"",
519 "",
520 "Next step: Level 4 - Mini PACS (Query/Retrieve)"
521 });
522
523 kcenon::pacs::samples::print_success("Storage Server terminated successfully.");
524
525 return 0;
526}
int main()
Definition main.cpp:34
Represents a DICOM Dataset (ordered collection of Data Elements)
auto get_numeric(dicom_tag tag) const -> std::optional< T >
Get the numeric value of an element.
auto get_string(dicom_tag tag, std::string_view default_value="") const -> std::string
Get the string value of an element.
DICOM Association management class.
std::string_view calling_ae() const noexcept
Get calling AE title.
std::string_view called_ae() const noexcept
Get called AE title.
void wait()
Block until shutdown signal is received.
PACS index database manager.
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 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 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.
Console output utilities for developer samples.
DICOM Dataset - ordered collection of Data Elements.
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.
constexpr const char * reset
Reset all formatting.
constexpr const char * red
constexpr const char * yellow
constexpr const char * green
constexpr const char * cyan
void print_error(std::string_view message)
Print an error message.
void print_table(std::string_view title, const std::vector< std::pair< std::string, std::string > > &rows)
Print a key-value table.
void print_header(std::string_view title)
Print a header/title with decorative border.
void print_success(std::string_view message)
Print a success message.
void print_section(std::string_view title)
Print a section header.
void print_box(const std::vector< std::string > &lines)
Print text in a decorative box.
storage_status
Storage operation status codes.
DICOM Server configuration structures.
Cross-platform signal handling for graceful shutdown.
DICOM Storage SCP service (C-STORE handler)
Configuration for DICOM server.
std::string ae_title
Application Entity Title for this server (16 chars max)
Configuration for Storage SCP service.
Definition storage_scp.h:49
Configuration for file_storage.
std::string file_extension
File extension for DICOM files.
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.
DICOM Verification SCP service (C-ECHO handler)