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.
@@ -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