Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MoveSCU implementation #108

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions movescu/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "dicom-movescu"
version = "0.1.0"
authors = ["Eduardo Pinho <enet4mikeenet@gmail.com>, José Moreira <joseppmoreira@ua.pt>"]
edition = "2018"
license = "MIT OR Apache-2.0"
repository = "https://github.com/Enet4/dicom-rs"
description = "A DICOM C-MOVE command line interface"
categories = ["command-line-utilities"]
keywords = ["dicom"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
dicom-ul = { path = '../ul' }
dicom = { path = '../parent' }
structopt = "0.3.21"
302 changes: 302 additions & 0 deletions movescu/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
use dicom::core::dicom_value;
use dicom::core::smallvec;
use dicom::encoding::transfer_syntax::TransferSyntaxIndex;
use dicom::object::open_file;
use dicom::transfer_syntax::TransferSyntaxRegistry;
use dicom::{
core::{DataElement, Tag, VR},
object::{mem::InMemDicomObject, StandardDataDictionary},
};
use dicom_ul::pdu::Pdu;
use dicom_ul::{
association::ClientAssociationOptions,
pdu::{PDataValue, PDataValueType},
};
use std::io::Write;
use std::path::PathBuf;
use structopt::StructOpt;

/// DICOM C-MOVE SCU
#[derive(Debug, StructOpt)]
struct App {
/// socket address to MOVE SCP (example: "127.0.0.1:104")
addr: String,
/// verbose mode
#[structopt(short = "v")]
verbose: bool,
/// the DICOM file to store
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This made sense in the store SCU, but not anymore in the move SCU, since the application might not have the objects it wants to move in the first place. Instead, we can follow the approach done by other tools (e.g. DCMTK's movescu), and declare that the input file represents a query object.

Suggested change
/// the DICOM file to store
/// the DICOM query file

file: PathBuf,
/// the C-MOVE destination
#[structopt(short = "mo", long = "move-destination", default_value = "")]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Short arguments longer than 1 character are not supported by clap.

In addition, I would say that empty string as the default value does not make much sense. The user has to provide a move destination, otherwise it is unclear where it should go.

Suggested change
#[structopt(short = "mo", long = "move-destination", default_value = "")]
#[structopt(long = "move-destination")]

move_destination: String,
/// the C-MOVE message ID
#[structopt(short = "m", long = "message-id", default_value = "1")]
message_id: u16,
/// the calling AE title
#[structopt(long = "calling-ae-title", default_value = "MOVESCU")]
calling_ae_title: String,
/// the called AE title
#[structopt(long = "called-ae-title", default_value = "ANY-SCP")]
called_ae_title: String,

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// the maximum PDU length

#[structopt(long = "max-pdu-length", default_value = "16384")]
max_pdu_length: u32,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
let App {
addr,
verbose,
file,
message_id,
move_destination,
calling_ae_title,
called_ae_title,
max_pdu_length,
} = App::from_args();

if verbose {
println!("Establishing association with '{}'...", &addr);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

println! and other macros already borrow arguments implicitly.

Suggested change
println!("Establishing association with '{}'...", &addr);
println!("Establishing association with '{}'...", addr);

}

let dicom_file = open_file(file)?;
let meta = dicom_file.meta();

let affected_sop_class_uid = "1.2.840.10008.5.1.4.1.2.2.2\u{0}";
let sop_instance_uid = &meta.media_storage_sop_instance_uid;
let transfer_syntax = "1.2.840.10008.1.2";

let retrieve_level = "STUDY ";
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had overlooked this until I actually tried to run the application. The retrieval level should be fetched from the query file (QueryRetrieveLevel (0008,0052)), and only the attributes listed therein should be considered. The study instance UID or the series instance UID might not even be present.

let study_instance_uid = dicom_file
.element_by_name("StudyInstanceUID")?
.to_clean_str()?;
let series_instance_uid = dicom_file
.element_by_name("SeriesInstanceUID")?
.to_clean_str()?;

let mut scu = ClientAssociationOptions::new()
.with_abstract_syntax(affected_sop_class_uid)
.with_transfer_syntax(transfer_syntax)
.calling_ae_title(calling_ae_title)
.called_ae_title(called_ae_title)
.max_pdu_length(max_pdu_length)
.establish(addr)?;

if verbose {
println!("Association established");
}

let ts = TransferSyntaxRegistry
.get(&transfer_syntax)
.expect("Poorly negotiated transfer syntax");

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As of the established version of the association API, the client association no longer provides presentation_context_id, because it keeps all accepted presentation contexts.

One would need to pick one of the accepted presentation contexts and use the id of that one in subsequent interactions.

Suggested change
let pc_selected = if let Some(pc) = scu
.presentation_contexts()
.iter()
.find(|pc| transfer_syntax == &pc.transfer_syntax)
{
pc.clone()
} else {
eprintln!("Could not choose a transfer syntax");
let _ = scu.abort();
std::process::exit(-2);
};

if verbose {
println!("Transfer Syntax: {}", ts.name());
};

let cmd = move_req_command(&affected_sop_class_uid, message_id, &move_destination);

let mut cmd_data = Vec::with_capacity(128);
cmd.write_dataset_with_ts(
&mut cmd_data,
&dicom::transfer_syntax::entries::IMPLICIT_VR_LITTLE_ENDIAN.erased(),
)?;

let obj = create_iod(
&sop_instance_uid,
&retrieve_level,
&study_instance_uid,
&series_instance_uid,
);

let mut object_data = Vec::with_capacity(128);
obj.write_dataset_with_ts(
&mut object_data,
&dicom::transfer_syntax::entries::IMPLICIT_VR_LITTLE_ENDIAN.erased(),
)?;

let nbytes = cmd_data.len() + object_data.len();

if verbose {
println!("Sending payload (~ {} Kb)...", nbytes / 1024);
}

if nbytes < max_pdu_length as usize - 100 {
let pdu = Pdu::PData {
data: vec![
PDataValue {
presentation_context_id: scu.presentation_context_id(),
value_type: PDataValueType::Command,
is_last: true,
data: cmd_data,
},
PDataValue {
presentation_context_id: scu.presentation_context_id(),
value_type: PDataValueType::Data,
is_last: true,
data: object_data,
},
],
};

scu.send(&pdu)?;
} else {
let pdu = Pdu::PData {
data: vec![PDataValue {
presentation_context_id: scu.presentation_context_id(),
value_type: PDataValueType::Command,
is_last: true,
data: cmd_data,
}],
};

scu.send(&pdu)?;

scu.send_pdata(scu.presentation_context_id())
.write_all(&object_data)?;
}

if verbose {
println!("Awaiting response...");
}

let rsp_pdu = scu.receive()?;

match rsp_pdu {
Pdu::PData { data } => {
let data_value = &data[0];

let cmd_obj = InMemDicomObject::read_dataset_with_ts(
&data_value.data[..],
&dicom::transfer_syntax::entries::IMPLICIT_VR_LITTLE_ENDIAN.erased(),
)?;

if verbose {
println!("Response: {:?}", cmd_obj);
}

let status = cmd_obj.element(Tag(0x0000, 0x0900))?.to_int::<u16>()?;
if status == 0 {
println!("Sucessfully moved instance '{}'", sop_instance_uid);
} else {
println!(
"Failed to move instance '{}' (status code {})",
sop_instance_uid, status
);
}

scu.release()?;
}

pdu @ Pdu::Unknown { .. }
| pdu @ Pdu::AssociationRQ { .. }
| pdu @ Pdu::AssociationAC { .. }
| pdu @ Pdu::AssociationRJ { .. }
| pdu @ Pdu::ReleaseRQ
| pdu @ Pdu::ReleaseRP
| pdu @ Pdu::AbortRQ { .. } => {
eprintln!("Unexpected SCP response: {:?}", pdu);
std::process::exit(-2);
}
}

Ok(())
}

fn move_req_command(
affected_sop_class_uid: &str,
message_id: u16,
move_destination: &str,
) -> InMemDicomObject<StandardDataDictionary> {
let mut obj = InMemDicomObject::create_empty();

// SOP Class UID
obj.put(DataElement::new(
Tag(0x0000, 0x0000),
VR::UL,
dicom_value!(U32, [98]),
));

// SOP Class UID
obj.put(DataElement::new(
Tag(0x0000, 0x0002),
VR::UI,
dicom_value!(Strs, [affected_sop_class_uid]),
));

// command field
obj.put(DataElement::new(
Tag(0x0000, 0x0100),
VR::US,
dicom_value!(U16, [0x0021]),
));

// message ID
obj.put(DataElement::new(
Tag(0x0000, 0x0110),
VR::US,
dicom_value!(U16, [message_id]),
));

// move destination
obj.put(DataElement::new(
Tag(0x0000, 0x0600),
VR::AE,
dicom_value!(Strs, [move_destination]),
));

//priority
obj.put(DataElement::new(
Tag(0x0000, 0x0700),
VR::US,
dicom_value!(U16, [0x0000]),
));

// data set type
obj.put(DataElement::new(
Tag(0x0000, 0x0800),
VR::US,
dicom_value!(U16, [0x0000]),
));

obj
}

fn create_iod(
Copy link
Owner

@Enet4 Enet4 Mar 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once we're upfront that the input file is already a query file, there does not seem to be a reason for this function to exist other than to map one query object (from the file) into another query object. It should probably just be removed.

sop_instance_uid: &str,
retrieve_level: &str,
study_instance_uid: &str,
series_instance_uid: &str,
) -> InMemDicomObject<StandardDataDictionary> {
let mut obj = InMemDicomObject::create_empty();

// SOP Instance UID
obj.put(DataElement::new(
Tag(0x0008, 0x0018),
VR::UI,
dicom_value!(Strs, [sop_instance_uid]),
));

// retrieve level
obj.put(DataElement::new(
Tag(0x0008, 0x0052),
VR::CS,
dicom_value!(Strs, [retrieve_level]),
));

// study instance UID
obj.put(DataElement::new(
Tag(0x0020, 0x000D),
VR::UI,
dicom_value!(Strs, [study_instance_uid]),
));

// series instance UID
obj.put(DataElement::new(
Tag(0x0020, 0x000E),
VR::UI,
dicom_value!(Strs, [series_instance_uid]),
));

obj
}