1use dicom_core::{dicom_value, DataElement, VR};
3use dicom_dictionary_std::{
4 tags,
5 uids::{self, VERIFICATION},
6};
7use dicom_object::{mem::InMemDicomObject, StandardDataDictionary};
8use dicom_transfer_syntax_registry::entries::IMPLICIT_VR_LITTLE_ENDIAN;
9use dicom_ul::{
10 association::ClientAssociationOptions,
11 pdu::{PDataValue, PDataValueType, Pdu},
12};
13use pyo3::prelude::*;
14
15use client_exceptions::Result;
16
17pub const DEFAULT_CALLED_AE_TITLE: &str = "ANY-SCP";
19
20pub const DEFAULT_CALLING_AE_TITLE: &str = "ECHOSCU";
22
23fn create_echo_command(message_id: u16) -> InMemDicomObject<StandardDataDictionary> {
25 InMemDicomObject::command_from_element_iter([
26 DataElement::new(tags::AFFECTED_SOP_CLASS_UID, VR::UI, uids::VERIFICATION),
28 DataElement::new(tags::COMMAND_FIELD, VR::US, dicom_value!(U16, [0x0030])),
30 DataElement::new(tags::MESSAGE_ID, VR::US, dicom_value!(U16, [message_id])),
32 DataElement::new(
34 tags::COMMAND_DATA_SET_TYPE,
35 VR::US,
36 dicom_value!(U16, [0x0101]),
37 ),
38 ])
39}
40
41#[pyfunction]
45#[pyo3(
46 signature = (
47 address, /,
48 called_ae_title=DEFAULT_CALLED_AE_TITLE.into(), calling_ae_title=DEFAULT_CALLING_AE_TITLE.into(),
49 message_id=1
50 ),
51 text_signature = "(address: str, /, called_ae_title: str = DEFAULT_CALLED_AE_TITLE, calling_ae_title: str = DEFAULT_CALLING_AE_TITLE, message_id: int = 1) -> int"
52)]
53pub fn send(
54 address: &str,
55 called_ae_title: &str,
56 calling_ae_title: &str,
57 message_id: u16,
58) -> Result<u16> {
59 let mut association = ClientAssociationOptions::new()
60 .with_abstract_syntax(VERIFICATION)
61 .calling_ae_title(calling_ae_title)
62 .called_ae_title(called_ae_title)
63 .establish_with(address)?;
64
65 let presentation_context = association.presentation_contexts().first().unwrap();
66 let dicom_object = create_echo_command(message_id);
67
68 let mut data = Vec::new();
69 let transfer_syntax = IMPLICIT_VR_LITTLE_ENDIAN.erased();
70
71 dicom_object
72 .write_dataset_with_ts(&mut data, &transfer_syntax)
73 .expect("in-memory dicom object should be serialized to byte vector");
74
75 association.send(&Pdu::PData {
76 data: vec![PDataValue {
77 presentation_context_id: presentation_context.id,
78 value_type: PDataValueType::Command,
79 is_last: true,
80 data,
81 }],
82 })?;
83
84 let pdu = association.receive()?;
85
86 match pdu {
87 Pdu::PData { data } => {
88 let data_value = &data[0];
89 let v = &data_value.data;
90 let obj = InMemDicomObject::read_dataset_with_ts(v.as_slice(), &transfer_syntax)
91 .expect("should be able to read the response dataset returned by the SCP");
92
93 let status = obj
94 .element(tags::STATUS)
95 .expect("response should include the status tag")
96 .to_int::<u16>()
97 .expect("status tag should be decoded to a u16");
98 Ok(status)
99 }
100 _ => {
101 panic!("unexpected response from SCP");
102 }
103 }
104}
105
106#[pymodule]
107fn backend(m: &Bound<'_, PyModule>) -> PyResult<()> {
108 m.add_function(wrap_pyfunction!(send, m)?)?;
109 m.add("DEFAULT_CALLED_AE_TITLE", DEFAULT_CALLED_AE_TITLE)?;
110 m.add("DEFAULT_CALLING_AE_TITLE", DEFAULT_CALLING_AE_TITLE)?;
111 Ok(())
112}
113
114pub mod client_exceptions;