PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
mpps_scp.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
11
16
17namespace kcenon::pacs::services {
18
19// =============================================================================
20// Construction
21// =============================================================================
22
23mpps_scp::mpps_scp(std::shared_ptr<di::ILogger> logger)
24 : scp_service(std::move(logger)) {}
25
26// =============================================================================
27// Configuration
28// =============================================================================
29
31 create_handler_ = std::move(handler);
32}
33
35 set_handler_ = std::move(handler);
36}
37
38// =============================================================================
39// scp_service Interface Implementation
40// =============================================================================
41
42std::vector<std::string> mpps_scp::supported_sop_classes() const {
43 return {std::string(mpps_sop_class_uid)};
44}
45
48 uint8_t context_id,
49 const network::dimse::dimse_message& request) {
50
51 using namespace network::dimse;
52
53 // Route to appropriate handler based on command type
54 switch (request.command()) {
55 case command_field::n_create_rq:
56 return handle_n_create(assoc, context_id, request);
57
58 case command_field::n_set_rq:
59 return handle_n_set(assoc, context_id, request);
60
61 default:
64 "Unexpected command for MPPS SCP: " +
65 std::string(to_string(request.command())));
66 }
67}
68
69std::string_view mpps_scp::service_name() const noexcept {
70 return "MPPS SCP";
71}
72
73// =============================================================================
74// Statistics
75// =============================================================================
76
77size_t mpps_scp::creates_processed() const noexcept {
78 return creates_processed_.load();
79}
80
81size_t mpps_scp::sets_processed() const noexcept {
82 return sets_processed_.load();
83}
84
85size_t mpps_scp::mpps_completed() const noexcept {
86 return mpps_completed_.load();
87}
88
89size_t mpps_scp::mpps_discontinued() const noexcept {
90 return mpps_discontinued_.load();
91}
92
99
100// =============================================================================
101// Private Implementation - N-CREATE Handler
102// =============================================================================
103
106 uint8_t context_id,
107 const network::dimse::dimse_message& request) {
108
109 using namespace network::dimse;
110
111 // Verify we have a handler configured
112 if (!create_handler_) {
115 "No N-CREATE handler configured for MPPS SCP");
116 }
117
118 // Verify the SOP Class is MPPS
119 auto sop_class_uid = request.affected_sop_class_uid();
120 if (sop_class_uid != mpps_sop_class_uid) {
122 assoc, context_id, request.message_id(),
123 "", status_refused_sop_class_not_supported);
124 }
125
126 // Get the SOP Instance UID from the request
127 auto sop_instance_uid = request.affected_sop_instance_uid();
128 if (sop_instance_uid.empty()) {
130 assoc, context_id, request.message_id(),
131 "", status_error_missing_attribute);
132 }
133
134 // Verify we have a dataset
135 if (!request.has_dataset()) {
137 assoc, context_id, request.message_id(),
138 sop_instance_uid, status_error_cannot_understand);
139 }
140
141 const auto& dataset = request.dataset().value().get();
142
143 // Build MPPS instance from request data
144 mpps_instance instance;
145 instance.sop_instance_uid = sop_instance_uid;
147 instance.data = dataset;
148
149 // Extract Performed Station AE Title if present
150 if (dataset.contains(mpps_tags::performed_station_ae_title)) {
151 instance.station_ae = dataset.get_string(mpps_tags::performed_station_ae_title);
152 }
153
154 // Validate initial status is IN PROGRESS
155 if (dataset.contains(mpps_tags::performed_procedure_step_status)) {
156 auto status_str = dataset.get_string(mpps_tags::performed_procedure_step_status);
157 auto parsed_status = parse_mpps_status(status_str);
158
159 if (!parsed_status.has_value() ||
160 parsed_status.value() != mpps_status::in_progress) {
161 // N-CREATE must have IN PROGRESS status
163 assoc, context_id, request.message_id(),
164 sop_instance_uid, status_error_cannot_understand);
165 }
166 }
167
168 // Call the handler to create the MPPS instance
169 auto result = create_handler_(instance);
170 if (result.is_err()) {
172 assoc, context_id, request.message_id(),
173 sop_instance_uid, status_error_unable_to_process);
174 }
175
176 // Update statistics
178
179 // Send success response
181 assoc, context_id, request.message_id(),
182 sop_instance_uid, status_success);
183}
184
185// =============================================================================
186// Private Implementation - N-SET Handler
187// =============================================================================
188
191 uint8_t context_id,
192 const network::dimse::dimse_message& request) {
193
194 using namespace network::dimse;
195
196 // Verify we have a handler configured
197 if (!set_handler_) {
200 "No N-SET handler configured for MPPS SCP");
201 }
202
203 // Verify the SOP Class is MPPS
204 auto sop_class_uid = request.affected_sop_class_uid();
205 if (sop_class_uid.empty()) {
206 // For N-SET, use requested SOP class if affected is empty
207 sop_class_uid = request.command_set().get_string(
208 tag_requested_sop_class_uid);
209 }
210
211 if (sop_class_uid != mpps_sop_class_uid) {
212 return send_n_set_response(
213 assoc, context_id, request.message_id(),
214 "", status_refused_sop_class_not_supported);
215 }
216
217 // Get the SOP Instance UID (from Requested SOP Instance UID for N-SET)
218 auto sop_instance_uid = request.command_set().get_string(
219 tag_requested_sop_instance_uid);
220
221 if (sop_instance_uid.empty()) {
222 // Try affected SOP instance UID as fallback
223 sop_instance_uid = request.affected_sop_instance_uid();
224 }
225
226 if (sop_instance_uid.empty()) {
227 return send_n_set_response(
228 assoc, context_id, request.message_id(),
229 "", status_error_missing_attribute);
230 }
231
232 // Verify we have a dataset with modifications
233 if (!request.has_dataset()) {
234 return send_n_set_response(
235 assoc, context_id, request.message_id(),
236 sop_instance_uid, status_error_cannot_understand);
237 }
238
239 const auto& dataset = request.dataset().value().get();
240
241 // Extract and validate the new status
242 if (!dataset.contains(mpps_tags::performed_procedure_step_status)) {
243 return send_n_set_response(
244 assoc, context_id, request.message_id(),
245 sop_instance_uid, status_error_missing_attribute);
246 }
247
248 auto status_str = dataset.get_string(mpps_tags::performed_procedure_step_status);
249 auto new_status = parse_mpps_status(status_str);
250
251 if (!new_status.has_value()) {
252 return send_n_set_response(
253 assoc, context_id, request.message_id(),
254 sop_instance_uid, status_error_cannot_understand);
255 }
256
257 // Validate status transition (must be to COMPLETED or DISCONTINUED)
258 if (new_status.value() == mpps_status::in_progress) {
259 // Cannot set back to IN PROGRESS
260 return send_n_set_response(
261 assoc, context_id, request.message_id(),
262 sop_instance_uid, status_error_cannot_understand);
263 }
264
265 // Call the handler to update the MPPS instance
266 auto result = set_handler_(sop_instance_uid, dataset, new_status.value());
267 if (result.is_err()) {
268 return send_n_set_response(
269 assoc, context_id, request.message_id(),
270 sop_instance_uid, status_error_unable_to_process);
271 }
272
273 // Update statistics
275 if (new_status.value() == mpps_status::completed) {
277 } else if (new_status.value() == mpps_status::discontinued) {
279 }
280
281 // Send success response
282 return send_n_set_response(
283 assoc, context_id, request.message_id(),
284 sop_instance_uid, status_success);
285}
286
287// =============================================================================
288// Private Implementation - Response Helpers
289// =============================================================================
290
293 uint8_t context_id,
294 uint16_t message_id,
295 const std::string& sop_instance_uid,
297
298 using namespace network::dimse;
299
300 // Create N-CREATE response message
301 dimse_message response{command_field::n_create_rsp, 0};
302 response.set_message_id_responded_to(message_id);
303 response.set_affected_sop_class_uid(mpps_sop_class_uid);
304 response.set_status(status);
305
306 if (!sop_instance_uid.empty()) {
307 response.set_affected_sop_instance_uid(sop_instance_uid);
308 }
309
310 // Send the response
311 return assoc.send_dimse(context_id, response);
312}
313
316 uint8_t context_id,
317 uint16_t message_id,
318 const std::string& sop_instance_uid,
320
321 using namespace network::dimse;
322
323 // Create N-SET response message
324 dimse_message response{command_field::n_set_rsp, 0};
325 response.set_message_id_responded_to(message_id);
326 response.set_affected_sop_class_uid(mpps_sop_class_uid);
327 response.set_status(status);
328
329 if (!sop_instance_uid.empty()) {
330 response.set_affected_sop_instance_uid(sop_instance_uid);
331 }
332
333 // Send the response
334 return assoc.send_dimse(context_id, response);
335}
336
337} // namespace kcenon::pacs::services
Result< std::monostate > send_dimse(uint8_t context_id, const dimse::dimse_message &msg)
Send a DIMSE message.
auto message_id() const noexcept -> uint16_t
Get the message ID.
auto affected_sop_class_uid() const -> std::string
Get the Affected SOP Class UID.
auto command_set() noexcept -> core::dicom_dataset &
Get mutable reference to the command set.
auto has_dataset() const noexcept -> bool
Check if the message has an associated data set.
auto dataset() -> kcenon::pacs::Result< std::reference_wrapper< core::dicom_dataset > >
Get mutable reference to the data set.
auto affected_sop_instance_uid() const -> std::string
Get the Affected SOP Instance UID.
auto command() const noexcept -> command_field
Get the command field.
void set_set_handler(mpps_set_handler handler)
Set the N-SET handler function.
Definition mpps_scp.cpp:34
void reset_statistics() noexcept
Reset statistics counters.
Definition mpps_scp.cpp:93
void set_create_handler(mpps_create_handler handler)
Set the N-CREATE handler function.
Definition mpps_scp.cpp:30
mpps_set_handler set_handler_
Definition mpps_scp.h:414
network::Result< std::monostate > handle_n_create(network::association &assoc, uint8_t context_id, const network::dimse::dimse_message &request)
Handle N-CREATE request.
Definition mpps_scp.cpp:104
size_t creates_processed() const noexcept
Get total number of N-CREATE requests processed.
Definition mpps_scp.cpp:77
network::Result< std::monostate > send_n_set_response(network::association &assoc, uint8_t context_id, uint16_t message_id, const std::string &sop_instance_uid, network::dimse::status_code status)
Send N-SET response.
Definition mpps_scp.cpp:314
std::atomic< size_t > creates_processed_
Definition mpps_scp.h:416
std::atomic< size_t > sets_processed_
Definition mpps_scp.h:417
network::Result< std::monostate > send_n_create_response(network::association &assoc, uint8_t context_id, uint16_t message_id, const std::string &sop_instance_uid, network::dimse::status_code status)
Send N-CREATE response.
Definition mpps_scp.cpp:291
network::Result< std::monostate > handle_message(network::association &assoc, uint8_t context_id, const network::dimse::dimse_message &request) override
Handle an incoming DIMSE message (N-CREATE-RQ or N-SET-RQ)
Definition mpps_scp.cpp:46
size_t mpps_completed() const noexcept
Get number of MPPS completed successfully.
Definition mpps_scp.cpp:85
size_t mpps_discontinued() const noexcept
Get number of MPPS discontinued.
Definition mpps_scp.cpp:89
std::vector< std::string > supported_sop_classes() const override
Get supported SOP Class UIDs.
Definition mpps_scp.cpp:42
network::Result< std::monostate > handle_n_set(network::association &assoc, uint8_t context_id, const network::dimse::dimse_message &request)
Handle N-SET request.
Definition mpps_scp.cpp:189
mpps_create_handler create_handler_
Definition mpps_scp.h:413
mpps_scp(std::shared_ptr< di::ILogger > logger=nullptr)
Construct MPPS SCP with optional logger.
Definition mpps_scp.cpp:23
size_t sets_processed() const noexcept
Get total number of N-SET requests processed.
Definition mpps_scp.cpp:81
std::atomic< size_t > mpps_completed_
Definition mpps_scp.h:418
std::atomic< size_t > mpps_discontinued_
Definition mpps_scp.h:419
std::string_view service_name() const noexcept override
Get the service name.
Definition mpps_scp.cpp:69
DIMSE command field enumeration.
Compile-time constants for commonly used DICOM tags.
DICOM MPPS (Modality Performed Procedure Step) SCP service.
constexpr int mpps_handler_not_set
Definition result.h:172
constexpr int mpps_unexpected_command
Definition result.h:174
uint16_t status_code
DIMSE status code type alias.
constexpr core::dicom_tag performed_station_ae_title
Performed Station AE Title (0040,0241)
Definition mpps_scp.h:429
constexpr core::dicom_tag performed_procedure_step_status
Performed Procedure Step Status (0040,0252)
Definition mpps_scp.h:444
std::function< network::Result< std::monostate >( const std::string &sop_instance_uid, const core::dicom_dataset &modifications, mpps_status new_status)> mpps_set_handler
N-SET handler function type.
Definition mpps_scp.h:137
@ completed
Procedure completed successfully.
@ discontinued
Procedure was stopped/cancelled.
@ in_progress
Procedure is currently being performed.
std::function< network::Result< std::monostate >( const mpps_instance &instance)> mpps_create_handler
N-CREATE handler function type.
Definition mpps_scp.h:124
auto to_string(mpps_status status) -> std::string_view
Convert mpps_status to DICOM string representation.
Definition mpps_scp.h:60
constexpr std::string_view mpps_sop_class_uid
MPPS (Modality Performed Procedure Step) SOP Class UID.
Definition mpps_scp.h:36
auto parse_mpps_status(std::string_view str) -> std::optional< mpps_status >
Parse DICOM string to mpps_status enum.
Definition mpps_scp.h:79
VoidResult pacs_void_error(int code, const std::string &message, const std::string &details="")
Create a PACS void error result.
Definition result.h:249
Result<T> type aliases and helpers for PACS system.
DIMSE status codes.
MPPS instance data structure.
Definition mpps_scp.h:98
mpps_status status
Current status (always IN PROGRESS for N-CREATE)
Definition mpps_scp.h:103
std::string sop_instance_uid
SOP Instance UID - unique identifier for this MPPS.
Definition mpps_scp.h:100
std::string station_ae
Performing station AE Title.
Definition mpps_scp.h:106
core::dicom_dataset data
Complete MPPS dataset from the request.
Definition mpps_scp.h:109