Writing your first SCU

This tutorial is intended for people who are new to pynetdicom. In it you’ll:

  • Learn a bit about the basics of DICOM networking

  • Learn how to use the echoscp application that comes with pynetdicom

  • Create an new application entity (AE) and associate with a DICOM peer

  • Learn how to perform some basic troubleshooting of associations

  • Modify your AE to be an Echo SCU

The tutorial is written for pynetdicom 2.0 and higher, which supports Python 3.7+. You can tell which version of pynetdicom you have by running the following command:

$ python -m pynetdicom --version

If you get an error or if your version is earlier than 2.0, then install or upgrade pynetdicom by following the instructions in the installation guide. For this tutorial we’ll also be using the echoscp application that comes with pynetdicom.

DICOM networking

pynetdicom is an implementation of the DICOM Upper Layer Protocol for TCP/IP, which is used to facilitate communication between DICOM Application Entities (AEs) over a TCP connection. Communication between two AEs starts by establishing a TCP connection, then progresses to negotiating an association, which is the term used to describe the communications channel between the AEs and the set of rules that govern their expected behaviour.

The AE that initiated the connection, the requestor, sends an A-ASSOCIATE-RQ message proposing which DICOM services it would like to use. The receiving AE, the acceptor, goes through the proposal and either:

  • Accepts the association and replies with an A-ASSOCIATE-AC message. However, just because an association has been accepted doesn’t mean that the proposed services have also been accepted.

  • Rejects the association by replying with an A-ASSOCIATE-RJ message.

  • Aborts the association negotiation by sending an A-ABORT message.

When the requestor receives the A-ASSOCIATE-AC message the negotiation phase ends and the association becomes established. The two AEs can then use the services that were agreed upon during negotiation by exchanging DIMSE-C and DIMSE-N messages.

When the association is no longer needed, it can be released by sending an A-RELEASE-RQ message. The association can be also be aborted at any time when either AE sends an A-ABORT message. Once the association has been aborted or released, the TCP connection is closed (if still open) and communication between the two AEs ends.

What is an SCU?

If you’re new to DICOM, you might be wondering what an Echo SCU or SCP actually are. The answer lies in the DICOM services that are available to an association; if an AE provides a service then it’s referred to as a Service Class Provider, or SCP, while if an AE uses a service then it’s a Service Class User, or SCU. A Verification SCU then, is an AE that uses the DICOM verification service, a Storage SCP is an AE that provides the DICOM storage service, and so on. Because the verification service is the sole user of the DIMSE C-ECHO messages, the labels “Verification SCU” and “Verification SCP” are frequently shortened to “Echo SCU” and “Echo SCP” instead.

The verification service itself is used to verify basic DICOM connectivity between two AEs; the Echo SCU sends a C-ECHO request to the SCP, which replies with an acknowledgement. By doing this you can test whether a DICOM application is active and reachable by your SCU, that your configuration is correct and that the connection isn’t being blocked by a firewall or anything else.

OK, enough about DICOM networking, let’s get started.

Start the Echo SCP

For this tutorial we need an Echo SCP. To make things simpler we’ll use the echoscp application that comes with pynetdicom, but you could also use any third-party application that supports the verification service as an SCP, such as DCMTK’s storescp. To find out if an application supports the verification service you should check their DICOM conformance statement.

In a new terminal, start echoscp listening for connection requests on port 11112 with the -v verbose flag (or you could use the -d debug flag for even more output):

$ python -m pynetdicom echoscp 11112 -v

If you get an error saying the address is already in use, try again with a different port - just remember to adapt the port used when making association requests accordingly. Depending on your OS or system configuration you may also need to allow access through the firewall for the port you end up using.

The SCP will continue to run until you interrupt it, either by closing the terminal or by pressing CTRL+C. Keep the application running in the background for the rest of the tutorial, but you may wish to look at its output every now and then to get a feel for what’s happening at the other end of the association.

Create an Application Entity and associate

