Skip to content

Commit

Permalink
Merge pull request #87 from dwikler/refact-echoscp
Browse files Browse the repository at this point in the history
Refact echoscp
  • Loading branch information
sjswerdloff authored Dec 15, 2024
2 parents f389961 + 5e64f09 commit d46e645
Show file tree
Hide file tree
Showing 9 changed files with 289 additions and 65 deletions.
33 changes: 0 additions & 33 deletions tdwii_plus_examples/TDWII_PPVS_subscriber/echoscp.py

This file was deleted.

10 changes: 5 additions & 5 deletions tdwii_plus_examples/TDWII_PPVS_subscriber/nevent_receiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from pynetdicom import ALL_TRANSFER_SYNTAXES, UnifiedProcedurePresentationContexts, evt

from tdwii_plus_examples.basescp import BaseSCP
from tdwii_plus_examples.TDWII_PPVS_subscriber.echoscp import EchoSCP
from tdwii_plus_examples.cechoscp import CEchoSCP
from tdwii_plus_examples.TDWII_PPVS_subscriber.nevent_receiver_handlers import (
handle_nevent,
)
Expand Down Expand Up @@ -58,23 +58,23 @@ def nevent_cb(**kwargs):
logger.warning(f"Unknown Event Type ID: {event_type_id}")


class NEventReceiver(EchoSCP):
class NEventReceiver(CEchoSCP):
def __init__(
self, nevent_callback=None, ae_title: str = "NEVENT_RECEIVER", port: int = 11115, logger=None, bind_address: str = ""
):
if nevent_callback is None:
self.nevent_callback = nevent_cb # fallback to something that just logs incoming events
else:
self.nevent_callback = nevent_callback
EchoSCP.__init__(self, ae_title=ae_title, port=port, logger=logger, bind_address=bind_address)
CEchoSCP.__init__(self, ae_title=ae_title, port=port, logger=logger, bind_address=bind_address)

def _add_contexts(self):
EchoSCP._add_contexts(self)
CEchoSCP._add_contexts(self)
for cx in UnifiedProcedurePresentationContexts:
self.ae.add_supported_context(cx.abstract_syntax, ALL_TRANSFER_SYNTAXES, scp_role=True, scu_role=False)

def _add_handlers(self):
EchoSCP._add_handlers(self)
CEchoSCP._add_handlers(self)
self.handlers.append((evt.EVT_N_EVENT_REPORT, handle_nevent, [self.nevent_callback, None, self.logger]))

def run(self):
Expand Down
10 changes: 5 additions & 5 deletions tdwii_plus_examples/TDWII_PPVS_subscriber/storescp.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@

from tdwii_plus_examples.basescp import BaseSCP
from tdwii_plus_examples.TDWII_PPVS_subscriber.cstore_handler import handle_store
from tdwii_plus_examples.TDWII_PPVS_subscriber.echoscp import EchoSCP
from tdwii_plus_examples.cechoscp import CEchoSCP


