Class: DICOM::Link
Overview
This class handles the construction and interpretation of network packages as well as network communication.
Instance Attribute Summary collapse
-
#file_handler ⇒ Object
A customized FileHandler class to use instead of the default FileHandler included with Ruby DICOM.
-
#max_package_size ⇒ Object
The maximum allowed size of network packages (in bytes).
-
#presentation_contexts ⇒ Object
A hash which keeps track of the relationship between context ID and chosen transfer syntax.
-
#session ⇒ Object
readonly
A TCP network session where the DICOM communication is done with a remote host or client.
Instance Method Summary collapse
-
#await_release ⇒ Object
Waits for an SCU to issue a release request, and answers it by launching the handle_release method.
-
#build_association_abort ⇒ Object
Builds the abort message which is transmitted when the server wishes to (abruptly) abort the connection.
-
#build_association_accept(info) ⇒ Object
Builds the binary string which is sent as the association accept (in response to an association request).
-
#build_association_reject(info) ⇒ Object
Builds the binary string which is sent as the association reject (in response to an association request).
-
#build_association_request(presentation_contexts, user_info) ⇒ Object
Builds the binary string which is sent as the association request.
-
#build_command_fragment(pdu, context, flags, command_elements) ⇒ Object
Builds the binary string which is sent as a command fragment.
-
#build_data_fragment(data_elements, presentation_context_id) ⇒ Object
Builds the binary string which is sent as a data fragment.
-
#build_release_request ⇒ Object
Builds the binary string which is sent as the release request.
-
#build_release_response ⇒ Object
Builds the binary string which is sent as the release response (which follows a release request).
-
#build_storage_fragment(pdu, context, flags, body) ⇒ Object
Builds the binary string which makes up a C-STORE data fragment.
-
#forward_to_interpret(message, pdu, file = nil) ⇒ Object
Delegates an incoming message to its appropriate interpreter method, based on its pdu type.
-
#handle_abort(default_message = true) ⇒ Object
Handles the abortion of a session, when a non-valid or unexpected message has been received.
-
#handle_association_accept(info) ⇒ Object
Handles the outgoing association accept message.
-
#handle_incoming_data(path) ⇒ Object
Processes incoming command & data fragments for the DServer.
-
#handle_rejection ⇒ Object
Handles the rejection message (The response used to an association request when its formalities are not correct).
-
#handle_release ⇒ Object
Handles the release message (which is the response to a release request).
-
#handle_response ⇒ Object
Handles the command fragment response.
-
#initialize(options = {}) ⇒ Link
constructor
Creates a Link instance, which is used by both DClient and DServer to handle network communication.
-
#interpret(message, file = nil) ⇒ Object
Decodes the header of an incoming message, analyzes its real length versus expected length, and handles any deviations to make sure that message strings are split up appropriately before they are being forwarded to interpretation.
-
#interpret_abort(message) ⇒ Object
Decodes the message received when the remote node wishes to abort the session.
-
#interpret_association_accept(message) ⇒ Object
Decodes the message received in the association response, and interprets its content.
-
#interpret_association_reject(message) ⇒ Object
Decodes the association reject message and extracts the error reasons given.
-
#interpret_association_request(message) ⇒ Object
Decodes the binary string received in the association request, and interprets its content.
-
#interpret_command_and_data(message, file = nil) ⇒ Object
Decodes the received command/data fragment message, and interprets its content.
-
#interpret_release_request(message) ⇒ Object
Decodes the message received in the release request and calls the handle_release method.
-
#interpret_release_response(message) ⇒ Object
Decodes the message received in the release response and closes the connection.
-
#receive_multiple_transmissions(file = nil) ⇒ Object
Handles the reception of multiple incoming transmissions.
-
#receive_single_transmission ⇒ Object
Handles the reception of a single, expected incoming transmission and returns the interpreted, received data.
-
#set_session(session) ⇒ Object
Sets the session of this Link instance (used when this session is already established externally).
-
#start_session(adress, port) ⇒ Object
Establishes a new session with a remote network node.
-
#stop_session ⇒ Object
Ends the current session by closing the connection.
-
#transmit ⇒ Object
Sends the outgoing message (encoded binary string) to the remote node.
Methods included from Logging
Constructor Details
#initialize(options = {}) ⇒ Link
Creates a Link instance, which is used by both DClient and DServer to handle network communication.
Parameters
-
options
– A hash of parameters.
Options
-
:ae
– String. The name of the client (application entity). -
:file_handler
– A customized FileHandler class to use instead of the default FileHandler. -
:host_ae
– String. The name of the server (application entity). -
:max_package_size
– Fixnum. The maximum allowed size of network packages (in bytes). -
:timeout
– Fixnum. The maximum period to wait for an answer before aborting the communication.
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/dicom/link.rb', line 32 def initialize(={}) require 'socket' # Optional parameters (and default values): @file_handler = [:file_handler] || FileHandler @ae = [:ae] || "RUBY_DICOM" @host_ae = [:host_ae] || "DEFAULT" @max_package_size = [:max_package_size] || 32768 # 16384 @max_receive_size = @max_package_size @timeout = [:timeout] || 10 # seconds @min_length = 10 # minimum number of bytes to expect in an incoming transmission # Variables used for monitoring state of transmission: @session = nil # TCP connection @association = nil # DICOM Association status @request_approved = nil # Status of our DICOM request @release = nil # Status of received, valid release response @command_request = Hash.new @presentation_contexts = Hash.new # Keeps track of the relationship between pc id and it's transfer syntax set_default_values set_user_information_array @outgoing = Stream.new(string=nil, endian=true) end |
Instance Attribute Details
#file_handler ⇒ Object
A customized FileHandler class to use instead of the default FileHandler included with Ruby DICOM.
10 11 12 |
# File 'lib/dicom/link.rb', line 10 def file_handler @file_handler end |
#max_package_size ⇒ Object
The maximum allowed size of network packages (in bytes).
12 13 14 |
# File 'lib/dicom/link.rb', line 12 def max_package_size @max_package_size end |
#presentation_contexts ⇒ Object
A hash which keeps track of the relationship between context ID and chosen transfer syntax.
14 15 16 |
# File 'lib/dicom/link.rb', line 14 def presentation_contexts @presentation_contexts end |
#session ⇒ Object (readonly)
A TCP network session where the DICOM communication is done with a remote host or client.
16 17 18 |
# File 'lib/dicom/link.rb', line 16 def session @session end |
Instance Method Details
#await_release ⇒ Object
Waits for an SCU to issue a release request, and answers it by launching the handle_release method. If invalid or no message is received, the connection is closed.
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/dicom/link.rb', line 57 def await_release segments = receive_single_transmission info = segments.first if info[:pdu] != PDU_RELEASE_REQUEST # For some reason we didn't get our expected release request. Determine why: if info[:valid] logger.error("Unexpected message type received (PDU: #{info[:pdu]}). Expected a release request. Closing the connection.") handle_abort(false) else logger.error("Timed out while waiting for a release request. Closing the connection.") end stop_session else # Properly release the association: handle_release end end |
#build_association_abort ⇒ Object
Builds the abort message which is transmitted when the server wishes to (abruptly) abort the connection.
Restrictions
For now, no reasons for the abortion are provided (and source of problems will always be set as client side).
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/dicom/link.rb', line 81 def build_association_abort # Big endian encoding: @outgoing.endian = @net_endian # Clear the outgoing binary string: @outgoing.reset # Reserved (2 bytes) @outgoing.encode_last("00"*2, "HEX") # Source (1 byte) source = "00" # (client side error) @outgoing.encode_last(source, "HEX") # Reason/Diag. (1 byte) reason = "00" # (Reason not specified) @outgoing.encode_last(reason, "HEX") append_header(PDU_ABORT) end |
#build_association_accept(info) ⇒ Object
Builds the binary string which is sent as the association accept (in response to an association request).
Parameters
-
info
– The association information hash.
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
# File 'lib/dicom/link.rb', line 103 def build_association_accept(info) # Big endian encoding: @outgoing.endian = @net_endian # Clear the outgoing binary string: @outgoing.reset # No abstract syntax in association response. To make this work with the method that # encodes the presentation context, we pass on a one-element array containing nil). abstract_syntaxes = Array.new(1, nil) # Note: The order of which these components are built is not arbitrary. append_application_context # Reset the presentation context instance variable: @presentation_contexts = Hash.new # Create the presentation context hash object that will be passed to the builder method: p_contexts = Hash.new # Build the presentation context strings, one by one: info[:pc].each do |pc| @presentation_contexts[pc[:presentation_context_id]] = pc[:selected_transfer_syntax] # Add the information from this pc item to the p_contexts hash: p_contexts[pc[:abstract_syntax]] = Hash.new unless p_contexts[pc[:abstract_syntax]] p_contexts[pc[:abstract_syntax]][pc[:presentation_context_id]] = {:transfer_syntaxes => [pc[:selected_transfer_syntax]], :result => pc[:result]} end append_presentation_contexts(p_contexts, ITEM_PRESENTATION_CONTEXT_RESPONSE) append_user_information(@user_information) # Header must be built last, because we need to know the length of the other components. append_association_header(PDU_ASSOCIATION_ACCEPT, info[:called_ae]) end |
#build_association_reject(info) ⇒ Object
Builds the binary string which is sent as the association reject (in response to an association request).
Parameters
-
info
– The association information hash.
Restrictions
-
For now, this method will only customize the “reason” value.
-
For a list of error codes, see the DICOM standard, PS3.8 Chapter 9.3.4, Table 9-21.
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/dicom/link.rb', line 141 def build_association_reject(info) # Big endian encoding: @outgoing.endian = @net_endian # Clear the outgoing binary string: @outgoing.reset # Reserved (1 byte) @outgoing.encode_last("00", "HEX") # Result (1 byte) @outgoing.encode_last("01", "HEX") # 1 for permament, 2 for transient # Source (1 byte) # (1: Service user, 2: Service provider (ACSE related function), 3: Service provider (Presentation related function) @outgoing.encode_last("01", "HEX") # Reason (1 byte) reason = info[:reason] @outgoing.encode_last(reason, "HEX") append_header(PDU_ASSOCIATION_REJECT) end |
#build_association_request(presentation_contexts, user_info) ⇒ Object
Builds the binary string which is sent as the association request.
Parameters
-
presentation_contexts
– A hash containing abstract_syntaxes, presentation context ids and transfer syntaxes. -
user_info
– A user information items array.
166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/dicom/link.rb', line 166 def build_association_request(presentation_contexts, user_info) # Big endian encoding: @outgoing.endian = @net_endian # Clear the outgoing binary string: @outgoing.reset # Note: The order of which these components are built is not arbitrary. # (The first three are built 'in order of appearance', the header is built last, but is put first in the message) append_application_context append_presentation_contexts(presentation_contexts, ITEM_PRESENTATION_CONTEXT_REQUEST, request=true) append_user_information(user_info) # Header must be built last, because we need to know the length of the other components. append_association_header(PDU_ASSOCIATION_REQUEST, @host_ae) end |
#build_command_fragment(pdu, context, flags, command_elements) ⇒ Object
Builds the binary string which is sent as a command fragment.
Parameters
-
pdu
– The command fragment’s PDU string. -
context
– Presentation context ID byte (references a presentation context from the association). -
flags
– The flag string, which identifies if this is the last command fragment or not. -
command_elements
– An array of command elements.
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'lib/dicom/link.rb', line 189 def build_command_fragment(pdu, context, flags, command_elements) # Little endian encoding: @outgoing.endian = @data_endian # Clear the outgoing binary string: @outgoing.reset # Build the last part first, the Command items: command_elements.each do |element| # Tag (4 bytes) @outgoing.add_last(@outgoing.encode_tag(element[0])) # Encode the value first, so we know its length: value = @outgoing.encode_value(element[2], element[1]) # Length (2 bytes) @outgoing.encode_last(value.length, "US") # Reserved (2 bytes) @outgoing.encode_last("0000", "HEX") # Value (variable length) @outgoing.add_last(value) end # The rest of the command fragment will be buildt in reverse, all the time # putting the elements first in the outgoing binary string. # Group length item: # Value (4 bytes) @outgoing.encode_first(@outgoing.string.length, "UL") # Reserved (2 bytes) @outgoing.encode_first("0000", "HEX") # Length (2 bytes) @outgoing.encode_first(4, "US") # Tag (4 bytes) @outgoing.add_first(@outgoing.encode_tag("0000,0000")) # Big endian encoding from now on: @outgoing.endian = @net_endian # Flags (1 byte) @outgoing.encode_first(flags, "HEX") # Presentation context ID (1 byte) @outgoing.encode_first(context, "BY") # Length (of remaining data) (4 bytes) @outgoing.encode_first(@outgoing.string.length, "UL") # PRESENTATION DATA VALUE (the above) append_header(pdu) end |
#build_data_fragment(data_elements, presentation_context_id) ⇒ Object
Builds the binary string which is sent as a data fragment.
Notes
-
The style of encoding will depend on whether we have an implicit or explicit transfer syntax.
Parameters
-
data_elements
– An array of data elements. -
presentation_context_id
– Presentation context ID byte (references a presentation context from the association).
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
# File 'lib/dicom/link.rb', line 241 def build_data_fragment(data_elements, presentation_context_id) # Set the transfer syntax to be used for encoding the data fragment: set_transfer_syntax(@presentation_contexts[presentation_context_id]) # Endianness of data fragment: @outgoing.endian = @data_endian # Clear the outgoing binary string: @outgoing.reset # Build the last part first, the Data items: data_elements.each do |element| # Encode all tags (even tags which are empty): # Tag (4 bytes) @outgoing.add_last(@outgoing.encode_tag(element[0])) # Encode the value in advance of putting it into the message, so we know its length: vr = LIBRARY.element(element[0]).vr value = @outgoing.encode_value(element[1], vr) if @explicit # Type (VR) (2 bytes) @outgoing.encode_last(vr, "STR") # Length (2 bytes) @outgoing.encode_last(value.length, "US") else # Implicit: # Length (4 bytes) @outgoing.encode_last(value.length, "UL") end # Value (variable length) @outgoing.add_last(value) end # The rest of the data fragment will be built in reverse, all the time # putting the elements first in the outgoing binary string. # Big endian encoding from now on: @outgoing.endian = @net_endian # Flags (1 byte) @outgoing.encode_first("02", "HEX") # Data, last fragment (identifier) # Presentation context ID (1 byte) @outgoing.encode_first(presentation_context_id, "BY") # Length (of remaining data) (4 bytes) @outgoing.encode_first(@outgoing.string.length, "UL") # PRESENTATION DATA VALUE (the above) append_header(PDU_DATA) end |
#build_release_request ⇒ Object
Builds the binary string which is sent as the release request.
285 286 287 288 289 290 291 292 293 |
# File 'lib/dicom/link.rb', line 285 def build_release_request # Big endian encoding: @outgoing.endian = @net_endian # Clear the outgoing binary string: @outgoing.reset # Reserved (4 bytes) @outgoing.encode_last("00"*4, "HEX") append_header(PDU_RELEASE_REQUEST) end |
#build_release_response ⇒ Object
Builds the binary string which is sent as the release response (which follows a release request).
297 298 299 300 301 302 303 304 305 |
# File 'lib/dicom/link.rb', line 297 def build_release_response # Big endian encoding: @outgoing.endian = @net_endian # Clear the outgoing binary string: @outgoing.reset # Reserved (4 bytes) @outgoing.encode_last("00000000", "HEX") append_header(PDU_RELEASE_RESPONSE) end |
#build_storage_fragment(pdu, context, flags, body) ⇒ Object
Builds the binary string which makes up a C-STORE data fragment.
Parameters
-
pdu
– The data fragment’s PDU string. -
context
– Presentation context ID byte (references a presentation context from the association). -
flags
– The flag string, which identifies if this is the last data fragment or not. -
body
– A pre-encoded binary string (typicall a segment of a DICOM file to be transmitted).
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 |
# File 'lib/dicom/link.rb', line 316 def build_storage_fragment(pdu, context, flags, body) # Big endian encoding: @outgoing.endian = @net_endian # Clear the outgoing binary string: @outgoing.reset # Build in reverse, putting elements in front of the binary string: # Insert the data (body): @outgoing.add_last(body) # Flags (1 byte) @outgoing.encode_first(flags, "HEX") # Context ID (1 byte) @outgoing.encode_first(context, "BY") # PDV Length (of remaining data) (4 bytes) @outgoing.encode_first(@outgoing.string.length, "UL") # PRESENTATION DATA VALUE (the above) append_header(pdu) end |
#forward_to_interpret(message, pdu, file = nil) ⇒ Object
Delegates an incoming message to its appropriate interpreter method, based on its pdu type. Returns the interpreted information hash.
Parameters
-
message
– The binary message string. -
pdu
– The PDU string of the message. -
file
– A boolean used to inform whether an incoming data fragment is part of a DICOM file reception or not.
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 |
# File 'lib/dicom/link.rb', line 343 def forward_to_interpret(, pdu, file=nil) case pdu when PDU_ASSOCIATION_REQUEST info = interpret_association_request() when PDU_ASSOCIATION_ACCEPT info = interpret_association_accept() when PDU_ASSOCIATION_REJECT info = interpret_association_reject() when PDU_DATA info = interpret_command_and_data(, file) when PDU_RELEASE_REQUEST info = interpret_release_request() when PDU_RELEASE_RESPONSE info = interpret_release_response() when PDU_ABORT info = interpret_abort() else info = {:valid => false} logger.error("An unknown PDU type was received in the incoming transmission. Can not decode this message. (PDU: #{pdu})") end return info end |
#handle_abort(default_message = true) ⇒ Object
Handles the abortion of a session, when a non-valid or unexpected message has been received.
Parameters
-
default_message
– A boolean which unless set as nil/false will make the method print the default status message.
372 373 374 375 376 |
# File 'lib/dicom/link.rb', line 372 def handle_abort(=true) logger.warn("An unregonizable (non-DICOM) message was received.") if build_association_abort transmit end |
#handle_association_accept(info) ⇒ Object
Handles the outgoing association accept message.
Parameters
-
info
– The association information hash.
384 385 386 387 388 389 390 391 |
# File 'lib/dicom/link.rb', line 384 def handle_association_accept(info) # Update the variable for calling ae (information gathered in the association request): @ae = info[:calling_ae] # Build message string and send it: set_user_information_array(info) build_association_accept(info) transmit end |
#handle_incoming_data(path) ⇒ Object
Processes incoming command & data fragments for the DServer. Returns a success boolean and an array of status messages.
Notes
The incoming traffic will in most cases be: A C-STORE-RQ (command fragment) followed by a bunch of data fragments. However, it may also be a C-ECHO-RQ command fragment, which is used to test connections.
Parameters
-
path
– The path used to save incoming DICOM files.
– FIXME: The code which handles incoming data isnt quite satisfactory. It would probably be wise to rewrite it at some stage to clean up the code somewhat. Probably a better handling of command requests (and their corresponding data fragments) would be a good idea.
409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 |
# File 'lib/dicom/link.rb', line 409 def handle_incoming_data(path) # Wait for incoming data: segments = receive_multiple_transmissions(file=true) # Reset command results arrays: @command_results = Array.new @data_results = Array.new file_transfer_syntaxes = Array.new files = Array.new single_file_data = Array.new # Proceed to extract data from the captured segments: segments.each do |info| if info[:valid] # Determine if it is command or data: if info[:presentation_context_flag] == DATA_MORE_FRAGMENTS @data_results << info[:results] single_file_data << info[:bin] elsif info[:presentation_context_flag] == DATA_LAST_FRAGMENT @data_results << info[:results] single_file_data << info[:bin] # Join the recorded data binary strings together to make a DICOM file binary string and put it in our files Array: files << single_file_data.join single_file_data = Array.new elsif info[:presentation_context_flag] == COMMAND_LAST_FRAGMENT @command_results << info[:results] @presentation_context_id = info[:presentation_context_id] # Does this actually do anything useful? file_transfer_syntaxes << @presentation_contexts[info[:presentation_context_id]] end end end # Process the received files using the customizable FileHandler class: success, = @file_handler.receive_files(path, files, file_transfer_syntaxes) return success, end |
#handle_rejection ⇒ Object
Handles the rejection message (The response used to an association request when its formalities are not correct).
445 446 447 448 449 450 451 452 |
# File 'lib/dicom/link.rb', line 445 def handle_rejection logger.warn("An incoming association request was rejected. Error code: #{association_error}") # Insert the error code in the info hash: info[:reason] = association_error # Send an association rejection: build_association_reject(info) transmit end |
#handle_release ⇒ Object
Handles the release message (which is the response to a release request).
456 457 458 459 460 461 462 |
# File 'lib/dicom/link.rb', line 456 def handle_release stop_receiving logger.info("Received a release request. Releasing association.") build_release_response transmit stop_session end |
#handle_response ⇒ Object
Handles the command fragment response.
Notes
This is usually a C-STORE-RSP which follows the (successful) reception of a DICOM file, but may also be a C-ECHO-RSP in response to an echo request.
471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 |
# File 'lib/dicom/link.rb', line 471 def handle_response # Need to construct the command elements array: command_elements = Array.new # SOP Class UID: command_elements << ["0000,0002", "UI", @command_request["0000,0002"]] # Command Field: command_elements << ["0000,0100", "US", command_field_response(@command_request["0000,0100"])] # Message ID Being Responded To: command_elements << ["0000,0120", "US", @command_request["0000,0110"]] # Data Set Type: command_elements << ["0000,0800", "US", NO_DATA_SET_PRESENT] # Status: command_elements << ["0000,0900", "US", SUCCESS] # Affected SOP Instance UID: command_elements << ["0000,1000", "UI", @command_request["0000,1000"]] if @command_request["0000,1000"] build_command_fragment(PDU_DATA, @presentation_context_id, COMMAND_LAST_FRAGMENT, command_elements) transmit end |
#interpret(message, file = nil) ⇒ Object
Decodes the header of an incoming message, analyzes its real length versus expected length, and handles any deviations to make sure that message strings are split up appropriately before they are being forwarded to interpretation. Returns an array of information hashes.
Parameters
-
message
– The binary message string. -
file
– A boolean used to inform whether an incoming data fragment is part of a DICOM file reception or not.
– FIXME: This method is rather complex and doesnt feature the best readability. A rewrite that is able to simplify it would be lovely.
502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 |
# File 'lib/dicom/link.rb', line 502 def interpret(, file=nil) if @first_part = @first_part + @first_part = nil end segments = Array.new # If the message is at least 8 bytes we can start decoding it: if .length > 8 # Create a new Stream instance to handle this response. msg = Stream.new(, @net_endian) # PDU type ( 1 byte) pdu = msg.decode(1, "HEX") # Reserved (1 byte) msg.skip(1) # Length of remaining data (4 bytes) specified_length = msg.decode(4, "UL") # Analyze the remaining length of the message versurs the specified_length value: if msg.rest_length > specified_length # If the remaining length of the string itself is bigger than this specified_length value, # then it seems that we have another message appended in our incoming transmission. fragment = msg.extract(specified_length) info = forward_to_interpret(fragment, pdu, file) info[:pdu] = pdu segments << info # It is possible that a fragment contains both a command and a data fragment. If so, we need to make sure we collect all the information: if info[:rest_string] additional_info = forward_to_interpret(info[:rest_string], pdu, file) segments << additional_info end # The information gathered from the interpretation is appended to a segments array, # and in the case of a recursive call some special logic is needed to build this array in the expected fashion. remaining_segments = interpret(msg.rest_string, file) remaining_segments.each do |remaining| segments << remaining end elsif msg.rest_length == specified_length # Proceed to analyze the rest of the message: fragment = msg.extract(specified_length) info = forward_to_interpret(fragment, pdu, file) info[:pdu] = pdu segments << info # It is possible that a fragment contains both a command and a data fragment. If so, we need to make sure we collect all the information: if info[:rest_string] additional_info = forward_to_interpret(info[:rest_string], pdu, file) segments << additional_info end else # Length of the message is less than what is specified in the message. Need to listen for more. This is hopefully handled properly now. #logger.error("Error. The length of the received message (#{msg.rest_length}) is smaller than what it claims (#{specified_length}). Aborting.") @first_part = msg.string end else # Assume that this is only the start of the message, and add it to the next incoming string: @first_part = end return segments end |
#interpret_abort(message) ⇒ Object
Decodes the message received when the remote node wishes to abort the session. Returns the processed information hash.
Parameters
-
message
– The binary message string.
567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 |
# File 'lib/dicom/link.rb', line 567 def interpret_abort() info = Hash.new msg = Stream.new(, @net_endian) # Reserved (2 bytes) reserved_bytes = msg.skip(2) # Source (1 byte) info[:source] = msg.decode(1, "HEX") # Reason/Diag. (1 byte) info[:reason] = msg.decode(1, "HEX") # Analyse the results: process_source(info[:source]) process_reason(info[:reason]) stop_receiving @abort = true info[:valid] = true return info end |
#interpret_association_accept(message) ⇒ Object
Decodes the message received in the association response, and interprets its content. Returns the processed information hash.
Parameters
-
message
– The binary message string.
592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 |
# File 'lib/dicom/link.rb', line 592 def interpret_association_accept() info = Hash.new msg = Stream.new(, @net_endian) # Protocol version (2 bytes) info[:protocol_version] = msg.decode(2, "HEX") # Reserved (2 bytes) msg.skip(2) # Called AE (shall be identical to the one sent in the request, but not tested against) (16 bytes) info[:called_ae] = msg.decode(16, "STR") # Calling AE (shall be identical to the one sent in the request, but not tested against) (16 bytes) info[:calling_ae] = msg.decode(16, "STR") # Reserved (32 bytes) msg.skip(32) # APPLICATION CONTEXT: # Item type (1 byte) info[:application_item_type] = msg.decode(1, "HEX") # Reserved (1 byte) msg.skip(1) # Application item length (2 bytes) info[:application_item_length] = msg.decode(2, "US") # Application context (variable length) info[:application_context] = msg.decode(info[:application_item_length], "STR") # PRESENTATION CONTEXT: # As multiple presentation contexts may occur, we need a loop to catch them all: # Each presentation context hash will be put in an array, which will be put in the info hash. presentation_contexts = Array.new pc_loop = true while pc_loop do # Item type (1 byte) item_type = msg.decode(1, "HEX") if item_type == ITEM_PRESENTATION_CONTEXT_RESPONSE pc = Hash.new pc[:presentation_item_type] = item_type # Reserved (1 byte) msg.skip(1) # Presentation item length (2 bytes) pc[:presentation_item_length] = msg.decode(2, "US") # Presentation context ID (1 byte) pc[:presentation_context_id] = msg.decode(1, "BY") # Reserved (1 byte) msg.skip(1) # Result (& Reason) (1 byte) pc[:result] = msg.decode(1, "BY") process_result(pc[:result]) # Reserved (1 byte) msg.skip(1) # Transfer syntax sub-item: # Item type (1 byte) pc[:transfer_syntax_item_type] = msg.decode(1, "HEX") # Reserved (1 byte) msg.skip(1) # Transfer syntax item length (2 bytes) pc[:transfer_syntax_item_length] = msg.decode(2, "US") # Transfer syntax name (variable length) pc[:transfer_syntax] = msg.decode(pc[:transfer_syntax_item_length], "STR") presentation_contexts << pc else # Break the presentation context loop, as we have probably reached the next stage, which is user info. Rewind: msg.skip(-1) pc_loop = false end end info[:pc] = presentation_contexts # USER INFORMATION: # Item type (1 byte) info[:user_info_item_type] = msg.decode(1, "HEX") # Reserved (1 byte) msg.skip(1) # User information item length (2 bytes) info[:user_info_item_length] = msg.decode(2, "US") while msg.index < msg.length do # Item type (1 byte) item_type = msg.decode(1, "HEX") # Reserved (1 byte) msg.skip(1) # Item length (2 bytes) item_length = msg.decode(2, "US") case item_type when ITEM_MAX_LENGTH info[:max_pdu_length] = msg.decode(item_length, "UL") @max_receive_size = info[:max_pdu_length] when ITEM_IMPLEMENTATION_UID info[:implementation_class_uid] = msg.decode(item_length, "STR") when ITEM_MAX_OPERATIONS_INVOKED # Asynchronous operations window negotiation (PS 3.7: D.3.3.3) (2*2 bytes) info[:maxnum_operations_invoked] = msg.decode(2, "US") info[:maxnum_operations_performed] = msg.decode(2, "US") when ITEM_ROLE_NEGOTIATION # SCP/SCU Role Selection Negotiation (PS 3.7 D.3.3.4) # Note: An association response may contain several instances of this item type (each with a different abstract syntax). uid_length = msg.decode(2, "US") role = Hash.new # SOP Class UID (Abstract syntax): role[:sop_uid] = msg.decode(uid_length, "STR") # SCU Role (1 byte): role[:scu] = msg.decode(1, "BY") # SCP Role (1 byte): role[:scp] = msg.decode(1, "BY") if info[:role_negotiation] info[:role_negotiation] << role else info[:role_negotiation] = [role] end when ITEM_IMPLEMENTATION_VERSION info[:implementation_version] = msg.decode(item_length, "STR") else # Value (variable length) value = msg.decode(item_length, "STR") logger.warn("Unknown user info item type received. Please update source code or contact author. (item type: #{item_type})") end end stop_receiving info[:valid] = true return info end |
#interpret_association_reject(message) ⇒ Object
Decodes the association reject message and extracts the error reasons given. Returns the processed information hash.
Parameters
-
message
– The binary message string.
715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 |
# File 'lib/dicom/link.rb', line 715 def interpret_association_reject() info = Hash.new msg = Stream.new(, @net_endian) # Reserved (1 byte) msg.skip(1) # Result (1 byte) info[:result] = msg.decode(1, "BY") # 1 for permanent and 2 for transient rejection # Source (1 byte) info[:source] = msg.decode(1, "BY") # Reason (1 byte) info[:reason] = msg.decode(1, "BY") logger.warn("ASSOCIATE Request was rejected by the host. Error codes: Result: #{info[:result]}, Source: #{info[:source]}, Reason: #{info[:reason]} (See DICOM PS3.8: Table 9-21 for details.)") stop_receiving info[:valid] = true return info end |
#interpret_association_request(message) ⇒ Object
Decodes the binary string received in the association request, and interprets its content. Returns the processed information hash.
Parameters
-
message
– The binary message string.
739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 |
# File 'lib/dicom/link.rb', line 739 def interpret_association_request() info = Hash.new msg = Stream.new(, @net_endian) # Protocol version (2 bytes) info[:protocol_version] = msg.decode(2, "HEX") # Reserved (2 bytes) msg.skip(2) # Called AE (shall be returned in the association response) (16 bytes) info[:called_ae] = msg.decode(16, "STR") # Calling AE (shall be returned in the association response) (16 bytes) info[:calling_ae] = msg.decode(16, "STR") # Reserved (32 bytes) msg.skip(32) # APPLICATION CONTEXT: # Item type (1 byte) info[:application_item_type] = msg.decode(1, "HEX") # 10H # Reserved (1 byte) msg.skip(1) # Application item length (2 bytes) info[:application_item_length] = msg.decode(2, "US") # Application context (variable length) info[:application_context] = msg.decode(info[:application_item_length], "STR") # PRESENTATION CONTEXT: # As multiple presentation contexts may occur, we need a loop to catch them all: # Each presentation context hash will be put in an array, which will be put in the info hash. presentation_contexts = Array.new pc_loop = true while pc_loop do # Item type (1 byte) item_type = msg.decode(1, "HEX") if item_type == ITEM_PRESENTATION_CONTEXT_REQUEST pc = Hash.new pc[:presentation_item_type] = item_type # Reserved (1 byte) msg.skip(1) # Presentation context item length (2 bytes) pc[:presentation_item_length] = msg.decode(2, "US") # Presentation context id (1 byte) pc[:presentation_context_id] = msg.decode(1, "BY") # Reserved (3 bytes) msg.skip(3) presentation_contexts << pc # A presentation context contains an abstract syntax and one or more transfer syntaxes. # ABSTRACT SYNTAX SUB-ITEM: # Abstract syntax item type (1 byte) pc[:abstract_syntax_item_type] = msg.decode(1, "HEX") # Reserved (1 byte) msg.skip(1) # Abstract syntax item length (2 bytes) pc[:abstract_syntax_item_length] = msg.decode(2, "US") # Abstract syntax (variable length) pc[:abstract_syntax] = msg.decode(pc[:abstract_syntax_item_length], "STR") ## TRANSFER SYNTAX SUB-ITEM(S): # As multiple transfer syntaxes may occur, we need a loop to catch them all: # Each transfer syntax hash will be put in an array, which will be put in the presentation context hash. transfer_syntaxes = Array.new ts_loop = true while ts_loop do # Item type (1 byte) item_type = msg.decode(1, "HEX") if item_type == ITEM_TRANSFER_SYNTAX ts = Hash.new ts[:transfer_syntax_item_type] = item_type # Reserved (1 byte) msg.skip(1) # Transfer syntax item length (2 bytes) ts[:transfer_syntax_item_length] = msg.decode(2, "US") # Transfer syntax name (variable length) ts[:transfer_syntax] = msg.decode(ts[:transfer_syntax_item_length], "STR") transfer_syntaxes << ts else # Break the transfer syntax loop, as we have probably reached the next stage, # which is either user info or a new presentation context entry. Rewind: msg.skip(-1) ts_loop = false end end pc[:ts] = transfer_syntaxes else # Break the presentation context loop, as we have probably reached the next stage, which is user info. Rewind: msg.skip(-1) pc_loop = false end end info[:pc] = presentation_contexts # USER INFORMATION: # Item type (1 byte) info[:user_info_item_type] = msg.decode(1, "HEX") # Reserved (1 byte) msg.skip(1) # User information item length (2 bytes) info[:user_info_item_length] = msg.decode(2, "US") # User data (variable length): while msg.index < msg.length do # Item type (1 byte) item_type = msg.decode(1, "HEX") # Reserved (1 byte) msg.skip(1) # Item length (2 bytes) item_length = msg.decode(2, "US") case item_type when ITEM_MAX_LENGTH info[:max_pdu_length] = msg.decode(item_length, "UL") when ITEM_IMPLEMENTATION_UID info[:implementation_class_uid] = msg.decode(item_length, "STR") when ITEM_MAX_OPERATIONS_INVOKED # Asynchronous operations window negotiation (PS 3.7: D.3.3.3) (2*2 bytes) info[:maxnum_operations_invoked] = msg.decode(2, "US") info[:maxnum_operations_performed] = msg.decode(2, "US") when ITEM_ROLE_NEGOTIATION # SCP/SCU Role Selection Negotiation (PS 3.7 D.3.3.4) # Note: An association request may contain several instances of this item type (each with a different abstract syntax). uid_length = msg.decode(2, "US") role = Hash.new # SOP Class UID (Abstract syntax): role[:sop_uid] = msg.decode(uid_length, "STR") # SCU Role (1 byte): role[:scu] = msg.decode(1, "BY") # SCP Role (1 byte): role[:scp] = msg.decode(1, "BY") if info[:role_negotiation] info[:role_negotiation] << role else info[:role_negotiation] = [role] end when ITEM_IMPLEMENTATION_VERSION info[:implementation_version] = msg.decode(item_length, "STR") else # Unknown item type: # Value (variable length) value = msg.decode(item_length, "STR") logger.warn("Unknown user info item type received. Please update source code or contact author. (item type: " + item_type + ")") end end stop_receiving info[:valid] = true return info end |
#interpret_command_and_data(message, file = nil) ⇒ Object
Decodes the received command/data fragment message, and interprets its content. Returns the processed information hash.
Notes
-
Decoding of a data fragment depends on the explicitness of the transmission.
Parameters
-
message
– The binary message string. -
file
– A boolean used to inform whether an incoming data fragment is part of a DICOM file reception or not.
890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 |
# File 'lib/dicom/link.rb', line 890 def interpret_command_and_data(, file=nil) info = Hash.new msg = Stream.new(, @net_endian) # Length (of remaining PDV data) (4 bytes) info[:presentation_data_value_length] = msg.decode(4, "UL") # Calculate the last index position of this message element: last_index = info[:presentation_data_value_length] + msg.index # Presentation context ID (1 byte) info[:presentation_context_id] = msg.decode(1, "BY") @presentation_context_id = info[:presentation_context_id] # Flags (1 byte) info[:presentation_context_flag] = msg.decode(1, "HEX") # "03" for command (last fragment), "02" for data # Apply the proper transfer syntax for this presentation context: set_transfer_syntax(@presentation_contexts[info[:presentation_context_id]]) # "Data endian" encoding from now on: msg.endian = @data_endian # We will put the results in a hash: results = Hash.new if info[:presentation_context_flag] == COMMAND_LAST_FRAGMENT # COMMAND, LAST FRAGMENT: while msg.index < last_index do # Tag (4 bytes) tag = msg.decode_tag # Length (2 bytes) length = msg.decode(2, "US") if length > msg.rest_length logger.error("Specified length of command element value exceeds remaining length of the received message! Something is wrong.") end # Reserved (2 bytes) msg.skip(2) # VR (from library - not the stream): vr = LIBRARY.element(tag).vr # Value (variable length) value = msg.decode(length, vr) # Put tag and value in a hash: results[tag] = value end # The results hash is put in an array along with (possibly) other results: info[:results] = results # Store the results in an instance variable (to be used later when sending a receipt for received data): @command_request = results # Check if the command fragment indicates that this was the last of the response fragments for this query: status = results["0000,0900"] if status # Note: This method will also stop the packet receiver if indicated by the status mesasge. process_status(status) end # Special case: Handle a possible C-ECHO-RQ: if info[:results]["0000,0100"] == C_ECHO_RQ logger.info("Received an Echo request. Returning an Echo response.") handle_response end elsif info[:presentation_context_flag] == DATA_MORE_FRAGMENTS or info[:presentation_context_flag] == DATA_LAST_FRAGMENT # DATA FRAGMENT: # If this is a file transmission, we will delay the decoding for later: if file # Just store the binary string: info[:bin] = msg.rest_string # If this was the last data fragment of a C-STORE, we need to send a receipt: # (However, for, say a C-FIND-RSP, which indicates the end of the query results, this method shall not be called) (Command Field (0000,0100) holds information on this) handle_response if info[:presentation_context_flag] == DATA_LAST_FRAGMENT else # Decode data elements: while msg.index < last_index do # Tag (4 bytes) tag = msg.decode_tag if @explicit # Type (VR) (2 bytes): type = msg.decode(2, "STR") # Length (2 bytes) length = msg.decode(2, "US") else # Implicit: type = nil # (needs to be defined as nil here or it will take the value from the previous step in the loop) # Length (4 bytes) length = msg.decode(4, "UL") end if length > msg.rest_length logger.error("The specified length of the data element value exceeds the remaining length of the received message!") end # Fetch type (if not defined already) for this data element: type = LIBRARY.element(tag).vr unless type # Value (variable length) value = msg.decode(length, type) # Put tag and value in a hash: results[tag] = value end # The results hash is put in an array along with (possibly) other results: info[:results] = results end else # Unknown. logger.error("Unknown presentation context flag received in the query/command response. (#{info[:presentation_context_flag]})") stop_receiving end # If only parts of the string was read, return the rest: info[:rest_string] = msg.rest_string if last_index < msg.length info[:valid] = true return info end |
#interpret_release_request(message) ⇒ Object
Decodes the message received in the release request and calls the handle_release method. Returns the processed information hash.
Parameters
-
message
– The binary message string.
998 999 1000 1001 1002 1003 1004 1005 1006 |
# File 'lib/dicom/link.rb', line 998 def interpret_release_request() info = Hash.new msg = Stream.new(, @net_endian) # Reserved (4 bytes) reserved_bytes = msg.decode(4, "HEX") handle_release info[:valid] = true return info end |
#interpret_release_response(message) ⇒ Object
Decodes the message received in the release response and closes the connection. Returns the processed information hash.
Parameters
-
message
– The binary message string.
1015 1016 1017 1018 1019 1020 1021 1022 1023 |
# File 'lib/dicom/link.rb', line 1015 def interpret_release_response() info = Hash.new msg = Stream.new(, @net_endian) # Reserved (4 bytes) reserved_bytes = msg.decode(4, "HEX") stop_receiving info[:valid] = true return info end |
#receive_multiple_transmissions(file = nil) ⇒ Object
Handles the reception of multiple incoming transmissions. Returns an array of interpreted message information hashes.
Parameters
-
file
– A boolean used to inform whether an incoming data fragment is part of a DICOM file reception or not.
1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 |
# File 'lib/dicom/link.rb', line 1032 def receive_multiple_transmissions(file=nil) # FIXME: The code which waits for incoming network packets seems to be very CPU intensive. # Perhaps there is a more elegant way to wait for incoming messages? # @listen = true segments = Array.new while @listen # Receive data and append the current data to our segments array, which will be returned. data = receive_transmission(@min_length) current_segments = interpret(data, file) if current_segments current_segments.each do |cs| segments << cs end end end segments << {:valid => false} unless segments return segments end |
#receive_single_transmission ⇒ Object
Handles the reception of a single, expected incoming transmission and returns the interpreted, received data.
1054 1055 1056 1057 1058 1059 1060 |
# File 'lib/dicom/link.rb', line 1054 def receive_single_transmission min_length = 8 data = receive_transmission(min_length) segments = interpret(data) segments << {:valid => false} unless segments.length > 0 return segments end |
#set_session(session) ⇒ Object
Sets the session of this Link instance (used when this session is already established externally).
Parameters
-
session
– A TCP network connection that has been established with a remote node.
1068 1069 1070 |
# File 'lib/dicom/link.rb', line 1068 def set_session(session) @session = session end |
#start_session(adress, port) ⇒ Object
Establishes a new session with a remote network node.
Parameters
-
adress
– String. The adress (IP) of the remote node. -
port
– Fixnum. The network port to be used in the network communication.
1079 1080 1081 |
# File 'lib/dicom/link.rb', line 1079 def start_session(adress, port) @session = TCPSocket.new(adress, port) end |
#stop_session ⇒ Object
Ends the current session by closing the connection.
1085 1086 1087 |
# File 'lib/dicom/link.rb', line 1085 def stop_session @session.close unless @session.closed? end |
#transmit ⇒ Object
Sends the outgoing message (encoded binary string) to the remote node.
1091 1092 1093 |
# File 'lib/dicom/link.rb', line 1091 def transmit @session.send(@outgoing.string, 0) end |