37constexpr const char* default_calling_ae =
"MPPS_SCU";
45enum class mpps_command {
53enum class cli_status_type {
65 std::string called_ae;
66 std::string calling_ae{default_calling_ae};
69 mpps_command command{mpps_command::create};
75 std::string procedure_id;
76 std::string study_uid;
80 cli_status_type
status{cli_status_type::completed};
81 std::string discontinuation_reason;
82 std::string series_uid;
91void print_usage(
const char* program_name) {
93MPPS SCU - Modality Performed Procedure Step Client
95Usage: )" << program_name << R"( <host> <port> <called_ae> <command> [options]
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'
104 create Create new MPPS instance with IN PROGRESS status
105 set Update existing MPPS instance to COMPLETED or DISCONTINUED
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)
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
121 --calling-ae <ae> Calling AE Title [default: MPPS_SCU]
122 --verbose, -v Show detailed progress
123 --help, -h Show this help message
126 # Start a new CT procedure
127 )" << program_name << R"( localhost 11112 RIS_SCP create \
128 --patient-id "12345" \
129 --patient-name "Doe^John" \
132 # Complete the procedure
133 )" << program_name << R"( localhost 11112 RIS_SCP set \
134 --mpps-uid "1.2.3.4.5.6.7.8" \
136 --series-uid "1.2.3.4.5.6.7.8.9"
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"
146 1 MPPS operation failed
147 2 Connection or argument error
154bool parse_arguments(
int argc,
char* argv[], options& opts) {
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";
168 opts.port =
static_cast<uint16_t
>(port_int);
169 }
catch (
const std::exception&) {
170 std::cerr <<
"Error: Invalid port number '" << argv[2] <<
"'\n";
174 opts.called_ae = argv[3];
175 if (opts.called_ae.length() > 16) {
176 std::cerr <<
"Error: Called AE title exceeds 16 characters\n";
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") {
189 std::cerr <<
"Error: Unknown command '" << cmd <<
"'. Use 'create' or 'set'\n";
194 for (
int i = 5; i < argc; ++i) {
195 std::string arg = argv[i];
197 if (arg ==
"--help" || arg ==
"-h") {
200 if (arg ==
"--verbose" || arg ==
"-v") {
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";
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];
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;
231 std::cerr <<
"Error: Invalid status '" << status_str
232 <<
"'. Use COMPLETED or DISCONTINUED\n";
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];
240 std::cerr <<
"Error: Unknown option '" << arg <<
"'\n";
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";
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";
264int perform_mpps_create(
const options& opts) {
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";
287 std::string(mpps_sop_class_uid),
289 "1.2.840.10008.1.2.1",
295 auto start_time = std::chrono::steady_clock::now();
296 auto connect_result = association::connect(opts.host, opts.port, config, default_timeout);
298 if (connect_result.is_err()) {
299 std::cerr <<
"Failed to establish association: "
300 << connect_result.error().message <<
"\n";
304 auto& assoc = connect_result.value();
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";
314 if (!assoc.has_accepted_context(mpps_sop_class_uid)) {
315 std::cerr <<
"Error: MPPS SOP Class not accepted by remote SCP\n";
326 create_data.
modality = opts.modality;
332 std::cout <<
"Sending N-CREATE request...\n";
336 auto create_result = scu.
create(assoc, create_data);
338 if (create_result.is_err()) {
339 std::cerr <<
"N-CREATE failed: " << create_result.error().message <<
"\n";
344 const auto& result = create_result.value();
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";
352 (void)assoc.release(default_timeout);
358 std::cout <<
"Releasing association...\n";
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";
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);
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";
392int perform_mpps_set(
const options& opts) {
396 const char* status_str = (opts.status == cli_status_type::completed)
397 ?
"COMPLETED" :
"DISCONTINUED";
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";
418 std::string(mpps_sop_class_uid),
420 "1.2.840.10008.1.2.1",
426 auto start_time = std::chrono::steady_clock::now();
427 auto connect_result = association::connect(opts.host, opts.port, config, default_timeout);
429 if (connect_result.is_err()) {
430 std::cerr <<
"Failed to establish association: "
431 << connect_result.error().message <<
"\n";
435 auto& assoc = connect_result.value();
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";
445 if (!assoc.has_accepted_context(mpps_sop_class_uid)) {
446 std::cerr <<
"Error: MPPS SOP Class not accepted by remote SCP\n";
455 std::cout <<
"Sending N-SET request...\n";
459 auto set_result = [&]() {
460 if (opts.status == cli_status_type::completed) {
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);
469 return scu.
complete(assoc, opts.mpps_uid, performed_series);
471 return scu.
discontinue(assoc, opts.mpps_uid, opts.discontinuation_reason);
475 if (set_result.is_err()) {
476 std::cerr <<
"N-SET failed: " << set_result.error().message <<
"\n";
481 const auto& result = set_result.value();
483 if (!result.is_success()) {
484 std::cerr <<
"N-SET returned error status: 0x"
485 << std::hex << result.status << std::dec <<
"\n";
488 if (result.status == 0xC310) {
489 std::cerr <<
" Note: Cannot modify MPPS that is already COMPLETED or DISCONTINUED\n";
491 if (!result.error_comment.empty()) {
492 std::cerr <<
" Error comment: " << result.error_comment <<
"\n";
495 (void)assoc.release(default_timeout);
501 std::cout <<
"Releasing association...\n";
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";
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);
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";
527int main(
int argc,
char* argv[]) {
529 __ __ ____ ____ ____ ____ ____ _ _
530 | \/ | _ \| _ \/ ___| / ___| / ___| | | |
531 | |\/| | |_) | |_) \___ \ \___ \| | | | | |
532 | | | | __/| __/ ___) | ___) | |___| |_| |
533 |_| |_|_| |_| |____/ |____/ \____|\___/
535 Modality Performed Procedure Step Client
540 if (!parse_arguments(argc, argv, opts)) {
541 print_usage(argv[0]);
546 if (opts.command == mpps_command::create) {
547 return perform_mpps_create(opts);
549 return perform_mpps_set(opts);
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)
network::Result< mpps_result > discontinue(network::association &assoc, std::string_view mpps_uid, std::string_view reason="")
Discontinue an MPPS instance (convenience method)
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)
DICOM MPPS (Modality Performed Procedure Step) SCU service.
@ completed
Job completed successfully.
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::string implementation_class_uid
std::string implementation_version_name
std::vector< proposed_presentation_context > proposed_contexts
Data for N-CREATE operation (start procedure)
std::string scheduled_procedure_step_id
std::string station_ae_title
std::string study_instance_uid