Class: DICOM::DServer

Inherits:
Object
  • Object
show all
Includes:
Logging
Defined in:
lib/dicom/d_server.rb

Overview

This class contains code for setting up a Service Class Provider (SCP), which will act as a simple storage node (a DICOM server that receives images).

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Logging

included, #logger

Constructor Details

#initialize(port = 104, options = {}) ⇒ DServer

Note:

To customize logging behaviour, refer to the Logging module documentation.

Creates a DServer instance.

Examples:

Create a server using default settings

s = DICOM::DServer.new

Create a server with a specific host name and a custom buildt file handler

require_relative 'my_file_handler'
server = DICOM::DServer.new(104, :host_ae => "RUBY_SERVER", :file_handler => DICOM::MyFileHandler)

Parameters:

  • port (Integer) (defaults to: 104)

    the network port to be used

  • options (Hash) (defaults to: {})

    the options to use for the DICOM server

Options Hash (options):

  • :file_handler (String)

    a customized FileHandler class to use instead of the default FileHandler

  • :host (String)

    the hostname that the TCPServer binds to (defaults to ‘0.0.0.0’)

  • :host_ae (String)

    the name of the server (application entity)

  • :max_package_size (String)

    the maximum allowed size of network packages (in bytes)

  • :timeout (String)

    the number of seconds the server will wait on an answer from a client before aborting the communication



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/dicom/d_server.rb', line 66

def initialize(port=104, options={})
  require 'socket'
  # Required parameters:
  @port = port
  # Optional parameters (and default values):
  @file_handler = options[:file_handler] || FileHandler
  @host = options[:host] || '0.0.0.0'
  @host_ae =  options[:host_ae]  || "RUBY_DICOM"
  @max_package_size = options[:max_package_size] || 32768 # 16384
  @timeout = options[:timeout] || 10 # seconds
  @min_length = 12 # minimum number of bytes to expect in an incoming transmission
  # Variables used for monitoring state of transmission:
  @connection = nil # TCP connection status
  @association = nil # DICOM Association status
  @request_approved = nil # Status of our DICOM request
  @release = nil # Status of received, valid release response
  set_default_accepted_syntaxes
end

Instance Attribute Details

#accepted_abstract_syntaxesObject (readonly)

A hash containing the abstract syntaxes that will be accepted.



44
45
46
# File 'lib/dicom/d_server.rb', line 44

def accepted_abstract_syntaxes
  @accepted_abstract_syntaxes
end

#accepted_transfer_syntaxesObject (readonly)

A hash containing the transfer syntaxes that will be accepted.



46
47
48
# File 'lib/dicom/d_server.rb', line 46

def accepted_transfer_syntaxes
  @accepted_transfer_syntaxes
end

#file_handlerObject

A customized FileHandler class to use instead of the default FileHandler included with ruby-dicom.



31
32
33
# File 'lib/dicom/d_server.rb', line 31

def file_handler
  @file_handler
end

#hostObject

The hostname that the TCPServer binds to.



33
34
35
# File 'lib/dicom/d_server.rb', line 33

def host
  @host
end

#host_aeObject

The name of the server (application entity).



35
36
37
# File 'lib/dicom/d_server.rb', line 35

def host_ae
  @host_ae
end

#max_package_sizeObject

The maximum allowed size of network packages (in bytes).



37
38
39
# File 'lib/dicom/d_server.rb', line 37

def max_package_size
  @max_package_size
end

#portObject

The network port to be used.



39
40
41
# File 'lib/dicom/d_server.rb', line 39

def port
  @port
end

#timeoutObject

The maximum period the server will wait on an answer from a client before aborting the communication.



41
42
43
# File 'lib/dicom/d_server.rb', line 41

def timeout
  @timeout
end

Class Method Details

.run(port = 104, path = './received/', &block) ⇒ Object

Runs the server and takes a block for initializing.

Examples:

Run a server instance with a custom file handler