Next we’re going to make a new application entity and request an association with the Echo SCP. Create a new file my_scu.py, open it in a text editor and add the following:

 1 from pynetdicom import AE
 2
 3 ae = AE()
 4 ae.add_requested_context("1.2.840.10008.1.1")
 5 assoc = ae.associate("127.0.0.1", 11112)
 6 if assoc.is_established:
 7     print("Association established with Echo SCP!")
 8     assoc.release()
 9 else:
10     # Association rejected, aborted or never connected
11     print("Failed to associate")

There’s a lot going on in these few lines, so let’s split it up a bit:

1 from pynetdicom import AE
2
3 ae = AE()
4 ae.add_requested_context("1.2.840.10008.1.1")

This imports the AE class, creates a new AE instance, ae, then adds a single presentation context with an abstract syntax of "1.2.840.10008.1.1" using the add_requested_context() method. All association requests must contain at least one presentation context, and in this case we’ve proposed one with the abstract syntax for the verification service. We’ll go into presentation contexts and how they’re used to define an association’s services a bit more later on.

5 assoc = ae.associate("127.0.0.1", 11112)

Here we initiate the association negotiation by connecting to the IP address "127.0.0.1" on port 11112 and sending an association request. "127.0.0.1" (also known as "localhost") is a special IP address that means this computer. This should be the same IP address and port that you started the echoscp application on earlier, so if you used a different port you should change this value accordingly.

Note

If the SCP isn’t running on your local computer, you call AE.associate() using the actual IP address and listen port of the SCP, for example ae.associate("148.60.155.4", 104).

The AE.associate() method returns an Association instance assoc, which is a subclass of threading.Thread. This allows us to make use of the association while pynetdicom monitors the connection behind the scenes.

 6 if assoc.is_established:
 7     print("Association established with Echo SCP!")
 8     assoc.release()
 9 else:
10     # Association rejected, aborted or never connected
11     print("Failed to associate")

As mentioned earlier, an acceptor may do a couple of things in response to an association request; accept it, reject it or abort the negotiation entirely. The request may also fail because there’s nothing there to associate with (a connection failure).

Because there’s more than one possible outcome to negotiation, we check to see if the association has been established using is_established. If it is, we print a message and send an association release request using release(). This ends the association and closes the connection with the Echo SCP. On the other hand, if we failed to establish an association for whatever reason, the connection is closed automatically (if required), and we don’t need to do anything further.

If you don’t release the association yourself then it’ll remain established until the connection is closed, usually when a timeout expires on either the requestor or acceptor AE.

So, let’s see what happens when we run our code. Open a new terminal and run the file:

$ python my_scu.py

If everything worked correctly, you should see:

Association established with Echo SCP

And if you take a look at the output for echoscp you should see it accept the association request and notify you of its release:

I: Accepting Association
I: Association Released

If instead you saw Failed to associate then not to worry; make sure the Echo SCP is running and your code is correct. If you still can’t associate, move on to the next section on troubleshooting associations.

Troubleshooting associations

By itself our output isn’t very helpful in understanding what’s going on. Fortunately pynetdicom has lots of logging output, but by default its configured to send it all to the NullHandler which prevents warnings and errors being printed to sys.stderr. This can be undone by importing logging and setting logging.getLogger("pynetdicom").handlers = [] or by adding your own logging handlers.

If you need to troubleshoot, then a quick way to send the debugging output to sys.stderr is by calling debug_logger():

1 from pynetdicom import AE, debug_logger
2
3 debug_logger()
4
5 ae = AE()
6 ae.add_requested_context("1.2.840.10008.1.1")
7 assoc = ae.associate("127.0.0.1", 11112)
8 if assoc.is_established:
9     assoc.release()

If you save the changes and run my_scu.py again you’ll see much more information:

