lis 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --protected
data/README.markdown ADDED
@@ -0,0 +1,9 @@
1
+ # LIS
2
+
3
+ A simple interface to medical laboratory instruments. It implments a rough subset of ASTM E1394-97 (or, possibly CLSI LIS02-A2)
4
+
5
+ It listens for test requests and results and forwards them via HTTP. It is intended to interface with the [worklist_manager](http://github.com/levinalex/worklist_manager) web application.
6
+
7
+ ## Copyright
8
+
9
+ Copyright (c) 2010 Levin Alexander. See LICENSE for details.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.0
1
+ 0.1.0
data/bin/lis2http ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby1.9
2
+
3
+ require 'lib/lis.rb'
4
+ require 'lis/commands/application.rb'
5
+
6
+ LIS::Application.new(ARGV).run!
data/features/lis.feature CHANGED
@@ -1,9 +1,50 @@
1
- Feature: something something
2
- In order to something something
3
- A user something something
4
- something something something
1
+ Feature:
2
+ In order to value
3
+ As a role
4
+ I want feature
5
5
 
6
- Scenario: something something
7
- Given inspiration
8
- When I create a sweet new gem
9
- Then everyone should see how awesome I am
6
+ Scenario: sending results from Immulite to LIS
7
+ Given LIS Interface listening for messages
8
+ When receiving
9
+ """
10
+ H|\^||PASSWORD|DPC||||SYSTEM||P|1|19940407085426
11
+ P|1|119813;TGH|||Last 1^First 1|||F|||||
12
+ O|1|130000445||^^^TT4|||19950118085700
13
+ R|1|^^^TT4|10.3|ug/dL|4.5\.4^12.5\24|N|N|F||test|19950119084508|19950119092826|SenderID
14
+ O|2|130000445||^^^TU|||19950118085700
15
+ R|1|^^^TU|26.6|Percnt|23\10^35\70|N|N|F||test|19950119084508|19950119092756|SenderID
16
+ P|2|325031;AH|||Last 2^First 2|||F|||||
17
+ O|1|130000617||^^^FER|||19950118103000
18
+ R|1|^^^FER|173.|ng/mL|.5\.5^1500\1500|N|N|F||test|19950119084641|19950119092858|SenderID
19
+ P|3|326829;AH|||Last 3^First 3|||F|||||
20
+ O|1|130000722||^^^FER|||19950118102000
21
+ R|1|^^^FER|490.|ng/mL|.5\.5^1500\1500|N|N|F||test|19950119084741|19950119092928|SenderID
22
+ P|4|124462;TGH|||Last 4^First 4|||F|||||
23
+ O|1|130000724||^^^E2|||19950118122000
24
+ R|1|^^^E2|25.3|pg/mL|12\12^2000\2000|N|N|F||test|19950119084815|19950119100049|SenderID
25
+ O|2|130000724||^^^FSH|||19950118122000
26
+ R|1|^^^FSH|60.6|mIU/mL|.1\.1^170\170|N|N|F||test|19950119084815|19950119093030|SenderID
27
+ O|3|130000724||^^^LH|||19950118122000
28
+ R|1|^^^LH|24.4|mIU/mL|.7\.7^400\400|N|N|F||test|19950119084815|19950119093101|SenderID
29
+ P|5|556395;AH|||Last 5^First 5|||M|||||
30
+ O|1|130000741||^^^FER|||19950118115500
31
+ R|1|^^^FER|238.|ng/mL|.5\.5^1500\1500|N|N|F||test|19950119084949|19950119093132|SenderID
32
+ P|6|556357;MB|||Last 6^First 6|||M|||||
33
+ O|1|130000790||^^^IGE|||19950118120000
34
+ R|1|^^^IGE|517.|IU/mL|.01\.01^600\600|N|N|F||test|19950119085018|19950119093202|SenderID
35
+ P|7|141053;TGH|||Last 7^First 7|||F|||||
36
+ O|1|130000805||^^^FER|||19950118120000
37
+ R|1|^^^FER|21.0|ng/mL|.5\.5^1500\1500|N|N|F||test|19950119085049|19950119093233|SenderID
38
+ P|8|320439;TGH|||Last 8^First 8|||F|||||
39
+ O|1|130000890||^^^FER|||19950118130000
40
+ R|1|^^^FER|12.9|ng/mL|.5\.5^1500\1500|N|N|F||test|19950119085254|19950119093609|SenderID
41
+ P|9||||Last 9^First 9||||||||
42
+ O|1|130000911||^^^E2
43
+ R|1|^^^E2|71.3|pg/mL|12\12^2000\2000|N|N|F||test|19950119085423|19950119100800|SenderID
44
+ P|10|358069;TGH|||Last 10^First 10|||F|||||
45
+ O|1|130000929||^^^FER|||19950118123000
46
+ R|1|^^^FER|219.|ng/mL|.5\.5^1500\1500|N|N|F||test|19950119085628|19950119093843|SenderID
47
+ L|1
48
+ """
49
+ Then should have posted results:
50
+ | device_name | barcode |
@@ -1,6 +1,7 @@
1
1
 
2
2
  module LIS::Transfer
3
3
  class ApplicationProtocol < Base
4
+ attr_reader :device_name
4
5
 
5
6
  def on_result(&block)
6
7
  @on_result_callback = block
@@ -10,13 +11,13 @@ module LIS::Transfer
10
11
  @on_request_callback = block
11
12
  end
12
13
 
13
-
14
14
  def received_header(message)
15
- @patient_information ||= {} # delete the list of patients
15
+ @patient_information_requests ||= {} # delete the list of patients
16
+ @device_name = message.sender_name
16
17
  end
17
18
 
18
19
  def result_for(patient, order, result)
19
- @on_result_callback.call(patient, order, result)
20
+ @on_result_callback.call(@device_name, patient, order, result)
20
21
  end
21
22
 
22
23
  def received_patient_information(message)
@@ -33,14 +34,30 @@ module LIS::Transfer
33
34
  end
34
35
 
35
36
  def received_request_for_information(message)
36
- @patient_information ||= {}
37
- requests = @on_request_callback.call(p.starting_range)
38
- @patient_information[p.sequence_number] = requests if requests
37
+ @patient_information_requests ||= {}
38
+ requests = @on_request_callback.call(@device_name, message.starting_range_id)
39
+ @patient_information_requests[message.sequence_number] = requests if requests
40
+ end
41
+
42
+ def send_pending_requests
43
+ sending_session(@patient_information_requests) do |patient_information|
44
+ patient_information.each do |sequence_nr, data|
45
+ write :message, LIS::Message::Patient.new(sequence_nr,
46
+ data["patient"]["number"],
47
+ data["patient"]["last_name"],
48
+ data["patient"]["first_name"]).to_message
49
+ data["types"].each do |request|
50
+ write :message, LIS::Message::Order.new(sequence_nr, data["id"], request).to_message
51
+ end
52
+ end
53
+ end
54
+ @patient_information_requests = {}
39
55
  end
40
56
 
41
57
  def initialize(*args)
42
58
  super
43
59
 
60
+ @patient_information_requests = {}
44
61
  @last_patient = nil
45
62
  @last_order = nil
46
63
  @handlers = {
@@ -53,17 +70,35 @@ module LIS::Transfer
53
70
  end
54
71
 
55
72
  def receive(type, message = nil)
73
+ warn "[R] #{message}" if type == :message and $VERBOSE
56
74
  case type
57
75
  when :begin
58
76
  @last_patient = nil
59
77
  @last_order = nil
60
78
  when :idle
79
+ send_pending_requests
61
80
  when :message
62
81
  @message = LIS::Message::Base.from_string(message)
63
82
  handler = @handlers[@message.class]
64
83
  send(handler, @message) if handler
65
84
  end
66
85
  end
67
- end
68
86
 
69
- end
87
+ def write(type, message=nil)
88
+ warn "[S] #{message}" if type == :message and $VERBOSE
89
+ super
90
+ end
91
+
92
+ # @yield data
93
+ def sending_session(data)
94
+ # don't send anything if there are no pending requests
95
+ return if data.nil? or data.empty?
96
+
97
+ write :begin
98
+ write :message, LIS::Message::Header.new("LIS", @device_name).to_message
99
+ yield data
100
+ write :message, LIS::Message::Terminator.new.to_message
101
+ write :idle
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,55 @@
1
+ require 'optparse'
2
+
3
+ module LIS
4
+ class Options < Hash
5
+ attr_reader :opts
6
+
7
+ def initialize(args)
8
+ super()
9
+
10
+ default_options = { :port => "/dev/ttyUSB0", :uri => "http://localhost/lis/" }
11
+ self.merge!(default_options)
12
+
13
+ @opts = OptionParser.new do |o|
14
+ appname = File.basename($0)
15
+ o.banner = "Usage: #{appname} [options]"
16
+
17
+ o.on('-l, --listen PORT', 'which port to listen on (default: "/dev/ttyUSB0")') do |port|
18
+ self[:port] = port
19
+ end
20
+ o.on('-e, --endpoint URI', 'HTTP endpoint (default: "http://localhost/lis/")') do |endpoint|
21
+ self[:uri] = endpoint
22
+ end
23
+ o.on("-v", "--[no-]verbose", "Run verbosely") do |v|
24
+ $VERBOSE = v
25
+ self[:verbose] = v
26
+ end
27
+ end
28
+
29
+ begin
30
+ @opts.parse!(args)
31
+ self[:project_name] = args.shift
32
+ rescue OptionParser::ParseError => e
33
+ self[:invalid_argument] = e.message
34
+ end
35
+ end
36
+ end
37
+
38
+ class Application
39
+ def initialize(opts)
40
+ @options = Options.new(opts)
41
+
42
+ if @options[:invalid_argument]
43
+ warn @options.opts
44
+ exit 1
45
+ end
46
+ end
47
+
48
+ def run!
49
+ warn "listening on: #{@options[:port]}"
50
+ port = File.open(@options[:port], "w+")
51
+ LIS::InterfaceServer.new(port, @options[:uri]).run!
52
+ end
53
+ end
54
+
55
+ end
@@ -0,0 +1,23 @@
1
+ module LIS
2
+ class InterfaceServer
3
+ def initialize(port, http_endpoint)
4
+ @server = LIS::Transfer::IOListener.new(port)
5
+ @packets = LIS::Transfer::PacketizedProtocol.new(@server)
6
+
7
+ app_protocol = LIS::Transfer::ApplicationProtocol.new(@packets)
8
+ interface = WorklistManagerInterface.new(http_endpoint)
9
+
10
+ app_protocol.on_request do |device_name, barcode|
11
+ interface.load_requests(device_name, barcode)
12
+ end
13
+ app_protocol.on_result do |*args|
14
+ interface.send_result(*args)
15
+ end
16
+ end
17
+
18
+ def run!
19
+ warn "listener started" if $VERBOSE
20
+ @server.run!
21
+ end
22
+ end
23
+ end
@@ -2,41 +2,72 @@ require 'strscan'
2
2
 
3
3
  module LIS::Transfer
4
4
 
5
- # a chainable IO-Listener that provides to methods:
5
+ # a chainable IO-Listener that provides two methods:
6
6
  #
7
- # +on_data+ :: a callback that is called whenever a message is received
8
- # +write+ :: can be called so send messages to the underlying IO
7
+ # {#on_data} ::
8
+ # a callback that is called whenever a message is received
9
+ # {#write} ::
10
+ # can be called so send messages to the underlying IO
9
11
  #
10
12
  # when overriding this class, you need to implement two methods:
11
13
  #
12
- # +receive+ :: is called from an underlying IO whenever a message is received
13
- # the message can be handled, if messages should be propagated, you
14
- # need to call `forward`
14
+ # {#receive} ::
15
+ # is called from an underlying IO whenever a message is received
16
+ # the message can be handled. Call {#forward} to propagate
15
17
  #
16
- # +write+ :: when data needs to be encoded, formatted before it can be send
17
- # send it with `super`
18
+ # {#write} ::
19
+ # transform/encode a message before sending it. Call +super+ to propagate
18
20
  #
21
+ # See {LineBasedProtocol} for a toy implementation which strips
22
+ # newlines and only forwards complete lines.
19
23
  #
20
24
  class Base
25
+
26
+ # @param [Base, #on_data] read data source
27
+ # @param [Base, IO, #write] write object where messages are written to
28
+ #
21
29
  def initialize(read, write = read)
22
30
  @reader, @writer = read, write
23
31
  @on_data = nil
24
32
  @reader.on_data { |*data| receive(*data) } if @reader.respond_to?(:on_data)
25
33
  end
26
34
 
35
+ # register a block, to be run whenever the protocol implementation
36
+ # receives data (by calling {#forward})
37
+ #
38
+ # this is used to chain protocol layers together
39
+ #
40
+ # @return [self]
41
+ # @yield [*args] called whenever the protocol implementaion calls {#forward}
42
+ # @yieldreturn [nil]
43
+ #
27
44
  def on_data(&block)
28
45
  @on_data = block
46
+ self
29
47
  end
30
48
 
31
- def write(message)
32
- @writer << message if @writer
33
- end
49
+ # @see #write
34
50
  def <<(*args)
35
51
  write(*args)
36
52
  end
37
53
 
38
- private
54
+ # write data to underlying interface. override if data needs to be preprocessed
55
+ #
56
+ def write(*args)
57
+ @writer.<<(*args) if @writer
58
+ end
59
+
39
60
 
61
+ protected
62
+
63
+ # override this method to handle data received from an underlying interface, for
64
+ # example splitting it into messages or only passing on complete lines
65
+ # (see {LineBasedProtocol#receive} for an example)
66
+ #
67
+ # call {#forward} to pass it on to higher levels
68
+ #
69
+ # @return [nil]
70
+ #
40
71
  def receive(data)
41
72
  forward(data)
42
73
  end
@@ -48,6 +79,11 @@ module LIS::Transfer
48
79
 
49
80
 
50
81
  class LineBasedProtocol < Base
82
+
83
+ # strip newlines from received data and pass on complete lines
84
+ #
85
+ # @param [String] data
86
+ #
51
87
  def receive(data)
52
88
  @memo ||= ""
53
89
  scanner = StringScanner.new(@memo + data)
@@ -58,6 +94,9 @@ module LIS::Transfer
58
94
  nil
59
95
  end
60
96
 
97
+ # add a newline to data and pass it on
98
+ # @param [String] data
99
+ #
61
100
  def write(data)
62
101
  super(data + "\n")
63
102
  end
@@ -73,4 +112,3 @@ module LIS::Transfer
73
112
  end
74
113
  end
75
114
 
76
-
@@ -0,0 +1,23 @@
1
+ module LIS::Message
2
+ class Header < Base
3
+ type_id "H"
4
+ has_field 2, :delimiter_definition, :default => "^&"
5
+ has_field 4, :access_password
6
+ has_field 5, :sender_name
7
+ has_field 6, :sender_address
8
+ has_field 7 # reserved
9
+ has_field 8 # sender_telephone_number
10
+ has_field 9, :sender_characteristics, :default => "8N1"
11
+ has_field 10, :receiver_name
12
+ has_field 11 # comments/special_instructions
13
+ has_field 12, :processing_id, :default => "P"
14
+ has_field 13, :version, :default => "1"
15
+ has_field 14, :timestamp, :default => lambda { Time.now.strftime("%Y%m%d%H%M%S") }
16
+
17
+ def initialize(sender_name = "SenderID", receiver_name = "ReceiverID", password = "")
18
+ self.sender_name = sender_name
19
+ self.receiver_name = receiver_name
20
+ self.access_password = password
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,35 @@
1
+ module LIS::Message
2
+ class Order < Base
3
+ type_id "O"
4
+
5
+ has_field 3, :specimen_id
6
+ has_field 5, :universal_test_id_internal
7
+ has_field 6, :priority, :default => "R" # routine
8
+ has_field 7, :requested_at
9
+ has_field 8, :collected_at
10
+ has_field 12 # :action_code
11
+
12
+ has_field 14 # :relevant_clinical_information
13
+
14
+ has_field 25 # :instrument_section_id
15
+
16
+ has_field 29 # nosocomial_infection_flag
17
+ has_field 30 # specimen_service
18
+ has_field 31 # specimen_institution
19
+
20
+ field_count 0
21
+
22
+ def initialize(sequence_number, specimen_id, universal_test_id)
23
+ self.sequence_number = sequence_number
24
+ self.specimen_id = specimen_id
25
+ self.universal_test_id = universal_test_id
26
+ end
27
+
28
+ def universal_test_id
29
+ self.universal_test_id_internal.gsub(/\^/,"")
30
+ end
31
+ def universal_test_id=(val)
32
+ self.universal_test_id_internal = "^^^#{val}"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,22 @@
1
+ module LIS::Message
2
+ class Patient < Base
3
+ type_id "P"
4
+
5
+ has_field 3, :practice_assigned_patient_id
6
+ has_field 4, :laboratory_assigned_patient_id
7
+ has_field 5, :patient_id
8
+ has_field 6, :name
9
+ has_field 7 # :mothers_maiden_name
10
+ has_field 8, :birthdate
11
+ has_field 9, :sex
12
+ has_field 10, :race
13
+ has_field 14, :attending_physician
14
+
15
+ def initialize(sequence_number, patient_id, last_name = "", first_name = "")
16
+ self.sequence_number = sequence_number
17
+ self.practice_assigned_patient_id = patient_id
18
+ self.name = [last_name, first_name].join("^")
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+ module LIS::Message
2
+ class Query < Base
3
+ type_id "Q"
4
+ has_field 3, :starting_range_id_internal
5
+
6
+ def starting_range_id
7
+ starting_range_id_internal.gsub(/\^/,"")
8
+ end
9
+ def starting_range_id=(val)
10
+ starting_range_id_internal = "^#{val}"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ module LIS::Message
2
+ class Result < Base
3
+ type_id "R"
4
+ has_field 3, :universal_test_id_internal
5
+ has_field 4, :result_value
6
+ has_field 5, :unit
7
+ has_field 6, :reference_ranges
8
+ has_field 7, :abnormal_flags
9
+ has_field 9, :result_status
10
+ has_field 12, :test_started_at, :type => :timestamp
11
+ has_field 13, :test_completed_at, :type => :timestamp
12
+
13
+ def universal_test_id
14
+ universal_test_id_internal.gsub(/\^/,"")
15
+ end
16
+ def universal_test_id=(val)
17
+ universal_test_id_internal = "^^^#{val}"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ module LIS::Message
2
+ class Terminator < Base
3
+ TERMINATION_CODES = {
4
+ "N" => "Normal termination",
5
+ "T" => "Sender Aborted",
6
+ "R" => "Receiver Abort",
7
+ "E" => "Unknown system error",
8
+ "Q" => "Error in last request for information",
9
+ "I" => "No information available from last query",
10
+ "F" => "Last request for information Processed"
11
+ }
12
+
13
+ type_id "L"
14
+ has_field 3, :termination_code, :default => "N"
15
+ end
16
+ end
data/lib/lis/messages.rb CHANGED
@@ -1,125 +1,122 @@
1
+ require 'date'
1
2
 
2
3
  module LIS::Message
3
4
  module ClassMethods
4
- FIELD_TYPES = {
5
+ CONVERSION_WRITER = {
5
6
  :string => lambda { |s| s },
6
- :int => lambda { |s| s.to_i }
7
+ :int => lambda { |s| s.to_i },
8
+ :timestamp => lambda { |s| DateTime.strptime(s, "%Y%m%d%H%M%S") }
7
9
  }
8
10
 
11
+ def field_count(val = nil)
12
+ @field_count = val if val
13
+ @field_count
14
+ end
15
+
9
16
  def from_string(message)
10
- frame_number, type, data = parse(message)
17
+ type, data = parse(message)
11
18
  klass = (@@messages_by_type || {})[type]
12
19
  raise "unknown message type #{type.inspect}" unless klass
13
20
 
14
21
  obj = klass.allocate
15
- obj.frame_number = frame_number
16
- obj.type_id = type
17
-
18
- # populate named fields
19
- data.each_with_index do |val, index|
20
- klass.get_named_field_attributes(index + 2)
21
- if field = klass.get_named_field_attributes(index+2)
22
- obj.send(:"#{field[:name]}=", FIELD_TYPES[field[:type]].call(val))
23
- end
22
+
23
+ data = data.to_enum(:each_with_index).inject({}) do |h,(elem,idx)|
24
+ h[idx+2] = elem; h
24
25
  end
25
26
 
27
+ obj.instance_variable_set("@fields", data)
26
28
  obj
27
29
  end
28
30
 
29
- def initialize_from_message(*list_of_fields)
31
+ def default_fields
32
+ arr = Array.new(@field_count)
33
+ (0 .. @field_count).inject(arr) do |a,i|
34
+ default = (get_field_attributes(i) || {})[:default]
35
+ if default
36
+ default = default.call if default.respond_to?(:call)
37
+ a[i-1] = default
38
+ end
39
+ arr
40
+ end
30
41
  end
31
42
 
32
- protected
43
+ def has_field(idx, name = nil, opts={})
44
+ set_index_for(name, idx) if name
45
+ set_field_attributes(idx, { :name => name,
46
+ :type => opts[:type] || :string,
47
+ :default => opts[:default]})
48
+
49
+ @field_count ||= 0
50
+ @field_count = [@field_count, idx].max
51
+
52
+ return unless name
53
+
54
+ define_method :"#{name}=" do |val|
55
+ @fields ||= {}
56
+ @fields[idx] = val
57
+ end
58
+
59
+ define_method :"#{name}" do
60
+ field_attrs = self.class.get_field_attributes(idx)
61
+ val = (@fields || {})[idx]
62
+ converter = CONVERSION_WRITER[field_attrs[:type]] if field_attrs
63
+ val = converter.call(val) if converter
64
+ val
65
+ end
66
+ end
33
67
 
34
68
  def parse(string)
35
- frame_number, type, data = string.scan(/^(.)(.)\|(.*)$/)[0]
69
+ type, data = string.scan(/^(.)\|(.*)$/)[0]
36
70
  data = data.split(/\|/)
37
71
 
38
- return [frame_number.to_i, type, data]
72
+ return [type, data]
39
73
  end
40
74
 
41
- def type_id(char)
75
+ def type_id(char = nil)
76
+ return @@messages_by_type.find {|c,klass| klass == self }.first unless char
42
77
  @@messages_by_type ||= {}
43
78
  @@messages_by_type[char] = self
44
79
  end
45
80
 
46
- def named_field(idx, name, type = :string)
47
- set_named_field_attributes(idx, :name => name, :type => type)
48
- attr_accessor name
81
+ def set_index_for(field_name, idx)
82
+ @field_indices ||= {}
83
+ @field_indices[field_name] = idx
49
84
  end
50
85
 
51
- def get_named_field_attributes(key)
52
- @field_names ||= {}
53
- val = (@field_names || {})[key]
54
- val ||= superclass.get_named_field_attributes(key) if superclass.respond_to?(:get_named_field_attributes)
86
+ def get_index_for(field_name)
87
+ val = @field_index[field_name]
88
+ val ||= superclass.get_index_for(field_name) if superclass.respond_to?(:get_index_for)
55
89
  val
56
90
  end
57
91
 
58
- private
92
+ def get_field_attributes(index)
93
+ @field_names ||= {}
94
+ val = (@field_names || {})[index]
95
+ val ||= superclass.get_field_attributes(index) if superclass.respond_to?(:get_field_attributes)
96
+ val
97
+ end
59
98
 
60
- def set_named_field_attributes(key, *val)
99
+ def set_field_attributes(index, hash)
61
100
  @field_names ||= {}
62
- @field_names[key] = *val
101
+ @field_names[index] = hash
63
102
  end
64
103
  end
65
104
 
66
105
  class Base
67
106
  extend ClassMethods
68
107
  attr_accessor :frame_number
69
- attr_accessor :type_id
70
108
 
71
- named_field 2, :sequence_number, :int
72
- end
73
-
74
-
75
- class Header < Base
76
- type_id "H"
77
- named_field 2, :delimiter_definition
78
- named_field 4, :access_password
79
- named_field 5, :sender_name
80
- named_field 10, :receiver_id
81
- end
82
-
83
- class Order < Base
84
- type_id "O"
85
- named_field 3, :specimen_id
86
- named_field 5, :universal_test_id
87
- named_field 6, :priority
88
- named_field 7, :requested_at
89
- named_field 8, :collected_at
90
- end
91
-
92
- class Result < Base
93
- type_id "R"
94
- named_field 3, :universal_test_id
95
- named_field 4, :result_value
96
- named_field 5, :unit
97
- named_field 6, :reference_ranges
98
- named_field 7, :abnormal_flags
99
- end
109
+ has_field 2, :sequence_number, :type => :int, :default => 1
100
110
 
101
- class Patient < Base
102
- type_id "P"
103
- end
104
-
105
- class Query < Base
106
- type_id "Q"
107
- end
108
-
109
-
110
- class TerminatorRecord < Base
111
- TERMINATION_CODES = {
112
- "N" => "Normal termination",
113
- "T" => "Sender Aborted",
114
- "R" => "Receiver Abort",
115
- "E" => "Unknown system error",
116
- "Q" => "Error in last request for information",
117
- "I" => "No information available from last query",
118
- "F" => "Last request for information Processed"
119
- }
111
+ def type_id
112
+ self.class.type_id
113
+ end
120
114
 
121
- type_id "L"
122
- named_field 3, :termination_code
115
+ def to_message
116
+ @fields ||= {}
117
+ arr = Array.new(self.class.default_fields)
118
+ type_id + @fields.inject(arr) { |a,(k,v)| a[k-1] = v; a }.join("|")
119
+ end
123
120
  end
124
121
 
125
- end
122
+ end
@@ -17,10 +17,17 @@ module LIS::Transfer
17
17
  ENQ = "\005"
18
18
  EOT = "\004"
19
19
 
20
+ # format of a message
20
21
  RX = /(?:
21
22
  \005 | # ENQ - start a transaction
22
23
  \004 | # EOT - ends a transaction
23
- (?:\002 (.*?) \015 \003 (.+?) \015 \012)) # a message with a checksum
24
+ \005 | # ACK
25
+ \025 | # NAK
26
+ (?:\002 (.) (.*?) \015 \003 (.+?) \015 \012) # a message with a checksum
27
+ # | | `-- checksum
28
+ # | `------------------ message
29
+ # `---------------------- frame number
30
+ )
24
31
  /xm
25
32
 
26
33
  def initialize(*args)
@@ -31,42 +38,71 @@ module LIS::Transfer
31
38
 
32
39
  def receive(data)
33
40
  scanner = StringScanner.new(@memo + data)
34
- while match = scanner.scan(RX)
41
+ while scanner.scan_until(RX)
42
+ match = scanner.matched
35
43
  case match
36
44
  when ENQ then transmission_start
37
45
  when EOT then transmission_end
46
+ when ACK, NAK then nil
38
47
  else
39
48
  received_message(match)
40
- write ACK
49
+ write :ack
41
50
  end
42
51
  end
43
52
  @memo = scanner.rest
44
53
  nil
45
54
  end
46
55
 
56
+ def write(type, data = nil)
57
+ str = case type
58
+ when :ack then ACK
59
+ when :nak then NAK
60
+ when :begin then
61
+ @frame_number = 0
62
+ ENQ
63
+ when :idle then EOT
64
+ when :message then
65
+ @frame_number = (@frame_number + 1) % 8
66
+ self.class.wrap_message(data, @frame_number)
67
+ else
68
+ raise ArgumentError
69
+ end
70
+ super(str)
71
+ end
72
+
47
73
 
48
74
  private
49
75
 
50
76
  def self.message_from_string(string)
51
77
  match = string.match(RX)
52
- data = match[1]
53
- checksum = match[2]
54
78
 
55
- expected_checksum = data.to_enum(:each_byte).inject(16) { |a,b| (a+b) % 0x100 }
79
+ frame_number, data, checksum = match[1 .. 3]
80
+
81
+ expected_checksum = (frame_number + data).each_byte.inject(16) { |a,b| (a+b) % 0x100 }
56
82
  actual_checksum = checksum.to_i(16)
57
83
 
58
84
  raise "checksum mismatch" unless expected_checksum == actual_checksum
59
- return data
85
+ return [frame_number.to_i, data]
60
86
  end
61
87
 
88
+ def self.wrap_message(string, frame_number)
89
+ frame_number = (frame_number % 8).to_s
90
+ checksum = (frame_number + string).each_byte.inject(16) { |a,b| (a+b) % 0x100 }
91
+ checksum = checksum.to_s(16).upcase.rjust(2,"0")
92
+
93
+ "\002#{frame_number}#{string}\015\003#{checksum}\015\012"
94
+ end
95
+
96
+
62
97
  def received_message(message)
63
98
  return false unless @inside_transmission
64
- forward(:message, self.class.message_from_string(message))
99
+ frame_number, message = *self.class.message_from_string(message)
100
+ forward(:message, message)
65
101
  end
66
102
 
67
103
  def transmission_start
68
104
  return false if @inside_transmission
69
- write ACK
105
+ write :ack
70
106
  forward :begin
71
107
  @inside_transmission = true
72
108
  true
@@ -80,4 +116,4 @@ module LIS::Transfer
80
116
  end
81
117
  end
82
118
 
83
- end
119
+ end
@@ -1,11 +1,14 @@
1
+ require 'net/http'
2
+ require 'yaml'
3
+
1
4
  class WorklistManagerInterface
2
5
  def initialize(endpoint)
3
6
  @endpoint = endpoint
4
7
  end
5
8
 
6
- def load_requests(barcode)
9
+ def load_requests(device_name, barcode)
7
10
  begin
8
- uri = URI.join(@endpoint,"find_requests/#{barcode}")
11
+ uri = URI.join(@endpoint,"find_requests/#{[device_name, barcode].join('-')}")
9
12
  result = fetch_with_redirect(uri.to_s)
10
13
  data = YAML.load(result.body)
11
14
  data["id"] = barcode
@@ -14,27 +17,35 @@ class WorklistManagerInterface
14
17
  puts e.backtrace
15
18
  data = nil
16
19
  end
20
+
21
+ data
17
22
  end
18
23
 
19
- def send_result(patient, order, result)
24
+ def send_result(device_name, patient, order, result)
20
25
  barcode = order.specimen_id
21
26
  data = {
22
- "test_name" => order.test_id,
23
- "value" => result.value,
27
+ "test_name" => order.universal_test_id,
28
+ "value" => result.result_value,
24
29
  "unit" => result.unit,
25
30
  "status" => result.result_status,
26
31
  "flags" => result.abnormal_flags,
27
- "result_timestamp" => result.timestamp
32
+ "result_timestamp" => result.test_completed_at
28
33
  }
29
34
 
30
- Net::HTTP.post_form(URI.join(@endpoint, "result/#{URI.encode(barcode)}"), data.to_hash)
35
+ p data
36
+ begin
37
+ res = Net::HTTP.post_form(URI.join(@endpoint, "result/#{[device_name, barcode].join('-')}"), data.to_hash)
38
+ rescue Exception => e
39
+ puts "EXCEPTION"
40
+ p e
41
+ end
31
42
  end
32
43
 
33
44
 
34
45
  private
35
46
 
36
47
  def fetch_with_redirect(uri_str, limit = 10)
37
- raise ArgumentError, 'HTTP redirect too deep' if limit == 0
48
+ raise ArgumentError, 'too many HTTP redirects' if limit == 0
38
49
 
39
50
  response = Net::HTTP.get_response(URI.parse(uri_str))
40
51
  case response
@@ -44,4 +55,4 @@ class WorklistManagerInterface
44
55
  response.error!
45
56
  end
46
57
  end
47
- end
58
+ end
data/lib/lis.rb CHANGED
@@ -5,6 +5,10 @@ end
5
5
 
6
6
  require 'lis/io_listener.rb'
7
7
  require 'lis/messages.rb'
8
+
9
+ Dir[File.join(File.dirname(__FILE__), 'lis/messages/**/*.rb')].each { |f| require f }
10
+
8
11
  require 'lis/packetized_protocol.rb'
9
12
  require 'lis/application_protocol.rb'
10
13
  require 'lis/worklist_manager_interface.rb'
14
+ require 'lis/interface_server.rb'
data/lis.gemspec CHANGED
@@ -5,40 +5,54 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{lis}
8
- s.version = "0.0.0"
8
+ s.version = "0.1.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Levin Alexander"]
12
- s.date = %q{2010-02-07}
13
- s.default_executable = %q{lis}
12
+ s.date = %q{2010-02-14}
13
+ s.default_executable = %q{lis2http}
14
14
  s.description = %q{}
15
15
  s.email = %q{mail@levinalex.net}
16
- s.executables = ["lis"]
16
+ s.executables = ["lis2http"]
17
17
  s.extra_rdoc_files = [
18
18
  "LICENSE",
19
- "README.rdoc"
19
+ "README.markdown"
20
20
  ]
21
21
  s.files = [
22
22
  ".document",
23
23
  ".gitignore",
24
+ ".yardopts",
24
25
  "LICENSE",
25
- "README.rdoc",
26
+ "README.markdown",
26
27
  "Rakefile",
27
28
  "VERSION",
28
- "bin/lis",
29
+ "bin/lis2http",
29
30
  "features/communication basics.feature",
30
31
  "features/lis.feature",
31
32
  "features/step_definitions/lis_steps.rb",
32
33
  "features/support/env.rb",
33
34
  "lib/lis.rb",
34
35
  "lib/lis/application_protocol.rb",
36
+ "lib/lis/commands/application.rb",
37
+ "lib/lis/interface_server.rb",
35
38
  "lib/lis/io_listener.rb",
36
39
  "lib/lis/messages.rb",
40
+ "lib/lis/messages/header.rb",
41
+ "lib/lis/messages/order.rb",
42
+ "lib/lis/messages/patient.rb",
43
+ "lib/lis/messages/query.rb",
44
+ "lib/lis/messages/result.rb",
45
+ "lib/lis/messages/terminator.rb",
37
46
  "lib/lis/packetized_protocol.rb",
38
47
  "lib/lis/worklist_manager_interface.rb",
39
48
  "lis.gemspec",
40
49
  "test/helper.rb",
41
50
  "test/lib/mock_server.rb",
51
+ "test/messages/test_header.rb",
52
+ "test/messages/test_order.rb",
53
+ "test/messages/test_patients.rb",
54
+ "test/messages/test_terminator.rb",
55
+ "test/test_application_protocol.rb",
42
56
  "test/test_io_listener.rb",
43
57
  "test/test_messages.rb",
44
58
  "test/test_packetized_protocol.rb"
@@ -51,6 +65,11 @@ Gem::Specification.new do |s|
51
65
  s.test_files = [
52
66
  "test/helper.rb",
53
67
  "test/lib/mock_server.rb",
68
+ "test/messages/test_header.rb",
69
+ "test/messages/test_order.rb",
70
+ "test/messages/test_patients.rb",
71
+ "test/messages/test_terminator.rb",
72
+ "test/test_application_protocol.rb",
54
73
  "test/test_io_listener.rb",
55
74
  "test/test_messages.rb",
56
75
  "test/test_packetized_protocol.rb"
data/test/helper.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'rubygems'
2
2
  require 'test/unit'
3
3
  require 'shoulda'
4
+ require 'mocha'
4
5
  require 'lib/mock_server'
5
6
 
6
7
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
@@ -0,0 +1,19 @@
1
+ require 'helper'
2
+
3
+ class TestHeaderMessages < Test::Unit::TestCase
4
+ context "header message" do
5
+ setup do
6
+ Time.stubs(:now).returns(Time.mktime(2010,2,15,17,28,32))
7
+ end
8
+
9
+ should "have sane defaults" do
10
+ @message = LIS::Message::Header.new()
11
+ assert_equal "H|\^&|||SenderID||||8N1|ReceiverID||P|1|20100215172832", @message.to_message
12
+ end
13
+
14
+ should "have overridable sender and receiver IDs" do
15
+ @message = LIS::Message::Header.new("SEND", "RECV", "PASSWORD")
16
+ assert_equal "H|\^&||PASSWORD|SEND||||8N1|RECV||P|1|20100215172832", @message.to_message
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,10 @@
1
+ require 'helper'
2
+
3
+ class TestOrderMessages < Test::Unit::TestCase
4
+ context "order message" do
5
+ should "have sane defaults" do
6
+ @message = LIS::Message::Order.new("4","123ABC","TU")
7
+ assert_equal "O|4|123ABC||^^^TU", @message.to_message
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ require 'helper'
2
+
3
+ class TestPatientMessages < Test::Unit::TestCase
4
+ context "order message" do
5
+ should "have sane defaults" do
6
+ @message = LIS::Message::Patient.new(1, 101, "Riker", "Al")
7
+ assert_equal "P|1|101|||Riker^Al||||||||", @message.to_message
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,25 @@
1
+ require 'helper'
2
+
3
+ class TestTerminatorMessages < Test::Unit::TestCase
4
+ context "message parsing" do
5
+ setup do
6
+ @message = LIS::Message::Base.from_string("L|1|N")
7
+ end
8
+
9
+ should "have correct type" do
10
+ assert_equal LIS::Message::Terminator, @message.class
11
+ assert_equal "L", @message.type_id
12
+ end
13
+
14
+ should "have correct sequence number" do
15
+ assert_equal 1, @message.sequence_number
16
+ end
17
+ end
18
+
19
+ context "terminator message" do
20
+ should "work" do
21
+ @message = LIS::Message::Terminator.new()
22
+ assert_equal "L|1|N", @message.to_message
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,15 @@
1
+ require 'helper'
2
+
3
+ class TestApplicationProtocol < Test::Unit::TestCase
4
+ context "application protocol" do
5
+ setup do
6
+ @protocol = LIS::Transfer::ApplicationProtocol.new(nil, nil)
7
+ end
8
+
9
+ should "set device name when receiving header message" do
10
+ @protocol.receive(:message,
11
+ "H|\^&||PASSWORD|SenderID|Randolph^New^Jersey^07869||(201)927- 2828|8N1|ReceiverID||P|1|19950522092817")
12
+ assert_equal "SenderID", @protocol.device_name
13
+ end
14
+ end
15
+ end
@@ -1,37 +1,42 @@
1
1
  require 'helper'
2
2
 
3
- class TestPacketizedProtocol < Test::Unit::TestCase
3
+ class TestMessages < Test::Unit::TestCase
4
4
 
5
- context "message parsing" do
5
+ context "parsing an order message" do
6
6
  setup do
7
- @message = LIS::Message::Base.from_string("3L|1|N")
7
+ @str = "O|1|8780||^^^ATA|R|||||||||||||||||||B0135"
8
+ @message = LIS::Message::Base.from_string(@str)
8
9
  end
9
10
 
10
- should "have correct frame number" do
11
- assert_equal 3, @message.frame_number
11
+ should "have correct type" do
12
+ assert_equal LIS::Message::Order, @message.class
13
+ assert_equal "O", @message.type_id
12
14
  end
13
15
 
14
- should "have correct type" do
15
- assert_equal LIS::Message::TerminatorRecord, @message.class
16
- assert_equal "L", @message.type_id
16
+ should "have correct speciment id" do
17
+ assert_equal "8780", @message.specimen_id
17
18
  end
18
19
 
19
- should "have correct sequence number" do
20
- assert_equal 1, @message.sequence_number
20
+ should "return message itself on #to_message" do
21
+ assert_equal @str, @message.to_message
21
22
  end
22
23
  end
23
24
 
24
25
  context "parsing a result message" do
25
26
  setup do
26
- @str = "7R|1|^^^TSH|0.902|mIU/L|0.400\\0.004^4.00\\75.0|N|N|R|||20100115105636|20100115120641|B0135"
27
+ @str = "R|1|^^^TSH|0.902|mIU/L|0.400\\0.004^4.00\\75.0|N|N|R|||20100115105636|20100115120641|B0135"
27
28
  @message = LIS::Message::Base.from_string(@str)
28
29
  end
29
30
 
30
31
  should "have correct type" do
31
- assert_equal LIS::Message::Result, @message.type
32
+ assert_equal LIS::Message::Result, @message.class
32
33
  assert_equal "R", @message.type_id
33
34
  end
34
35
 
36
+ should "have correct timestamp" do
37
+ assert_equal "2010-01-15T10:56:36+00:00", @message.test_started_at.to_s
38
+ end
39
+
35
40
  should "have correct test id" do
36
41
  assert_equal "TSH", @message.universal_test_id
37
42
  end
@@ -43,6 +48,10 @@ class TestPacketizedProtocol < Test::Unit::TestCase
43
48
  should "have currect value and unit" do
44
49
  assert_equal "mIU/L", @message.unit
45
50
  end
51
+
52
+ should "return message itself on #to_message" do
53
+ assert_equal @str, @message.to_message
54
+ end
46
55
  end
47
56
 
48
57
  end
@@ -54,6 +54,11 @@ class TestPacketizedProtocol < Test::Unit::TestCase
54
54
  assert_equal [[:begin], [:idle]], @data
55
55
  end
56
56
 
57
+ should "ignore AKS and NAKs" do
58
+ @protocol.receive("\005\006\025\004")
59
+ assert_equal [[:begin], [:idle]], @data
60
+ end
61
+
57
62
  should "not fire end_of_transmission event after EOT is received" do
58
63
  @protocol.receive("\004")
59
64
  assert_equal [], @data
@@ -74,7 +79,19 @@ class TestPacketizedProtocol < Test::Unit::TestCase
74
79
  @protocol.receive(@str)
75
80
  @protocol.receive("\004")
76
81
 
77
- assert_equal [[:begin], [:message, "3L|1"], [:idle]], @data
82
+ assert_equal [[:begin], [:message, "L|1"], [:idle]], @data
83
+ end
84
+
85
+ should "add frame number and checksum when sending a message" do
86
+ @protocol.write(:begin)
87
+ @protocol.write(:message, "O|1|130000911||^^^E2")
88
+ @protocol.write(:message, "L|1")
89
+ @protocol.write(:idle)
90
+
91
+ assert_equal ["\005",
92
+ "\0021O|1|130000911||^^^E2\r\00301\r\n",
93
+ "\0022L|1\r\0033B\r\n",
94
+ "\004"], @sent
78
95
  end
79
96
  end
80
97
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Levin Alexander
@@ -9,8 +9,8 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-02-07 00:00:00 +01:00
13
- default_executable: lis
12
+ date: 2010-02-14 00:00:00 +01:00
13
+ default_executable: lis2http
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: thoughtbot-shoulda
@@ -45,33 +45,47 @@ dependencies:
45
45
  description: ""
46
46
  email: mail@levinalex.net
47
47
  executables:
48
- - lis
48
+ - lis2http
49
49
  extensions: []
50
50
 
51
51
  extra_rdoc_files:
52
52
  - LICENSE
53
- - README.rdoc
53
+ - README.markdown
54
54
  files:
55
55
  - .document
56
56
  - .gitignore
57
+ - .yardopts
57
58
  - LICENSE
58
- - README.rdoc
59
+ - README.markdown
59
60
  - Rakefile
60
61
  - VERSION
61
- - bin/lis
62
+ - bin/lis2http
62
63
  - features/communication basics.feature
63
64
  - features/lis.feature
64
65
  - features/step_definitions/lis_steps.rb
65
66
  - features/support/env.rb
66
67
  - lib/lis.rb
67
68
  - lib/lis/application_protocol.rb
69
+ - lib/lis/commands/application.rb
70
+ - lib/lis/interface_server.rb
68
71
  - lib/lis/io_listener.rb
69
72
  - lib/lis/messages.rb
73
+ - lib/lis/messages/header.rb
74
+ - lib/lis/messages/order.rb
75
+ - lib/lis/messages/patient.rb
76
+ - lib/lis/messages/query.rb
77
+ - lib/lis/messages/result.rb
78
+ - lib/lis/messages/terminator.rb
70
79
  - lib/lis/packetized_protocol.rb
71
80
  - lib/lis/worklist_manager_interface.rb
72
81
  - lis.gemspec
73
82
  - test/helper.rb
74
83
  - test/lib/mock_server.rb
84
+ - test/messages/test_header.rb
85
+ - test/messages/test_order.rb
86
+ - test/messages/test_patients.rb
87
+ - test/messages/test_terminator.rb
88
+ - test/test_application_protocol.rb
75
89
  - test/test_io_listener.rb
76
90
  - test/test_messages.rb
77
91
  - test/test_packetized_protocol.rb
@@ -106,6 +120,11 @@ summary: LIS interface to Siemens Immulite 2000XPi or other similar analyzers
106
120
  test_files:
107
121
  - test/helper.rb
108
122
  - test/lib/mock_server.rb
123
+ - test/messages/test_header.rb
124
+ - test/messages/test_order.rb
125
+ - test/messages/test_patients.rb
126
+ - test/messages/test_terminator.rb
127
+ - test/test_application_protocol.rb
109
128
  - test/test_io_listener.rb
110
129
  - test/test_messages.rb
111
130
  - test/test_packetized_protocol.rb
data/README.rdoc DELETED
@@ -1,7 +0,0 @@
1
- = LIS
2
-
3
- Description goes here.
4
-
5
- == Copyright
6
-
7
- Copyright (c) 2010 Levin Alexander. See LICENSE for details.
data/bin/lis DELETED
@@ -1,32 +0,0 @@
1
- #!/usr/bin/env ruby -w
2
- require 'lib/lis.rb'
3
-
4
- # protocol = LIS::Transfer::PacketizedProtocol.new
5
- # server = LIS::Transfer::Server.new(protocol, socket)
6
-
7
- socket = File.open("/dev/cu.usbserial-FTC95RQI", "w+")
8
- server = LIS::Transfer::IOListener.new(socket)
9
- packet_protocol = LIS::Transfer::PacketizedProtocol.new(server)
10
- app_protocol = LIS::Transfer::ApplicationProtocol.new(packet_protocol)
11
-
12
- interface = WorklistManagerInterface.new("http://localhost:3000")
13
-
14
- app_protocol.on_request do |*args|
15
- p "ON REQUEST"
16
- p args
17
-
18
- nil
19
- # interface.load_reqests(*args)
20
- end
21
-
22
- app_protocol.on_result do |*args|
23
- p "SEND RESULT"
24
- p args
25
- interface.send_result(*args)
26
- end
27
-
28
- app_protocol.on_data do |data|
29
- p data
30
- end
31
-
32
- server.run!