dicom 0.5 → 0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1079 @@
1
+ # Copyright 2009 Christoffer Lervag
2
+
3
+ module DICOM
4
+
5
+ # This class handles the construction and interpretation of network packages
6
+ # as well as network communication.
7
+ class Link
8
+
9
+ attr_accessor :max_package_size, :verbose
10
+ attr_reader :errors, :notices
11
+
12
+ # Initialize the instance with a host adress and a port number.
13
+ def initialize(options={})
14
+ require 'socket'
15
+ # Optional parameters (and default values):
16
+ @ae = options[:ae] || "RUBY_DICOM"
17
+ @lib = options[:lib] || DLibrary.new
18
+ @host_ae = options[:host_ae] || "DEFAULT"
19
+ @max_package_size = options[:max_package_size] || 32768 # 16384
20
+ @max_receive_size = @max_package_size
21
+ @timeout = options[:timeout] || 10 # seconds
22
+ @min_length = 12 # minimum number of bytes to expect in an incoming transmission
23
+ @verbose = options[:verbose]
24
+ @verbose = true if @verbose == nil # Default verbosity is 'on'.
25
+ # Other instance variables:
26
+ @errors = Array.new # errors and warnings are put in this array
27
+ @notices = Array.new # information on successful transmissions are put in this array
28
+ # Variables used for monitoring state of transmission:
29
+ @connection = nil # TCP connection status
30
+ @association = nil # DICOM Association status
31
+ @request_approved = nil # Status of our DICOM request
32
+ @release = nil # Status of received, valid release response
33
+ set_default_values
34
+ set_user_information_array
35
+ @outgoing = Stream.new(nil, true, true)
36
+ end
37
+
38
+
39
+ # Build the abort message which is transmitted when the server wishes to (abruptly) abort the connection.
40
+ # For the moment: NO REASONS WILL BE PROVIDED. (and source of problems will always be set as client side)
41
+ def build_abort(message)
42
+ # Big endian encoding:
43
+ @outgoing.set_endian(@net_endian)
44
+ # Clear the outgoing binary string:
45
+ @outgoing.reset
46
+ # Reserved (2 bytes)
47
+ @outgoing.encode_first("00"*2, "HEX")
48
+ # Source (1 byte)
49
+ source = "00" # (client side error)
50
+ @outgoing.encode_first(source, "HEX")
51
+ # Reason/Diag. (1 byte)
52
+ reason = "00" # (Reason not specified)
53
+ @outgoing.encode_first(reason, "HEX")
54
+ end
55
+
56
+
57
+ # Build the binary string that will be sent as TCP data in the Association accept response.
58
+ def build_association_accept(ac_uid, ts, ui, result)
59
+ # Big endian encoding:
60
+ @outgoing.set_endian(@net_endian)
61
+ # Clear the outgoing binary string:
62
+ @outgoing.reset
63
+ # Set item types (pdu and presentation context):
64
+ pdu = "02"
65
+ pc = "21"
66
+ # No abstract syntax in association response:
67
+ as = nil
68
+ # Note: The order of which these components are built is not arbitrary.
69
+ append_application_context(ac_uid)
70
+ append_presentation_context(as, pc, ts, result)
71
+ append_user_information(ui)
72
+ # Header must be built last, because we need to know the length of the other components.
73
+ append_association_header(pdu)
74
+ end
75
+
76
+
77
+ # Build the binary string that will be sent as TCP data in the association rejection.
78
+ # NB: For the moment, this method will only customize the "reason" value.
79
+ # For a list of error codes, see the official dicom 08_08.pdf, page 41.
80
+ def build_association_reject(info)
81
+ # Big endian encoding:
82
+ @outgoing.set_endian(@net_endian)
83
+ # Clear the outgoing binary string:
84
+ @outgoing.reset
85
+ pdu = "03"
86
+ # Reserved (1 byte)
87
+ @outgoing.encode_last("00", "HEX")
88
+ # Result (1 byte)
89
+ @outgoing.encode_last("01", "HEX") # 1 for permament, 2 for transient
90
+ # Source (1 byte)
91
+ # (1: Service user, 2: Service provider (ACSE related function), 3: Service provider (Presentation related function)
92
+ @outgoing.encode_last("01", "HEX")
93
+ # Reason (1 byte)
94
+ reason = info[:reason]
95
+ @outgoing.encode_last(reason, "HEX")
96
+ append_header(pdu)
97
+ end
98
+
99
+
100
+ # Build the binary string that will be sent as TCP data in the Association request.
101
+ def build_association_request(ac_uid, as, ts, ui)
102
+ # Big endian encoding:
103
+ @outgoing.set_endian(@net_endian)
104
+ # Clear the outgoing binary string:
105
+ @outgoing.reset
106
+ # Set item types (pdu and presentation context):
107
+ pdu = "01"
108
+ pc = "20"
109
+ # Note: The order of which these components are built is not arbitrary.
110
+ append_application_context(ac_uid)
111
+ append_presentation_context(as, pc, ts)
112
+ append_user_information(ui)
113
+ # Header must be built last, because we need to know the length of the other components.
114
+ append_association_header(pdu)
115
+ end
116
+
117
+
118
+ # Build the binary string that will be sent as TCP data in the query command fragment.
119
+ # Typical values:
120
+ # pdu = "04" (data), context = "01", flags = "03"
121
+ def build_command_fragment(pdu, context, flags, command_elements)
122
+ # Little endian encoding:
123
+ @outgoing.set_endian(@data_endian)
124
+ # Clear the outgoing binary string:
125
+ @outgoing.reset
126
+ # Build the last part first, the Command items:
127
+ command_elements.each do |element|
128
+ # Tag (4 bytes)
129
+ @outgoing.add_last(@outgoing.encode_tag(element[0]))
130
+ # Encode the value first, so we know its length:
131
+ value = @outgoing.encode_value(element[2], element[1])
132
+ # Length (2 bytes)
133
+ @outgoing.encode_last(value.length, "US")
134
+ # Reserved (2 bytes)
135
+ @outgoing.encode_last("0000", "HEX")
136
+ # Value (variable length)
137
+ @outgoing.add_last(value)
138
+ end
139
+ # The rest of the command fragment will be buildt in reverse, all the time
140
+ # putting the elements first in the outgoing binary string.
141
+ # Group length item:
142
+ # Value (4 bytes)
143
+ @outgoing.encode_first(@outgoing.string.length, "UL")
144
+ # Reserved (2 bytes)
145
+ @outgoing.encode_first("0000", "HEX")
146
+ # Length (2 bytes)
147
+ @outgoing.encode_first(4, "US")
148
+ # Tag (4 bytes)
149
+ @outgoing.add_first(@outgoing.encode_tag("0000,0000"))
150
+ # Big endian encoding from now on:
151
+ @outgoing.set_endian(@net_endian)
152
+ # Flags (1 byte)
153
+ @outgoing.encode_first(flags, "HEX") # Command, last fragment (identifier)
154
+ # Presentation context ID (1 byte)
155
+ @outgoing.encode_first(context, "HEX") # Explicit VR Little Endian, Study Root Query/Retrieve.... (what does this reference, the earlier abstract syntax? transfer syntax?)
156
+ # Length (of remaining data) (4 bytes)
157
+ @outgoing.encode_first(@outgoing.string.length, "UL")
158
+ # PRESENTATION DATA VALUE (the above)
159
+ append_header(pdu)
160
+ end
161
+
162
+
163
+ # Build the binary string that will be sent as TCP data in the query data fragment.
164
+ # NB!! This method does not (yet) take explicitness into consideration when building content.
165
+ # It might go wrong if an implicit data stream is encountered!
166
+ def build_data_fragment(data_elements)
167
+ # Little endian encoding:
168
+ @outgoing.set_endian(@data_endian)
169
+ # Clear the outgoing binary string:
170
+ @outgoing.reset
171
+ pdu = "04"
172
+ # Build the last part first, the Data items:
173
+ data_elements.each do |element|
174
+ # Encode all tags (even tags which are empty):
175
+ # Tag (4 bytes)
176
+ @outgoing.add_last(@outgoing.encode_tag(element[0]))
177
+ # Type (VR) (2 bytes)
178
+ vr = @lib.get_name_vr(element[0])[1]
179
+ @outgoing.encode_last(vr, "STR")
180
+ # Encode the value first, so we know its length:
181
+ value = @outgoing.encode_value(element[1], vr)
182
+ # Length (2 bytes)
183
+ @outgoing.encode_last(value.length, "US")
184
+ # Value (variable length)
185
+ @outgoing.add_last(value)
186
+ end
187
+ # The rest of the data fragment will be built in reverse, all the time
188
+ # putting the elements first in the outgoing binary string.
189
+ # Big endian encoding from now on:
190
+ @outgoing.set_endian(@net_endian)
191
+ # Flags (1 byte)
192
+ @outgoing.encode_first("02", "HEX") # Data, last fragment (identifier)
193
+ # Presentation context ID (1 byte)
194
+ @outgoing.encode_first("01", "HEX") # Explicit VR Little Endian, Study Root Query/Retrieve.... (what does this reference, the earlier abstract syntax? transfer syntax?)
195
+ # Length (of remaining data) (4 bytes)
196
+ @outgoing.encode_first(@outgoing.string.length, "UL")
197
+ # PRESENTATION DATA VALUE (the above)
198
+ append_header(pdu)
199
+ end
200
+
201
+
202
+ # Build the binary string that will be sent as TCP data in the association release request:
203
+ def build_release_request
204
+ # Big endian encoding:
205
+ @outgoing.set_endian(@net_endian)
206
+ # Clear the outgoing binary string:
207
+ @outgoing.reset
208
+ pdu = "05"
209
+ # Reserved (4 bytes)
210
+ @outgoing.encode_last("00"*4, "HEX")
211
+ append_header(pdu)
212
+ end
213
+
214
+
215
+ # Build the binary string that will be sent as TCP data in the association release response.
216
+ def build_release_response
217
+ # Big endian encoding:
218
+ @outgoing.set_endian(@net_endian)
219
+ # Clear the outgoing binary string:
220
+ @outgoing.reset
221
+ pdu = "06"
222
+ # Reserved (4 bytes)
223
+ @outgoing.encode_last("00000000", "HEX")
224
+ append_header(pdu)
225
+ end
226
+
227
+
228
+ # Build the binary string that makes up a storage data fragment.
229
+ # Typical value: flags = "00" (more fragments following), flags = "02" (last fragment)
230
+ # pdu = "04", context = "01"
231
+ def build_storage_fragment(pdu, context, flags, body)
232
+ # Big endian encoding:
233
+ @outgoing.set_endian(@net_endian)
234
+ # Clear the outgoing binary string:
235
+ @outgoing.reset
236
+ # Build in reverse, putting elements in front of the binary string:
237
+ # Insert the data (body):
238
+ @outgoing.add_last(body)
239
+ # Flags (1 byte)
240
+ @outgoing.encode_first(flags, "HEX")
241
+ # Context ID (1 byte)
242
+ @outgoing.encode_first(context, "HEX")
243
+ # PDV Length (of remaining data) (4 bytes)
244
+ @outgoing.encode_first(@outgoing.string.length, "UL")
245
+ # PRESENTATION DATA VALUE (the above)
246
+ append_header(pdu)
247
+ end
248
+
249
+
250
+ # Delegates an incoming message to its correct interpreter method, based on pdu type.
251
+ def forward_to_interpret(message, pdu, file = nil)
252
+ case pdu
253
+ when "01" # Associatin request
254
+ info = interpret_association_request(message)
255
+ when "02" # Accepted association
256
+ info = interpret_association_accept(message)
257
+ when "03" # Rejected association
258
+ info = interpret_association_reject(message)
259
+ when "04" # Data
260
+ info = interpret_command_and_data(message, file)
261
+ when "05"
262
+ info = interpret_release_request(message)
263
+ when "06" # Release response
264
+ info = interpret_release_response(message)
265
+ when "07" # Abort connection
266
+ info = interpret_abort(message)
267
+ else
268
+ info = {:valid => false}
269
+ add_error("An unknown pdu type was received in the incoming transmission. Can not decode this message. (pdu: #{pdu})")
270
+ end
271
+ return info
272
+ end
273
+
274
+
275
+ # Handles the abortion of a session, when a non-valid message has been received.
276
+ def handle_abort(session)
277
+ add_notice("An unregonizable (non-DICOM) message was received.")
278
+ build_association_abort
279
+ transmit(session)
280
+ end
281
+
282
+
283
+ # Handles the association accept.
284
+ def handle_association_accept(session, info, syntax_result)
285
+ application_context = info[:application_context]
286
+ abstract_syntax = info[:abstract_syntax]
287
+ transfer_syntax = info[:ts].first[:transfer_syntax]
288
+ build_association_accept(application_context, transfer_syntax, @user_information, syntax_result)
289
+ transmit(session)
290
+ end
291
+
292
+
293
+ # Process the data that was received from the user.
294
+ # We expect this to be an initial C-STORE-RQ followed by a bunch of data fragments.
295
+ def handle_incoming_data(session, path)
296
+ # Wait for incoming data:
297
+ segments = receive_multiple_transmissions(session, file = true)
298
+ # Reset command results arrays:
299
+ @command_results = Array.new
300
+ @data_results = Array.new
301
+ # Try to extract data:
302
+ file_data = Array.new
303
+ segments.each do |info|
304
+ if info[:valid]
305
+ # Determine if it is command or data:
306
+ if info[:presentation_context_flag] == "00" or info[:presentation_context_flag] == "02"
307
+ # Data (last fragment)
308
+ @data_results << info[:results]
309
+ file_data << info[:bin]
310
+ elsif info[:presentation_context_flag] == "03"
311
+ # Command (last fragment):
312
+ @command_results << info[:results]
313
+ @presentation_context_id = info[:presentation_context_id]
314
+ end
315
+ end
316
+ end
317
+ data = file_data.join
318
+ # Read the received data stream and load it as a DICOM object:
319
+ obj = DObject.new(data, :bin => true, :syntax => @transfer_syntax)
320
+ # File will be saved with the following path:
321
+ # original_path/<PatientID>/<StudyDate>/<Modality>/
322
+ # File name will be equal to the SOP Instance UID
323
+ file_name = obj.get_value("0008,0018")
324
+ folders = Array.new(3)
325
+ folders[0] = obj.get_value("0010,0020") || "PatientID"
326
+ folders[1] = obj.get_value("0008,0020") || "StudyDate"
327
+ folders[2] = obj.get_value("0008,0060") || "Modality"
328
+ full_path = path + folders.join(File::SEPARATOR) + File::SEPARATOR + file_name
329
+ obj.write(full_path, @transfer_syntax)
330
+ return full_path
331
+ end
332
+
333
+
334
+ # Handles the rejection of an association, when the formalities of the association is not correct.
335
+ def handle_rejection(session)
336
+ add_notice("An incoming association request was rejected. Error code: #{association_error}")
337
+ # Insert the error code in the info hash:
338
+ info[:reason] = association_error
339
+ # Send an association rejection:
340
+ build_association_reject(info)
341
+ transmit(session)
342
+ end
343
+
344
+
345
+ # Handles the release of an association.
346
+ def handle_release(session)
347
+ segments = receive_single_transmission(session)
348
+ info = segments.first
349
+ if info[:pdu] == "05"
350
+ add_notice("Received a release request. Releasing association.")
351
+ build_release_response
352
+ transmit(session)
353
+ end
354
+ end
355
+
356
+
357
+ # Handles the response (C-STORE-RSP) when a DICOM object has been (successfully) received.
358
+ def handle_response(session)
359
+ tags = @command_results.first
360
+ # Need to construct the command elements array:
361
+ command_elements = Array.new
362
+ # SOP Class UID:
363
+ command_elements << ["0000,0002", "UI", tags["0000,0002"]]
364
+ # Command Field:
365
+ command_elements << ["0000,0100", "US", 32769] # C-STORE-RSP
366
+ # Message ID Being Responded To:
367
+ command_elements << ["0000,0120", "US", tags["0000,0110"]] # (Message ID)
368
+ # Data Set Type:
369
+ command_elements << ["0000,0800", "US", 257]
370
+ # Status:
371
+ command_elements << ["0000,0900", "US", 0] # (Success)
372
+ # Affected SOP Instance UID:
373
+ command_elements << ["0000,1000", "UI", tags["0000,1000"]]
374
+ pdu = "04"
375
+ context = @presentation_context_id
376
+ flag = "03" # (Command, last fragment)
377
+ build_command_fragment(pdu, context, flag, command_elements)
378
+ transmit(session)
379
+ end
380
+
381
+
382
+ # Decode an incoming transmission., decide its type, and forward its content to the various methods that process these.
383
+ def interpret(message, file = nil)
384
+ if @first_part
385
+ message = @first_part + message
386
+ @first_part = nil
387
+ end
388
+ segments = Array.new
389
+ # If the message is at least 8 bytes we can start decoding it:
390
+ if message.length > 8
391
+ # Create a new Stream instance to handle this response.
392
+ msg = Stream.new(message, @net_endian, @explicit)
393
+ # PDU type ( 1 byte)
394
+ pdu = msg.decode(1, "HEX")
395
+ # Reserved (1 byte)
396
+ msg.skip(1)
397
+ # Length of remaining data (4 bytes)
398
+ specified_length = msg.decode(4, "UL")
399
+ # Analyze the remaining length of the message versurs the specified_length value:
400
+ if msg.rest_length > specified_length
401
+ # If the remaining length of the string itself is bigger than this specified_length value,
402
+ # then it seems that we have another message appended in our incoming transmission.
403
+ fragment = msg.extract(specified_length)
404
+ info = forward_to_interpret(fragment, pdu, file)
405
+ info[:pdu] = pdu
406
+ # The information gathered from the interpretation is appended to a segments array,
407
+ # and in the case of a recursive call some special logic is needed to build this array in the expected fashion.
408
+ segments << info
409
+ remaining_segments = interpret(msg.rest_string, file)
410
+ remaining_segments.each do |remaining|
411
+ segments << remaining
412
+ end
413
+ elsif msg.rest_length == specified_length
414
+ # Proceed to analyze the rest of the message:
415
+ fragment = msg.extract(specified_length)
416
+ info = forward_to_interpret(fragment, pdu, file)
417
+ info[:pdu] = pdu
418
+ segments << info
419
+ else
420
+ # Length of the message is less than what is specified in the message. Throw error:
421
+ #add_error("Error. The length of the received message (#{msg.rest_length}) is smaller than what it claims (#{specified_length}). Aborting.")
422
+ @first_part = msg.string
423
+ end
424
+ else
425
+ # Assume that this is only the start of the message, and add it to the next incoming string:
426
+ @first_part = message
427
+ end
428
+ return segments
429
+ end
430
+
431
+
432
+ # Decode the binary string received when the provider wishes to abort the connection, for some reason.
433
+ def interpret_abort(message)
434
+ info = Hash.new
435
+ msg = Stream.new(message, @net_endian, @explicit)
436
+ # Reserved (2 bytes)
437
+ reserved_bytes = msg.skip(2)
438
+ # Source (1 byte)
439
+ info[:source] = msg.decode(1, "HEX")
440
+ # Reason/Diag. (1 byte)
441
+ info[:reason] = msg.decode(1, "HEX")
442
+ # Analyse the results:
443
+ if info[:source] == "00"
444
+ add_error("Warning: Connection has been aborted by the service provider because of an error by the service user (client side).")
445
+ elsif info[:source] == "02"
446
+ add_error("Warning: Connection has been aborted by the service provider because of an error by the service provider (server side).")
447
+ else
448
+ add_error("Warning: Connection has been aborted by the service provider, with an unknown cause of the problems. (error code: #{info[:source]})")
449
+ end
450
+ if info[:source] != "00"
451
+ # Display reason for error:
452
+ case info[:reason]
453
+ when "00"
454
+ add_error("Reason specified for abort: Reason not specified")
455
+ when "01"
456
+ add_error("Reason specified for abort: Unrecognized PDU")
457
+ when "02"
458
+ add_error("Reason specified for abort: Unexpected PDU")
459
+ when "04"
460
+ add_error("Reason specified for abort: Unrecognized PDU parameter")
461
+ when "05"
462
+ add_error("Reason specified for abort: Unexpected PDU parameter")
463
+ when "06"
464
+ add_error("Reason specified for abort: Invalid PDU parameter value")
465
+ else
466
+ add_error("Reason specified for abort: Unknown reason (Error code: #{info[:reason]})")
467
+ end
468
+ end
469
+ @listen = false
470
+ @receive = false
471
+ @abort = true
472
+ info[:valid] = true
473
+ return info
474
+ end
475
+
476
+
477
+ # Decode the binary string received in the association response, and interpret its content.
478
+ def interpret_association_accept(message)
479
+ info = Hash.new
480
+ msg = Stream.new(message, @net_endian, @explicit)
481
+ # Protocol version (2 bytes)
482
+ info[:protocol_version] = msg.decode(2, "HEX")
483
+ # Reserved (2 bytes)
484
+ msg.skip(2)
485
+ # Called AE (shall be identical to the one sent in the request, but not tested against) (16 bytes)
486
+ info[:called_ae] = msg.decode(16, "STR")
487
+ # Calling AE (shall be identical to the one sent in the request, but not tested against) (16 bytes)
488
+ info[:calling_ae] = msg.decode(16, "STR")
489
+ # Reserved (32 bytes)
490
+ msg.skip(32)
491
+ # APPLICATION CONTEXT:
492
+ # Item type (1 byte)
493
+ info[:application_item_type] = msg.decode(1, "HEX")
494
+ # Reserved (1 byte)
495
+ msg.skip(1)
496
+ # Application item length (2 bytes)
497
+ info[:application_item_length] = msg.decode(2, "US")
498
+ # Application context (variable length)
499
+ info[:application_context] = msg.decode(info[:application_item_length], "STR")
500
+ # PRESENTATION CONTEXT:
501
+ # Item type (1 byte)
502
+ info[:presentation_item_type] = msg.decode(1, "HEX")
503
+ # Reserved (1 byte)
504
+ msg.skip(1)
505
+ # Presentation item length (2 bytes)
506
+ info[:presentation_item_length] = msg.decode(2, "US")
507
+ # Presentation context ID (1 byte)
508
+ info[:presentation_context_id] = msg.decode(1, "HEX")
509
+ # Reserved (1 byte)
510
+ msg.skip(1)
511
+ # Result (& Reason) (1 byte)
512
+ info[:result] = msg.decode(1, "BY")
513
+ # Analyse the results:
514
+ unless info[:result] == 0
515
+ case info[:result]
516
+ when 1
517
+ add_error("Warning: DICOM Request was rejected by the host, reason: 'User-rejection'")
518
+ when 2
519
+ add_error("Warning: DICOM Request was rejected by the host, reason: 'No reason (provider rejection)'")
520
+ when 3
521
+ add_error("Warning: DICOM Request was rejected by the host, reason: 'Abstract syntax not supported'")
522
+ when 4
523
+ add_error("Warning: DICOM Request was rejected by the host, reason: 'Transfer syntaxes not supported'")
524
+ else
525
+ add_error("Warning: DICOM Request was rejected by the host, reason: 'UNKNOWN (#{info[:result]})' (Illegal reason provided)")
526
+ end
527
+ end
528
+ # Reserved (1 byte)
529
+ msg.skip(1)
530
+ # Transfer syntax sub-item:
531
+ # Item type (1 byte)
532
+ info[:transfer_syntax_item_type] = msg.decode(1, "HEX")
533
+ # Reserved (1 byte)
534
+ msg.skip(1)
535
+ # Transfer syntax item length (2 bytes)
536
+ info[:transfer_syntax_item_length] = msg.decode(2, "US")
537
+ # Transfer syntax name (variable length)
538
+ info[:transfer_syntax] = msg.decode(info[:transfer_syntax_item_length], "STR")
539
+ # USER INFORMATION:
540
+ # Item type (1 byte)
541
+ info[:user_info_item_type] = msg.decode(1, "HEX")
542
+ # Reserved (1 byte)
543
+ msg.skip(1)
544
+ # User information item length (2 bytes)
545
+ info[:user_info_item_length] = msg.decode(2, "US")
546
+ while msg.index < msg.length do
547
+ # Item type (1 byte)
548
+ item_type = msg.decode(1, "HEX")
549
+ # Reserved (1 byte)
550
+ msg.skip(1)
551
+ # Item length (2 bytes)
552
+ item_length = msg.decode(2, "US")
553
+ case item_type
554
+ when "51"
555
+ info[:max_pdu_length] = msg.decode(item_length, "UL")
556
+ @max_receive_size = info[:max_pdu_length]
557
+ when "52"
558
+ info[:implementation_class_uid] = msg.decode(item_length, "STR")
559
+ when "55"
560
+ info[:implementation_version] = msg.decode(item_length, "STR")
561
+ else
562
+ add_error("Unknown user info item type received. Please update source code or contact author. (item type: " + item_type + ")")
563
+ end
564
+ end
565
+ info[:valid] = true
566
+ # Update key values for this instance:
567
+ set_transfer_syntax(info[:transfer_syntax])
568
+ return info
569
+ end # of interpret_association_accept
570
+
571
+
572
+ # Decode the association reject message and extract the error reasons given.
573
+ def interpret_association_reject(message)
574
+ info = Hash.new
575
+ msg = Stream.new(message, @net_endian, @explicit)
576
+ # Reserved (1 byte)
577
+ msg.skip(1)
578
+ # Result (1 byte)
579
+ info[:result] = msg.decode(1, "BY") # 1 for permanent and 2 for transient rejection
580
+ # Source (1 byte)
581
+ info[:source] = msg.decode(1, "BY")
582
+ # Reason (1 byte)
583
+ info[:reason] = msg.decode(1, "BY")
584
+ add_error("Warning: ASSOCIATE Request was rejected by the host. Error codes: Result: #{info[:result]}, Source: #{info[:source]}, Reason: #{info[:reason]} (See DICOM 08_08, page 41: Table 9-21 for details.)")
585
+ info[:valid] = true
586
+ return info
587
+ end
588
+
589
+
590
+ # Decode the binary string received in the association request, and interpret its content.
591
+ def interpret_association_request(message)
592
+ info = Hash.new
593
+ msg = Stream.new(message, @net_endian, @explicit)
594
+ # Protocol version (2 bytes)
595
+ info[:protocol_version] = msg.decode(2, "HEX")
596
+ # Reserved (2 bytes)
597
+ msg.skip(2)
598
+ # Called AE (shall be returned in the association response) (16 bytes)
599
+ info[:called_ae] = msg.decode(16, "STR")
600
+ # Calling AE (shall be returned in the association response) (16 bytes)
601
+ info[:calling_ae] = msg.decode(16, "STR")
602
+ # Reserved (32 bytes)
603
+ msg.skip(32)
604
+ # APPLICATION CONTEXT:
605
+ # Item type (1 byte)
606
+ info[:application_item_type] = msg.decode(1, "HEX") # "10"
607
+ # Reserved (1 byte)
608
+ msg.skip(1)
609
+ # Application item length (2 bytes)
610
+ info[:application_item_length] = msg.decode(2, "US")
611
+ # Application context (variable length)
612
+ info[:application_context] = msg.decode(info[:application_item_length], "STR")
613
+ # PRESENTATION CONTEXT:
614
+ # Item type (1 byte)
615
+ info[:presentation_item_type] = msg.decode(1, "HEX") # "20"
616
+ # Reserved (1 byte)
617
+ msg.skip(1)
618
+ # Presentation context item length (2 bytes)
619
+ info[:presentation_item_length] = msg.decode(2, "US")
620
+ # Presentation context ID (1 byte)
621
+ info[:presentation_context_id] = msg.decode(1, "HEX")
622
+ # Reserved (3 bytes)
623
+ msg.skip(3)
624
+ # ABSTRACT SYNTAX SUB-ITEM:
625
+ # Abstract syntax item type (1 byte)
626
+ info[:abstract_syntax_item_type] = msg.decode(1, "HEX") # "30"
627
+ # Reserved (1 byte)
628
+ msg.skip(1)
629
+ # Abstract syntax item length (2 bytes)
630
+ info[:abstract_syntax_item_length] = msg.decode(2, "US")
631
+ # Abstract syntax (variable length)
632
+ info[:abstract_syntax] = msg.decode(info[:abstract_syntax_item_length], "STR")
633
+ ## TRANSFER SYNTAX SUB-ITEM(S):
634
+ item_type = "40"
635
+ # As multiple TS may occur, we need a loop to catch them all:
636
+ # Each TS Hash will be put in an array, which will be put in the info hash.
637
+ ts_array = Array.new
638
+ while item_type == "40" do
639
+ # Item type (1 byte)
640
+ item_type = msg.decode(1, "HEX")
641
+ if item_type == "40"
642
+ ts = Hash.new
643
+ ts[:transfer_syntax_item_type] = item_type
644
+ # Reserved (1 byte)
645
+ msg.skip(1)
646
+ # Transfer syntax item length (2 bytes)
647
+ ts[:transfer_syntax_item_length] = msg.decode(2, "US")
648
+ # Transfer syntax name (variable length)
649
+ ts[:transfer_syntax] = msg.decode(ts[:transfer_syntax_item_length], "STR")
650
+ ts_array << ts
651
+ else
652
+ info[:user_info_item_type] = item_type # "50"
653
+ end
654
+ end
655
+ info[:ts] = ts_array
656
+ # USER INFORMATION:
657
+ # Reserved (1 byte)
658
+ msg.skip(1)
659
+ # User information item length (2 bytes)
660
+ info[:user_info_item_length] = msg.decode(2, "US")
661
+ # User data (variable length):
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 "51"
671
+ info[:max_pdu_length] = msg.decode(item_length, "UL")
672
+ when "52"
673
+ info[:implementation_class_uid] = msg.decode(item_length, "STR")
674
+ when "55"
675
+ info[:implementation_version] = msg.decode(item_length, "STR")
676
+ else
677
+ add_error("Unknown user info item type received. Please update source code or contact author. (item type: " + item_type + ")")
678
+ end
679
+ end
680
+ info[:valid] = true
681
+ return info
682
+ end # of interpret_association_request
683
+
684
+
685
+ # Decode the binary string received in the query response, and interpret its content.
686
+ # NB!! This method does not (yet) take explicitness into consideration when decoding content.
687
+ # It might go wrong if an implicit data stream is encountered!
688
+ def interpret_command_and_data(message, file = nil)
689
+ info = Hash.new
690
+ msg = Stream.new(message, @net_endian, @explicit)
691
+ # Length (of remaining PDV data) (4 bytes)
692
+ info[:presentation_data_value_length] = msg.decode(4, "UL")
693
+ # Presentation context ID (1 byte)
694
+ info[:presentation_context_id] = msg.decode(1, "HEX") # "01" expected
695
+ # Flags (1 byte)
696
+ info[:presentation_context_flag] = msg.decode(1, "HEX") # "03" for command (last fragment), "02" for data
697
+ # Little endian encoding from now on:
698
+ msg.set_endian(@data_endian)
699
+ # We will put the results in a hash:
700
+ results = Hash.new
701
+ if info[:presentation_context_flag] == "03"
702
+ # COMMAND, LAST FRAGMENT:
703
+ while msg.index < msg.length do
704
+ # Tag (4 bytes)
705
+ tag = msg.decode_tag
706
+ # Length (2 bytes)
707
+ length = msg.decode(2, "US")
708
+ if length > msg.rest_length
709
+ add_error("Error: Specified length of command element value exceeds remaining length of the received message! Something is wrong.")
710
+ end
711
+ # Reserved (2 bytes)
712
+ msg.skip(2)
713
+ # Type (VR) (from library - not the stream):
714
+ result = @lib.get_name_vr(tag)
715
+ name = result[0]
716
+ type = result[1]
717
+ # Value (variable length)
718
+ value = msg.decode(length, type)
719
+ # Put tag and value in a hash:
720
+ results[tag] = value
721
+ end
722
+ # The results hash is put in an array along with (possibly) other results:
723
+ info[:results] = results
724
+ # Check if the command fragment indicates that this was the last of the response fragments for this query:
725
+ status = results["0000,0900"]
726
+ if status
727
+ if status == 0
728
+ # Last fragment (Break the while loop that listens continuously for incoming packets):
729
+ add_notice("Receipt for successful execution of the desired request has been received. Closing communication.")
730
+ @listen = false
731
+ @receive = false
732
+ elsif status == 65281
733
+ # Status = "01 ff": More command/data fragments to follow.
734
+ # (No particular action taken, the program will listen for and receive the coming fragments)
735
+ elsif status == 65280
736
+ # Status = "00 ff": Sub-operations are continuing.
737
+ # (No particular action taken, the program will listen for and receive the coming fragments)
738
+ else
739
+ add_error("Error! Something was NOT successful regarding the desired operation. (SCP responded with error code: #{status}) (tag: 0000,0900)")
740
+ end
741
+ end
742
+ elsif info[:presentation_context_flag] == "00" or info[:presentation_context_flag] == "02"
743
+ # DATA FRAGMENT:
744
+ # If this is a file transmission, we will delay the decoding for later:
745
+ if file
746
+ # Just store the binary string:
747
+ info[:bin] = msg.rest_string
748
+ # Abort the listening if this is last data fragment:
749
+ if info[:presentation_context_flag] == "02"
750
+ @listen = false
751
+ @receive = false
752
+ end
753
+ else
754
+ # Decode data elements:
755
+ while msg.index < msg.length do
756
+ # Tag (4 bytes)
757
+ tag = msg.decode_tag
758
+ # Type (VR) (2 bytes):
759
+ type = msg.decode(2, "STR")
760
+ # Length (2 bytes)
761
+ length = msg.decode(2, "US")
762
+ if length > msg.rest_length
763
+ add_error("Error: Specified length of data element value exceeds remaining length of the received message! Something is wrong.")
764
+ end
765
+ # Value (variable length)
766
+ value = msg.decode(length, type)
767
+ result = @lib.get_name_vr(tag)
768
+ name = result[0]
769
+ # Put tag and value in a hash:
770
+ results[tag] = value
771
+ end
772
+ # The results hash is put in an array along with (possibly) other results:
773
+ info[:results] = results
774
+ end
775
+ else
776
+ # Unknown.
777
+ add_error("Error: Unknown presentation context flag received in the query/command response. (#{info[:presentation_context_flag]})")
778
+ @listen = false
779
+ @receive = false
780
+ end
781
+ info[:valid] = true
782
+ return info
783
+ end
784
+
785
+
786
+ # Decode the binary string received in the release request, and interpret its content.
787
+ def interpret_release_request(message)
788
+ info = Hash.new
789
+ msg = Stream.new(message, @net_endian, @explicit)
790
+ # Reserved (4 bytes)
791
+ reserved_bytes = msg.decode(4, "HEX")
792
+ info[:valid] = true
793
+ return info
794
+ end
795
+
796
+
797
+ # Decode the binary string received in the release response, and interpret its content.
798
+ def interpret_release_response(message)
799
+ info = Hash.new
800
+ msg = Stream.new(message, @net_endian, @explicit)
801
+ # Reserved (4 bytes)
802
+ reserved_bytes = msg.decode(4, "HEX")
803
+ info[:valid] = true
804
+ return info
805
+ end
806
+
807
+
808
+ # Handle multiple incoming transmissions and return the interpreted, received data.
809
+ def receive_multiple_transmissions(session, file = nil)
810
+ @listen = true
811
+ segments = Array.new
812
+ while @listen
813
+ # Receive data and append the current data to our segments array, which will be returned.
814
+ data = receive_transmission(session, @min_length)
815
+ current_segments = interpret(data, file)
816
+ if current_segments
817
+ current_segments.each do |cs|
818
+ segments << cs
819
+ end
820
+ end
821
+ end
822
+ segments << {:valid => false} unless segments
823
+ return segments
824
+ end
825
+
826
+
827
+ # Handle an expected single incoming transmission and return the interpreted, received data.
828
+ def receive_single_transmission(session)
829
+ min_length = 8
830
+ data = receive_transmission(session, min_length)
831
+ segments = interpret(data)
832
+ segments << {:valid => false} unless segments.length > 0
833
+ return segments
834
+ end
835
+
836
+
837
+ # Send the encoded binary string (package) to its destination.
838
+ def transmit(session)
839
+ session.send(@outgoing.string, 0)
840
+ end
841
+
842
+
843
+ # Following methods are private:
844
+ private
845
+
846
+
847
+ # Adds a warning or error message to the instance array holding messages,
848
+ # and if verbose variable is true, prints the message as well.
849
+ def add_error(error)
850
+ puts error if @verbose
851
+ @errors << error
852
+ end
853
+
854
+
855
+ # Adds a notice (information regarding progress or successful communications) to the instance array,
856
+ # and if verbosity is set for these kinds of messages, prints it to the screen as well.
857
+ def add_notice(notice)
858
+ puts notice if @verbose
859
+ @notices << notice
860
+ end
861
+
862
+
863
+ # Builds the application context that is part of the association request.
864
+ def append_application_context(ac_uid)
865
+ # Application context item type (1 byte)
866
+ @outgoing.encode_last("10", "HEX")
867
+ # Reserved (1 byte)
868
+ @outgoing.encode_last("00", "HEX")
869
+ # Application context item length (2 bytes)
870
+ @outgoing.encode_last(ac_uid.length, "US")
871
+ # Application context (variable length)
872
+ @outgoing.encode_last(ac_uid, "STR")
873
+ end
874
+
875
+
876
+ # Build the binary string that makes up the header part (part of the association request).
877
+ def append_association_header(pdu)
878
+ # Big endian encoding:
879
+ @outgoing.set_endian(@net_endian)
880
+ # Header will be encoded in opposite order, where the elements are being put first in the outgoing binary string.
881
+ # Build last part of header first. This is necessary to be able to assess the length value.
882
+ # Reserved (32 bytes)
883
+ @outgoing.encode_first("00"*32, "HEX")
884
+ # Calling AE title (16 bytes)
885
+ calling_ae = @outgoing.encode_string_with_trailing_spaces(@ae, 16)
886
+ @outgoing.add_first(calling_ae) # (pre-encoded value)
887
+ # Called AE title (16 bytes)
888
+ called_ae = @outgoing.encode_string_with_trailing_spaces(@host_ae, 16)
889
+ @outgoing.add_first(called_ae) # (pre-encoded value)
890
+ # Reserved (2 bytes)
891
+ @outgoing.encode_first("0000", "HEX")
892
+ # Protocol version (2 bytes)
893
+ @outgoing.encode_first("0001", "HEX")
894
+ append_header(pdu)
895
+ end
896
+
897
+
898
+ # Adds the header bytes to the outgoing, binary string (this part has the same structure for all dicom network messages)
899
+ # PDU: "01", "02", etc..
900
+ def append_header(pdu)
901
+ # Length (of remaining data) (4 bytes)
902
+ @outgoing.encode_first(@outgoing.string.length, "UL")
903
+ # Reserved (1 byte)
904
+ @outgoing.encode_first("00", "HEX")
905
+ # PDU type (1 byte)
906
+ @outgoing.encode_first(pdu, "HEX")
907
+ end
908
+
909
+
910
+ # Build the binary string that makes up the presentation context part (part of the association request).
911
+ # For a list of error codes, see the official dicom 08_08.pdf, page 39.
912
+ def append_presentation_context(as, pc, ts, result = "00")
913
+ # PRESENTATION CONTEXT:
914
+ # Presentation context item type (1 byte)
915
+ @outgoing.encode_last(pc, "HEX") # "20" (request) & "21" (response)
916
+ # Reserved (1 byte)
917
+ @outgoing.encode_last("00", "HEX")
918
+ # Presentation context item length (2 bytes)
919
+ if ts.is_a?(Array)
920
+ ts_length = 4*ts.length + ts.join.length
921
+ else # (String)
922
+ ts_length = 4 + ts.length
923
+ end
924
+ if as
925
+ items_length = 4 + (4 + as.length) + ts_length
926
+ else
927
+ items_length = 4 + ts_length
928
+ end
929
+ @outgoing.encode_last(items_length, "US")
930
+ # Presentation context ID (1 byte)
931
+ @outgoing.encode_last("01", "HEX")
932
+ # Reserved (1 byte)
933
+ @outgoing.encode_last("00", "HEX")
934
+ # (1 byte) Reserved (for association request) & Result/reason (for association accept response)
935
+ @outgoing.encode_last(result, "HEX")
936
+ # Reserved (1 byte)
937
+ @outgoing.encode_last("00", "HEX")
938
+ ## ABSTRACT SYNTAX SUB-ITEM: (only for request, not response)
939
+ if as
940
+ # Abstract syntax item type (1 byte)
941
+ @outgoing.encode_last("30", "HEX")
942
+ # Reserved (1 byte)
943
+ @outgoing.encode_last("00", "HEX")
944
+ # Abstract syntax item length (2 bytes)
945
+ @outgoing.encode_last(as.length, "US")
946
+ # Abstract syntax (variable length)
947
+ @outgoing.encode_last(as, "STR")
948
+ end
949
+ ## TRANSFER SYNTAX SUB-ITEM:
950
+ ts = [ts] if ts.is_a?(String)
951
+ ts.each do |t|
952
+ # Transfer syntax item type (1 byte)
953
+ @outgoing.encode_last("40", "HEX")
954
+ # Reserved (1 byte)
955
+ @outgoing.encode_last("00", "HEX")
956
+ # Transfer syntax item length (2 bytes)
957
+ @outgoing.encode_last(t.length, "US")
958
+ # Transfer syntax (variable length)
959
+ @outgoing.encode_last(t, "STR")
960
+ end
961
+ # Update key values for this instance:
962
+ set_transfer_syntax(ts.first)
963
+ end
964
+
965
+
966
+ # Adds the binary string that makes up the user information (part of the association request).
967
+ def append_user_information(ui)
968
+ # USER INFORMATION:
969
+ # User information item type (1 byte)
970
+ @outgoing.encode_last("50", "HEX")
971
+ # Reserved (1 byte)
972
+ @outgoing.encode_last("00", "HEX")
973
+ # Encode the user information item values so we can determine the remaining length of this section:
974
+ values = Array.new
975
+ ui.each_index do |i|
976
+ values << @outgoing.encode(ui[i][2], ui[i][1])
977
+ end
978
+ # User information item length (2 bytes)
979
+ items_length = 4*ui.length + values.join.length
980
+ @outgoing.encode_last(items_length, "US")
981
+ # SUB-ITEMS:
982
+ ui.each_index do |i|
983
+ # UI item type (1 byte)
984
+ @outgoing.encode_last(ui[i][0], "HEX")
985
+ # Reserved (1 byte)
986
+ @outgoing.encode_last("00", "HEX")
987
+ # UI item length (2 bytes)
988
+ @outgoing.encode_last(values[i].length, "US")
989
+ # UI value (4 bytes)
990
+ @outgoing.add_last(values[i])
991
+ end
992
+ end
993
+
994
+
995
+ # Handles an incoming transmission.
996
+ # Optional: Specify a minimum length of the incoming transmission. (If a message is received
997
+ # which is shorter than this limit, the method will keep listening for more incoming packets to append)
998
+ def receive_transmission(session, min_length=0)
999
+ data = receive_transmission_data(session)
1000
+ # Check the nature of the received data variable:
1001
+ if data
1002
+ # Sometimes the incoming transmission may be broken up into smaller pieces:
1003
+ # Unless a short answer is expected, we will continue to listen if the first answer was too short:
1004
+ unless min_length == 0
1005
+ if data.length <= min_length
1006
+ addition = receive_transmission_data(session)
1007
+ data = data + addition if addition
1008
+ end
1009
+ end
1010
+ else
1011
+ # It seems there was no incoming message and the operation timed out.
1012
+ # Convert the variable to an empty string.
1013
+ data = ""
1014
+ end
1015
+ return data
1016
+ end
1017
+
1018
+
1019
+ # Receives the incoming transmission data.
1020
+ def receive_transmission_data(session)
1021
+ data = false
1022
+ t1 = Time.now.to_f
1023
+ @receive = true
1024
+ thr = Thread.new{ data = session.recv(@max_receive_size); @receive = false }
1025
+ while @receive
1026
+ if (Time.now.to_f - t1) > @timeout
1027
+ Thread.kill(thr)
1028
+ add_error("No answer was received within the specified timeout period. Aborting.")
1029
+ @listen = false
1030
+ @receive = false
1031
+ end
1032
+ end
1033
+ return data
1034
+ end
1035
+
1036
+
1037
+ # Some default values.
1038
+ def set_default_values
1039
+ # Default endianness for network transmissions is Big Endian:
1040
+ @net_endian = true
1041
+ # Default endianness of data is little endian:
1042
+ @data_endian = false
1043
+ # Explicitness (this may turn out not to be necessary...)
1044
+ @explicit = true
1045
+ # Transfer syntax (Implicit, little endian):
1046
+ set_transfer_syntax("1.2.840.10008.1.2")
1047
+ # Version information:
1048
+ @implementation_uid = "1.2.826.0.1.3680043.8.641"
1049
+ @implementation_name = "RUBY_DICOM_0.6"
1050
+ end
1051
+
1052
+
1053
+ # Set instance variables related to the transfer syntax.
1054
+ def set_transfer_syntax(value)
1055
+ # Query the library with our particular transfer syntax string:
1056
+ result = @lib.process_transfer_syntax(value)
1057
+ # Result is a 3-element array: [Validity of ts, explicitness, endianness]
1058
+ unless result[0]
1059
+ add_error("Warning: Invalid/unknown transfer syntax encountered! Will try to continue, but errors may occur.")
1060
+ end
1061
+ # Update encoding variables:
1062
+ @explicit = result[1]
1063
+ @data_endian = result[2]
1064
+ @transfer_syntax = value
1065
+ end
1066
+
1067
+
1068
+ # Set user information [item type code, vr/type, value]
1069
+ def set_user_information_array
1070
+ @user_information = [
1071
+ ["51", "UL", @max_package_size], # Max PDU Length
1072
+ ["52", "STR", @implementation_uid],
1073
+ ["55", "STR", @implementation_name]
1074
+ ]
1075
+ end
1076
+
1077
+
1078
+ end
1079
+ end