dicom 0.7 → 0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- # as well as network communication.
5
+ # This class handles the construction and interpretation of network packages as well as network communication.
6
+ #
7
7
  class Link
8
8
 
9
- attr_accessor :max_package_size, :verbose, :file_handler
10
- attr_reader :errors, :notices
11
-
12
- # Initialize the instance with a host adress and a port number.
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 = 12 # minimum number of bytes to expect in an incoming transmission
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
- @connection = nil # TCP connection status
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, 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
- # Build the abort message which is transmitted when the server wishes to (abruptly) abort the connection.
40
- # For the moment: NO REASONS WILL BE PROVIDED. (and source of problems will always be set as client side)
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.set_endian(@net_endian)
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(pdu)
106
+ append_header(PDU_ABORT)
56
107
  end
57
108
 
58
-
59
- # Build the binary string that will be sent as TCP data in the Association accept response.
60
- def build_association_accept(info, ac_uid, ui, result)
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.set_endian(@net_endian)
117
+ @outgoing.endian = @net_endian
63
118
  # Clear the outgoing binary string:
64
119
  @outgoing.reset
65
- # Set item types (pdu and presentation context):
66
- pdu = "02"
67
- pc_type = "21"
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(ac_uid)
72
- # Return one presentation context for each of the proposed abstract syntaxes:
73
- abstract_syntaxes = Array.new
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
- unless abstract_syntaxes.include?(pc[:abstract_syntax])
76
- abstract_syntaxes << pc[:abstract_syntax]
77
- context_id = pc[:presentation_context_id]
78
- transfer_syntax = pc[:ts].first[:transfer_syntax]
79
- append_presentation_context(abstract_syntax, pc_type, transfer_syntax, context_id, result)
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(ui)
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(pdu)
137
+ append_association_header(PDU_ASSOCIATION_ACCEPT, info[:called_ae])
85
138
  end
86
139
 
87
-
88
- # Build the binary string that will be sent as TCP data in the association rejection.
89
- # NB: For the moment, this method will only customize the "reason" value.
90
- # For a list of error codes, see the official dicom PS3.8 document, page 41.
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.set_endian(@net_endian)
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(pdu)
166
+ append_header(PDU_ASSOCIATION_REJECT)
108
167
  end
109
168
 
110
-
111
- # Build the binary string that will be sent as TCP data in the Association request.
112
- def build_association_request(ac_uid, as, ts, ui)
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.set_endian(@net_endian)
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
- append_presentation_context(as, pc, ts)
124
- append_user_information(ui)
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(pdu)
189
+ append_association_header(PDU_ASSOCIATION_REQUEST, @host_ae)
127
190
  end
128
191
 
129
-
130
- # Build the binary string that will be sent as TCP data in the query command fragment.
131
- # Typical values:
132
- # pdu = "04" (data), context = "01", flags = "03"
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.set_endian(@data_endian)
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.set_endian(@net_endian)
231
+ @outgoing.endian = @net_endian
164
232
  # Flags (1 byte)
165
- @outgoing.encode_first(flags, "HEX") # Command, last fragment (identifier)
233
+ @outgoing.encode_first(flags, "HEX")
166
234
  # Presentation context ID (1 byte)
167
- @outgoing.encode_first(context, "HEX") # Explicit VR Little Endian, Study Root Query/Retrieve.... (what does this reference, the earlier abstract syntax? transfer syntax?)
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
- # Build the binary string that will be sent as TCP data in the query data fragment.
176
- # The style of encoding will depend on whether we have an implicit or explicit transfer syntax.
177
- def build_data_fragment(data_elements)
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.set_endian(@data_endian)
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.set_endian(@net_endian)
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("01", "HEX") # Explicit VR Little Endian, Study Root Query/Retrieve.... (what does this reference, the earlier abstract syntax? transfer syntax?)
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(pdu)
292
+ append_header(PDU_DATA)
216
293
  end
217
294
 
218
-
219
- # Build the binary string that will be sent as TCP data in the association release request:
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.set_endian(@net_endian)
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(pdu)
304
+ append_header(PDU_RELEASE_REQUEST)
229
305
  end
230
306
 
231
-
232
- # Build the binary string that will be sent as TCP data in the association release response.
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.set_endian(@net_endian)
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(pdu)
316
+ append_header(PDU_RELEASE_RESPONSE)
242
317
  end
243
318
 
