PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
ups_push_scp.cpp
Go to the documentation of this file.
1
7
14
15namespace kcenon::pacs::services {
16
17// =============================================================================
18// Construction
19// =============================================================================
20
21ups_push_scp::ups_push_scp(std::shared_ptr<di::ILogger> logger)
22 : scp_service(std::move(logger)) {}
23
24// =============================================================================
25// Configuration
26// =============================================================================
27
29 create_handler_ = std::move(handler);
30}
31
33 set_handler_ = std::move(handler);
34}
35
37 get_handler_ = std::move(handler);
38}
39
43
47
48// =============================================================================
49// scp_service Interface Implementation
50// =============================================================================
51
52std::vector<std::string> ups_push_scp::supported_sop_classes() const {
53 return {std::string(ups_push_sop_class_uid)};
54}
55
58 uint8_t context_id,
59 const network::dimse::dimse_message& request) {
60
61 using namespace network::dimse;
62
63 switch (request.command()) {
64 case command_field::n_create_rq:
65 return handle_n_create(assoc, context_id, request);
66
67 case command_field::n_set_rq:
68 return handle_n_set(assoc, context_id, request);
69
70 case command_field::n_get_rq:
71 return handle_n_get(assoc, context_id, request);
72
73 case command_field::n_action_rq:
74 return handle_n_action(assoc, context_id, request);
75
76 default:
79 "Unexpected command for UPS Push SCP: " +
80 std::string(to_string(request.command())));
81 }
82}
83
84std::string_view ups_push_scp::service_name() const noexcept {
85 return "UPS Push SCP";
86}
87
88// =============================================================================
89// Statistics
90// =============================================================================
91
92size_t ups_push_scp::creates_processed() const noexcept {
93 return creates_processed_.load();
94}
95
96size_t ups_push_scp::sets_processed() const noexcept {
97 return sets_processed_.load();
98}
99
100size_t ups_push_scp::gets_processed() const noexcept {
101 return gets_processed_.load();
102}
103
104size_t ups_push_scp::actions_processed() const noexcept {
105 return actions_processed_.load();
106}
107
108size_t ups_push_scp::state_changes() const noexcept {
109 return state_changes_.load();
110}
111
112size_t ups_push_scp::cancel_requests() const noexcept {
113 return cancel_requests_.load();
114}
115
124
125// =============================================================================
126// Private Implementation - N-CREATE Handler
127// =============================================================================
128
131 uint8_t context_id,
132 const network::dimse::dimse_message& request) {
133
134 using namespace network::dimse;
135
136 // Verify we have a handler configured
137 if (!create_handler_) {
140 "No N-CREATE handler configured for UPS Push SCP");
141 }
142
143 // Verify the SOP Class is UPS Push
144 auto sop_class_uid = request.affected_sop_class_uid();
145 if (sop_class_uid != ups_push_sop_class_uid) {
147 assoc, context_id, request.message_id(),
148 "", status_refused_sop_class_not_supported);
149 }
150
151 // Get the SOP Instance UID (workitem UID)
152 auto sop_instance_uid = request.affected_sop_instance_uid();
153 if (sop_instance_uid.empty()) {
155 assoc, context_id, request.message_id(),
156 "", status_error_missing_attribute);
157 }
158
159 // Verify we have a dataset
160 if (!request.has_dataset()) {
162 assoc, context_id, request.message_id(),
163 sop_instance_uid, status_error_cannot_understand);
164 }
165
166 const auto& dataset = request.dataset().value().get();
167
168 // Validate initial state is SCHEDULED (if present in dataset)
169 if (dataset.contains(ups_tags::procedure_step_state)) {
170 auto state_str = dataset.get_string(ups_tags::procedure_step_state);
171 auto parsed_state = storage::parse_ups_state(state_str);
172
173 if (!parsed_state.has_value() ||
174 parsed_state.value() != storage::ups_state::scheduled) {
176 assoc, context_id, request.message_id(),
177 sop_instance_uid, status_error_cannot_understand);
178 }
179 }
180
181 // Build UPS workitem from request data
182 storage::ups_workitem workitem;
183 workitem.workitem_uid = sop_instance_uid;
184 workitem.state = "SCHEDULED";
185
186 // Extract optional fields from dataset
187 if (dataset.contains(ups_tags::procedure_step_label)) {
188 workitem.procedure_step_label =
189 dataset.get_string(ups_tags::procedure_step_label);
190 }
191 if (dataset.contains(ups_tags::worklist_label)) {
192 workitem.worklist_label =
193 dataset.get_string(ups_tags::worklist_label);
194 }
195 if (dataset.contains(ups_tags::scheduled_procedure_step_priority)) {
196 workitem.priority =
198 }
199
200 // Call the handler to create the workitem
201 auto result = create_handler_(workitem);
202 if (result.is_err()) {
204 assoc, context_id, request.message_id(),
205 sop_instance_uid, status_error_unable_to_process);
206 }
207
209
211 assoc, context_id, request.message_id(),
212 sop_instance_uid, status_success);
213}
214
215// =============================================================================
216// Private Implementation - N-SET Handler
217// =============================================================================
218
221 uint8_t context_id,
222 const network::dimse::dimse_message& request) {
223
224 using namespace network::dimse;
225
226 // Verify we have a handler configured
227 if (!set_handler_) {
230 "No N-SET handler configured for UPS Push SCP");
231 }
232
233 // Get SOP Instance UID (from Requested SOP Instance UID for N-SET)
234 auto sop_instance_uid = request.command_set().get_string(
235 tag_requested_sop_instance_uid);
236
237 if (sop_instance_uid.empty()) {
238 sop_instance_uid = request.affected_sop_instance_uid();
239 }
240
241 if (sop_instance_uid.empty()) {
242 return send_n_set_response(
243 assoc, context_id, request.message_id(),
244 "", status_error_missing_attribute);
245 }
246
247 // Verify we have a dataset with modifications
248 if (!request.has_dataset()) {
249 return send_n_set_response(
250 assoc, context_id, request.message_id(),
251 sop_instance_uid, status_error_cannot_understand);
252 }
253
254 const auto& dataset = request.dataset().value().get();
255
256 // Reject if dataset tries to set a final state directly via N-SET
257 // (state changes must go through N-ACTION)
258 if (dataset.contains(ups_tags::procedure_step_state)) {
259 auto state_str = dataset.get_string(ups_tags::procedure_step_state);
260 auto parsed_state = storage::parse_ups_state(state_str);
261 if (parsed_state.has_value() &&
262 (parsed_state.value() == storage::ups_state::completed ||
263 parsed_state.value() == storage::ups_state::canceled)) {
264 return send_n_set_response(
265 assoc, context_id, request.message_id(),
266 sop_instance_uid, status_error_cannot_understand);
267 }
268 }
269
270 // Call the handler to update the workitem
271 auto result = set_handler_(sop_instance_uid, dataset);
272 if (result.is_err()) {
273 return send_n_set_response(
274 assoc, context_id, request.message_id(),
275 sop_instance_uid, status_error_unable_to_process);
276 }
277
279
280 return send_n_set_response(
281 assoc, context_id, request.message_id(),
282 sop_instance_uid, status_success);
283}
284
285// =============================================================================
286// Private Implementation - N-GET Handler
287// =============================================================================
288
291 uint8_t context_id,
292 const network::dimse::dimse_message& request) {
293
294 using namespace network::dimse;
295
296 // Verify we have a handler configured
297 if (!get_handler_) {
300 "No N-GET handler configured for UPS Push SCP");
301 }
302
303 // Extract Requested SOP Instance UID
304 auto sop_instance_uid = request.requested_sop_instance_uid();
305 if (sop_instance_uid.empty()) {
306 return send_n_get_response(
307 assoc, context_id, request.message_id(),
308 "", status_error_missing_attribute);
309 }
310
311 // Call the handler to retrieve the workitem
312 auto result = get_handler_(sop_instance_uid);
313 if (result.is_err()) {
314 return send_n_get_response(
315 assoc, context_id, request.message_id(),
316 sop_instance_uid, status_error_invalid_object_instance);
317 }
318
320
321 // Build response dataset from workitem
322 const auto& workitem = result.value();
323 core::dicom_dataset response_dataset;
324
325 response_dataset.set_string(
327 encoding::vr_type::CS, workitem.state);
328
329 if (!workitem.procedure_step_label.empty()) {
330 response_dataset.set_string(
332 encoding::vr_type::LO, workitem.procedure_step_label);
333 }
334 if (!workitem.worklist_label.empty()) {
335 response_dataset.set_string(
337 encoding::vr_type::LO, workitem.worklist_label);
338 }
339 if (!workitem.priority.empty()) {
340 response_dataset.set_string(
342 encoding::vr_type::CS, workitem.priority);
343 }
344 if (!workitem.progress_description.empty()) {
345 response_dataset.set_string(
348 std::to_string(workitem.progress_percent));
349 }
350 if (!workitem.transaction_uid.empty()) {
351 response_dataset.set_string(
353 encoding::vr_type::UI, workitem.transaction_uid);
354 }
355
356 return send_n_get_response(
357 assoc, context_id, request.message_id(),
358 sop_instance_uid, status_success, &response_dataset);
359}
360
361// =============================================================================
362// Private Implementation - N-ACTION Handler
363// =============================================================================
364
367 uint8_t context_id,
368 const network::dimse::dimse_message& request) {
369
370 using namespace network::dimse;
371
372 // Get Action Type ID
373 auto action_type = request.action_type_id();
374 if (!action_type.has_value()) {
377 "Missing Action Type ID in N-ACTION request");
378 }
379
380 // Get SOP Instance UID
381 auto sop_instance_uid = request.requested_sop_instance_uid();
382 if (sop_instance_uid.empty()) {
383 sop_instance_uid = request.affected_sop_instance_uid();
384 }
385
386 if (sop_instance_uid.empty()) {
388 assoc, context_id, request.message_id(),
389 "", action_type.value(), status_error_missing_attribute);
390 }
391
392 uint16_t action_id = action_type.value();
393
394 if (action_id == ups_action_change_state) {
395 // N-ACTION Type 1: Change UPS State
399 "No Change State handler configured for UPS Push SCP");
400 }
401
402 if (!request.has_dataset()) {
404 assoc, context_id, request.message_id(),
405 sop_instance_uid, action_id, status_error_cannot_understand);
406 }
407
408 const auto& dataset = request.dataset().value().get();
409
410 // Extract new state
411 if (!dataset.contains(ups_tags::procedure_step_state)) {
413 assoc, context_id, request.message_id(),
414 sop_instance_uid, action_id, status_error_missing_attribute);
415 }
416
417 auto new_state = dataset.get_string(ups_tags::procedure_step_state);
418
419 // Validate the state value
420 auto parsed_state = storage::parse_ups_state(new_state);
421 if (!parsed_state.has_value()) {
423 assoc, context_id, request.message_id(),
424 sop_instance_uid, action_id, status_error_cannot_understand);
425 }
426
427 // Extract Transaction UID (required when transitioning to IN PROGRESS)
428 std::string txn_uid;
429 if (dataset.contains(ups_tags::transaction_uid)) {
430 txn_uid = dataset.get_string(ups_tags::transaction_uid);
431 }
432
433 if (parsed_state.value() == storage::ups_state::in_progress &&
434 txn_uid.empty()) {
436 assoc, context_id, request.message_id(),
437 sop_instance_uid, action_id, status_error_missing_attribute);
438 }
439
440 auto result = change_state_handler_(sop_instance_uid, new_state, txn_uid);
441 if (result.is_err()) {
443 assoc, context_id, request.message_id(),
444 sop_instance_uid, action_id, status_error_unable_to_process);
445 }
446
449
451 assoc, context_id, request.message_id(),
452 sop_instance_uid, action_id, status_success);
453
454 } else if (action_id == ups_action_request_cancel) {
455 // N-ACTION Type 3: Request Cancellation
459 "No Request Cancel handler configured for UPS Push SCP");
460 }
461
462 // Extract cancellation reason (optional)
463 std::string reason;
464 if (request.has_dataset()) {
465 const auto& dataset = request.dataset().value().get();
466 if (dataset.contains(ups_tags::reason_for_cancellation)) {
467 reason = dataset.get_string(ups_tags::reason_for_cancellation);
468 }
469 }
470
471 auto result = request_cancel_handler_(sop_instance_uid, reason);
472 if (result.is_err()) {
474 assoc, context_id, request.message_id(),
475 sop_instance_uid, action_id, status_error_unable_to_process);
476 }
477
480
482 assoc, context_id, request.message_id(),
483 sop_instance_uid, action_id, status_success);
484
485 } else {
486 // Unknown action type
488 assoc, context_id, request.message_id(),
489 sop_instance_uid, action_id, status_error_no_such_action_type);
490 }
491}
492
493// =============================================================================
494// Private Implementation - Response Helpers
495// =============================================================================
496
499 uint8_t context_id,
500 uint16_t message_id,
501 const std::string& sop_instance_uid,
503
504 using namespace network::dimse;
505
506 dimse_message response{command_field::n_create_rsp, 0};
507 response.set_message_id_responded_to(message_id);
508 response.set_affected_sop_class_uid(ups_push_sop_class_uid);
509 response.set_status(status);
510
511 if (!sop_instance_uid.empty()) {
512 response.set_affected_sop_instance_uid(sop_instance_uid);
513 }
514
515 return assoc.send_dimse(context_id, response);
516}
517
520 uint8_t context_id,
521 uint16_t message_id,
522 const std::string& sop_instance_uid,
524
525 using namespace network::dimse;
526
527 dimse_message response{command_field::n_set_rsp, 0};
528 response.set_message_id_responded_to(message_id);
529 response.set_affected_sop_class_uid(ups_push_sop_class_uid);
530 response.set_status(status);
531
532 if (!sop_instance_uid.empty()) {
533 response.set_affected_sop_instance_uid(sop_instance_uid);
534 }
535
536 return assoc.send_dimse(context_id, response);
537}
538
541 uint8_t context_id,
542 uint16_t message_id,
543 const std::string& sop_instance_uid,
545 core::dicom_dataset* dataset) {
546
547 using namespace network::dimse;
548
549 auto response = make_n_get_rsp(
550 message_id, ups_push_sop_class_uid, sop_instance_uid, status);
551
552 if (dataset != nullptr) {
553 response.set_dataset(std::move(*dataset));
554 }
555
556 return assoc.send_dimse(context_id, response);
557}
558
561 uint8_t context_id,
562 uint16_t message_id,
563 const std::string& sop_instance_uid,
564 uint16_t action_type_id,
566
567 using namespace network::dimse;
568
569 auto response = make_n_action_rsp(
570 message_id, ups_push_sop_class_uid, sop_instance_uid,
571 action_type_id, status);
572
573 return assoc.send_dimse(context_id, response);
574}
575
576} // namespace kcenon::pacs::services
void set_string(dicom_tag tag, encoding::vr_type vr, std::string_view value)
Set a string value for the given tag.
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 requested_sop_instance_uid() const -> std::string
Get the Requested SOP Instance UID (for N-SET, N-GET, N-ACTION, N-DELETE)
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 action_type_id() const -> std::optional< uint16_t >
Get the Action Type ID (for N-ACTION)
auto affected_sop_instance_uid() const -> std::string
Get the Affected SOP Instance UID.
auto command() const noexcept -> command_field
Get the command field.
network::Result< std::monostate > send_n_get_response(network::association &assoc, uint8_t context_id, uint16_t message_id, const std::string &sop_instance_uid, network::dimse::status_code status, core::dicom_dataset *dataset=nullptr)
network::Result< std::monostate > handle_n_set(network::association &assoc, uint8_t context_id, const network::dimse::dimse_message &request)
size_t creates_processed() const noexcept
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)
network::Result< std::monostate > handle_n_create(network::association &assoc, uint8_t context_id, const network::dimse::dimse_message &request)
std::atomic< size_t > state_changes_
std::atomic< size_t > cancel_requests_
void set_create_handler(ups_create_handler handler)
ups_request_cancel_handler request_cancel_handler_
size_t state_changes() const noexcept
std::vector< std::string > supported_sop_classes() const override
Get the list of SOP Class UIDs supported by this service.
std::string_view service_name() const noexcept override
Get the service name for logging/debugging.
void set_request_cancel_handler(ups_request_cancel_handler handler)
size_t gets_processed() const noexcept
size_t cancel_requests() const noexcept
size_t actions_processed() const noexcept
std::atomic< size_t > actions_processed_
std::atomic< size_t > creates_processed_
void set_get_handler(ups_get_handler handler)
ups_push_scp(std::shared_ptr< di::ILogger > logger=nullptr)
Construct UPS Push SCP with optional logger.
std::atomic< size_t > gets_processed_
void set_set_handler(ups_set_handler handler)
network::Result< std::monostate > send_n_action_response(network::association &assoc, uint8_t context_id, uint16_t message_id, const std::string &sop_instance_uid, uint16_t action_type_id, network::dimse::status_code status)
size_t sets_processed() const noexcept
std::atomic< size_t > sets_processed_
ups_change_state_handler change_state_handler_
network::Result< std::monostate > handle_n_action(network::association &assoc, uint8_t context_id, const network::dimse::dimse_message &request)
network::Result< std::monostate > handle_n_get(network::association &assoc, uint8_t context_id, const network::dimse::dimse_message &request)
void set_change_state_handler(ups_change_state_handler handler)
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)
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.
DIMSE command field enumeration.
Compile-time constants for commonly used DICOM tags.
DIMSE message encoding and decoding.
@ LO
Long String (64 chars max)
@ DS
Decimal String (16 chars max)
@ UI
Unique Identifier (64 chars max)
@ CS
Code String (16 chars max, uppercase + digits + space + underscore)
constexpr int ups_unexpected_command
Definition result.h:210
constexpr int ups_invalid_action_type
Definition result.h:214
constexpr int ups_handler_not_set
Definition result.h:209
uint16_t status_code
DIMSE status code type alias.
constexpr core::dicom_tag procedure_step_label
Procedure Step Label (0074,1204)
constexpr core::dicom_tag procedure_step_progress
Procedure Step Progress (0074,1004)
constexpr core::dicom_tag transaction_uid
Transaction UID (0008,1195)
constexpr core::dicom_tag procedure_step_state
Procedure Step State (0074,1000)
constexpr core::dicom_tag worklist_label
Worklist Label (0074,1202)
constexpr core::dicom_tag reason_for_cancellation
Reason for Cancellation (0074,1238)
constexpr core::dicom_tag scheduled_procedure_step_priority
Scheduled Procedure Step Priority (0074,1200)
std::function< network::Result< storage::ups_workitem >( const std::string &workitem_uid)> ups_get_handler
N-GET handler function type.
std::function< network::Result< std::monostate >( const std::string &workitem_uid, const core::dicom_dataset &modifications)> ups_set_handler
N-SET handler function type.
std::function< network::Result< std::monostate >( const storage::ups_workitem &workitem)> ups_create_handler
N-CREATE handler function type.
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 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)
std::function< network::Result< std::monostate >( const std::string &workitem_uid, const std::string &reason)> ups_request_cancel_handler
N-ACTION Type 3 handler: Request Cancellation.
std::function< network::Result< std::monostate >( const std::string &workitem_uid, const std::string &new_state, const std::string &transaction_uid)> ups_change_state_handler
N-ACTION Type 1 handler: Change UPS State.
@ scheduled
Workitem is scheduled (initial state)
@ completed
Workitem completed successfully (final)
@ canceled
Workitem was canceled (final)
@ in_progress
Workitem is being performed.
auto parse_ups_state(std::string_view str) -> std::optional< ups_state >
Parse string to ups_state enum.
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.
UPS workitem record from the database.
std::string workitem_uid
UPS SOP Instance UID - unique identifier for this workitem.
std::string state
Current state of the workitem.
std::string priority
Priority (LOW, MEDIUM, HIGH)
std::string worklist_label
Worklist Label (for grouping workitems)
std::string procedure_step_label
Procedure Step Label (human-readable description)
DICOM UPS (Unified Procedure Step) Push SCP service.