33constexpr const char* uid_root =
"1.2.826.0.1.3680043.2.1545";
41network::dimse::dimse_message make_n_create_rq(
43 std::string_view sop_instance_uid,
44 core::dicom_dataset dataset) {
46 using namespace network::dimse;
48 dimse_message msg{command_field::n_create_rq,
message_id};
50 msg.set_affected_sop_instance_uid(sop_instance_uid);
51 msg.set_dataset(std::move(dataset));
59network::dimse::dimse_message make_n_set_rq(
61 std::string_view sop_instance_uid,
62 core::dicom_dataset modifications) {
64 using namespace network::dimse;
65 using namespace encoding;
67 dimse_message msg{command_field::n_set_rq,
message_id};
71 msg.command_set().set_string(
72 tag_requested_sop_instance_uid,
74 std::string(sop_instance_uid));
75 msg.set_dataset(std::move(modifications));
87 : logger_(logger ? std::move(logger) : di::null_logger()) {}
90 std::shared_ptr<di::ILogger> logger)
91 : logger_(logger ? std::move(logger) : di::null_logger()),
102 using namespace network::dimse;
104 auto start_time = std::chrono::steady_clock::now();
110 "Association not established");
118 "No accepted presentation context for MPPS SOP Class");
127 if (mpps_uid.empty()) {
130 "MPPS SOP Instance UID is required");
137 auto request = make_n_create_rq(
next_message_id(), mpps_uid, std::move(dataset));
139 logger_->debug(
"Sending N-CREATE request for MPPS: " + mpps_uid);
142 auto send_result = assoc.
send_dimse(*context_id, request);
143 if (send_result.is_err()) {
144 logger_->error(
"Failed to send N-CREATE: " + send_result.error().message);
145 return send_result.error();
150 if (recv_result.is_err()) {
151 logger_->error(
"Failed to receive N-CREATE response: " + recv_result.error().message);
152 return recv_result.error();
155 const auto& [recv_context_id, response] = recv_result.value();
158 if (response.command() != command_field::n_create_rsp) {
161 "Expected N-CREATE-RSP but received " +
162 std::string(
to_string(response.command())));
165 auto end_time = std::chrono::steady_clock::now();
166 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
167 end_time - start_time);
172 result.
status =
static_cast<uint16_t
>(response.status());
176 if (response.command_set().contains(tag_error_comment)) {
177 result.
error_comment = response.command_set().get_string(tag_error_comment);
184 logger_->info(
"N-CREATE successful for MPPS: " + mpps_uid);
186 logger_->warn(
"N-CREATE returned status 0x" +
187 std::to_string(result.
status) +
" for MPPS: " + mpps_uid);
201 using namespace network::dimse;
203 auto start_time = std::chrono::steady_clock::now();
209 "Association not established");
216 "MPPS SOP Instance UID is required for N-SET");
223 "Cannot set MPPS status back to IN PROGRESS");
231 "No accepted presentation context for MPPS SOP Class");
238 auto request = make_n_set_rq(
246 auto send_result = assoc.
send_dimse(*context_id, request);
247 if (send_result.is_err()) {
248 logger_->error(
"Failed to send N-SET: " + send_result.error().message);
249 return send_result.error();
254 if (recv_result.is_err()) {
255 logger_->error(
"Failed to receive N-SET response: " + recv_result.error().message);
256 return recv_result.error();
259 const auto& [recv_context_id, response] = recv_result.value();
262 if (response.command() != command_field::n_set_rsp) {
265 "Expected N-SET-RSP but received " +
266 std::string(
to_string(response.command())));
269 auto end_time = std::chrono::steady_clock::now();
270 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
271 end_time - start_time);
276 result.
status =
static_cast<uint16_t
>(response.status());
280 if (response.command_set().contains(tag_error_comment)) {
281 result.
error_comment = response.command_set().get_string(tag_error_comment);
289 " (status: " + std::string(to_string(data.
status)) +
")");
291 logger_->warn(
"N-SET returned status 0x" +
292 std::to_string(result.
status) +
" for MPPS: " +
301 std::string_view mpps_uid,
302 const std::vector<performed_series_info>& performed_series) {
311 return set(assoc, data);
316 std::string_view mpps_uid,
317 std::string_view reason) {
326 return set(assoc, data);
351 using namespace core;
352 using namespace encoding;
426 using namespace core;
427 using namespace encoding;
448 dicom_dataset series_item;
450 if (!s.series_uid.empty()) {
453 if (!s.series_description.empty()) {
455 s.series_description);
457 if (!s.modality.empty()) {
460 if (!s.performing_physician.empty()) {
462 s.performing_physician);
464 if (!s.operator_name.empty()) {
468 seq.push_back(std::move(series_item));
480 static std::mt19937_64 gen{std::random_device{}()};
481 static std::uniform_int_distribution<uint64_t> dist;
483 auto now = std::chrono::system_clock::now();
484 auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
485 now.time_since_epoch()).count();
487 return std::string(uid_root) +
"." + std::to_string(timestamp) +
488 "." + std::to_string(dist(gen) % 100000);
492 auto now = std::time(
nullptr);
495 localtime_s(&tm, &now);
497 localtime_r(&now, &tm);
499 std::ostringstream oss;
500 oss << std::put_time(&tm,
"%Y%m%d");
505 auto now = std::time(
nullptr);
508 localtime_s(&tm, &now);
510 localtime_r(&now, &tm);
512 std::ostringstream oss;
513 oss << std::put_time(&tm,
"%H%M%S");
bool is_established() const noexcept
Check if association is established and ready for DIMSE.
Result< std::monostate > send_dimse(uint8_t context_id, const dimse::dimse_message &msg)
Send a DIMSE message.
Result< std::pair< uint8_t, dimse::dimse_message > > receive_dimse(duration timeout=default_timeout)
Receive a DIMSE message.
std::optional< uint8_t > accepted_context_id(std::string_view abstract_syntax) const
Get the presentation context ID for an abstract syntax.
size_t sets_performed() const noexcept
Get the number of N-SET operations performed.
network::Result< mpps_result > create(network::association &assoc, const mpps_create_data &data)
Create a new MPPS instance (N-CREATE)
core::dicom_dataset build_create_dataset(const mpps_create_data &data) const
Build DICOM dataset for N-CREATE request.
void reset_statistics() noexcept
Reset statistics counters to zero.
std::shared_ptr< di::ILogger > logger_
Logger instance.
std::atomic< uint16_t > message_id_counter_
Message ID counter.
mpps_scu_config config_
Configuration.
size_t creates_performed() const noexcept
Get the number of N-CREATE operations performed.
network::Result< mpps_result > discontinue(network::association &assoc, std::string_view mpps_uid, std::string_view reason="")
Discontinue an MPPS instance (convenience method)
core::dicom_dataset build_set_dataset(const mpps_set_data &data) const
Build DICOM dataset for N-SET request.
mpps_scu(std::shared_ptr< di::ILogger > logger=nullptr)
Construct MPPS SCU with default configuration.
network::Result< mpps_result > set(network::association &assoc, const mpps_set_data &data)
Update an existing MPPS instance (N-SET)
std::string get_current_time() const
Get current time in DICOM TM format (HHMMSS)
std::atomic< size_t > creates_performed_
Statistics: N-CREATE operations performed.
std::atomic< size_t > sets_performed_
Statistics: N-SET operations performed.
std::string get_current_date() const
Get current date in DICOM DA format (YYYYMMDD)
uint16_t next_message_id() noexcept
Get the next message ID for DIMSE operations.
std::string generate_mpps_uid() const
Generate a unique MPPS SOP Instance UID.
network::Result< mpps_result > complete(network::association &assoc, std::string_view mpps_uid, const std::vector< performed_series_info > &performed_series)
Complete an MPPS instance (convenience method)
DIMSE command field enumeration.
Compile-time constants for commonly used DICOM tags.
DICOM MPPS (Modality Performed Procedure Step) SCU service.
constexpr int mpps_invalid_status_transition
constexpr int mpps_context_not_accepted
constexpr int mpps_missing_uid
constexpr int mpps_unexpected_command
constexpr int association_not_established
constexpr core::dicom_tag tag_requested_sop_instance_uid
Requested SOP Instance UID (0000,1001) - UI.
@ completed
Procedure completed successfully.
@ discontinued
Procedure was stopped/cancelled.
@ in_progress
Procedure is currently being performed.
auto to_string(mpps_status status) -> std::string_view
Convert mpps_status to DICOM string representation.
constexpr std::string_view mpps_sop_class_uid
MPPS (Modality Performed Procedure Step) SOP Class UID.
Result< T > pacs_error(int code, const std::string &message, const std::string &details="")
Create a PACS error result with module context.
Result<T> type aliases and helpers for PACS system.
Data for N-CREATE operation (start procedure)
std::string accession_number
std::string procedure_step_start_date
DICOM DA format (YYYYMMDD)
std::string procedure_step_start_time
DICOM TM format (HHMMSS)
std::string scheduled_procedure_step_id
std::string patient_birth_date
std::string station_ae_title
std::string performing_physician
std::string mpps_sop_instance_uid
Generated if empty.
std::string procedure_description
std::string operator_name
std::string study_instance_uid
Result of an MPPS operation.
std::chrono::milliseconds elapsed
Time taken for the operation.
bool is_success() const noexcept
Check if the operation was successful.
uint16_t status
DIMSE status code (0x0000 = success)
std::string mpps_sop_instance_uid
MPPS SOP Instance UID.
std::string error_comment
Error comment from the SCP (if any)
Configuration for MPPS SCU service.
std::chrono::milliseconds timeout
Timeout for receiving DIMSE response.
bool auto_generate_uid
Auto-generate MPPS UID if not provided.
Data for N-SET operation (update/complete procedure)
mpps_status status
New status (COMPLETED or DISCONTINUED)
std::string procedure_step_end_date
Procedure Step End Date (required for COMPLETED/DISCONTINUED)
std::string discontinuation_reason
Discontinuation reason (for DISCONTINUED status)
std::vector< performed_series_info > performed_series
Performed Series Sequence (for COMPLETED status)
std::string procedure_step_end_time
Procedure Step End Time (required for COMPLETED/DISCONTINUED)
std::string mpps_sop_instance_uid
MPPS SOP Instance UID (required)