244
-
245
- # Build the binary string that makes up a storage data fragment.
246
- # Typical value: flags = "00" (more fragments following), flags = "02" (last fragment)
247
- # pdu = "04", context = "01"
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.set_endian(@net_endian)
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, "HEX")
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
- # Extracts the abstrax syntax from the first presentation context in the info hash object:
268
- def extract_abstract_syntax(info)
269
- return info[:pc].first[:abstract_syntax]
270
- end
271
-
272
-
273
- # Extracts the (first) transfer syntax from the first presentation context in the info hash object:
274
- def extract_transfer_syntax(info)
275
- return info[:pc].first[:ts].first[:transfer_syntax]
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 "01" # Associatin request
357
+ when PDU_ASSOCIATION_REQUEST
283
358
  info = interpret_association_request(message)
284
- when "02" # Accepted association
359
+ when PDU_ASSOCIATION_ACCEPT
285
360
  info = interpret_association_accept(message)
286
- when "03" # Rejected association
361
+ when PDU_ASSOCIATION_REJECT
287
362
  info = interpret_association_reject(message)
288
- when "04" # Data
363
+ when PDU_DATA
289
364
  info = interpret_command_and_data(message, file)
290
- when "05"
365
+ when PDU_RELEASE_REQUEST
291
366
  info = interpret_release_request(message)
292
- when "06" # Release response
367
+ when PDU_RELEASE_RESPONSE
293
368
  info = interpret_release_response(message)
294
- when "07" # Abort connection
369
+ when PDU_ABORT
295
370
  info = interpret_abort(message)
296
371
  else
297
372
  info = {:valid => false}
298
- add_error("An unknown pdu type was received in the incoming transmission. Can not decode this message. (pdu: #{pdu})")
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
- # Handles the abortion of a session, when a non-valid message has been received.
305
- def handle_abort(session)
306
- add_notice("An unregonizable (non-DICOM) message was received.")
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(session)
387
+ transmit
309
388
  end
310
389
 
311
-
312
- # Handles the association accept.
313
- def handle_association_accept(session, info, syntax_result)
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
- application_context = info[:application_context]
399
+ # Build message string and send it:
317
400
  set_user_information_array(info)
318
- build_association_accept(info, application_context, @user_information, syntax_result)
319
- transmit(session)
401
+ build_association_accept(info)
402
+ transmit
320
403
  end
321
404
 
322
-
323
- # Process the data that was received from the user.
324
- # We expect this to be an initial C-STORE-RQ followed by a bunch of data fragments.
325
- def handle_incoming_data(session, path)
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(session, file = true)
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
- # Try to extract data:
332
- file_data = Array.new
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] == "00" or info[:presentation_context_flag] == "02"
337
- # Data (last fragment)
434
+ if info[:presentation_context_flag] == DATA_MORE_FRAGMENTS
338
435
  @data_results << info[:results]
339
- file_data << info[:bin]
340
- elsif info[:presentation_context_flag] == "03"
341
- # Command (last fragment):
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
- data = file_data.join
348
- if data.length > 8
349
- # Read the received data stream and load it as a DICOM object:
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
- # Handles the rejection of an association, when the formalities of the association is not correct.
365
- def handle_rejection(session)
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(session)
463
+ transmit
372
464
  end
373
465
 
374
-
375
- # Handles the release of an association.
376
- def handle_release(session)
377
- segments = receive_single_transmission(session)
378
- info = segments.first
379
- if info[:pdu] == "05"
380
- add_notice("Received a release request. Releasing association.")
381
- build_release_response
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
- # Handles the response (C-STORE-RSP) when a DICOM object has been (successfully) received.
388
- def handle_response(session)
389
- tags = @command_results.first
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", tags["0000,0002"]]
487
+ command_elements << ["0000,0002", "UI", @command_request["0000,0002"]]
394
488
  # Command Field:
395
- command_elements << ["0000,0100", "US", 32769] # C-STORE-RSP
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", tags["0000,0110"]] # (Message ID)
491
+ command_elements << ["0000,0120", "US", @command_request["0000,0110"]]
398
492
  # Data Set Type:
399
- command_elements << ["0000,0800", "US", 257]
493
+ command_elements << ["0000,0800", "US", NO_DATA_SET_PRESENT]
400
494
  # Status:
401
- command_elements << ["0000,0900", "US", 0] # (Success)
495
+ command_elements << ["0000,0900", "US", SUCCESS]
402
496
  # Affected SOP Instance UID:
