dicom 0.7 → 0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +55 -0
- data/README +51 -29
- data/init.rb +1 -0
- data/lib/dicom.rb +35 -21
- data/lib/dicom/{Anonymizer.rb → anonymizer.rb} +178 -80
- data/lib/dicom/constants.rb +121 -0
- data/lib/dicom/d_client.rb +888 -0
- data/lib/dicom/d_library.rb +208 -0
- data/lib/dicom/d_object.rb +424 -0
- data/lib/dicom/d_read.rb +433 -0
- data/lib/dicom/d_server.rb +397 -0
- data/lib/dicom/d_write.rb +420 -0
- data/lib/dicom/data_element.rb +175 -0
- data/lib/dicom/{Dictionary.rb → dictionary.rb} +390 -398
- data/lib/dicom/elements.rb +82 -0
- data/lib/dicom/file_handler.rb +116 -0
- data/lib/dicom/item.rb +87 -0
- data/lib/dicom/{Link.rb → link.rb} +749 -388
- data/lib/dicom/ruby_extensions.rb +44 -35
- data/lib/dicom/sequence.rb +62 -0
- data/lib/dicom/stream.rb +493 -0
- data/lib/dicom/super_item.rb +696 -0
- data/lib/dicom/super_parent.rb +615 -0
- metadata +25 -18
- data/DOCUMENTATION +0 -469
- data/lib/dicom/DClient.rb +0 -584
- data/lib/dicom/DLibrary.rb +0 -194
- data/lib/dicom/DObject.rb +0 -1579
- data/lib/dicom/DRead.rb +0 -532
- data/lib/dicom/DServer.rb +0 -304
- data/lib/dicom/DWrite.rb +0 -410
- data/lib/dicom/FileHandler.rb +0 -50
- data/lib/dicom/Stream.rb +0 -354
@@ -0,0 +1,82 @@
|
|
1
|
+
# Copyright 2010 Christoffer Lervag
|
2
|
+
|
3
|
+
module DICOM
|
4
|
+
|
5
|
+
# The Elements mix-in module contains methods that are common among the different element classes:
|
6
|
+
# * DataElement
|
7
|
+
# * Item
|
8
|
+
# * Sequence
|
9
|
+
#
|
10
|
+
module Elements
|
11
|
+
|
12
|
+
# The encoded, binary value of the element (String).
|
13
|
+
attr_reader :bin
|
14
|
+
# The element's length (Fixnum).
|
15
|
+
attr_reader :length
|
16
|
+
# The element's name (String).
|
17
|
+
attr_reader :name
|
18
|
+
# The parent of this element (which may be an Item, Sequence or DObject).
|
19
|
+
attr_reader :parent
|
20
|
+
# The elementss tag (String).
|
21
|
+
attr_reader :tag
|
22
|
+
# The element's value representation (String).
|
23
|
+
attr_reader :vr
|
24
|
+
|
25
|
+
# Retrieves the entire chain of parents connected to this element.
|
26
|
+
# The parents are returned in an array, where the first element is the
|
27
|
+
# immediate parent and the last element is the top parent.
|
28
|
+
# Returns an empty array if no parent is defined.
|
29
|
+
#
|
30
|
+
def parents
|
31
|
+
all_parents = Array.new
|
32
|
+
# Extract all parents and add to array recursively:
|
33
|
+
if parent
|
34
|
+
all_parents = parent.parents if parent.parent
|
35
|
+
all_parents.insert(0, parent)
|
36
|
+
end
|
37
|
+
return all_parents
|
38
|
+
end
|
39
|
+
|
40
|
+
# Sets a specified element as this element's parent.
|
41
|
+
#
|
42
|
+
# === Parameters
|
43
|
+
#
|
44
|
+
# * <tt>new_parent</tt> -- A parent object (which can be either a DObject, Item or Sequence instance).
|
45
|
+
#
|
46
|
+
# === Examples
|
47
|
+
#
|
48
|
+
# # Create a new Sequence and connect it to a DObject instance:
|
49
|
+
# structure_set_roi = Sequence.new("3006,0020")
|
50
|
+
# structure_set_roi.parent = obj
|
51
|
+
#
|
52
|
+
def parent=(new_parent)
|
53
|
+
# Remove ourselves from the previous parent (if any) first:
|
54
|
+
# Don't do this if parent is set as nil (by the remove method), or we'll get an endless loop!
|
55
|
+
if self.parent
|
56
|
+
self.parent.remove(self.tag) if new_parent and self.parent != new_parent
|
57
|
+
end
|
58
|
+
# Set the new parent (should we bother to test for parent validity here?):
|
59
|
+
@parent = new_parent
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns the top parent of a particular element.
|
63
|
+
#
|
64
|
+
# === Notes
|
65
|
+
#
|
66
|
+
# Unless an element, or one of its parent elements, are independent, the top parent will be a DObject instance.
|
67
|
+
#
|
68
|
+
def top_parent
|
69
|
+
# The top parent is determined recursively:
|
70
|
+
if parent
|
71
|
+
if parent.is_a?(DObject)
|
72
|
+
return parent
|
73
|
+
else
|
74
|
+
return parent.top_parent
|
75
|
+
end
|
76
|
+
else
|
77
|
+
return self
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# Copyright 2010 Christoffer Lervag
|
2
|
+
#
|
3
|
+
# The purpose of this file is to make it as easy as possible for users to customize the way
|
4
|
+
# DICOM files are handled when they are received through the network.
|
5
|
+
#
|
6
|
+
# The default behaviour is to save the files to disk using a folder structure determined by a few select tags of the DICOM file.
|
7
|
+
#
|
8
|
+
# Some suggested alternatives for user customization:
|
9
|
+
# * Analyzing tags and/or image data to determine further actions.
|
10
|
+
# * Modify the DICOM object before it is saved to disk.
|
11
|
+
# * Modify the folder structure in which DICOM files are saved to disk.
|
12
|
+
# * Store DICOM contents in a database (highly relevant if you are building a Ruby on Rails DICOM application).
|
13
|
+
# * Retransmit the DICOM object to another network destination using the DClient class.
|
14
|
+
# * Write information to a log file.
|
15
|
+
|
16
|
+
module DICOM
|
17
|
+
|
18
|
+
# This class handles DICOM files that have been received through network communication.
|
19
|
+
#
|
20
|
+
class FileHandler
|
21
|
+
|
22
|
+
# Saves a single DICOM object to file.
|
23
|
+
# Returns a status message stating where the file has been saved.
|
24
|
+
#
|
25
|
+
# Modify this method if you want to change the way your server saves incoming files.
|
26
|
+
#
|
27
|
+
# === Notes
|
28
|
+
#
|
29
|
+
# As default, files will be saved with the following path:
|
30
|
+
# <tt> path_prefix/<PatientID>/<StudyDate>/<Modality>/ </tt>
|
31
|
+
#
|
32
|
+
# === Parameters
|
33
|
+
#
|
34
|
+
# * <tt>path_prefix</tt> -- String. Specifies the root path of the DICOM storage.
|
35
|
+
# * <tt>obj</tt> -- A DObject instance which will be written to file.
|
36
|
+
# * <tt>transfer_syntax</tt> -- String. Specifies the transfer syntax that will be used to write the DICOM file.
|
37
|
+
#
|
38
|
+
def self.save_file(path_prefix, obj, transfer_syntax)
|
39
|
+
# File name is set using the SOP Instance UID:
|
40
|
+
file_name = obj.value("0008,0018") || "missing_SOP_UID.dcm"
|
41
|
+
folders = Array.new(3)
|
42
|
+
folders[0] = obj.value("0010,0020") || "PatientID"
|
43
|
+
folders[1] = obj.value("0008,0020") || "StudyDate"
|
44
|
+
folders[2] = obj.value("0008,0060") || "Modality"
|
45
|
+
local_path = folders.join(File::SEPARATOR) + File::SEPARATOR + file_name
|
46
|
+
full_path = path_prefix + local_path
|
47
|
+
# Save the DICOM object to disk:
|
48
|
+
obj.write(full_path, :transfer_syntax => transfer_syntax)
|
49
|
+
message = "DICOM file saved to: #{full_path}"
|
50
|
+
return message
|
51
|
+
end
|
52
|
+
|
53
|
+
# Handles the reception of a series of DICOM objects which are received in a single association.
|
54
|
+
#
|
55
|
+
# Modify this method if you want to change the way your server handles incoming file series.
|
56
|
+
#
|
57
|
+
# === Notes
|
58
|
+
#
|
59
|
+
# Default action: Pass each file to the class method which saves files to disk.
|
60
|
+
#
|
61
|
+
# === Parameters
|
62
|
+
#
|
63
|
+
# * <tt>path</tt> -- String. Specifies the root path of the DICOM storage.
|
64
|
+
# * <tt>objects</tt> -- An array containing the DObject instances which were received.
|
65
|
+
# * <tt>transfer_syntaxes</tt> -- An array containing the transfer syntaxes belonging to the received objects.
|
66
|
+
#
|
67
|
+
def self.receive_files(path, objects, transfer_syntaxes)
|
68
|
+
all_success = true
|
69
|
+
successful, too_short, parse_fail, handle_fail = 0, 0, 0, 0
|
70
|
+
total = objects.length
|
71
|
+
message = nil
|
72
|
+
messages = Array.new
|
73
|
+
# Process each DICOM object:
|
74
|
+
objects.each_index do |i|
|
75
|
+
if objects[i].length > 8
|
76
|
+
# Parse the received data stream and load it as a DICOM object:
|
77
|
+
obj = DObject.new(objects[i], :bin => true, :syntax => transfer_syntaxes[i])
|
78
|
+
if obj.read_success
|
79
|
+
begin
|
80
|
+
message = self.save_file(path, obj, transfer_syntaxes[i])
|
81
|
+
successful += 1
|
82
|
+
rescue
|
83
|
+
handle_fail += 1
|
84
|
+
all_success = false
|
85
|
+
messages << "Error: Saving file failed!"
|
86
|
+
end
|
87
|
+
else
|
88
|
+
parse_fail += 1
|
89
|
+
all_success = false
|
90
|
+
messages << "Error: Invalid DICOM data encountered: The DICOM data string could not be parsed successfully."
|
91
|
+
end
|
92
|
+
else
|
93
|
+
too_short += 1
|
94
|
+
all_success = false
|
95
|
+
messages << "Error: Invalid data encountered: The data was too small to be a valid DICOM file."
|
96
|
+
end
|
97
|
+
end
|
98
|
+
# Create a summary status message, when multiple files have been received:
|
99
|
+
if total > 1
|
100
|
+
if successful == total
|
101
|
+
messages << "All #{total} DICOM files received successfully."
|
102
|
+
else
|
103
|
+
if successful == 0
|
104
|
+
messages << "All #{total} received DICOM files failed!"
|
105
|
+
else
|
106
|
+
messages << "Only #{successful} of #{total} DICOM files received successfully!"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
else
|
110
|
+
messages = [message] if all_success
|
111
|
+
end
|
112
|
+
return all_success, messages
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
data/lib/dicom/item.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# Copyright 2010 Christoffer Lervag
|
2
|
+
|
3
|
+
module DICOM
|
4
|
+
|
5
|
+
# The Item class handles information related to items - the elements contained in sequences.
|
6
|
+
#
|
7
|
+
class Item < SuperItem
|
8
|
+
|
9
|
+
# Include the Elements mix-in module:
|
10
|
+
include Elements
|
11
|
+
|
12
|
+
# The index of this Item in the group of items belonging to its parent. If the Item is without parent, index is nil.
|
13
|
+
attr_accessor :index
|
14
|
+
|
15
|
+
# Creates an Item instance.
|
16
|
+
#
|
17
|
+
# === Notes
|
18
|
+
#
|
19
|
+
# Normally, an Item contains data elements and/or sequences. However, in some cases, an Item will instead/also
|
20
|
+
# carry binary string data, like the pixel data of an encapsulated image fragment.
|
21
|
+
#
|
22
|
+
# === Parameters
|
23
|
+
#
|
24
|
+
# * <tt>options</tt> -- A hash of parameters.
|
25
|
+
#
|
26
|
+
# === Options
|
27
|
+
#
|
28
|
+
# * <tt>:bin</tt> -- A binary string to be carried by the Item.
|
29
|
+
# * <tt>:index</tt> -- Fixnum. If the Item is to be inserted at a specific index (Item number), this option parameter needs to set.
|
30
|
+
# * <tt>:length</tt> -- Fixnum. The Item length (which either refers to the length of the encoded string of children of this Item, or the length of its binary data).
|
31
|
+
# * <tt>:name</tt> - String. The name of the Item may be specified upon creation. If it is not, a default name is chosen.
|
32
|
+
# * <tt>:parent</tt> - Sequence or DObject instance which the Item instance shall belong to.
|
33
|
+
# * <tt>:vr</tt> -- String. The value representation of the Item may be specified upon creation. If it is not, a default vr is chosen.
|
34
|
+
#
|
35
|
+
# === Examples
|
36
|
+
#
|
37
|
+
# # Create an empty Item and connect it to the "Structure Set ROI Sequence":
|
38
|
+
# item = Item.new(:parent => obj["3006,0020"])
|
39
|
+
# # Create a "Pixel Data Item" which carries an encapsulated image frame (a pre-encoded binary):
|
40
|
+
# pixel_item = Item.new(:bin => processed_pixel_data, :parent => obj["7FE0,0010"][1])
|
41
|
+
#
|
42
|
+
def initialize(options={})
|
43
|
+
# Set common parent variables:
|
44
|
+
initialize_parent
|
45
|
+
# Set instance variables:
|
46
|
+
@tag = ITEM_TAG
|
47
|
+
@value = nil
|
48
|
+
@name = options[:name] || "Item"
|
49
|
+
@vr = options[:vr] || ITEM_VR
|
50
|
+
@bin = options[:bin]
|
51
|
+
@length = options[:length]
|
52
|
+
@length = -1 unless options[:length] or options[:bin]
|
53
|
+
if options[:parent]
|
54
|
+
@parent = options[:parent]
|
55
|
+
@index = options[:index] if options[:index]
|
56
|
+
@parent.add_item(self, :index => options[:index])
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Sets the binary string that the Item will contain.
|
61
|
+
#
|
62
|
+
# === Parameters
|
63
|
+
#
|
64
|
+
# * <tt>new_bin</tt> -- A binary string of encoded data.
|
65
|
+
#
|
66
|
+
# === Examples
|
67
|
+
#
|
68
|
+
# # Insert a custom jpeg in the (encapsulated) pixel data element, in it's first pixel data item:
|
69
|
+
# obj["7FE0,0010"][1].children.first.bin = jpeg_binary_string
|
70
|
+
#
|
71
|
+
def bin=(new_bin)
|
72
|
+
if new_bin.is_a?(String)
|
73
|
+
# Add an empty byte at the end if the length of the binary is odd:
|
74
|
+
if new_bin.length[0] == 1
|
75
|
+
@bin = new_bin + "\x00"
|
76
|
+
else
|
77
|
+
@bin = new_bin
|
78
|
+
end
|
79
|
+
@value = nil
|
80
|
+
@length = @bin.length
|
81
|
+
else
|
82
|
+
raise "Invalid parameter type. String was expected, got #{new_bin.class}."
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
@@ -2,14 +2,40 @@
|
|
2
2
|
|
3
3
|
module DICOM
|
4
4
|
|
5
|
-
# This class handles the construction and interpretation of network packages
|
6
|
-
#
|
5
|
+
# This class handles the construction and interpretation of network packages as well as network communication.
|
6
|
+
#
|
7
7
|
class Link
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
# A customized FileHandler class to use instead of the default FileHandler included with Ruby DICOM.
|
10
|
+
attr_accessor :file_handler
|
11
|
+
# The maximum allowed size of network packages (in bytes).
|
12
|
+
attr_accessor :max_package_size
|
13
|
+
# A hash which keeps track of the relationship between context ID and chosen transfer syntax.
|
14
|
+
attr_accessor :presentation_contexts
|
15
|
+
# A boolean which defines if notices/warnings/errors will be printed to the screen (true) or not (false).
|
16
|
+
attr_accessor :verbose
|
17
|
+
# An array containing any error messages recorded.
|
18
|
+
attr_reader :errors
|
19
|
+
# An array containing any status messages recorded.
|
20
|
+
attr_reader :notices
|
21
|
+
# A TCP network session where the DICOM communication is done with a remote host or client.
|
22
|
+
attr_reader :session
|
23
|
+
|
24
|
+
# Creates a Link instance, which is used by both DClient and DServer to handle network communication.
|
25
|
+
#
|
26
|
+
# === Parameters
|
27
|
+
#
|
28
|
+
# * <tt>options</tt> -- A hash of parameters.
|
29
|
+
#
|
30
|
+
# === Options
|
31
|
+
#
|
32
|
+
# * <tt>:ae</tt> -- String. The name of the client (application entity).
|
33
|
+
# * <tt>:file_handler</tt> -- A customized FileHandler class to use instead of the default FileHandler.
|
34
|
+
# * <tt>:host_ae</tt> -- String. The name of the server (application entity).
|
35
|
+
# * <tt>:max_package_size</tt> -- Fixnum. The maximum allowed size of network packages (in bytes).
|
36
|
+
# * <tt>:timeout</tt> -- Fixnum. The maximum period to wait for an answer before aborting the communication.
|
37
|
+
# * <tt>:verbose</tt> -- Boolean. If set to false, the DLink instance will run silently and not output warnings and error messages to the screen. Defaults to true.
|
38
|
+
#
|
13
39
|
def initialize(options={})
|
14
40
|
require 'socket'
|
15
41
|
# Optional parameters (and default values):
|
@@ -19,31 +45,56 @@ module DICOM
|
|
19
45
|
@max_package_size = options[:max_package_size] || 32768 # 16384
|
20
46
|
@max_receive_size = @max_package_size
|
21
47
|
@timeout = options[:timeout] || 10 # seconds
|
22
|
-
@min_length =
|
48
|
+
@min_length = 10 # minimum number of bytes to expect in an incoming transmission
|
23
49
|
@verbose = options[:verbose]
|
24
50
|
@verbose = true if @verbose == nil # Default verbosity is 'on'.
|
25
51
|
# Other instance variables:
|
26
52
|
@errors = Array.new # errors and warnings are put in this array
|
27
53
|
@notices = Array.new # information on successful transmissions are put in this array
|
28
54
|
# Variables used for monitoring state of transmission:
|
29
|
-
@
|
55
|
+
@session = nil # TCP connection
|
30
56
|
@association = nil # DICOM Association status
|
31
57
|
@request_approved = nil # Status of our DICOM request
|
32
58
|
@release = nil # Status of received, valid release response
|
59
|
+
@command_request = Hash.new
|
60
|
+
@presentation_contexts = Hash.new # Keeps track of the relationship between pc id and it's transfer syntax
|
33
61
|
set_default_values
|
34
62
|
set_user_information_array
|
35
|
-
@outgoing = Stream.new(nil, true
|
63
|
+
@outgoing = Stream.new(string=nil, endian=true)
|
36
64
|
end
|
37
65
|
|
66
|
+
# Waits for an SCU to issue a release request, and answers it by launching the handle_release method.
|
67
|
+
# If invalid or no message is received, the connection is closed.
|
68
|
+
#
|
69
|
+
def await_release
|
70
|
+
segments = receive_single_transmission
|
71
|
+
info = segments.first
|
72
|
+
if info[:pdu] != PDU_RELEASE_REQUEST
|
73
|
+
# For some reason we didnt get our expected release request. Determine why:
|
74
|
+
if info[:valid]
|
75
|
+
add_error("Unexpected message type received (PDU: #{info[:pdu]}). Expected a release request. Closing the connection.")
|
76
|
+
handle_abort(false)
|
77
|
+
else
|
78
|
+
add_error("Timed out while waiting for a release request. Closing the connection.")
|
79
|
+
end
|
80
|
+
stop_session
|
81
|
+
else
|
82
|
+
# Properly release the association:
|
83
|
+
handle_release
|
84
|
+
end
|
85
|
+
end
|
38
86
|
|
39
|
-
#
|
40
|
-
#
|
87
|
+
# Builds the abort message which is transmitted when the server wishes to (abruptly) abort the connection.
|
88
|
+
#
|
89
|
+
# === Restrictions
|
90
|
+
#
|
91
|
+
# For now, no reasons for the abortion are provided (and source of problems will always be set as client side).
|
92
|
+
#
|
41
93
|
def build_association_abort
|
42
94
|
# Big endian encoding:
|
43
|
-
@outgoing.
|
95
|
+
@outgoing.endian = @net_endian
|
44
96
|
# Clear the outgoing binary string:
|
45
97
|
@outgoing.reset
|
46
|
-
pdu = "07"
|
47
98
|
# Reserved (2 bytes)
|
48
99
|
@outgoing.encode_last("00"*2, "HEX")
|
49
100
|
# Source (1 byte)
|
@@ -52,48 +103,56 @@ module DICOM
|
|
52
103
|
# Reason/Diag. (1 byte)
|
53
104
|
reason = "00" # (Reason not specified)
|
54
105
|
@outgoing.encode_last(reason, "HEX")
|
55
|
-
append_header(
|
106
|
+
append_header(PDU_ABORT)
|
56
107
|
end
|
57
108
|
|
58
|
-
|
59
|
-
#
|
60
|
-
|
109
|
+
# Builds the binary string which is sent as the association accept (in response to an association request).
|
110
|
+
#
|
111
|
+
# === Parameters
|
112
|
+
#
|
113
|
+
# * <tt>info</tt> -- The association information hash.
|
114
|
+
#
|
115
|
+
def build_association_accept(info)
|
61
116
|
# Big endian encoding:
|
62
|
-
@outgoing.
|
117
|
+
@outgoing.endian = @net_endian
|
63
118
|
# Clear the outgoing binary string:
|
64
119
|
@outgoing.reset
|
65
|
-
#
|
66
|
-
|
67
|
-
|
68
|
-
# No abstract syntax in association response:
|
69
|
-
abstract_syntax = nil
|
120
|
+
# No abstract syntax in association response. To make this work with the method that
|
121
|
+
# encodes the presentation context, we pass on a one-element array containing nil).
|
122
|
+
abstract_syntaxes = Array.new(1, nil)
|
70
123
|
# Note: The order of which these components are built is not arbitrary.
|
71
|
-
append_application_context(
|
72
|
-
#
|
73
|
-
|
124
|
+
append_application_context(info[:application_context])
|
125
|
+
# Reset the presentation context instance variable:
|
126
|
+
@presentation_contexts = Hash.new
|
127
|
+
# Build the presentation context strings, one by one:
|
74
128
|
info[:pc].each do |pc|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
end
|
129
|
+
context_id = pc[:presentation_context_id]
|
130
|
+
result = pc[:result]
|
131
|
+
transfer_syntax = pc[:selected_transfer_syntax]
|
132
|
+
@presentation_contexts[context_id] = transfer_syntax
|
133
|
+
append_presentation_contexts(abstract_syntaxes, ITEM_PRESENTATION_CONTEXT_RESPONSE, transfer_syntax, context_id, result)
|
81
134
|
end
|
82
|
-
append_user_information(
|
135
|
+
append_user_information(@user_information)
|
83
136
|
# Header must be built last, because we need to know the length of the other components.
|
84
|
-
append_association_header(
|
137
|
+
append_association_header(PDU_ASSOCIATION_ACCEPT, info[:called_ae])
|
85
138
|
end
|
86
139
|
|
87
|
-
|
88
|
-
#
|
89
|
-
#
|
90
|
-
#
|
140
|
+
# Builds the binary string which is sent as the association reject (in response to an association request).
|
141
|
+
#
|
142
|
+
# === Parameters
|
143
|
+
#
|
144
|
+
# * <tt>info</tt> -- The association information hash.
|
145
|
+
#
|
146
|
+
# === Restrictions
|
147
|
+
#
|
148
|
+
# * For now, this method will only customize the "reason" value.
|
149
|
+
# * For a list of error codes, see the DICOM standard, PS3.8 Chapter 9.3.4, Table 9-21.
|
150
|
+
#
|
91
151
|
def build_association_reject(info)
|
92
152
|
# Big endian encoding:
|
93
|
-
@outgoing.
|
153
|
+
@outgoing.endian = @net_endian
|
94
154
|
# Clear the outgoing binary string:
|
95
155
|
@outgoing.reset
|
96
|
-
pdu = "03"
|
97
156
|
# Reserved (1 byte)
|
98
157
|
@outgoing.encode_last("00", "HEX")
|
99
158
|
# Result (1 byte)
|
@@ -104,35 +163,44 @@ module DICOM
|
|
104
163
|
# Reason (1 byte)
|
105
164
|
reason = info[:reason]
|
106
165
|
@outgoing.encode_last(reason, "HEX")
|
107
|
-
append_header(
|
166
|
+
append_header(PDU_ASSOCIATION_REJECT)
|
108
167
|
end
|
109
168
|
|
110
|
-
|
111
|
-
#
|
112
|
-
|
169
|
+
# Builds the binary string which is sent as the association request.
|
170
|
+
#
|
171
|
+
# === Parameters
|
172
|
+
#
|
173
|
+
# * <tt>ac_uid</tt> -- The application context UID string.
|
174
|
+
# * <tt>as</tt> -- An array of abstract syntax strings.
|
175
|
+
# * <tt>ts</tt> -- An array of transfer syntax strings.
|
176
|
+
# * <tt>user_info</tt> -- A user information items array.
|
177
|
+
#
|
178
|
+
def build_association_request(ac_uid, as, ts, user_info)
|
113
179
|
# Big endian encoding:
|
114
|
-
@outgoing.
|
180
|
+
@outgoing.endian = @net_endian
|
115
181
|
# Clear the outgoing binary string:
|
116
182
|
@outgoing.reset
|
117
|
-
# Set item types (pdu and presentation context):
|
118
|
-
pdu = "01"
|
119
|
-
pc = "20"
|
120
183
|
# Note: The order of which these components are built is not arbitrary.
|
121
184
|
# (The first three are built 'in order of appearance', the header is built last, but is put first in the message)
|
122
185
|
append_application_context(ac_uid)
|
123
|
-
|
124
|
-
append_user_information(
|
186
|
+
append_presentation_contexts(as, ITEM_PRESENTATION_CONTEXT_REQUEST, ts)
|
187
|
+
append_user_information(user_info)
|
125
188
|
# Header must be built last, because we need to know the length of the other components.
|
126
|
-
append_association_header(
|
189
|
+
append_association_header(PDU_ASSOCIATION_REQUEST, @host_ae)
|
127
190
|
end
|
128
191
|
|
129
|
-
|
130
|
-
#
|
131
|
-
#
|
132
|
-
#
|
192
|
+
# Builds the binary string which is sent as a command fragment.
|
193
|
+
#
|
194
|
+
# === Parameters
|
195
|
+
#
|
196
|
+
# * <tt>pdu</tt> -- The command fragment's PDU string.
|
197
|
+
# * <tt>context</tt> -- Presentation context ID byte (references a presentation context from the association).
|
198
|
+
# * <tt>flags</tt> -- The flag string, which identifies if this is the last command fragment or not.
|
199
|
+
# * <tt>command_elements</tt> -- An array of command elements.
|
200
|
+
#
|
133
201
|
def build_command_fragment(pdu, context, flags, command_elements)
|
134
202
|
# Little endian encoding:
|
135
|
-
@outgoing.
|
203
|
+
@outgoing.endian = @data_endian
|
136
204
|
# Clear the outgoing binary string:
|
137
205
|
@outgoing.reset
|
138
206
|
# Build the last part first, the Command items:
|
@@ -160,26 +228,35 @@ module DICOM
|
|
160
228
|
# Tag (4 bytes)
|
161
229
|
@outgoing.add_first(@outgoing.encode_tag("0000,0000"))
|
162
230
|
# Big endian encoding from now on:
|
163
|
-
@outgoing.
|
231
|
+
@outgoing.endian = @net_endian
|
164
232
|
# Flags (1 byte)
|
165
|
-
@outgoing.encode_first(flags, "HEX")
|
233
|
+
@outgoing.encode_first(flags, "HEX")
|
166
234
|
# Presentation context ID (1 byte)
|
167
|
-
@outgoing.encode_first(context, "
|
235
|
+
@outgoing.encode_first(context, "BY")
|
168
236
|
# Length (of remaining data) (4 bytes)
|
169
237
|
@outgoing.encode_first(@outgoing.string.length, "UL")
|
170
238
|
# PRESENTATION DATA VALUE (the above)
|
171
239
|
append_header(pdu)
|
172
240
|
end
|
173
241
|
|
174
|
-
|
175
|
-
#
|
176
|
-
#
|
177
|
-
|
242
|
+
# Builds the binary string which is sent as a data fragment.
|
243
|
+
#
|
244
|
+
# === Notes
|
245
|
+
#
|
246
|
+
# * The style of encoding will depend on whether we have an implicit or explicit transfer syntax.
|
247
|
+
#
|
248
|
+
# === Parameters
|
249
|
+
#
|
250
|
+
# * <tt>data_elements</tt> -- An array of data elements.
|
251
|
+
# * <tt>presentation_context_id</tt> -- Presentation context ID byte (references a presentation context from the association).
|
252
|
+
#
|
253
|
+
def build_data_fragment(data_elements, presentation_context_id)
|
254
|
+
# Set the transfer syntax to be used for encoding the data fragment:
|
255
|
+
set_transfer_syntax(@presentation_contexts[presentation_context_id])
|
178
256
|
# Endianness of data fragment:
|
179
|
-
@outgoing.
|
257
|
+
@outgoing.endian = @data_endian
|
180
258
|
# Clear the outgoing binary string:
|
181
259
|
@outgoing.reset
|
182
|
-
pdu = "04"
|
183
260
|
# Build the last part first, the Data items:
|
184
261
|
data_elements.each do |element|
|
185
262
|
# Encode all tags (even tags which are empty):
|
@@ -204,50 +281,53 @@ module DICOM
|
|
204
281
|
# The rest of the data fragment will be built in reverse, all the time
|
205
282
|
# putting the elements first in the outgoing binary string.
|
206
283
|
# Big endian encoding from now on:
|
207
|
-
@outgoing.
|
284
|
+
@outgoing.endian = @net_endian
|
208
285
|
# Flags (1 byte)
|
209
286
|
@outgoing.encode_first("02", "HEX") # Data, last fragment (identifier)
|
210
287
|
# Presentation context ID (1 byte)
|
211
|
-
@outgoing.encode_first(
|
288
|
+
@outgoing.encode_first(presentation_context_id, "BY")
|
212
289
|
# Length (of remaining data) (4 bytes)
|
213
290
|
@outgoing.encode_first(@outgoing.string.length, "UL")
|
214
291
|
# PRESENTATION DATA VALUE (the above)
|
215
|
-
append_header(
|
292
|
+
append_header(PDU_DATA)
|
216
293
|
end
|
217
294
|
|
218
|
-
|
219
|
-
#
|
295
|
+
# Builds the binary string which is sent as the release request.
|
296
|
+
#
|
220
297
|
def build_release_request
|
221
298
|
# Big endian encoding:
|
222
|
-
@outgoing.
|
299
|
+
@outgoing.endian = @net_endian
|
223
300
|
# Clear the outgoing binary string:
|
224
301
|
@outgoing.reset
|
225
|
-
pdu = "05"
|
226
302
|
# Reserved (4 bytes)
|
227
303
|
@outgoing.encode_last("00"*4, "HEX")
|
228
|
-
append_header(
|
304
|
+
append_header(PDU_RELEASE_REQUEST)
|
229
305
|
end
|
230
306
|
|
231
|
-
|
232
|
-
#
|
307
|
+
# Builds the binary string which is sent as the release response (which follows a release request).
|
308
|
+
#
|
233
309
|
def build_release_response
|
234
310
|
# Big endian encoding:
|
235
|
-
@outgoing.
|
311
|
+
@outgoing.endian = @net_endian
|
236
312
|
# Clear the outgoing binary string:
|
237
313
|
@outgoing.reset
|
238
|
-
pdu = "06"
|
239
314
|
# Reserved (4 bytes)
|
240
315
|
@outgoing.encode_last("00000000", "HEX")
|
241
|
-
append_header(
|
316
|
+
append_header(PDU_RELEASE_RESPONSE)
|
242
317
|
end
|
243
318
|
|
244
|
-
|
245
|
-
#
|
246
|
-
#
|
247
|
-
#
|
319
|
+
# Builds the binary string which makes up a C-STORE data fragment.
|
320
|
+
#
|
321
|
+
# === Parameters
|
322
|
+
#
|
323
|
+
# * <tt>pdu</tt> -- The data fragment's PDU string.
|
324
|
+
# * <tt>context</tt> -- Presentation context ID byte (references a presentation context from the association).
|
325
|
+
# * <tt>flags</tt> -- The flag string, which identifies if this is the last data fragment or not.
|
326
|
+
# * <tt>body</tt> -- A pre-encoded binary string (typicall a segment of a DICOM file to be transmitted).
|
327
|
+
#
|
248
328
|
def build_storage_fragment(pdu, context, flags, body)
|
249
329
|
# Big endian encoding:
|
250
|
-
@outgoing.
|
330
|
+
@outgoing.endian = @net_endian
|
251
331
|
# Clear the outgoing binary string:
|
252
332
|
@outgoing.reset
|
253
333
|
# Build in reverse, putting elements in front of the binary string:
|
@@ -256,161 +336,182 @@ module DICOM
|
|
256
336
|
# Flags (1 byte)
|
257
337
|
@outgoing.encode_first(flags, "HEX")
|
258
338
|
# Context ID (1 byte)
|
259
|
-
@outgoing.encode_first(context, "
|
339
|
+
@outgoing.encode_first(context, "BY")
|
260
340
|
# PDV Length (of remaining data) (4 bytes)
|
261
341
|
@outgoing.encode_first(@outgoing.string.length, "UL")
|
262
342
|
# PRESENTATION DATA VALUE (the above)
|
263
343
|
append_header(pdu)
|
264
344
|
end
|
265
345
|
|
266
|
-
|
267
|
-
#
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
#
|
274
|
-
|
275
|
-
|
276
|
-
end
|
277
|
-
|
278
|
-
|
279
|
-
# Delegates an incoming message to its correct interpreter method, based on pdu type.
|
280
|
-
def forward_to_interpret(message, pdu, file = nil)
|
346
|
+
# Delegates an incoming message to its appropriate interpreter method, based on its pdu type.
|
347
|
+
# Returns the interpreted information hash.
|
348
|
+
#
|
349
|
+
# === Parameters
|
350
|
+
#
|
351
|
+
# * <tt>message</tt> -- The binary message string.
|
352
|
+
# * <tt>pdu</tt> -- The PDU string of the message.
|
353
|
+
# * <tt>file</tt> -- A boolean used to inform whether an incoming data fragment is part of a DICOM file reception or not.
|
354
|
+
#
|
355
|
+
def forward_to_interpret(message, pdu, file=nil)
|
281
356
|
case pdu
|
282
|
-
when
|
357
|
+
when PDU_ASSOCIATION_REQUEST
|
283
358
|
info = interpret_association_request(message)
|
284
|
-
when
|
359
|
+
when PDU_ASSOCIATION_ACCEPT
|
285
360
|
info = interpret_association_accept(message)
|
286
|
-
when
|
361
|
+
when PDU_ASSOCIATION_REJECT
|
287
362
|
info = interpret_association_reject(message)
|
288
|
-
when
|
363
|
+
when PDU_DATA
|
289
364
|
info = interpret_command_and_data(message, file)
|
290
|
-
when
|
365
|
+
when PDU_RELEASE_REQUEST
|
291
366
|
info = interpret_release_request(message)
|
292
|
-
when
|
367
|
+
when PDU_RELEASE_RESPONSE
|
293
368
|
info = interpret_release_response(message)
|
294
|
-
when
|
369
|
+
when PDU_ABORT
|
295
370
|
info = interpret_abort(message)
|
296
371
|
else
|
297
372
|
info = {:valid => false}
|
298
|
-
add_error("An unknown
|
373
|
+
add_error("An unknown PDU type was received in the incoming transmission. Can not decode this message. (PDU: #{pdu})")
|
299
374
|
end
|
300
375
|
return info
|
301
376
|
end
|
302
377
|
|
303
|
-
|
304
|
-
#
|
305
|
-
|
306
|
-
|
378
|
+
# Handles the abortion of a session, when a non-valid or unexpected message has been received.
|
379
|
+
#
|
380
|
+
# === Parameters
|
381
|
+
#
|
382
|
+
# * <tt>default_message</tt> -- A boolean which unless set as nil/false will make the method print the default status message.
|
383
|
+
#
|
384
|
+
def handle_abort(default_message=true)
|
385
|
+
add_notice("An unregonizable (non-DICOM) message was received.") if default_message
|
307
386
|
build_association_abort
|
308
|
-
transmit
|
387
|
+
transmit
|
309
388
|
end
|
310
389
|
|
311
|
-
|
312
|
-
#
|
313
|
-
|
390
|
+
# Handles the outgoing association accept message.
|
391
|
+
#
|
392
|
+
# === Parameters
|
393
|
+
#
|
394
|
+
# * <tt>info</tt> -- The association information hash.
|
395
|
+
#
|
396
|
+
def handle_association_accept(info)
|
314
397
|
# Update the variable for calling ae (information gathered in the association request):
|
315
398
|
@ae = info[:calling_ae]
|
316
|
-
|
399
|
+
# Build message string and send it:
|
317
400
|
set_user_information_array(info)
|
318
|
-
build_association_accept(info
|
319
|
-
transmit
|
401
|
+
build_association_accept(info)
|
402
|
+
transmit
|
320
403
|
end
|
321
404
|
|
322
|
-
|
323
|
-
#
|
324
|
-
#
|
325
|
-
|
405
|
+
# Processes incoming command & data fragments for the DServer.
|
406
|
+
# Returns a success boolean and an array of status messages.
|
407
|
+
#
|
408
|
+
# === Notes
|
409
|
+
#
|
410
|
+
# The incoming traffic will in most cases be: A C-STORE-RQ (command fragment) followed by a bunch of data fragments.
|
411
|
+
# However, it may also be a C-ECHO-RQ command fragment, which is used to test connections.
|
412
|
+
#
|
413
|
+
# === Parameters
|
414
|
+
#
|
415
|
+
# * <tt>path</tt> -- The path used to save incoming DICOM files.
|
416
|
+
#
|
417
|
+
#--
|
418
|
+
# FIXME: The code which handles incoming data isnt quite satisfactory. It would probably be wise to rewrite it at some stage to clean up
|
419
|
+
# the code somewhat. Probably a better handling of command requests (and their corresponding data fragments) would be a good idea.
|
420
|
+
#
|
421
|
+
def handle_incoming_data(path)
|
326
422
|
# Wait for incoming data:
|
327
|
-
segments = receive_multiple_transmissions(
|
423
|
+
segments = receive_multiple_transmissions(file=true)
|
328
424
|
# Reset command results arrays:
|
329
425
|
@command_results = Array.new
|
330
426
|
@data_results = Array.new
|
331
|
-
|
332
|
-
|
427
|
+
file_transfer_syntaxes = Array.new
|
428
|
+
files = Array.new
|
429
|
+
single_file_data = Array.new
|
430
|
+
# Proceed to extract data from the captured segments:
|
333
431
|
segments.each do |info|
|
334
432
|
if info[:valid]
|
335
433
|
# Determine if it is command or data:
|
336
|
-
if info[:presentation_context_flag] ==
|
337
|
-
# Data (last fragment)
|
434
|
+
if info[:presentation_context_flag] == DATA_MORE_FRAGMENTS
|
338
435
|
@data_results << info[:results]
|
339
|
-
|
340
|
-
elsif info[:presentation_context_flag] ==
|
341
|
-
|
436
|
+
single_file_data << info[:bin]
|
437
|
+
elsif info[:presentation_context_flag] == DATA_LAST_FRAGMENT
|
438
|
+
@data_results << info[:results]
|
439
|
+
single_file_data << info[:bin]
|
440
|
+
# Join the recorded data binary strings together to make a DICOM file binary string and put it in our files Array:
|
441
|
+
files << single_file_data.join
|
442
|
+
single_file_data = Array.new
|
443
|
+
elsif info[:presentation_context_flag] == COMMAND_LAST_FRAGMENT
|
342
444
|
@command_results << info[:results]
|
343
|
-
@presentation_context_id = info[:presentation_context_id]
|
445
|
+
@presentation_context_id = info[:presentation_context_id] # Does this actually do anything useful?
|
446
|
+
file_transfer_syntaxes << @presentation_contexts[info[:presentation_context_id]]
|
344
447
|
end
|
345
448
|
end
|
346
449
|
end
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
obj = DObject.new(data, :bin => true, :syntax => @transfer_syntax)
|
351
|
-
# The actual handling of the DICOM object and (processing, saving, database storage, retransmission, etc)
|
352
|
-
# is handled by the external FileHandler class, in order to make it as easy as possible for users to write
|
353
|
-
# their own customised solutions for handling the incoming DICOM files:
|
354
|
-
success, message = @file_handler.receive_file(obj, path, @transfer_syntax)
|
355
|
-
else
|
356
|
-
# Valid DICOM data not received:
|
357
|
-
success = false
|
358
|
-
message = "Error: Invalid data received (the data was too small to be a valid DICOM file)."
|
359
|
-
end
|
360
|
-
return success, message
|
450
|
+
# Process the received files using the customizable FileHandler class:
|
451
|
+
success, messages = @file_handler.receive_files(path, files, file_transfer_syntaxes)
|
452
|
+
return success, messages
|
361
453
|
end
|
362
454
|
|
363
|
-
|
364
|
-
#
|
365
|
-
def handle_rejection
|
455
|
+
# Handles the rejection message (The response used to an association request when its formalities are not correct).
|
456
|
+
#
|
457
|
+
def handle_rejection
|
366
458
|
add_notice("An incoming association request was rejected. Error code: #{association_error}")
|
367
459
|
# Insert the error code in the info hash:
|
368
460
|
info[:reason] = association_error
|
369
461
|
# Send an association rejection:
|
370
462
|
build_association_reject(info)
|
371
|
-
transmit
|
463
|
+
transmit
|
372
464
|
end
|
373
465
|
|
374
|
-
|
375
|
-
#
|
376
|
-
def handle_release
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
transmit(session)
|
383
|
-
end
|
466
|
+
# Handles the release message (which is the response to a release request).
|
467
|
+
#
|
468
|
+
def handle_release
|
469
|
+
stop_receiving
|
470
|
+
add_notice("Received a release request. Releasing association.")
|
471
|
+
build_release_response
|
472
|
+
transmit
|
473
|
+
stop_session
|
384
474
|
end
|
385
475
|
|
386
|
-
|
387
|
-
#
|
388
|
-
|
389
|
-
|
476
|
+
# Handles the command fragment response.
|
477
|
+
#
|
478
|
+
# === Notes
|
479
|
+
#
|
480
|
+
# This is usually a C-STORE-RSP which follows the (successful) reception of a DICOM file, but may also
|
481
|
+
# be a C-ECHO-RSP in response to an echo request.
|
482
|
+
#
|
483
|
+
def handle_response
|
390
484
|
# Need to construct the command elements array:
|
391
485
|
command_elements = Array.new
|
392
486
|
# SOP Class UID:
|
393
|
-
command_elements << ["0000,0002", "UI",
|
487
|
+
command_elements << ["0000,0002", "UI", @command_request["0000,0002"]]
|
394
488
|
# Command Field:
|
395
|
-
command_elements << ["0000,0100", "US",
|
489
|
+
command_elements << ["0000,0100", "US", command_field_response(@command_request["0000,0100"])]
|
396
490
|
# Message ID Being Responded To:
|
397
|
-
command_elements << ["0000,0120", "US",
|
491
|
+
command_elements << ["0000,0120", "US", @command_request["0000,0110"]]
|
398
492
|
# Data Set Type:
|
399
|
-
command_elements << ["0000,0800", "US",
|
493
|
+
command_elements << ["0000,0800", "US", NO_DATA_SET_PRESENT]
|
400
494
|
# Status:
|
401
|
-
command_elements << ["0000,0900", "US",
|
495
|
+
command_elements << ["0000,0900", "US", SUCCESS]
|
402
496
|
# Affected SOP Instance UID:
|
403
|
-
command_elements << ["0000,1000", "UI",
|
404
|
-
|
405
|
-
|
406
|
-
flag = "03" # (Command, last fragment)
|
407
|
-
build_command_fragment(pdu, context, flag, command_elements)
|
408
|
-
transmit(session)
|
497
|
+
command_elements << ["0000,1000", "UI", @command_request["0000,1000"]] if @command_request["0000,1000"]
|
498
|
+
build_command_fragment(PDU_DATA, @presentation_context_id, COMMAND_LAST_FRAGMENT, command_elements)
|
499
|
+
transmit
|
409
500
|
end
|
410
501
|
|
411
|
-
|
412
|
-
#
|
413
|
-
|
502
|
+
# Decodes the header of an incoming message, analyzes its real length versus expected length, and handles any
|
503
|
+
# deviations to make sure that message strings are split up appropriately before they are being forwarded to interpretation.
|
504
|
+
# Returns an array of information hashes.
|
505
|
+
#
|
506
|
+
# === Parameters
|
507
|
+
#
|
508
|
+
# * <tt>message</tt> -- The binary message string.
|
509
|
+
# * <tt>file</tt> -- A boolean used to inform whether an incoming data fragment is part of a DICOM file reception or not.
|
510
|
+
#
|
511
|
+
#--
|
512
|
+
# FIXME: This method is rather complex and doesnt feature the best readability. A rewrite that is able to simplify it would be lovely.
|
513
|
+
#
|
514
|
+
def interpret(message, file=nil)
|
414
515
|
if @first_part
|
415
516
|
message = @first_part + message
|
416
517
|
@first_part = nil
|
@@ -419,7 +520,7 @@ module DICOM
|
|
419
520
|
# If the message is at least 8 bytes we can start decoding it:
|
420
521
|
if message.length > 8
|
421
522
|
# Create a new Stream instance to handle this response.
|
422
|
-
msg = Stream.new(message, @net_endian
|
523
|
+
msg = Stream.new(message, @net_endian)
|
423
524
|
# PDU type ( 1 byte)
|
424
525
|
pdu = msg.decode(1, "HEX")
|
425
526
|
# Reserved (1 byte)
|
@@ -468,11 +569,16 @@ module DICOM
|
|
468
569
|
return segments
|
469
570
|
end
|
470
571
|
|
471
|
-
|
472
|
-
#
|
572
|
+
# Decodes the message received when the remote node wishes to abort the session.
|
573
|
+
# Returns the processed information hash.
|
574
|
+
#
|
575
|
+
# === Parameters
|
576
|
+
#
|
577
|
+
# * <tt>message</tt> -- The binary message string.
|
578
|
+
#
|
473
579
|
def interpret_abort(message)
|
474
580
|
info = Hash.new
|
475
|
-
msg = Stream.new(message, @net_endian
|
581
|
+
msg = Stream.new(message, @net_endian)
|
476
582
|
# Reserved (2 bytes)
|
477
583
|
reserved_bytes = msg.skip(2)
|
478
584
|
# Source (1 byte)
|
@@ -488,11 +594,16 @@ module DICOM
|
|
488
594
|
return info
|
489
595
|
end
|
490
596
|
|
491
|
-
|
492
|
-
#
|
597
|
+
# Decodes the message received in the association response, and interprets its content.
|
598
|
+
# Returns the processed information hash.
|
599
|
+
#
|
600
|
+
# === Parameters
|
601
|
+
#
|
602
|
+
# * <tt>message</tt> -- The binary message string.
|
603
|
+
#
|
493
604
|
def interpret_association_accept(message)
|
494
605
|
info = Hash.new
|
495
|
-
msg = Stream.new(message, @net_endian
|
606
|
+
msg = Stream.new(message, @net_endian)
|
496
607
|
# Protocol version (2 bytes)
|
497
608
|
info[:protocol_version] = msg.decode(2, "HEX")
|
498
609
|
# Reserved (2 bytes)
|
@@ -513,32 +624,48 @@ module DICOM
|
|
513
624
|
# Application context (variable length)
|
514
625
|
info[:application_context] = msg.decode(info[:application_item_length], "STR")
|
515
626
|
# PRESENTATION CONTEXT:
|
516
|
-
#
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
627
|
+
# As multiple presentation contexts may occur, we need a loop to catch them all:
|
628
|
+
# Each presentation context hash will be put in an array, which will be put in the info hash.
|
629
|
+
presentation_contexts = Array.new
|
630
|
+
pc_loop = true
|
631
|
+
while pc_loop do
|
632
|
+
# Item type (1 byte)
|
633
|
+
item_type = msg.decode(1, "HEX")
|
634
|
+
if item_type == ITEM_PRESENTATION_CONTEXT_RESPONSE
|
635
|
+
pc = Hash.new
|
636
|
+
pc[:presentation_item_type] = item_type
|
637
|
+
# Reserved (1 byte)
|
638
|
+
msg.skip(1)
|
639
|
+
# Presentation item length (2 bytes)
|
640
|
+
pc[:presentation_item_length] = msg.decode(2, "US")
|
641
|
+
# Presentation context ID (1 byte)
|
642
|
+
pc[:presentation_context_id] = msg.decode(1, "BY")
|
643
|
+
# Reserved (1 byte)
|
644
|
+
msg.skip(1)
|
645
|
+
# Result (& Reason) (1 byte)
|
646
|
+
pc[:result] = msg.decode(1, "BY")
|
647
|
+
process_result(pc[:result])
|
648
|
+
# Reserved (1 byte)
|
649
|
+
msg.skip(1)
|
650
|
+
# Transfer syntax sub-item:
|
651
|
+
# Item type (1 byte)
|
652
|
+
pc[:transfer_syntax_item_type] = msg.decode(1, "HEX")
|
653
|
+
# Reserved (1 byte)
|
654
|
+
msg.skip(1)
|
655
|
+
# Transfer syntax item length (2 bytes)
|
656
|
+
pc[:transfer_syntax_item_length] = msg.decode(2, "US")
|
657
|
+
# Transfer syntax name (variable length)
|
658
|
+
pc[:transfer_syntax] = msg.decode(pc[:transfer_syntax_item_length], "STR")
|
659
|
+
presentation_contexts << pc
|
660
|
+
else
|
661
|
+
# Break the presentation context loop, as we have probably reached the next stage, which is user info. Rewind:
|
662
|
+
msg.skip(-1)
|
663
|
+
pc_loop = false
|
664
|
+
end
|
665
|
+
end
|
666
|
+
info[:pc] = presentation_contexts
|
540
667
|
# USER INFORMATION:
|
541
|
-
# Item type (1 byte)
|
668
|
+
# Item type (1 byte)
|
542
669
|
info[:user_info_item_type] = msg.decode(1, "HEX")
|
543
670
|
# Reserved (1 byte)
|
544
671
|
msg.skip(1)
|
@@ -552,35 +679,54 @@ module DICOM
|
|
552
679
|
# Item length (2 bytes)
|
553
680
|
item_length = msg.decode(2, "US")
|
554
681
|
case item_type
|
555
|
-
when
|
682
|
+
when ITEM_MAX_LENGTH
|
556
683
|
info[:max_pdu_length] = msg.decode(item_length, "UL")
|
557
684
|
@max_receive_size = info[:max_pdu_length]
|
558
|
-
when
|
685
|
+
when ITEM_IMPLEMENTATION_UID
|
559
686
|
info[:implementation_class_uid] = msg.decode(item_length, "STR")
|
560
|
-
when
|
687
|
+
when ITEM_MAX_OPERATIONS_INVOKED
|
561
688
|
# Asynchronous operations window negotiation (PS 3.7: D.3.3.3) (2*2 bytes)
|
562
689
|
info[:maxnum_operations_invoked] = msg.decode(2, "US")
|
563
690
|
info[:maxnum_operations_performed] = msg.decode(2, "US")
|
564
|
-
when
|
691
|
+
when ITEM_ROLE_NEGOTIATION
|
692
|
+
# SCP/SCU Role Selection Negotiation (PS 3.7 D.3.3.4)
|
693
|
+
# Note: An association response may contain several instances of this item type (each with a different abstract syntax).
|
694
|
+
uid_length = msg.decode(2, "US")
|
695
|
+
role = Hash.new
|
696
|
+
# SOP Class UID (Abstract syntax):
|
697
|
+
role[:sop_uid] = msg.decode(uid_length, "STR")
|
698
|
+
# SCU Role (1 byte):
|
699
|
+
role[:scu] = msg.decode(1, "BY")
|
700
|
+
# SCP Role (1 byte):
|
701
|
+
role[:scp] = msg.decode(1, "BY")
|
702
|
+
if info[:role_negotiation]
|
703
|
+
info[:role_negotiation] << role
|
704
|
+
else
|
705
|
+
info[:role_negotiation] = [role]
|
706
|
+
end
|
707
|
+
when ITEM_IMPLEMENTATION_VERSION
|
565
708
|
info[:implementation_version] = msg.decode(item_length, "STR")
|
566
709
|
else
|
567
710
|
# Value (variable length)
|
568
711
|
value = msg.decode(item_length, "STR")
|
569
|
-
add_error("Unknown user info item type received. Please update source code or contact author. (item type:
|
712
|
+
add_error("Unknown user info item type received. Please update source code or contact author. (item type: #{item_type})")
|
570
713
|
end
|
571
714
|
end
|
572
715
|
stop_receiving
|
573
716
|
info[:valid] = true
|
574
|
-
# Update transfer syntax settings for this instance:
|
575
|
-
set_transfer_syntax(info[:transfer_syntax])
|
576
717
|
return info
|
577
|
-
end
|
578
|
-
|
718
|
+
end
|
579
719
|
|
580
|
-
#
|
720
|
+
# Decodes the association reject message and extracts the error reasons given.
|
721
|
+
# Returns the processed information hash.
|
722
|
+
#
|
723
|
+
# === Parameters
|
724
|
+
#
|
725
|
+
# * <tt>message</tt> -- The binary message string.
|
726
|
+
#
|
581
727
|
def interpret_association_reject(message)
|
582
728
|
info = Hash.new
|
583
|
-
msg = Stream.new(message, @net_endian
|
729
|
+
msg = Stream.new(message, @net_endian)
|
584
730
|
# Reserved (1 byte)
|
585
731
|
msg.skip(1)
|
586
732
|
# Result (1 byte)
|
@@ -595,11 +741,16 @@ module DICOM
|
|
595
741
|
return info
|
596
742
|
end
|
597
743
|
|
598
|
-
|
599
|
-
#
|
744
|
+
# Decodes the binary string received in the association request, and interprets its content.
|
745
|
+
# Returns the processed information hash.
|
746
|
+
#
|
747
|
+
# === Parameters
|
748
|
+
#
|
749
|
+
# * <tt>message</tt> -- The binary message string.
|
750
|
+
#
|
600
751
|
def interpret_association_request(message)
|
601
752
|
info = Hash.new
|
602
|
-
msg = Stream.new(message, @net_endian
|
753
|
+
msg = Stream.new(message, @net_endian)
|
603
754
|
# Protocol version (2 bytes)
|
604
755
|
info[:protocol_version] = msg.decode(2, "HEX")
|
605
756
|
# Reserved (2 bytes)
|
@@ -612,7 +763,7 @@ module DICOM
|
|
612
763
|
msg.skip(32)
|
613
764
|
# APPLICATION CONTEXT:
|
614
765
|
# Item type (1 byte)
|
615
|
-
info[:application_item_type] = msg.decode(1, "HEX") #
|
766
|
+
info[:application_item_type] = msg.decode(1, "HEX") # 10H
|
616
767
|
# Reserved (1 byte)
|
617
768
|
msg.skip(1)
|
618
769
|
# Application item length (2 bytes)
|
@@ -627,7 +778,7 @@ module DICOM
|
|
627
778
|
while pc_loop do
|
628
779
|
# Item type (1 byte)
|
629
780
|
item_type = msg.decode(1, "HEX")
|
630
|
-
if item_type ==
|
781
|
+
if item_type == ITEM_PRESENTATION_CONTEXT_REQUEST
|
631
782
|
pc = Hash.new
|
632
783
|
pc[:presentation_item_type] = item_type
|
633
784
|
# Reserved (1 byte)
|
@@ -635,14 +786,14 @@ module DICOM
|
|
635
786
|
# Presentation context item length (2 bytes)
|
636
787
|
pc[:presentation_item_length] = msg.decode(2, "US")
|
637
788
|
# Presentation context id (1 byte)
|
638
|
-
pc[:presentation_context_id] = msg.decode(1, "
|
789
|
+
pc[:presentation_context_id] = msg.decode(1, "BY")
|
639
790
|
# Reserved (3 bytes)
|
640
791
|
msg.skip(3)
|
641
792
|
presentation_contexts << pc
|
642
793
|
# A presentation context contains an abstract syntax and one or more transfer syntaxes.
|
643
794
|
# ABSTRACT SYNTAX SUB-ITEM:
|
644
795
|
# Abstract syntax item type (1 byte)
|
645
|
-
pc[:abstract_syntax_item_type] = msg.decode(1, "HEX")
|
796
|
+
pc[:abstract_syntax_item_type] = msg.decode(1, "HEX")
|
646
797
|
# Reserved (1 byte)
|
647
798
|
msg.skip(1)
|
648
799
|
# Abstract syntax item length (2 bytes)
|
@@ -657,7 +808,7 @@ module DICOM
|
|
657
808
|
while ts_loop do
|
658
809
|
# Item type (1 byte)
|
659
810
|
item_type = msg.decode(1, "HEX")
|
660
|
-
if item_type ==
|
811
|
+
if item_type == ITEM_TRANSFER_SYNTAX
|
661
812
|
ts = Hash.new
|
662
813
|
ts[:transfer_syntax_item_type] = item_type
|
663
814
|
# Reserved (1 byte)
|
@@ -698,15 +849,31 @@ module DICOM
|
|
698
849
|
# Item length (2 bytes)
|
699
850
|
item_length = msg.decode(2, "US")
|
700
851
|
case item_type
|
701
|
-
when
|
852
|
+
when ITEM_MAX_LENGTH
|
702
853
|
info[:max_pdu_length] = msg.decode(item_length, "UL")
|
703
|
-
when
|
854
|
+
when ITEM_IMPLEMENTATION_UID
|
704
855
|
info[:implementation_class_uid] = msg.decode(item_length, "STR")
|
705
|
-
when
|
856
|
+
when ITEM_MAX_OPERATIONS_INVOKED
|
706
857
|
# Asynchronous operations window negotiation (PS 3.7: D.3.3.3) (2*2 bytes)
|
707
858
|
info[:maxnum_operations_invoked] = msg.decode(2, "US")
|
708
859
|
info[:maxnum_operations_performed] = msg.decode(2, "US")
|
709
|
-
when
|
860
|
+
when ITEM_ROLE_NEGOTIATION
|
861
|
+
# SCP/SCU Role Selection Negotiation (PS 3.7 D.3.3.4)
|
862
|
+
# Note: An association request may contain several instances of this item type (each with a different abstract syntax).
|
863
|
+
uid_length = msg.decode(2, "US")
|
864
|
+
role = Hash.new
|
865
|
+
# SOP Class UID (Abstract syntax):
|
866
|
+
role[:sop_uid] = msg.decode(uid_length, "STR")
|
867
|
+
# SCU Role (1 byte):
|
868
|
+
role[:scu] = msg.decode(1, "BY")
|
869
|
+
# SCP Role (1 byte):
|
870
|
+
role[:scp] = msg.decode(1, "BY")
|
871
|
+
if info[:role_negotiation]
|
872
|
+
info[:role_negotiation] << role
|
873
|
+
else
|
874
|
+
info[:role_negotiation] = [role]
|
875
|
+
end
|
876
|
+
when ITEM_IMPLEMENTATION_VERSION
|
710
877
|
info[:implementation_version] = msg.decode(item_length, "STR")
|
711
878
|
else
|
712
879
|
# Unknown item type:
|
@@ -718,27 +885,39 @@ module DICOM
|
|
718
885
|
stop_receiving
|
719
886
|
info[:valid] = true
|
720
887
|
return info
|
721
|
-
end
|
722
|
-
|
888
|
+
end
|
723
889
|
|
724
|
-
#
|
725
|
-
#
|
726
|
-
|
890
|
+
# Decodes the received command/data fragment message, and interprets its content.
|
891
|
+
# Returns the processed information hash.
|
892
|
+
#
|
893
|
+
# === Notes
|
894
|
+
#
|
895
|
+
# * Decoding of a data fragment depends on the explicitness of the transmission.
|
896
|
+
#
|
897
|
+
# === Parameters
|
898
|
+
#
|
899
|
+
# * <tt>message</tt> -- The binary message string.
|
900
|
+
# * <tt>file</tt> -- A boolean used to inform whether an incoming data fragment is part of a DICOM file reception or not.
|
901
|
+
#
|
902
|
+
def interpret_command_and_data(message, file=nil)
|
727
903
|
info = Hash.new
|
728
|
-
msg = Stream.new(message, @net_endian
|
904
|
+
msg = Stream.new(message, @net_endian)
|
729
905
|
# Length (of remaining PDV data) (4 bytes)
|
730
906
|
info[:presentation_data_value_length] = msg.decode(4, "UL")
|
731
907
|
# Calculate the last index position of this message element:
|
732
908
|
last_index = info[:presentation_data_value_length] + msg.index
|
733
909
|
# Presentation context ID (1 byte)
|
734
|
-
info[:presentation_context_id] = msg.decode(1, "
|
910
|
+
info[:presentation_context_id] = msg.decode(1, "BY")
|
911
|
+
@presentation_context_id = info[:presentation_context_id]
|
735
912
|
# Flags (1 byte)
|
736
913
|
info[:presentation_context_flag] = msg.decode(1, "HEX") # "03" for command (last fragment), "02" for data
|
737
|
-
#
|
738
|
-
|
914
|
+
# Apply the proper transfer syntax for this presentation context:
|
915
|
+
set_transfer_syntax(@presentation_contexts[info[:presentation_context_id]])
|
916
|
+
# "Data endian" encoding from now on:
|
917
|
+
msg.endian = @data_endian
|
739
918
|
# We will put the results in a hash:
|
740
919
|
results = Hash.new
|
741
|
-
if info[:presentation_context_flag] ==
|
920
|
+
if info[:presentation_context_flag] == COMMAND_LAST_FRAGMENT
|
742
921
|
# COMMAND, LAST FRAGMENT:
|
743
922
|
while msg.index < last_index do
|
744
923
|
# Tag (4 bytes)
|
@@ -761,22 +940,28 @@ module DICOM
|
|
761
940
|
end
|
762
941
|
# The results hash is put in an array along with (possibly) other results:
|
763
942
|
info[:results] = results
|
943
|
+
# Store the results in an instance variable (to be used later when sending a receipt for received data):
|
944
|
+
@command_request = results
|
764
945
|
# Check if the command fragment indicates that this was the last of the response fragments for this query:
|
765
946
|
status = results["0000,0900"]
|
766
947
|
if status
|
767
948
|
# Note: This method will also stop the packet receiver if indicated by the status mesasge.
|
768
949
|
process_status(status)
|
769
950
|
end
|
770
|
-
|
951
|
+
# Special case: Handle a possible C-ECHO-RQ:
|
952
|
+
if info[:results]["0000,0100"] == C_ECHO_RQ
|
953
|
+
add_notice("Received an Echo request. Returning an Echo response.")
|
954
|
+
handle_response
|
955
|
+
end
|
956
|
+
elsif info[:presentation_context_flag] == DATA_MORE_FRAGMENTS or info[:presentation_context_flag] == DATA_LAST_FRAGMENT
|
771
957
|
# DATA FRAGMENT:
|
772
958
|
# If this is a file transmission, we will delay the decoding for later:
|
773
959
|
if file
|
774
960
|
# Just store the binary string:
|
775
961
|
info[:bin] = msg.rest_string
|
776
|
-
#
|
777
|
-
|
778
|
-
|
779
|
-
end
|
962
|
+
# If this was the last data fragment of a C-STORE, we need to send a receipt:
|
963
|
+
# (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)
|
964
|
+
handle_response if info[:presentation_context_flag] == DATA_LAST_FRAGMENT
|
780
965
|
else
|
781
966
|
# Decode data elements:
|
782
967
|
while msg.index < last_index do
|
@@ -819,23 +1004,33 @@ module DICOM
|
|
819
1004
|
return info
|
820
1005
|
end
|
821
1006
|
|
822
|
-
|
823
|
-
#
|
1007
|
+
# Decodes the message received in the release request and calls the handle_release method.
|
1008
|
+
# Returns the processed information hash.
|
1009
|
+
#
|
1010
|
+
# === Parameters
|
1011
|
+
#
|
1012
|
+
# * <tt>message</tt> -- The binary message string.
|
1013
|
+
#
|
824
1014
|
def interpret_release_request(message)
|
825
1015
|
info = Hash.new
|
826
|
-
msg = Stream.new(message, @net_endian
|
1016
|
+
msg = Stream.new(message, @net_endian)
|
827
1017
|
# Reserved (4 bytes)
|
828
1018
|
reserved_bytes = msg.decode(4, "HEX")
|
829
|
-
|
1019
|
+
handle_release
|
830
1020
|
info[:valid] = true
|
831
1021
|
return info
|
832
1022
|
end
|
833
1023
|
|
834
|
-
|
835
|
-
#
|
1024
|
+
# Decodes the message received in the release response and closes the connection.
|
1025
|
+
# Returns the processed information hash.
|
1026
|
+
#
|
1027
|
+
# === Parameters
|
1028
|
+
#
|
1029
|
+
# * <tt>message</tt> -- The binary message string.
|
1030
|
+
#
|
836
1031
|
def interpret_release_response(message)
|
837
1032
|
info = Hash.new
|
838
|
-
msg = Stream.new(message, @net_endian
|
1033
|
+
msg = Stream.new(message, @net_endian)
|
839
1034
|
# Reserved (4 bytes)
|
840
1035
|
reserved_bytes = msg.decode(4, "HEX")
|
841
1036
|
stop_receiving
|
@@ -843,14 +1038,19 @@ module DICOM
|
|
843
1038
|
return info
|
844
1039
|
end
|
845
1040
|
|
846
|
-
|
847
|
-
#
|
848
|
-
|
1041
|
+
# Handles the reception of multiple incoming transmissions.
|
1042
|
+
# Returns an array of interpreted message information hashes.
|
1043
|
+
#
|
1044
|
+
# === Parameters
|
1045
|
+
#
|
1046
|
+
# * <tt>file</tt> -- A boolean used to inform whether an incoming data fragment is part of a DICOM file reception or not.
|
1047
|
+
#
|
1048
|
+
def receive_multiple_transmissions(file=nil)
|
849
1049
|
@listen = true
|
850
1050
|
segments = Array.new
|
851
1051
|
while @listen
|
852
1052
|
# Receive data and append the current data to our segments array, which will be returned.
|
853
|
-
data = receive_transmission(
|
1053
|
+
data = receive_transmission(@min_length)
|
854
1054
|
current_segments = interpret(data, file)
|
855
1055
|
if current_segments
|
856
1056
|
current_segments.each do |cs|
|
@@ -862,20 +1062,47 @@ module DICOM
|
|
862
1062
|
return segments
|
863
1063
|
end
|
864
1064
|
|
865
|
-
|
866
|
-
#
|
867
|
-
def receive_single_transmission
|
1065
|
+
# Handles the reception of a single, expected incoming transmission and returns the interpreted, received data.
|
1066
|
+
#
|
1067
|
+
def receive_single_transmission
|
868
1068
|
min_length = 8
|
869
|
-
data = receive_transmission(
|
1069
|
+
data = receive_transmission(min_length)
|
870
1070
|
segments = interpret(data)
|
871
1071
|
segments << {:valid => false} unless segments.length > 0
|
872
1072
|
return segments
|
873
1073
|
end
|
874
1074
|
|
1075
|
+
# Sets the session of this Link instance (used when this session is already established externally).
|
1076
|
+
#
|
1077
|
+
# === Parameters
|
1078
|
+
#
|
1079
|
+
# * <tt>session</tt> -- A TCP network connection that has been established with a remote node.
|
1080
|
+
#
|
1081
|
+
def set_session(session)
|
1082
|
+
@session = session
|
1083
|
+
end
|
1084
|
+
|
1085
|
+
# Establishes a new session with a remote network node.
|
1086
|
+
#
|
1087
|
+
# === Parameters
|
1088
|
+
#
|
1089
|
+
# * <tt>adress</tt> -- String. The adress (IP) of the remote node.
|
1090
|
+
# * <tt>port</tt> -- Fixnum. The network port to be used in the network communication.
|
1091
|
+
#
|
1092
|
+
def start_session(adress, port)
|
1093
|
+
@session = TCPSocket.new(adress, port)
|
1094
|
+
end
|
1095
|
+
|
1096
|
+
# Ends the current session by closing the connection.
|
1097
|
+
#
|
1098
|
+
def stop_session
|
1099
|
+
@session.close unless @session.closed?
|
1100
|
+
end
|
875
1101
|
|
876
|
-
#
|
877
|
-
|
878
|
-
|
1102
|
+
# Sends the outgoing message (encoded binary string) to the remote node.
|
1103
|
+
#
|
1104
|
+
def transmit
|
1105
|
+
@session.send(@outgoing.string, 0)
|
879
1106
|
end
|
880
1107
|
|
881
1108
|
|
@@ -884,25 +1111,38 @@ module DICOM
|
|
884
1111
|
|
885
1112
|
|
886
1113
|
# Adds a warning or error message to the instance array holding messages,
|
887
|
-
# and
|
1114
|
+
# and prints the information to the screen if verbose is set.
|
1115
|
+
#
|
1116
|
+
# === Parameters
|
1117
|
+
#
|
1118
|
+
# * <tt>error</tt> -- A single error message or an array of error messages.
|
1119
|
+
#
|
888
1120
|
def add_error(error)
|
889
1121
|
puts error if @verbose
|
890
1122
|
@errors << error
|
891
1123
|
end
|
892
1124
|
|
893
|
-
|
894
1125
|
# Adds a notice (information regarding progress or successful communications) to the instance array,
|
895
|
-
# and
|
1126
|
+
# and prints the information to the screen if verbose is set.
|
1127
|
+
#
|
1128
|
+
# === Parameters
|
1129
|
+
#
|
1130
|
+
# * <tt>notice</tt> -- A single status message or an array of status messages.
|
1131
|
+
#
|
896
1132
|
def add_notice(notice)
|
897
1133
|
puts notice if @verbose
|
898
1134
|
@notices << notice
|
899
1135
|
end
|
900
1136
|
|
901
|
-
|
902
|
-
#
|
1137
|
+
# Builds the application context (which is part of the association request/response).
|
1138
|
+
#
|
1139
|
+
# === Parameters
|
1140
|
+
#
|
1141
|
+
# * <tt>ac_uid</tt> -- Application context UID string.
|
1142
|
+
#
|
903
1143
|
def append_application_context(ac_uid)
|
904
1144
|
# Application context item type (1 byte)
|
905
|
-
@outgoing.encode_last(
|
1145
|
+
@outgoing.encode_last(ITEM_APPLICATION_CONTEXT, "HEX")
|
906
1146
|
# Reserved (1 byte)
|
907
1147
|
@outgoing.encode_last("00", "HEX")
|
908
1148
|
# Application context item length (2 bytes)
|
@@ -911,11 +1151,16 @@ module DICOM
|
|
911
1151
|
@outgoing.encode_last(ac_uid, "STR")
|
912
1152
|
end
|
913
1153
|
|
914
|
-
|
915
|
-
#
|
916
|
-
|
1154
|
+
# Builds the binary string that makes up the header part the association request/response.
|
1155
|
+
#
|
1156
|
+
# === Parameters
|
1157
|
+
#
|
1158
|
+
# * <tt>pdu</tt> -- The command fragment's PDU string.
|
1159
|
+
# * <tt>called_ae</tt> -- Application entity (name) of the SCP (host).
|
1160
|
+
#
|
1161
|
+
def append_association_header(pdu, called_ae)
|
917
1162
|
# Big endian encoding:
|
918
|
-
@outgoing.
|
1163
|
+
@outgoing.endian = @net_endian
|
919
1164
|
# Header will be encoded in opposite order, where the elements are being put first in the outgoing binary string.
|
920
1165
|
# Build last part of header first. This is necessary to be able to assess the length value.
|
921
1166
|
# Reserved (32 bytes)
|
@@ -923,9 +1168,9 @@ module DICOM
|
|
923
1168
|
# Calling AE title (16 bytes)
|
924
1169
|
calling_ae = @outgoing.encode_string_with_trailing_spaces(@ae, 16)
|
925
1170
|
@outgoing.add_first(calling_ae) # (pre-encoded value)
|
926
|
-
# Called AE title (16 bytes)
|
927
|
-
|
928
|
-
@outgoing.add_first(
|
1171
|
+
# Called AE title (16 bytes) (return the name that the SCU used in the association request)
|
1172
|
+
formatted_called_ae = @outgoing.encode_string_with_trailing_spaces(called_ae, 16)
|
1173
|
+
@outgoing.add_first(formatted_called_ae) # (pre-encoded value)
|
929
1174
|
# Reserved (2 bytes)
|
930
1175
|
@outgoing.encode_first("0000", "HEX")
|
931
1176
|
# Protocol version (2 bytes)
|
@@ -933,9 +1178,12 @@ module DICOM
|
|
933
1178
|
append_header(pdu)
|
934
1179
|
end
|
935
1180
|
|
936
|
-
|
937
|
-
#
|
938
|
-
#
|
1181
|
+
# Adds the header bytes to the outgoing message (the header structure is equal for all of the message types).
|
1182
|
+
#
|
1183
|
+
# === Parameters
|
1184
|
+
#
|
1185
|
+
# * <tt>pdu</tt> -- The command fragment's PDU string.
|
1186
|
+
#
|
939
1187
|
def append_header(pdu)
|
940
1188
|
# Length (of remaining data) (4 bytes)
|
941
1189
|
@outgoing.encode_first(@outgoing.string.length, "UL")
|
@@ -945,68 +1193,94 @@ module DICOM
|
|
945
1193
|
@outgoing.encode_first(pdu, "HEX")
|
946
1194
|
end
|
947
1195
|
|
948
|
-
|
949
|
-
#
|
950
|
-
#
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
963
|
-
|
964
|
-
|
965
|
-
|
966
|
-
|
967
|
-
|
968
|
-
|
969
|
-
# Presentation context ID (1 byte)
|
970
|
-
@outgoing.encode_last(context_id, "HEX")
|
971
|
-
# Reserved (1 byte)
|
972
|
-
@outgoing.encode_last("00", "HEX")
|
973
|
-
# (1 byte) Reserved (for association request) & Result/reason (for association accept response)
|
974
|
-
@outgoing.encode_last(result, "HEX")
|
975
|
-
# Reserved (1 byte)
|
976
|
-
@outgoing.encode_last("00", "HEX")
|
977
|
-
## ABSTRACT SYNTAX SUB-ITEM: (only for request, not response)
|
978
|
-
if as
|
979
|
-
# Abstract syntax item type (1 byte)
|
980
|
-
@outgoing.encode_last("30", "HEX")
|
1196
|
+
# Builds the binary string that makes up the presentation context part of the association request/accept.
|
1197
|
+
#
|
1198
|
+
# === Notes
|
1199
|
+
#
|
1200
|
+
# * The values of the parameters will differ somewhat depending on whether this is related to a request or response.
|
1201
|
+
# * Description of error codes are given in the DICOM Standard, PS 3.8, Chapter 9.3.3.2 (Table 9-18).
|
1202
|
+
#
|
1203
|
+
# === Parameters
|
1204
|
+
#
|
1205
|
+
# * <tt>abstract_syntaxes</tt> -- An array of abstract syntax strings.
|
1206
|
+
# * <tt>pc</tt> -- Presentation context item (request or response).
|
1207
|
+
# * <tt>ts</tt> -- An array of transfer syntax strings.
|
1208
|
+
# * <tt>context_id</tt> -- The ID of the current presentation context.
|
1209
|
+
# * <tt>result</tt> -- The result (accepted/refused) for the current presentation context.
|
1210
|
+
#
|
1211
|
+
def append_presentation_contexts(abstract_syntaxes, pc, ts, context_id=nil, result=ACCEPTANCE)
|
1212
|
+
# One presentation context for each abstract syntax:
|
1213
|
+
abstract_syntaxes.each_with_index do |as, index|
|
1214
|
+
# PRESENTATION CONTEXT:
|
1215
|
+
# Presentation context item type (1 byte)
|
1216
|
+
@outgoing.encode_last(pc, "HEX")
|
981
1217
|
# Reserved (1 byte)
|
982
1218
|
@outgoing.encode_last("00", "HEX")
|
983
|
-
#
|
984
|
-
|
985
|
-
|
986
|
-
|
987
|
-
|
988
|
-
|
989
|
-
|
990
|
-
|
991
|
-
|
992
|
-
|
1219
|
+
# Presentation context item length (2 bytes)
|
1220
|
+
if ts.is_a?(Array)
|
1221
|
+
ts_length = 4*ts.length + ts.join.length
|
1222
|
+
else # (String)
|
1223
|
+
ts_length = 4 + ts.length
|
1224
|
+
end
|
1225
|
+
if as
|
1226
|
+
items_length = 4 + (4 + as.length) + ts_length
|
1227
|
+
else
|
1228
|
+
items_length = 4 + ts_length
|
1229
|
+
end
|
1230
|
+
@outgoing.encode_last(items_length, "US")
|
1231
|
+
# Presentation context ID (1 byte)
|
1232
|
+
# Generate a number based on the index of the abstract syntax, unless one has been supplied to this method already.
|
1233
|
+
# (NB! This number should be odd, and in the range 1..255)
|
1234
|
+
if context_id
|
1235
|
+
presentation_context_id = context_id
|
1236
|
+
else
|
1237
|
+
presentation_context_id = index*2 + 1
|
1238
|
+
end
|
1239
|
+
@outgoing.encode_last(presentation_context_id, "BY")
|
1240
|
+
# Reserved (1 byte)
|
1241
|
+
@outgoing.encode_last("00", "HEX")
|
1242
|
+
# (1 byte) Reserved (for association request) & Result/reason (for association accept response)
|
1243
|
+
@outgoing.encode_last(result, "BY")
|
993
1244
|
# Reserved (1 byte)
|
994
1245
|
@outgoing.encode_last("00", "HEX")
|
995
|
-
|
996
|
-
|
997
|
-
|
998
|
-
|
1246
|
+
## ABSTRACT SYNTAX SUB-ITEM: (only for request, not response)
|
1247
|
+
if as
|
1248
|
+
# Abstract syntax item type (1 byte)
|
1249
|
+
@outgoing.encode_last(ITEM_ABSTRACT_SYNTAX, "HEX")
|
1250
|
+
# Reserved (1 byte)
|
1251
|
+
@outgoing.encode_last("00", "HEX")
|
1252
|
+
# Abstract syntax item length (2 bytes)
|
1253
|
+
@outgoing.encode_last(as.length, "US")
|
1254
|
+
# Abstract syntax (variable length)
|
1255
|
+
@outgoing.encode_last(as, "STR")
|
1256
|
+
end
|
1257
|
+
## TRANSFER SYNTAX SUB-ITEM (not included if result indicates error):
|
1258
|
+
if result == ACCEPTANCE
|
1259
|
+
ts = [ts] if ts.is_a?(String)
|
1260
|
+
ts.each do |t|
|
1261
|
+
# Transfer syntax item type (1 byte)
|
1262
|
+
@outgoing.encode_last(ITEM_TRANSFER_SYNTAX, "HEX")
|
1263
|
+
# Reserved (1 byte)
|
1264
|
+
@outgoing.encode_last("00", "HEX")
|
1265
|
+
# Transfer syntax item length (2 bytes)
|
1266
|
+
@outgoing.encode_last(t.length, "US")
|
1267
|
+
# Transfer syntax (variable length)
|
1268
|
+
@outgoing.encode_last(t, "STR")
|
1269
|
+
end
|
1270
|
+
end
|
999
1271
|
end
|
1000
|
-
# Update transfer syntax settings for this instance:
|
1001
|
-
set_transfer_syntax(ts.first)
|
1002
1272
|
end
|
1003
1273
|
|
1004
|
-
|
1005
|
-
#
|
1274
|
+
# Adds the binary string that makes up the user information part of the association request/response.
|
1275
|
+
#
|
1276
|
+
# === Parameters
|
1277
|
+
#
|
1278
|
+
# * <tt>ui</tt> -- User information items array.
|
1279
|
+
#
|
1006
1280
|
def append_user_information(ui)
|
1007
1281
|
# USER INFORMATION:
|
1008
1282
|
# User information item type (1 byte)
|
1009
|
-
@outgoing.encode_last(
|
1283
|
+
@outgoing.encode_last(ITEM_USER_INFORMATION, "HEX")
|
1010
1284
|
# Reserved (1 byte)
|
1011
1285
|
@outgoing.encode_last("00", "HEX")
|
1012
1286
|
# Encode the user information item values so we can determine the remaining length of this section:
|
@@ -1030,9 +1304,30 @@ module DICOM
|
|
1030
1304
|
end
|
1031
1305
|
end
|
1032
1306
|
|
1307
|
+
# Returns the appropriate response value for the Command Field (0000,0100) to be used in a command fragment (response).
|
1308
|
+
#
|
1309
|
+
# === Parameters
|
1310
|
+
#
|
1311
|
+
# * <tt>request</tt> -- The Command Field value in a command fragment (request).
|
1312
|
+
#
|
1313
|
+
def command_field_response(request)
|
1314
|
+
case request
|
1315
|
+
when C_STORE_RQ
|
1316
|
+
return C_STORE_RSP
|
1317
|
+
when C_ECHO_RQ
|
1318
|
+
return C_ECHO_RSP
|
1319
|
+
else
|
1320
|
+
add_error("Unknown or unsupported request (#{request}) encountered.")
|
1321
|
+
return C_CANCEL_RQ
|
1322
|
+
end
|
1323
|
+
end
|
1033
1324
|
|
1034
|
-
#
|
1035
|
-
#
|
1325
|
+
# Processes the value of the reason byte received in the association abort, and prints an explanation of the error.
|
1326
|
+
#
|
1327
|
+
# === Parameters
|
1328
|
+
#
|
1329
|
+
# * <tt>reason</tt> -- String. Reason code for an error that has occured.
|
1330
|
+
#
|
1036
1331
|
def process_reason(reason)
|
1037
1332
|
case reason
|
1038
1333
|
when "00"
|
@@ -1052,9 +1347,17 @@ module DICOM
|
|
1052
1347
|
end
|
1053
1348
|
end
|
1054
1349
|
|
1055
|
-
|
1056
|
-
#
|
1057
|
-
#
|
1350
|
+
# Processes the value of the result byte received in the association response.
|
1351
|
+
# Prints an explanation if an error is indicated.
|
1352
|
+
#
|
1353
|
+
# === Notes
|
1354
|
+
#
|
1355
|
+
# A value other than 0 indicates an error.
|
1356
|
+
#
|
1357
|
+
# === Parameters
|
1358
|
+
#
|
1359
|
+
# * <tt>result</tt> -- Fixnum. The result code from an association response.
|
1360
|
+
#
|
1058
1361
|
def process_result(result)
|
1059
1362
|
unless result == 0
|
1060
1363
|
# Analyse the result and report what is wrong:
|
@@ -1073,9 +1376,12 @@ module DICOM
|
|
1073
1376
|
end
|
1074
1377
|
end
|
1075
1378
|
|
1076
|
-
|
1077
|
-
#
|
1078
|
-
#
|
1379
|
+
# Processes the value of the source byte in the association abort, and prints an explanation of the source (of the error).
|
1380
|
+
#
|
1381
|
+
# === Parameters
|
1382
|
+
#
|
1383
|
+
# * <tt>source</tt> -- String. A code which informs which part has been the source of an error.
|
1384
|
+
#
|
1079
1385
|
def process_source(source)
|
1080
1386
|
if source == "00"
|
1081
1387
|
add_error("Warning: Connection has been aborted by the service provider because of an error by the service user (client side).")
|
@@ -1086,16 +1392,24 @@ module DICOM
|
|
1086
1392
|
end
|
1087
1393
|
end
|
1088
1394
|
|
1089
|
-
|
1090
|
-
#
|
1091
|
-
#
|
1092
|
-
#
|
1093
|
-
#
|
1395
|
+
# Processes the value of the status element (0000,0900) received in the command fragment.
|
1396
|
+
# Prints an explanation where deemed appropriate.
|
1397
|
+
#
|
1398
|
+
# === Notes
|
1399
|
+
#
|
1400
|
+
# The status element has vr 'US', and the status as reported here is therefore a number.
|
1401
|
+
# In the official DICOM documents however, the values of the various status options are given in hex format.
|
1402
|
+
# Resources: The DICOM standard; PS3.4, Annex Q 2.1.1.4 & PS3.7 Annex C 4.
|
1403
|
+
#
|
1404
|
+
# === Parameters
|
1405
|
+
#
|
1406
|
+
# * <tt>status</tt> -- Fixnum. A status code from a command fragment.
|
1407
|
+
#
|
1094
1408
|
def process_status(status)
|
1095
1409
|
case status
|
1096
1410
|
when 0 # "0000"
|
1097
1411
|
# Last fragment (Break the while loop that listens continuously for incoming packets):
|
1098
|
-
add_notice("Receipt for successful execution of the desired
|
1412
|
+
add_notice("Receipt for successful execution of the desired operation has been received.")
|
1099
1413
|
stop_receiving
|
1100
1414
|
when 42752 # "a700"
|
1101
1415
|
# Failure: Out of resources. Related fields: 0000,0902
|
@@ -1126,19 +1440,27 @@ module DICOM
|
|
1126
1440
|
end
|
1127
1441
|
end
|
1128
1442
|
|
1129
|
-
|
1130
|
-
#
|
1131
|
-
#
|
1132
|
-
#
|
1133
|
-
|
1134
|
-
|
1443
|
+
# Handles an incoming network transmission.
|
1444
|
+
# Returns the binary string data received.
|
1445
|
+
#
|
1446
|
+
# === Notes
|
1447
|
+
#
|
1448
|
+
# If a minimum length has been specified, and a message is received which is shorter than this length,
|
1449
|
+
# the method will keep listening for more incoming network packets to append.
|
1450
|
+
#
|
1451
|
+
# === Parameters
|
1452
|
+
#
|
1453
|
+
# * <tt>min_length</tt> -- Fixnum. The minimum possible length of a valid incoming transmission.
|
1454
|
+
#
|
1455
|
+
def receive_transmission(min_length=0)
|
1456
|
+
data = receive_transmission_data
|
1135
1457
|
# Check the nature of the received data variable:
|
1136
1458
|
if data
|
1137
1459
|
# Sometimes the incoming transmission may be broken up into smaller pieces:
|
1138
1460
|
# Unless a short answer is expected, we will continue to listen if the first answer was too short:
|
1139
1461
|
unless min_length == 0
|
1140
|
-
if data.length
|
1141
|
-
addition = receive_transmission_data
|
1462
|
+
if data.length < min_length
|
1463
|
+
addition = receive_transmission_data
|
1142
1464
|
data = data + addition if addition
|
1143
1465
|
end
|
1144
1466
|
end
|
@@ -1150,13 +1472,14 @@ module DICOM
|
|
1150
1472
|
return data
|
1151
1473
|
end
|
1152
1474
|
|
1153
|
-
|
1154
|
-
#
|
1155
|
-
|
1475
|
+
# Receives the data from an incoming network transmission.
|
1476
|
+
# Returns the binary string data received.
|
1477
|
+
#
|
1478
|
+
def receive_transmission_data
|
1156
1479
|
data = false
|
1157
1480
|
t1 = Time.now.to_f
|
1158
1481
|
@receive = true
|
1159
|
-
thr = Thread.new{ data
|
1482
|
+
thr = Thread.new{ data=@session.recv(@max_receive_size); @receive=false }
|
1160
1483
|
while @receive
|
1161
1484
|
if (Time.now.to_f - t1) > @timeout
|
1162
1485
|
Thread.kill(thr)
|
@@ -1167,58 +1490,96 @@ module DICOM
|
|
1167
1490
|
return data
|
1168
1491
|
end
|
1169
1492
|
|
1170
|
-
|
1171
|
-
#
|
1493
|
+
# Sets some default values related to encoding.
|
1494
|
+
#
|
1172
1495
|
def set_default_values
|
1173
1496
|
# Default endianness for network transmissions is Big Endian:
|
1174
1497
|
@net_endian = true
|
1175
1498
|
# Default endianness of data is little endian:
|
1176
1499
|
@data_endian = false
|
1177
1500
|
# It may turn out to be unncessary to define the following values at this early stage.
|
1178
|
-
# Explicitness
|
1179
|
-
@explicit =
|
1180
|
-
# Transfer syntax
|
1181
|
-
set_transfer_syntax(
|
1501
|
+
# Explicitness:
|
1502
|
+
@explicit = false
|
1503
|
+
# Transfer syntax:
|
1504
|
+
set_transfer_syntax(IMPLICIT_LITTLE_ENDIAN)
|
1182
1505
|
end
|
1183
1506
|
|
1184
|
-
|
1185
|
-
#
|
1186
|
-
|
1507
|
+
# Set instance variables related to a transfer syntax.
|
1508
|
+
#
|
1509
|
+
# === Parameters
|
1510
|
+
#
|
1511
|
+
# * <tt>syntax</tt> -- A transfer syntax string.
|
1512
|
+
#
|
1513
|
+
def set_transfer_syntax(syntax)
|
1514
|
+
@transfer_syntax = syntax
|
1187
1515
|
# Query the library with our particular transfer syntax string:
|
1188
|
-
|
1189
|
-
|
1190
|
-
unless result[0]
|
1516
|
+
valid_syntax, @explicit, @data_endian = LIBRARY.process_transfer_syntax(syntax)
|
1517
|
+
unless valid_syntax
|
1191
1518
|
add_error("Warning: Invalid/unknown transfer syntax encountered! Will try to continue, but errors may occur.")
|
1192
1519
|
end
|
1193
|
-
# Update encoding variables:
|
1194
|
-
@explicit = result[1]
|
1195
|
-
@data_endian = result[2]
|
1196
|
-
@transfer_syntax = value
|
1197
1520
|
end
|
1198
1521
|
|
1199
|
-
|
1200
|
-
#
|
1201
|
-
|
1522
|
+
# Sets the @user_information items instance array.
|
1523
|
+
#
|
1524
|
+
# === Notes
|
1525
|
+
#
|
1526
|
+
# Each user information item is a three element array consisting of: item type code, VR & value.
|
1527
|
+
#
|
1528
|
+
# === Parameters
|
1529
|
+
#
|
1530
|
+
# * <tt>info</tt> -- An association information hash.
|
1531
|
+
#
|
1532
|
+
def set_user_information_array(info=nil)
|
1202
1533
|
@user_information = [
|
1203
|
-
[
|
1204
|
-
[
|
1205
|
-
[
|
1534
|
+
[ITEM_MAX_LENGTH, "UL", @max_package_size],
|
1535
|
+
[ITEM_IMPLEMENTATION_UID, "STR", UID],
|
1536
|
+
[ITEM_IMPLEMENTATION_VERSION, "STR", NAME]
|
1206
1537
|
]
|
1207
|
-
# A bit of a hack to include "asynchronous operations window negotiation"
|
1538
|
+
# A bit of a hack to include "asynchronous operations window negotiation" and/or "role negotiation",
|
1539
|
+
# in cases where this has been included in the association request:
|
1208
1540
|
if info
|
1209
|
-
|
1541
|
+
if info[:maxnum_operations_invoked]
|
1542
|
+
@user_information.insert(2, [ITEM_MAX_OPERATIONS_INVOKED, "HEX", "00010001"])
|
1543
|
+
end
|
1544
|
+
if info[:role_negotiation]
|
1545
|
+
pos = 3
|
1546
|
+
info[:role_negotiation].each do |role|
|
1547
|
+
msg = Stream.new(message, @net_endian)
|
1548
|
+
uid = role[:sop_uid]
|
1549
|
+
# Length of UID (2 bytes):
|
1550
|
+
msg.encode_first(uid.length, "US")
|
1551
|
+
# SOP UID being negotiated (Variable length):
|
1552
|
+
msg.encode_last(uid, "STR")
|
1553
|
+
# SCU Role (Always accept SCU) (1 byte):
|
1554
|
+
if role[:scu] == 1
|
1555
|
+
msg.encode_last(1, "BY")
|
1556
|
+
else
|
1557
|
+
msg.encode_last(0, "BY")
|
1558
|
+
end
|
1559
|
+
# SCP Role (Never accept SCP) (1 byte):
|
1560
|
+
if role[:scp] == 1
|
1561
|
+
msg.encode_last(0, "BY")
|
1562
|
+
else
|
1563
|
+
msg.encode_last(1, "BY")
|
1564
|
+
end
|
1565
|
+
@user_information.insert(pos, [ITEM_ROLE_NEGOTIATION, "STR", msg.string])
|
1566
|
+
pos += 1
|
1567
|
+
end
|
1568
|
+
end
|
1210
1569
|
end
|
1211
1570
|
end
|
1212
1571
|
|
1213
|
-
|
1214
|
-
#
|
1572
|
+
# Toggles two instance variables that in causes the loops that listen for incoming network packets to break.
|
1573
|
+
#
|
1574
|
+
# === Notes
|
1575
|
+
#
|
1215
1576
|
# This method is called by the various methods that interpret incoming data when they have verified that
|
1216
1577
|
# the entire message has been received, or when a timeout is reached.
|
1578
|
+
#
|
1217
1579
|
def stop_receiving
|
1218
1580
|
@listen = false
|
1219
1581
|
@receive = false
|
1220
1582
|
end
|
1221
1583
|
|
1222
|
-
|
1223
|
-
|
1224
|
-
end # of module
|
1584
|
+
end
|
1585
|
+
end
|