backend/
lib.rs

1//! Implement the `dicom_echo.backend` module in Rust.
2use 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
17/// By default, specify this AE title for the target SCP.
18pub const DEFAULT_CALLED_AE_TITLE: &str = "ANY-SCP";
19
20/// By default, specify this AE title for the SCU sending the `C-ECHO` request.
21pub const DEFAULT_CALLING_AE_TITLE: &str = "ECHOSCU";
22
23/// ref: <https://github.com/Enet4/dicom-rs/blob/de7dc5831171202fb20e1928e2c7ff27c5b95f85/echoscu/src/main.rs#L177-L192>
24fn create_echo_command(message_id: u16) -> InMemDicomObject<StandardDataDictionary> {
25    InMemDicomObject::command_from_element_iter([
26        // service
27        DataElement::new(tags::AFFECTED_SOP_CLASS_UID, VR::UI, uids::VERIFICATION),
28        // command
29        DataElement::new(tags::COMMAND_FIELD, VR::US, dicom_value!(U16, [0x0030])),
30        // message ID
31        DataElement::new(tags::MESSAGE_ID, VR::US, dicom_value!(U16, [message_id])),
32        // data set type
33        DataElement::new(
34            tags::COMMAND_DATA_SET_TYPE,
35            VR::US,
36            dicom_value!(U16, [0x0101]),
37        ),
38    ])
39}
40
41/// Send a `C-ECHO` message to the given address.
42///
43/// Reference: [DICOM Standard Part 7, Section 9.1.5](https://www.dicomstandard.org/standards/view/message-exchange#sect_9.1.5)
44#[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;