403
- command_elements << ["0000,1000", "UI", tags["0000,1000"]]
404
- pdu = "04"
405
- context = @presentation_context_id
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
- # Decode an incoming transmission., decide its type, and forward its content to the various methods that process these.
413
- def interpret(message, file = nil)
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, @explicit)
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
- # Decode the binary string received when the provider wishes to abort the connection, for some reason.
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, @explicit)
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
- # Decode the binary string received in the association response, and interpret its content.
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, @explicit)
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
- # Item type (1 byte)
517
- info[:presentation_item_type] = msg.decode(1, "HEX")
518
- # Reserved (1 byte)
519
- msg.skip(1)
520
- # Presentation item length (2 bytes)
521
- info[:presentation_item_length] = msg.decode(2, "US")
522
- # Presentation context ID (1 byte)
523
- info[:presentation_context_id] = msg.decode(1, "HEX")
524
- # Reserved (1 byte)
525
- msg.skip(1)
526
- # Result (& Reason) (1 byte)
527
- info[:result] = msg.decode(1, "BY")
528
- process_result(info[:result])
529
- # Reserved (1 byte)
530
- msg.skip(1)
531
- # Transfer syntax sub-item:
532
- # Item type (1 byte)
533
- info[:transfer_syntax_item_type] = msg.decode(1, "HEX")
534
- # Reserved (1 byte)
535
- msg.skip(1)
536
- # Transfer syntax item length (2 bytes)
537
- info[:transfer_syntax_item_length] = msg.decode(2, "US")
538
- # Transfer syntax name (variable length)
539
- info[:transfer_syntax] = msg.decode(info[:transfer_syntax_item_length], "STR")
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) ("50")
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 "51"
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 "52"
685
+ when ITEM_IMPLEMENTATION_UID
559
686
  info[:implementation_class_uid] = msg.decode(item_length, "STR")
560
- when "53"
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 "55"
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: " + 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 # of interpret_association_accept
578
-
718
+ end
579
719
 
580
- # Decode the association reject message and extract the error reasons given.
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, @explicit)
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
- # Decode the binary string received in the association request, and interpret its content.
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, @explicit)
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") # "10"
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 == "20"
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, "HEX")
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") # "30"
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 == "40"
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 "51"
852
+ when ITEM_MAX_LENGTH
702
853
  info[:max_pdu_length] = msg.decode(item_length, "UL")
703
- when "52"
854
+ when ITEM_IMPLEMENTATION_UID
704
855
  info[:implementation_class_uid] = msg.decode(item_length, "STR")
705
- when "53"
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 "55"
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 # of interpret_association_request
722
-
888
+ end
723
889
 
724
- # Decode the received command/data binary string, and interpret its content.
725
- # Decoding of data fragment will depend on the explicitness of the transmission.
726
- def interpret_command_and_data(message, file = nil)
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, @explicit)
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, "HEX") # "01" expected
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
- # Little endian encoding from now on:
738
- msg.set_endian(@data_endian)
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] == "03"
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
- elsif info[:presentation_context_flag] == "00" or info[:presentation_context_flag] == "02"
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
- # Abort the listening if this is last data fragment:
777
- if info[:presentation_context_flag] == "02"
778
- stop_receiving
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
- # Decode the binary string received in the release request, and interpret its content.
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, @explicit)
1016
+ msg = Stream.new(message, @net_endian)
827
1017
  # Reserved (4 bytes)
828
1018
  reserved_bytes = msg.decode(4, "HEX")
829
- stop_receiving
1019
+ handle_release
830
1020
  info[:valid] = true
831
1021
  return info
832
1022
  end
833
1023
 