$ python my_scu.py
I: Requesting Association
D: Request Parameters:
D: ======================= OUTGOING A-ASSOCIATE-RQ PDU ========================
D: Our Implementation Class UID:      1.2.826.0.1.3680043.9.3811.2.0.0
D: Our Implementation Version Name:   PYNETDICOM_200
D: Application Context Name:    1.2.840.10008.3.1.1.1
D: Calling Application Name:    PYNETDICOM
D: Called Application Name:     ANY-SCP
D: Our Max PDU Receive Size:    16382
D: Presentation Context:
D:   Context ID:        1 (Proposed)
D:     Abstract Syntax: =Verification SOP Class
D:     Proposed SCP/SCU Role: Default
D:     Proposed Transfer Syntaxes:
D:       =Implicit VR Little Endian
D:       =Explicit VR Little Endian
D:       =Explicit VR Big Endian
D: Requested Extended Negotiation: None
D: Requested Common Extended Negotiation: None
D: Requested Asynchronous Operations Window Negotiation: None
D: Requested User Identity Negotiation: None
D: ========================== END A-ASSOCIATE-RQ PDU ==========================
D: Accept Parameters:
D: ======================= INCOMING A-ASSOCIATE-AC PDU ========================
D: Their Implementation Class UID:    1.2.826.0.1.3680043.9.3811.2.0.0
D: Their Implementation Version Name: PYNETDICOM_200
D: Application Context Name:    1.2.840.10008.3.1.1.1
D: Calling Application Name:    PYNETDICOM
D: Called Application Name:     ANY-SCP
D: Their Max PDU Receive Size:  16382
D: Presentation Contexts:
D:   Context ID:        1 (Accepted)
D:     Abstract Syntax: =Verification SOP Class
D:     Accepted SCP/SCU Role: Default
D:     Accepted Transfer Syntax: =Explicit VR Little Endian
D: Accepted Extended Negotiation: None
D: Accepted Asynchronous Operations Window Negotiation: None
D: User Identity Negotiation Response: None
D: ========================== END A-ASSOCIATE-AC PDU ==========================
I: Association Accepted
I: Releasing Association

Here you can see the stages of the association:

  1. The association negotiation is initiated by sending an A-ASSOCIATE-RQ message

  2. An A-ASSOCIATE-AC message is received and the association accepted

  3. The association is released.

In general, the debugging log can be broken down into a couple of categories:

  • Information about the state of the association and services, usually prefixed by I:

  • Errors and exceptions that have occurred, prefixed by E:

  • The contents of various association related messages, such as the A-ASSOCIATE-RQ and A-ASSOCIATE-AC messages, as well as summaries of exchanged DIMSE messages (not shown here), usually prefixed by D:

The first step to troubleshooting an association should be to look at the debug output. Some common reasons for an association failure are:

  • TCP Initialisation Error: Connection refused indicates that nothing is listening on the IP address and port you specified. Check they’re correct, that the SCP is up and running, and that the firewall is allowing traffic through.

  • Called AE title not recognised: indicates that the SCP requires the A-ASSOCIATE-RQ’s Called AE Title value match its own. This can be set with the ae_title keyword parameter in associate()

  • Calling AE title not recognised: indicates that the SCP requires the A-ASSOCIATE-RQ’s Calling AE Title value match one its familiar with. This can be set with the AE.ae_title property. Alternatively, you may need to configure the SCP with the details of your SCU

  • Local limit exceeded: The SCP has too many current associations active, try again later

  • Association Aborted: this is more unusual during association negotiation, typically it’s seen afterwards or during DIMSE messaging. It may be due to the SCP using TLS or other methods to secure the connection

Hopefully by this point you’ve managed to get your AE associating with the SCP, so let’s turn it into an Echo SCU.

Creating the Echo SCU

Presentation Contexts

Note

What follows is a basic introduction to presentation contexts. More information is available in the presentation contexts section of the User Guide.

We’ve cheated a little bit in our code by including a presentation context used to propose the use of the verification service; 1.2.840.10008.1.1 - Verification SOP Class. This is visible in the debug output in the A-ASSOCIATE-RQ section as:

