PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
mpps_scu.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
17
18#include <chrono>
19#include <ctime>
20#include <iomanip>
21#include <random>
22#include <sstream>
23
24namespace kcenon::pacs::services {
25
26// =============================================================================
27// Local Helper Functions
28// =============================================================================
29
30namespace {
31
33constexpr const char* uid_root = "1.2.826.0.1.3680043.2.1545";
34
36constexpr core::dicom_tag tag_requested_sop_instance_uid{0x0000, 0x1001};
37
41network::dimse::dimse_message make_n_create_rq(
42 uint16_t message_id,
43 std::string_view sop_instance_uid,
44 core::dicom_dataset dataset) {
45
46 using namespace network::dimse;
47
48 dimse_message msg{command_field::n_create_rq, message_id};
49 msg.set_affected_sop_class_uid(mpps_sop_class_uid);
50 msg.set_affected_sop_instance_uid(sop_instance_uid);
51 msg.set_dataset(std::move(dataset));
52
53 return msg;
54}
55
59network::dimse::dimse_message make_n_set_rq(
60 uint16_t message_id,
61 std::string_view sop_instance_uid,
62 core::dicom_dataset modifications) {
63
64 using namespace network::dimse;
65 using namespace encoding;
66
67 dimse_message msg{command_field::n_set_rq, message_id};
68 msg.set_affected_sop_class_uid(mpps_sop_class_uid);
69
70 // For N-SET, use Requested SOP Instance UID in command set
71 msg.command_set().set_string(
72 tag_requested_sop_instance_uid,
73 vr_type::UI,
74 std::string(sop_instance_uid));
75 msg.set_dataset(std::move(modifications));
76
77 return msg;
78}
79
80} // namespace
81
82// =============================================================================
83// Construction
84// =============================================================================
85
86mpps_scu::mpps_scu(std::shared_ptr<di::ILogger> logger)
87 : logger_(logger ? std::move(logger) : di::null_logger()) {}
88
90 std::shared_ptr<di::ILogger> logger)
91 : logger_(logger ? std::move(logger) : di::null_logger()),
92 config_(config) {}
93
94// =============================================================================
95// N-CREATE Operation
96// =============================================================================
97
100 const mpps_create_data& data) {
101
102 using namespace network::dimse;
103
104 auto start_time = std::chrono::steady_clock::now();
105
106 // Verify association is established
107 if (!assoc.is_established()) {
110 "Association not established");
111 }
112
113 // Get accepted presentation context for MPPS
114 auto context_id = assoc.accepted_context_id(mpps_sop_class_uid);
115 if (!context_id) {
118 "No accepted presentation context for MPPS SOP Class");
119 }
120
121 // Generate or use provided MPPS UID
122 std::string mpps_uid = data.mpps_sop_instance_uid;
123 if (mpps_uid.empty() && config_.auto_generate_uid) {
124 mpps_uid = generate_mpps_uid();
125 }
126
127 if (mpps_uid.empty()) {
130 "MPPS SOP Instance UID is required");
131 }
132
133 // Build the MPPS creation dataset
134 auto dataset = build_create_dataset(data);
135
136 // Create the N-CREATE request
137 auto request = make_n_create_rq(next_message_id(), mpps_uid, std::move(dataset));
138
139 logger_->debug("Sending N-CREATE request for MPPS: " + mpps_uid);
140
141 // Send the request
142 auto send_result = assoc.send_dimse(*context_id, request);
143 if (send_result.is_err()) {
144 logger_->error("Failed to send N-CREATE: " + send_result.error().message);
145 return send_result.error();
146 }
147
148 // Receive the response
149 auto recv_result = assoc.receive_dimse(config_.timeout);
150 if (recv_result.is_err()) {
151 logger_->error("Failed to receive N-CREATE response: " + recv_result.error().message);
152 return recv_result.error();
153 }
154
155 const auto& [recv_context_id, response] = recv_result.value();
156
157 // Verify it's an N-CREATE response
158 if (response.command() != command_field::n_create_rsp) {
161 "Expected N-CREATE-RSP but received " +
162 std::string(to_string(response.command())));
163 }
164
165 auto end_time = std::chrono::steady_clock::now();
166 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
167 end_time - start_time);
168
169 // Build result
170 mpps_result result;
171 result.mpps_sop_instance_uid = mpps_uid;
172 result.status = static_cast<uint16_t>(response.status());
173 result.elapsed = elapsed;
174
175 // Extract error comment if present
176 if (response.command_set().contains(tag_error_comment)) {
177 result.error_comment = response.command_set().get_string(tag_error_comment);
178 }
179
180 // Update statistics
181 creates_performed_.fetch_add(1, std::memory_order_relaxed);
182
183 if (result.is_success()) {
184 logger_->info("N-CREATE successful for MPPS: " + mpps_uid);
185 } else {
186 logger_->warn("N-CREATE returned status 0x" +
187 std::to_string(result.status) + " for MPPS: " + mpps_uid);
188 }
189
190 return result;
191}
192
193// =============================================================================
194// N-SET Operations
195// =============================================================================
196
199 const mpps_set_data& data) {
200
201 using namespace network::dimse;
202
203 auto start_time = std::chrono::steady_clock::now();
204
205 // Verify association is established
206 if (!assoc.is_established()) {
209 "Association not established");
210 }
211
212 // Validate MPPS UID is provided
213 if (data.mpps_sop_instance_uid.empty()) {
216 "MPPS SOP Instance UID is required for N-SET");
217 }
218
219 // Validate status is not IN PROGRESS (can only set to COMPLETED or DISCONTINUED)
220 if (data.status == mpps_status::in_progress) {
223 "Cannot set MPPS status back to IN PROGRESS");
224 }
225
226 // Get accepted presentation context for MPPS
227 auto context_id = assoc.accepted_context_id(mpps_sop_class_uid);
228 if (!context_id) {
231 "No accepted presentation context for MPPS SOP Class");
232 }
233
234 // Build the modification dataset
235 auto dataset = build_set_dataset(data);
236
237 // Create the N-SET request
238 auto request = make_n_set_rq(
241 std::move(dataset));
242
243 logger_->debug("Sending N-SET request for MPPS: " + data.mpps_sop_instance_uid);
244
245 // Send the request
246 auto send_result = assoc.send_dimse(*context_id, request);
247 if (send_result.is_err()) {
248 logger_->error("Failed to send N-SET: " + send_result.error().message);
249 return send_result.error();
250 }
251
252 // Receive the response
253 auto recv_result = assoc.receive_dimse(config_.timeout);
254 if (recv_result.is_err()) {
255 logger_->error("Failed to receive N-SET response: " + recv_result.error().message);
256 return recv_result.error();
257 }
258
259 const auto& [recv_context_id, response] = recv_result.value();
260
261 // Verify it's an N-SET response
262 if (response.command() != command_field::n_set_rsp) {
265 "Expected N-SET-RSP but received " +
266 std::string(to_string(response.command())));
267 }
268
269 auto end_time = std::chrono::steady_clock::now();
270 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
271 end_time - start_time);
272
273 // Build result
274 mpps_result result;
276 result.status = static_cast<uint16_t>(response.status());
277 result.elapsed = elapsed;
278
279 // Extract error comment if present
280 if (response.command_set().contains(tag_error_comment)) {
281 result.error_comment = response.command_set().get_string(tag_error_comment);
282 }
283
284 // Update statistics
285 sets_performed_.fetch_add(1, std::memory_order_relaxed);
286
287 if (result.is_success()) {
288 logger_->info("N-SET successful for MPPS: " + data.mpps_sop_instance_uid +
289 " (status: " + std::string(to_string(data.status)) + ")");
290 } else {
291 logger_->warn("N-SET returned status 0x" +
292 std::to_string(result.status) + " for MPPS: " +
294 }
295
296 return result;
297}
298
301 std::string_view mpps_uid,
302 const std::vector<performed_series_info>& performed_series) {
303
304 mpps_set_data data;
305 data.mpps_sop_instance_uid = std::string(mpps_uid);
309 data.performed_series = performed_series;
310
311 return set(assoc, data);
312}
313
316 std::string_view mpps_uid,
317 std::string_view reason) {
318
319 mpps_set_data data;
320 data.mpps_sop_instance_uid = std::string(mpps_uid);
324 data.discontinuation_reason = std::string(reason);
325
326 return set(assoc, data);
327}
328
329// =============================================================================
330// Statistics
331// =============================================================================
332
333size_t mpps_scu::creates_performed() const noexcept {
334 return creates_performed_.load(std::memory_order_relaxed);
335}
336
337size_t mpps_scu::sets_performed() const noexcept {
338 return sets_performed_.load(std::memory_order_relaxed);
339}
340
342 creates_performed_.store(0, std::memory_order_relaxed);
343 sets_performed_.store(0, std::memory_order_relaxed);
344}
345
346// =============================================================================
347// Private Implementation - Dataset Building
348// =============================================================================
349
351 using namespace core;
352 using namespace encoding;
353
354 dicom_dataset ds;
355
356 // Performed Procedure Step Status - always IN PROGRESS for N-CREATE
357 ds.set_string(mpps_tags::performed_procedure_step_status, vr_type::CS, "IN PROGRESS");
358
359 // Performed Procedure Step Timing
360 std::string start_date = data.procedure_step_start_date.empty()
362 std::string start_time = data.procedure_step_start_time.empty()
364
365 ds.set_string(mpps_tags::performed_procedure_step_start_date, vr_type::DA, start_date);
366 ds.set_string(mpps_tags::performed_procedure_step_start_time, vr_type::TM, start_time);
367
368 // Modality and Station
369 if (!data.modality.empty()) {
370 ds.set_string(tags::modality, vr_type::CS, data.modality);
371 }
372 if (!data.station_ae_title.empty()) {
373 ds.set_string(mpps_tags::performed_station_ae_title, vr_type::AE, data.station_ae_title);
374 }
375 if (!data.station_name.empty()) {
376 ds.set_string(mpps_tags::performed_station_name, vr_type::SH, data.station_name);
377 }
378
379 // Procedure Step ID
380 if (!data.scheduled_procedure_step_id.empty()) {
381 ds.set_string(mpps_tags::performed_procedure_step_id, vr_type::SH,
383 }
384
385 // Procedure Description
386 if (!data.procedure_description.empty()) {
387 ds.set_string(mpps_tags::performed_procedure_step_description, vr_type::LO,
389 }
390
391 // Patient Information (required)
392 std::string patient_name = data.patient_name.empty() ? "ANONYMOUS" : data.patient_name;
393 ds.set_string(tags::patient_name, vr_type::PN, patient_name);
394
395 if (!data.patient_id.empty()) {
396 ds.set_string(tags::patient_id, vr_type::LO, data.patient_id);
397 }
398 if (!data.patient_birth_date.empty()) {
399 ds.set_string(tags::patient_birth_date, vr_type::DA, data.patient_birth_date);
400 }
401 if (!data.patient_sex.empty()) {
402 ds.set_string(tags::patient_sex, vr_type::CS, data.patient_sex);
403 }
404
405 // Study reference
406 if (!data.study_instance_uid.empty()) {
407 ds.set_string(tags::study_instance_uid, vr_type::UI, data.study_instance_uid);
408 }
409 if (!data.accession_number.empty()) {
410 ds.set_string(tags::accession_number, vr_type::SH, data.accession_number);
411 }
412
413 // Performing Information
414 if (!data.performing_physician.empty()) {
415 ds.set_string(mpps_tags::performing_physicians_name, vr_type::PN,
417 }
418 if (!data.operator_name.empty()) {
419 ds.set_string(mpps_tags::operators_name, vr_type::PN, data.operator_name);
420 }
421
422 return ds;
423}
424
426 using namespace core;
427 using namespace encoding;
428
429 dicom_dataset ds;
430
431 // Update status
432 ds.set_string(mpps_tags::performed_procedure_step_status, vr_type::CS,
433 std::string(to_string(data.status)));
434
435 // End date/time
436 std::string end_date = data.procedure_step_end_date.empty()
438 std::string end_time = data.procedure_step_end_time.empty()
440
441 ds.set_string(mpps_tags::performed_procedure_step_end_date, vr_type::DA, end_date);
442 ds.set_string(mpps_tags::performed_procedure_step_end_time, vr_type::TM, end_time);
443
444 // Add performed series sequence for COMPLETED status
445 if (data.status == mpps_status::completed && !data.performed_series.empty()) {
446 auto& seq = ds.get_or_create_sequence(mpps_tags::performed_series_sequence);
447 for (const auto& s : data.performed_series) {
448 dicom_dataset series_item;
449
450 if (!s.series_uid.empty()) {
451 series_item.set_string(tags::series_instance_uid, vr_type::UI, s.series_uid);
452 }
453 if (!s.series_description.empty()) {
454 series_item.set_string(mpps_tags::series_description, vr_type::LO,
455 s.series_description);
456 }
457 if (!s.modality.empty()) {
458 series_item.set_string(tags::modality, vr_type::CS, s.modality);
459 }
460 if (!s.performing_physician.empty()) {
461 series_item.set_string(mpps_tags::performing_physicians_name, vr_type::PN,
462 s.performing_physician);
463 }
464 if (!s.operator_name.empty()) {
465 series_item.set_string(mpps_tags::operators_name, vr_type::PN, s.operator_name);
466 }
467
468 seq.push_back(std::move(series_item));
469 }
470 }
471
472 return ds;
473}
474
475// =============================================================================
476// Private Implementation - Utility Functions
477// =============================================================================
478
479std::string mpps_scu::generate_mpps_uid() const {
480 static std::mt19937_64 gen{std::random_device{}()};
481 static std::uniform_int_distribution<uint64_t> dist;
482
483 auto now = std::chrono::system_clock::now();
484 auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
485 now.time_since_epoch()).count();
486
487 return std::string(uid_root) + "." + std::to_string(timestamp) +
488 "." + std::to_string(dist(gen) % 100000);
489}
490
491std::string mpps_scu::get_current_date() const {
492 auto now = std::time(nullptr);
493 std::tm tm{};
494#if defined(_WIN32)
495 localtime_s(&tm, &now);
496#else
497 localtime_r(&now, &tm);
498#endif
499 std::ostringstream oss;
500 oss << std::put_time(&tm, "%Y%m%d");
501 return oss.str();
502}
503
504std::string mpps_scu::get_current_time() const {
505 auto now = std::time(nullptr);
506 std::tm tm{};
507#if defined(_WIN32)
508 localtime_s(&tm, &now);
509#else
510 localtime_r(&now, &tm);
511#endif
512 std::ostringstream oss;
513 oss << std::put_time(&tm, "%H%M%S");
514 return oss.str();
515}
516
517uint16_t mpps_scu::next_message_id() noexcept {
518 uint16_t id = message_id_counter_.fetch_add(1, std::memory_order_relaxed);
519 // Wrap around at 0xFFFF, skip 0 (reserved)
520 if (id == 0) {
521 id = message_id_counter_.fetch_add(1, std::memory_order_relaxed);
522 }
523 return id;
524}
525
526} // namespace kcenon::pacs::services
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 sets_performed() const noexcept
Get the number of N-SET operations performed.
Definition mpps_scu.cpp:337
network::Result< mpps_result > create(network::association &assoc, const mpps_create_data &data)
Create a new MPPS instance (N-CREATE)
Definition mpps_scu.cpp:98
core::dicom_dataset build_create_dataset(const mpps_create_data &data) const
Build DICOM dataset for N-CREATE request.
Definition mpps_scu.cpp:350
void reset_statistics() noexcept
Reset statistics counters to zero.
Definition mpps_scu.cpp:341
std::shared_ptr< di::ILogger > logger_
Logger instance.
Definition mpps_scu.h:414
std::atomic< uint16_t > message_id_counter_
Message ID counter.
Definition mpps_scu.h:420
mpps_scu_config config_
Configuration.
Definition mpps_scu.h:417
size_t creates_performed() const noexcept
Get the number of N-CREATE operations performed.
Definition mpps_scu.cpp:333
network::Result< mpps_result > discontinue(network::association &assoc, std::string_view mpps_uid, std::string_view reason="")
Discontinue an MPPS instance (convenience method)
Definition mpps_scu.cpp:314
core::dicom_dataset build_set_dataset(const mpps_set_data &data) const
Build DICOM dataset for N-SET request.
Definition mpps_scu.cpp:425
mpps_scu(std::shared_ptr< di::ILogger > logger=nullptr)
Construct MPPS SCU with default configuration.
Definition mpps_scu.cpp:86
network::Result< mpps_result > set(network::association &assoc, const mpps_set_data &data)
Update an existing MPPS instance (N-SET)
Definition mpps_scu.cpp:197
std::string get_current_time() const
Get current time in DICOM TM format (HHMMSS)
Definition mpps_scu.cpp:504
std::atomic< size_t > creates_performed_
Statistics: N-CREATE operations performed.
Definition mpps_scu.h:423
std::atomic< size_t > sets_performed_
Statistics: N-SET operations performed.
Definition mpps_scu.h:426
std::string get_current_date() const
Get current date in DICOM DA format (YYYYMMDD)
Definition mpps_scu.cpp:491
uint16_t next_message_id() noexcept
Get the next message ID for DIMSE operations.
Definition mpps_scu.cpp:517
std::string generate_mpps_uid() const
Generate a unique MPPS SOP Instance UID.
Definition mpps_scu.cpp:479
network::Result< mpps_result > complete(network::association &assoc, std::string_view mpps_uid, const std::vector< performed_series_info > &performed_series)
Complete an MPPS instance (convenience method)
Definition mpps_scu.cpp:299
DIMSE command field enumeration.
Compile-time constants for commonly used DICOM tags.
DICOM MPPS (Modality Performed Procedure Step) SCU service.
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag message_id
Message ID.
constexpr dicom_tag patient_birth_date
Patient's Birth Date.
constexpr dicom_tag accession_number
Accession Number.
constexpr dicom_tag modality
Modality.
constexpr dicom_tag patient_sex
Patient's Sex.
constexpr dicom_tag study_instance_uid
Study Instance UID.
constexpr dicom_tag patient_name
Patient's Name.
constexpr dicom_tag series_instance_uid
Series Instance UID.
constexpr int mpps_invalid_status_transition
Definition result.h:177
constexpr int mpps_context_not_accepted
Definition result.h:175
constexpr int mpps_missing_uid
Definition result.h:176
constexpr int mpps_unexpected_command
Definition result.h:174
constexpr int association_not_established
Definition result.h:202
constexpr core::dicom_tag tag_requested_sop_instance_uid
Requested SOP Instance UID (0000,1001) - UI.
constexpr core::dicom_tag performed_series_sequence
Performed Series Sequence (0040,0340)
Definition mpps_scp.h:450
constexpr core::dicom_tag performed_procedure_step_end_date
Performed Procedure Step End Date (0040,0250)
Definition mpps_scp.h:438
constexpr core::dicom_tag operators_name
Operators' Name (0008,1070)
Definition mpps_scu.h:457
constexpr core::dicom_tag performed_procedure_step_id
Performed Procedure Step ID (0040,0253)
Definition mpps_scp.h:447
constexpr core::dicom_tag performing_physicians_name
Performing Physician's Name (0008,1050)
Definition mpps_scu.h:454
constexpr core::dicom_tag performed_station_name
Performed Station Name (0040,0242)
Definition mpps_scp.h:432
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
constexpr core::dicom_tag performed_procedure_step_start_date
Performed Procedure Step Start Date (0040,0244)
Definition mpps_scu.h:436
constexpr core::dicom_tag performed_procedure_step_end_time
Performed Procedure Step End Time (0040,0251)
Definition mpps_scp.h:441
constexpr core::dicom_tag performed_procedure_step_start_time
Performed Procedure Step Start Time (0040,0245)
Definition mpps_scu.h:439
constexpr core::dicom_tag series_description
Series Description (0008,103E)
Definition mpps_scu.h:460
constexpr core::dicom_tag performed_procedure_step_description
Performed Procedure Step Description (0040,0254)
Definition mpps_scu.h:442
@ completed
Procedure completed successfully.
@ discontinued
Procedure was stopped/cancelled.
@ in_progress
Procedure is currently being performed.
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
Result< T > pacs_error(int code, const std::string &message, const std::string &details="")
Create a PACS error result with module context.
Definition result.h:234
Result<T> type aliases and helpers for PACS system.
DIMSE status codes.
Data for N-CREATE operation (start procedure)
Definition mpps_scu.h:75
std::string procedure_step_start_date
DICOM DA format (YYYYMMDD)
Definition mpps_scu.h:89
std::string procedure_step_start_time
DICOM TM format (HHMMSS)
Definition mpps_scu.h:90
std::string mpps_sop_instance_uid
Generated if empty.
Definition mpps_scu.h:88
Result of an MPPS operation.
Definition mpps_scu.h:132
std::chrono::milliseconds elapsed
Time taken for the operation.
Definition mpps_scu.h:143
bool is_success() const noexcept
Check if the operation was successful.
Definition mpps_scu.h:146
uint16_t status
DIMSE status code (0x0000 = success)
Definition mpps_scu.h:137
std::string mpps_sop_instance_uid
MPPS SOP Instance UID.
Definition mpps_scu.h:134
std::string error_comment
Error comment from the SCP (if any)
Definition mpps_scu.h:140
Configuration for MPPS SCU service.
Definition mpps_scu.h:164
std::chrono::milliseconds timeout
Timeout for receiving DIMSE response.
Definition mpps_scu.h:166
bool auto_generate_uid
Auto-generate MPPS UID if not provided.
Definition mpps_scu.h:169
Data for N-SET operation (update/complete procedure)
Definition mpps_scu.h:107
mpps_status status
New status (COMPLETED or DISCONTINUED)
Definition mpps_scu.h:112
std::string procedure_step_end_date
Procedure Step End Date (required for COMPLETED/DISCONTINUED)
Definition mpps_scu.h:115
std::string discontinuation_reason
Discontinuation reason (for DISCONTINUED status)
Definition mpps_scu.h:124
std::vector< performed_series_info > performed_series
Performed Series Sequence (for COMPLETED status)
Definition mpps_scu.h:121
std::string procedure_step_end_time
Procedure Step End Time (required for COMPLETED/DISCONTINUED)
Definition mpps_scu.h:118
std::string mpps_sop_instance_uid
MPPS SOP Instance UID (required)
Definition mpps_scu.h:109