# HG changeset patch # User Alain Mazy # Date 1655802401 -7200 # Node ID 6fd815fae50f7f94dc4df7a79e19ff33c05e9a5d # Parent c2ebc47f4f184b54db66d4e0f876bc4f96a54561# Parent 1ff06e0ea532f603c5f006197072585b76f8dc2e merge diff -r c2ebc47f4f18 -r 6fd815fae50f NEWS --- a/NEWS Mon Jun 20 16:53:21 2022 +0200 +++ b/NEWS Tue Jun 21 11:06:41 2022 +0200 @@ -23,6 +23,7 @@ * API version upgraded to 18 * /system is now reporting "DatabaseServerIdentifier" +* Added an Asynchronous mode to /modalities/../move. Version 1.11.0 (2022-05-09) diff -r c2ebc47f4f18 -r 6fd815fae50f OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp --- a/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp Mon Jun 20 16:53:21 2022 +0200 +++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp Tue Jun 21 11:06:41 2022 +0200 @@ -362,27 +362,7 @@ DcmDataset* dataset = query->GetDcmtkObject().getDataset(); const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel; - switch (level) - { - case ResourceType_Patient: - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT"); - break; - - case ResourceType_Study: - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY"); - break; - - case ResourceType_Series: - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES"); - break; - - case ResourceType_Instance: - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE"); - break; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } + DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, ResourceTypeToDicomQueryRetrieveLevel(level)); // Figure out which of the accepted presentation contexts should be used int presID = ASC_findAcceptedPresentationContextID(&association_->GetDcmtkAssociation(), sopClass); @@ -519,32 +499,26 @@ DcmDataset* dataset = query->GetDcmtkObject().getDataset(); assert(dataset != NULL); - const char* clevel = NULL; + const char* clevel = ResourceTypeToDicomQueryRetrieveLevel(level); const char* sopClass = NULL; + DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, clevel); + switch (level) { case ResourceType_Patient: - clevel = "PATIENT"; - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT"); sopClass = UID_FINDPatientRootQueryRetrieveInformationModel; break; case ResourceType_Study: - clevel = "STUDY"; - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY"); sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; break; case ResourceType_Series: - clevel = "SERIES"; - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES"); sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; break; case ResourceType_Instance: - clevel = "IMAGE"; - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE"); sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; break; diff -r c2ebc47f4f18 -r 6fd815fae50f OrthancFramework/Sources/Enumerations.cpp --- a/OrthancFramework/Sources/Enumerations.cpp Mon Jun 20 16:53:21 2022 +0200 +++ b/OrthancFramework/Sources/Enumerations.cpp Tue Jun 21 11:06:41 2022 +0200 @@ -1314,6 +1314,27 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } + const char* ResourceTypeToDicomQueryRetrieveLevel(ResourceType type) + { + if (type == ResourceType_Patient) + { + return "PATIENT"; + } + else if (type == ResourceType_Study) + { + return "STUDY"; + } + else if (type == ResourceType_Series) + { + return "SERIES"; + } + else if (type == ResourceType_Instance) + { + return "IMAGE"; + } + + throw OrthancException(ErrorCode_ParameterOutOfRange); + } ImageFormat StringToImageFormat(const char* format) { diff -r c2ebc47f4f18 -r 6fd815fae50f OrthancFramework/Sources/Enumerations.h --- a/OrthancFramework/Sources/Enumerations.h Mon Jun 20 16:53:21 2022 +0200 +++ b/OrthancFramework/Sources/Enumerations.h Tue Jun 21 11:06:41 2022 +0200 @@ -862,6 +862,9 @@ bool IsResourceLevelAboveOrEqual(ResourceType level, ResourceType reference); +ORTHANC_PUBLIC + const char* ResourceTypeToDicomQueryRetrieveLevel(ResourceType type); + ORTHANC_PUBLIC DicomModule GetModule(ResourceType type); diff -r c2ebc47f4f18 -r 6fd815fae50f OrthancServer/Sources/LuaScripting.cpp --- a/OrthancServer/Sources/LuaScripting.cpp Mon Jun 20 16:53:21 2022 +0200 +++ b/OrthancServer/Sources/LuaScripting.cpp Tue Jun 21 11:06:41 2022 +0200 @@ -34,6 +34,8 @@ #include +static const char* ON_HEART_BEAT = "OnHeartBeat"; + namespace Orthanc { @@ -785,11 +787,11 @@ void LuaScripting::HeartBeatThread(LuaScripting* that) { - static const boost::posix_time::time_duration PERIODICITY = + static const unsigned int GRANULARITY = 100; // In milliseconds + + const boost::posix_time::time_duration PERIODICITY = boost::posix_time::seconds(that->heartBeatPeriod_); - unsigned int sleepDelay = 100; - boost::posix_time::ptime next = boost::posix_time::microsec_clock::universal_time() + PERIODICITY; @@ -797,25 +799,26 @@ while (!shouldStop) { - boost::this_thread::sleep(boost::posix_time::milliseconds(sleepDelay)); + boost::this_thread::sleep(boost::posix_time::milliseconds(GRANULARITY)); if (boost::posix_time::microsec_clock::universal_time() >= next) { LuaScripting::Lock lock(*that); - if (lock.GetLua().IsExistingFunction("OnHeartBeat")) + if (lock.GetLua().IsExistingFunction(ON_HEART_BEAT)) { - LuaFunctionCall call(lock.GetLua(), "OnHeartBeat"); + LuaFunctionCall call(lock.GetLua(), ON_HEART_BEAT); call.Execute(); } next = boost::posix_time::microsec_clock::universal_time() + PERIODICITY; } - boost::recursive_mutex::scoped_lock lock(that->mutex_); - shouldStop = that->state_ == State_Done; + { + boost::recursive_mutex::scoped_lock lock(that->mutex_); + shouldStop = (that->state_ == State_Done); + } } - } void LuaScripting::EventThread(LuaScripting* that) @@ -865,9 +868,9 @@ LOG(INFO) << "Starting the Lua engine"; eventThread_ = boost::thread(EventThread, this); - LuaScripting::Lock lock(*this); + LuaScripting::Lock luaLock(*this); - if (heartBeatPeriod_ > 0 && lock.GetLua().IsExistingFunction("OnHeartBeat")) + if (heartBeatPeriod_ > 0 && luaLock.GetLua().IsExistingFunction(ON_HEART_BEAT)) { LOG(INFO) << "Starting the Lua HeartBeat thread with a period of " << heartBeatPeriod_ << " seconds"; heartBeatThread_ = boost::thread(HeartBeatThread, this); diff -r c2ebc47f4f18 -r 6fd815fae50f OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp Mon Jun 20 16:53:21 2022 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp Tue Jun 21 11:06:41 2022 +0200 @@ -1517,6 +1517,7 @@ { if (call.IsDocumentation()) { + OrthancRestApi::DocumentSubmitCommandsJob(call); call.GetDocumentation() .SetTag("Networking") .SetSummary("Trigger C-MOVE SCU") @@ -1534,12 +1535,12 @@ "Target AET that will be used by the remote DICOM modality as a target for its C-STORE SCU " "commands, defaults to `DicomAet` configuration option in order to do a simple query/retrieve", false) .SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number, - "Timeout for the C-STORE command, in seconds", false) + "Timeout for the C-MOVE command, in seconds", false) .SetUriArgument("id", "Identifier of the modality of interest"); return; } - const ServerContext& context = OrthancRestApi::GetContext(call); + ServerContext& context = OrthancRestApi::GetContext(call); Json::Value request; @@ -1564,21 +1565,33 @@ const RemoteModalityParameters source = MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - DicomAssociationParameters params(localAet, source); - InjectAssociationTimeout(params, request); // Handles KEY_TIMEOUT + std::unique_ptr job(new DicomMoveScuJob(context)); - DicomControlUserConnection connection(params); + job->SetQueryFormat(DicomToJsonFormat_Short); + + // QueryAccessor query(call); + job->SetTargetAet(targetAet); + job->SetLocalAet(localAet); + job->SetRemoteModality(source); + + if (request.isMember(KEY_TIMEOUT)) + { + job->SetTimeout(SerializationToolbox::ReadUnsignedInteger(request, KEY_TIMEOUT)); + } for (Json::Value::ArrayIndex i = 0; i < request[KEY_RESOURCES].size(); i++) { DicomMap resource; FromDcmtkBridge::FromJson(resource, request[KEY_RESOURCES][i], "Resources elements"); - - connection.Move(targetAet, level, resource); + + resource.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, std::string(ResourceTypeToDicomQueryRetrieveLevel(level)), false); + + job->AddQuery(resource); } - // Move has succeeded - call.GetOutput().AnswerBuffer("{}", MimeType_Json); + OrthancRestApi::GetApi(call).SubmitCommandsJob + (call, job.release(), true /* synchronous by default */, request); + return; } diff -r c2ebc47f4f18 -r 6fd815fae50f OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp --- a/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp Mon Jun 20 16:53:21 2022 +0200 +++ b/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp Tue Jun 21 11:06:41 2022 +0200 @@ -107,7 +107,8 @@ query.GetAnswer(query.GetSize() - 1).Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET); } - + // this method is used to implement the retrieve part of a Q&R + // it keeps only the main dicom tags from the C-Find answer void DicomMoveScuJob::AddFindAnswer(const DicomMap& answer) { DicomMap item; @@ -122,6 +123,13 @@ AddCommand(new Command(*this, answer)); } + // this method is used to implement a C-Move + // it keeps all tags from the C-Move query + void DicomMoveScuJob::AddQuery(const DicomMap& query) + { + AddToQuery(query_, query); + AddCommand(new Command(*this, query)); + } void DicomMoveScuJob::AddFindAnswer(QueryRetrieveHandler& query, size_t i) diff -r c2ebc47f4f18 -r 6fd815fae50f OrthancServer/Sources/ServerJobs/DicomMoveScuJob.h --- a/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.h Mon Jun 20 16:53:21 2022 +0200 +++ b/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.h Tue Jun 21 11:06:41 2022 +0200 @@ -51,7 +51,7 @@ public: explicit DicomMoveScuJob(ServerContext& context) : context_(context), - query_(true /* this is for worklists */), + query_(false /* this is not for worklists */), queryFormat_(DicomToJsonFormat_Short) { } @@ -61,6 +61,8 @@ void AddFindAnswer(const DicomMap& answer); + void AddQuery(const DicomMap& query); + void AddFindAnswer(QueryRetrieveHandler& query, size_t i); diff -r c2ebc47f4f18 -r 6fd815fae50f TODO --- a/TODO Mon Jun 20 16:53:21 2022 +0200 +++ b/TODO Tue Jun 21 11:06:41 2022 +0200 @@ -108,8 +108,6 @@ * Support "/preview" and "/matlab" for LUT color images * Try to transcode files if a simple decoding fails: https://groups.google.com/g/orthanc-users/c/b8168-NkAhA/m/Df3j-CO9CgAJ -* (1) Add asynchronous mode in "/modalitities/.../move" for C-MOVE SCU: - https://groups.google.com/g/orthanc-users/c/G3_jBy4X4NQ/m/8BanTsdMBQAJ * (2) Ranges of DICOM tags for "Keep" and "Remove" in ".../modify" and ".../anonymize": https://groups.google.com/g/orthanc-users/c/6dETktKo9v8/m/b0LUvSfwAgAJ * return error code/reason in HTTP response if resubmit/cancel/pause fails ...