46 if (result.is_err()) {
47 std::cerr <<
"Failed to start server: " << result.error().message <<
"\n";
127 file_storage_ = std::make_shared<storage::file_storage>(fs_config);
132 if (db_result.is_err()) {
133 throw std::runtime_error(
"Failed to open index database: " +
134 db_result.error().message);
136 index_db_ = std::move(db_result.value());
145 storage_scp_ = std::make_shared<services::storage_scp>(scp_config);
147 [
this](
const auto& ds,
const auto& ae,
const auto& sop_class,
148 const auto& sop_instance) {
153 query_scp_ = std::make_shared<services::query_scp>();
155 [
this](
auto level,
const auto& keys,
const auto& ae) {
170 [
this](
const auto& keys,
const auto& ae) {
177 mpps_scp_ = std::make_shared<services::mpps_scp>();
181 [
this](
const auto&
uid,
const auto& mods,
auto status) {
194 server_config.implementation_class_uid =
"1.2.410.200001.1.4";
214 server_->on_association_established(
219 std::cout <<
"[CONNECT] " << assoc.
calling_ae()
225 server_->on_association_released(
229 std::cout <<
"[RELEASE] " << assoc.
calling_ae()
235 [
this](
const std::string& msg) {
236 std::cerr <<
"[ERROR] " << msg <<
"\n";
246 const std::string& calling_ae,
248 const std::string& sop_instance_uid) {
253 std::cout <<
"[C-STORE] From: " << calling_ae
260 if (store_result.is_err()) {
261 std::cerr <<
" Storage failed: " << store_result.error().message <<
"\n";
266 auto file_path =
file_storage_->get_file_path(sop_instance_uid);
272 ? std::filesystem::file_size(file_path)
280 const std::filesystem::path& file_path) {
283 auto patient_result =
index_db_->upsert_patient(
289 if (patient_result.is_err()) {
290 std::cerr <<
"Database error (patient): "
291 << patient_result.error().message <<
"\n";
294 int64_t patient_pk = patient_result.value();
297 auto study_result =
index_db_->upsert_study(
307 if (study_result.is_err()) {
308 std::cerr <<
"Database error (study): "
309 << study_result.error().message <<
"\n";
312 int64_t study_pk = study_result.value();
315 auto series_result =
index_db_->upsert_series(
324 if (series_result.is_err()) {
325 std::cerr <<
"Database error (series): "
326 << series_result.error().message <<
"\n";
329 int64_t series_pk = series_result.value();
332 auto file_size = std::filesystem::exists(file_path)
333 ?
static_cast<int64_t
>(std::filesystem::file_size(file_path))
336 auto instance_result =
index_db_->upsert_instance(
345 if (instance_result.is_err()) {
346 std::cerr <<
"Database error (instance): "
347 << instance_result.error().message <<
"\n";
361 const std::string& ) {
365 std::vector<core::dicom_dataset> results;
375 auto patients =
index_db_->search_patients(pq);
377 if (patients.is_ok()) {
378 for (
const auto& p : patients.value()) {
384 results.push_back(std::move(ds));
397 if (!study_uid.empty()) sq.
study_uid = study_uid;
398 if (!study_date.empty()) sq.
study_date = study_date;
401 auto studies =
index_db_->search_studies(sq);
403 if (studies.is_ok()) {
404 for (
const auto& s : studies.value()) {
413 results.push_back(std::move(ds));
424 if (!study_uid.empty()) sq.
study_uid = study_uid;
425 if (!series_uid.empty()) sq.
series_uid = series_uid;
426 if (!modality.empty()) sq.
modality = modality;
428 auto series =
index_db_->search_series(sq);
430 if (series.is_ok()) {
431 for (
const auto& ser : series.value()) {
435 if (ser.series_number.has_value()) {
440 results.push_back(std::move(ds));
450 if (!series_uid.empty()) iq.
series_uid = series_uid;
451 if (!sop_uid.empty()) iq.
sop_uid = sop_uid;
453 auto instances =
index_db_->search_instances(iq);
455 if (instances.is_ok()) {
456 for (
const auto& inst : instances.value()) {
460 if (inst.instance_number.has_value()) {
463 results.push_back(std::move(ds));
471 std::cout <<
"[C-FIND] Level: " << to_string(level)
472 <<
" Results: " << results.size() <<
"\n";
485 std::vector<core::dicom_file> files;
492 std::vector<storage::instance_record> instances;
494 if (!sop_uid.empty()) {
498 auto result =
index_db_->search_instances(iq);
499 if (result.is_ok()) {
500 instances = result.value();
502 }
else if (!series_uid.empty()) {
506 auto result =
index_db_->search_instances(iq);
507 if (result.is_ok()) {
508 instances = result.value();
510 }
else if (!study_uid.empty()) {
514 auto series_result =
index_db_->search_series(sq);
515 if (series_result.is_ok()) {
516 for (
const auto& ser : series_result.value()) {
519 auto inst_result =
index_db_->search_instances(iq);
520 if (inst_result.is_ok()) {
521 for (
const auto& inst : inst_result.value()) {
522 instances.push_back(inst);
530 for (
const auto& inst : instances) {
532 if (file_result.is_ok()) {
533 files.push_back(std::move(file_result.value()));
540 std::cout <<
"[C-MOVE/C-GET] Files: " << files.size() <<
"\n";
547 const std::string& ae_title) {
563 const std::string& ) {
567 std::vector<core::dicom_dataset> results;
578 if (!filter_modality.empty() && item.modality != filter_modality) {
581 if (!filter_date.empty() && item.scheduled_date != filter_date) {
584 if (!filter_patient_id.empty() && item.patient_id != filter_patient_id) {
592 std::cout <<
"[MWL] Results: " << results.size() <<
"\n";
635 entry.
status =
"IN PROGRESS";
646 <<
" Station: " << instance.
station_ae <<
"\n";
653 const std::string& sop_instance_uid,
662 if (entry.sop_instance_uid == sop_instance_uid) {
666 std::cout <<
"[MPPS N-SET] UID: " << sop_instance_uid
667 <<
" Status: " << entry.status <<
"\n";
676 "MPPS instance not found: " + sop_instance_uid);
Represents a DICOM Dataset (ordered collection of Data Elements)
void set_numeric(dicom_tag tag, encoding::vr_type vr, T value)
Set a numeric value for the given tag.
auto get_numeric(dicom_tag tag) const -> std::optional< T >
Get the numeric value of an element.
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.
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.
std::unique_ptr< network::dicom_server > server_
std::shared_ptr< services::retrieve_scp > retrieve_scp_
std::shared_ptr< services::mpps_scp > mpps_scp_
std::atomic< bool > running_
std::shared_ptr< storage::file_storage > file_storage_
void stop()
Stop the PACS server.
std::vector< core::dicom_dataset > handle_worklist_query(const core::dicom_dataset &query_keys, const std::string &calling_ae)
std::shared_ptr< services::query_scp > query_scp_
std::shared_ptr< services::worklist_scp > worklist_scp_
std::vector< core::dicom_dataset > handle_query(services::query_level level, const core::dicom_dataset &query_keys, const std::string &calling_ae)
const mini_pacs_statistics & statistics() const noexcept
Get current statistics.
std::shared_ptr< services::storage_scp > storage_scp_
std::mutex worklist_mutex_
std::vector< worklist_entry > worklist_items_
core::dicom_dataset create_worklist_response(const worklist_entry &item)
void setup_event_handlers()
~mini_pacs()
Destructor - ensures clean shutdown.
bool is_running() const noexcept
Check if server is running.
std::vector< mpps_entry > mpps_instances_
std::vector< worklist_entry > get_worklist_items() const
Get current worklist items.
bool start()
Start the PACS server.
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)
std::optional< std::pair< std::string, uint16_t > > resolve_destination(const std::string &ae_title)
network::Result< std::monostate > handle_mpps_set(const std::string &sop_instance_uid, const core::dicom_dataset &modifications, services::mpps_status new_status)
std::vector< core::dicom_file > handle_retrieve(const core::dicom_dataset &query_keys)
void initialize_services()
void wait()
Block until server shutdown.
const mini_pacs_config & config() const noexcept
Get the configuration.
std::shared_ptr< services::verification_scp > verification_scp_
std::shared_ptr< storage::index_database > index_db_
network::Result< std::monostate > handle_mpps_create(const services::mpps_instance &instance)
bool update_index(const core::dicom_dataset &dataset, const std::filesystem::path &file_path)
mini_pacs_statistics stats_
void clear_worklist()
Clear all worklist items.
void initialize_storage()
void add_worklist_item(const worklist_entry &item)
Add a worklist item for MWL queries.
mini_pacs(const mini_pacs_config &config)
Construct Mini PACS with configuration.
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.
Mini PACS class integrating all DICOM services.
@ 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)
@ AE
Application Entity (16 chars max)
@ SH
Short String (16 chars max)
constexpr int instance_not_found
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.
@ uid_hierarchical
{StudyUID}/{SeriesUID}/{SOPUID}.dcm
@ replace
Overwrite existing instance.
Result< T > pacs_error(int code, const std::string &message, const std::string &details="")
Create a PACS error result with module context.
Configuration for DICOM server.
Configuration options for Mini PACS.
uint16_t port
TCP port to listen on.
std::string ae_title
Application Entity title (max 16 characters)
bool verbose_logging
Enable verbose logging.
bool enable_mpps
Enable MPPS service.
std::filesystem::path storage_path
Root path for DICOM file storage.
size_t max_associations
Maximum concurrent associations.
bool enable_worklist
Enable Modality Worklist service.
Runtime statistics for Mini PACS operations.
std::atomic< int > c_move_count
C-MOVE operations processed.
std::atomic< int > associations_total
Total associations established since start.
std::atomic< int > associations_active
Currently active associations.
std::atomic< int > c_store_count
C-STORE operations processed.
std::atomic< int > mwl_count
MWL queries processed.
std::atomic< int > c_find_count
C-FIND operations processed.
std::atomic< size_t > bytes_received
Total bytes received.
std::atomic< int > mpps_create_count
MPPS N-CREATE processed.
std::atomic< int > mpps_set_count
MPPS N-SET processed.
MPPS instance for tracking procedure progress.
std::string sop_instance_uid
DICOM server configuration.
std::uint16_t port
TCP port for DICOM connections.
std::size_t max_associations
Maximum concurrent associations.
std::chrono::seconds idle_timeout
Idle timeout for associations (seconds)
std::string ae_title
Application Entity title (max 16 characters)
Worklist item for MWL queries.
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.
core::dicom_dataset data
Complete MPPS dataset from the request.
Configuration for Storage SCP service.
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.
Query parameters for instance search.
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)
Query parameters for patient search.
std::optional< std::string > patient_name
Patient name pattern (supports * wildcard)
std::optional< std::string > patient_id
Patient ID pattern (supports * wildcard)
Query parameters for series search.
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")
Query parameters for study search.
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_id
Patient ID for filtering by patient (exact match or wildcard)