26 : logger_(logger ? std::move(logger) : di::null_logger()) {}
29 std::shared_ptr<di::ILogger> logger)
30 : logger_(logger ? std::move(logger) : di::null_logger()), config_(config) {}
39 std::string_view destination_ae,
42 return perform_move(assoc, query_keys, destination_ae, progress);
48 std::string_view destination_ae,
51 using namespace network::dimse;
53 auto start_time = std::chrono::steady_clock::now();
60 "Association not established");
64 if (destination_ae.empty()) {
67 "Move destination AE title is required");
78 "No accepted presentation context for SOP Class: " +
79 std::string(sop_class_uid));
83 dimse_message request{command_field::c_move_rq, message_id};
84 request.set_affected_sop_class_uid(sop_class_uid);
88 request.command_set().set_string(
91 std::string(destination_ae));
93 request.set_dataset(query_keys);
95 logger_->debug_fmt(
"Sending C-MOVE request (message_id={}, dest={}, sop_class={})",
96 message_id, destination_ae, sop_class_uid);
99 auto send_result = assoc.
send_dimse(*context_id, request);
100 if (send_result.is_err()) {
101 return send_result.error();
110 bool move_complete =
false;
112 while (!move_complete) {
114 if (recv_result.is_err()) {
115 return recv_result.error();
118 const auto& [recv_context_id, response] = recv_result.value();
121 if (response.command() != command_field::c_move_rsp) {
124 "Expected C-MOVE-RSP but received " +
125 std::string(
to_string(response.command())));
128 auto status = response.status();
131 if (
auto remaining = response.remaining_subops()) {
134 if (
auto completed = response.completed_subops()) {
138 if (
auto failed = response.failed_subops()) {
142 if (
auto warning = response.warning_subops()) {
153 if (status == status_success) {
154 move_complete =
true;
156 }
else if (status == status_cancel) {
157 move_complete =
true;
159 }
else if ((status & 0xF000) == 0xA000 ||
160 (status & 0xF000) == 0xC000) {
162 move_complete =
true;
168 auto end_time = std::chrono::steady_clock::now();
169 result.
elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
170 end_time - start_time);
176 logger_->debug_fmt(
"C-MOVE completed: {} completed, {} failed in {} ms",
199 using namespace network::dimse;
201 auto start_time = std::chrono::steady_clock::now();
208 "Association not established");
219 "No accepted presentation context for SOP Class: " +
220 std::string(sop_class_uid));
224 dimse_message request{command_field::c_get_rq, message_id};
225 request.set_affected_sop_class_uid(sop_class_uid);
227 request.set_dataset(query_keys);
229 logger_->debug_fmt(
"Sending C-GET request (message_id={}, sop_class={})",
230 message_id, sop_class_uid);
233 auto send_result = assoc.
send_dimse(*context_id, request);
234 if (send_result.is_err()) {
235 return send_result.error();
244 bool retrieve_complete =
false;
246 while (!retrieve_complete) {
248 if (recv_result.is_err()) {
249 return recv_result.error();
252 auto& [recv_context_id, msg] = recv_result.value();
253 auto cmd = msg.command();
255 if (cmd == command_field::c_get_rsp) {
256 auto status = msg.status();
259 if (
auto remaining = msg.remaining_subops()) {
262 if (
auto completed = msg.completed_subops()) {
266 if (
auto failed = msg.failed_subops()) {
270 if (
auto warning = msg.warning_subops()) {
281 if (status == status_success) {
282 retrieve_complete =
true;
284 }
else if (status == status_cancel) {
285 retrieve_complete =
true;
287 }
else if ((status & 0xF000) == 0xA000 ||
288 (status & 0xF000) == 0xC000) {
290 retrieve_complete =
true;
294 }
else if (cmd == command_field::c_store_rq) {
296 if (msg.has_dataset()) {
297 auto dataset_result = msg.dataset();
298 if (dataset_result.is_ok()) {
307 auto sop_class = msg.affected_sop_class_uid();
308 auto sop_instance = msg.affected_sop_instance_uid();
310 auto store_rsp = make_c_store_rsp(
317 auto send_rsp_result = assoc.
send_dimse(recv_context_id, store_rsp);
318 if (send_rsp_result.is_err()) {
319 logger_->warn_fmt(
"Failed to send C-STORE response: {}",
320 send_rsp_result.error().message);
325 auto end_time = std::chrono::steady_clock::now();
326 result.
elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
327 end_time - start_time);
333 logger_->debug_fmt(
"C-GET completed: {} completed, {} failed, {} received in {} ms",
346 std::string_view study_uid,
355 "Move destination is required for C-MOVE mode");
359 return get(assoc, query_ds, progress);
365 std::string_view series_uid,
374 "Move destination is required for C-MOVE mode");
378 return get(assoc, query_ds, progress);
384 std::string_view sop_instance_uid,
393 "Move destination is required for C-MOVE mode");
397 return get(assoc, query_ds, progress);
407 uint16_t message_id) {
409 using namespace network::dimse;
419 "No accepted presentation context for cancel");
423 dimse_message cancel_rq{command_field::c_cancel_rq, message_id};
424 return assoc.
send_dimse(*context_id, cancel_rq);
501 std::string_view study_uid)
const {
503 using namespace core;
504 using namespace encoding;
514 std::string_view series_uid)
const {
516 using namespace core;
517 using namespace encoding;
527 std::string_view sop_instance_uid)
const {
529 using namespace core;
530 using namespace encoding;
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.
network::Result< retrieve_result > retrieve_instance(network::association &assoc, std::string_view sop_instance_uid, retrieve_progress_callback progress=nullptr)
Retrieve a single instance by SOP Instance UID.
network::Result< std::monostate > cancel(network::association &assoc, uint16_t message_id)
Send a C-CANCEL request to stop an ongoing retrieve.
std::atomic< size_t > instances_retrieved_
Statistics: total number of instances retrieved.
network::Result< retrieve_result > retrieve_series(network::association &assoc, std::string_view series_uid, retrieve_progress_callback progress=nullptr)
Retrieve a series by Series Instance UID.
retrieve_scu_config config_
Configuration.
network::Result< retrieve_result > perform_get(network::association &assoc, const core::dicom_dataset &query_keys, retrieve_progress_callback progress)
Internal C-GET implementation.
const retrieve_scu_config & config() const noexcept
Get the current configuration.
size_t retrieves_performed() const noexcept
Get the number of retrieves performed since construction.
core::dicom_dataset build_instance_query(std::string_view sop_instance_uid) const
Build query dataset for instance retrieval.
network::Result< retrieve_result > retrieve_study(network::association &assoc, std::string_view study_uid, retrieve_progress_callback progress=nullptr)
Retrieve a study by Study Instance UID.
void set_move_destination(std::string_view ae_title)
Set the move destination AE title.
network::Result< retrieve_result > get(network::association &assoc, const core::dicom_dataset &query_keys, retrieve_progress_callback progress=nullptr)
Perform a C-GET operation with raw dataset.
void reset_statistics() noexcept
Reset statistics counters to zero.
std::string_view get_get_sop_class_uid() const noexcept
Get GET SOP Class UID based on current configuration.
std::atomic< size_t > retrieves_performed_
Statistics: number of retrieves performed.
size_t instances_retrieved() const noexcept
Get the total number of instances retrieved since construction.
uint16_t next_message_id() noexcept
Get the next message ID for DIMSE operations.
std::atomic< uint16_t > message_id_counter_
Message ID counter.
std::atomic< size_t > bytes_retrieved_
Statistics: total bytes retrieved.
void set_config(const retrieve_scu_config &config)
Update the SCU configuration.
network::Result< retrieve_result > move(network::association &assoc, const core::dicom_dataset &query_keys, std::string_view destination_ae, retrieve_progress_callback progress=nullptr)
Perform a C-MOVE operation with raw dataset.
network::Result< retrieve_result > perform_move(network::association &assoc, const core::dicom_dataset &query_keys, std::string_view destination_ae, retrieve_progress_callback progress)
Internal C-MOVE implementation.
std::string_view get_move_sop_class_uid() const noexcept
Get MOVE SOP Class UID based on current configuration.
std::shared_ptr< di::ILogger > logger_
Logger instance for service logging.
size_t bytes_retrieved() const noexcept
Get the total bytes retrieved since construction (C-GET only)
core::dicom_dataset build_study_query(std::string_view study_uid) const
Build query dataset for study retrieval.
core::dicom_dataset build_series_query(std::string_view series_uid) const
Build query dataset for series retrieval.
retrieve_scu(std::shared_ptr< di::ILogger > logger=nullptr)
Construct a Retrieve SCU with default configuration.
DIMSE command field enumeration.
Compile-time constants for commonly used DICOM tags.
@ AE
Application Entity (16 chars max)
constexpr int no_acceptable_context
constexpr int retrieve_missing_destination
constexpr int retrieve_unexpected_command
constexpr int association_not_established
@ warning
Printer has a non-critical issue.
@ c_move
Request SCP to send to third party (requires move destination)
@ completed
Procedure completed successfully.
std::function< void(const retrieve_progress &)> retrieve_progress_callback
Callback type for retrieve progress updates.
constexpr std::string_view study_root_move_sop_class_uid
Study Root Query/Retrieve Information Model - MOVE.
constexpr std::string_view study_root_get_sop_class_uid
Study Root Query/Retrieve Information Model - GET.
auto to_string(mpps_status status) -> std::string_view
Convert mpps_status to DICOM string representation.
constexpr std::string_view patient_root_move_sop_class_uid
Patient Root Query/Retrieve Information Model - MOVE.
constexpr std::string_view patient_root_get_sop_class_uid
Patient Root Query/Retrieve Information Model - GET.
@ study_root
Study Root Query/Retrieve Information Model.
@ patient_root
Patient Root Query/Retrieve Information Model.
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.
DICOM Retrieve SCU service (C-MOVE/C-GET sender)
Progress information for a retrieve operation.
uint16_t failed
Number of failed sub-operations.
uint16_t remaining
Number of remaining sub-operations.
uint16_t completed
Number of completed sub-operations.
uint16_t warning
Number of sub-operations with warnings.
std::chrono::steady_clock::time_point start_time
Result of a retrieve operation (C-MOVE or C-GET)
uint16_t warning
Number of sub-operations with warnings.
uint16_t completed
Number of successfully completed sub-operations.
uint16_t failed
Number of failed sub-operations.
uint16_t final_status
Final DIMSE status code.
std::vector< core::dicom_dataset > received_instances
Received instances (for C-GET mode only)
std::chrono::milliseconds elapsed
Retrieve execution time.
Configuration for Retrieve SCU service.
std::chrono::milliseconds timeout
Timeout for receiving responses (milliseconds)
query_model model
Query information model (Patient Root or Study Root)
retrieve_mode mode
Retrieve mode (C-MOVE or C-GET)
uint16_t priority
Priority for DIMSE operations (0=medium, 1=high, 2=low)
std::string move_destination
Move destination AE title (required for C-MOVE mode)