require 'dicom'
require 'my_file_handler'
include DICOM
DServer.run(104, 'c:/temp/') do |s|
  s.timeout = 100
  s.file_handler = MyFileHandler
end

Parameters:

  • port (Integer) (defaults to: 104)

    the network port to be used (defaults to 104)

  • path (String) (defaults to: './received/')

    the directory where incoming DICOM files will be stored (defaults to ‘./received/’)

  • block (&block)

    a block of code that will be run on the DServer instance, between creation and the launch of the SCP itself



24
25
26
27
28
# File 'lib/dicom/d_server.rb', line 24

def self.run(port=104, path='./received/', &block)
  server = DServer.new(port)
  server.instance_eval(&block)
  server.start_scp(path)
end

Instance Method Details

#add_abstract_syntax(uid) ⇒ Object

Adds an abstract syntax to the list of abstract syntaxes that the server will accept.

Parameters:

  • uid (String)

    an abstract syntax UID



89
90
91
92
93
# File 'lib/dicom/d_server.rb', line 89

def add_abstract_syntax(uid)
  lib_uid = LIBRARY.uid(uid)
  raise "Invalid/unknown UID: #{uid}" unless lib_uid
  @accepted_abstract_syntaxes[uid] = lib_uid.name
end

#add_transfer_syntax(uid) ⇒ Object

Adds a transfer syntax to the list of transfer syntaxes that the server will accept.

Parameters:

  • uid (String)

    a transfer syntax UID



99
100
101
102
103
# File 'lib/dicom/d_server.rb', line 99

def add_transfer_syntax(uid)
  lib_uid = LIBRARY.uid(uid)
  raise "Invalid/unknown UID: #{uid}" unless lib_uid
  @accepted_transfer_syntaxes[uid] = lib_uid.name
end

#clear_abstract_syntaxesObject

Completely clears the list of abstract syntaxes that the server will accept.

Following such a clearance, the user must ensure to add the specific abstract syntaxes that are to be accepted by the server.



158
159
160
# File 'lib/dicom/d_server.rb', line 158

def clear_abstract_syntaxes
  @accepted_abstract_syntaxes = Hash.new
end

#clear_transfer_syntaxesObject

Completely clears the list of transfer syntaxes that the server will accept.

Following such a clearance, the user must ensure to add the specific transfer syntaxes that are to be accepted by the server.



167
168
169
# File 'lib/dicom/d_server.rb', line 167

def clear_transfer_syntaxes
  @accepted_transfer_syntaxes = Hash.new
end

#delete_abstract_syntax(uid) ⇒ Object

Deletes a specific abstract syntax from the list of abstract syntaxes that the server will accept.

Parameters:

  • uid (String)

    an abstract syntax UID



132
133
134
135
136
137
138
# File 'lib/dicom/d_server.rb', line 132

def delete_abstract_syntax(uid)
  if uid.is_a?(String)
    @accepted_abstract_syntaxes.delete(uid)
  else
    raise "Invalid type of UID. Expected String, got #{uid.class}!"
  end
end

#delete_transfer_syntax(uid) ⇒ Object

Deletes a specific transfer syntax from the list of transfer syntaxes that the server will accept.

Parameters:

  • uid (String)

    a transfer syntax UID



145
146
147
148
149
150
151
# File 'lib/dicom/d_server.rb', line 145

def delete_transfer_syntax(uid)
  if uid.is_a?(String)
    @accepted_transfer_syntaxes.delete(uid)
  else
    raise "Invalid type of UID. Expected String, got #{uid.class}!"
  end
end

Prints the list of accepted abstract syntaxes to the screen.



107
108
109
110
111
112
113
114
# File 'lib/dicom/d_server.rb', line 107

def print_abstract_syntaxes
  # Determine length of longest key to ensure pretty print:
  max_uid = @accepted_abstract_syntaxes.keys.collect{|k| k.length}.max
  puts "Abstract syntaxes which are accepted by this SCP:"
  @accepted_abstract_syntaxes.sort.each do |pair|
    puts "#{pair[0]}#{' '*(max_uid-pair[0].length)} #{pair[1]}"
  end
