diff --git a/LoopbackApplicationEntities.json b/LoopbackApplicationEntities.json new file mode 100644 index 0000000..3456f0b --- /dev/null +++ b/LoopbackApplicationEntities.json @@ -0,0 +1,37 @@ +[ + { + "AETitle": "OST", + "IPAddr": "127.0.0.1", + "Port": 11112 + }, + { + "AETitle": "QRSCP", + "IPAddr": "127.0.0.1", + "Port": 11112 + }, + { + "AETitle": "TMS", + "IPAddr": "127.0.0.1", + "Port": 11114 + }, + { + "AETitle": "UPSSCP", + "IPAddr": "127.0.0.1", + "Port": 11114 + }, + { + "AETitle": "PPVS_SCP", + "IPAddr": "127.0.0.1", + "Port": 11115 + }, + { + "AETitle": "NEVENT_RECEIVER", + "IPAddr": "127.0.0.1", + "Port": 11115 + }, + { + "AETitle": "TDD", + "IPAddr": "127.0.0.1", + "Port": 11113 + } +] diff --git a/README.md b/README.md index 3e20c22..8a8ba23 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ If the required packages are not shown (e.g. pydicom, pynetdicom), you may have Try: ```console export VIRTUAL_ENV=$(pyenv virtualenv-prefix)/envs/$(pyenv version | cut -f1 -d ' ') +poetry install ``` @@ -45,7 +46,7 @@ To generate .dcm files needed by ups enabled findscu dump2dcm queryfile.dcmdump.txt queryfile.dcm ``` -## A sample C-FIND SCU is available from pynetdicom (it has been enhanced with support for UPS): clone from https://github.com/pynetdicom/pynetdicom.git +## A sample C-FIND SCU is available from pynetdicom (it has been enhanced with support for UPS): clone from https://github.com/pydicom/pynetdicom then use findscu to query the TMS (Treatment Management System): in pynetdicom/apps @@ -129,9 +130,9 @@ python nactionscu.py -T "1.2.826.0.1.3680043.8.498.23133079088775253446636289730 ``` the above will request that upsscp (listening at 11114) change the state using the Transaction UID (-T) of the UPS with the shown UID to "COMPLETED". The Transaction UID here is not optional. -## A sample application for receiving notifications (N-EVENT-REPORT-RQ) is provided in neventscp.py +## A sample application for receiving notifications (N-EVENT-REPORT-RQ) is provided in nevent_receiver.py ```console -python neventscp.py --debug +python nevent_receiver.py --debug ``` which listens on port 11115 by default, @@ -140,9 +141,9 @@ The application does not take specific actions when receiving an N-EVENT-REPORT -## A sample application for sending notifications is provided in neventscu.py (which can be run against neventscp.py mentioned above) +## A sample application for sending notifications is provided in nevent_sender.py (which can be run against nevent_receiver.py mentioned above) ```console -python neventscu.py 127.0.0.1 11115 +python nevent_sender.py 127.0.0.1 11115 ``` ## A Qt/PySide6 based utility for generating RT Beams Delivery Instructions and Unified Procedure Step content diff --git a/tdwii_plus_examples/ApplicationEntities.json b/tdwii_plus_examples/ApplicationEntities.json new file mode 100644 index 0000000..3456f0b --- /dev/null +++ b/tdwii_plus_examples/ApplicationEntities.json @@ -0,0 +1,37 @@ +[ + { + "AETitle": "OST", + "IPAddr": "127.0.0.1", + "Port": 11112 + }, + { + "AETitle": "QRSCP", + "IPAddr": "127.0.0.1", + "Port": 11112 + }, + { + "AETitle": "TMS", + "IPAddr": "127.0.0.1", + "Port": 11114 + }, + { + "AETitle": "UPSSCP", + "IPAddr": "127.0.0.1", + "Port": 11114 + }, + { + "AETitle": "PPVS_SCP", + "IPAddr": "127.0.0.1", + "Port": 11115 + }, + { + "AETitle": "NEVENT_RECEIVER", + "IPAddr": "127.0.0.1", + "Port": 11115 + }, + { + "AETitle": "TDD", + "IPAddr": "127.0.0.1", + "Port": 11113 + } +] diff --git a/tdwii_plus_examples/TDWII_PPVS_subscriber/__init__.py b/tdwii_plus_examples/TDWII_PPVS_subscriber/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tdwii_plus_examples/TDWII_PPVS_subscriber/basescp.py b/tdwii_plus_examples/TDWII_PPVS_subscriber/basescp.py index df1b996..28a7876 100644 --- a/tdwii_plus_examples/TDWII_PPVS_subscriber/basescp.py +++ b/tdwii_plus_examples/TDWII_PPVS_subscriber/basescp.py @@ -7,7 +7,7 @@ from typing import Tuple import pydicom.config -from neventscp_handlers import handle_echo +from tdwii_plus_examples.TDWII_PPVS_subscriber.nevent_receiver_handlers import handle_echo from pynetdicom import ( AE, ALL_TRANSFER_SYNTAXES, diff --git a/tdwii_plus_examples/TDWII_PPVS_subscriber/echoscp.py b/tdwii_plus_examples/TDWII_PPVS_subscriber/echoscp.py index 249297f..30fbd00 100644 --- a/tdwii_plus_examples/TDWII_PPVS_subscriber/echoscp.py +++ b/tdwii_plus_examples/TDWII_PPVS_subscriber/echoscp.py @@ -7,7 +7,7 @@ from typing import Tuple from time import sleep import pydicom.config -from neventscp_handlers import handle_echo +from tdwii_plus_examples.TDWII_PPVS_subscriber.nevent_receiver_handlers import handle_echo from pynetdicom import ( AE, ALL_TRANSFER_SYNTAXES, @@ -20,7 +20,7 @@ from pynetdicom.sop_class import Verification from pynetdicom.utils import set_ae -from basescp import BaseSCP +from tdwii_plus_examples.TDWII_PPVS_subscriber.basescp import BaseSCP class EchoSCP: def __init__(self, diff --git a/tdwii_plus_examples/TDWII_PPVS_subscriber/neventscp.py b/tdwii_plus_examples/TDWII_PPVS_subscriber/nevent_receiver.py similarity index 90% rename from tdwii_plus_examples/TDWII_PPVS_subscriber/neventscp.py rename to tdwii_plus_examples/TDWII_PPVS_subscriber/nevent_receiver.py index 7f08821..09e5023 100644 --- a/tdwii_plus_examples/TDWII_PPVS_subscriber/neventscp.py +++ b/tdwii_plus_examples/TDWII_PPVS_subscriber/nevent_receiver.py @@ -7,7 +7,7 @@ from typing import Tuple from time import sleep import pydicom.config -from neventscp_handlers import handle_echo, handle_nevent +from tdwii_plus_examples.TDWII_PPVS_subscriber.nevent_receiver_handlers import handle_echo, handle_nevent from pynetdicom import ( AE, ALL_TRANSFER_SYNTAXES, @@ -20,8 +20,8 @@ from pynetdicom.sop_class import Verification from pynetdicom.utils import set_ae -from basescp import BaseSCP -from echoscp import EchoSCP +from tdwii_plus_examples.TDWII_PPVS_subscriber.basescp import BaseSCP +from tdwii_plus_examples.TDWII_PPVS_subscriber.echoscp import EchoSCP def nevent_cb(**kwargs): logger = None @@ -75,10 +75,10 @@ def nevent_cb(**kwargs): if logger: logger.warning(f"Unknown Event Type ID: {event_type_id}") -class NEventSCP(EchoSCP): +class NEventReceiver(EchoSCP): def __init__(self, nevent_callback=None, - ae_title:str="NEVENT_SCP", + ae_title:str="NEVENT_RECEIVER", port:int=11115, logger=None, bind_address:str="" @@ -104,6 +104,6 @@ def run(self): BaseSCP.run(self) if __name__ == '__main__': - my_scp = NEventSCP() + my_scp = NEventReceiver() my_scp.run() while True: sleep(100) # sleep forever \ No newline at end of file diff --git a/tdwii_plus_examples/TDWII_PPVS_subscriber/neventscp_handlers.py b/tdwii_plus_examples/TDWII_PPVS_subscriber/nevent_receiver_handlers.py similarity index 99% rename from tdwii_plus_examples/TDWII_PPVS_subscriber/neventscp_handlers.py rename to tdwii_plus_examples/TDWII_PPVS_subscriber/nevent_receiver_handlers.py index e61fd51..cfc824e 100644 --- a/tdwii_plus_examples/TDWII_PPVS_subscriber/neventscp_handlers.py +++ b/tdwii_plus_examples/TDWII_PPVS_subscriber/nevent_receiver_handlers.py @@ -1,4 +1,4 @@ -"""Event handlers for neventscp.py""" +"""Event handlers for nevent_receiver.py""" from pydicom import dcmread diff --git a/tdwii_plus_examples/TDWII_PPVS_subscriber/ppvs_subscriber_widget.py b/tdwii_plus_examples/TDWII_PPVS_subscriber/ppvs_subscriber_widget.py old mode 100644 new mode 100755 index 859c470..e7a316f --- a/tdwii_plus_examples/TDWII_PPVS_subscriber/ppvs_subscriber_widget.py +++ b/tdwii_plus_examples/TDWII_PPVS_subscriber/ppvs_subscriber_widget.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python # This Python file uses the following encoding: utf-8 import sys from pathlib import Path diff --git a/tdwii_plus_examples/TDWII_PPVS_subscriber/ppvsscp.py b/tdwii_plus_examples/TDWII_PPVS_subscriber/ppvsscp.py index 41af5a4..0d3c73c 100644 --- a/tdwii_plus_examples/TDWII_PPVS_subscriber/ppvsscp.py +++ b/tdwii_plus_examples/TDWII_PPVS_subscriber/ppvsscp.py @@ -23,13 +23,13 @@ from basescp import BaseSCP from echoscp import EchoSCP -from neventscp import NEventSCP +from tdwii_plus_examples.TDWII_PPVS_subscriber.nevent_receiver import NEventReceiver from storescp import StoreSCP -class PPVS_SCP(NEventSCP,StoreSCP): +class PPVS_SCP(NEventReceiver,StoreSCP): def __init__(self, ae_title:str="PPVS_SCP", - port:int=11112, + port:int=11115, logger=None, bind_address:str="", storage_presentation_contexts=AllStoragePresentationContexts, @@ -50,7 +50,7 @@ def __init__(self, storage_presentation_contexts=storage_presentation_contexts, transfer_syntaxes=transfer_syntaxes, store_directory=store_directory) - NEventSCP.__init__(self, + NEventReceiver.__init__(self, nevent_callback=nevent_callback, ae_title=ae_title, port=port, @@ -61,7 +61,7 @@ def __init__(self, def _add_contexts(self): StoreSCP._add_contexts(self) - NEventSCP._add_contexts(self) + NEventReceiver._add_contexts(self) @@ -69,7 +69,7 @@ def _add_contexts(self): def _add_handlers(self): StoreSCP._add_handlers(self) - NEventSCP._add_handlers(self) + NEventReceiver._add_handlers(self) def run(self): # Listen for incoming association requests diff --git a/tdwii_plus_examples/TDWII_PPVS_subscriber/tdwii_ppvs_subscriber.ui b/tdwii_plus_examples/TDWII_PPVS_subscriber/tdwii_ppvs_subscriber.ui index e36cbc6..d20da9e 100644 --- a/tdwii_plus_examples/TDWII_PPVS_subscriber/tdwii_ppvs_subscriber.ui +++ b/tdwii_plus_examples/TDWII_PPVS_subscriber/tdwii_ppvs_subscriber.ui @@ -23,7 +23,7 @@ - group_box_aes_and_machine_name + Remote System Configuration @@ -68,7 +68,7 @@ - ppvs_scp_group_box + PPVS Configuration @@ -131,7 +131,7 @@ - group_box_cfind_request_and_response + UPS Query Specification @@ -210,7 +210,7 @@ - group_box_get_reference_data + Reference Data Retrieval diff --git a/tdwii_plus_examples/handlers.py b/tdwii_plus_examples/handlers.py index 7acac88..107409b 100644 --- a/tdwii_plus_examples/handlers.py +++ b/tdwii_plus_examples/handlers.py @@ -23,6 +23,7 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from upsdb import Instance, InvalidIdentifier, add_instance, search +import tdwii_config _SERVICE_STATUS = { "SCHEDULED": { @@ -55,6 +56,8 @@ _global_subscribers = dict() # AE Title and delection lock boolean "TRUE" or "FALSE" is the text representation _filtered_subscribers = dict() # AE Title and the Dataset acting as the query filter +REMOTE_AE_CONFIG_FILE = "ApplicationEntities.json" +tdwii_config.load_ae_config(REMOTE_AE_CONFIG_FILE) def _add_global_subscriber(subscriber_ae_title: str, deletion_lock: bool = False, logger=None): if subscriber_ae_title not in _global_subscribers.keys(): @@ -610,7 +613,7 @@ def handle_nset(event, db_path, cli_config, logger): Parameters ---------- event : pynetdicom.events.Event - The C-GET request :class:`~pynetdicom.events.Event`. + The N-SET request :class:`~pynetdicom.events.Event`. db_path : str The database path to use with create_engine(). cli_config : dict @@ -623,13 +626,13 @@ def handle_nset(event, db_path, cli_config, logger): int The number of sub-operations required to complete the request. int or pydicom.dataset.Dataset, pydicom.dataset.Dataset or None - The C-GET response's *Status* and if the *Status* is pending then + The N-SET response's *Status* and if the *Status* is pending then the dataset to be sent, otherwise ``None``. """ requestor = event.assoc.requestor timestamp = event.timestamp.strftime("%Y-%m-%d %H:%M:%S") addr, port = requestor.address, requestor.port - logger.info(f"Received C-GET request from {addr}:{port} at {timestamp}") + logger.info(f"Received N-SET request from {addr}:{port} at {timestamp}") model = event.request.AffectedSOPClassUID @@ -642,7 +645,7 @@ def handle_nset(event, db_path, cli_config, logger): matches = search(model, event.identifier, session) except InvalidIdentifier as exc: session.rollback() - logger.error("Invalid C-GET Identifier received") + logger.error("Invalid N-SET Identifier received") logger.error(str(exc)) yield 0xA900, None return @@ -820,19 +823,42 @@ def handle_ncreate(event, storage_dir, db_path, cli_config, logger): for globalsubscriber in _global_subscribers: # Request association with subscriber ae = AE(ae_title=acceptor.ae_title) - # hard code for the moment, deal with configuration of AE's soon + if (not globalsubscriber in tdwii_config.known_ae_ipaddr): + logger.error(f"{globalsubscriber} missing IP Address configuration in {REMOTE_AE_CONFIG_FILE}") + continue + + if (not globalsubscriber in tdwii_config.known_ae_port): + logger.error(f"{globalsubscriber} missing Port configuration in {REMOTE_AE_CONFIG_FILE}") + continue + + subscriber_ip_addr = tdwii_config.known_ae_ipaddr[globalsubscriber] + subscriber_port = tdwii_config.known_ae_port[globalsubscriber] assoc = ae.associate( - "127.0.0.1", - 11112, + subscriber_ip_addr, + subscriber_port, contexts=UnifiedProcedurePresentationContexts, ae_title=globalsubscriber, max_pdu=16382, ) if assoc.is_established: + message_id=0 try: - logger.info(f"Send UPS State Report: {ds.SOPInstanceUID}, {ds.ProcedureStepState}") - assoc.send_n_event_report(event_info, event_type, UnifiedProcedureStepPush, ds.SOPInstanceUID) + if (event_type==1): + logger.info(f"Sending UPS State Report: {ds.SOPInstanceUID}, {ds.ProcedureStepState}") + message_id +=1 + assoc.send_n_event_report(event_info, event_type, UnifiedProcedureStepPush, ds.SOPInstanceUID, message_id) + elif (event_type==5): # The assignment took place at the time of creation + # notify of creation first, i.e. event type == 1 + logger.info(f"Sending UPS State Report: {ds.SOPInstanceUID}, {ds.ProcedureStepState}") + message_id +=1 + assoc.send_n_event_report(event_info, 1, UnifiedProcedureStepPush, ds.SOPInstanceUID, message_id) + # if the assignment happened after the creation (e.g. via N-SET or internal change in a TMS) + # then *only* send an N-EVENT-REPORT regarding the UPS Assignment + logger.info(f"Sending UPS Assignment: {ds.ScheduledStationNameCodeSequence}") + message_id +=1 + assoc.send_n_event_report(event_info, event_type, UnifiedProcedureStepPush, ds.SOPInstanceUID, message_id) + logger.info(f"Notified global subscriber: {globalsubscriber}") except InvalidDicomError: logger.error("Bad DICOM: ") @@ -845,6 +871,6 @@ def handle_ncreate(event, storage_dir, db_path, cli_config, logger): assoc.release() else: - logger.error(f"Failed to establish assocation with subscriber: {globalsubscriber}") + logger.error(f"Failed to establish association with subscriber: {globalsubscriber}") return 0x0000, ds diff --git a/tdwii_plus_examples/neventscp.py b/tdwii_plus_examples/nevent_receiver.py similarity index 94% rename from tdwii_plus_examples/neventscp.py rename to tdwii_plus_examples/nevent_receiver.py index f6c7a31..0807c22 100755 --- a/tdwii_plus_examples/neventscp.py +++ b/tdwii_plus_examples/nevent_receiver.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -"""A Verification and N_EVENT_REPORT SCP application.""" +"""A Verification SCP and N_EVENT_REPORT receiver application.""" import argparse import os @@ -7,7 +7,7 @@ from configparser import ConfigParser import pydicom.config -from neventscp_handlers import handle_echo, handle_nevent +from nevent_receiver_handlers import handle_echo, handle_nevent from pynetdicom import ( AE, ALL_TRANSFER_SYNTAXES, @@ -20,7 +20,6 @@ from pynetdicom.sop_class import Verification from pynetdicom.utils import set_ae -# from pynetdicom.apps.neventscp import db # Use `None` for empty values pydicom.config.use_none_as_empty_text_VR_value = True @@ -80,11 +79,11 @@ def _setup_argparser(): # Description parser = argparse.ArgumentParser( description=( - "The neventscp application implements a Service Class Provider (SCP) " - "for the Verification, Storage and Query/Retrieve (QR) Service " + "The nevent_receiver application implements a Service Class Provider (SCP) " + "for the Verification and Unified Procedure Step Service " "Classes." ), - usage="neventscp [options]", + usage="nevent_receiver [options]", ) # General Options @@ -126,7 +125,7 @@ def _setup_argparser(): choices=["critical", "error", "warn", "info", "debug"], ) fdir = os.path.abspath(os.path.dirname(__file__)) - fpath = os.path.join(fdir, "neventscp_default.ini") + fpath = os.path.join(fdir, "nevent_receiver_default.ini") gen_opts.add_argument( "-c", "--config", @@ -252,7 +251,7 @@ def nevent_cb(**kwargs): ) else: if logger: - logger.warning(f"Unkown Event Type ID: {event_type_id}") + logger.warning(f"Unknown Event Type ID: {event_type_id}") def main(args=None): @@ -263,11 +262,11 @@ def main(args=None): args = _setup_argparser() if args.version: - print(f"neventscp.py v{__version__}") + print(f"nevent_receiver.py v{__version__}") sys.exit() - APP_LOGGER = setup_logging(args, "neventscp") - APP_LOGGER.debug(f"neventscp.py v{__version__}") + APP_LOGGER = setup_logging(args, "nevent_receiver") + APP_LOGGER.debug(f"nevent_receiver.py v{__version__}") APP_LOGGER.debug("") APP_LOGGER.debug("Using configuration from:") diff --git a/tdwii_plus_examples/neventscp_default.ini b/tdwii_plus_examples/nevent_receiver_default.ini similarity index 91% rename from tdwii_plus_examples/neventscp_default.ini rename to tdwii_plus_examples/nevent_receiver_default.ini index 2f819ee..02db302 100644 --- a/tdwii_plus_examples/neventscp_default.ini +++ b/tdwii_plus_examples/nevent_receiver_default.ini @@ -1,9 +1,9 @@ -# Default configuration file for upsscp.py +# Default configuration file for nevent_receiver.py ## Application settings [DEFAULT] # Our AE Title - ae_title: NEVENT_SCP + ae_title: NEVENT_RECEIVER # Our listen port port: 11115 # Our maximum PDU size; 0 for unlimited diff --git a/tdwii_plus_examples/neventscp_handlers.py b/tdwii_plus_examples/nevent_receiver_handlers.py similarity index 99% rename from tdwii_plus_examples/neventscp_handlers.py rename to tdwii_plus_examples/nevent_receiver_handlers.py index e61fd51..cfc824e 100644 --- a/tdwii_plus_examples/neventscp_handlers.py +++ b/tdwii_plus_examples/nevent_receiver_handlers.py @@ -1,4 +1,4 @@ -"""Event handlers for neventscp.py""" +"""Event handlers for nevent_receiver.py""" from pydicom import dcmread diff --git a/tdwii_plus_examples/neventscu.py b/tdwii_plus_examples/nevent_sender.py similarity index 91% rename from tdwii_plus_examples/neventscu.py rename to tdwii_plus_examples/nevent_sender.py index 01f9e6d..6b8903f 100755 --- a/tdwii_plus_examples/neventscu.py +++ b/tdwii_plus_examples/nevent_sender.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -"""neventscu +"""nevent_sender Used for sending events to AE's who subscribes for UPS Events Currently at the toy level of functionality: @@ -104,10 +104,12 @@ def _setup_argparser(): # Description parser = argparse.ArgumentParser( description=( - "The neventscu application implements a Service Class User " + "The nevent_sender application implements a Service Class User " "(SCU) for the UPS Event Class. " + "Real world applications will often embed the nevent_sender functionality in an SCP" + "With the nevent_sender acting under role reversal (i.e.) as an SCU within the SCP" ), - usage="neventscu [options] addr port", + usage="nevent_sender [options] addr port", ) # Parameters @@ -173,25 +175,25 @@ def _setup_argparser(): "-aet", "--calling-aet", metavar="[a]etitle", - help="set my calling AE title (default: WATCH_SCU)", + help="set my calling AE title (default: NEVENT_SENDER)", type=str, - default="WATCH_SCU", + default="NEVENT_SENDER", ) net_opts.add_argument( "-aec", "--called-aet", metavar="[a]etitle", - help="set called AE title of peer (default: WATCH_SCP)", + help="set called AE title of peer (default: NEVENT_RECEIVER)", type=str, - default="WATCH_SCP", + default="NEVENT_RECEIVER", ) net_opts.add_argument( "-aer", "--receiver-aet", metavar="[a]etitle", - help="set receiver AE title of peer (default: EVENT_SCP)", + help="set receiver AE title of peer (default: NEVENT_RECEIVER)", type=str, - default="EVENT_SCP", + default="NEVENT_RECEIVER", ) net_opts.add_argument( "-ta", @@ -309,11 +311,11 @@ def main(args=None): args = _setup_argparser() if args.version: - print(f"neventscu.py v{__version__}") + print(f"nevent_sender.py v{__version__}") sys.exit() - APP_LOGGER = setup_logging(args, "neventscu") - APP_LOGGER.debug(f"neventscu.py v{__version__}") + APP_LOGGER = setup_logging(args, "nevent_sender") + APP_LOGGER.debug(f"nevent_sender.py v{__version__}") APP_LOGGER.debug("") APP_LOGGER.debug("Using configuration from:") @@ -354,11 +356,13 @@ def main(args=None): status, response = send_ups_state_report(assoc, UID("1.2.3.4"), "SCHEDULED") APP_LOGGER.info(f"Status: {os.linesep}{status}") APP_LOGGER.info(f"Response: {os.linesep}{response}") + status, response = send_ups_state_report( assoc, UID("1.2.3.4"), "IN PROGRESS" ) APP_LOGGER.info(f"Status: {os.linesep}{status}") APP_LOGGER.info(f"Response: {os.linesep}{response}") + status, response = send_ups_state_report(assoc, UID("1.2.3.4"), "COMPLETED") APP_LOGGER.info(f"Status: {os.linesep}{status}") APP_LOGGER.info(f"Response: {os.linesep}{response}") @@ -372,6 +376,7 @@ def main(args=None): assoc.release() else: + APP_LOGGER.error("Unable to form association with " + args.called_aet) sys.exit(1) diff --git a/tdwii_plus_examples/rtbdi_creator/README.md b/tdwii_plus_examples/rtbdi_creator/README.md index 90094bf..2b0d42b 100644 --- a/tdwii_plus_examples/rtbdi_creator/README.md +++ b/tdwii_plus_examples/rtbdi_creator/README.md @@ -3,3 +3,19 @@ RT Beams Delivery Instruction and UPS Creator PySide6 GUI and/or command line driven tool to automate construction of fraction/session specific files for use in the IHE-RO TDW-II profile. +Command Line Interface: + +generate_course_sessions_for_plan.py plan_filename cmove_AE_TITLE + +will generate an entire course worth of RT Beams Delivery Instructions and Unified Procedure Steps, based on the number of fractions specified in the plan, with the start datetime being "now", with one fraction scheduled per day. +Example: +You are using OpenTPS to generate RT Ion Plans, are saving your plans in a subdirectory of your home directory in OpenTPS/Plans and saved a plan in a file named my_plan.dcm and you are using the pynetdicom sample application qrscp as the Object Store: + +generate_course_sessions_for_plan.py ~/OpenTPS/Plans/my_plan.dcm QRSCP + + +GUI: + +mainbdiwidget.py + +After generating the RT BDI and UPS, you will want to C-STORE the RT BDI object(s) to QRSCP (e.g. using the pynetdicom sample application storescu) and then use ncreatescu.py to have the UPS(s) created in the UPSSCP diff --git a/tdwii_plus_examples/rtbdi_creator/__init__.py b/tdwii_plus_examples/rtbdi_creator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tdwii_plus_examples/rtbdi_creator/mainbdiwidget.py b/tdwii_plus_examples/rtbdi_creator/mainbdiwidget.py old mode 100644 new mode 100755 index 091d273..ef92d9f --- a/tdwii_plus_examples/rtbdi_creator/mainbdiwidget.py +++ b/tdwii_plus_examples/rtbdi_creator/mainbdiwidget.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python # This Python file uses the following encoding: utf-8 from datetime import datetime from pathlib import Path diff --git a/tdwii_plus_examples/rtbdi_creator/rtbdi_creator.pyproject.user b/tdwii_plus_examples/rtbdi_creator/rtbdi_creator.pyproject.user index eb50f15..72e4daa 100644 --- a/tdwii_plus_examples/rtbdi_creator/rtbdi_creator.pyproject.user +++ b/tdwii_plus_examples/rtbdi_creator/rtbdi_creator.pyproject.user @@ -1,6 +1,6 @@ - + EnvironmentId diff --git a/tdwii_plus_examples/tests/test_nevent_sender.py b/tdwii_plus_examples/tests/test_nevent_sender.py new file mode 100644 index 0000000..ff11d5a --- /dev/null +++ b/tdwii_plus_examples/tests/test_nevent_sender.py @@ -0,0 +1,168 @@ +"""Unit tests for nevent_sender.py""" + +from time import sleep +import os +import subprocess +import sys +import time + +import pytest +from pydicom import Dataset, dcmread +from pydicom.uid import ( + DeflatedExplicitVRLittleEndian, + ExplicitVRBigEndian, + ExplicitVRLittleEndian, + ImplicitVRLittleEndian, +) +from pynetdicom import ( + AE, + ALL_TRANSFER_SYNTAXES, + UnifiedProcedurePresentationContexts, + evt, +) +from pynetdicom.sop_class import UnifiedProcedureStepPush, Verification + +# from nevent_receiver_handlers import handle_nevent +from tdwii_plus_examples.TDWII_PPVS_subscriber.nevent_receiver import NEventReceiver + +# debug_logger() + + +APP_DIR = os.path.join(os.path.dirname(__file__), "../") +APP_FILE = os.path.join(APP_DIR, "./", "nevent_sender.py") +DATA_DIR = os.path.join(APP_DIR, "../", "responses", "dcm") +DATASET_FILE = os.path.join(DATA_DIR, "rsp000001.dcm") +LIB_DIR = os.path.join(APP_DIR, "../") + + +def start_nevent_sender(args): + """Start the nevent_sender.py app and return the process.""" + pargs = [sys.executable, APP_FILE] + [*args] + return subprocess.Popen(pargs) + + +def default_handle_nevent(event): + req = event.request + # attr_list = event.attribute_list + ds = Dataset() + + # Add the SOP Common module elements (Annex C.12.1) + ds.AffectedSOPClassUID = UnifiedProcedureStepPush + ds.AffectedSOPInstanceUID = req.AffectedSOPInstanceUID + + # Update with the requested attributes + # ds.update(attr_list) + ds.is_little_endian = True + ds.is_implicit_VR = True + ds.Status = 0x0000 + return ds, None + + +class neventsenderBase: + """Tests for nevent_sender.py""" + + def setup_method(self): + """Run prior to each test""" + self.ae = None + self.func = None + + def teardown_method(self): + """Clear any active threads""" + if self.ae: + self.ae.shutdown() + + def test_default(self): + """Test default settings.""" + + events = [] + + def handle_nevent_receive(event): + events.append(event) + yield 0 + yield 0 + + def handle_release(event): + events.append(event) + + handlers = [ + (evt.EVT_N_EVENT_REPORT, handle_nevent_receive), + (evt.EVT_RELEASED, handle_release), + ] + + self.ae = ae = AE() + ae.acse_timeout = 5 + ae.dimse_timeout = 5 + ae.network_timeout = 5 + ae.add_supported_context(UnifiedProcedureStepPush) + scp = ae.start_server(("localhost", 11115), block=False, evt_handlers=handlers) + + + p = self.func(["127.0.0.1", "11115"]) + p.wait() + assert p.returncode == 0 + # sleep(1.0) + scp.shutdown() + + + + assert events[0].event == evt.EVT_N_EVENT_REPORT + current_event = events[0] + nevent_primitive = current_event.request + model = current_event.request.AffectedSOPClassUID + nevent_type_id = nevent_primitive.EventTypeID + nevent_information = dcmread(nevent_primitive.EventInformation, force=True) + assert model.keyword in ["UnifiedProcedureStepPush"] + assert nevent_type_id == 1 + assert nevent_information.ProcedureStepState == "SCHEDULED" + + assert events[1].event == evt.EVT_N_EVENT_REPORT + current_event = events[1] + nevent_primitive = current_event.request + model = current_event.request.AffectedSOPClassUID + nevent_type_id = nevent_primitive.EventTypeID + nevent_information = dcmread(nevent_primitive.EventInformation, force=True) + assert model.keyword in ["UnifiedProcedureStepPush"] + assert nevent_type_id == 1 + assert nevent_information.ProcedureStepState == "IN PROGRESS" + + assert events[2].event == evt.EVT_N_EVENT_REPORT + current_event = events[2] + nevent_primitive = current_event.request + model = current_event.request.AffectedSOPClassUID + nevent_type_id = nevent_primitive.EventTypeID + nevent_information = dcmread(nevent_primitive.EventInformation, force=True) + assert model.keyword in ["UnifiedProcedureStepPush"] + assert nevent_type_id == 1 + assert nevent_information.ProcedureStepState == "COMPLETED" + + assert events[3].event == evt.EVT_RELEASED + + # def test_no_peer(self, capfd): + # """Test trying to connect to non-existent host.""" + # p = self.func([DATASET_FILE]) + # p.wait() + # assert p.returncode == 1 + # out, err = capfd.readouterr() + # assert "Association request failed: unable to connect to remote" in err + # assert "TCP Initialisation Error" in err + + + # def test_bad_input(self, capfd): + # """Test being unable to read the input file.""" + # p = self.func(["no-such-file.dcm", "-d"]) + # p.wait() + # assert p.returncode == 0 + + # out, err = capfd.readouterr() + # assert "No suitable DICOM files found" in err + # assert "Cannot access path: no-such-file.dcm" in err + + + +class Testneventsender(neventsenderBase): + """Tests for nevent_sender.py""" + + def setup_method(self): + """Run prior to each test""" + self.ae = None + self.func = start_nevent_sender diff --git a/tdwii_plus_examples/upsdb.py b/tdwii_plus_examples/upsdb.py index 70dd93d..90afc24 100644 --- a/tdwii_plus_examples/upsdb.py +++ b/tdwii_plus_examples/upsdb.py @@ -23,7 +23,7 @@ try: from sqlalchemy import Column, ForeignKey, Integer, String, create_engine except ImportError: - sys.exit("qrscp requires the sqlalchemy package") + sys.exit("upsdb.py requires the sqlalchemy package") from pydicom import dcmread from pydicom.dataset import Dataset diff --git a/tdwii_plus_examples/upsscp.py b/tdwii_plus_examples/upsscp.py index 0adae61..b956c4b 100755 --- a/tdwii_plus_examples/upsscp.py +++ b/tdwii_plus_examples/upsscp.py @@ -8,7 +8,6 @@ import pydicom.config -# from neventscp_handlers import handle_nevent import upsdb from handlers import ( handle_echo, @@ -30,7 +29,6 @@ from pynetdicom.sop_class import Verification from pynetdicom.utils import set_ae -# from pynetdicom.apps.upsscp import db # Use `None` for empty values pydicom.config.use_none_as_empty_text_VR_value = True @@ -91,7 +89,7 @@ def _setup_argparser(): parser = argparse.ArgumentParser( description=( "The upsscp application implements a Service Class Provider (SCP) " - "for the Verification, Storage and Query/Retrieve (QR) Service " + "for the Verification and Unified Procedure Step (UPS) Service " "Classes." ), usage="upsscp [options]", diff --git a/tdwii_plus_examples/watchscu.py b/tdwii_plus_examples/watchscu.py index e9836d1..e349521 100755 --- a/tdwii_plus_examples/watchscu.py +++ b/tdwii_plus_examples/watchscu.py @@ -162,17 +162,17 @@ def _setup_argparser(): "-aec", "--called-aet", metavar="[a]etitle", - help="set called AE title of peer (default: WATCH_SCP)", + help="set called AE title of peer (default: UPSSCP)", type=str, - default="WATCH_SCP", + default="UPSSCP", ) net_opts.add_argument( "-aer", "--receiver-aet", metavar="[a]etitle", - help="set receiver AE title of peer (default: EVENT_SCP)", + help="set receiver AE title of peer (default: NEVENT_RECEIVER)", type=str, - default="EVENT_SCP", + default="NEVENT_RECEIVER" ) net_opts.add_argument( "-ta",