diff OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp @ 5853:4d932683049d get-scu tip

very first implementation of C-Get SCU
author Alain Mazy <am@orthanc.team>
date Tue, 29 Oct 2024 17:25:49 +0100
parents f7adfb22e20e
children
line wrap: on
line diff
--- a/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp	Wed Oct 16 19:34:42 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp	Tue Oct 29 17:25:49 2024 +0100
@@ -234,7 +234,7 @@
 
 
 
-  void DicomControlUserConnection::SetupPresentationContexts()
+  void DicomControlUserConnection::SetupPresentationContexts() // TODO-GET, setup only the presentation contexts that are enabled for that modality
   {
     assert(association_.get() != NULL);
     association_->ProposeGenericPresentationContext(UID_VerificationSOPClass);
@@ -243,6 +243,12 @@
     association_->ProposeGenericPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel);
     association_->ProposeGenericPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel);
     association_->ProposeGenericPresentationContext(UID_FINDModalityWorklistInformationModel);
+    association_->ProposeGenericPresentationContext(UID_GETStudyRootQueryRetrieveInformationModel);
+    association_->ProposeGenericPresentationContext(UID_GETPatientRootQueryRetrieveInformationModel);
+    
+    // for C-GET SCU, in order to receive the C-Store message  TODO-GET: we need to refine this list based on what we know we are going to retrieve
+    association_->ProposeGenericPresentationContext(UID_ComputedRadiographyImageStorage);
+    association_->ProposeGenericPresentationContext(UID_MRImageStorage);
   }
     
 
@@ -445,6 +451,220 @@
   }
     
 