end

Prints the list of accepted transfer syntaxes to the screen.



118
119
120
121
122
123
124
125
# File 'lib/dicom/d_server.rb', line 118

def print_transfer_syntaxes
  # Determine length of longest key to ensure pretty print:
  max_uid = @accepted_transfer_syntaxes.keys.collect{|k| k.length}.max
  puts "Transfer syntaxes which are accepted by this SCP:"
  @accepted_transfer_syntaxes.sort.each do |pair|
    puts "#{pair[0]}#{' '*(max_uid-pair[0].length)} #{pair[1]}"
  end
end

#start_scp(path = './received/') ⇒ Object

Starts the Service Class Provider (SCP).

This service acts as a simple storage node, which receives DICOM files and stores them in the specified folder.

Customized storage actions can be set my modifying or replacing the FileHandler class.

Parameters:

  • path (String) (defaults to: './received/')

    the directory where incoming files are to be saved



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/dicom/d_server.rb', line 180

def start_scp(path='./received/')
  if @accepted_abstract_syntaxes.size > 0 and @accepted_transfer_syntaxes.size > 0
    logger.info("Started DICOM SCP server on port #{@port}.")
    logger.info("Waiting for incoming transmissions...\n\n")
    # Initiate server:
    @scp = TCPServer.new(@host, @port)
    # Use a loop to listen for incoming messages:
    loop do
      Thread.start(@scp.accept) do |session|
        # Initialize the network package handler for this session:
        link = Link.new(:host_ae => @host_ae, :max_package_size => @max_package_size, :timeout => @timeout, :file_handler => @file_handler)
        link.set_session(session)
        # Note who has contacted us:
        logger.info("Connection established with:  #{session.peeraddr[2]}  (IP: #{session.peeraddr[3]})")
        # Receive an incoming message:
        segments = link.receive_multiple_transmissions
        info = segments.first
        # Interpret the received message:
        if info[:valid]
          association_error = check_association_request(info)
          unless association_error
            info, approved, rejected = process_syntax_requests(info)
            link.handle_association_accept(info)
            context = (LIBRARY.uid(info[:pc].first[:abstract_syntax]) ? LIBRARY.uid(info[:pc].first[:abstract_syntax]).name : 'Unknown UID!')
            if approved > 0
              if approved == 1
                logger.info("Accepted the association request with context: #{context}")
              else
                if rejected == 0
                  logger.info("Accepted all #{approved} proposed contexts in the association request.")
                else
                  logger.warn("Accepted only #{approved} of #{approved+rejected} of the proposed contexts in the association request.")
                end
              end
              # Process the incoming data. This method will also take care of releasing the association:
              success, messages = link.handle_incoming_data(path)
              # Pass along any messages that has been recorded:
              messages.each { |m| logger.public_send(m.first, m.last) } if messages.first
            else
              # No abstract syntaxes in the incoming request were accepted:
              if rejected == 1
                logger.warn("Rejected the association request with proposed context: #{context}")
              else
                logger.warn("Rejected all #{rejected} proposed contexts in the association request.")
              end
              # Since the requested abstract syntax was not accepted, the association must be released.
              link.await_release
            end
          else
            # The incoming association was not formally correct.
            link.handle_rejection
          end
        else
          # The incoming message was not recognised as a valid DICOM message. Abort:
          link.handle_abort
        end
        # Terminate the connection:
        link.stop_session
        logger.info("Connection closed.\n\n")
      end
    end
  else
    raise "Unable to start SCP server as no accepted abstract syntaxes have been set!" if @accepted_abstract_syntaxes.length == 0
    raise "Unable to start SCP server as no accepted transfer syntaxes have been set!" if @accepted_transfer_syntaxes.length == 0
  end
end