class StoreSCP(EchoSCP):
class StoreSCP(CEchoSCP):
def __init__(
self,
ae_title: str = "STORE_SCP",
Expand All @@ -28,15 +28,15 @@ def __init__(
else:
self.handle_cstore = custom_handler
self.store_directory = store_directory
EchoSCP.__init__(self, ae_title=ae_title, port=port, logger=logger, bind_address=bind_address)
CEchoSCP.__init__(self, ae_title=ae_title, port=port, logger=logger, bind_address=bind_address)

def _add_contexts(self):
EchoSCP._add_contexts(self)
CEchoSCP._add_contexts(self)
for context in self.storage_presentation_contexts:
self.ae.add_supported_context(context.abstract_syntax, self.transfer_syntaxes)

def _add_handlers(self):
EchoSCP._add_handlers(self)
CEchoSCP._add_handlers(self)
args = Namespace(ignore=False, output_directory=self.store_directory)

self.handlers.append((evt.EVT_C_STORE, self.handle_cstore, [args, self.logger]))
Expand Down
22 changes: 14 additions & 8 deletions tdwii_plus_examples/basescp.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from argparse import Namespace
import logging

from pynetdicom import AE, evt
from pynetdicom.apps.common import setup_logging
Expand All @@ -23,7 +24,7 @@ class BaseSCP:
ae_title (str): The title of the Application Entity (AE)
bind_address (str): The IP address or hostname of the AE
port (int): The port number the AE will listen on
logger: A logger instance (optional)
logger(logging.Logger): A logger instance
Methods:
_add_contexts(self)
Expand Down Expand Up @@ -66,8 +67,8 @@ def __init__(self,
if logger is None:
self.logger = setup_logging(
Namespace(log_type="d", log_level="debug"), "base_scp")
# elif not isinstance(self.logger, logging.Logger):
# raise TypeError("logger must be an instance of logging.Logger")
elif not isinstance(logger, logging.Logger):
raise TypeError("logger must be an instance of logging.Logger")
else:
self.logger = logger

Expand Down Expand Up @@ -103,7 +104,7 @@ def _add_contexts(self):

def _add_handlers(self):
"""
Adds handlers for processing DICOM events from incoming associations.
Adds handlers for processing TCP events from incoming associations.
This method is intended to be overridden in derived classes.
"""
Expand All @@ -122,10 +123,15 @@ def run(self):
not block the calling thread.
"""
# Listen for incoming association requests
self.threaded_server = self.ae.start_server(
(self.bind_address, self.port),
evt_handlers=self.handlers,
block=False)
try:
self.threaded_server = self.ae.start_server(
(self.bind_address, self.port),
evt_handlers=self.handlers,
block=False)
if self.threaded_server is not None:
self.logger.info("SCP server started successfully")
except Exception as e:
self.logger.error("SCP server failed to start: %s", e)

def stop(self):
"""
Expand Down
22 changes: 22 additions & 0 deletions tdwii_plus_examples/cechohandler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
def handle_cecho(event, logger):
"""Handler for evt.EVT_C_ECHO.
Parameters
----------
event : events.Event
The corresponding event.
logger : logging.Logger
The application's logger.
Returns
-------
int
The status of the C-ECHO operation, always ``0x0000`` (Success).
"""
requestor = event.assoc.requestor
timestamp = event.timestamp.strftime("%Y-%m-%d %H:%M:%S")
aet, addr, port = requestor.ae_title, requestor.address, requestor.port
logger.info(
f"Received C-ECHO request from {aet}@{addr}:{port} at {timestamp}"
)

return 0x0000
92 changes: 92 additions & 0 deletions tdwii_plus_examples/cechoscp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from pynetdicom import evt
from pynetdicom.sop_class import Verification

from tdwii_plus_examples.basescp import BaseSCP
from tdwii_plus_examples.cechohandler import handle_cecho


class CEchoSCP(BaseSCP):
"""
A subclass of the BaseSCP class that implements the DICOM Verification
Service Class Provider (SCP).
Usage:
Create an instance of CEchoSCP and call the run method inherited
from the BaseSCP parent class to start listening for incoming
DICOM association requests.
The CEchoSCP class is generally not used directly. Instead, create
a subclass and override the [_add_contexts] and [_add_handlers] methods
to add presentation contexts and handlers for specific DICOM services.
Attributes:
ae_title (str): The title of the Application Entity (AE)
bind_address (str): The IP address or hostname of the AE
port (int): The port number the AE will listen on
logger (logging.Logger): A logger instance
Methods:
_add_contexts(self)
Adds presentation contexts to the SCP instance.
_add_handlers(self)
Adds handlers for DICOM communication events to the SCP instance.
"""
def __init__(self,
ae_title: str = "ECHO_SCP",
bind_address: str = "",
port: int = 11112,
logger=None):

"""
Initializes a new instance of the CEchoSCP class.
This method creates an AE without presentation contexts.
Parameters
----------
ae_title : str
The title of the Application Entity (AE)
Optional, default: "ECHO_SCP"
bind_address : str
A specific IP address or hostname of the AE
Optional, default: "" will bind to all interfaces.
port: int
The port number to listen on
Optional, default: 11112 (as registered for DICOM at IANA)
logger: logging.Logger
A logger instance
Optional, default: None, a debug logger will be used.
"""
super().__init__(
ae_title=ae_title,
bind_address=bind_address,
port=port,
logger=logger)

def _add_contexts(self):
"""
Adds the DICOM Verification SOP Class presentation context to the AE.
Only the default Implicit VR Little Endian transfer syntax is included.
This method overrides the base class method to add support for the
Verification SOP Class which is required for any DICOM SCP.
This method is intended to be overridden in derived classes.
"""
super()._add_contexts()
self.ae.add_supported_context(Verification, "1.2.840.10008.1.2")

def _add_handlers(self):
"""
Adds a handler for for processing incoming C-ECHO requests.
This method overrides the base class method to add a handler
for the Verification SOP Class, allowing the AE to respond
to C-ECHO requests.
This method is intended to be overridden in derived classes.
"""
super()._add_handlers()
self.handlers.append((evt.EVT_C_ECHO, handle_cecho,
[self.logger]))
57 changes: 57 additions & 0 deletions tdwii_plus_examples/cli/echoscp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env python
import argparse
import logging

from tdwii_plus_examples.cechoscp import CEchoSCP


def main():
parser = argparse.ArgumentParser(
description="Run a DICOM Verification SCP."
)
parser.add_argument(
'-a', '--ae_title', type=str, default='ECHO_SCP',
help='Application Entity Title'
)
parser.add_argument(
'-b', '--bind_address', type=str, default='',
help='Specific IP address or hostname, omit to bind to all interfaces'
)
parser.add_argument(
'-p', '--port', type=int, default=11112,
help='Port number'
)
parser.add_argument(
'-v', '--verbose', action='store_true',
help='Set log level to INFO'
)
parser.add_argument(
'-d', '--debug', action='store_true',
help='Set log level to DEBUG'
)

args = parser.parse_args()

log_level = logging.WARNING
if args.verbose:
log_level = logging.INFO
elif args.debug:
log_level = logging.DEBUG

logging.basicConfig(level=log_level)
logger = logging.getLogger('echoscp')

logger.info("Starting up the DICOM Verification SCP...")
cechoscp = CEchoSCP(ae_title=args.ae_title, bind_address=args.bind_address,
port=args.port, logger=logger)
cechoscp.run()
# Keep the main application running
try:
while True:
pass # You can replace this with your main application logic
except KeyboardInterrupt:
logger.info("Shutting down the DICOM Verification SCP...")


if __name__ == "__main__":
main()
Loading

0 comments on commit d46e645

Please sign in to comment.