D: Presentation Context:
D:   Context ID:        1 (Proposed)
D:     Abstract Syntax: =Verification SOP Class
D:     Proposed SCP/SCU Role: Default
D:     Proposed Transfer Syntaxes:
D:       =Implicit VR Little Endian
D:       =Explicit VR Little Endian
D:       =Explicit VR Big Endian

Presentation contexts are how DICOM applications agree on which services are available to an association. Each DICOM service has a corresponding set of SOP Class UIDs which can be found in the relevant sections of Part 3 of the DICOM Standard. Setting one of these as the abstract syntax parameter in a proposed presentation context indicates to the acceptor that the corresponding service is requested for data matching that SOP class. Additionally, the presentation context includes a description on how any exchanged data is to be encoded, its transfer syntax.

So if you want to use the DICOM verification service, you propose a presentation context for the Verification SOP Class. If you wanted to use the storage service to store CT Images, you’d propose a presentation context for the CT Image Storage SOP class with a transfer syntax that matches the encoding of the CT data.

When the acceptor receives the proposed presentation contexts it goes through them one-by-one, either accepting or rejecting each. The results are visible in the A-ASSOCIATE-AC section of the debug log:

D: Presentation Contexts:
D:   Context ID:        1 (Accepted)
D:     Abstract Syntax: =Verification SOP Class
D:     Accepted SCP/SCU Role: Default
D:     Accepted Transfer Syntax: =Explicit VR Little Endian

Here you can see that the context was accepted and any transferred data must use Explicit VR Little Endian encoding. If a context is rejected and you still try to use it, or if a context is accepted but your data isn’t encoded with the same transfer syntax, you’ll get a ValueError exception similar to:

No presentation context for 'CT Image Storage' has been accepted by the peer with 'Implicit VR Little Endian' transfer syntax for the SCU role

Turning our AE into an Echo SCU

Since we’re able to associate with the Echo SCP, our next step is to request the use of it’s verification service. We do this by sending a DIMSE C-ECHO request:

 1 from pynetdicom import AE, debug_logger
 2
 3 debug_logger()
 4
 5 ae = AE()
 6 ae.add_requested_context("1.2.840.10008.1.1")
 7 assoc = ae.associate("127.0.0.1", 11112)
 8 if assoc.is_established:
 9     status = assoc.send_c_echo()
10     assoc.release()

The only thing we need to change is to include a call to send_c_echo(), which sends the C-ECHO request and returns a pydicom Dataset instance status. If we received a response to our C-ECHO request, then status will contain at least an (0000,0900) Status element containing the outcome of our request. If no response was received (due to a connection failure, a timeout, or because the association was aborted) then status will be an empty Dataset.

The Status element value is a code that indicates the result of the C-ECHO request. Each service class has a number of defined status codes which are usually a mix of generic codes for each DIMSE message type and code values specific to the service class. For example, if you look at the API reference for send_c_store() you’ll see there are general C-STORE status codes, such as 0x0000 (Success), as well as service specific codes, such as 0xC000 (Failure - cannot understand) for the storage service. The API reference for each Association.send_* method contains a list of possible status codes and their meaning.

Note

Service specific status codes are defined in the corresponding sections of Part 3 of the DICOM Standard, while the general codes are in Sections 9 and 10 and Annex C of Part 7.

If you run your modified code then at the end of the output you should see:

I: Association Accepted
I: Sending Echo Request: MsgID 1
D: pydicom.read_dataset() TransferSyntax="Little Endian Implicit"
I: Received Echo Response (Status: 0x0000 - Success)
I: Releasing Association

The SCP has responded with a Status: 0x0000 - Success, which indicates that the verification service request was successful. Congratulations, you’ve written your first DICOM application using pynetdicom.

Next steps

We recommend that you move on to writing your first SCP next. However, you might also be interested in the SCU examples available in the documentation, or the applications that come with pynetdicom.