lis 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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!