21 if (scheme ==
"flat") {
24 if (scheme ==
"date_hierarchical") {
32 if (policy ==
"replace") {
35 if (policy ==
"ignore") {
42std::string log_prefix() {
43 auto now = std::chrono::system_clock::now();
44 auto time_t = std::chrono::system_clock::to_time_t(now);
46 std::strftime(buf,
sizeof(buf),
"%Y-%m-%d %H:%M:%S", std::localtime(&time_t));
47 return std::string(
"[") + buf +
"] ";
68 std::cout << log_prefix() <<
"Initializing PACS Server...\n";
87 std::cout << log_prefix() <<
"PACS Server initialized successfully\n";
93 std::cerr << log_prefix() <<
"Error: Server not initialized\n";
97 std::cout << log_prefix() <<
"Starting DICOM server...\n";
102 auto result =
server_->start();
103 if (result.is_err()) {
104 std::cerr << log_prefix() <<
"Error: Failed to start server\n";
108 std::cout << log_prefix() <<
"PACS Server started successfully\n";
109 std::cout << log_prefix() <<
"Listening on port " <<
config_.
server.
port <<
"...\n";
110 std::cout << log_prefix() <<
"Press Ctrl+C to stop\n";
117 std::cout << log_prefix() <<
"Stopping DICOM server...\n";
119 std::cout << log_prefix() <<
"DICOM server stopped\n";
143 auto stats =
server_->get_statistics();
144 auto uptime = stats.uptime();
147 std::cout <<
"=== PACS Server Statistics ===\n";
148 std::cout <<
"Uptime: " << uptime.count() <<
" seconds\n";
149 std::cout <<
"Total Associations: " << stats.total_associations <<
"\n";
150 std::cout <<
"Active Associations: " << stats.active_associations <<
"\n";
151 std::cout <<
"Rejected Associations: " << stats.rejected_associations <<
"\n";
152 std::cout <<
"Messages Processed: " << stats.messages_processed <<
"\n";
153 std::cout <<
"Bytes Received: " << stats.bytes_received <<
"\n";
154 std::cout <<
"Bytes Sent: " << stats.bytes_sent <<
"\n";
155 std::cout <<
"==============================\n";
164 std::cout << log_prefix() <<
"Setting up file storage...\n";
171 std::cerr << log_prefix() <<
"Error: Failed to create storage directory: "
172 << ec.message() <<
"\n";
184 }
catch (
const std::exception& e) {
185 std::cerr << log_prefix() <<
"Error: Failed to create file storage: "
190 std::cout << log_prefix() <<
"File storage ready\n";
195 std::cout << log_prefix() <<
"Setting up database...\n";
200 if (!db_dir.empty()) {
202 std::filesystem::create_directories(db_dir, ec);
204 std::cerr << log_prefix() <<
"Error: Failed to create database directory: "
205 << ec.message() <<
"\n";
216 if (result.is_err()) {
217 std::cerr << log_prefix() <<
"Error: Failed to open database\n";
222 std::cout << log_prefix() <<
"Database ready\n";
227 std::cout << log_prefix() <<
"Setting up DICOM services...\n";
228 std::cout << log_prefix() <<
" - Verification SCP (C-ECHO)\n";
229 std::cout << log_prefix() <<
" - Storage SCP (C-STORE)\n";
230 std::cout << log_prefix() <<
" - Query SCP (C-FIND)\n";
231 std::cout << log_prefix() <<
" - Retrieve SCP (C-MOVE/C-GET)\n";
232 std::cout << log_prefix() <<
" - Worklist SCP (MWL)\n";
233 std::cout << log_prefix() <<
" - MPPS SCP (N-CREATE/N-SET)\n";
234 std::cout << log_prefix() <<
"All DICOM services configured\n";
239 std::cout << log_prefix() <<
"Setting up DICOM server...\n";
250 server_config.accept_unknown_calling_ae =
false;
253 server_ = std::make_unique<network::dicom_server>(server_config);
256 server_->register_service(std::make_shared<services::verification_scp>());
259 auto storage_scp = std::make_shared<services::storage_scp>();
260 storage_scp->set_handler(
261 [
this](
const auto& ds,
const auto& ae,
const auto& sop_class,
const auto& sop_uid) {
264 server_->register_service(storage_scp);
267 auto query_scp = std::make_shared<services::query_scp>();
268 query_scp->set_handler(
269 [
this](
auto level,
const auto& keys,
const auto& ae) {
272 server_->register_service(query_scp);
275 auto retrieve_scp = std::make_shared<services::retrieve_scp>();
276 retrieve_scp->set_retrieve_handler(
277 [
this](
const auto& keys) {
280 server_->register_service(retrieve_scp);
283 auto worklist_scp = std::make_shared<services::worklist_scp>();
284 worklist_scp->set_handler(
285 [
this](
const auto& keys,
const auto& ae) {
288 server_->register_service(worklist_scp);
291 auto mpps_scp = std::make_shared<services::mpps_scp>();
292 mpps_scp->set_create_handler(
293 [
this](
const auto& instance) {
296 mpps_scp->set_set_handler(
297 [
this](
const auto&
uid,
const auto& mods,
auto status) {
300 server_->register_service(mpps_scp);
304 std::cout << log_prefix() <<
"Association established: "
309 std::cout << log_prefix() <<
"Association released: "
313 server_->on_error([](
const std::string& error) {
314 std::cerr << log_prefix() <<
"Server error: " << error <<
"\n";
317 std::cout << log_prefix() <<
"DICOM server configured\n";
327 const std::string& calling_ae,
328 const std::string& sop_class_uid,
329 const std::string& sop_instance_uid) {
331 std::cout << log_prefix() <<
"C-STORE from " << calling_ae
332 <<
": " << sop_instance_uid <<
"\n";
336 if (store_result.is_err()) {
337 std::cerr << log_prefix() <<
"Storage error\n";
347 if (patient_id.empty()) {
348 std::cerr << log_prefix() <<
"Warning: Missing PatientID\n";
355 auto patient_result =
database_->upsert_patient(
361 if (patient_result.is_err()) {
362 std::cerr << log_prefix() <<
"Database error (patient)\n";
365 int64_t patient_pk = patient_result.value();
368 int64_t study_pk = 0;
369 if (!study_uid.empty()) {
377 auto study_result =
database_->upsert_study(
387 if (study_result.is_err()) {
388 std::cerr << log_prefix() <<
"Database error (study)\n";
391 study_pk = study_result.value();
395 int64_t series_pk = 0;
396 if (!series_uid.empty() && study_pk > 0) {
401 std::optional<int> series_number;
402 if (!series_number_str.empty()) {
404 series_number = std::stoi(series_number_str);
408 auto series_result =
database_->upsert_series(
417 if (series_result.is_err()) {
418 std::cerr << log_prefix() <<
"Database error (series)\n";
421 series_pk = series_result.value();
427 auto file_path =
file_storage_->get_file_path(sop_instance_uid);
429 int64_t file_size = 0;
430 if (std::filesystem::exists(file_path)) {
431 file_size =
static_cast<int64_t
>(std::filesystem::file_size(file_path));
434 std::optional<int> instance_number;
435 if (!instance_number_str.empty()) {
437 instance_number = std::stoi(instance_number_str);
441 auto instance_result =
database_->upsert_instance(
450 if (instance_result.is_err()) {
451 std::cerr << log_prefix() <<
"Database error (instance)\n";
461 const std::string& calling_ae) {
463 std::cout << log_prefix() <<
"C-FIND from " << calling_ae
466 std::vector<core::dicom_dataset> results;
473 query.patient_id = id;
477 query.patient_name =
name;
480 auto patients_result =
database_->search_patients(query);
481 if (patients_result.is_ok()) {
482 for (
const auto& patient : patients_result.value()) {
488 results.push_back(std::move(ds));
498 query.patient_id = id;
502 query.study_uid =
uid;
506 query.study_date = date;
509 auto studies_result =
database_->search_studies(query);
510 if (studies_result.is_ok()) {
511 for (
const auto& study : studies_result.value()) {
518 results.push_back(std::move(ds));
528 query.study_uid =
uid;
532 query.modality = mod;
535 auto series_result =
database_->search_series(query);
536 if (series_result.is_ok()) {
537 for (
const auto& series : series_result.value()) {
541 if (series.series_number.has_value()) {
543 std::to_string(series.series_number.value()));
546 results.push_back(std::move(ds));
556 query.series_uid =
uid;
559 auto instances_result =
database_->search_instances(query);
560 if (instances_result.is_ok()) {
561 for (
const auto& instance : instances_result.value()) {
565 if (instance.instance_number.has_value()) {
567 std::to_string(instance.instance_number.value()));
569 results.push_back(std::move(ds));
576 std::cout << log_prefix() <<
" Found " << results.size() <<
" matches\n";
583 std::cout << log_prefix() <<
"C-MOVE/C-GET retrieve request\n";
585 std::vector<core::dicom_file> files;
586 std::vector<std::string> file_paths;
590 if (!sop_uid.empty()) {
592 auto path_result =
database_->get_file_path(sop_uid);
593 if (path_result.is_ok() && path_result.value().has_value()) {
594 file_paths.push_back(path_result.value().value());
598 if (!series_uid.empty()) {
600 auto series_files_result =
database_->get_series_files(series_uid);
601 if (series_files_result.is_ok()) {
602 file_paths = std::move(series_files_result.value());
606 if (!study_uid.empty()) {
608 auto study_files_result =
database_->get_study_files(study_uid);
609 if (study_files_result.is_ok()) {
610 file_paths = std::move(study_files_result.value());
617 for (
const auto& path : file_paths) {
619 if (file_result.is_ok()) {
620 files.push_back(std::move(file_result.value()));
624 std::cout << log_prefix() <<
" Found " << files.size() <<
" files to transfer\n";
630 const std::string& calling_ae) {
632 std::cout << log_prefix() <<
"MWL query from " << calling_ae <<
"\n";
634 std::vector<core::dicom_dataset> results;
639 query.patient_id = id;
642 auto items_result =
database_->query_worklist(query);
644 if (items_result.is_ok()) {
645 for (
const auto& item : items_result.value()) {
651 results.push_back(std::move(ds));
655 std::cout << log_prefix() <<
" Found " << results.size() <<
" worklist items\n";
662 std::cout << log_prefix() <<
"MPPS N-CREATE: " << instance.
sop_instance_uid <<
"\n";
669 if (result.is_err()) {
670 return kcenon::common::make_error<std::monostate>(1,
"MPPS creation failed");
677 const std::string& sop_instance_uid,
681 std::cout << log_prefix() <<
"MPPS N-SET: " << sop_instance_uid
689 if (result.is_err()) {
690 return kcenon::common::make_error<std::monostate>(1,
"MPPS update failed");
void set_string(dicom_tag tag, encoding::vr_type vr, std::string_view value)
Set a string value for the given tag.
auto get_string(dicom_tag tag, std::string_view default_value="") const -> std::string
Get the string value of an element.
static auto open(const std::filesystem::path &path) -> kcenon::pacs::Result< dicom_file >
Open and read a DICOM file from disk.
std::unique_ptr< network::dicom_server > server_
DICOM server.
std::vector< core::dicom_file > handle_retrieve(const core::dicom_dataset &query_keys)
Handle C-MOVE/C-GET retrieve.
bool setup_server()
Set up DICOM server.
network::Result< std::monostate > handle_mpps_create(const services::mpps_instance &instance)
Handle MPPS N-CREATE.
bool setup_storage()
Set up file storage.
pacs_server_app(const pacs_server_config &config)
Construct server application with configuration.
bool start()
Start the DICOM server.
bool initialized_
Initialization flag.
std::unique_ptr< storage::index_database > database_
Index database.
bool setup_services()
Set up DICOM services.
~pacs_server_app()
Destructor - stops server if running.
std::vector< core::dicom_dataset > handle_worklist_query(const core::dicom_dataset &query_keys, const std::string &calling_ae)
Handle worklist query.
services::storage_status handle_store(const core::dicom_dataset &dataset, const std::string &calling_ae, const std::string &sop_class_uid, const std::string &sop_instance_uid)
Handle incoming C-STORE request.
bool is_running() const noexcept
Check if server is running.
std::atomic< bool > shutdown_requested_
Shutdown flag.
void print_statistics() const
Get current server statistics.
void stop()
Stop the server gracefully.
network::Result< std::monostate > handle_mpps_set(const std::string &sop_instance_uid, const core::dicom_dataset &modifications, services::mpps_status new_status)
Handle MPPS N-SET.
std::vector< core::dicom_dataset > handle_query(services::query_level level, const core::dicom_dataset &query_keys, const std::string &calling_ae)
Handle C-FIND query.
bool initialize()
Initialize all components.
pacs_server_config config_
Server configuration.
std::unique_ptr< storage::file_storage > file_storage_
File storage.
void wait_for_shutdown()
Wait for server shutdown.
void request_shutdown()
Request shutdown.
bool setup_database()
Set up database.
std::string_view calling_ae() const noexcept
Get calling AE title.
std::string_view called_ae() const noexcept
Get called AE title.
static auto open(std::string_view db_path) -> Result< std::unique_ptr< index_database > >
Open or create a database with default configuration.
Compile-time constants for commonly used DICOM tags.
@ DA
Date (8 chars, format: YYYYMMDD)
@ IS
Integer String (12 chars max)
@ LO
Long String (64 chars max)
@ UI
Unique Identifier (64 chars max)
@ PN
Person Name (64 chars max per component group)
@ CS
Code String (16 chars max, uppercase + digits + space + underscore)
@ TM
Time (14 chars max, format: HHMMSS.FFFFFF)
@ SH
Short String (16 chars max)
kcenon::pacs::Result< T > Result
Result type alias using standardized kcenon::pacs::Result<T>
storage_status
Storage operation status codes.
@ storage_error
Failure: Unable to process - storage error (0xC001)
@ success
Success - image stored successfully (0x0000)
mpps_status
MPPS status enumeration.
auto to_string(mpps_status status) -> std::string_view
Convert mpps_status to DICOM string representation.
query_level
DICOM Query/Retrieve level enumeration.
@ study
Study level - query study information.
@ image
Image (Instance) level - query instance information.
@ patient
Patient level - query patient demographics.
@ series
Series level - query series information.
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.
PACS Server application class.
Storage SCP status codes for C-STORE operations.
std::vector< std::string > allowed_ae_titles
Allowed AE titles (empty = accept all)
bool wal_mode
Enable WAL (Write-Ahead Logging) mode for better concurrency.
std::filesystem::path path
Path to SQLite database file.
Complete PACS server configuration.
database_config database
Database settings.
access_control_config access_control
Access control settings.
server_network_config server
Server network settings.
storage_config storage
Storage settings.
uint16_t port
Port to listen on.
std::chrono::seconds idle_timeout
Idle timeout for associations in seconds (0 = no timeout)
std::string ae_title
Application Entity Title for this server (max 16 chars)
size_t max_associations
Maximum concurrent associations (0 = unlimited)
std::filesystem::path directory
Root directory for DICOM file storage.
std::string duplicate_policy
Duplicate handling policy: "reject", "replace", "ignore".
std::string naming
File naming scheme: "hierarchical" or "flat".
std::string ae_title
Application Entity Title for this server (16 chars max)
MPPS instance data structure.
std::string sop_instance_uid
SOP Instance UID - unique identifier for this MPPS.
std::string station_ae
Performing station AE Title.
Configuration for file_storage.
Configuration for index database.
bool wal_mode
Enable WAL (Write-Ahead Logging) mode for better concurrency.