PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
worklist_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
19
20#include <chrono>
21#include <ctime>
22#include <iomanip>
23#include <sstream>
24
25namespace kcenon::pacs::services {
26
27// =============================================================================
28// Construction
29// =============================================================================
30
31worklist_scu::worklist_scu(std::shared_ptr<di::ILogger> logger)
32 : logger_(logger ? std::move(logger) : di::null_logger()) {}
33
35 std::shared_ptr<di::ILogger> logger)
36 : logger_(logger ? std::move(logger) : di::null_logger()), config_(config) {}
37
38// =============================================================================
39// Generic Query Operations
40// =============================================================================
41
44 const worklist_query_keys& keys) {
45
46 auto query_ds = build_query_dataset(keys);
47 return query(assoc, query_ds);
48}
49
52 const core::dicom_dataset& query_keys) {
53
54 return query_impl(assoc, query_keys, next_message_id());
55}
56
59 const core::dicom_dataset& query_keys,
60 uint16_t message_id) {
61
62 using namespace network::dimse;
63
64 auto start_time = std::chrono::steady_clock::now();
65
66 // Verify association is established
67 if (!assoc.is_established()) {
70 "Association not established");
71 }
72
73 // Get accepted presentation context for MWL
74 auto context_id = assoc.accepted_context_id(worklist_find_sop_class_uid);
75 if (!context_id) {
78 "No accepted presentation context for Modality Worklist: " +
79 std::string(worklist_find_sop_class_uid));
80 }
81
82 // Build C-FIND-RQ message
83 auto request = make_c_find_rq(message_id, worklist_find_sop_class_uid);
84 request.set_dataset(query_keys);
85
86 logger_->debug_fmt("Sending MWL C-FIND request (message_id={})", message_id);
87
88 // Send the request
89 auto send_result = assoc.send_dimse(*context_id, request);
90 if (send_result.is_err()) {
91 return send_result.error();
92 }
93
94 // Receive responses
95 worklist_result result;
96 bool query_complete = false;
97
98 while (!query_complete) {
99 auto recv_result = assoc.receive_dimse(config_.timeout);
100 if (recv_result.is_err()) {
101 return recv_result.error();
102 }
103
104 const auto& [recv_context_id, response] = recv_result.value();
105
106 // Verify it's a C-FIND response
107 if (response.command() != command_field::c_find_rsp) {
110 "Expected C-FIND-RSP but received " +
111 std::string(to_string(response.command())));
112 }
113
114 auto status = response.status();
115
116 if (status == status_pending || status == status_pending_warning) {
117 ++result.total_pending;
118
119 if (response.has_dataset()) {
120 // Check if we should collect this result
121 if (config_.max_results == 0 ||
122 result.items.size() < config_.max_results) {
123
124 auto dataset_result = response.dataset();
125 if (dataset_result.is_ok()) {
126 result.items.push_back(
127 parse_worklist_item(dataset_result.value().get()));
128 }
129 }
130
131 // Check if we should cancel due to max_results
132 if (config_.max_results > 0 &&
133 result.items.size() >= config_.max_results &&
135
136 logger_->debug_fmt(
137 "Max results ({}) reached, sending C-CANCEL",
139
140 // Send C-CANCEL
141 auto cancel_result = cancel(assoc, message_id);
142 if (cancel_result.is_err()) {
143 logger_->warn_fmt("Failed to send C-CANCEL: {}",
144 cancel_result.error().message);
145 }
146 }
147 }
148 } else if (status == status_success) {
149 query_complete = true;
150 result.status = static_cast<uint16_t>(status);
151 } else if (status == status_cancel) {
152 query_complete = true;
153 result.status = static_cast<uint16_t>(status);
154 } else {
155 // Error status
156 query_complete = true;
157 result.status = static_cast<uint16_t>(status);
158 }
159 }
160
161 auto end_time = std::chrono::steady_clock::now();
162 result.elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
163 end_time - start_time);
164
165 // Update statistics
166 queries_performed_.fetch_add(1, std::memory_order_relaxed);
167 total_items_.fetch_add(result.items.size(), std::memory_order_relaxed);
168
169 logger_->debug_fmt("MWL C-FIND completed: {} items in {} ms",
170 result.items.size(), result.elapsed.count());
171
172 return result;
173}
174
175// =============================================================================
176// Convenience Query Methods
177// =============================================================================
178
181 std::string_view station_ae,
182 std::string_view modality) {
183
185 keys.scheduled_station_ae = std::string(station_ae);
186 keys.modality = std::string(modality);
188
189 return query(assoc, keys);
190}
191
194 std::string_view start_date,
195 std::string_view end_date,
196 std::string_view modality) {
197
199 // DICOM date range format: YYYYMMDD-YYYYMMDD
200 keys.scheduled_date = std::string(start_date) + "-" + std::string(end_date);
201 keys.modality = std::string(modality);
202
203 return query(assoc, keys);
204}
205
208 std::string_view patient_id) {
209
211 keys.patient_id = std::string(patient_id);
212
213 return query(assoc, keys);
214}
215
216// =============================================================================
217// Streaming Query
218// =============================================================================
219
222 const worklist_query_keys& keys,
224
225 using namespace network::dimse;
226
227 auto message_id = next_message_id();
228 auto start_time = std::chrono::steady_clock::now();
229
230 // Verify association is established
231 if (!assoc.is_established()) {
234 "Association not established");
235 }
236
237 // Get accepted presentation context for MWL
238 auto context_id = assoc.accepted_context_id(worklist_find_sop_class_uid);
239 if (!context_id) {
242 "No accepted presentation context for Modality Worklist");
243 }
244
245 // Build query dataset and C-FIND-RQ message
246 auto query_ds = build_query_dataset(keys);
247 auto request = make_c_find_rq(message_id, worklist_find_sop_class_uid);
248 request.set_dataset(query_ds);
249
250 logger_->debug_fmt("Sending streaming MWL C-FIND request (message_id={})",
251 message_id);
252
253 // Send the request
254 auto send_result = assoc.send_dimse(*context_id, request);
255 if (send_result.is_err()) {
256 return send_result.error();
257 }
258
259 // Receive responses
260 size_t count = 0;
261 bool query_complete = false;
262 bool should_cancel = false;
263
264 while (!query_complete) {
265 auto recv_result = assoc.receive_dimse(config_.timeout);
266 if (recv_result.is_err()) {
267 return recv_result.error();
268 }
269
270 const auto& [recv_context_id, response] = recv_result.value();
271
272 // Verify it's a C-FIND response
273 if (response.command() != command_field::c_find_rsp) {
276 "Expected C-FIND-RSP but received " +
277 std::string(to_string(response.command())));
278 }
279
280 auto status = response.status();
281
282 if (status == status_pending || status == status_pending_warning) {
283 if (response.has_dataset()) {
284 auto dataset_result = response.dataset();
285 if (dataset_result.is_ok()) {
286 ++count;
287 auto item = parse_worklist_item(dataset_result.value().get());
288
289 // Call the callback
290 if (callback) {
291 if (!callback(item)) {
292 should_cancel = true;
293 }
294 }
295
296 // Check if we need to cancel
297 if (should_cancel ||
298 (config_.max_results > 0 &&
299 count >= config_.max_results &&
301
302 logger_->debug("Cancelling streaming MWL query");
303 auto cancel_result = cancel(assoc, message_id);
304 if (cancel_result.is_err()) {
305 logger_->warn_fmt("Failed to send C-CANCEL: {}",
306 cancel_result.error().message);
307 }
308 should_cancel = false; // Already sent cancel
309 }
310 }
311 }
312 } else {
313 query_complete = true;
314 }
315 }
316
317 // Update statistics
318 queries_performed_.fetch_add(1, std::memory_order_relaxed);
319 total_items_.fetch_add(count, std::memory_order_relaxed);
320
321 auto end_time = std::chrono::steady_clock::now();
322 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
323 end_time - start_time);
324
325 logger_->debug_fmt("Streaming MWL C-FIND completed: {} items in {} ms",
326 count, elapsed.count());
327
328 return count;
329}
330
331// =============================================================================
332// C-CANCEL Support
333// =============================================================================
334
337 uint16_t message_id) {
338
339 using namespace network::dimse;
340
341 auto context_id = assoc.accepted_context_id(worklist_find_sop_class_uid);
342 if (!context_id) {
345 "No accepted presentation context for cancel");
346 }
347
348 // Build C-CANCEL-RQ
349 dimse_message cancel_rq{command_field::c_cancel_rq, message_id};
350 return assoc.send_dimse(*context_id, cancel_rq);
351}
352
353// =============================================================================
354// Configuration
355// =============================================================================
356
360
362 return config_;
363}
364
365// =============================================================================
366// Statistics
367// =============================================================================
368
369size_t worklist_scu::queries_performed() const noexcept {
370 return queries_performed_.load(std::memory_order_relaxed);
371}
372
373size_t worklist_scu::total_items() const noexcept {
374 return total_items_.load(std::memory_order_relaxed);
375}
376
378 queries_performed_.store(0, std::memory_order_relaxed);
379 total_items_.store(0, std::memory_order_relaxed);
380}
381
382// =============================================================================
383// Private Implementation
384// =============================================================================
385
386uint16_t worklist_scu::next_message_id() noexcept {
387 uint16_t id = message_id_counter_.fetch_add(1, std::memory_order_relaxed);
388 // Wrap around at 0xFFFF, skip 0 (reserved)
389 if (id == 0) {
390 id = message_id_counter_.fetch_add(1, std::memory_order_relaxed);
391 }
392 return id;
393}
394
396 auto now = std::time(nullptr);
397 auto* tm = std::localtime(&now);
398 std::ostringstream oss;
399 oss << std::put_time(tm, "%Y%m%d");
400 return oss.str();
401}
402
404 const core::dicom_dataset& ds) const {
405
406 using namespace core;
407
408 worklist_item item;
409
410 // Patient demographics
411 item.patient_name = ds.get_string(tags::patient_name);
412 item.patient_id = ds.get_string(tags::patient_id);
413 item.patient_birth_date = ds.get_string(tags::patient_birth_date);
414 item.patient_sex = ds.get_string(tags::patient_sex);
415
416 // Study-level attributes
417 item.study_instance_uid = ds.get_string(tags::study_instance_uid);
418 item.accession_number = ds.get_string(tags::accession_number);
419 item.referring_physician = ds.get_string(tags::referring_physician_name);
420 item.institution = ds.get_string(tags::institution_name);
421
422 // Requested Procedure attributes
423 item.requested_procedure_id = ds.get_string(tags::requested_procedure_id);
424 item.requested_procedure_description = ds.get_string(tags::study_description);
425
426 // Scheduled Procedure Step attributes (flat structure)
427 item.scheduled_station_ae = ds.get_string(tags::scheduled_station_ae_title);
428 item.modality = ds.get_string(tags::modality);
431 item.scheduled_procedure_step_id = ds.get_string(tags::scheduled_procedure_step_id);
432 item.scheduled_procedure_step_description =
434
435 // Store original dataset for full access
436 item.dataset = ds;
437
438 return item;
439}
440
442 const worklist_query_keys& keys) const {
443
444 using namespace core;
445 using namespace encoding;
446
447 dicom_dataset ds;
448
449 // Patient demographics (return keys with optional search criteria)
450 ds.set_string(tags::patient_name, vr_type::PN, keys.patient_name);
451 ds.set_string(tags::patient_id, vr_type::LO, keys.patient_id);
452
453 if (!keys.patient_birth_date.empty()) {
454 ds.set_string(tags::patient_birth_date, vr_type::DA, keys.patient_birth_date);
455 } else {
456 ds.set_string(tags::patient_birth_date, vr_type::DA, "");
457 }
458
459 if (!keys.patient_sex.empty()) {
460 ds.set_string(tags::patient_sex, vr_type::CS, keys.patient_sex);
461 } else {
462 ds.set_string(tags::patient_sex, vr_type::CS, "");
463 }
464
465 // Study-level return keys
466 ds.set_string(tags::study_instance_uid, vr_type::UI, "");
467 ds.set_string(tags::accession_number, vr_type::SH, keys.accession_number);
468 ds.set_string(tags::referring_physician_name, vr_type::PN, keys.referring_physician);
469 ds.set_string(tags::institution_name, vr_type::LO, keys.institution);
470
471 // Requested Procedure attributes
472 ds.set_string(tags::requested_procedure_id, vr_type::SH, keys.requested_procedure_id);
473 ds.set_string(tags::study_description, vr_type::LO, keys.requested_procedure_description);
474
475 // Scheduled Procedure Step attributes (flat structure)
476 ds.set_string(tags::scheduled_station_ae_title, vr_type::AE, keys.scheduled_station_ae);
477 ds.set_string(tags::scheduled_procedure_step_start_date, vr_type::DA, keys.scheduled_date);
478 ds.set_string(tags::scheduled_procedure_step_start_time, vr_type::TM, keys.scheduled_time);
479 ds.set_string(tags::modality, vr_type::CS, keys.modality);
480 ds.set_string(tags::scheduled_performing_physician_name, vr_type::PN, keys.scheduled_physician);
481 ds.set_string(tags::scheduled_procedure_step_id, vr_type::SH, keys.scheduled_procedure_step_id);
482
483 // Additional return keys
484 ds.set_string(tags::scheduled_procedure_step_description, vr_type::LO, "");
485 ds.set_string(tags::scheduled_station_name, vr_type::SH, "");
486 ds.set_string(tags::scheduled_procedure_step_location, vr_type::SH, "");
487
488 return ds;
489}
490
491} // 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.
void reset_statistics() noexcept
Reset statistics counters to zero.
worklist_scu(std::shared_ptr< di::ILogger > logger=nullptr)
Construct a Worklist SCU with default configuration.
std::atomic< size_t > total_items_
Statistics: total number of items received.
network::Result< worklist_result > query_impl(network::association &assoc, const core::dicom_dataset &query_keys, uint16_t message_id)
Internal query implementation.
std::atomic< uint16_t > message_id_counter_
Message ID counter.
size_t queries_performed() const noexcept
Get the number of queries performed since construction.
std::shared_ptr< di::ILogger > logger_
Logger instance for service logging.
network::Result< worklist_result > query_patient(network::association &assoc, std::string_view patient_id)
Query worklist by patient ID.
network::Result< std::monostate > cancel(network::association &assoc, uint16_t message_id)
Send a C-CANCEL request to stop an ongoing query.
uint16_t next_message_id() noexcept
Get the next message ID for DIMSE operations.
const worklist_scu_config & config() const noexcept
Get the current configuration.
network::Result< worklist_result > query(network::association &assoc, const worklist_query_keys &keys)
Perform a MWL C-FIND query with typed keys.
static std::string get_today_date()
Get today's date in DICOM format (YYYYMMDD)
std::atomic< size_t > queries_performed_
Statistics: number of queries performed.
core::dicom_dataset build_query_dataset(const worklist_query_keys &keys) const
Build query dataset from typed keys.
worklist_scu_config config_
Configuration.
void set_config(const worklist_scu_config &config)
Update the SCU configuration.
size_t total_items() const noexcept
Get the total number of items received since construction.
worklist_item parse_worklist_item(const core::dicom_dataset &ds) const
Parse a worklist item from a response dataset.
network::Result< size_t > query_streaming(network::association &assoc, const worklist_query_keys &keys, worklist_streaming_callback callback)
Perform a streaming MWL query for large worklists.
network::Result< worklist_result > query_date_range(network::association &assoc, std::string_view start_date, std::string_view end_date, std::string_view modality="")
Query worklist by date range.
network::Result< worklist_result > query_today(network::association &assoc, std::string_view station_ae, std::string_view modality="")
Query today's worklist for a station.
DIMSE command field enumeration.
Compile-time constants for commonly used DICOM tags.
constexpr dicom_tag referring_physician_name
Referring Physician's Name.
constexpr dicom_tag scheduled_procedure_step_description
Scheduled Procedure Step Description.
constexpr dicom_tag institution_name
Institution Name.
constexpr dicom_tag study_description
Study Description.
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag scheduled_procedure_step_start_date
Scheduled Procedure Step Start Date.
constexpr dicom_tag scheduled_procedure_step_location
Scheduled Procedure Step Location.
constexpr dicom_tag patient_birth_date
Patient's Birth Date.
constexpr dicom_tag scheduled_performing_physician_name
Scheduled Performing Physician's Name.
constexpr dicom_tag accession_number
Accession Number.
constexpr dicom_tag modality
Modality.
constexpr dicom_tag scheduled_station_ae_title
Scheduled Station AE Title.
constexpr dicom_tag patient_sex
Patient's Sex.
constexpr dicom_tag study_instance_uid
Study Instance UID.
constexpr dicom_tag scheduled_station_name
Scheduled Station Name.
constexpr dicom_tag requested_procedure_id
Requested Procedure ID.
constexpr dicom_tag patient_name
Patient's Name.
constexpr dicom_tag scheduled_procedure_step_start_time
Scheduled Procedure Step Start Time.
constexpr dicom_tag scheduled_procedure_step_id
Scheduled Procedure Step ID.
constexpr int no_acceptable_context
Definition result.h:103
constexpr int find_unexpected_command
Definition result.h:157
constexpr int association_not_established
Definition result.h:202
constexpr std::string_view worklist_find_sop_class_uid
Modality Worklist Information Model - FIND SOP Class UID.
std::function< bool(const worklist_item &)> worklist_streaming_callback
Callback type for streaming worklist query results.
auto to_string(mpps_status status) -> std::string_view
Convert mpps_status to DICOM string representation.
Definition mpps_scp.h:60
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.
Parsed worklist item from MWL query response.
Typed query keys for Modality Worklist queries.
std::string scheduled_station_ae
Scheduled Station AE Title (0040,0001)
std::string institution
Institution Name (0008,0080)
std::string scheduled_date
Scheduled Procedure Step Start Date (0040,0002) - YYYYMMDD or range.
std::string modality
Modality (0008,0060) - e.g., CT, MR, US, XR.
std::string requested_procedure_id
Requested Procedure ID (0040,1001)
std::string patient_sex
Patient's Sex (0010,0040) - M, F, O.
std::string scheduled_physician
Scheduled Performing Physician's Name (0040,0006)
std::string referring_physician
Referring Physician's Name (0008,0090)
std::string accession_number
Accession Number (0008,0050)
std::string requested_procedure_description
Requested Procedure Description (0032,1060)
std::string patient_name
Patient's Name (0010,0010) - supports wildcards (* ?)
std::string patient_id
Patient ID (0010,0020)
std::string scheduled_procedure_step_id
Scheduled Procedure Step ID (0040,0009)
std::string patient_birth_date
Patient's Birth Date (0010,0030)
std::string scheduled_time
Scheduled Procedure Step Start Time (0040,0003) - HHMMSS or range.
Result of a Modality Worklist query operation.
std::vector< worklist_item > items
Parsed worklist items from the query.
std::chrono::milliseconds elapsed
Query execution time.
size_t total_pending
Total pending responses received (may differ from items.size() if max_results was enforced)
uint16_t status
Final DIMSE status code (0x0000 = success)
Configuration for Worklist SCU service.
std::chrono::milliseconds timeout
Timeout for receiving query responses (milliseconds)
size_t max_results
Maximum number of results to return (0 = unlimited)
bool cancel_on_max
Send C-CANCEL when max_results is reached.
DICOM Modality Worklist SCP service (MWL C-FIND handler)
DICOM Modality Worklist SCU service (MWL C-FIND sender)