834
-
835
- # Decode the binary string received in the release response, and interpret its content.
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, @explicit)
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
- # Handle multiple incoming transmissions and return the interpreted, received data.
848
- def receive_multiple_transmissions(session, file = nil)
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(session, @min_length)
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
- # Handle an expected single incoming transmission and return the interpreted, received data.
867
- def receive_single_transmission(session)
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(session, min_length)
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
- # Send the encoded binary string (package) to its destination.
877
- def transmit(session)
878
- session.send(@outgoing.string, 0)
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 if verbose variable is true, prints the message as well.
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 if verbosity is set for these kinds of messages, prints it to the screen as well.
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
- # Builds the application context that is part of the association request.
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("10", "HEX")
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
- # Build the binary string that makes up the header part (part of the association request).
916
- def append_association_header(pdu)
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.set_endian(@net_endian)
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
- called_ae = @outgoing.encode_string_with_trailing_spaces(@host_ae, 16)
928
- @outgoing.add_first(called_ae) # (pre-encoded value)
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
- # Adds the header bytes to the outgoing, binary string (this part has the same structure for all dicom network messages)
938
- # PDU: "01", "02", etc..
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
- # Build the binary string that makes up the presentation context part (part of the association request).
950
- # For a list of error codes, see the official DICOM PS3.8 document, (page 39).
951
- def append_presentation_context(as, pc, ts, context_id = "01", result = "00")
952
- # PRESENTATION CONTEXT:
953
- # Presentation context item type (1 byte)
954
- @outgoing.encode_last(pc, "HEX") # "20" (request) & "21" (response)
955
- # Reserved (1 byte)
956
- @outgoing.encode_last("00", "HEX")
957
- # Presentation context item length (2 bytes)
958
- if ts.is_a?(Array)
959
- ts_length = 4*ts.length + ts.join.length
960
- else # (String)
961
- ts_length = 4 + ts.length
962
- end
963
- if as
964
- items_length = 4 + (4 + as.length) + ts_length
965
- else
966
- items_length = 4 + ts_length
967
- end
968
- @outgoing.encode_last(items_length, "US")
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
- # Abstract syntax item length (2 bytes)
984
- @outgoing.encode_last(as.length, "US")
985
- # Abstract syntax (variable length)
986
- @outgoing.encode_last(as, "STR")
987
- end
988
- ## TRANSFER SYNTAX SUB-ITEM:
989
- ts = [ts] if ts.is_a?(String)
990
- ts.each do |t|
991
- # Transfer syntax item type (1 byte)
992
- @outgoing.encode_last("40", "HEX")
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
- # Transfer syntax item length (2 bytes)
996
- @outgoing.encode_last(t.length, "US")
997
- # Transfer syntax (variable length)
998
- @outgoing.encode_last(t, "STR")
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
- # Adds the binary string that makes up the user information (part of the association request).
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("50", "HEX")
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
- # Process the value of the reason byte (in an association abort).
1035
- # This will provide information on what is the reason for the error.
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
- # Process the value of the result byte (in the association response).
1057
- # Something is wrong if result is different from 0.
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
- # Process the value of the source byte (in an association abort).
1078
- # This will provide information on who is the source of the error.
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
- # Process the value of the status tag 0000,0900 received in the command fragment.
1091
- # Note: The status tag has vr 'US', and the status as reported here is therefore a number.
1092
- # In the official DICOM documents however, the value of the various status options is given in hex format.
1093
- # Resources: DICOM PS3.4 Annex Q 2.1.1.4, DICOM PS3.7 Annex C 4.
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 request has been received. Closing communication.")
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
- # Handles an incoming transmission.
1131
- # Optional: Specify a minimum length of the incoming transmission. (If a message is received
1132
- # which is shorter than this limit, the method will keep listening for more incoming packets to append)
1133
- def receive_transmission(session, min_length=0)
1134
- data = receive_transmission_data(session)
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 <= min_length
1141
- addition = receive_transmission_data(session)
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
- # Receives the incoming transmission data.
1155
- def receive_transmission_data(session)
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 = session.recv(@max_receive_size); @receive = false }
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
- # Some default values.
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 = true
1180
- # Transfer syntax (Implicit, little endian):
1181
- set_transfer_syntax("1.2.840.10008.1.2")
1501
+ # Explicitness:
1502
+ @explicit = false
1503
+ # Transfer syntax:
1504
+ set_transfer_syntax(IMPLICIT_LITTLE_ENDIAN)
1182
1505
  end
1183
1506
 
1184
-
1185
- # Set instance variables related to the transfer syntax.
1186
- def set_transfer_syntax(value)
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
- result = LIBRARY.process_transfer_syntax(value)
1189
- # Result is a 3-element array: [Validity of ts, explicitness, endianness]
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
- # Set user information [item type code, vr/type, value]
1201
- def set_user_information_array(info = nil)
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
- ["51", "UL", @max_package_size], # Max PDU Length
1204
- ["52", "STR", UID],
1205
- ["55", "STR", NAME]
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", if this has been included in the association request:
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
- @user_information.insert(2, ["53", "HEX", "00010001"]) if info[:maxnum_operations_invoked]
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
- # Breaks the loops that listen for incoming packets by changing a couple of instance variables.
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
- end # of class
1224
- end # of module
1584
+ end
1585
+ end