27constexpr const char* uid_root =
"1.2.826.0.1.3680043.2.1545.3";
35network::dimse::dimse_message make_ups_n_create_rq(
37 std::string_view sop_instance_uid,
38 core::dicom_dataset dataset) {
40 using namespace network::dimse;
42 dimse_message msg{command_field::n_create_rq,
message_id};
44 msg.set_affected_sop_instance_uid(sop_instance_uid);
45 msg.set_dataset(std::move(dataset));
53network::dimse::dimse_message make_ups_n_set_rq(
55 std::string_view sop_instance_uid,
56 core::dicom_dataset modifications) {
58 using namespace network::dimse;
59 using namespace encoding;
61 dimse_message msg{command_field::n_set_rq,
message_id};
63 msg.command_set().set_string(
64 tag_requested_sop_instance_uid,
66 std::string(sop_instance_uid));
67 msg.set_dataset(std::move(modifications));
79 : logger_(logger ? std::move(logger) : di::null_logger()) {}
82 std::shared_ptr<di::ILogger> logger)
83 : logger_(logger ? std::move(logger) : di::null_logger()),
94 using namespace network::dimse;
96 auto start_time = std::chrono::steady_clock::now();
102 "Association not established");
110 "No accepted presentation context for UPS Push SOP Class");
122 "Workitem UID is required");
131 logger_->debug(
"Sending N-CREATE request for UPS workitem: " +
uid);
134 auto send_result = assoc.
send_dimse(*context_id, request);
135 if (send_result.is_err()) {
136 logger_->error(
"Failed to send N-CREATE: " + send_result.error().message);
137 return send_result.error();
142 if (recv_result.is_err()) {
143 logger_->error(
"Failed to receive N-CREATE response: " +
144 recv_result.error().message);
145 return recv_result.error();
148 const auto& [recv_context_id, response] = recv_result.value();
151 if (response.command() != command_field::n_create_rsp) {
154 "Expected N-CREATE-RSP but received " +
155 std::string(
to_string(response.command())));
158 auto end_time = std::chrono::steady_clock::now();
159 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
160 end_time - start_time);
165 result.
status =
static_cast<uint16_t
>(response.status());
168 if (response.command_set().contains(tag_error_comment)) {
169 result.
error_comment = response.command_set().get_string(tag_error_comment);
175 logger_->info(
"N-CREATE successful for UPS workitem: " +
uid);
177 logger_->warn(
"N-CREATE returned status 0x" +
178 std::to_string(result.
status) +
179 " for UPS workitem: " +
uid);
193 using namespace network::dimse;
195 auto start_time = std::chrono::steady_clock::now();
200 "Association not established");
206 "Workitem UID is required for N-SET");
213 "No accepted presentation context for UPS Push SOP Class");
217 auto request = make_ups_n_set_rq(
224 auto send_result = assoc.
send_dimse(*context_id, request);
225 if (send_result.is_err()) {
226 logger_->error(
"Failed to send N-SET: " + send_result.error().message);
227 return send_result.error();
231 if (recv_result.is_err()) {
232 logger_->error(
"Failed to receive N-SET response: " +
233 recv_result.error().message);
234 return recv_result.error();
237 const auto& [recv_context_id, response] = recv_result.value();
239 if (response.command() != command_field::n_set_rsp) {
242 "Expected N-SET-RSP but received " +
243 std::string(
to_string(response.command())));
246 auto end_time = std::chrono::steady_clock::now();
247 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
248 end_time - start_time);
252 result.
status =
static_cast<uint16_t
>(response.status());
255 if (response.command_set().contains(tag_error_comment)) {
256 result.
error_comment = response.command_set().get_string(tag_error_comment);
264 logger_->warn(
"N-SET returned status 0x" +
265 std::to_string(result.
status) +
280 using namespace network::dimse;
282 auto start_time = std::chrono::steady_clock::now();
287 "Association not established");
293 "Workitem UID is required for N-GET");
300 "No accepted presentation context for UPS Push SOP Class");
304 auto request = make_n_get_rq(
315 auto send_result = assoc.
send_dimse(*context_id, request);
316 if (send_result.is_err()) {
317 logger_->error(
"Failed to send N-GET: " + send_result.error().message);
318 return send_result.error();
322 if (recv_result.is_err()) {
323 logger_->error(
"Failed to receive N-GET response: " +
324 recv_result.error().message);
325 return recv_result.error();
328 const auto& [recv_context_id, response] = recv_result.value();
330 if (response.command() != command_field::n_get_rsp) {
333 "Expected N-GET-RSP but received " +
334 std::string(
to_string(response.command())));
337 auto end_time = std::chrono::steady_clock::now();
338 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
339 end_time - start_time);
343 result.
status =
static_cast<uint16_t
>(response.status());
346 if (response.command_set().contains(tag_error_comment)) {
347 result.
error_comment = response.command_set().get_string(tag_error_comment);
351 if (response.has_dataset()) {
352 auto dataset_result = response.dataset();
353 if (dataset_result.is_ok()) {
363 logger_->warn(
"N-GET returned status 0x" +
364 std::to_string(result.
status) +
379 using namespace network::dimse;
381 auto start_time = std::chrono::steady_clock::now();
386 "Association not established");
392 "Workitem UID is required for state change");
398 "Transaction UID is required for state change");
405 "No accepted presentation context for UPS Push SOP Class");
409 auto request = make_n_action_rq(
417 request.set_dataset(std::move(action_dataset));
419 logger_->debug(
"Sending N-ACTION (Change State) for UPS workitem: " +
422 auto send_result = assoc.
send_dimse(*context_id, request);
423 if (send_result.is_err()) {
424 logger_->error(
"Failed to send N-ACTION: " + send_result.error().message);
425 return send_result.error();
429 if (recv_result.is_err()) {
430 logger_->error(
"Failed to receive N-ACTION response: " +
431 recv_result.error().message);
432 return recv_result.error();
435 const auto& [recv_context_id, response] = recv_result.value();
437 if (response.command() != command_field::n_action_rsp) {
440 "Expected N-ACTION-RSP but received " +
441 std::string(
to_string(response.command())));
444 auto end_time = std::chrono::steady_clock::now();
445 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
446 end_time - start_time);
450 result.
status =
static_cast<uint16_t
>(response.status());
453 if (response.command_set().contains(tag_error_comment)) {
454 result.
error_comment = response.command_set().get_string(tag_error_comment);
460 logger_->info(
"N-ACTION (Change State) successful for UPS workitem: " +
463 logger_->warn(
"N-ACTION returned status 0x" +
464 std::to_string(result.
status) +
475 using namespace network::dimse;
477 auto start_time = std::chrono::steady_clock::now();
482 "Association not established");
488 "Workitem UID is required for cancel request");
495 "No accepted presentation context for UPS Push SOP Class");
499 auto request = make_n_action_rq(
506 if (!data.
reason.empty()) {
508 request.set_dataset(std::move(cancel_dataset));
511 logger_->debug(
"Sending N-ACTION (Request Cancel) for UPS workitem: " +
514 auto send_result = assoc.
send_dimse(*context_id, request);
515 if (send_result.is_err()) {
516 logger_->error(
"Failed to send N-ACTION: " + send_result.error().message);
517 return send_result.error();
521 if (recv_result.is_err()) {
522 logger_->error(
"Failed to receive N-ACTION response: " +
523 recv_result.error().message);
524 return recv_result.error();
527 const auto& [recv_context_id, response] = recv_result.value();
529 if (response.command() != command_field::n_action_rsp) {
532 "Expected N-ACTION-RSP but received " +
533 std::string(
to_string(response.command())));
536 auto end_time = std::chrono::steady_clock::now();
537 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
538 end_time - start_time);
542 result.
status =
static_cast<uint16_t
>(response.status());
545 if (response.command_set().contains(tag_error_comment)) {
546 result.
error_comment = response.command_set().get_string(tag_error_comment);
552 logger_->info(
"N-ACTION (Request Cancel) successful for UPS workitem: " +
555 logger_->warn(
"N-ACTION (Request Cancel) returned status 0x" +
556 std::to_string(result.
status) +
597 using namespace core;
598 using namespace encoding;
626 dicom_tag{0x0040, 0x4005}, vr_type::DT,
633 dicom_tag{0x0040, 0x4011}, vr_type::DT,
640 dicom_tag{0x0040, 0x4001}, vr_type::CS,
650 using namespace core;
651 using namespace encoding;
669 using namespace core;
670 using namespace encoding;
674 if (!data.
reason.empty()) {
687 static std::mt19937_64 gen{std::random_device{}()};
688 static std::uniform_int_distribution<uint64_t> dist;
690 auto now = std::chrono::system_clock::now();
691 auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
692 now.time_since_epoch()).count();
694 return std::string(uid_root) +
"." + std::to_string(timestamp) +
695 "." + std::to_string(dist(gen) % 100000);
auto get(dicom_tag tag) noexcept -> dicom_element *
Get a pointer to the element with the given tag.
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 creates_performed() const noexcept
network::Result< ups_result > request_cancel(network::association &assoc, const ups_request_cancel_data &data)
Request cancellation of a UPS workitem (N-ACTION Type 3)
std::atomic< size_t > gets_performed_
std::atomic< size_t > sets_performed_
core::dicom_dataset build_request_cancel_dataset(const ups_request_cancel_data &data) const
ups_push_scu_config config_
std::string generate_workitem_uid() const
network::Result< ups_result > set(network::association &assoc, const ups_set_data &data)
Modify an existing UPS workitem (N-SET)
size_t sets_performed() const noexcept
size_t gets_performed() const noexcept
network::Result< ups_result > change_state(network::association &assoc, const ups_change_state_data &data)
Change UPS workitem state (N-ACTION Type 1)
std::atomic< size_t > actions_performed_
size_t actions_performed() const noexcept
uint16_t next_message_id() noexcept
ups_push_scu(std::shared_ptr< di::ILogger > logger=nullptr)
Construct UPS Push SCU with default configuration.
std::shared_ptr< di::ILogger > logger_
network::Result< ups_result > get(network::association &assoc, const ups_get_data &data)
Retrieve UPS workitem attributes from remote SCP (N-GET)
core::dicom_dataset build_create_dataset(const ups_create_data &data) const
void reset_statistics() noexcept
core::dicom_dataset build_change_state_dataset(const ups_change_state_data &data) const
std::atomic< size_t > creates_performed_
std::atomic< uint16_t > message_id_counter_
network::Result< ups_result > create(network::association &assoc, const ups_create_data &data)
Create a new UPS workitem on the remote SCP (N-CREATE)
DIMSE command field enumeration.
Compile-time constants for commonly used DICOM tags.
constexpr int ups_unexpected_command
constexpr int ups_context_not_accepted
constexpr int ups_missing_transaction_uid
constexpr int ups_missing_uid
constexpr int association_not_established
constexpr core::dicom_tag tag_requested_sop_instance_uid
Requested SOP Instance UID (0000,1001) - UI.
auto to_string(mpps_status status) -> std::string_view
Convert mpps_status to DICOM string representation.
constexpr std::string_view ups_push_sop_class_uid
UPS Push SOP Class UID (PS3.4 Table CC.2-1)
constexpr uint16_t ups_action_change_state
N-ACTION Type 1: Change UPS State (PS3.4 CC.2.4)
constexpr uint16_t ups_action_request_cancel
N-ACTION Type 3: Request Cancellation (PS3.4 CC.2.5)
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-ACTION Type 1 (change UPS state)
std::string requested_state
Requested new state: "IN PROGRESS", "COMPLETED", "CANCELED".
std::string workitem_uid
Workitem SOP Instance UID (required)
std::string transaction_uid
Transaction UID (required for claiming/completing/canceling)
Data for N-CREATE operation (create new workitem)
std::string priority
Priority: LOW, MEDIUM, HIGH.
std::string procedure_step_label
Procedure Step Label (required)
std::string scheduled_station_name
Scheduled Station Name AE.
std::string expected_completion_datetime
Expected completion date/time (DICOM DT format)
std::string workitem_uid
Workitem SOP Instance UID (generated if empty)
std::string worklist_label
Worklist Label.
std::string scheduled_start_datetime
Scheduled start date/time (DICOM DT format)
Data for N-GET operation (retrieve workitem)
std::string workitem_uid
Workitem SOP Instance UID (required)
std::vector< core::dicom_tag > attribute_tags
Specific attribute tags to retrieve (empty = all)
Configuration for UPS Push SCU service.
std::chrono::milliseconds timeout
Timeout for receiving DIMSE response.
bool auto_generate_uid
Auto-generate workitem UID if not provided.
Data for N-ACTION Type 3 (request cancellation)
std::string reason
Reason for cancellation request (optional)
std::string workitem_uid
Workitem SOP Instance UID (required)
Result of a UPS SCU operation.
std::chrono::milliseconds elapsed
Time taken for the operation.
bool is_success() const noexcept
Check if the operation was successful.
std::string workitem_uid
Workitem SOP Instance UID.
uint16_t status
DIMSE status code (0x0000 = success)
core::dicom_dataset attributes
Response dataset (for N-GET operations)
std::string error_comment
Error comment from the SCP (if any)
Data for N-SET operation (modify workitem attributes)
core::dicom_dataset modifications
Modification dataset.
std::string workitem_uid
Workitem SOP Instance UID (required)
DICOM UPS (Unified Procedure Step) Push SCU service.