PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1
26
28
29#include <chrono>
30#include <cstdlib>
31#include <iostream>
32#include <string>
33
34namespace {
35
37constexpr const char* default_calling_ae = "MPPS_SCU";
38
40constexpr auto default_timeout = std::chrono::milliseconds{30000};
41
45enum class mpps_command {
46 create, // N-CREATE (start procedure)
47 set // N-SET (complete/discontinue)
48};
49
53enum class cli_status_type {
56};
57
61struct options {
62 // Connection
63 std::string host;
64 uint16_t port{0};
65 std::string called_ae;
66 std::string calling_ae{default_calling_ae};
67
68 // Command
69 mpps_command command{mpps_command::create};
70
71 // N-CREATE options (create new MPPS)
72 std::string patient_name;
73 std::string patient_id;
74 std::string modality{"CT"};
75 std::string procedure_id;
76 std::string study_uid;
77
78 // N-SET options (update existing MPPS)
79 std::string mpps_uid;
80 cli_status_type status{cli_status_type::completed};
81 std::string discontinuation_reason;
82 std::string series_uid;
83
84 // Output options
85 bool verbose{false};
86};
87
91void print_usage(const char* program_name) {
92 std::cout << R"(
93MPPS SCU - Modality Performed Procedure Step Client
94
95Usage: )" << program_name << R"( <host> <port> <called_ae> <command> [options]
96
97Arguments:
98 host Remote host address (IP or hostname)
99 port Remote port number (typically 11112)
100 called_ae Called AE Title (remote MPPS SCP's AE title, e.g., RIS_SCP)
101 command 'create' or 'set'
102
103Commands:
104 create Create new MPPS instance with IN PROGRESS status
105 set Update existing MPPS instance to COMPLETED or DISCONTINUED
106
107Create Options (N-CREATE):
108 --patient-name <name> Patient name (format: LAST^FIRST)
109 --patient-id <id> Patient ID (required)
110 --modality <mod> Modality code (CT, MR, US, XR, etc.) [default: CT]
111 --procedure-id <id> Performed Procedure Step ID
112 --study-uid <uid> Study Instance UID (auto-generated if not provided)
113
114Set Options (N-SET):
115 --mpps-uid <uid> MPPS SOP Instance UID (required)
116 --status <status> New status: COMPLETED or DISCONTINUED [default: COMPLETED]
117 --reason <text> Discontinuation reason (for DISCONTINUED status)
118 --series-uid <uid> Performed Series Instance UID
119
120General Options:
121 --calling-ae <ae> Calling AE Title [default: MPPS_SCU]
122 --verbose, -v Show detailed progress
123 --help, -h Show this help message
124
125Examples:
126 # Start a new CT procedure
127 )" << program_name << R"( localhost 11112 RIS_SCP create \
128 --patient-id "12345" \
129 --patient-name "Doe^John" \
130 --modality CT
131
132 # Complete the procedure
133 )" << program_name << R"( localhost 11112 RIS_SCP set \
134 --mpps-uid "1.2.3.4.5.6.7.8" \
135 --status COMPLETED \
136 --series-uid "1.2.3.4.5.6.7.8.9"
137
138 # Discontinue (cancel) the procedure
139 )" << program_name << R"( localhost 11112 RIS_SCP set \
140 --mpps-uid "1.2.3.4.5.6.7.8" \
141 --status DISCONTINUED \
142 --reason "Patient refused"
143
144Exit Codes:
145 0 Success
146 1 MPPS operation failed
147 2 Connection or argument error
148)";
149}
150
154bool parse_arguments(int argc, char* argv[], options& opts) {
155 if (argc < 5) {
156 return false;
157 }
158
159 opts.host = argv[1];
160
161 // Parse port
162 try {
163 int port_int = std::stoi(argv[2]);
164 if (port_int < 1 || port_int > 65535) {
165 std::cerr << "Error: Port must be between 1 and 65535\n";
166 return false;
167 }
168 opts.port = static_cast<uint16_t>(port_int);
169 } catch (const std::exception&) {
170 std::cerr << "Error: Invalid port number '" << argv[2] << "'\n";
171 return false;
172 }
173
174 opts.called_ae = argv[3];
175 if (opts.called_ae.length() > 16) {
176 std::cerr << "Error: Called AE title exceeds 16 characters\n";
177 return false;
178 }
179
180 // Parse command
181 std::string cmd = argv[4];
182 if (cmd == "create") {
183 opts.command = mpps_command::create;
184 } else if (cmd == "set") {
185 opts.command = mpps_command::set;
186 } else if (cmd == "--help" || cmd == "-h") {
187 return false;
188 } else {
189 std::cerr << "Error: Unknown command '" << cmd << "'. Use 'create' or 'set'\n";
190 return false;
191 }
192
193 // Parse optional arguments
194 for (int i = 5; i < argc; ++i) {
195 std::string arg = argv[i];
196
197 if (arg == "--help" || arg == "-h") {
198 return false;
199 }
200 if (arg == "--verbose" || arg == "-v") {
201 opts.verbose = true;
202 } else if (arg == "--calling-ae" && i + 1 < argc) {
203 opts.calling_ae = argv[++i];
204 if (opts.calling_ae.length() > 16) {
205 std::cerr << "Error: Calling AE title exceeds 16 characters\n";
206 return false;
207 }
208 }
209 // N-CREATE options
210 else if (arg == "--patient-name" && i + 1 < argc) {
211 opts.patient_name = argv[++i];
212 } else if (arg == "--patient-id" && i + 1 < argc) {
213 opts.patient_id = argv[++i];
214 } else if (arg == "--modality" && i + 1 < argc) {
215 opts.modality = argv[++i];
216 } else if (arg == "--procedure-id" && i + 1 < argc) {
217 opts.procedure_id = argv[++i];
218 } else if (arg == "--study-uid" && i + 1 < argc) {
219 opts.study_uid = argv[++i];
220 }
221 // N-SET options
222 else if (arg == "--mpps-uid" && i + 1 < argc) {
223 opts.mpps_uid = argv[++i];
224 } else if (arg == "--status" && i + 1 < argc) {
225 std::string status_str = argv[++i];
226 if (status_str == "COMPLETED") {
227 opts.status = cli_status_type::completed;
228 } else if (status_str == "DISCONTINUED") {
229 opts.status = cli_status_type::discontinued;
230 } else {
231 std::cerr << "Error: Invalid status '" << status_str
232 << "'. Use COMPLETED or DISCONTINUED\n";
233 return false;
234 }
235 } else if (arg == "--reason" && i + 1 < argc) {
236 opts.discontinuation_reason = argv[++i];
237 } else if (arg == "--series-uid" && i + 1 < argc) {
238 opts.series_uid = argv[++i];
239 } else {
240 std::cerr << "Error: Unknown option '" << arg << "'\n";
241 return false;
242 }
243 }
244
245 // Validate required options
246 if (opts.command == mpps_command::create) {
247 if (opts.patient_id.empty()) {
248 std::cerr << "Error: --patient-id is required for 'create' command\n";
249 return false;
250 }
251 } else if (opts.command == mpps_command::set) {
252 if (opts.mpps_uid.empty()) {
253 std::cerr << "Error: --mpps-uid is required for 'set' command\n";
254 return false;
255 }
256 }
257
258 return true;
259}
260
264int perform_mpps_create(const options& opts) {
265 using namespace kcenon::pacs::network;
266 using namespace kcenon::pacs::services;
267
268 if (opts.verbose) {
269 std::cout << "=== MPPS N-CREATE (Start Procedure) ===\n";
270 std::cout << "Connecting to " << opts.host << ":" << opts.port << "...\n";
271 std::cout << " Calling AE: " << opts.calling_ae << "\n";
272 std::cout << " Called AE: " << opts.called_ae << "\n";
273 std::cout << " Patient ID: " << opts.patient_id << "\n";
274 std::cout << " Modality: " << opts.modality << "\n\n";
275 }
276
277 // Configure association
278 association_config config;
279 config.calling_ae_title = opts.calling_ae;
280 config.called_ae_title = opts.called_ae;
281 config.implementation_class_uid = "1.2.826.0.1.3680043.2.1545.1";
282 config.implementation_version_name = "MPPS_SCU_001";
283
284 // Propose MPPS SOP Class
285 config.proposed_contexts.push_back({
286 1, // Context ID
287 std::string(mpps_sop_class_uid),
288 {
289 "1.2.840.10008.1.2.1", // Explicit VR Little Endian
290 "1.2.840.10008.1.2" // Implicit VR Little Endian
291 }
292 });
293
294 // Establish association
295 auto start_time = std::chrono::steady_clock::now();
296 auto connect_result = association::connect(opts.host, opts.port, config, default_timeout);
297
298 if (connect_result.is_err()) {
299 std::cerr << "Failed to establish association: "
300 << connect_result.error().message << "\n";
301 return 2;
302 }
303
304 auto& assoc = connect_result.value();
305
306 if (opts.verbose) {
307 auto connect_time = std::chrono::steady_clock::now();
308 auto connect_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
309 connect_time - start_time);
310 std::cout << "Association established in " << connect_duration.count() << " ms\n";
311 }
312
313 // Check if context was accepted
314 if (!assoc.has_accepted_context(mpps_sop_class_uid)) {
315 std::cerr << "Error: MPPS SOP Class not accepted by remote SCP\n";
316 assoc.abort();
317 return 2;
318 }
319
320 // Create MPPS SCU instance and prepare data
321 mpps_scu scu;
322
323 mpps_create_data create_data;
324 create_data.patient_name = opts.patient_name;
325 create_data.patient_id = opts.patient_id;
326 create_data.modality = opts.modality;
327 create_data.station_ae_title = opts.calling_ae;
328 create_data.scheduled_procedure_step_id = opts.procedure_id;
329 create_data.study_instance_uid = opts.study_uid;
330
331 if (opts.verbose) {
332 std::cout << "Sending N-CREATE request...\n";
333 }
334
335 // Perform N-CREATE
336 auto create_result = scu.create(assoc, create_data);
337
338 if (create_result.is_err()) {
339 std::cerr << "N-CREATE failed: " << create_result.error().message << "\n";
340 assoc.abort();
341 return 1;
342 }
343
344 const auto& result = create_result.value();
345
346 if (!result.is_success()) {
347 std::cerr << "N-CREATE returned error status: 0x"
348 << std::hex << result.status << std::dec << "\n";
349 if (!result.error_comment.empty()) {
350 std::cerr << " Error comment: " << result.error_comment << "\n";
351 }
352 (void)assoc.release(default_timeout);
353 return 1;
354 }
355
356 // Release association
357 if (opts.verbose) {
358 std::cout << "Releasing association...\n";
359 }
360 auto release_result = assoc.release(default_timeout);
361 if (release_result.is_err() && opts.verbose) {
362 std::cerr << "Warning: Release failed: " << release_result.error().message << "\n";
363 }
364
365 auto end_time = std::chrono::steady_clock::now();
366 auto total_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
367 end_time - start_time);
368
369 // Success output
370 std::cout << "\n";
371 std::cout << "========================================\n";
372 std::cout << " MPPS Created Successfully\n";
373 std::cout << "========================================\n";
374 std::cout << " MPPS UID: " << result.mpps_sop_instance_uid << "\n";
375 std::cout << " Status: IN PROGRESS\n";
376 std::cout << " Patient ID: " << opts.patient_id << "\n";
377 std::cout << " Modality: " << opts.modality << "\n";
378 std::cout << " Total time: " << total_duration.count() << " ms\n";
379 std::cout << "========================================\n";
380 std::cout << "\nUse this MPPS UID to update the procedure:\n";
381 std::cout << " " << opts.calling_ae << " " << opts.host << " " << opts.port
382 << " " << opts.called_ae << " set \\\n";
383 std::cout << " --mpps-uid \"" << result.mpps_sop_instance_uid << "\" \\\n";
384 std::cout << " --status COMPLETED\n";
385
386 return 0;
387}
388
392int perform_mpps_set(const options& opts) {
393 using namespace kcenon::pacs::network;
394 using namespace kcenon::pacs::services;
395
396 const char* status_str = (opts.status == cli_status_type::completed)
397 ? "COMPLETED" : "DISCONTINUED";
398
399 if (opts.verbose) {
400 std::cout << "=== MPPS N-SET (Update Status to " << status_str << ") ===\n";
401 std::cout << "Connecting to " << opts.host << ":" << opts.port << "...\n";
402 std::cout << " Calling AE: " << opts.calling_ae << "\n";
403 std::cout << " Called AE: " << opts.called_ae << "\n";
404 std::cout << " MPPS UID: " << opts.mpps_uid << "\n";
405 std::cout << " New Status: " << status_str << "\n\n";
406 }
407
408 // Configure association
409 association_config config;
410 config.calling_ae_title = opts.calling_ae;
411 config.called_ae_title = opts.called_ae;
412 config.implementation_class_uid = "1.2.826.0.1.3680043.2.1545.1";
413 config.implementation_version_name = "MPPS_SCU_001";
414
415 // Propose MPPS SOP Class
416 config.proposed_contexts.push_back({
417 1,
418 std::string(mpps_sop_class_uid),
419 {
420 "1.2.840.10008.1.2.1",
421 "1.2.840.10008.1.2"
422 }
423 });
424
425 // Establish association
426 auto start_time = std::chrono::steady_clock::now();
427 auto connect_result = association::connect(opts.host, opts.port, config, default_timeout);
428
429 if (connect_result.is_err()) {
430 std::cerr << "Failed to establish association: "
431 << connect_result.error().message << "\n";
432 return 2;
433 }
434
435 auto& assoc = connect_result.value();
436
437 if (opts.verbose) {
438 auto connect_time = std::chrono::steady_clock::now();
439 auto connect_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
440 connect_time - start_time);
441 std::cout << "Association established in " << connect_duration.count() << " ms\n";
442 }
443
444 // Check if context was accepted
445 if (!assoc.has_accepted_context(mpps_sop_class_uid)) {
446 std::cerr << "Error: MPPS SOP Class not accepted by remote SCP\n";
447 assoc.abort();
448 return 2;
449 }
450
451 // Create MPPS SCU instance
452 mpps_scu scu;
453
454 if (opts.verbose) {
455 std::cout << "Sending N-SET request...\n";
456 }
457
458 // Perform N-SET using convenience methods
459 auto set_result = [&]() {
460 if (opts.status == cli_status_type::completed) {
461 // Build performed series info if provided
462 std::vector<performed_series_info> performed_series;
463 if (!opts.series_uid.empty()) {
465 series.series_uid = opts.series_uid;
466 series.modality = opts.modality;
467 performed_series.push_back(series);
468 }
469 return scu.complete(assoc, opts.mpps_uid, performed_series);
470 } else {
471 return scu.discontinue(assoc, opts.mpps_uid, opts.discontinuation_reason);
472 }
473 }();
474
475 if (set_result.is_err()) {
476 std::cerr << "N-SET failed: " << set_result.error().message << "\n";
477 assoc.abort();
478 return 1;
479 }
480
481 const auto& result = set_result.value();
482
483 if (!result.is_success()) {
484 std::cerr << "N-SET returned error status: 0x"
485 << std::hex << result.status << std::dec << "\n";
486
487 // Common error: trying to modify completed/discontinued MPPS
488 if (result.status == 0xC310) {
489 std::cerr << " Note: Cannot modify MPPS that is already COMPLETED or DISCONTINUED\n";
490 }
491 if (!result.error_comment.empty()) {
492 std::cerr << " Error comment: " << result.error_comment << "\n";
493 }
494
495 (void)assoc.release(default_timeout);
496 return 1;
497 }
498
499 // Release association
500 if (opts.verbose) {
501 std::cout << "Releasing association...\n";
502 }
503 auto release_result = assoc.release(default_timeout);
504 if (release_result.is_err() && opts.verbose) {
505 std::cerr << "Warning: Release failed: " << release_result.error().message << "\n";
506 }
507
508 auto end_time = std::chrono::steady_clock::now();
509 auto total_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
510 end_time - start_time);
511
512 // Success output
513 std::cout << "\n";
514 std::cout << "========================================\n";
515 std::cout << " MPPS Updated Successfully\n";
516 std::cout << "========================================\n";
517 std::cout << " MPPS UID: " << opts.mpps_uid << "\n";
518 std::cout << " New Status: " << status_str << "\n";
519 std::cout << " Total time: " << total_duration.count() << " ms\n";
520 std::cout << "========================================\n";
521
522 return 0;
523}
524
525} // namespace
526
527int main(int argc, char* argv[]) {
528 std::cout << R"(
529 __ __ ____ ____ ____ ____ ____ _ _
530 | \/ | _ \| _ \/ ___| / ___| / ___| | | |
531 | |\/| | |_) | |_) \___ \ \___ \| | | | | |
532 | | | | __/| __/ ___) | ___) | |___| |_| |
533 |_| |_|_| |_| |____/ |____/ \____|\___/
534
535 Modality Performed Procedure Step Client
536)" << "\n";
537
538 options opts;
539
540 if (!parse_arguments(argc, argv, opts)) {
541 print_usage(argv[0]);
542 return 2;
543 }
544
545 // Execute requested command
546 if (opts.command == mpps_command::create) {
547 return perform_mpps_create(opts);
548 } else {
549 return perform_mpps_set(opts);
550 }
551}
DICOM Association management per PS3.8.
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
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
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
int main()
Definition main.cpp:84
DICOM MPPS (Modality Performed Procedure Step) SCU service.
@ completed
Job completed successfully.
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag status
Status.
constexpr dicom_tag modality
Modality.
constexpr dicom_tag patient_name
Patient's Name.
std::chrono::milliseconds default_timeout()
Default timeout for test operations (5s normal, 30s CI)
@ discontinued
Procedure was stopped/cancelled.
Configuration for SCU association request.
std::string called_ae_title
Remote AE Title (16 chars max)
std::string calling_ae_title
Our AE Title (16 chars max)
std::vector< proposed_presentation_context > proposed_contexts
Data for N-CREATE operation (start procedure)
Definition mpps_scu.h:75
Information about a performed series for N-SET COMPLETED.
Definition mpps_scu.h:46