31constexpr const char* uid_root =
"1.2.826.0.1.3680043.2.1545.1";
39network::dimse::dimse_message make_n_create_rq(
41 std::string_view sop_class_uid,
42 std::string_view sop_instance_uid,
43 core::dicom_dataset dataset) {
45 using namespace network::dimse;
47 dimse_message msg{command_field::n_create_rq,
message_id};
48 msg.set_affected_sop_class_uid(sop_class_uid);
50 msg.set_affected_sop_instance_uid(sop_instance_uid);
52 msg.set_dataset(std::move(dataset));
60network::dimse::dimse_message make_n_set_rq(
62 std::string_view sop_class_uid,
63 std::string_view sop_instance_uid,
64 core::dicom_dataset modifications) {
66 using namespace network::dimse;
67 using namespace encoding;
69 dimse_message msg{command_field::n_set_rq,
message_id};
70 msg.set_affected_sop_class_uid(sop_class_uid);
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));
83network::dimse::dimse_message make_n_get_rq(
85 std::string_view sop_class_uid,
86 std::string_view sop_instance_uid) {
88 using namespace network::dimse;
89 using namespace encoding;
91 dimse_message msg{command_field::n_get_rq,
message_id};
92 msg.set_affected_sop_class_uid(sop_class_uid);
93 msg.command_set().set_string(
94 tag_requested_sop_instance_uid,
96 std::string(sop_instance_uid));
104network::dimse::dimse_message make_n_action_rq(
106 std::string_view sop_class_uid,
107 std::string_view sop_instance_uid,
108 uint16_t action_type_id) {
110 using namespace network::dimse;
112 dimse_message msg{command_field::n_action_rq,
message_id};
113 msg.set_affected_sop_class_uid(sop_class_uid);
114 msg.set_affected_sop_instance_uid(sop_instance_uid);
115 msg.set_action_type_id(action_type_id);
123network::dimse::dimse_message make_n_delete_rq(
125 std::string_view sop_class_uid,
126 std::string_view sop_instance_uid) {
128 using namespace network::dimse;
130 dimse_message msg{command_field::n_delete_rq,
message_id};
131 msg.set_affected_sop_class_uid(sop_class_uid);
132 msg.set_affected_sop_instance_uid(sop_instance_uid);
144 : logger_(logger ? std::move(logger) : di::null_logger()) {}
147 std::shared_ptr<di::ILogger> logger)
148 : logger_(logger ? std::move(logger) : di::null_logger()),
159 using namespace network::dimse;
160 using namespace encoding;
162 auto start_time = std::chrono::steady_clock::now();
167 "Association not established");
174 "No accepted presentation context for Film Session SOP Class");
199 auto request = make_n_create_rq(
205 logger_->debug(
"Sending N-CREATE Film Session: " + session_uid);
207 auto send_result = assoc.
send_dimse(*context_id, request);
208 if (send_result.is_err()) {
209 logger_->error(
"Failed to send N-CREATE Film Session: " + send_result.error().message);
210 return send_result.error();
214 if (recv_result.is_err()) {
215 logger_->error(
"Failed to receive N-CREATE Film Session response: " +
216 recv_result.error().message);
217 return recv_result.error();
220 const auto& [recv_context_id, response] = recv_result.value();
222 if (response.command() != command_field::n_create_rsp) {
225 "Expected N-CREATE-RSP but received " +
226 std::string(
to_string(response.command())));
229 auto end_time = std::chrono::steady_clock::now();
233 ? session_uid : std::string(response.affected_sop_instance_uid());
234 result.
status =
static_cast<uint16_t
>(response.status());
235 result.
elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
236 end_time - start_time);
238 if (response.command_set().contains(tag_error_comment)) {
239 result.
error_comment = response.command_set().get_string(tag_error_comment);
247 logger_->warn(
"N-CREATE Film Session status 0x" +
256 std::string_view session_uid) {
258 using namespace network::dimse;
260 auto start_time = std::chrono::steady_clock::now();
265 "Association not established");
272 "No accepted presentation context for Film Session SOP Class");
275 auto request = make_n_delete_rq(
280 logger_->debug(
"Sending N-DELETE Film Session: " + std::string(session_uid));
282 auto send_result = assoc.
send_dimse(*context_id, request);
283 if (send_result.is_err()) {
284 logger_->error(
"Failed to send N-DELETE Film Session: " + send_result.error().message);
285 return send_result.error();
289 if (recv_result.is_err()) {
290 logger_->error(
"Failed to receive N-DELETE Film Session response: " +
291 recv_result.error().message);
292 return recv_result.error();
295 const auto& [recv_context_id, response] = recv_result.value();
297 if (response.command() != command_field::n_delete_rsp) {
300 "Expected N-DELETE-RSP but received " +
301 std::string(
to_string(response.command())));
304 auto end_time = std::chrono::steady_clock::now();
308 result.
status =
static_cast<uint16_t
>(response.status());
309 result.
elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
310 end_time - start_time);
312 if (response.command_set().contains(tag_error_comment)) {
313 result.
error_comment = response.command_set().get_string(tag_error_comment);
317 logger_->info(
"N-DELETE Film Session successful: " + std::string(session_uid));
319 logger_->warn(
"N-DELETE Film Session status 0x" +
320 std::to_string(result.
status) +
": " + std::string(session_uid));
334 using namespace network::dimse;
335 using namespace encoding;
337 auto start_time = std::chrono::steady_clock::now();
342 "Association not established");
349 "No accepted presentation context for Film Box SOP Class");
378 seq.push_back(std::move(ref_item));
381 auto request = make_n_create_rq(
387 logger_->debug(
"Sending N-CREATE Film Box");
389 auto send_result = assoc.
send_dimse(*context_id, request);
390 if (send_result.is_err()) {
391 logger_->error(
"Failed to send N-CREATE Film Box: " + send_result.error().message);
392 return send_result.error();
396 if (recv_result.is_err()) {
397 logger_->error(
"Failed to receive N-CREATE Film Box response: " +
398 recv_result.error().message);
399 return recv_result.error();
402 const auto& [recv_context_id, response] = recv_result.value();
404 if (response.command() != command_field::n_create_rsp) {
407 "Expected N-CREATE-RSP but received " +
408 std::string(
to_string(response.command())));
411 auto end_time = std::chrono::steady_clock::now();
414 result.
sop_instance_uid = std::string(response.affected_sop_instance_uid());
415 result.
status =
static_cast<uint16_t
>(response.status());
416 result.
elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
417 end_time - start_time);
419 if (response.command_set().contains(tag_error_comment)) {
420 result.
error_comment = response.command_set().get_string(tag_error_comment);
424 if (response.has_dataset()) {
433 logger_->warn(
"N-CREATE Film Box status 0x" +
434 std::to_string(result.
status));
442 std::string_view film_box_uid) {
444 using namespace network::dimse;
446 auto start_time = std::chrono::steady_clock::now();
451 "Association not established");
458 "No accepted presentation context for Film Box SOP Class");
462 auto request = make_n_action_rq(
468 logger_->debug(
"Sending N-ACTION Print Film Box: " + std::string(film_box_uid));
470 auto send_result = assoc.
send_dimse(*context_id, request);
471 if (send_result.is_err()) {
472 logger_->error(
"Failed to send N-ACTION Print: " + send_result.error().message);
473 return send_result.error();
477 if (recv_result.is_err()) {
478 logger_->error(
"Failed to receive N-ACTION Print response: " +
479 recv_result.error().message);
480 return recv_result.error();
483 const auto& [recv_context_id, response] = recv_result.value();
485 if (response.command() != command_field::n_action_rsp) {
488 "Expected N-ACTION-RSP but received " +
489 std::string(
to_string(response.command())));
492 auto end_time = std::chrono::steady_clock::now();
496 result.
status =
static_cast<uint16_t
>(response.status());
497 result.
elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
498 end_time - start_time);
500 if (response.command_set().contains(tag_error_comment)) {
501 result.
error_comment = response.command_set().get_string(tag_error_comment);
507 logger_->info(
"N-ACTION Print successful: " + std::string(film_box_uid));
509 logger_->warn(
"N-ACTION Print status 0x" +
510 std::to_string(result.
status) +
": " + std::string(film_box_uid));
518 std::string_view film_box_uid) {
520 using namespace network::dimse;
522 auto start_time = std::chrono::steady_clock::now();
527 "Association not established");
534 "No accepted presentation context for Film Box SOP Class");
537 auto request = make_n_delete_rq(
542 logger_->debug(
"Sending N-DELETE Film Box: " + std::string(film_box_uid));
544 auto send_result = assoc.
send_dimse(*context_id, request);
545 if (send_result.is_err()) {
546 logger_->error(
"Failed to send N-DELETE Film Box: " + send_result.error().message);
547 return send_result.error();
551 if (recv_result.is_err()) {
552 logger_->error(
"Failed to receive N-DELETE Film Box response: " +
553 recv_result.error().message);
554 return recv_result.error();
557 const auto& [recv_context_id, response] = recv_result.value();
559 if (response.command() != command_field::n_delete_rsp) {
562 "Expected N-DELETE-RSP but received " +
563 std::string(
to_string(response.command())));
566 auto end_time = std::chrono::steady_clock::now();
570 result.
status =
static_cast<uint16_t
>(response.status());
571 result.
elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
572 end_time - start_time);
574 if (response.command_set().contains(tag_error_comment)) {
575 result.
error_comment = response.command_set().get_string(tag_error_comment);
579 logger_->info(
"N-DELETE Film Box successful: " + std::string(film_box_uid));
581 logger_->warn(
"N-DELETE Film Box status 0x" +
582 std::to_string(result.
status) +
": " + std::string(film_box_uid));
594 std::string_view image_box_uid,
598 using namespace network::dimse;
599 using namespace encoding;
601 auto start_time = std::chrono::steady_clock::now();
606 "Association not established");
609 auto sop_class_uid = use_color
617 "No accepted presentation context for Image Box SOP Class");
628 auto request = make_n_set_rq(
634 logger_->debug(
"Sending N-SET Image Box: " + std::string(image_box_uid));
636 auto send_result = assoc.
send_dimse(*context_id, request);
637 if (send_result.is_err()) {
638 logger_->error(
"Failed to send N-SET Image Box: " + send_result.error().message);
639 return send_result.error();
643 if (recv_result.is_err()) {
644 logger_->error(
"Failed to receive N-SET Image Box response: " +
645 recv_result.error().message);
646 return recv_result.error();
649 const auto& [recv_context_id, response] = recv_result.value();
651 if (response.command() != command_field::n_set_rsp) {
654 "Expected N-SET-RSP but received " +
655 std::string(
to_string(response.command())));
658 auto end_time = std::chrono::steady_clock::now();
662 result.
status =
static_cast<uint16_t
>(response.status());
663 result.
elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
664 end_time - start_time);
666 if (response.command_set().contains(tag_error_comment)) {
667 result.
error_comment = response.command_set().get_string(tag_error_comment);
670 images_set_.fetch_add(1, std::memory_order_relaxed);
673 logger_->info(
"N-SET Image Box successful: " + std::string(image_box_uid));
675 logger_->warn(
"N-SET Image Box status 0x" +
676 std::to_string(result.
status) +
": " + std::string(image_box_uid));
689 using namespace network::dimse;
691 auto start_time = std::chrono::steady_clock::now();
696 "Association not established");
703 "No accepted presentation context for Printer SOP Class");
707 constexpr std::string_view printer_instance_uid =
708 "1.2.840.10008.5.1.1.17";
710 auto request = make_n_get_rq(
713 printer_instance_uid);
715 logger_->debug(
"Sending N-GET Printer Status");
717 auto send_result = assoc.
send_dimse(*context_id, request);
718 if (send_result.is_err()) {
719 logger_->error(
"Failed to send N-GET Printer Status: " + send_result.error().message);
720 return send_result.error();
724 if (recv_result.is_err()) {
725 logger_->error(
"Failed to receive N-GET Printer Status response: " +
726 recv_result.error().message);
727 return recv_result.error();
730 const auto& [recv_context_id, response] = recv_result.value();
732 if (response.command() != command_field::n_get_rsp) {
735 "Expected N-GET-RSP but received " +
736 std::string(
to_string(response.command())));
739 auto end_time = std::chrono::steady_clock::now();
743 result.
status =
static_cast<uint16_t
>(response.status());
744 result.
elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
745 end_time - start_time);
747 if (response.command_set().contains(tag_error_comment)) {
748 result.
error_comment = response.command_set().get_string(tag_error_comment);
751 if (response.has_dataset()) {
758 logger_->info(
"N-GET Printer Status successful");
760 logger_->warn(
"N-GET Printer Status returned 0x" +
761 std::to_string(result.
status));
780 return images_set_.load(std::memory_order_relaxed);
805 std::string_view sop_class_uid)
const {
824 static std::mt19937_64 gen{std::random_device{}()};
825 static std::uniform_int_distribution<uint64_t> dist;
827 auto now = std::chrono::system_clock::now();
828 auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
829 now.time_since_epoch()).count();
831 return std::string(uid_root) +
"." + std::to_string(timestamp) +
832 "." + std::to_string(dist(gen) % 100000);
auto get(dicom_tag tag) noexcept -> dicom_element *
Get a pointer to the element with the given tag.
void set_string(dicom_tag tag, encoding::vr_type vr, std::string_view value)
Set a string value for the given tag.
auto get_or_create_sequence(dicom_tag tag) -> std::vector< dicom_dataset > &
Insert or create a sequence 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.
print_scu(std::shared_ptr< di::ILogger > logger=nullptr)
Construct Print SCU with default configuration.
size_t film_boxes_created() const noexcept
void reset_statistics() noexcept
network::Result< print_result > create_film_session(network::association &assoc, const print_session_data &data)
Create a new Film Session (N-CREATE)
network::Result< print_result > print_film_box(network::association &assoc, std::string_view film_box_uid)
Print a Film Box (N-ACTION)
network::Result< print_result > create_film_box(network::association &assoc, const print_film_box_data &data)
Create a new Film Box (N-CREATE)
size_t prints_executed() const noexcept
std::atomic< size_t > sessions_created_
Statistics.
size_t sessions_created() const noexcept
network::Result< print_result > delete_film_box(network::association &assoc, std::string_view film_box_uid)
Delete a Film Box (N-DELETE)
network::Result< print_result > delete_film_session(network::association &assoc, std::string_view session_uid)
Delete a Film Session (N-DELETE)
std::optional< uint8_t > find_print_context(network::association &assoc, std::string_view sop_class_uid) const
Find an accepted presentation context for a print SOP class.
network::Result< print_result > query_printer_status(network::association &assoc)
Query printer status (N-GET)
size_t printer_queries() const noexcept
std::atomic< uint16_t > message_id_counter_
Message ID counter.
std::atomic< size_t > printer_queries_
std::string generate_uid() const
Generate a unique SOP Instance UID.
print_scu_config config_
Configuration.
network::Result< print_result > set_image_box(network::association &assoc, std::string_view image_box_uid, const print_image_data &data, bool use_color=false)
Set Image Box pixel data (N-SET)
std::shared_ptr< di::ILogger > logger_
Logger instance.
size_t images_set() const noexcept
std::atomic< size_t > film_boxes_created_
std::atomic< size_t > prints_executed_
uint16_t next_message_id() noexcept
Get the next message ID for DIMSE operations.
std::atomic< size_t > images_set_
DIMSE command field enumeration.
Compile-time constants for commonly used DICOM tags.
constexpr int print_invalid_sop_class
constexpr int print_unexpected_command
constexpr int association_not_established
constexpr core::dicom_tag tag_requested_sop_instance_uid
Requested SOP Instance UID (0000,1001) - UI.
constexpr std::string_view basic_grayscale_print_meta_sop_class_uid
Basic Grayscale Print Management Meta SOP Class UID.
constexpr std::string_view basic_color_print_meta_sop_class_uid
Basic Color Print Management Meta SOP Class UID.
constexpr std::string_view basic_film_session_sop_class_uid
Basic Film Session SOP Class UID.
constexpr std::string_view basic_color_image_box_sop_class_uid
Basic Color Image Box SOP Class UID.
constexpr std::string_view basic_film_box_sop_class_uid
Basic Film Box SOP Class UID.
auto to_string(mpps_status status) -> std::string_view
Convert mpps_status to DICOM string representation.
constexpr std::string_view basic_grayscale_image_box_sop_class_uid
Basic Grayscale Image Box SOP Class UID.
constexpr std::string_view printer_sop_class_uid
Printer 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.
DICOM Print Management SCU service (PS3.4 Annex H)
Result<T> type aliases and helpers for PACS system.
Data for creating a Film Box via N-CREATE.
std::string magnification_type
Magnification type (REPLICATE, BILINEAR, CUBIC, NONE)
std::string film_orientation
Film orientation (PORTRAIT, LANDSCAPE)
std::string film_size_id
Film size ID (8INX10IN, 14INX17IN, etc.)
std::string image_display_format
Image display format (e.g., "STANDARD\\1,1")
std::string film_session_uid
Parent film session SOP Instance UID.
Data for setting an Image Box via N-SET.
core::dicom_dataset pixel_data
Pixel data to set on the image box.
uint16_t image_position
Image position within the film box (1-based)
Result of a Print DIMSE-N operation.
core::dicom_dataset response_data
Response dataset (e.g., printer status, referenced image box UIDs)
std::chrono::milliseconds elapsed
Time taken for the operation.
std::string error_comment
Error comment from the SCP (if any)
uint16_t status
DIMSE status code (0x0000 = success)
std::string sop_instance_uid
SOP Instance UID (session, film box, image box, or printer)
bool is_success() const noexcept
Check if the operation was successful.
Configuration for Print SCU service.
bool auto_generate_uid
Auto-generate SOP Instance UIDs if not provided.
std::chrono::milliseconds timeout
Timeout for receiving DIMSE response.
Data for creating a Film Session via N-CREATE.
std::string medium_type
Medium type (PAPER, CLEAR FILM, BLUE FILM)
std::string film_session_label
Film session label.
std::string film_destination
Film destination (MAGAZINE, PROCESSOR)
std::string sop_instance_uid
SOP Instance UID (generated if empty)
uint32_t number_of_copies
Number of copies to print.
std::string print_priority
Print priority (HIGH, MED, LOW)