dicom 0.9.6 → 0.9.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,1528 +1,1529 @@
1
- module DICOM
2
-
3
- # This class handles the construction and interpretation of network packages
4
- # as well as network communication.
5
- #
6
- class Link
7
- include Logging
8
-
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 TCP network session where the DICOM communication is done with a remote host or client.
16
- attr_reader :session
17
-
18
- # Creates a Link instance, which is used by both DClient and DServer to handle network communication.
19
- #
20
- # === Parameters
21
- #
22
- # * <tt>options</tt> -- A hash of parameters.
23
- #
24
- # === Options
25
- #
26
- # * <tt>:ae</tt> -- String. The name of the client (application entity).
27
- # * <tt>:file_handler</tt> -- A customized FileHandler class to use instead of the default FileHandler.
28
- # * <tt>:host_ae</tt> -- String. The name of the server (application entity).
29
- # * <tt>:max_package_size</tt> -- Fixnum. The maximum allowed size of network packages (in bytes).
30
- # * <tt>:timeout</tt> -- Fixnum. The maximum period to wait for an answer before aborting the communication.
31
- #
32
- def initialize(options={})
33
- require 'socket'
34
- # Optional parameters (and default values):
35
- @file_handler = options[:file_handler] || FileHandler
36
- @ae = options[:ae] || "RUBY_DICOM"
37
- @host_ae = options[:host_ae] || "DEFAULT"
38
- @max_package_size = options[:max_package_size] || 32768 # 16384
39
- @max_receive_size = @max_package_size
40
- @timeout = options[:timeout] || 10 # seconds
41
- @min_length = 10 # minimum number of bytes to expect in an incoming transmission
42
- # Variables used for monitoring state of transmission:
43
- @session = nil # TCP connection
44
- @association = nil # DICOM Association status
45
- @request_approved = nil # Status of our DICOM request
46
- @release = nil # Status of received, valid release response
47
- @command_request = Hash.new
48
- @presentation_contexts = Hash.new # Keeps track of the relationship between pc id and it's transfer syntax
49
- set_default_values
50
- set_user_information_array
51
- @outgoing = Stream.new(string=nil, endian=true)
52
- end
53
-
54
- # Waits for an SCU to issue a release request, and answers it by launching the handle_release method.
55
- # If invalid or no message is received, the connection is closed.
56
- #
57
- def await_release
58
- segments = receive_single_transmission
59
- info = segments.first
60
- if info[:pdu] != PDU_RELEASE_REQUEST
61
- # For some reason we didn't get our expected release request. Determine why:
62
- if info[:valid]
63
- logger.error("Unexpected message type received (PDU: #{info[:pdu]}). Expected a release request. Closing the connection.")
64
- handle_abort(false)
65
- else
66
- logger.error("Timed out while waiting for a release request. Closing the connection.")
67
- end
68
- stop_session
69
- else
70
- # Properly release the association:
71
- handle_release
72
- end
73
- end
74
-
75
- # Builds the abort message which is transmitted when the server wishes to (abruptly) abort the connection.
76
- #
77
- # === Restrictions
78
- #
79
- # For now, no reasons for the abortion are provided (and source of problems will always be set as client side).
80
- #
81
- def build_association_abort
82
- # Big endian encoding:
83
- @outgoing.endian = @net_endian
84
- # Clear the outgoing binary string:
85
- @outgoing.reset
86
- # Reserved (2 bytes)
87
- @outgoing.encode_last("00"*2, "HEX")
88
- # Source (1 byte)
89
- source = "00" # (client side error)
90
- @outgoing.encode_last(source, "HEX")
91
- # Reason/Diag. (1 byte)
92
- reason = "00" # (Reason not specified)
93
- @outgoing.encode_last(reason, "HEX")
94
- append_header(PDU_ABORT)
95
- end
96
-
97
- # Builds the binary string which is sent as the association accept (in response to an association request).
98
- #
99
- # === Parameters
100
- #
101
- # * <tt>info</tt> -- The association information hash.
102
- #
103
- def build_association_accept(info)
104
- # Big endian encoding:
105
- @outgoing.endian = @net_endian
106
- # Clear the outgoing binary string:
107
- @outgoing.reset
108
- # No abstract syntax in association response. To make this work with the method that
109
- # encodes the presentation context, we pass on a one-element array containing nil).
110
- abstract_syntaxes = Array.new(1, nil)
111
- # Note: The order of which these components are built is not arbitrary.
112
- append_application_context
113
- # Reset the presentation context instance variable:
114
- @presentation_contexts = Hash.new
115
- # Create the presentation context hash object that will be passed to the builder method:
116
- p_contexts = Hash.new
117
- # Build the presentation context strings, one by one:
118
- info[:pc].each do |pc|
119
- @presentation_contexts[pc[:presentation_context_id]] = pc[:selected_transfer_syntax]
120
- # Add the information from this pc item to the p_contexts hash:
121
- p_contexts[pc[:abstract_syntax]] = Hash.new unless p_contexts[pc[:abstract_syntax]]
122
- p_contexts[pc[:abstract_syntax]][pc[:presentation_context_id]] = {:transfer_syntaxes => [pc[:selected_transfer_syntax]], :result => pc[:result]}
123
- end
124
- append_presentation_contexts(p_contexts, ITEM_PRESENTATION_CONTEXT_RESPONSE)
125
- append_user_information(@user_information)
126
- # Header must be built last, because we need to know the length of the other components.
127
- append_association_header(PDU_ASSOCIATION_ACCEPT, info[:called_ae])
128
- end
129
-
130
- # Builds the binary string which is sent as the association reject (in response to an association request).
131
- #
132
- # === Parameters
133
- #
134
- # * <tt>info</tt> -- The association information hash.
135
- #
136
- # === Restrictions
137
- #
138
- # * For now, this method will only customize the "reason" value.
139
- # * For a list of error codes, see the DICOM standard, PS3.8 Chapter 9.3.4, Table 9-21.
140
- #
141
- def build_association_reject(info)
142
- # Big endian encoding:
143
- @outgoing.endian = @net_endian
144
- # Clear the outgoing binary string:
145
- @outgoing.reset
146
- # Reserved (1 byte)
147
- @outgoing.encode_last("00", "HEX")
148
- # Result (1 byte)
149
- @outgoing.encode_last("01", "HEX") # 1 for permament, 2 for transient
150
- # Source (1 byte)
151
- # (1: Service user, 2: Service provider (ACSE related function), 3: Service provider (Presentation related function)
152
- @outgoing.encode_last("01", "HEX")
153
- # Reason (1 byte)
154
- reason = info[:reason]
155
- @outgoing.encode_last(reason, "HEX")
156
- append_header(PDU_ASSOCIATION_REJECT)
157
- end
158
-
159
- # Builds the binary string which is sent as the association request.
160
- #
161
- # === Parameters
162
- #
163
- # * <tt>presentation_contexts</tt> -- A hash containing abstract_syntaxes, presentation context ids and transfer syntaxes.
164
- # * <tt>user_info</tt> -- A user information items array.
165
- #
166
- def build_association_request(presentation_contexts, user_info)
167
- # Big endian encoding:
168
- @outgoing.endian = @net_endian
169
- # Clear the outgoing binary string:
170
- @outgoing.reset
171
- # Note: The order of which these components are built is not arbitrary.
172
- # (The first three are built 'in order of appearance', the header is built last, but is put first in the message)
173
- append_application_context
174
- append_presentation_contexts(presentation_contexts, ITEM_PRESENTATION_CONTEXT_REQUEST, request=true)
175
- append_user_information(user_info)
176
- # Header must be built last, because we need to know the length of the other components.
177
- append_association_header(PDU_ASSOCIATION_REQUEST, @host_ae)
178
- end
179
-
180
- # Builds the binary string which is sent as a command fragment.
181
- #
182
- # === Parameters
183
- #
184
- # * <tt>pdu</tt> -- The command fragment's PDU string.
185
- # * <tt>context</tt> -- Presentation context ID byte (references a presentation context from the association).
186
- # * <tt>flags</tt> -- The flag string, which identifies if this is the last command fragment or not.
187
- # * <tt>command_elements</tt> -- An array of command elements.
188
- #
189
- def build_command_fragment(pdu, context, flags, command_elements)
190
- # Little endian encoding:
191
- @outgoing.endian = @data_endian
192
- # Clear the outgoing binary string:
193
- @outgoing.reset
194
- # Build the last part first, the Command items:
195
- command_elements.each do |element|
196
- # Tag (4 bytes)
197
- @outgoing.add_last(@outgoing.encode_tag(element[0]))
198
- # Encode the value first, so we know its length:
199
- value = @outgoing.encode_value(element[2], element[1])
200
- # Length (2 bytes)
201
- @outgoing.encode_last(value.length, "US")
202
- # Reserved (2 bytes)
203
- @outgoing.encode_last("0000", "HEX")
204
- # Value (variable length)
205
- @outgoing.add_last(value)
206
- end
207
- # The rest of the command fragment will be buildt in reverse, all the time
208
- # putting the elements first in the outgoing binary string.
209
- # Group length item:
210
- # Value (4 bytes)
211
- @outgoing.encode_first(@outgoing.string.length, "UL")
212
- # Reserved (2 bytes)
213
- @outgoing.encode_first("0000", "HEX")
214
- # Length (2 bytes)
215
- @outgoing.encode_first(4, "US")
216
- # Tag (4 bytes)
217
- @outgoing.add_first(@outgoing.encode_tag("0000,0000"))
218
- # Big endian encoding from now on:
219
- @outgoing.endian = @net_endian
220
- # Flags (1 byte)
221
- @outgoing.encode_first(flags, "HEX")
222
- # Presentation context ID (1 byte)
223
- @outgoing.encode_first(context, "BY")
224
- # Length (of remaining data) (4 bytes)
225
- @outgoing.encode_first(@outgoing.string.length, "UL")
226
- # PRESENTATION DATA VALUE (the above)
227
- append_header(pdu)
228
- end
229
-
230
- # Builds the binary string which is sent as a data fragment.
231
- #
232
- # === Notes
233
- #
234
- # * The style of encoding will depend on whether we have an implicit or explicit transfer syntax.
235
- #
236
- # === Parameters
237
- #
238
- # * <tt>data_elements</tt> -- An array of data elements.
239
- # * <tt>presentation_context_id</tt> -- Presentation context ID byte (references a presentation context from the association).
240
- #
241
- def build_data_fragment(data_elements, presentation_context_id)
242
- # Set the transfer syntax to be used for encoding the data fragment:
243
- set_transfer_syntax(@presentation_contexts[presentation_context_id])
244
- # Endianness of data fragment:
245
- @outgoing.endian = @data_endian
246
- # Clear the outgoing binary string:
247
- @outgoing.reset
248
- # Build the last part first, the Data items:
249
- data_elements.each do |element|
250
- # Encode all tags (even tags which are empty):
251
- # Tag (4 bytes)
252
- @outgoing.add_last(@outgoing.encode_tag(element[0]))
253
- # Encode the value in advance of putting it into the message, so we know its length:
254
- vr = LIBRARY.element(element[0]).vr
255
- value = @outgoing.encode_value(element[1], vr)
256
- if @explicit
257
- # Type (VR) (2 bytes)
258
- @outgoing.encode_last(vr, "STR")
259
- # Length (2 bytes)
260
- @outgoing.encode_last(value.length, "US")
261
- else
262
- # Implicit:
263
- # Length (4 bytes)
264
- @outgoing.encode_last(value.length, "UL")
265
- end
266
- # Value (variable length)
267
- @outgoing.add_last(value)
268
- end
269
- # The rest of the data fragment will be built in reverse, all the time
270
- # putting the elements first in the outgoing binary string.
271
- # Big endian encoding from now on:
272
- @outgoing.endian = @net_endian
273
- # Flags (1 byte)
274
- @outgoing.encode_first("02", "HEX") # Data, last fragment (identifier)
275
- # Presentation context ID (1 byte)
276
- @outgoing.encode_first(presentation_context_id, "BY")
277
- # Length (of remaining data) (4 bytes)
278
- @outgoing.encode_first(@outgoing.string.length, "UL")
279
- # PRESENTATION DATA VALUE (the above)
280
- append_header(PDU_DATA)
281
- end
282
-
283
- # Builds the binary string which is sent as the release request.
284
- #
285
- def build_release_request
286
- # Big endian encoding:
287
- @outgoing.endian = @net_endian
288
- # Clear the outgoing binary string:
289
- @outgoing.reset
290
- # Reserved (4 bytes)
291
- @outgoing.encode_last("00"*4, "HEX")
292
- append_header(PDU_RELEASE_REQUEST)
293
- end
294
-
295
- # Builds the binary string which is sent as the release response (which follows a release request).
296
- #
297
- def build_release_response
298
- # Big endian encoding:
299
- @outgoing.endian = @net_endian
300
- # Clear the outgoing binary string:
301
- @outgoing.reset
302
- # Reserved (4 bytes)
303
- @outgoing.encode_last("00000000", "HEX")
304
- append_header(PDU_RELEASE_RESPONSE)
305
- end
306
-
307
- # Builds the binary string which makes up a C-STORE data fragment.
308
- #
309
- # === Parameters
310
- #
311
- # * <tt>pdu</tt> -- The data fragment's PDU string.
312
- # * <tt>context</tt> -- Presentation context ID byte (references a presentation context from the association).
313
- # * <tt>flags</tt> -- The flag string, which identifies if this is the last data fragment or not.
314
- # * <tt>body</tt> -- A pre-encoded binary string (typicall a segment of a DICOM file to be transmitted).
315
- #
316
- def build_storage_fragment(pdu, context, flags, body)
317
- # Big endian encoding:
318
- @outgoing.endian = @net_endian
319
- # Clear the outgoing binary string:
320
- @outgoing.reset
321
- # Build in reverse, putting elements in front of the binary string:
322
- # Insert the data (body):
323
- @outgoing.add_last(body)
324
- # Flags (1 byte)
325
- @outgoing.encode_first(flags, "HEX")
326
- # Context ID (1 byte)
327
- @outgoing.encode_first(context, "BY")
328
- # PDV Length (of remaining data) (4 bytes)
329
- @outgoing.encode_first(@outgoing.string.length, "UL")
330
- # PRESENTATION DATA VALUE (the above)
331
- append_header(pdu)
332
- end
333
-
334
- # Delegates an incoming message to its appropriate interpreter method, based on its pdu type.
335
- # Returns the interpreted information hash.
336
- #
337
- # === Parameters
338
- #
339
- # * <tt>message</tt> -- The binary message string.
340
- # * <tt>pdu</tt> -- The PDU string of the message.
341
- # * <tt>file</tt> -- A boolean used to inform whether an incoming data fragment is part of a DICOM file reception or not.
342
- #
343
- def forward_to_interpret(message, pdu, file=nil)
344
- case pdu
345
- when PDU_ASSOCIATION_REQUEST
346
- info = interpret_association_request(message)
347
- when PDU_ASSOCIATION_ACCEPT
348
- info = interpret_association_accept(message)
349
- when PDU_ASSOCIATION_REJECT
350
- info = interpret_association_reject(message)
351
- when PDU_DATA
352
- info = interpret_command_and_data(message, file)
353
- when PDU_RELEASE_REQUEST
354
- info = interpret_release_request(message)
355
- when PDU_RELEASE_RESPONSE
356
- info = interpret_release_response(message)
357
- when PDU_ABORT
358
- info = interpret_abort(message)
359
- else
360
- info = {:valid => false}
361
- logger.error("An unknown PDU type was received in the incoming transmission. Can not decode this message. (PDU: #{pdu})")
362
- end
363
- return info
364
- end
365
-
366
- # Handles the abortion of a session, when a non-valid or unexpected message has been received.
367
- #
368
- # === Parameters
369
- #
370
- # * <tt>default_message</tt> -- A boolean which unless set as nil/false will make the method print the default status message.
371
- #
372
- def handle_abort(default_message=true)
373
- logger.warn("An unregonizable (non-DICOM) message was received.") if default_message
374
- build_association_abort
375
- transmit
376
- end
377
-
378
- # Handles the outgoing association accept message.
379
- #
380
- # === Parameters
381
- #
382
- # * <tt>info</tt> -- The association information hash.
383
- #
384
- def handle_association_accept(info)
385
- # Update the variable for calling ae (information gathered in the association request):
386
- @ae = info[:calling_ae]
387
- # Build message string and send it:
388
- set_user_information_array(info)
389
- build_association_accept(info)
390
- transmit
391
- end
392
-
393
- # Processes incoming command & data fragments for the DServer.
394
- # Returns a success boolean and an array of status messages.
395
- #
396
- # === Notes
397
- #
398
- # The incoming traffic will in most cases be: A C-STORE-RQ (command fragment) followed by a bunch of data fragments.
399
- # However, it may also be a C-ECHO-RQ command fragment, which is used to test connections.
400
- #
401
- # === Parameters
402
- #
403
- # * <tt>path</tt> -- The path used to save incoming DICOM files.
404
- #
405
- #--
406
- # FIXME: The code which handles incoming data isnt quite satisfactory. It would probably be wise to rewrite it at some stage to clean up
407
- # the code somewhat. Probably a better handling of command requests (and their corresponding data fragments) would be a good idea.
408
- #
409
- def handle_incoming_data(path)
410
- # Wait for incoming data:
411
- segments = receive_multiple_transmissions(file=true)
412
- # Reset command results arrays:
413
- @command_results = Array.new
414
- @data_results = Array.new
415
- file_transfer_syntaxes = Array.new
416
- files = Array.new
417
- single_file_data = Array.new
418
- # Proceed to extract data from the captured segments:
419
- segments.each do |info|
420
- if info[:valid]
421
- # Determine if it is command or data:
422
- if info[:presentation_context_flag] == DATA_MORE_FRAGMENTS
423
- @data_results << info[:results]
424
- single_file_data << info[:bin]
425
- elsif info[:presentation_context_flag] == DATA_LAST_FRAGMENT
426
- @data_results << info[:results]
427
- single_file_data << info[:bin]
428
- # Join the recorded data binary strings together to make a DICOM file binary string and put it in our files Array:
429
- files << single_file_data.join
430
- single_file_data = Array.new
431
- elsif info[:presentation_context_flag] == COMMAND_LAST_FRAGMENT
432
- @command_results << info[:results]
433
- @presentation_context_id = info[:presentation_context_id] # Does this actually do anything useful?
434
- file_transfer_syntaxes << @presentation_contexts[info[:presentation_context_id]]
435
- end
436
- end
437
- end
438
- # Process the received files using the customizable FileHandler class:
439
- success, messages = @file_handler.receive_files(path, files, file_transfer_syntaxes)
440
- return success, messages
441
- end
442
-
443
- # Handles the rejection message (The response used to an association request when its formalities are not correct).
444
- #
445
- def handle_rejection
446
- logger.warn("An incoming association request was rejected. Error code: #{association_error}")
447
- # Insert the error code in the info hash:
448
- info[:reason] = association_error
449
- # Send an association rejection:
450
- build_association_reject(info)
451
- transmit
452
- end
453
-
454
- # Handles the release message (which is the response to a release request).
455
- #
456
- def handle_release
457
- stop_receiving
458
- logger.info("Received a release request. Releasing association.")
459
- build_release_response
460
- transmit
461
- stop_session
462
- end
463
-
464
- # Handles the command fragment response.
465
- #
466
- # === Notes
467
- #
468
- # This is usually a C-STORE-RSP which follows the (successful) reception of a DICOM file, but may also
469
- # be a C-ECHO-RSP in response to an echo request.
470
- #
471
- def handle_response
472
- # Need to construct the command elements array:
473
- command_elements = Array.new
474
- # SOP Class UID:
475
- command_elements << ["0000,0002", "UI", @command_request["0000,0002"]]
476
- # Command Field:
477
- command_elements << ["0000,0100", "US", command_field_response(@command_request["0000,0100"])]
478
- # Message ID Being Responded To:
479
- command_elements << ["0000,0120", "US", @command_request["0000,0110"]]
480
- # Data Set Type:
481
- command_elements << ["0000,0800", "US", NO_DATA_SET_PRESENT]
482
- # Status:
483
- command_elements << ["0000,0900", "US", SUCCESS]
484
- # Affected SOP Instance UID:
485
- command_elements << ["0000,1000", "UI", @command_request["0000,1000"]] if @command_request["0000,1000"]
486
- build_command_fragment(PDU_DATA, @presentation_context_id, COMMAND_LAST_FRAGMENT, command_elements)
487
- transmit
488
- end
489
-
490
- # Decodes the header of an incoming message, analyzes its real length versus expected length, and handles any
491
- # deviations to make sure that message strings are split up appropriately before they are being forwarded to interpretation.
492
- # Returns an array of information hashes.
493
- #
494
- # === Parameters
495
- #
496
- # * <tt>message</tt> -- The binary message string.
497
- # * <tt>file</tt> -- A boolean used to inform whether an incoming data fragment is part of a DICOM file reception or not.
498
- #
499
- #--
500
- # FIXME: This method is rather complex and doesnt feature the best readability. A rewrite that is able to simplify it would be lovely.
501
- #
502
- def interpret(message, file=nil)
503
- if @first_part
504
- message = @first_part + message
505
- @first_part = nil
506
- end
507
- segments = Array.new
508
- # If the message is at least 8 bytes we can start decoding it:
509
- if message.length > 8
510
- # Create a new Stream instance to handle this response.
511
- msg = Stream.new(message, @net_endian)
512
- # PDU type ( 1 byte)
513
- pdu = msg.decode(1, "HEX")
514
- # Reserved (1 byte)
515
- msg.skip(1)
516
- # Length of remaining data (4 bytes)
517
- specified_length = msg.decode(4, "UL")
518
- # Analyze the remaining length of the message versurs the specified_length value:
519
- if msg.rest_length > specified_length
520
- # If the remaining length of the string itself is bigger than this specified_length value,
521
- # then it seems that we have another message appended in our incoming transmission.
522
- fragment = msg.extract(specified_length)
523
- info = forward_to_interpret(fragment, pdu, file)
524
- info[:pdu] = pdu
525
- segments << info
526
- # It is possible that a fragment contains both a command and a data fragment. If so, we need to make sure we collect all the information:
527
- if info[:rest_string]
528
- additional_info = forward_to_interpret(info[:rest_string], pdu, file)
529
- segments << additional_info
530
- end
531
- # The information gathered from the interpretation is appended to a segments array,
532
- # and in the case of a recursive call some special logic is needed to build this array in the expected fashion.
533
- remaining_segments = interpret(msg.rest_string, file)
534
- remaining_segments.each do |remaining|
535
- segments << remaining
536
- end
537
- elsif msg.rest_length == specified_length
538
- # Proceed to analyze the rest of the message:
539
- fragment = msg.extract(specified_length)
540
- info = forward_to_interpret(fragment, pdu, file)
541
- info[:pdu] = pdu
542
- segments << info
543
- # It is possible that a fragment contains both a command and a data fragment. If so, we need to make sure we collect all the information:
544
- if info[:rest_string]
545
- additional_info = forward_to_interpret(info[:rest_string], pdu, file)
546
- segments << additional_info
547
- end
548
- else
549
- # Length of the message is less than what is specified in the message. Need to listen for more. This is hopefully handled properly now.
550
- #logger.error("Error. The length of the received message (#{msg.rest_length}) is smaller than what it claims (#{specified_length}). Aborting.")
551
- @first_part = msg.string
552
- end
553
- else
554
- # Assume that this is only the start of the message, and add it to the next incoming string:
555
- @first_part = message
556
- end
557
- return segments
558
- end
559
-
560
- # Decodes the message received when the remote node wishes to abort the session.
561
- # Returns the processed information hash.
562
- #
563
- # === Parameters
564
- #
565
- # * <tt>message</tt> -- The binary message string.
566
- #
567
- def interpret_abort(message)
568
- info = Hash.new
569
- msg = Stream.new(message, @net_endian)
570
- # Reserved (2 bytes)
571
- reserved_bytes = msg.skip(2)
572
- # Source (1 byte)
573
- info[:source] = msg.decode(1, "HEX")
574
- # Reason/Diag. (1 byte)
575
- info[:reason] = msg.decode(1, "HEX")
576
- # Analyse the results:
577
- process_source(info[:source])
578
- process_reason(info[:reason])
579
- stop_receiving
580
- @abort = true
581
- info[:valid] = true
582
- return info
583
- end
584
-
585
- # Decodes the message received in the association response, and interprets its content.
586
- # Returns the processed information hash.
587
- #
588
- # === Parameters
589
- #
590
- # * <tt>message</tt> -- The binary message string.
591
- #
592
- def interpret_association_accept(message)
593
- info = Hash.new
594
- msg = Stream.new(message, @net_endian)
595
- # Protocol version (2 bytes)
596
- info[:protocol_version] = msg.decode(2, "HEX")
597
- # Reserved (2 bytes)
598
- msg.skip(2)
599
- # Called AE (shall be identical to the one sent in the request, but not tested against) (16 bytes)
600
- info[:called_ae] = msg.decode(16, "STR")
601
- # Calling AE (shall be identical to the one sent in the request, but not tested against) (16 bytes)
602
- info[:calling_ae] = msg.decode(16, "STR")
603
- # Reserved (32 bytes)
604
- msg.skip(32)
605
- # APPLICATION CONTEXT:
606
- # Item type (1 byte)
607
- info[:application_item_type] = msg.decode(1, "HEX")
608
- # Reserved (1 byte)
609
- msg.skip(1)
610
- # Application item length (2 bytes)
611
- info[:application_item_length] = msg.decode(2, "US")
612
- # Application context (variable length)
613
- info[:application_context] = msg.decode(info[:application_item_length], "STR")
614
- # PRESENTATION CONTEXT:
615
- # As multiple presentation contexts may occur, we need a loop to catch them all:
616
- # Each presentation context hash will be put in an array, which will be put in the info hash.
617
- presentation_contexts = Array.new
618
- pc_loop = true
619
- while pc_loop do
620
- # Item type (1 byte)
621
- item_type = msg.decode(1, "HEX")
622
- if item_type == ITEM_PRESENTATION_CONTEXT_RESPONSE
623
- pc = Hash.new
624
- pc[:presentation_item_type] = item_type
625
- # Reserved (1 byte)
626
- msg.skip(1)
627
- # Presentation item length (2 bytes)
628
- pc[:presentation_item_length] = msg.decode(2, "US")
629
- # Presentation context ID (1 byte)
630
- pc[:presentation_context_id] = msg.decode(1, "BY")
631
- # Reserved (1 byte)
632
- msg.skip(1)
633
- # Result (& Reason) (1 byte)
634
- pc[:result] = msg.decode(1, "BY")
635
- process_result(pc[:result])
636
- # Reserved (1 byte)
637
- msg.skip(1)
638
- # Transfer syntax sub-item:
639
- # Item type (1 byte)
640
- pc[:transfer_syntax_item_type] = msg.decode(1, "HEX")
641
- # Reserved (1 byte)
642
- msg.skip(1)
643
- # Transfer syntax item length (2 bytes)
644
- pc[:transfer_syntax_item_length] = msg.decode(2, "US")
645
- # Transfer syntax name (variable length)
646
- pc[:transfer_syntax] = msg.decode(pc[:transfer_syntax_item_length], "STR")
647
- presentation_contexts << pc
648
- else
649
- # Break the presentation context loop, as we have probably reached the next stage, which is user info. Rewind:
650
- msg.skip(-1)
651
- pc_loop = false
652
- end
653
- end
654
- info[:pc] = presentation_contexts
655
- # USER INFORMATION:
656
- # Item type (1 byte)
657
- info[:user_info_item_type] = msg.decode(1, "HEX")
658
- # Reserved (1 byte)
659
- msg.skip(1)
660
- # User information item length (2 bytes)
661
- info[:user_info_item_length] = msg.decode(2, "US")
662
- while msg.index < msg.length do
663
- # Item type (1 byte)
664
- item_type = msg.decode(1, "HEX")
665
- # Reserved (1 byte)
666
- msg.skip(1)
667
- # Item length (2 bytes)
668
- item_length = msg.decode(2, "US")
669
- case item_type
670
- when ITEM_MAX_LENGTH
671
- info[:max_pdu_length] = msg.decode(item_length, "UL")
672
- @max_receive_size = info[:max_pdu_length]
673
- when ITEM_IMPLEMENTATION_UID
674
- info[:implementation_class_uid] = msg.decode(item_length, "STR")
675
- when ITEM_MAX_OPERATIONS_INVOKED
676
- # Asynchronous operations window negotiation (PS 3.7: D.3.3.3) (2*2 bytes)
677
- info[:maxnum_operations_invoked] = msg.decode(2, "US")
678
- info[:maxnum_operations_performed] = msg.decode(2, "US")
679
- when ITEM_ROLE_NEGOTIATION
680
- # SCP/SCU Role Selection Negotiation (PS 3.7 D.3.3.4)
681
- # Note: An association response may contain several instances of this item type (each with a different abstract syntax).
682
- uid_length = msg.decode(2, "US")
683
- role = Hash.new
684
- # SOP Class UID (Abstract syntax):
685
- role[:sop_uid] = msg.decode(uid_length, "STR")
686
- # SCU Role (1 byte):
687
- role[:scu] = msg.decode(1, "BY")
688
- # SCP Role (1 byte):
689
- role[:scp] = msg.decode(1, "BY")
690
- if info[:role_negotiation]
691
- info[:role_negotiation] << role
692
- else
693
- info[:role_negotiation] = [role]
694
- end
695
- when ITEM_IMPLEMENTATION_VERSION
696
- info[:implementation_version] = msg.decode(item_length, "STR")
697
- else
698
- # Value (variable length)
699
- value = msg.decode(item_length, "STR")
700
- logger.warn("Unknown user info item type received. Please update source code or contact author. (item type: #{item_type})")
701
- end
702
- end
703
- stop_receiving
704
- info[:valid] = true
705
- return info
706
- end
707
-
708
- # Decodes the association reject message and extracts the error reasons given.
709
- # Returns the processed information hash.
710
- #
711
- # === Parameters
712
- #
713
- # * <tt>message</tt> -- The binary message string.
714
- #
715
- def interpret_association_reject(message)
716
- info = Hash.new
717
- msg = Stream.new(message, @net_endian)
718
- # Reserved (1 byte)
719
- msg.skip(1)
720
- # Result (1 byte)
721
- info[:result] = msg.decode(1, "BY") # 1 for permanent and 2 for transient rejection
722
- # Source (1 byte)
723
- info[:source] = msg.decode(1, "BY")
724
- # Reason (1 byte)
725
- info[:reason] = msg.decode(1, "BY")
726
- logger.warn("ASSOCIATE Request was rejected by the host. Error codes: Result: #{info[:result]}, Source: #{info[:source]}, Reason: #{info[:reason]} (See DICOM PS3.8: Table 9-21 for details.)")
727
- stop_receiving
728
- info[:valid] = true
729
- return info
730
- end
731
-
732
- # Decodes the binary string received in the association request, and interprets its content.
733
- # Returns the processed information hash.
734
- #
735
- # === Parameters
736
- #
737
- # * <tt>message</tt> -- The binary message string.
738
- #
739
- def interpret_association_request(message)
740
- info = Hash.new
741
- msg = Stream.new(message, @net_endian)
742
- # Protocol version (2 bytes)
743
- info[:protocol_version] = msg.decode(2, "HEX")
744
- # Reserved (2 bytes)
745
- msg.skip(2)
746
- # Called AE (shall be returned in the association response) (16 bytes)
747
- info[:called_ae] = msg.decode(16, "STR")
748
- # Calling AE (shall be returned in the association response) (16 bytes)
749
- info[:calling_ae] = msg.decode(16, "STR")
750
- # Reserved (32 bytes)
751
- msg.skip(32)
752
- # APPLICATION CONTEXT:
753
- # Item type (1 byte)
754
- info[:application_item_type] = msg.decode(1, "HEX") # 10H
755
- # Reserved (1 byte)
756
- msg.skip(1)
757
- # Application item length (2 bytes)
758
- info[:application_item_length] = msg.decode(2, "US")
759
- # Application context (variable length)
760
- info[:application_context] = msg.decode(info[:application_item_length], "STR")
761
- # PRESENTATION CONTEXT:
762
- # As multiple presentation contexts may occur, we need a loop to catch them all:
763
- # Each presentation context hash will be put in an array, which will be put in the info hash.
764
- presentation_contexts = Array.new
765
- pc_loop = true
766
- while pc_loop do
767
- # Item type (1 byte)
768
- item_type = msg.decode(1, "HEX")
769
- if item_type == ITEM_PRESENTATION_CONTEXT_REQUEST
770
- pc = Hash.new
771
- pc[:presentation_item_type] = item_type
772
- # Reserved (1 byte)
773
- msg.skip(1)
774
- # Presentation context item length (2 bytes)
775
- pc[:presentation_item_length] = msg.decode(2, "US")
776
- # Presentation context id (1 byte)
777
- pc[:presentation_context_id] = msg.decode(1, "BY")
778
- # Reserved (3 bytes)
779
- msg.skip(3)
780
- presentation_contexts << pc
781
- # A presentation context contains an abstract syntax and one or more transfer syntaxes.
782
- # ABSTRACT SYNTAX SUB-ITEM:
783
- # Abstract syntax item type (1 byte)
784
- pc[:abstract_syntax_item_type] = msg.decode(1, "HEX")
785
- # Reserved (1 byte)
786
- msg.skip(1)
787
- # Abstract syntax item length (2 bytes)
788
- pc[:abstract_syntax_item_length] = msg.decode(2, "US")
789
- # Abstract syntax (variable length)
790
- pc[:abstract_syntax] = msg.decode(pc[:abstract_syntax_item_length], "STR")
791
- ## TRANSFER SYNTAX SUB-ITEM(S):
792
- # As multiple transfer syntaxes may occur, we need a loop to catch them all:
793
- # Each transfer syntax hash will be put in an array, which will be put in the presentation context hash.
794
- transfer_syntaxes = Array.new
795
- ts_loop = true
796
- while ts_loop do
797
- # Item type (1 byte)
798
- item_type = msg.decode(1, "HEX")
799
- if item_type == ITEM_TRANSFER_SYNTAX
800
- ts = Hash.new
801
- ts[:transfer_syntax_item_type] = item_type
802
- # Reserved (1 byte)
803
- msg.skip(1)
804
- # Transfer syntax item length (2 bytes)
805
- ts[:transfer_syntax_item_length] = msg.decode(2, "US")
806
- # Transfer syntax name (variable length)
807
- ts[:transfer_syntax] = msg.decode(ts[:transfer_syntax_item_length], "STR")
808
- transfer_syntaxes << ts
809
- else
810
- # Break the transfer syntax loop, as we have probably reached the next stage,
811
- # which is either user info or a new presentation context entry. Rewind:
812
- msg.skip(-1)
813
- ts_loop = false
814
- end
815
- end
816
- pc[:ts] = transfer_syntaxes
817
- else
818
- # Break the presentation context loop, as we have probably reached the next stage, which is user info. Rewind:
819
- msg.skip(-1)
820
- pc_loop = false
821
- end
822
- end
823
- info[:pc] = presentation_contexts
824
- # USER INFORMATION:
825
- # Item type (1 byte)
826
- info[:user_info_item_type] = msg.decode(1, "HEX")
827
- # Reserved (1 byte)
828
- msg.skip(1)
829
- # User information item length (2 bytes)
830
- info[:user_info_item_length] = msg.decode(2, "US")
831
- # User data (variable length):
832
- while msg.index < msg.length do
833
- # Item type (1 byte)
834
- item_type = msg.decode(1, "HEX")
835
- # Reserved (1 byte)
836
- msg.skip(1)
837
- # Item length (2 bytes)
838
- item_length = msg.decode(2, "US")
839
- case item_type
840
- when ITEM_MAX_LENGTH
841
- info[:max_pdu_length] = msg.decode(item_length, "UL")
842
- when ITEM_IMPLEMENTATION_UID
843
- info[:implementation_class_uid] = msg.decode(item_length, "STR")
844
- when ITEM_MAX_OPERATIONS_INVOKED
845
- # Asynchronous operations window negotiation (PS 3.7: D.3.3.3) (2*2 bytes)
846
- info[:maxnum_operations_invoked] = msg.decode(2, "US")
847
- info[:maxnum_operations_performed] = msg.decode(2, "US")
848
- when ITEM_ROLE_NEGOTIATION
849
- # SCP/SCU Role Selection Negotiation (PS 3.7 D.3.3.4)
850
- # Note: An association request may contain several instances of this item type (each with a different abstract syntax).
851
- uid_length = msg.decode(2, "US")
852
- role = Hash.new
853
- # SOP Class UID (Abstract syntax):
854
- role[:sop_uid] = msg.decode(uid_length, "STR")
855
- # SCU Role (1 byte):
856
- role[:scu] = msg.decode(1, "BY")
857
- # SCP Role (1 byte):
858
- role[:scp] = msg.decode(1, "BY")
859
- if info[:role_negotiation]
860
- info[:role_negotiation] << role
861
- else
862
- info[:role_negotiation] = [role]
863
- end
864
- when ITEM_IMPLEMENTATION_VERSION
865
- info[:implementation_version] = msg.decode(item_length, "STR")
866
- else
867
- # Unknown item type:
868
- # Value (variable length)
869
- value = msg.decode(item_length, "STR")
870
- logger.warn("Unknown user info item type received. Please update source code or contact author. (item type: " + item_type + ")")
871
- end
872
- end
873
- stop_receiving
874
- info[:valid] = true
875
- return info
876
- end
877
-
878
- # Decodes the received command/data fragment message, and interprets its content.
879
- # Returns the processed information hash.
880
- #
881
- # === Notes
882
- #
883
- # * Decoding of a data fragment depends on the explicitness of the transmission.
884
- #
885
- # === Parameters
886
- #
887
- # * <tt>message</tt> -- The binary message string.
888
- # * <tt>file</tt> -- A boolean used to inform whether an incoming data fragment is part of a DICOM file reception or not.
889
- #
890
- def interpret_command_and_data(message, file=nil)
891
- info = Hash.new
892
- msg = Stream.new(message, @net_endian)
893
- # Length (of remaining PDV data) (4 bytes)
894
- info[:presentation_data_value_length] = msg.decode(4, "UL")
895
- # Calculate the last index position of this message element:
896
- last_index = info[:presentation_data_value_length] + msg.index
897
- # Presentation context ID (1 byte)
898
- info[:presentation_context_id] = msg.decode(1, "BY")
899
- @presentation_context_id = info[:presentation_context_id]
900
- # Flags (1 byte)
901
- info[:presentation_context_flag] = msg.decode(1, "HEX") # "03" for command (last fragment), "02" for data
902
- # Apply the proper transfer syntax for this presentation context:
903
- set_transfer_syntax(@presentation_contexts[info[:presentation_context_id]])
904
- # "Data endian" encoding from now on:
905
- msg.endian = @data_endian
906
- # We will put the results in a hash:
907
- results = Hash.new
908
- if info[:presentation_context_flag] == COMMAND_LAST_FRAGMENT
909
- # COMMAND, LAST FRAGMENT:
910
- while msg.index < last_index do
911
- # Tag (4 bytes)
912
- tag = msg.decode_tag
913
- # Length (2 bytes)
914
- length = msg.decode(2, "US")
915
- if length > msg.rest_length
916
- logger.error("Specified length of command element value exceeds remaining length of the received message! Something is wrong.")
917
- end
918
- # Reserved (2 bytes)
919
- msg.skip(2)
920
- # VR (from library - not the stream):
921
- vr = LIBRARY.element(tag).vr
922
- # Value (variable length)
923
- value = msg.decode(length, vr)
924
- # Put tag and value in a hash:
925
- results[tag] = value
926
- end
927
- # The results hash is put in an array along with (possibly) other results:
928
- info[:results] = results
929
- # Store the results in an instance variable (to be used later when sending a receipt for received data):
930
- @command_request = results
931
- # Check if the command fragment indicates that this was the last of the response fragments for this query:
932
- status = results["0000,0900"]
933
- if status
934
- # Note: This method will also stop the packet receiver if indicated by the status mesasge.
935
- process_status(status)
936
- end
937
- # Special case: Handle a possible C-ECHO-RQ:
938
- if info[:results]["0000,0100"] == C_ECHO_RQ
939
- logger.info("Received an Echo request. Returning an Echo response.")
940
- handle_response
941
- end
942
- elsif info[:presentation_context_flag] == DATA_MORE_FRAGMENTS or info[:presentation_context_flag] == DATA_LAST_FRAGMENT
943
- # DATA FRAGMENT:
944
- # If this is a file transmission, we will delay the decoding for later:
945
- if file
946
- # Just store the binary string:
947
- info[:bin] = msg.rest_string
948
- # If this was the last data fragment of a C-STORE, we need to send a receipt:
949
- # (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)
950
- handle_response if info[:presentation_context_flag] == DATA_LAST_FRAGMENT
951
- else
952
- # Decode data elements:
953
- while msg.index < last_index do
954
- # Tag (4 bytes)
955
- tag = msg.decode_tag
956
- if @explicit
957
- # Type (VR) (2 bytes):
958
- type = msg.decode(2, "STR")
959
- # Length (2 bytes)
960
- length = msg.decode(2, "US")
961
- else
962
- # Implicit:
963
- type = nil # (needs to be defined as nil here or it will take the value from the previous step in the loop)
964
- # Length (4 bytes)
965
- length = msg.decode(4, "UL")
966
- end
967
- if length > msg.rest_length
968
- logger.error("The specified length of the data element value exceeds the remaining length of the received message!")
969
- end
970
- # Fetch type (if not defined already) for this data element:
971
- type = LIBRARY.element(tag).vr unless type
972
- # Value (variable length)
973
- value = msg.decode(length, type)
974
- # Put tag and value in a hash:
975
- results[tag] = value
976
- end
977
- # The results hash is put in an array along with (possibly) other results:
978
- info[:results] = results
979
- end
980
- else
981
- # Unknown.
982
- logger.error("Unknown presentation context flag received in the query/command response. (#{info[:presentation_context_flag]})")
983
- stop_receiving
984
- end
985
- # If only parts of the string was read, return the rest:
986
- info[:rest_string] = msg.rest_string if last_index < msg.length
987
- info[:valid] = true
988
- return info
989
- end
990
-
991
- # Decodes the message received in the release request and calls the handle_release method.
992
- # Returns the processed information hash.
993
- #
994
- # === Parameters
995
- #
996
- # * <tt>message</tt> -- The binary message string.
997
- #
998
- def interpret_release_request(message)
999
- info = Hash.new
1000
- msg = Stream.new(message, @net_endian)
1001
- # Reserved (4 bytes)
1002
- reserved_bytes = msg.decode(4, "HEX")
1003
- handle_release
1004
- info[:valid] = true
1005
- return info
1006
- end
1007
-
1008
- # Decodes the message received in the release response and closes the connection.
1009
- # Returns the processed information hash.
1010
- #
1011
- # === Parameters
1012
- #
1013
- # * <tt>message</tt> -- The binary message string.
1014
- #
1015
- def interpret_release_response(message)
1016
- info = Hash.new
1017
- msg = Stream.new(message, @net_endian)
1018
- # Reserved (4 bytes)
1019
- reserved_bytes = msg.decode(4, "HEX")
1020
- stop_receiving
1021
- info[:valid] = true
1022
- return info
1023
- end
1024
-
1025
- # Handles the reception of multiple incoming transmissions.
1026
- # Returns an array of interpreted message information hashes.
1027
- #
1028
- # === Parameters
1029
- #
1030
- # * <tt>file</tt> -- A boolean used to inform whether an incoming data fragment is part of a DICOM file reception or not.
1031
- #
1032
- def receive_multiple_transmissions(file=nil)
1033
- # FIXME: The code which waits for incoming network packets seems to be very CPU intensive.
1034
- # Perhaps there is a more elegant way to wait for incoming messages?
1035
- #
1036
- @listen = true
1037
- segments = Array.new
1038
- while @listen
1039
- # Receive data and append the current data to our segments array, which will be returned.
1040
- data = receive_transmission(@min_length)
1041
- current_segments = interpret(data, file)
1042
- if current_segments
1043
- current_segments.each do |cs|
1044
- segments << cs
1045
- end
1046
- end
1047
- end
1048
- segments << {:valid => false} unless segments
1049
- return segments
1050
- end
1051
-
1052
- # Handles the reception of a single, expected incoming transmission and returns the interpreted, received data.
1053
- #
1054
- def receive_single_transmission
1055
- min_length = 8
1056
- data = receive_transmission(min_length)
1057
- segments = interpret(data)
1058
- segments << {:valid => false} unless segments.length > 0
1059
- return segments
1060
- end
1061
-
1062
- # Sets the session of this Link instance (used when this session is already established externally).
1063
- #
1064
- # === Parameters
1065
- #
1066
- # * <tt>session</tt> -- A TCP network connection that has been established with a remote node.
1067
- #
1068
- def set_session(session)
1069
- @session = session
1070
- end
1071
-
1072
- # Establishes a new session with a remote network node.
1073
- #
1074
- # === Parameters
1075
- #
1076
- # * <tt>adress</tt> -- String. The adress (IP) of the remote node.
1077
- # * <tt>port</tt> -- Fixnum. The network port to be used in the network communication.
1078
- #
1079
- def start_session(adress, port)
1080
- @session = TCPSocket.new(adress, port)
1081
- end
1082
-
1083
- # Ends the current session by closing the connection.
1084
- #
1085
- def stop_session
1086
- @session.close unless @session.closed?
1087
- end
1088
-
1089
- # Sends the outgoing message (encoded binary string) to the remote node.
1090
- #
1091
- def transmit
1092
- @session.send(@outgoing.string, 0)
1093
- end
1094
-
1095
-
1096
- private
1097
-
1098
-
1099
- # Builds the application context (which is part of the association request/response).
1100
- #
1101
- def append_application_context
1102
- # Application context item type (1 byte)
1103
- @outgoing.encode_last(ITEM_APPLICATION_CONTEXT, "HEX")
1104
- # Reserved (1 byte)
1105
- @outgoing.encode_last("00", "HEX")
1106
- # Application context item length (2 bytes)
1107
- @outgoing.encode_last(APPLICATION_CONTEXT.length, "US")
1108
- # Application context (variable length)
1109
- @outgoing.encode_last(APPLICATION_CONTEXT, "STR")
1110
- end
1111
-
1112
- # Builds the binary string that makes up the header part the association request/response.
1113
- #
1114
- # === Parameters
1115
- #
1116
- # * <tt>pdu</tt> -- The command fragment's PDU string.
1117
- # * <tt>called_ae</tt> -- Application entity (name) of the SCP (host).
1118
- #
1119
- def append_association_header(pdu, called_ae)
1120
- # Big endian encoding:
1121
- @outgoing.endian = @net_endian
1122
- # Header will be encoded in opposite order, where the elements are being put first in the outgoing binary string.
1123
- # Build last part of header first. This is necessary to be able to assess the length value.
1124
- # Reserved (32 bytes)
1125
- @outgoing.encode_first("00"*32, "HEX")
1126
- # Calling AE title (16 bytes)
1127
- calling_ae = @outgoing.encode_string_with_trailing_spaces(@ae, 16)
1128
- @outgoing.add_first(calling_ae) # (pre-encoded value)
1129
- # Called AE title (16 bytes) (return the name that the SCU used in the association request)
1130
- formatted_called_ae = @outgoing.encode_string_with_trailing_spaces(called_ae, 16)
1131
- @outgoing.add_first(formatted_called_ae) # (pre-encoded value)
1132
- # Reserved (2 bytes)
1133
- @outgoing.encode_first("0000", "HEX")
1134
- # Protocol version (2 bytes)
1135
- @outgoing.encode_first("0001", "HEX")
1136
- append_header(pdu)
1137
- end
1138
-
1139
- # Adds the header bytes to the outgoing message (the header structure is equal for all of the message types).
1140
- #
1141
- # === Parameters
1142
- #
1143
- # * <tt>pdu</tt> -- The command fragment's PDU string.
1144
- #
1145
- def append_header(pdu)
1146
- # Length (of remaining data) (4 bytes)
1147
- @outgoing.encode_first(@outgoing.string.length, "UL")
1148
- # Reserved (1 byte)
1149
- @outgoing.encode_first("00", "HEX")
1150
- # PDU type (1 byte)
1151
- @outgoing.encode_first(pdu, "HEX")
1152
- end
1153
-
1154
- # Builds the binary string that makes up the presentation context part of the association request/accept.
1155
- #
1156
- # === Notes
1157
- #
1158
- # * The values of the parameters will differ somewhat depending on whether this is related to a request or response.
1159
- # * Description of error codes are given in the DICOM Standard, PS 3.8, Chapter 9.3.3.2 (Table 9-18).
1160
- #
1161
- # === Parameters
1162
- #
1163
- # * <tt>presentation_contexts</tt> -- A nested hash object with abstract syntaxes, presentation context ids, transfer syntaxes and result codes.
1164
- # * <tt>item_type</tt> -- Presentation context item (request or response).
1165
- # * <tt>request</tt> -- Boolean. If true, an ossociate request message is generated, if false, an asoociate accept message is generated.
1166
- #
1167
- def append_presentation_contexts(presentation_contexts, item_type, request=false)
1168
- # Iterate the abstract syntaxes:
1169
- presentation_contexts.each_pair do |abstract_syntax, context_ids|
1170
- # Iterate the context ids:
1171
- context_ids.each_pair do |context_id, syntax|
1172
- # PRESENTATION CONTEXT:
1173
- # Presentation context item type (1 byte)
1174
- @outgoing.encode_last(item_type, "HEX")
1175
- # Reserved (1 byte)
1176
- @outgoing.encode_last("00", "HEX")
1177
- # Presentation context item length (2 bytes)
1178
- ts_length = 4*syntax[:transfer_syntaxes].length + syntax[:transfer_syntaxes].join.length
1179
- # Abstract syntax item only included in requests, not accepts:
1180
- items_length = 4 + ts_length
1181
- items_length += 4 + abstract_syntax.length if request
1182
- @outgoing.encode_last(items_length, "US")
1183
- # Presentation context ID (1 byte)
1184
- @outgoing.encode_last(context_id, "BY")
1185
- # Reserved (1 byte)
1186
- @outgoing.encode_last("00", "HEX")
1187
- # (1 byte) Reserved (for association request) & Result/reason (for association accept response)
1188
- result = (syntax[:result] ? syntax[:result] : 0)
1189
- @outgoing.encode_last(result, "BY")
1190
- # Reserved (1 byte)
1191
- @outgoing.encode_last("00", "HEX")
1192
- ## ABSTRACT SYNTAX SUB-ITEM: (only for request, not response)
1193
- if request
1194
- # Abstract syntax item type (1 byte)
1195
- @outgoing.encode_last(ITEM_ABSTRACT_SYNTAX, "HEX")
1196
- # Reserved (1 byte)
1197
- @outgoing.encode_last("00", "HEX")
1198
- # Abstract syntax item length (2 bytes)
1199
- @outgoing.encode_last(abstract_syntax.length, "US")
1200
- # Abstract syntax (variable length)
1201
- @outgoing.encode_last(abstract_syntax, "STR")
1202
- end
1203
- ## TRANSFER SYNTAX SUB-ITEM (not included if result indicates error):
1204
- if result == ACCEPTANCE
1205
- syntax[:transfer_syntaxes].each do |t|
1206
- # Transfer syntax item type (1 byte)
1207
- @outgoing.encode_last(ITEM_TRANSFER_SYNTAX, "HEX")
1208
- # Reserved (1 byte)
1209
- @outgoing.encode_last("00", "HEX")
1210
- # Transfer syntax item length (2 bytes)
1211
- @outgoing.encode_last(t.length, "US")
1212
- # Transfer syntax (variable length)
1213
- @outgoing.encode_last(t, "STR")
1214
- end
1215
- end
1216
- end
1217
- end
1218
- end
1219
-
1220
- # Adds the binary string that makes up the user information part of the association request/response.
1221
- #
1222
- # === Parameters
1223
- #
1224
- # * <tt>ui</tt> -- User information items array.
1225
- #
1226
- def append_user_information(ui)
1227
- # USER INFORMATION:
1228
- # User information item type (1 byte)
1229
- @outgoing.encode_last(ITEM_USER_INFORMATION, "HEX")
1230
- # Reserved (1 byte)
1231
- @outgoing.encode_last("00", "HEX")
1232
- # Encode the user information item values so we can determine the remaining length of this section:
1233
- values = Array.new
1234
- ui.each_index do |i|
1235
- values << @outgoing.encode(ui[i][2], ui[i][1])
1236
- end
1237
- # User information item length (2 bytes)
1238
- items_length = 4*ui.length + values.join.length
1239
- @outgoing.encode_last(items_length, "US")
1240
- # SUB-ITEMS:
1241
- ui.each_index do |i|
1242
- # UI item type (1 byte)
1243
- @outgoing.encode_last(ui[i][0], "HEX")
1244
- # Reserved (1 byte)
1245
- @outgoing.encode_last("00", "HEX")
1246
- # UI item length (2 bytes)
1247
- @outgoing.encode_last(values[i].length, "US")
1248
- # UI value (4 bytes)
1249
- @outgoing.add_last(values[i])
1250
- end
1251
- end
1252
-
1253
- # Returns the appropriate response value for the Command Field (0000,0100) to be used in a command fragment (response).
1254
- #
1255
- # === Parameters
1256
- #
1257
- # * <tt>request</tt> -- The Command Field value in a command fragment (request).
1258
- #
1259
- def command_field_response(request)
1260
- case request
1261
- when C_STORE_RQ
1262
- return C_STORE_RSP
1263
- when C_ECHO_RQ
1264
- return C_ECHO_RSP
1265
- else
1266
- logger.error("Unknown or unsupported request (#{request}) encountered.")
1267
- return C_CANCEL_RQ
1268
- end
1269
- end
1270
-
1271
- # Processes the value of the reason byte received in the association abort, and prints an explanation of the error.
1272
- #
1273
- # === Parameters
1274
- #
1275
- # * <tt>reason</tt> -- String. Reason code for an error that has occured.
1276
- #
1277
- def process_reason(reason)
1278
- case reason
1279
- when "00"
1280
- logger.error("Reason specified for abort: Reason not specified")
1281
- when "01"
1282
- logger.error("Reason specified for abort: Unrecognized PDU")
1283
- when "02"
1284
- logger.error("Reason specified for abort: Unexpected PDU")
1285
- when "04"
1286
- logger.error("Reason specified for abort: Unrecognized PDU parameter")
1287
- when "05"
1288
- logger.error("Reason specified for abort: Unexpected PDU parameter")
1289
- when "06"
1290
- logger.error("Reason specified for abort: Invalid PDU parameter value")
1291
- else
1292
- logger.error("Reason specified for abort: Unknown reason (Error code: #{reason})")
1293
- end
1294
- end
1295
-
1296
- # Processes the value of the result byte received in the association response.
1297
- # Prints an explanation if an error is indicated.
1298
- #
1299
- # === Notes
1300
- #
1301
- # A value other than 0 indicates an error.
1302
- #
1303
- # === Parameters
1304
- #
1305
- # * <tt>result</tt> -- Fixnum. The result code from an association response.
1306
- #
1307
- def process_result(result)
1308
- unless result == 0
1309
- # Analyse the result and report what is wrong:
1310
- case result
1311
- when 1
1312
- logger.warn("DICOM Request was rejected by the host, reason: 'User-rejection'")
1313
- when 2
1314
- logger.warn("DICOM Request was rejected by the host, reason: 'No reason (provider rejection)'")
1315
- when 3
1316
- logger.warn("DICOM Request was rejected by the host, reason: 'Abstract syntax not supported'")
1317
- when 4
1318
- logger.warn("DICOM Request was rejected by the host, reason: 'Transfer syntaxes not supported'")
1319
- else
1320
- logger.warn("DICOM Request was rejected by the host, reason: 'UNKNOWN (#{result})' (Illegal reason provided)")
1321
- end
1322
- end
1323
- end
1324
-
1325
- # Processes the value of the source byte in the association abort, and prints an explanation of the source (of the error).
1326
- #
1327
- # === Parameters
1328
- #
1329
- # * <tt>source</tt> -- String. A code which informs which part has been the source of an error.
1330
- #
1331
- def process_source(source)
1332
- if source == "00"
1333
- logger.warn("Connection has been aborted by the service provider because of an error by the service user (client side).")
1334
- elsif source == "02"
1335
- logger.warn("Connection has been aborted by the service provider because of an error by the service provider (server side).")
1336
- else
1337
- logger.warn("Connection has been aborted by the service provider, with an unknown cause of the problems. (error code: #{source})")
1338
- end
1339
- end
1340
-
1341
- # Processes the value of the status element (0000,0900) received in the command fragment.
1342
- # Prints an explanation where deemed appropriate.
1343
- #
1344
- # === Notes
1345
- #
1346
- # The status element has vr 'US', and the status as reported here is therefore a number.
1347
- # In the official DICOM documents however, the values of the various status options are given in hex format.
1348
- # Resources: The DICOM standard; PS3.4, Annex Q 2.1.1.4 & PS3.7 Annex C 4.
1349
- #
1350
- # === Parameters
1351
- #
1352
- # * <tt>status</tt> -- Fixnum. A status code from a command fragment.
1353
- #
1354
- def process_status(status)
1355
- case status
1356
- when 0 # "0000"
1357
- # Last fragment (Break the while loop that listens continuously for incoming packets):
1358
- logger.info("Receipt for successful execution of the desired operation has been received.")
1359
- stop_receiving
1360
- when 42752 # "a700"
1361
- # Failure: Out of resources. Related fields: 0000,0902
1362
- logger.error("Failure! SCP has given the following reason: 'Out of Resources'.")
1363
- when 43264 # "a900"
1364
- # Failure: Identifier Does Not Match SOP Class. Related fields: 0000,0901, 0000,0902
1365
- logger.error("Failure! SCP has given the following reason: 'Identifier Does Not Match SOP Class'.")
1366
- when 49152 # "c000"
1367
- # Failure: Unable to process. Related fields: 0000,0901, 0000,0902
1368
- logger.error("Failure! SCP has given the following reason: 'Unable to process'.")
1369
- when 49408 # "c100"
1370
- # Failure: More than one match found. Related fields: 0000,0901, 0000,0902
1371
- logger.error("Failure! SCP has given the following reason: 'More than one match found'.")
1372
- when 49664 # "c200"
1373
- # Failure: Unable to support requested template. Related fields: 0000,0901, 0000,0902
1374
- logger.error("Failure! SCP has given the following reason: 'Unable to support requested template'.")
1375
- when 65024 # "fe00"
1376
- # Cancel: Matching terminated due to Cancel request.
1377
- logger.info("Cancel! SCP has given the following reason: 'Matching terminated due to Cancel request'.")
1378
- when 65280 # "ff00"
1379
- # Sub-operations are continuing.
1380
- # (No particular action taken, the program will listen for and receive the coming fragments)
1381
- when 65281 # "ff01"
1382
- # More command/data fragments to follow.
1383
- # (No particular action taken, the program will listen for and receive the coming fragments)
1384
- else
1385
- logger.error("Something was NOT successful regarding the desired operation. SCP responded with error code: #{status} (tag: 0000,0900). See DICOM PS3.7, Annex C for details.")
1386
- end
1387
- end
1388
-
1389
- # Handles an incoming network transmission.
1390
- # Returns the binary string data received.
1391
- #
1392
- # === Notes
1393
- #
1394
- # If a minimum length has been specified, and a message is received which is shorter than this length,
1395
- # the method will keep listening for more incoming network packets to append.
1396
- #
1397
- # === Parameters
1398
- #
1399
- # * <tt>min_length</tt> -- Fixnum. The minimum possible length of a valid incoming transmission.
1400
- #
1401
- def receive_transmission(min_length=0)
1402
- data = receive_transmission_data
1403
- # Check the nature of the received data variable:
1404
- if data
1405
- # Sometimes the incoming transmission may be broken up into smaller pieces:
1406
- # Unless a short answer is expected, we will continue to listen if the first answer was too short:
1407
- unless min_length == 0
1408
- if data.length < min_length
1409
- addition = receive_transmission_data
1410
- data = data + addition if addition
1411
- end
1412
- end
1413
- else
1414
- # It seems there was no incoming message and the operation timed out.
1415
- # Convert the variable to an empty string.
1416
- data = ""
1417
- end
1418
- data
1419
- end
1420
-
1421
- # Receives the data from an incoming network transmission.
1422
- # Returns the binary string data received.
1423
- #
1424
- def receive_transmission_data
1425
- data = false
1426
- response = IO.select([@session], nil, nil, @timeout)
1427
- if response.nil?
1428
- logger.error("No answer was received within the specified timeout period. Aborting.")
1429
- stop_receiving
1430
- else
1431
- data = @session.recv(@max_receive_size)
1432
- end
1433
- data
1434
- end
1435
-
1436
- # Sets some default values related to encoding.
1437
- #
1438
- def set_default_values
1439
- # Default endianness for network transmissions is Big Endian:
1440
- @net_endian = true
1441
- # Default endianness of data is little endian:
1442
- @data_endian = false
1443
- # It may turn out to be unncessary to define the following values at this early stage.
1444
- # Explicitness:
1445
- @explicit = false
1446
- # Transfer syntax:
1447
- set_transfer_syntax(IMPLICIT_LITTLE_ENDIAN)
1448
- end
1449
-
1450
- # Set instance variables related to a transfer syntax.
1451
- #
1452
- # === Parameters
1453
- #
1454
- # * <tt>syntax</tt> -- A transfer syntax string.
1455
- #
1456
- def set_transfer_syntax(syntax)
1457
- @transfer_syntax = syntax
1458
- # Query the library with our particular transfer syntax string:
1459
- ts = LIBRARY.uid(@transfer_syntax)
1460
- @explicit = ts ? ts.explicit? : true
1461
- @data_endian = ts ? ts.big_endian? : false
1462
- logger.warn("Invalid/unknown transfer syntax encountered: #{@transfer_syntax} Will try to continue, but errors may occur.") unless ts
1463
- end
1464
-
1465
- # Sets the @user_information items instance array.
1466
- #
1467
- # === Notes
1468
- #
1469
- # Each user information item is a three element array consisting of: item type code, VR & value.
1470
- #
1471
- # === Parameters
1472
- #
1473
- # * <tt>info</tt> -- An association information hash.
1474
- #
1475
- def set_user_information_array(info=nil)
1476
- @user_information = [
1477
- [ITEM_MAX_LENGTH, "UL", @max_package_size],
1478
- [ITEM_IMPLEMENTATION_UID, "STR", UID_ROOT],
1479
- [ITEM_IMPLEMENTATION_VERSION, "STR", NAME]
1480
- ]
1481
- # A bit of a hack to include "asynchronous operations window negotiation" and/or "role negotiation",
1482
- # in cases where this has been included in the association request:
1483
- if info
1484
- if info[:maxnum_operations_invoked]
1485
- @user_information.insert(2, [ITEM_MAX_OPERATIONS_INVOKED, "HEX", "00010001"])
1486
- end
1487
- if info[:role_negotiation]
1488
- pos = 3
1489
- info[:role_negotiation].each do |role|
1490
- msg = Stream.new('', @net_endian)
1491
- uid = role[:sop_uid]
1492
- # Length of UID (2 bytes):
1493
- msg.encode_first(uid.length, "US")
1494
- # SOP UID being negotiated (Variable length):
1495
- msg.encode_last(uid, "STR")
1496
- # SCU Role (Always accept SCU) (1 byte):
1497
- if role[:scu] == 1
1498
- msg.encode_last(1, "BY")
1499
- else
1500
- msg.encode_last(0, "BY")
1501
- end
1502
- # SCP Role (Never accept SCP) (1 byte):
1503
- if role[:scp] == 1
1504
- msg.encode_last(0, "BY")
1505
- else
1506
- msg.encode_last(1, "BY")
1507
- end
1508
- @user_information.insert(pos, [ITEM_ROLE_NEGOTIATION, "STR", msg.string])
1509
- pos += 1
1510
- end
1511
- end
1512
- end
1513
- end
1514
-
1515
- # Toggles two instance variables that in causes the loops that listen for incoming network packets to break.
1516
- #
1517
- # === Notes
1518
- #
1519
- # This method is called by the various methods that interpret incoming data when they have verified that
1520
- # the entire message has been received, or when a timeout is reached.
1521
- #
1522
- def stop_receiving
1523
- @listen = false
1524
- @receive = false
1525
- end
1526
-
1527
- end
1528
- end
1
+ module DICOM
2
+
3
+ # This class handles the construction and interpretation of network packages
4
+ # as well as network communication.
5
+ #
6
+ class Link
7
+ include Logging
8
+
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 TCP network session where the DICOM communication is done with a remote host or client.
16
+ attr_reader :session
17
+
18
+ # Creates a Link instance, which is used by both DClient and DServer to handle network communication.
19
+ #
20
+ # === Parameters
21
+ #
22
+ # * <tt>options</tt> -- A hash of parameters.
23
+ #
24
+ # === Options
25
+ #
26
+ # * <tt>:ae</tt> -- String. The name of the client (application entity).
27
+ # * <tt>:file_handler</tt> -- A customized FileHandler class to use instead of the default FileHandler.
28
+ # * <tt>:host_ae</tt> -- String. The name of the server (application entity).
29
+ # * <tt>:max_package_size</tt> -- Fixnum. The maximum allowed size of network packages (in bytes).
30
+ # * <tt>:timeout</tt> -- Fixnum. The maximum period to wait for an answer before aborting the communication.
31
+ #
32
+ def initialize(options={})
33
+ require 'socket'
34
+ # Optional parameters (and default values):
35
+ @file_handler = options[:file_handler] || FileHandler
36
+ @ae = options[:ae] || "RUBY_DICOM"
37
+ @host_ae = options[:host_ae] || "DEFAULT"
38
+ @max_package_size = options[:max_package_size] || 32768 # 16384
39
+ @max_receive_size = @max_package_size
40
+ @timeout = options[:timeout] || 10 # seconds
41
+ @min_length = 10 # minimum number of bytes to expect in an incoming transmission
42
+ # Variables used for monitoring state of transmission:
43
+ @session = nil # TCP connection
44
+ @association = nil # DICOM Association status
45
+ @request_approved = nil # Status of our DICOM request
46
+ @release = nil # Status of received, valid release response
47
+ @command_request = Hash.new
48
+ @presentation_contexts = Hash.new # Keeps track of the relationship between pc id and it's transfer syntax
49
+ set_default_values
50
+ set_user_information_array
51
+ @outgoing = Stream.new(string=nil, endian=true)
52
+ end
53
+
54
+ # Waits for an SCU to issue a release request, and answers it by launching the handle_release method.
55
+ # If invalid or no message is received, the connection is closed.
56
+ #
57
+ def await_release
58
+ segments = receive_single_transmission
59
+ info = segments.first
60
+ if info[:pdu] != PDU_RELEASE_REQUEST
61
+ # For some reason we didn't get our expected release request. Determine why:
62
+ if info[:valid]
63
+ logger.error("Unexpected message type received (PDU: #{info[:pdu]}). Expected a release request. Closing the connection.")
64
+ handle_abort(false)
65
+ else
66
+ logger.error("Timed out while waiting for a release request. Closing the connection.")
67
+ end
68
+ stop_session
69
+ else
70
+ # Properly release the association:
71
+ handle_release
72
+ end
73
+ end
74
+
75
+ # Builds the abort message which is transmitted when the server wishes to (abruptly) abort the connection.
76
+ #
77
+ # === Restrictions
78
+ #
79
+ # For now, no reasons for the abortion are provided (and source of problems will always be set as client side).
80
+ #
81
+ def build_association_abort
82
+ # Big endian encoding:
83
+ @outgoing.endian = @net_endian
84
+ # Clear the outgoing binary string:
85
+ @outgoing.reset
86
+ # Reserved (2 bytes)
87
+ @outgoing.encode_last("00"*2, "HEX")
88
+ # Source (1 byte)
89
+ source = "00" # (client side error)
90
+ @outgoing.encode_last(source, "HEX")
91
+ # Reason/Diag. (1 byte)
92
+ reason = "00" # (Reason not specified)
93
+ @outgoing.encode_last(reason, "HEX")
94
+ append_header(PDU_ABORT)
95
+ end
96
+
97
+ # Builds the binary string which is sent as the association accept (in response to an association request).
98
+ #
99
+ # === Parameters
100
+ #
101
+ # * <tt>info</tt> -- The association information hash.
102
+ #
103
+ def build_association_accept(info)
104
+ # Big endian encoding:
105
+ @outgoing.endian = @net_endian
106
+ # Clear the outgoing binary string:
107
+ @outgoing.reset
108
+ # No abstract syntax in association response. To make this work with the method that
109
+ # encodes the presentation context, we pass on a one-element array containing nil).
110
+ abstract_syntaxes = Array.new(1, nil)
111
+ # Note: The order of which these components are built is not arbitrary.
112
+ append_application_context
113
+ # Reset the presentation context instance variable:
114
+ @presentation_contexts = Hash.new
115
+ # Create the presentation context hash object that will be passed to the builder method:
116
+ p_contexts = Hash.new
117
+ # Build the presentation context strings, one by one:
118
+ info[:pc].each do |pc|
119
+ @presentation_contexts[pc[:presentation_context_id]] = pc[:selected_transfer_syntax]
120
+ # Add the information from this pc item to the p_contexts hash:
121
+ p_contexts[pc[:abstract_syntax]] = Hash.new unless p_contexts[pc[:abstract_syntax]]
122
+ transfer_syntaxes = pc[:selected_transfer_syntax].nil? ? [] : [ pc[:selected_transfer_syntax] ]
123
+ p_contexts[pc[:abstract_syntax]][pc[:presentation_context_id]] = {:transfer_syntaxes => transfer_syntaxes, :result => pc[:result]}
124
+ end
125
+ append_presentation_contexts(p_contexts, ITEM_PRESENTATION_CONTEXT_RESPONSE)
126
+ append_user_information(@user_information)
127
+ # Header must be built last, because we need to know the length of the other components.
128
+ append_association_header(PDU_ASSOCIATION_ACCEPT, info[:called_ae])
129
+ end
130
+
131
+ # Builds the binary string which is sent as the association reject (in response to an association request).
132
+ #
133
+ # === Parameters
134
+ #
135
+ # * <tt>info</tt> -- The association information hash.
136
+ #
137
+ # === Restrictions
138
+ #
139
+ # * For now, this method will only customize the "reason" value.
140
+ # * For a list of error codes, see the DICOM standard, PS3.8 Chapter 9.3.4, Table 9-21.
141
+ #
142
+ def build_association_reject(info)
143
+ # Big endian encoding:
144
+ @outgoing.endian = @net_endian
145
+ # Clear the outgoing binary string:
146
+ @outgoing.reset
147
+ # Reserved (1 byte)
148
+ @outgoing.encode_last("00", "HEX")
149
+ # Result (1 byte)
150
+ @outgoing.encode_last("01", "HEX") # 1 for permament, 2 for transient
151
+ # Source (1 byte)
152
+ # (1: Service user, 2: Service provider (ACSE related function), 3: Service provider (Presentation related function)
153
+ @outgoing.encode_last("01", "HEX")
154
+ # Reason (1 byte)
155
+ reason = info[:reason]
156
+ @outgoing.encode_last(reason, "HEX")
157
+ append_header(PDU_ASSOCIATION_REJECT)
158
+ end
159
+
160
+ # Builds the binary string which is sent as the association request.
161
+ #
162
+ # === Parameters
163
+ #
164
+ # * <tt>presentation_contexts</tt> -- A hash containing abstract_syntaxes, presentation context ids and transfer syntaxes.
165
+ # * <tt>user_info</tt> -- A user information items array.
166
+ #
167
+ def build_association_request(presentation_contexts, user_info)
168
+ # Big endian encoding:
169
+ @outgoing.endian = @net_endian
170
+ # Clear the outgoing binary string:
171
+ @outgoing.reset
172
+ # Note: The order of which these components are built is not arbitrary.
173
+ # (The first three are built 'in order of appearance', the header is built last, but is put first in the message)
174
+ append_application_context
175
+ append_presentation_contexts(presentation_contexts, ITEM_PRESENTATION_CONTEXT_REQUEST, request=true)
176
+ append_user_information(user_info)
177
+ # Header must be built last, because we need to know the length of the other components.
178
+ append_association_header(PDU_ASSOCIATION_REQUEST, @host_ae)
179
+ end
180
+
181
+ # Builds the binary string which is sent as a command fragment.
182
+ #
183
+ # === Parameters
184
+ #
185
+ # * <tt>pdu</tt> -- The command fragment's PDU string.
186
+ # * <tt>context</tt> -- Presentation context ID byte (references a presentation context from the association).
187
+ # * <tt>flags</tt> -- The flag string, which identifies if this is the last command fragment or not.
188
+ # * <tt>command_elements</tt> -- An array of command elements.
189
+ #
190
+ def build_command_fragment(pdu, context, flags, command_elements)
191
+ # Little endian encoding:
192
+ @outgoing.endian = @data_endian
193
+ # Clear the outgoing binary string:
194
+ @outgoing.reset
195
+ # Build the last part first, the Command items:
196
+ command_elements.each do |element|
197
+ # Tag (4 bytes)
198
+ @outgoing.add_last(@outgoing.encode_tag(element[0]))
199
+ # Encode the value first, so we know its length:
200
+ value = @outgoing.encode_value(element[2], element[1])
201
+ # Length (2 bytes)
202
+ @outgoing.encode_last(value.length, "US")
203
+ # Reserved (2 bytes)
204
+ @outgoing.encode_last("0000", "HEX")
205
+ # Value (variable length)
206
+ @outgoing.add_last(value)
207
+ end
208
+ # The rest of the command fragment will be buildt in reverse, all the time
209
+ # putting the elements first in the outgoing binary string.
210
+ # Group length item:
211
+ # Value (4 bytes)
212
+ @outgoing.encode_first(@outgoing.string.length, "UL")
213
+ # Reserved (2 bytes)
214
+ @outgoing.encode_first("0000", "HEX")
215
+ # Length (2 bytes)
216
+ @outgoing.encode_first(4, "US")
217
+ # Tag (4 bytes)
218
+ @outgoing.add_first(@outgoing.encode_tag("0000,0000"))
219
+ # Big endian encoding from now on:
220
+ @outgoing.endian = @net_endian
221
+ # Flags (1 byte)
222
+ @outgoing.encode_first(flags, "HEX")
223
+ # Presentation context ID (1 byte)
224
+ @outgoing.encode_first(context, "BY")
225
+ # Length (of remaining data) (4 bytes)
226
+ @outgoing.encode_first(@outgoing.string.length, "UL")
227
+ # PRESENTATION DATA VALUE (the above)
228
+ append_header(pdu)
229
+ end
230
+
231
+ # Builds the binary string which is sent as a data fragment.
232
+ #
233
+ # === Notes
234
+ #
235
+ # * The style of encoding will depend on whether we have an implicit or explicit transfer syntax.
236
+ #
237
+ # === Parameters
238
+ #
239
+ # * <tt>data_elements</tt> -- An array of data elements.
240
+ # * <tt>presentation_context_id</tt> -- Presentation context ID byte (references a presentation context from the association).
241
+ #
242
+ def build_data_fragment(data_elements, presentation_context_id)
243
+ # Set the transfer syntax to be used for encoding the data fragment:
244
+ set_transfer_syntax(@presentation_contexts[presentation_context_id])
245
+ # Endianness of data fragment:
246
+ @outgoing.endian = @data_endian
247
+ # Clear the outgoing binary string:
248
+ @outgoing.reset
249
+ # Build the last part first, the Data items:
250
+ data_elements.each do |element|
251
+ # Encode all tags (even tags which are empty):
252
+ # Tag (4 bytes)
253
+ @outgoing.add_last(@outgoing.encode_tag(element[0]))
254
+ # Encode the value in advance of putting it into the message, so we know its length:
255
+ vr = LIBRARY.element(element[0]).vr
256
+ value = @outgoing.encode_value(element[1], vr)
257
+ if @explicit
258
+ # Type (VR) (2 bytes)
259
+ @outgoing.encode_last(vr, "STR")
260
+ # Length (2 bytes)
261
+ @outgoing.encode_last(value.length, "US")
262
+ else
263
+ # Implicit:
264
+ # Length (4 bytes)
265
+ @outgoing.encode_last(value.length, "UL")
266
+ end
267
+ # Value (variable length)
268
+ @outgoing.add_last(value)
269
+ end
270
+ # The rest of the data fragment will be built in reverse, all the time
271
+ # putting the elements first in the outgoing binary string.
272
+ # Big endian encoding from now on:
273
+ @outgoing.endian = @net_endian
274
+ # Flags (1 byte)
275
+ @outgoing.encode_first("02", "HEX") # Data, last fragment (identifier)
276
+ # Presentation context ID (1 byte)
277
+ @outgoing.encode_first(presentation_context_id, "BY")
278
+ # Length (of remaining data) (4 bytes)
279
+ @outgoing.encode_first(@outgoing.string.length, "UL")
280
+ # PRESENTATION DATA VALUE (the above)
281
+ append_header(PDU_DATA)
282
+ end
283
+
284
+ # Builds the binary string which is sent as the release request.
285
+ #
286
+ def build_release_request
287
+ # Big endian encoding:
288
+ @outgoing.endian = @net_endian
289
+ # Clear the outgoing binary string:
290
+ @outgoing.reset
291
+ # Reserved (4 bytes)
292
+ @outgoing.encode_last("00"*4, "HEX")
293
+ append_header(PDU_RELEASE_REQUEST)
294
+ end
295
+
296
+ # Builds the binary string which is sent as the release response (which follows a release request).
297
+ #
298
+ def build_release_response
299
+ # Big endian encoding:
300
+ @outgoing.endian = @net_endian
301
+ # Clear the outgoing binary string:
302
+ @outgoing.reset
303
+ # Reserved (4 bytes)
304
+ @outgoing.encode_last("00000000", "HEX")
305
+ append_header(PDU_RELEASE_RESPONSE)
306
+ end
307
+
308
+ # Builds the binary string which makes up a C-STORE data fragment.
309
+ #
310
+ # === Parameters
311
+ #
312
+ # * <tt>pdu</tt> -- The data fragment's PDU string.
313
+ # * <tt>context</tt> -- Presentation context ID byte (references a presentation context from the association).
314
+ # * <tt>flags</tt> -- The flag string, which identifies if this is the last data fragment or not.
315
+ # * <tt>body</tt> -- A pre-encoded binary string (typicall a segment of a DICOM file to be transmitted).
316
+ #
317
+ def build_storage_fragment(pdu, context, flags, body)
318
+ # Big endian encoding:
319
+ @outgoing.endian = @net_endian
320
+ # Clear the outgoing binary string:
321
+ @outgoing.reset
322
+ # Build in reverse, putting elements in front of the binary string:
323
+ # Insert the data (body):
324
+ @outgoing.add_last(body)
325
+ # Flags (1 byte)
326
+ @outgoing.encode_first(flags, "HEX")
327
+ # Context ID (1 byte)
328
+ @outgoing.encode_first(context, "BY")
329
+ # PDV Length (of remaining data) (4 bytes)
330
+ @outgoing.encode_first(@outgoing.string.length, "UL")
331
+ # PRESENTATION DATA VALUE (the above)
332
+ append_header(pdu)
333
+ end
334
+
335
+ # Delegates an incoming message to its appropriate interpreter method, based on its pdu type.
336
+ # Returns the interpreted information hash.
337
+ #
338
+ # === Parameters
339
+ #
340
+ # * <tt>message</tt> -- The binary message string.
341
+ # * <tt>pdu</tt> -- The PDU string of the message.
342
+ # * <tt>file</tt> -- A boolean used to inform whether an incoming data fragment is part of a DICOM file reception or not.
343
+ #
344
+ def forward_to_interpret(message, pdu, file=nil)
345
+ case pdu
346
+ when PDU_ASSOCIATION_REQUEST
347
+ info = interpret_association_request(message)
348
+ when PDU_ASSOCIATION_ACCEPT
349
+ info = interpret_association_accept(message)
350
+ when PDU_ASSOCIATION_REJECT
351
+ info = interpret_association_reject(message)
352
+ when PDU_DATA
353
+ info = interpret_command_and_data(message, file)
354
+ when PDU_RELEASE_REQUEST
355
+ info = interpret_release_request(message)
356
+ when PDU_RELEASE_RESPONSE
357
+ info = interpret_release_response(message)
358
+ when PDU_ABORT
359
+ info = interpret_abort(message)
360
+ else
361
+ info = {:valid => false}
362
+ logger.error("An unknown PDU type was received in the incoming transmission. Can not decode this message. (PDU: #{pdu})")
363
+ end
364
+ return info
365
+ end
366
+
367
+ # Handles the abortion of a session, when a non-valid or unexpected message has been received.
368
+ #
369
+ # === Parameters
370
+ #
371
+ # * <tt>default_message</tt> -- A boolean which unless set as nil/false will make the method print the default status message.
372
+ #
373
+ def handle_abort(default_message=true)
374
+ logger.warn("An unregonizable (non-DICOM) message was received.") if default_message
375
+ build_association_abort
376
+ transmit
377
+ end
378
+
379
+ # Handles the outgoing association accept message.
380
+ #
381
+ # === Parameters
382
+ #
383
+ # * <tt>info</tt> -- The association information hash.
384
+ #
385
+ def handle_association_accept(info)
386
+ # Update the variable for calling ae (information gathered in the association request):
387
+ @ae = info[:calling_ae]
388
+ # Build message string and send it:
389
+ set_user_information_array(info)
390
+ build_association_accept(info)
391
+ transmit
392
+ end
393
+
394
+ # Processes incoming command & data fragments for the DServer.
395
+ # Returns a success boolean and an array of status messages.
396
+ #
397
+ # === Notes
398
+ #
399
+ # The incoming traffic will in most cases be: A C-STORE-RQ (command fragment) followed by a bunch of data fragments.
400
+ # However, it may also be a C-ECHO-RQ command fragment, which is used to test connections.
401
+ #
402
+ # === Parameters
403
+ #
404
+ # * <tt>path</tt> -- The path used to save incoming DICOM files.
405
+ #
406
+ #--
407
+ # FIXME: The code which handles incoming data isnt quite satisfactory. It would probably be wise to rewrite it at some stage to clean up
408
+ # the code somewhat. Probably a better handling of command requests (and their corresponding data fragments) would be a good idea.
409
+ #
410
+ def handle_incoming_data(path)
411
+ # Wait for incoming data:
412
+ segments = receive_multiple_transmissions(file=true)
413
+ # Reset command results arrays:
414
+ @command_results = Array.new
415
+ @data_results = Array.new
416
+ file_transfer_syntaxes = Array.new
417
+ files = Array.new
418
+ single_file_data = Array.new
419
+ # Proceed to extract data from the captured segments:
420
+ segments.each do |info|
421
+ if info[:valid]
422
+ # Determine if it is command or data:
423
+ if info[:presentation_context_flag] == DATA_MORE_FRAGMENTS
424
+ @data_results << info[:results]
425
+ single_file_data << info[:bin]
426
+ elsif info[:presentation_context_flag] == DATA_LAST_FRAGMENT
427
+ @data_results << info[:results]
428
+ single_file_data << info[:bin]
429
+ # Join the recorded data binary strings together to make a DICOM file binary string and put it in our files Array:
430
+ files << single_file_data.join
431
+ single_file_data = Array.new
432
+ elsif info[:presentation_context_flag] == COMMAND_LAST_FRAGMENT
433
+ @command_results << info[:results]
434
+ @presentation_context_id = info[:presentation_context_id] # Does this actually do anything useful?
435
+ file_transfer_syntaxes << @presentation_contexts[info[:presentation_context_id]]
436
+ end
437
+ end
438
+ end
439
+ # Process the received files using the customizable FileHandler class:
440
+ success, messages = @file_handler.receive_files(path, files, file_transfer_syntaxes)
441
+ return success, messages
442
+ end
443
+
444
+ # Handles the rejection message (The response used to an association request when its formalities are not correct).
445
+ #
446
+ def handle_rejection
447
+ logger.warn("An incoming association request was rejected. Error code: #{association_error}")
448
+ # Insert the error code in the info hash:
449
+ info[:reason] = association_error
450
+ # Send an association rejection:
451
+ build_association_reject(info)
452
+ transmit
453
+ end
454
+
455
+ # Handles the release message (which is the response to a release request).
456
+ #
457
+ def handle_release
458
+ stop_receiving
459
+ logger.info("Received a release request. Releasing association.")
460
+ build_release_response
461
+ transmit
462
+ stop_session
463
+ end
464
+
465
+ # Handles the command fragment response.
466
+ #
467
+ # === Notes
468
+ #
469
+ # This is usually a C-STORE-RSP which follows the (successful) reception of a DICOM file, but may also
470
+ # be a C-ECHO-RSP in response to an echo request.
471
+ #
472
+ def handle_response
473
+ # Need to construct the command elements array:
474
+ command_elements = Array.new
475
+ # SOP Class UID:
476
+ command_elements << ["0000,0002", "UI", @command_request["0000,0002"]]
477
+ # Command Field:
478
+ command_elements << ["0000,0100", "US", command_field_response(@command_request["0000,0100"])]
479
+ # Message ID Being Responded To:
480
+ command_elements << ["0000,0120", "US", @command_request["0000,0110"]]
481
+ # Data Set Type:
482
+ command_elements << ["0000,0800", "US", NO_DATA_SET_PRESENT]
483
+ # Status:
484
+ command_elements << ["0000,0900", "US", SUCCESS]
485
+ # Affected SOP Instance UID:
486
+ command_elements << ["0000,1000", "UI", @command_request["0000,1000"]] if @command_request["0000,1000"]
487
+ build_command_fragment(PDU_DATA, @presentation_context_id, COMMAND_LAST_FRAGMENT, command_elements)
488
+ transmit
489
+ end
490
+
491
+ # Decodes the header of an incoming message, analyzes its real length versus expected length, and handles any
492
+ # deviations to make sure that message strings are split up appropriately before they are being forwarded to interpretation.
493
+ # Returns an array of information hashes.
494
+ #
495
+ # === Parameters
496
+ #
497
+ # * <tt>message</tt> -- The binary message string.
498
+ # * <tt>file</tt> -- A boolean used to inform whether an incoming data fragment is part of a DICOM file reception or not.
499
+ #
500
+ #--
501
+ # FIXME: This method is rather complex and doesnt feature the best readability. A rewrite that is able to simplify it would be lovely.
502
+ #
503
+ def interpret(message, file=nil)
504
+ if @first_part
505
+ message = @first_part + message
506
+ @first_part = nil
507
+ end
508
+ segments = Array.new
509
+ # If the message is at least 8 bytes we can start decoding it:
510
+ if message.length > 8
511
+ # Create a new Stream instance to handle this response.
512
+ msg = Stream.new(message, @net_endian)
513
+ # PDU type ( 1 byte)
514
+ pdu = msg.decode(1, "HEX")
515
+ # Reserved (1 byte)
516
+ msg.skip(1)
517
+ # Length of remaining data (4 bytes)
518
+ specified_length = msg.decode(4, "UL")
519
+ # Analyze the remaining length of the message versurs the specified_length value:
520
+ if msg.rest_length > specified_length
521
+ # If the remaining length of the string itself is bigger than this specified_length value,
522
+ # then it seems that we have another message appended in our incoming transmission.
523
+ fragment = msg.extract(specified_length)
524
+ info = forward_to_interpret(fragment, pdu, file)
525
+ info[:pdu] = pdu
526
+ segments << info
527
+ # It is possible that a fragment contains both a command and a data fragment. If so, we need to make sure we collect all the information:
528
+ if info[:rest_string]
529
+ additional_info = forward_to_interpret(info[:rest_string], pdu, file)
530
+ segments << additional_info
531
+ end
532
+ # The information gathered from the interpretation is appended to a segments array,
533
+ # and in the case of a recursive call some special logic is needed to build this array in the expected fashion.
534
+ remaining_segments = interpret(msg.rest_string, file)
535
+ remaining_segments.each do |remaining|
536
+ segments << remaining
537
+ end
538
+ elsif msg.rest_length == specified_length
539
+ # Proceed to analyze the rest of the message:
540
+ fragment = msg.extract(specified_length)
541
+ info = forward_to_interpret(fragment, pdu, file)
542
+ info[:pdu] = pdu
543
+ segments << info
544
+ # It is possible that a fragment contains both a command and a data fragment. If so, we need to make sure we collect all the information:
545
+ if info[:rest_string]
546
+ additional_info = forward_to_interpret(info[:rest_string], pdu, file)
547
+ segments << additional_info
548
+ end
549
+ else
550
+ # Length of the message is less than what is specified in the message. Need to listen for more. This is hopefully handled properly now.
551
+ #logger.error("Error. The length of the received message (#{msg.rest_length}) is smaller than what it claims (#{specified_length}). Aborting.")
552
+ @first_part = msg.string
553
+ end
554
+ else
555
+ # Assume that this is only the start of the message, and add it to the next incoming string:
556
+ @first_part = message
557
+ end
558
+ return segments
559
+ end
560
+
561
+ # Decodes the message received when the remote node wishes to abort the session.
562
+ # Returns the processed information hash.
563
+ #
564
+ # === Parameters
565
+ #
566
+ # * <tt>message</tt> -- The binary message string.
567
+ #
568
+ def interpret_abort(message)
569
+ info = Hash.new
570
+ msg = Stream.new(message, @net_endian)
571
+ # Reserved (2 bytes)
572
+ reserved_bytes = msg.skip(2)
573
+ # Source (1 byte)
574
+ info[:source] = msg.decode(1, "HEX")
575
+ # Reason/Diag. (1 byte)
576
+ info[:reason] = msg.decode(1, "HEX")
577
+ # Analyse the results:
578
+ process_source(info[:source])
579
+ process_reason(info[:reason])
580
+ stop_receiving
581
+ @abort = true
582
+ info[:valid] = true
583
+ return info
584
+ end
585
+
586
+ # Decodes the message received in the association response, and interprets its content.
587
+ # Returns the processed information hash.
588
+ #
589
+ # === Parameters
590
+ #
591
+ # * <tt>message</tt> -- The binary message string.
592
+ #
593
+ def interpret_association_accept(message)
594
+ info = Hash.new
595
+ msg = Stream.new(message, @net_endian)
596
+ # Protocol version (2 bytes)
597
+ info[:protocol_version] = msg.decode(2, "HEX")
598
+ # Reserved (2 bytes)
599
+ msg.skip(2)
600
+ # Called AE (shall be identical to the one sent in the request, but not tested against) (16 bytes)
601
+ info[:called_ae] = msg.decode(16, "STR")
602
+ # Calling AE (shall be identical to the one sent in the request, but not tested against) (16 bytes)
603
+ info[:calling_ae] = msg.decode(16, "STR")
604
+ # Reserved (32 bytes)
605
+ msg.skip(32)
606
+ # APPLICATION CONTEXT:
607
+ # Item type (1 byte)
608
+ info[:application_item_type] = msg.decode(1, "HEX")
609
+ # Reserved (1 byte)
610
+ msg.skip(1)
611
+ # Application item length (2 bytes)
612
+ info[:application_item_length] = msg.decode(2, "US")
613
+ # Application context (variable length)
614
+ info[:application_context] = msg.decode(info[:application_item_length], "STR")
615
+ # PRESENTATION CONTEXT:
616
+ # As multiple presentation contexts may occur, we need a loop to catch them all:
617
+ # Each presentation context hash will be put in an array, which will be put in the info hash.
618
+ presentation_contexts = Array.new
619
+ pc_loop = true
620
+ while pc_loop do
621
+ # Item type (1 byte)
622
+ item_type = msg.decode(1, "HEX")
623
+ if item_type == ITEM_PRESENTATION_CONTEXT_RESPONSE
624
+ pc = Hash.new
625
+ pc[:presentation_item_type] = item_type
626
+ # Reserved (1 byte)
627
+ msg.skip(1)
628
+ # Presentation item length (2 bytes)
629
+ pc[:presentation_item_length] = msg.decode(2, "US")
630
+ # Presentation context ID (1 byte)
631
+ pc[:presentation_context_id] = msg.decode(1, "BY")
632
+ # Reserved (1 byte)
633
+ msg.skip(1)
634
+ # Result (& Reason) (1 byte)
635
+ pc[:result] = msg.decode(1, "BY")
636
+ process_result(pc[:result])
637
+ # Reserved (1 byte)
638
+ msg.skip(1)
639
+ # Transfer syntax sub-item:
640
+ # Item type (1 byte)
641
+ pc[:transfer_syntax_item_type] = msg.decode(1, "HEX")
642
+ # Reserved (1 byte)
643
+ msg.skip(1)
644
+ # Transfer syntax item length (2 bytes)
645
+ pc[:transfer_syntax_item_length] = msg.decode(2, "US")
646
+ # Transfer syntax name (variable length)
647
+ pc[:transfer_syntax] = msg.decode(pc[:transfer_syntax_item_length], "STR")
648
+ presentation_contexts << pc
649
+ else
650
+ # Break the presentation context loop, as we have probably reached the next stage, which is user info. Rewind:
651
+ msg.skip(-1)
652
+ pc_loop = false
653
+ end
654
+ end
655
+ info[:pc] = presentation_contexts
656
+ # USER INFORMATION:
657
+ # Item type (1 byte)
658
+ info[:user_info_item_type] = msg.decode(1, "HEX")
659
+ # Reserved (1 byte)
660
+ msg.skip(1)
661
+ # User information item length (2 bytes)
662
+ info[:user_info_item_length] = msg.decode(2, "US")
663
+ while msg.index < msg.length do
664
+ # Item type (1 byte)
665
+ item_type = msg.decode(1, "HEX")
666
+ # Reserved (1 byte)
667
+ msg.skip(1)
668
+ # Item length (2 bytes)
669
+ item_length = msg.decode(2, "US")
670
+ case item_type
671
+ when ITEM_MAX_LENGTH
672
+ info[:max_pdu_length] = msg.decode(item_length, "UL")
673
+ @max_receive_size = info[:max_pdu_length]
674
+ when ITEM_IMPLEMENTATION_UID
675
+ info[:implementation_class_uid] = msg.decode(item_length, "STR")
676
+ when ITEM_MAX_OPERATIONS_INVOKED
677
+ # Asynchronous operations window negotiation (PS 3.7: D.3.3.3) (2*2 bytes)
678
+ info[:maxnum_operations_invoked] = msg.decode(2, "US")
679
+ info[:maxnum_operations_performed] = msg.decode(2, "US")
680
+ when ITEM_ROLE_NEGOTIATION
681
+ # SCP/SCU Role Selection Negotiation (PS 3.7 D.3.3.4)
682
+ # Note: An association response may contain several instances of this item type (each with a different abstract syntax).
683
+ uid_length = msg.decode(2, "US")
684
+ role = Hash.new
685
+ # SOP Class UID (Abstract syntax):
686
+ role[:sop_uid] = msg.decode(uid_length, "STR")
687
+ # SCU Role (1 byte):
688
+ role[:scu] = msg.decode(1, "BY")
689
+ # SCP Role (1 byte):
690
+ role[:scp] = msg.decode(1, "BY")
691
+ if info[:role_negotiation]
692
+ info[:role_negotiation] << role
693
+ else
694
+ info[:role_negotiation] = [role]
695
+ end
696
+ when ITEM_IMPLEMENTATION_VERSION
697
+ info[:implementation_version] = msg.decode(item_length, "STR")
698
+ else
699
+ # Value (variable length)
700
+ value = msg.decode(item_length, "STR")
701
+ logger.warn("Unknown user info item type received. Please update source code or contact author. (item type: #{item_type})")
702
+ end
703
+ end
704
+ stop_receiving
705
+ info[:valid] = true
706
+ return info
707
+ end
708
+
709
+ # Decodes the association reject message and extracts the error reasons given.
710
+ # Returns the processed information hash.
711
+ #
712
+ # === Parameters
713
+ #
714
+ # * <tt>message</tt> -- The binary message string.
715
+ #
716
+ def interpret_association_reject(message)
717
+ info = Hash.new
718
+ msg = Stream.new(message, @net_endian)
719
+ # Reserved (1 byte)
720
+ msg.skip(1)
721
+ # Result (1 byte)
722
+ info[:result] = msg.decode(1, "BY") # 1 for permanent and 2 for transient rejection
723
+ # Source (1 byte)
724
+ info[:source] = msg.decode(1, "BY")
725
+ # Reason (1 byte)
726
+ info[:reason] = msg.decode(1, "BY")
727
+ logger.warn("ASSOCIATE Request was rejected by the host. Error codes: Result: #{info[:result]}, Source: #{info[:source]}, Reason: #{info[:reason]} (See DICOM PS3.8: Table 9-21 for details.)")
728
+ stop_receiving
729
+ info[:valid] = true
730
+ return info
731
+ end
732
+
733
+ # Decodes the binary string received in the association request, and interprets its content.
734
+ # Returns the processed information hash.
735
+ #
736
+ # === Parameters
737
+ #
738
+ # * <tt>message</tt> -- The binary message string.
739
+ #
740
+ def interpret_association_request(message)
741
+ info = Hash.new
742
+ msg = Stream.new(message, @net_endian)
743
+ # Protocol version (2 bytes)
744
+ info[:protocol_version] = msg.decode(2, "HEX")
745
+ # Reserved (2 bytes)
746
+ msg.skip(2)
747
+ # Called AE (shall be returned in the association response) (16 bytes)
748
+ info[:called_ae] = msg.decode(16, "STR")
749
+ # Calling AE (shall be returned in the association response) (16 bytes)
750
+ info[:calling_ae] = msg.decode(16, "STR")
751
+ # Reserved (32 bytes)
752
+ msg.skip(32)
753
+ # APPLICATION CONTEXT:
754
+ # Item type (1 byte)
755
+ info[:application_item_type] = msg.decode(1, "HEX") # 10H
756
+ # Reserved (1 byte)
757
+ msg.skip(1)
758
+ # Application item length (2 bytes)
759
+ info[:application_item_length] = msg.decode(2, "US")
760
+ # Application context (variable length)
761
+ info[:application_context] = msg.decode(info[:application_item_length], "STR")
762
+ # PRESENTATION CONTEXT:
763
+ # As multiple presentation contexts may occur, we need a loop to catch them all:
764
+ # Each presentation context hash will be put in an array, which will be put in the info hash.
765
+ presentation_contexts = Array.new
766
+ pc_loop = true
767
+ while pc_loop do
768
+ # Item type (1 byte)
769
+ item_type = msg.decode(1, "HEX")
770
+ if item_type == ITEM_PRESENTATION_CONTEXT_REQUEST
771
+ pc = Hash.new
772
+ pc[:presentation_item_type] = item_type
773
+ # Reserved (1 byte)
774
+ msg.skip(1)
775
+ # Presentation context item length (2 bytes)
776
+ pc[:presentation_item_length] = msg.decode(2, "US")
777
+ # Presentation context id (1 byte)
778
+ pc[:presentation_context_id] = msg.decode(1, "BY")
779
+ # Reserved (3 bytes)
780
+ msg.skip(3)
781
+ presentation_contexts << pc
782
+ # A presentation context contains an abstract syntax and one or more transfer syntaxes.
783
+ # ABSTRACT SYNTAX SUB-ITEM:
784
+ # Abstract syntax item type (1 byte)
785
+ pc[:abstract_syntax_item_type] = msg.decode(1, "HEX")
786
+ # Reserved (1 byte)
787
+ msg.skip(1)
788
+ # Abstract syntax item length (2 bytes)
789
+ pc[:abstract_syntax_item_length] = msg.decode(2, "US")
790
+ # Abstract syntax (variable length)
791
+ pc[:abstract_syntax] = msg.decode(pc[:abstract_syntax_item_length], "STR")
792
+ ## TRANSFER SYNTAX SUB-ITEM(S):
793
+ # As multiple transfer syntaxes may occur, we need a loop to catch them all:
794
+ # Each transfer syntax hash will be put in an array, which will be put in the presentation context hash.
795
+ transfer_syntaxes = Array.new
796
+ ts_loop = true
797
+ while ts_loop do
798
+ # Item type (1 byte)
799
+ item_type = msg.decode(1, "HEX")
800
+ if item_type == ITEM_TRANSFER_SYNTAX
801
+ ts = Hash.new
802
+ ts[:transfer_syntax_item_type] = item_type
803
+ # Reserved (1 byte)
804
+ msg.skip(1)
805
+ # Transfer syntax item length (2 bytes)
806
+ ts[:transfer_syntax_item_length] = msg.decode(2, "US")
807
+ # Transfer syntax name (variable length)
808
+ ts[:transfer_syntax] = msg.decode(ts[:transfer_syntax_item_length], "STR")
809
+ transfer_syntaxes << ts
810
+ else
811
+ # Break the transfer syntax loop, as we have probably reached the next stage,
812
+ # which is either user info or a new presentation context entry. Rewind:
813
+ msg.skip(-1)
814
+ ts_loop = false
815
+ end
816
+ end
817
+ pc[:ts] = transfer_syntaxes
818
+ else
819
+ # Break the presentation context loop, as we have probably reached the next stage, which is user info. Rewind:
820
+ msg.skip(-1)
821
+ pc_loop = false
822
+ end
823
+ end
824
+ info[:pc] = presentation_contexts
825
+ # USER INFORMATION:
826
+ # Item type (1 byte)
827
+ info[:user_info_item_type] = msg.decode(1, "HEX")
828
+ # Reserved (1 byte)
829
+ msg.skip(1)
830
+ # User information item length (2 bytes)
831
+ info[:user_info_item_length] = msg.decode(2, "US")
832
+ # User data (variable length):
833
+ while msg.index < msg.length do
834
+ # Item type (1 byte)
835
+ item_type = msg.decode(1, "HEX")
836
+ # Reserved (1 byte)
837
+ msg.skip(1)
838
+ # Item length (2 bytes)
839
+ item_length = msg.decode(2, "US")
840
+ case item_type
841
+ when ITEM_MAX_LENGTH
842
+ info[:max_pdu_length] = msg.decode(item_length, "UL")
843
+ when ITEM_IMPLEMENTATION_UID
844
+ info[:implementation_class_uid] = msg.decode(item_length, "STR")
845
+ when ITEM_MAX_OPERATIONS_INVOKED
846
+ # Asynchronous operations window negotiation (PS 3.7: D.3.3.3) (2*2 bytes)
847
+ info[:maxnum_operations_invoked] = msg.decode(2, "US")
848
+ info[:maxnum_operations_performed] = msg.decode(2, "US")
849
+ when ITEM_ROLE_NEGOTIATION
850
+ # SCP/SCU Role Selection Negotiation (PS 3.7 D.3.3.4)
851
+ # Note: An association request may contain several instances of this item type (each with a different abstract syntax).
852
+ uid_length = msg.decode(2, "US")
853
+ role = Hash.new
854
+ # SOP Class UID (Abstract syntax):
855
+ role[:sop_uid] = msg.decode(uid_length, "STR")
856
+ # SCU Role (1 byte):
857
+ role[:scu] = msg.decode(1, "BY")
858
+ # SCP Role (1 byte):
859
+ role[:scp] = msg.decode(1, "BY")
860
+ if info[:role_negotiation]
861
+ info[:role_negotiation] << role
862
+ else
863
+ info[:role_negotiation] = [role]
864
+ end
865
+ when ITEM_IMPLEMENTATION_VERSION
866
+ info[:implementation_version] = msg.decode(item_length, "STR")
867
+ else
868
+ # Unknown item type:
869
+ # Value (variable length)
870
+ value = msg.decode(item_length, "STR")
871
+ logger.warn("Unknown user info item type received. Please update source code or contact author. (item type: " + item_type + ")")
872
+ end
873
+ end
874
+ stop_receiving
875
+ info[:valid] = true
876
+ return info
877
+ end
878
+
879
+ # Decodes the received command/data fragment message, and interprets its content.
880
+ # Returns the processed information hash.
881
+ #
882
+ # === Notes
883
+ #
884
+ # * Decoding of a data fragment depends on the explicitness of the transmission.
885
+ #
886
+ # === Parameters
887
+ #
888
+ # * <tt>message</tt> -- The binary message string.
889
+ # * <tt>file</tt> -- A boolean used to inform whether an incoming data fragment is part of a DICOM file reception or not.
890
+ #
891
+ def interpret_command_and_data(message, file=nil)
892
+ info = Hash.new
893
+ msg = Stream.new(message, @net_endian)
894
+ # Length (of remaining PDV data) (4 bytes)
895
+ info[:presentation_data_value_length] = msg.decode(4, "UL")
896
+ # Calculate the last index position of this message element:
897
+ last_index = info[:presentation_data_value_length] + msg.index
898
+ # Presentation context ID (1 byte)
899
+ info[:presentation_context_id] = msg.decode(1, "BY")
900
+ @presentation_context_id = info[:presentation_context_id]
901
+ # Flags (1 byte)
902
+ info[:presentation_context_flag] = msg.decode(1, "HEX") # "03" for command (last fragment), "02" for data
903
+ # Apply the proper transfer syntax for this presentation context:
904
+ set_transfer_syntax(@presentation_contexts[info[:presentation_context_id]])
905
+ # "Data endian" encoding from now on:
906
+ msg.endian = @data_endian
907
+ # We will put the results in a hash:
908
+ results = Hash.new
909
+ if info[:presentation_context_flag] == COMMAND_LAST_FRAGMENT
910
+ # COMMAND, LAST FRAGMENT:
911
+ while msg.index < last_index do
912
+ # Tag (4 bytes)
913
+ tag = msg.decode_tag
914
+ # Length (2 bytes)
915
+ length = msg.decode(2, "US")
916
+ if length > msg.rest_length
917
+ logger.error("Specified length of command element value exceeds remaining length of the received message! Something is wrong.")
918
+ end
919
+ # Reserved (2 bytes)
920
+ msg.skip(2)
921
+ # VR (from library - not the stream):
922
+ vr = LIBRARY.element(tag).vr
923
+ # Value (variable length)
924
+ value = msg.decode(length, vr)
925
+ # Put tag and value in a hash:
926
+ results[tag] = value
927
+ end
928
+ # The results hash is put in an array along with (possibly) other results:
929
+ info[:results] = results
930
+ # Store the results in an instance variable (to be used later when sending a receipt for received data):
931
+ @command_request = results
932
+ # Check if the command fragment indicates that this was the last of the response fragments for this query:
933
+ status = results["0000,0900"]
934
+ if status
935
+ # Note: This method will also stop the packet receiver if indicated by the status mesasge.
936
+ process_status(status)
937
+ end
938
+ # Special case: Handle a possible C-ECHO-RQ:
939
+ if info[:results]["0000,0100"] == C_ECHO_RQ
940
+ logger.info("Received an Echo request. Returning an Echo response.")
941
+ handle_response
942
+ end
943
+ elsif info[:presentation_context_flag] == DATA_MORE_FRAGMENTS or info[:presentation_context_flag] == DATA_LAST_FRAGMENT
944
+ # DATA FRAGMENT:
945
+ # If this is a file transmission, we will delay the decoding for later:
946
+ if file
947
+ # Just store the binary string:
948
+ info[:bin] = msg.rest_string
949
+ # If this was the last data fragment of a C-STORE, we need to send a receipt:
950
+ # (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)
951
+ handle_response if info[:presentation_context_flag] == DATA_LAST_FRAGMENT
952
+ else
953
+ # Decode data elements:
954
+ while msg.index < last_index do
955
+ # Tag (4 bytes)
956
+ tag = msg.decode_tag
957
+ if @explicit
958
+ # Type (VR) (2 bytes):
959
+ type = msg.decode(2, "STR")
960
+ # Length (2 bytes)
961
+ length = msg.decode(2, "US")
962
+ else
963
+ # Implicit:
964
+ type = nil # (needs to be defined as nil here or it will take the value from the previous step in the loop)
965
+ # Length (4 bytes)
966
+ length = msg.decode(4, "UL")
967
+ end
968
+ if length > msg.rest_length
969
+ logger.error("The specified length of the data element value exceeds the remaining length of the received message!")
970
+ end
971
+ # Fetch type (if not defined already) for this data element:
972
+ type = LIBRARY.element(tag).vr unless type
973
+ # Value (variable length)
974
+ value = msg.decode(length, type)
975
+ # Put tag and value in a hash:
976
+ results[tag] = value
977
+ end
978
+ # The results hash is put in an array along with (possibly) other results:
979
+ info[:results] = results
980
+ end
981
+ else
982
+ # Unknown.
983
+ logger.error("Unknown presentation context flag received in the query/command response. (#{info[:presentation_context_flag]})")
984
+ stop_receiving
985
+ end
986
+ # If only parts of the string was read, return the rest:
987
+ info[:rest_string] = msg.rest_string if last_index < msg.length
988
+ info[:valid] = true
989
+ return info
990
+ end
991
+
992
+ # Decodes the message received in the release request and calls the handle_release method.
993
+ # Returns the processed information hash.
994
+ #
995
+ # === Parameters
996
+ #
997
+ # * <tt>message</tt> -- The binary message string.
998
+ #
999
+ def interpret_release_request(message)
1000
+ info = Hash.new
1001
+ msg = Stream.new(message, @net_endian)
1002
+ # Reserved (4 bytes)
1003
+ reserved_bytes = msg.decode(4, "HEX")
1004
+ handle_release
1005
+ info[:valid] = true
1006
+ return info
1007
+ end
1008
+
1009
+ # Decodes the message received in the release response and closes the connection.
1010
+ # Returns the processed information hash.
1011
+ #
1012
+ # === Parameters
1013
+ #
1014
+ # * <tt>message</tt> -- The binary message string.
1015
+ #
1016
+ def interpret_release_response(message)
1017
+ info = Hash.new
1018
+ msg = Stream.new(message, @net_endian)
1019
+ # Reserved (4 bytes)
1020
+ reserved_bytes = msg.decode(4, "HEX")
1021
+ stop_receiving
1022
+ info[:valid] = true
1023
+ return info
1024
+ end
1025
+
1026
+ # Handles the reception of multiple incoming transmissions.
1027
+ # Returns an array of interpreted message information hashes.
1028
+ #
1029
+ # === Parameters
1030
+ #
1031
+ # * <tt>file</tt> -- A boolean used to inform whether an incoming data fragment is part of a DICOM file reception or not.
1032
+ #
1033
+ def receive_multiple_transmissions(file=nil)
1034
+ # FIXME: The code which waits for incoming network packets seems to be very CPU intensive.
1035
+ # Perhaps there is a more elegant way to wait for incoming messages?
1036
+ #
1037
+ @listen = true
1038
+ segments = Array.new
1039
+ while @listen
1040
+ # Receive data and append the current data to our segments array, which will be returned.
1041
+ data = receive_transmission(@min_length)
1042
+ current_segments = interpret(data, file)
1043
+ if current_segments
1044
+ current_segments.each do |cs|
1045
+ segments << cs
1046
+ end
1047
+ end
1048
+ end
1049
+ segments << {:valid => false} unless segments
1050
+ return segments
1051
+ end
1052
+
1053
+ # Handles the reception of a single, expected incoming transmission and returns the interpreted, received data.
1054
+ #
1055
+ def receive_single_transmission
1056
+ min_length = 8
1057
+ data = receive_transmission(min_length)
1058
+ segments = interpret(data)
1059
+ segments << {:valid => false} unless segments.length > 0
1060
+ return segments
1061
+ end
1062
+
1063
+ # Sets the session of this Link instance (used when this session is already established externally).
1064
+ #
1065
+ # === Parameters
1066
+ #
1067
+ # * <tt>session</tt> -- A TCP network connection that has been established with a remote node.
1068
+ #
1069
+ def set_session(session)
1070
+ @session = session
1071
+ end
1072
+
1073
+ # Establishes a new session with a remote network node.
1074
+ #
1075
+ # === Parameters
1076
+ #
1077
+ # * <tt>adress</tt> -- String. The adress (IP) of the remote node.
1078
+ # * <tt>port</tt> -- Fixnum. The network port to be used in the network communication.
1079
+ #
1080
+ def start_session(adress, port)
1081
+ @session = TCPSocket.new(adress, port)
1082
+ end
1083
+
1084
+ # Ends the current session by closing the connection.
1085
+ #
1086
+ def stop_session
1087
+ @session.close unless @session.closed?
1088
+ end
1089
+
1090
+ # Sends the outgoing message (encoded binary string) to the remote node.
1091
+ #
1092
+ def transmit
1093
+ @session.send(@outgoing.string, 0)
1094
+ end
1095
+
1096
+
1097
+ private
1098
+
1099
+
1100
+ # Builds the application context (which is part of the association request/response).
1101
+ #
1102
+ def append_application_context
1103
+ # Application context item type (1 byte)
1104
+ @outgoing.encode_last(ITEM_APPLICATION_CONTEXT, "HEX")
1105
+ # Reserved (1 byte)
1106
+ @outgoing.encode_last("00", "HEX")
1107
+ # Application context item length (2 bytes)
1108
+ @outgoing.encode_last(APPLICATION_CONTEXT.length, "US")
1109
+ # Application context (variable length)
1110
+ @outgoing.encode_last(APPLICATION_CONTEXT, "STR")
1111
+ end
1112
+
1113
+ # Builds the binary string that makes up the header part the association request/response.
1114
+ #
1115
+ # === Parameters
1116
+ #
1117
+ # * <tt>pdu</tt> -- The command fragment's PDU string.
1118
+ # * <tt>called_ae</tt> -- Application entity (name) of the SCP (host).
1119
+ #
1120
+ def append_association_header(pdu, called_ae)
1121
+ # Big endian encoding:
1122
+ @outgoing.endian = @net_endian
1123
+ # Header will be encoded in opposite order, where the elements are being put first in the outgoing binary string.
1124
+ # Build last part of header first. This is necessary to be able to assess the length value.
1125
+ # Reserved (32 bytes)
1126
+ @outgoing.encode_first("00"*32, "HEX")
1127
+ # Calling AE title (16 bytes)
1128
+ calling_ae = @outgoing.encode_string_with_trailing_spaces(@ae, 16)
1129
+ @outgoing.add_first(calling_ae) # (pre-encoded value)
1130
+ # Called AE title (16 bytes) (return the name that the SCU used in the association request)
1131
+ formatted_called_ae = @outgoing.encode_string_with_trailing_spaces(called_ae, 16)
1132
+ @outgoing.add_first(formatted_called_ae) # (pre-encoded value)
1133
+ # Reserved (2 bytes)
1134
+ @outgoing.encode_first("0000", "HEX")
1135
+ # Protocol version (2 bytes)
1136
+ @outgoing.encode_first("0001", "HEX")
1137
+ append_header(pdu)
1138
+ end
1139
+
1140
+ # Adds the header bytes to the outgoing message (the header structure is equal for all of the message types).
1141
+ #
1142
+ # === Parameters
1143
+ #
1144
+ # * <tt>pdu</tt> -- The command fragment's PDU string.
1145
+ #
1146
+ def append_header(pdu)
1147
+ # Length (of remaining data) (4 bytes)
1148
+ @outgoing.encode_first(@outgoing.string.length, "UL")
1149
+ # Reserved (1 byte)
1150
+ @outgoing.encode_first("00", "HEX")
1151
+ # PDU type (1 byte)
1152
+ @outgoing.encode_first(pdu, "HEX")
1153
+ end
1154
+
1155
+ # Builds the binary string that makes up the presentation context part of the association request/accept.
1156
+ #
1157
+ # === Notes
1158
+ #
1159
+ # * The values of the parameters will differ somewhat depending on whether this is related to a request or response.
1160
+ # * Description of error codes are given in the DICOM Standard, PS 3.8, Chapter 9.3.3.2 (Table 9-18).
1161
+ #
1162
+ # === Parameters
1163
+ #
1164
+ # * <tt>presentation_contexts</tt> -- A nested hash object with abstract syntaxes, presentation context ids, transfer syntaxes and result codes.
1165
+ # * <tt>item_type</tt> -- Presentation context item (request or response).
1166
+ # * <tt>request</tt> -- Boolean. If true, an ossociate request message is generated, if false, an asoociate accept message is generated.
1167
+ #
1168
+ def append_presentation_contexts(presentation_contexts, item_type, request=false)
1169
+ # Iterate the abstract syntaxes:
1170
+ presentation_contexts.each_pair do |abstract_syntax, context_ids|
1171
+ # Iterate the context ids:
1172
+ context_ids.each_pair do |context_id, syntax|
1173
+ # PRESENTATION CONTEXT:
1174
+ # Presentation context item type (1 byte)
1175
+ @outgoing.encode_last(item_type, "HEX")
1176
+ # Reserved (1 byte)
1177
+ @outgoing.encode_last("00", "HEX")
1178
+ # Presentation context item length (2 bytes)
1179
+ ts_length = 4*syntax[:transfer_syntaxes].length + syntax[:transfer_syntaxes].join.length
1180
+ # Abstract syntax item only included in requests, not accepts:
1181
+ items_length = 4 + ts_length
1182
+ items_length += 4 + abstract_syntax.length if request
1183
+ @outgoing.encode_last(items_length, "US")
1184
+ # Presentation context ID (1 byte)
1185
+ @outgoing.encode_last(context_id, "BY")
1186
+ # Reserved (1 byte)
1187
+ @outgoing.encode_last("00", "HEX")
1188
+ # (1 byte) Reserved (for association request) & Result/reason (for association accept response)
1189
+ result = (syntax[:result] ? syntax[:result] : 0)
1190
+ @outgoing.encode_last(result, "BY")
1191
+ # Reserved (1 byte)
1192
+ @outgoing.encode_last("00", "HEX")
1193
+ ## ABSTRACT SYNTAX SUB-ITEM: (only for request, not response)
1194
+ if request
1195
+ # Abstract syntax item type (1 byte)
1196
+ @outgoing.encode_last(ITEM_ABSTRACT_SYNTAX, "HEX")
1197
+ # Reserved (1 byte)
1198
+ @outgoing.encode_last("00", "HEX")
1199
+ # Abstract syntax item length (2 bytes)
1200
+ @outgoing.encode_last(abstract_syntax.length, "US")
1201
+ # Abstract syntax (variable length)
1202
+ @outgoing.encode_last(abstract_syntax, "STR")
1203
+ end
1204
+ ## TRANSFER SYNTAX SUB-ITEM (not included if result indicates error):
1205
+ if result == ACCEPTANCE
1206
+ syntax[:transfer_syntaxes].each do |t|
1207
+ # Transfer syntax item type (1 byte)
1208
+ @outgoing.encode_last(ITEM_TRANSFER_SYNTAX, "HEX")
1209
+ # Reserved (1 byte)
1210
+ @outgoing.encode_last("00", "HEX")
1211
+ # Transfer syntax item length (2 bytes)
1212
+ @outgoing.encode_last(t.length, "US")
1213
+ # Transfer syntax (variable length)
1214
+ @outgoing.encode_last(t, "STR")
1215
+ end
1216
+ end
1217
+ end
1218
+ end
1219
+ end
1220
+
1221
+ # Adds the binary string that makes up the user information part of the association request/response.
1222
+ #
1223
+ # === Parameters
1224
+ #
1225
+ # * <tt>ui</tt> -- User information items array.
1226
+ #
1227
+ def append_user_information(ui)
1228
+ # USER INFORMATION:
1229
+ # User information item type (1 byte)
1230
+ @outgoing.encode_last(ITEM_USER_INFORMATION, "HEX")
1231
+ # Reserved (1 byte)
1232
+ @outgoing.encode_last("00", "HEX")
1233
+ # Encode the user information item values so we can determine the remaining length of this section:
1234
+ values = Array.new
1235
+ ui.each_index do |i|
1236
+ values << @outgoing.encode(ui[i][2], ui[i][1])
1237
+ end
1238
+ # User information item length (2 bytes)
1239
+ items_length = 4*ui.length + values.join.length
1240
+ @outgoing.encode_last(items_length, "US")
1241
+ # SUB-ITEMS:
1242
+ ui.each_index do |i|
1243
+ # UI item type (1 byte)
1244
+ @outgoing.encode_last(ui[i][0], "HEX")
1245
+ # Reserved (1 byte)
1246
+ @outgoing.encode_last("00", "HEX")
1247
+ # UI item length (2 bytes)
1248
+ @outgoing.encode_last(values[i].length, "US")
1249
+ # UI value (4 bytes)
1250
+ @outgoing.add_last(values[i])
1251
+ end
1252
+ end
1253
+
1254
+ # Returns the appropriate response value for the Command Field (0000,0100) to be used in a command fragment (response).
1255
+ #
1256
+ # === Parameters
1257
+ #
1258
+ # * <tt>request</tt> -- The Command Field value in a command fragment (request).
1259
+ #
1260
+ def command_field_response(request)
1261
+ case request
1262
+ when C_STORE_RQ
1263
+ return C_STORE_RSP
1264
+ when C_ECHO_RQ
1265
+ return C_ECHO_RSP
1266
+ else
1267
+ logger.error("Unknown or unsupported request (#{request}) encountered.")
1268
+ return C_CANCEL_RQ
1269
+ end
1270
+ end
1271
+
1272
+ # Processes the value of the reason byte received in the association abort, and prints an explanation of the error.
1273
+ #
1274
+ # === Parameters
1275
+ #
1276
+ # * <tt>reason</tt> -- String. Reason code for an error that has occured.
1277
+ #
1278
+ def process_reason(reason)
1279
+ case reason
1280
+ when "00"
1281
+ logger.error("Reason specified for abort: Reason not specified")
1282
+ when "01"
1283
+ logger.error("Reason specified for abort: Unrecognized PDU")
1284
+ when "02"
1285
+ logger.error("Reason specified for abort: Unexpected PDU")
1286
+ when "04"
1287
+ logger.error("Reason specified for abort: Unrecognized PDU parameter")
1288
+ when "05"
1289
+ logger.error("Reason specified for abort: Unexpected PDU parameter")
1290
+ when "06"
1291
+ logger.error("Reason specified for abort: Invalid PDU parameter value")
1292
+ else
1293
+ logger.error("Reason specified for abort: Unknown reason (Error code: #{reason})")
1294
+ end
1295
+ end
1296
+
1297
+ # Processes the value of the result byte received in the association response.
1298
+ # Prints an explanation if an error is indicated.
1299
+ #
1300
+ # === Notes
1301
+ #
1302
+ # A value other than 0 indicates an error.
1303
+ #
1304
+ # === Parameters
1305
+ #
1306
+ # * <tt>result</tt> -- Fixnum. The result code from an association response.
1307
+ #
1308
+ def process_result(result)
1309
+ unless result == 0
1310
+ # Analyse the result and report what is wrong:
1311
+ case result
1312
+ when 1
1313
+ logger.warn("DICOM Request was rejected by the host, reason: 'User-rejection'")
1314
+ when 2
1315
+ logger.warn("DICOM Request was rejected by the host, reason: 'No reason (provider rejection)'")
1316
+ when 3
1317
+ logger.warn("DICOM Request was rejected by the host, reason: 'Abstract syntax not supported'")
1318
+ when 4
1319
+ logger.warn("DICOM Request was rejected by the host, reason: 'Transfer syntaxes not supported'")
1320
+ else
1321
+ logger.warn("DICOM Request was rejected by the host, reason: 'UNKNOWN (#{result})' (Illegal reason provided)")
1322
+ end
1323
+ end
1324
+ end
1325
+
1326
+ # Processes the value of the source byte in the association abort, and prints an explanation of the source (of the error).
1327
+ #
1328
+ # === Parameters
1329
+ #
1330
+ # * <tt>source</tt> -- String. A code which informs which part has been the source of an error.
1331
+ #
1332
+ def process_source(source)
1333
+ if source == "00"
1334
+ logger.warn("Connection has been aborted by the service provider because of an error by the service user (client side).")
1335
+ elsif source == "02"
1336
+ logger.warn("Connection has been aborted by the service provider because of an error by the service provider (server side).")
1337
+ else
1338
+ logger.warn("Connection has been aborted by the service provider, with an unknown cause of the problems. (error code: #{source})")
1339
+ end
1340
+ end
1341
+
1342
+ # Processes the value of the status element (0000,0900) received in the command fragment.
1343
+ # Prints an explanation where deemed appropriate.
1344
+ #
1345
+ # === Notes
1346
+ #
1347
+ # The status element has vr 'US', and the status as reported here is therefore a number.
1348
+ # In the official DICOM documents however, the values of the various status options are given in hex format.
1349
+ # Resources: The DICOM standard; PS3.4, Annex Q 2.1.1.4 & PS3.7 Annex C 4.
1350
+ #
1351
+ # === Parameters
1352
+ #
1353
+ # * <tt>status</tt> -- Fixnum. A status code from a command fragment.
1354
+ #
1355
+ def process_status(status)
1356
+ case status
1357
+ when 0 # "0000"
1358
+ # Last fragment (Break the while loop that listens continuously for incoming packets):
1359
+ logger.info("Receipt for successful execution of the desired operation has been received.")
1360
+ stop_receiving
1361
+ when 42752 # "a700"
1362
+ # Failure: Out of resources. Related fields: 0000,0902
1363
+ logger.error("Failure! SCP has given the following reason: 'Out of Resources'.")
1364
+ when 43264 # "a900"
1365
+ # Failure: Identifier Does Not Match SOP Class. Related fields: 0000,0901, 0000,0902
1366
+ logger.error("Failure! SCP has given the following reason: 'Identifier Does Not Match SOP Class'.")
1367
+ when 49152 # "c000"
1368
+ # Failure: Unable to process. Related fields: 0000,0901, 0000,0902
1369
+ logger.error("Failure! SCP has given the following reason: 'Unable to process'.")
1370
+ when 49408 # "c100"
1371
+ # Failure: More than one match found. Related fields: 0000,0901, 0000,0902
1372
+ logger.error("Failure! SCP has given the following reason: 'More than one match found'.")
1373
+ when 49664 # "c200"
1374
+ # Failure: Unable to support requested template. Related fields: 0000,0901, 0000,0902
1375
+ logger.error("Failure! SCP has given the following reason: 'Unable to support requested template'.")
1376
+ when 65024 # "fe00"
1377
+ # Cancel: Matching terminated due to Cancel request.
1378
+ logger.info("Cancel! SCP has given the following reason: 'Matching terminated due to Cancel request'.")
1379
+ when 65280 # "ff00"
1380
+ # Sub-operations are continuing.
1381
+ # (No particular action taken, the program will listen for and receive the coming fragments)
1382
+ when 65281 # "ff01"
1383
+ # More command/data fragments to follow.
1384
+ # (No particular action taken, the program will listen for and receive the coming fragments)
1385
+ else
1386
+ logger.error("Something was NOT successful regarding the desired operation. SCP responded with error code: #{status} (tag: 0000,0900). See DICOM PS3.7, Annex C for details.")
1387
+ end
1388
+ end
1389
+
1390
+ # Handles an incoming network transmission.
1391
+ # Returns the binary string data received.
1392
+ #
1393
+ # === Notes
1394
+ #
1395
+ # If a minimum length has been specified, and a message is received which is shorter than this length,
1396
+ # the method will keep listening for more incoming network packets to append.
1397
+ #
1398
+ # === Parameters
1399
+ #
1400
+ # * <tt>min_length</tt> -- Fixnum. The minimum possible length of a valid incoming transmission.
1401
+ #
1402
+ def receive_transmission(min_length=0)
1403
+ data = receive_transmission_data
1404
+ # Check the nature of the received data variable:
1405
+ if data
1406
+ # Sometimes the incoming transmission may be broken up into smaller pieces:
1407
+ # Unless a short answer is expected, we will continue to listen if the first answer was too short:
1408
+ unless min_length == 0
1409
+ if data.length < min_length
1410
+ addition = receive_transmission_data
1411
+ data = data + addition if addition
1412
+ end
1413
+ end
1414
+ else
1415
+ # It seems there was no incoming message and the operation timed out.
1416
+ # Convert the variable to an empty string.
1417
+ data = ""
1418
+ end
1419
+ data
1420
+ end
1421
+
1422
+ # Receives the data from an incoming network transmission.
1423
+ # Returns the binary string data received.
1424
+ #
1425
+ def receive_transmission_data
1426
+ data = false
1427
+ response = IO.select([@session], nil, nil, @timeout)
1428
+ if response.nil?
1429
+ logger.error("No answer was received within the specified timeout period. Aborting.")
1430
+ stop_receiving
1431
+ else
1432
+ data = @session.recv(@max_receive_size)
1433
+ end
1434
+ data
1435
+ end
1436
+
1437
+ # Sets some default values related to encoding.
1438
+ #
1439
+ def set_default_values
1440
+ # Default endianness for network transmissions is Big Endian:
1441
+ @net_endian = true
1442
+ # Default endianness of data is little endian:
1443
+ @data_endian = false
1444
+ # It may turn out to be unncessary to define the following values at this early stage.
1445
+ # Explicitness:
1446
+ @explicit = false
1447
+ # Transfer syntax:
1448
+ set_transfer_syntax(IMPLICIT_LITTLE_ENDIAN)
1449
+ end
1450
+
1451
+ # Set instance variables related to a transfer syntax.
1452
+ #
1453
+ # === Parameters
1454
+ #
1455
+ # * <tt>syntax</tt> -- A transfer syntax string.
1456
+ #
1457
+ def set_transfer_syntax(syntax)
1458
+ @transfer_syntax = syntax
1459
+ # Query the library with our particular transfer syntax string:
1460
+ ts = LIBRARY.uid(@transfer_syntax)
1461
+ @explicit = ts ? ts.explicit? : true
1462
+ @data_endian = ts ? ts.big_endian? : false
1463
+ logger.warn("Invalid/unknown transfer syntax encountered: #{@transfer_syntax} Will try to continue, but errors may occur.") unless ts
1464
+ end
1465
+
1466
+ # Sets the @user_information items instance array.
1467
+ #
1468
+ # === Notes
1469
+ #
1470
+ # Each user information item is a three element array consisting of: item type code, VR & value.
1471
+ #
1472
+ # === Parameters
1473
+ #
1474
+ # * <tt>info</tt> -- An association information hash.
1475
+ #
1476
+ def set_user_information_array(info=nil)
1477
+ @user_information = [
1478
+ [ITEM_MAX_LENGTH, "UL", @max_package_size],
1479
+ [ITEM_IMPLEMENTATION_UID, "STR", UID_ROOT],
1480
+ [ITEM_IMPLEMENTATION_VERSION, "STR", NAME]
1481
+ ]
1482
+ # A bit of a hack to include "asynchronous operations window negotiation" and/or "role negotiation",
1483
+ # in cases where this has been included in the association request:
1484
+ if info
1485
+ if info[:maxnum_operations_invoked]
1486
+ @user_information.insert(2, [ITEM_MAX_OPERATIONS_INVOKED, "HEX", "00010001"])
1487
+ end
1488
+ if info[:role_negotiation]
1489
+ pos = 3
1490
+ info[:role_negotiation].each do |role|
1491
+ msg = Stream.new('', @net_endian)
1492
+ uid = role[:sop_uid]
1493
+ # Length of UID (2 bytes):
1494
+ msg.encode_first(uid.length, "US")
1495
+ # SOP UID being negotiated (Variable length):
1496
+ msg.encode_last(uid, "STR")
1497
+ # SCU Role (Always accept SCU) (1 byte):
1498
+ if role[:scu] == 1
1499
+ msg.encode_last(1, "BY")
1500
+ else
1501
+ msg.encode_last(0, "BY")
1502
+ end
1503
+ # SCP Role (Never accept SCP) (1 byte):
1504
+ if role[:scp] == 1
1505
+ msg.encode_last(0, "BY")
1506
+ else
1507
+ msg.encode_last(1, "BY")
1508
+ end
1509
+ @user_information.insert(pos, [ITEM_ROLE_NEGOTIATION, "STR", msg.string])
1510
+ pos += 1
1511
+ end
1512
+ end
1513
+ end
1514
+ end
1515
+
1516
+ # Toggles two instance variables that in causes the loops that listen for incoming network packets to break.
1517
+ #
1518
+ # === Notes
1519
+ #
1520
+ # This method is called by the various methods that interpret incoming data when they have verified that
1521
+ # the entire message has been received, or when a timeout is reached.
1522
+ #
1523
+ def stop_receiving
1524
+ @listen = false
1525
+ @receive = false
1526
+ end
1527
+
1528
+ end
1529
+ end