PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
ups_watch_scu.cpp
Go to the documentation of this file.
1
7
13
14#include <chrono>
15
16namespace kcenon::pacs::services {
17
18// =============================================================================
19// Construction
20// =============================================================================
21
22ups_watch_scu::ups_watch_scu(std::shared_ptr<di::ILogger> logger)
23 : logger_(logger ? std::move(logger) : di::null_logger()) {}
24
26 std::shared_ptr<di::ILogger> logger)
27 : logger_(logger ? std::move(logger) : di::null_logger()),
28 config_(config) {}
29
30// =============================================================================
31// Callback Configuration
32// =============================================================================
33
37
38// =============================================================================
39// N-ACTION Operations
40// =============================================================================
41
44 const ups_subscribe_data& data) {
45
46 // Determine the SOP Instance UID: workitem-specific or global
47 std::string sop_instance_uid = data.workitem_uid.empty()
49 : data.workitem_uid;
50
51 logger_->debug("UPS Watch SCU: Subscribe request" +
52 (data.workitem_uid.empty() ? std::string(" (global)") :
53 " for workitem " + data.workitem_uid));
54
55 auto result = send_watch_action(
56 assoc, sop_instance_uid, ups_watch_action_subscribe);
57
58 if (result.is_ok() && result.value().is_success()) {
59 subscribes_performed_.fetch_add(1, std::memory_order_relaxed);
60 logger_->info("UPS Watch SCU: Subscribe successful" +
61 (data.workitem_uid.empty() ? std::string(" (global)") :
62 " for workitem " + data.workitem_uid));
63 }
64
65 return result;
66}
67
70 const ups_unsubscribe_data& data) {
71
72 std::string sop_instance_uid = data.workitem_uid.empty()
74 : data.workitem_uid;
75
76 logger_->debug("UPS Watch SCU: Unsubscribe request" +
77 (data.workitem_uid.empty() ? std::string(" (global)") :
78 " for workitem " + data.workitem_uid));
79
80 auto result = send_watch_action(
81 assoc, sop_instance_uid, ups_watch_action_unsubscribe);
82
83 if (result.is_ok() && result.value().is_success()) {
84 unsubscribes_performed_.fetch_add(1, std::memory_order_relaxed);
85 logger_->info("UPS Watch SCU: Unsubscribe successful" +
86 (data.workitem_uid.empty() ? std::string(" (global)") :
87 " for workitem " + data.workitem_uid));
88 }
89
90 return result;
91}
92
94 network::association& assoc) {
95
96 logger_->debug("UPS Watch SCU: Suspend global subscription");
97
98 auto result = send_watch_action(
99 assoc,
102
103 if (result.is_ok() && result.value().is_success()) {
104 unsubscribes_performed_.fetch_add(1, std::memory_order_relaxed);
105 logger_->info("UPS Watch SCU: Global subscription suspended");
106 }
107
108 return result;
109}
110
111// =============================================================================
112// N-EVENT-REPORT Handler
113// =============================================================================
114
116 const network::dimse::dimse_message& event_rq) {
117
118 using namespace network::dimse;
119
120 // Verify command is N-EVENT-REPORT-RQ
121 if (event_rq.command() != command_field::n_event_report_rq) {
124 "Expected N-EVENT-REPORT-RQ, got: " +
125 std::string(to_string(event_rq.command())));
126 }
127
128 // Get event type ID
129 auto event_type = event_rq.event_type_id();
130 if (!event_type.has_value()) {
133 "Missing Event Type ID in N-EVENT-REPORT");
134 }
135
136 // Get workitem UID from the affected SOP Instance UID
137 std::string workitem_uid{event_rq.affected_sop_instance_uid()};
138
139 // Parse dataset if present
140 ups_watch_event event;
141 if (event_rq.has_dataset()) {
142 auto dataset_result = event_rq.dataset();
143 if (dataset_result.is_ok()) {
144 event = parse_event_dataset(
145 event_type.value(), workitem_uid,
146 dataset_result.value().get());
147 } else {
148 event.event_type_id = event_type.value();
149 event.workitem_uid = workitem_uid;
150 event.timestamp = std::chrono::system_clock::now();
151 }
152 } else {
153 event.event_type_id = event_type.value();
154 event.workitem_uid = workitem_uid;
155 event.timestamp = std::chrono::system_clock::now();
156 }
157
158 events_received_.fetch_add(1, std::memory_order_relaxed);
159
160 logger_->info("UPS Watch SCU: Received event type " +
161 std::to_string(event.event_type_id) +
162 " for workitem " + event.workitem_uid);
163
164 // Invoke callback if registered
165 if (event_callback_) {
166 event_callback_(event);
167 }
168
169 return kcenon::pacs::Result<ups_watch_event>::ok(std::move(event));
170}
171
172// =============================================================================
173// Statistics
174// =============================================================================
175
176size_t ups_watch_scu::subscribes_performed() const noexcept {
177 return subscribes_performed_.load(std::memory_order_relaxed);
178}
179
181 return unsubscribes_performed_.load(std::memory_order_relaxed);
182}
183
184size_t ups_watch_scu::events_received() const noexcept {
185 return events_received_.load(std::memory_order_relaxed);
186}
187
189 subscribes_performed_.store(0, std::memory_order_relaxed);
190 unsubscribes_performed_.store(0, std::memory_order_relaxed);
191 events_received_.store(0, std::memory_order_relaxed);
192}
193
194// =============================================================================
195// Private Implementation - Send Watch Action
196// =============================================================================
197
200 const std::string& sop_instance_uid,
201 uint16_t action_type_id) {
202
203 using namespace network::dimse;
204
205 auto start_time = std::chrono::steady_clock::now();
206
207 // Verify association is established
208 if (!assoc.is_established()) {
211 "Association not established");
212 }
213
214 // Get accepted presentation context for UPS Watch
215 auto context_id = assoc.accepted_context_id(ups_watch_sop_class_uid);
216 if (!context_id) {
219 "No accepted presentation context for UPS Watch SOP Class");
220 }
221
222 // Build the N-ACTION request
223 auto request = make_n_action_rq(
226 sop_instance_uid,
227 action_type_id);
228
229 // Send the request
230 auto send_result = assoc.send_dimse(*context_id, request);
231 if (send_result.is_err()) {
232 logger_->error("UPS Watch SCU: Failed to send N-ACTION: " +
233 send_result.error().message);
234 return send_result.error();
235 }
236
237 // Receive the response
238 auto recv_result = assoc.receive_dimse(config_.timeout);
239 if (recv_result.is_err()) {
240 logger_->error("UPS Watch SCU: Failed to receive N-ACTION response: " +
241 recv_result.error().message);
242 return recv_result.error();
243 }
244
245 const auto& [recv_context_id, response] = recv_result.value();
246
247 // Verify it's an N-ACTION response
248 if (response.command() != command_field::n_action_rsp) {
251 "Expected N-ACTION-RSP but received " +
252 std::string(to_string(response.command())));
253 }
254
255 auto end_time = std::chrono::steady_clock::now();
256 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
257 end_time - start_time);
258
259 // Build result
260 ups_result result;
261 result.workitem_uid = sop_instance_uid;
262 result.status = static_cast<uint16_t>(response.status());
263 result.elapsed = elapsed;
264
265 if (response.command_set().contains(tag_error_comment)) {
266 result.error_comment = response.command_set().get_string(tag_error_comment);
267 }
268
269 if (!result.is_success()) {
270 logger_->warn("UPS Watch SCU: N-ACTION returned status 0x" +
271 std::to_string(result.status));
272 }
273
274 return result;
275}
276
277// =============================================================================
278// Private Implementation - Event Parsing
279// =============================================================================
280
282 uint16_t event_type_id,
283 const std::string& workitem_uid,
284 const core::dicom_dataset& dataset) {
285
286 ups_watch_event event;
287 event.event_type_id = event_type_id;
288 event.workitem_uid = workitem_uid;
289 event.event_info = dataset;
290 event.timestamp = std::chrono::system_clock::now();
291
292 // Extract fields based on event type
293 switch (event_type_id) {
295 event.procedure_step_state = dataset.get_string(
297 break;
298
300 event.cancellation_reason = dataset.get_string(
302 break;
303
305 event.progress = dataset.get_string(
307 break;
308
309 default:
310 break;
311 }
312
313 return event;
314}
315
316// =============================================================================
317// Private Implementation - Utility Functions
318// =============================================================================
319
321 uint16_t id = message_id_counter_.fetch_add(1, std::memory_order_relaxed);
322 if (id == 0) {
323 id = message_id_counter_.fetch_add(1, std::memory_order_relaxed);
324 }
325 return id;
326}
327
328} // namespace kcenon::pacs::services
auto get_string(dicom_tag tag, std::string_view default_value="") const -> std::string
Get the string value of an element.
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.
auto event_type_id() const -> std::optional< uint16_t >
Get the Event Type ID (for N-EVENT-REPORT)
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.
event_received_callback event_callback_
network::Result< ups_result > send_watch_action(network::association &assoc, const std::string &sop_instance_uid, uint16_t action_type_id)
size_t unsubscribes_performed() const noexcept
network::Result< ups_result > subscribe(network::association &assoc, const ups_subscribe_data &data)
Subscribe to UPS workitem events (N-ACTION Type 3)
std::atomic< size_t > unsubscribes_performed_
std::function< void( const ups_watch_event &event)> event_received_callback
Callback type for received event notifications.
size_t subscribes_performed() const noexcept
std::shared_ptr< di::ILogger > logger_
void set_event_received_callback(event_received_callback cb)
Set callback for received event notifications.
network::Result< ups_result > suspend_global(network::association &assoc)
Suspend global subscription (N-ACTION Type 5)
ups_watch_scu(std::shared_ptr< di::ILogger > logger=nullptr)
Construct UPS Watch SCU with default configuration.
std::atomic< size_t > subscribes_performed_
size_t events_received() const noexcept
std::atomic< uint16_t > message_id_counter_
network::Result< ups_result > unsubscribe(network::association &assoc, const ups_unsubscribe_data &data)
Unsubscribe from UPS workitem events (N-ACTION Type 4)
network::Result< ups_watch_event > handle_event_report(const network::dimse::dimse_message &event_rq)
Handle an N-EVENT-REPORT-RQ received from the SCP.
static ups_watch_event parse_event_dataset(uint16_t event_type_id, const std::string &workitem_uid, const core::dicom_dataset &dataset)
DIMSE command field enumeration.
Compile-time constants for commonly used DICOM tags.
constexpr int ups_unexpected_command
Definition result.h:210
constexpr int ups_context_not_accepted
Definition result.h:216
constexpr int ups_invalid_action_type
Definition result.h:214
constexpr int association_not_established
Definition result.h:202
constexpr core::dicom_tag procedure_step_progress
Procedure Step Progress (0074,1004)
constexpr core::dicom_tag procedure_step_state
Procedure Step State (0074,1000)
constexpr core::dicom_tag reason_for_cancellation
Reason for Cancellation (0074,1238)
constexpr uint16_t ups_event_state_report
Event Type 1: UPS State Report — workitem state changed.
constexpr uint16_t ups_event_progress_report
Event Type 3: UPS Progress Report — progress percentage update.
constexpr std::string_view ups_watch_sop_class_uid
UPS Watch SOP Class UID (PS3.4 Table CC.2-1)
constexpr uint16_t ups_watch_action_suspend_global
N-ACTION Type 5: Suspend Global Subscription (PS3.4 CC.2.3.5)
constexpr std::string_view ups_global_subscription_instance_uid
UPS Global Subscription SOP Instance UID Used as the SOP Instance UID for global (non-workitem-specif...
auto to_string(mpps_status status) -> std::string_view
Convert mpps_status to DICOM string representation.
Definition mpps_scp.h:60
constexpr uint16_t ups_event_cancel_requested
Event Type 2: UPS Cancel Requested — performer should stop processing.
constexpr uint16_t ups_watch_action_unsubscribe
N-ACTION Type 4: Unsubscribe from Receiving UPS Event Reports (PS3.4 CC.2.3.4)
constexpr uint16_t ups_watch_action_subscribe
N-ACTION Type 3: Subscribe to Receive UPS Event Reports (PS3.4 CC.2.3.3)
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.
Result of a UPS SCU operation.
std::chrono::milliseconds elapsed
Time taken for the operation.
bool is_success() const noexcept
Check if the operation was successful.
std::string workitem_uid
Workitem SOP Instance UID.
uint16_t status
DIMSE status code (0x0000 = success)
std::string error_comment
Error comment from the SCP (if any)
Data for N-ACTION subscribe request.
std::string workitem_uid
Workitem SOP Instance UID (empty for global subscription)
Data for N-ACTION unsubscribe request.
std::string workitem_uid
Workitem SOP Instance UID (empty for global unsubscription)
Parsed UPS event notification received from SCP.
uint16_t event_type_id
Event type ID (ups_event_state_report, ups_event_cancel_requested, etc.)
std::string workitem_uid
Workitem SOP Instance UID that triggered the event.
Configuration for UPS Watch SCU service.
std::chrono::milliseconds timeout
Timeout for receiving DIMSE response.
DICOM UPS Watch SCU service (subscription client and event receiver)