+  void DicomControlUserConnection::Get(const DicomMap& findResult,
+                                       CGetInstanceReceivedCallback instanceReceivedCallback,
+                                       void* callbackContext)
+  {
+    assert(association_.get() != NULL);
+    association_->Open(parameters_);
+
+    // TODO-GET: if findResults is the result of a C-Find, we can use the SopClassUIDs for the negotiation
+
+    std::unique_ptr<ParsedDicomFile> query(
+      ConvertQueryFields(findResult, parameters_.GetRemoteModality().GetManufacturer()));
+    DcmDataset* queryDataset = query->GetDcmtkObject().getDataset();
+
+    std::string remoteAet;
+    std::string remoteIp;
+    std::string calledAet;
+
+    association_->GetAssociationParameters(remoteAet, remoteIp, calledAet);
+
+    const char* sopClass = NULL;
+    const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent();
+    ResourceType level = StringToResourceType(tmp.c_str());
+    switch (level)
+    {
+      case ResourceType_Patient:
+        sopClass = UID_GETPatientRootQueryRetrieveInformationModel;
+        // DU_putStringDOElement(queryDataset, DCM_QueryRetrieveLevel, ResourceTypeToDicomQueryRetrieveLevel(ResourceType_Patient));  // TODO-GET
+        break;
+      case ResourceType_Study:
+        sopClass = UID_GETStudyRootQueryRetrieveInformationModel;
+        // DU_putStringDOElement(queryDataset, DCM_QueryRetrieveLevel, ResourceTypeToDicomQueryRetrieveLevel(ResourceType_Study));  // TODO-GET
+        break;
+      default:
+        throw OrthancException(ErrorCode_InternalError); // TODO-GET: implement series + instances
+    }
+
+    // Figure out which of the accepted presentation contexts should be used
+    int cgetPresID = ASC_findAcceptedPresentationContextID(&association_->GetDcmtkAssociation(), sopClass);
+    if (cgetPresID == 0)
+    {
+      throw OrthancException(ErrorCode_DicomGetUnavailable,
+                             "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle());
+    }
+
+    T_DIMSE_Message msgGetRequest;
+    memset((char*)&msgGetRequest, 0, sizeof(msgGetRequest));
+    msgGetRequest.CommandField = DIMSE_C_GET_RQ;
+
+    T_DIMSE_C_GetRQ* request = &(msgGetRequest.msg.CGetRQ);
+    request->MessageID = association_->GetDcmtkAssociation().nextMsgID++;
+    strncpy(request->AffectedSOPClassUID, sopClass, DIC_UI_LEN);
+    request->Priority = DIMSE_PRIORITY_MEDIUM;
+    request->DataSetType = DIMSE_DATASET_PRESENT;
+
+    {
+      OFString str;
+      CLOG(TRACE, DICOM) << "Sending Get Request:" << std::endl
+                         << DIMSE_dumpMessage(str, *request, DIMSE_OUTGOING, NULL, cgetPresID);
+    }
+    
+    OFCondition cond = DIMSE_sendMessageUsingMemoryData(
+          &(association_->GetDcmtkAssociation()), cgetPresID, &msgGetRequest, NULL /* statusDetail */, queryDataset,
+          NULL /* progress callback TODO-GET */, NULL /* callback context */, NULL /* commandSet */);
+      
+    if (cond.bad())
+    {
+        OFString tempStr;
+        CLOG(TRACE, DICOM) << "Failed sending C-GET request: " << DimseCondition::dump(tempStr, cond);
+        // return cond;
+    }
+
+    // equivalent to handleCGETSession in DCMTK
+    bool continueSession = true;
+
+    // As long we want to continue (usually, as long as we receive more objects,
+    // i.e. the final C-GET response has not arrived yet)
+    while (continueSession)
+    {
+        T_DIMSE_Message rsp;
+        // Make sure everything is zeroed (especially options)
+        memset((char*)&rsp, 0, sizeof(rsp));
+
+        // DcmDataset* statusDetail = NULL;
+        T_ASC_PresentationContextID cmdPresId = 0;
+
+        OFCondition result = DIMSE_receiveCommand(&(association_->GetDcmtkAssociation()),
+                                                  (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                                                  parameters_.GetTimeout(),
+                                                  &cmdPresId,
+                                                  &rsp,
+                                                  NULL /* statusDetail */,
+                                                  NULL /* not interested in the command set */);
+
+        if (result.bad())
+        {
+          OFString tempStr;
+          CLOG(TRACE, DICOM) << "Failed receiving DIMSE command: " << DimseCondition::dump(tempStr, result);
+          // delete statusDetail;
+          break;  // TODO: return value
+        }
+        // Handle C-GET Response
+        if (rsp.CommandField == DIMSE_C_GET_RSP)
+        {
+          {
+            OFString tempStr;
+            CLOG(TRACE, DICOM) << "Received C-GET Response: " << std::endl
+              << DIMSE_dumpMessage(tempStr, rsp, DIMSE_INCOMING, NULL, cmdPresId);
+          }
+
+          // TODO-GET: for progress handler
+          // OFunique_ptr<RetrieveResponse> getRSP(new RetrieveResponse());
+          // getRSP->m_affectedSOPClassUID     = rsp.msg.CGetRSP.AffectedSOPClassUID;
+          // getRSP->m_messageIDRespondedTo    = rsp.msg.CGetRSP.MessageIDBeingRespondedTo;
+          // getRSP->m_status                  = rsp.msg.CGetRSP.DimseStatus;
+          // getRSP->m_numberOfRemainingSubops = rsp.msg.CGetRSP.NumberOfRemainingSubOperations;
+          // getRSP->m_numberOfCompletedSubops = rsp.msg.CGetRSP.NumberOfCompletedSubOperations;
+          // getRSP->m_numberOfFailedSubops    = rsp.msg.CGetRSP.NumberOfFailedSubOperations;
+          // getRSP->m_numberOfWarningSubops   = rsp.msg.CGetRSP.NumberOfWarningSubOperations;
+          // getRSP->m_statusDetail            = statusDetail;
+
+        }
+        // Handle C-STORE Request
+        else if (rsp.CommandField == DIMSE_C_STORE_RQ)
+        {
+          {
+            OFString tempStr;
+            CLOG(TRACE, DICOM) << "Received C-STORE Request: " << std::endl
+              << DIMSE_dumpMessage(tempStr, rsp, DIMSE_INCOMING, NULL, cmdPresId);
+          }
+
+          T_DIMSE_C_StoreRQ* storeRequest = &(rsp.msg.CStoreRQ);
+
+          // Check if dataset is announced correctly
+          if (rsp.msg.CStoreRQ.DataSetType == DIMSE_DATASET_NULL)
+          {
+            CLOG(WARNING, DICOM) << "C-GET SCU handler: Incoming C-STORE with no dataset";
+          }
+
+          Uint16 desiredCStoreReturnStatus = 0;
+          DcmDataset* dataObject = NULL;
+
+          // Receive dataset
+          result = DIMSE_receiveDataSetInMemory(&(association_->GetDcmtkAssociation()),
+                                                  (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                                                  parameters_.GetTimeout(),
+                                                  &cmdPresId,
+                                                  &dataObject,
+                                                  NULL /*callback*/, NULL /*callbackData*/);  // TODO-GET
+
+          if (result.bad())
+          {
+            desiredCStoreReturnStatus = STATUS_STORE_Error_CannotUnderstand;
+            // TODO-GET: return ?
+          }
+          else
+          {
+            // callback the OrthancServer with the received data
+            if (instanceReceivedCallback != NULL)
+            {
+              desiredCStoreReturnStatus = instanceReceivedCallback(callbackContext, *dataObject, remoteAet, remoteIp, calledAet);
+            }
+
+            // send the Store response
+            T_DIMSE_Message storeResponse;
+            memset((char*)&storeResponse, 0, sizeof(storeResponse));
+            storeResponse.CommandField         = DIMSE_C_STORE_RSP;
+
+            T_DIMSE_C_StoreRSP& storeRsp       = storeResponse.msg.CStoreRSP;
+            storeRsp.MessageIDBeingRespondedTo = storeRequest->MessageID;
+            storeRsp.DimseStatus               = desiredCStoreReturnStatus;
+            storeRsp.DataSetType               = DIMSE_DATASET_NULL;
+
+            OFStandard::strlcpy(
+                storeRsp.AffectedSOPClassUID, storeRequest->AffectedSOPClassUID, sizeof(storeRsp.AffectedSOPClassUID));
+            OFStandard::strlcpy(
+                storeRsp.AffectedSOPInstanceUID, storeRequest->AffectedSOPInstanceUID, sizeof(storeRsp.AffectedSOPInstanceUID));
+            storeRsp.opts = O_STORE_AFFECTEDSOPCLASSUID | O_STORE_AFFECTEDSOPINSTANCEUID;
+
+            result = DIMSE_sendMessageUsingMemoryData(&(association_->GetDcmtkAssociation()), 
+                                                      cmdPresId, 
+                                                      &storeResponse, NULL /* statusDetail */, NULL /* dataObject */,
+                                                      NULL /* progress callback TODO-GET */, NULL /* callback context */, NULL /* commandSet */);
+            if (result.bad())
+            {
+              continueSession = false;
+            }
+            else
+            {
+              OFString tempStr;
+              CLOG(TRACE, DICOM) << "Sent C-STORE Response: " << std::endl
+                << DIMSE_dumpMessage(tempStr, storeResponse, DIMSE_OUTGOING, NULL, cmdPresId);
+            }
+          }
+        }
+        // Handle other DIMSE command (error since other command than GET/STORE not expected)
+        else
+        {
+          CLOG(WARNING, DICOM) << "Expected C-GET response or C-STORE request but received DIMSE command 0x"
+                               << std::hex << std::setfill('0') << std::setw(4)
+                               << static_cast<unsigned int>(rsp.CommandField);
+          
+          result          = DIMSE_BADCOMMANDTYPE;
+          continueSession = false;
+        }
+
+        // delete statusDetail; // should be NULL if not existing or added to response list
+        // statusDetail = NULL;
+    }
+    /* All responses received or break signal occurred */
+
+    // return result;
+}
+
+
   DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params) :
     parameters_(params),
     association_(new DicomAssociation)