lis 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +23 -0
- data/LICENSE +20 -0
- data/README.rdoc +7 -0
- data/Rakefile +64 -0
- data/VERSION +1 -0
- data/bin/lis +32 -0
- data/features/communication basics.feature +6 -0
- data/features/lis.feature +9 -0
- data/features/step_definitions/lis_steps.rb +0 -0
- data/features/support/env.rb +6 -0
- data/lib/lis.rb +10 -0
- data/lib/lis/application_protocol.rb +69 -0
- data/lib/lis/io_listener.rb +76 -0
- data/lib/lis/messages.rb +125 -0
- data/lib/lis/packetized_protocol.rb +83 -0
- data/lib/lis/worklist_manager_interface.rb +47 -0
- data/lis.gemspec +78 -0
- data/test/helper.rb +12 -0
- data/test/lib/mock_server.rb +50 -0
- data/test/test_io_listener.rb +34 -0
- data/test/test_messages.rb +48 -0
- data/test/test_packetized_protocol.rb +81 -0
- metadata +111 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Levin Alexander
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "lis"
|
8
|
+
gem.summary = %Q{LIS interface to Siemens Immulite 2000XPi or other similar analyzers}
|
9
|
+
gem.description = %Q{}
|
10
|
+
gem.email = "mail@levinalex.net"
|
11
|
+
gem.homepage = "http://github.com/levinalex/lis"
|
12
|
+
gem.authors = ["Levin Alexander"]
|
13
|
+
gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
|
14
|
+
gem.add_development_dependency "yard", ">= 0"
|
15
|
+
gem.add_development_dependency "cucumber", ">= 0"
|
16
|
+
end
|
17
|
+
Jeweler::GemcutterTasks.new
|
18
|
+
rescue LoadError
|
19
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
20
|
+
end
|
21
|
+
|
22
|
+
require 'rake/testtask'
|
23
|
+
Rake::TestTask.new(:test) do |test|
|
24
|
+
test.libs << 'lib' << 'test'
|
25
|
+
test.pattern = 'test/**/test_*.rb'
|
26
|
+
test.verbose = true
|
27
|
+
end
|
28
|
+
|
29
|
+
begin
|
30
|
+
require 'rcov/rcovtask'
|
31
|
+
Rcov::RcovTask.new do |test|
|
32
|
+
test.libs << 'test'
|
33
|
+
test.pattern = 'test/**/test_*.rb'
|
34
|
+
test.verbose = true
|
35
|
+
end
|
36
|
+
rescue LoadError
|
37
|
+
task :rcov do
|
38
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
task :test => :check_dependencies
|
43
|
+
|
44
|
+
begin
|
45
|
+
require 'cucumber/rake/task'
|
46
|
+
Cucumber::Rake::Task.new(:features)
|
47
|
+
|
48
|
+
task :features => :check_dependencies
|
49
|
+
rescue LoadError
|
50
|
+
task :features do
|
51
|
+
abort "Cucumber is not available. In order to run features, you must: sudo gem install cucumber"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
task :default => :test
|
56
|
+
|
57
|
+
begin
|
58
|
+
require 'yard'
|
59
|
+
YARD::Rake::YardocTask.new
|
60
|
+
rescue LoadError
|
61
|
+
task :yardoc do
|
62
|
+
abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
|
63
|
+
end
|
64
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.0
|
data/bin/lis
ADDED
@@ -0,0 +1,32 @@
|
|
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!
|
File without changes
|
data/lib/lis.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
|
2
|
+
module LIS::Transfer
|
3
|
+
class ApplicationProtocol < Base
|
4
|
+
|
5
|
+
def on_result(&block)
|
6
|
+
@on_result_callback = block
|
7
|
+
end
|
8
|
+
|
9
|
+
def on_request(&block)
|
10
|
+
@on_request_callback = block
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
def received_header(message)
|
15
|
+
@patient_information ||= {} # delete the list of patients
|
16
|
+
end
|
17
|
+
|
18
|
+
def result_for(patient, order, result)
|
19
|
+
@on_result_callback.call(patient, order, result)
|
20
|
+
end
|
21
|
+
|
22
|
+
def received_patient_information(message)
|
23
|
+
@last_order = nil
|
24
|
+
@last_patient = message
|
25
|
+
end
|
26
|
+
|
27
|
+
def received_order_record(message)
|
28
|
+
@last_order = message
|
29
|
+
end
|
30
|
+
|
31
|
+
def received_result(message)
|
32
|
+
result_for(@last_patient, @last_order, message)
|
33
|
+
end
|
34
|
+
|
35
|
+
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
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(*args)
|
42
|
+
super
|
43
|
+
|
44
|
+
@last_patient = nil
|
45
|
+
@last_order = nil
|
46
|
+
@handlers = {
|
47
|
+
LIS::Message::Header => :received_header,
|
48
|
+
LIS::Message::Patient => :received_patient_information,
|
49
|
+
LIS::Message::Order => :received_order_record,
|
50
|
+
LIS::Message::Result => :received_result,
|
51
|
+
LIS::Message::Query => :received_request_for_information
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def receive(type, message = nil)
|
56
|
+
case type
|
57
|
+
when :begin
|
58
|
+
@last_patient = nil
|
59
|
+
@last_order = nil
|
60
|
+
when :idle
|
61
|
+
when :message
|
62
|
+
@message = LIS::Message::Base.from_string(message)
|
63
|
+
handler = @handlers[@message.class]
|
64
|
+
send(handler, @message) if handler
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
module LIS::Transfer
|
4
|
+
|
5
|
+
# a chainable IO-Listener that provides to methods:
|
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
|
9
|
+
#
|
10
|
+
# when overriding this class, you need to implement two methods:
|
11
|
+
#
|
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`
|
15
|
+
#
|
16
|
+
# +write+ :: when data needs to be encoded, formatted before it can be send
|
17
|
+
# send it with `super`
|
18
|
+
#
|
19
|
+
#
|
20
|
+
class Base
|
21
|
+
def initialize(read, write = read)
|
22
|
+
@reader, @writer = read, write
|
23
|
+
@on_data = nil
|
24
|
+
@reader.on_data { |*data| receive(*data) } if @reader.respond_to?(:on_data)
|
25
|
+
end
|
26
|
+
|
27
|
+
def on_data(&block)
|
28
|
+
@on_data = block
|
29
|
+
end
|
30
|
+
|
31
|
+
def write(message)
|
32
|
+
@writer << message if @writer
|
33
|
+
end
|
34
|
+
def <<(*args)
|
35
|
+
write(*args)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def receive(data)
|
41
|
+
forward(data)
|
42
|
+
end
|
43
|
+
|
44
|
+
def forward(*data)
|
45
|
+
@on_data.call(*data) if @on_data
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
class LineBasedProtocol < Base
|
51
|
+
def receive(data)
|
52
|
+
@memo ||= ""
|
53
|
+
scanner = StringScanner.new(@memo + data)
|
54
|
+
while s = scanner.scan(/.*?\n/)
|
55
|
+
forward(s.strip)
|
56
|
+
end
|
57
|
+
@memo = scanner.rest
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
|
61
|
+
def write(data)
|
62
|
+
super(data + "\n")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class IOListener < Base
|
67
|
+
def run!
|
68
|
+
while not @reader.eof?
|
69
|
+
str = @reader.readpartial(4096)
|
70
|
+
forward(str)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
|
data/lib/lis/messages.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
|
2
|
+
module LIS::Message
|
3
|
+
module ClassMethods
|
4
|
+
FIELD_TYPES = {
|
5
|
+
:string => lambda { |s| s },
|
6
|
+
:int => lambda { |s| s.to_i }
|
7
|
+
}
|
8
|
+
|
9
|
+
def from_string(message)
|
10
|
+
frame_number, type, data = parse(message)
|
11
|
+
klass = (@@messages_by_type || {})[type]
|
12
|
+
raise "unknown message type #{type.inspect}" unless klass
|
13
|
+
|
14
|
+
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
|
24
|
+
end
|
25
|
+
|
26
|
+
obj
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize_from_message(*list_of_fields)
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
34
|
+
def parse(string)
|
35
|
+
frame_number, type, data = string.scan(/^(.)(.)\|(.*)$/)[0]
|
36
|
+
data = data.split(/\|/)
|
37
|
+
|
38
|
+
return [frame_number.to_i, type, data]
|
39
|
+
end
|
40
|
+
|
41
|
+
def type_id(char)
|
42
|
+
@@messages_by_type ||= {}
|
43
|
+
@@messages_by_type[char] = self
|
44
|
+
end
|
45
|
+
|
46
|
+
def named_field(idx, name, type = :string)
|
47
|
+
set_named_field_attributes(idx, :name => name, :type => type)
|
48
|
+
attr_accessor name
|
49
|
+
end
|
50
|
+
|
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)
|
55
|
+
val
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def set_named_field_attributes(key, *val)
|
61
|
+
@field_names ||= {}
|
62
|
+
@field_names[key] = *val
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class Base
|
67
|
+
extend ClassMethods
|
68
|
+
attr_accessor :frame_number
|
69
|
+
attr_accessor :type_id
|
70
|
+
|
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
|
100
|
+
|
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
|
+
}
|
120
|
+
|
121
|
+
type_id "L"
|
122
|
+
named_field 3, :termination_code
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
|
2
|
+
module LIS::Transfer
|
3
|
+
|
4
|
+
# splits a stream into lis packets and only lets packets through
|
5
|
+
# that are inside a session delimited by ENQ .. EOT
|
6
|
+
#
|
7
|
+
# check the checksum and do acknowledgement of messages
|
8
|
+
#
|
9
|
+
# forwards the following events:
|
10
|
+
#
|
11
|
+
# - :message, String :: when a message is received
|
12
|
+
# - :idle :: when a transmission is finished (after EOT is received)
|
13
|
+
#
|
14
|
+
class PacketizedProtocol < Base
|
15
|
+
ACK = "\006"
|
16
|
+
NAK = "\025"
|
17
|
+
ENQ = "\005"
|
18
|
+
EOT = "\004"
|
19
|
+
|
20
|
+
RX = /(?:
|
21
|
+
\005 | # ENQ - start a transaction
|
22
|
+
\004 | # EOT - ends a transaction
|
23
|
+
(?:\002 (.*?) \015 \003 (.+?) \015 \012)) # a message with a checksum
|
24
|
+
/xm
|
25
|
+
|
26
|
+
def initialize(*args)
|
27
|
+
super(*args)
|
28
|
+
@memo = ""
|
29
|
+
@inside_transmission = false
|
30
|
+
end
|
31
|
+
|
32
|
+
def receive(data)
|
33
|
+
scanner = StringScanner.new(@memo + data)
|
34
|
+
while match = scanner.scan(RX)
|
35
|
+
case match
|
36
|
+
when ENQ then transmission_start
|
37
|
+
when EOT then transmission_end
|
38
|
+
else
|
39
|
+
received_message(match)
|
40
|
+
write ACK
|
41
|
+
end
|
42
|
+
end
|
43
|
+
@memo = scanner.rest
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def self.message_from_string(string)
|
51
|
+
match = string.match(RX)
|
52
|
+
data = match[1]
|
53
|
+
checksum = match[2]
|
54
|
+
|
55
|
+
expected_checksum = data.to_enum(:each_byte).inject(16) { |a,b| (a+b) % 0x100 }
|
56
|
+
actual_checksum = checksum.to_i(16)
|
57
|
+
|
58
|
+
raise "checksum mismatch" unless expected_checksum == actual_checksum
|
59
|
+
return data
|
60
|
+
end
|
61
|
+
|
62
|
+
def received_message(message)
|
63
|
+
return false unless @inside_transmission
|
64
|
+
forward(:message, self.class.message_from_string(message))
|
65
|
+
end
|
66
|
+
|
67
|
+
def transmission_start
|
68
|
+
return false if @inside_transmission
|
69
|
+
write ACK
|
70
|
+
forward :begin
|
71
|
+
@inside_transmission = true
|
72
|
+
true
|
73
|
+
end
|
74
|
+
|
75
|
+
def transmission_end
|
76
|
+
return false unless @inside_transmission
|
77
|
+
forward :idle
|
78
|
+
@inside_transmission = false
|
79
|
+
true
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
class WorklistManagerInterface
|
2
|
+
def initialize(endpoint)
|
3
|
+
@endpoint = endpoint
|
4
|
+
end
|
5
|
+
|
6
|
+
def load_requests(barcode)
|
7
|
+
begin
|
8
|
+
uri = URI.join(@endpoint,"find_requests/#{barcode}")
|
9
|
+
result = fetch_with_redirect(uri.to_s)
|
10
|
+
data = YAML.load(result.body)
|
11
|
+
data["id"] = barcode
|
12
|
+
rescue Exception => e
|
13
|
+
puts e
|
14
|
+
puts e.backtrace
|
15
|
+
data = nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def send_result(patient, order, result)
|
20
|
+
barcode = order.specimen_id
|
21
|
+
data = {
|
22
|
+
"test_name" => order.test_id,
|
23
|
+
"value" => result.value,
|
24
|
+
"unit" => result.unit,
|
25
|
+
"status" => result.result_status,
|
26
|
+
"flags" => result.abnormal_flags,
|
27
|
+
"result_timestamp" => result.timestamp
|
28
|
+
}
|
29
|
+
|
30
|
+
Net::HTTP.post_form(URI.join(@endpoint, "result/#{URI.encode(barcode)}"), data.to_hash)
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def fetch_with_redirect(uri_str, limit = 10)
|
37
|
+
raise ArgumentError, 'HTTP redirect too deep' if limit == 0
|
38
|
+
|
39
|
+
response = Net::HTTP.get_response(URI.parse(uri_str))
|
40
|
+
case response
|
41
|
+
when Net::HTTPSuccess then response
|
42
|
+
when Net::HTTPRedirection then fetch_with_redirect(response['location'], limit - 1)
|
43
|
+
else
|
44
|
+
response.error!
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lis.gemspec
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{lis}
|
8
|
+
s.version = "0.0.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Levin Alexander"]
|
12
|
+
s.date = %q{2010-02-07}
|
13
|
+
s.default_executable = %q{lis}
|
14
|
+
s.description = %q{}
|
15
|
+
s.email = %q{mail@levinalex.net}
|
16
|
+
s.executables = ["lis"]
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"LICENSE",
|
19
|
+
"README.rdoc"
|
20
|
+
]
|
21
|
+
s.files = [
|
22
|
+
".document",
|
23
|
+
".gitignore",
|
24
|
+
"LICENSE",
|
25
|
+
"README.rdoc",
|
26
|
+
"Rakefile",
|
27
|
+
"VERSION",
|
28
|
+
"bin/lis",
|
29
|
+
"features/communication basics.feature",
|
30
|
+
"features/lis.feature",
|
31
|
+
"features/step_definitions/lis_steps.rb",
|
32
|
+
"features/support/env.rb",
|
33
|
+
"lib/lis.rb",
|
34
|
+
"lib/lis/application_protocol.rb",
|
35
|
+
"lib/lis/io_listener.rb",
|
36
|
+
"lib/lis/messages.rb",
|
37
|
+
"lib/lis/packetized_protocol.rb",
|
38
|
+
"lib/lis/worklist_manager_interface.rb",
|
39
|
+
"lis.gemspec",
|
40
|
+
"test/helper.rb",
|
41
|
+
"test/lib/mock_server.rb",
|
42
|
+
"test/test_io_listener.rb",
|
43
|
+
"test/test_messages.rb",
|
44
|
+
"test/test_packetized_protocol.rb"
|
45
|
+
]
|
46
|
+
s.homepage = %q{http://github.com/levinalex/lis}
|
47
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
48
|
+
s.require_paths = ["lib"]
|
49
|
+
s.rubygems_version = %q{1.3.5}
|
50
|
+
s.summary = %q{LIS interface to Siemens Immulite 2000XPi or other similar analyzers}
|
51
|
+
s.test_files = [
|
52
|
+
"test/helper.rb",
|
53
|
+
"test/lib/mock_server.rb",
|
54
|
+
"test/test_io_listener.rb",
|
55
|
+
"test/test_messages.rb",
|
56
|
+
"test/test_packetized_protocol.rb"
|
57
|
+
]
|
58
|
+
|
59
|
+
if s.respond_to? :specification_version then
|
60
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
61
|
+
s.specification_version = 3
|
62
|
+
|
63
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
64
|
+
s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
65
|
+
s.add_development_dependency(%q<yard>, [">= 0"])
|
66
|
+
s.add_development_dependency(%q<cucumber>, [">= 0"])
|
67
|
+
else
|
68
|
+
s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
69
|
+
s.add_dependency(%q<yard>, [">= 0"])
|
70
|
+
s.add_dependency(%q<cucumber>, [">= 0"])
|
71
|
+
end
|
72
|
+
else
|
73
|
+
s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
74
|
+
s.add_dependency(%q<yard>, [">= 0"])
|
75
|
+
s.add_dependency(%q<cucumber>, [">= 0"])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
data/test/helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'shoulda'
|
4
|
+
require 'lib/mock_server'
|
5
|
+
|
6
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
7
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
8
|
+
require 'lis'
|
9
|
+
|
10
|
+
class Test::Unit::TestCase
|
11
|
+
end
|
12
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Mock
|
2
|
+
end
|
3
|
+
|
4
|
+
class Mock::Server
|
5
|
+
def initialize(read, write)
|
6
|
+
@read, @write = read, write
|
7
|
+
@queue = Queue.new
|
8
|
+
@thread = Thread.new do
|
9
|
+
parse_commands
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def write(string)
|
14
|
+
@queue.push [:write, string]
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def read_all
|
19
|
+
@read.readpartial(4096)
|
20
|
+
end
|
21
|
+
|
22
|
+
def wait(seconds = 0.02)
|
23
|
+
@queue.push [:wait, seconds]
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def eof
|
28
|
+
@queue.push [:close]
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def parse_commands
|
35
|
+
loop do
|
36
|
+
action, data = @queue.pop
|
37
|
+
|
38
|
+
case action
|
39
|
+
when :close
|
40
|
+
@write.close
|
41
|
+
break
|
42
|
+
when :write
|
43
|
+
@write.write(data)
|
44
|
+
@write.flush
|
45
|
+
when :wait
|
46
|
+
sleep data
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestIOListener < Test::Unit::TestCase
|
4
|
+
context "a server" do
|
5
|
+
setup do
|
6
|
+
r1, w1 = IO.pipe # Immulite -> LIS
|
7
|
+
r2, w2 = IO.pipe # LIS -> Immulite
|
8
|
+
|
9
|
+
@server = LIS::Transfer::IOListener.new(r1, w2)
|
10
|
+
@protocol = LIS::Transfer::LineBasedProtocol.new(@server)
|
11
|
+
@device = Mock::Server.new(r2, w1)
|
12
|
+
end
|
13
|
+
|
14
|
+
should "exist" do
|
15
|
+
assert_not_nil @server
|
16
|
+
end
|
17
|
+
|
18
|
+
should "yield packets written to it" do
|
19
|
+
@packets = []
|
20
|
+
@protocol.on_data { |packet| @packets << packet }
|
21
|
+
|
22
|
+
@device.write("fo").wait.write("o\n").wait.write("bar\n").eof
|
23
|
+
@server.run!
|
24
|
+
|
25
|
+
assert_equal ["foo", "bar"], @packets
|
26
|
+
end
|
27
|
+
|
28
|
+
should "send data" do
|
29
|
+
@protocol << "hello world"
|
30
|
+
data = @device.read_all
|
31
|
+
assert_equal "hello world\n", data
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestPacketizedProtocol < Test::Unit::TestCase
|
4
|
+
|
5
|
+
context "message parsing" do
|
6
|
+
setup do
|
7
|
+
@message = LIS::Message::Base.from_string("3L|1|N")
|
8
|
+
end
|
9
|
+
|
10
|
+
should "have correct frame number" do
|
11
|
+
assert_equal 3, @message.frame_number
|
12
|
+
end
|
13
|
+
|
14
|
+
should "have correct type" do
|
15
|
+
assert_equal LIS::Message::TerminatorRecord, @message.class
|
16
|
+
assert_equal "L", @message.type_id
|
17
|
+
end
|
18
|
+
|
19
|
+
should "have correct sequence number" do
|
20
|
+
assert_equal 1, @message.sequence_number
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context "parsing a result message" do
|
25
|
+
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
|
+
@message = LIS::Message::Base.from_string(@str)
|
28
|
+
end
|
29
|
+
|
30
|
+
should "have correct type" do
|
31
|
+
assert_equal LIS::Message::Result, @message.type
|
32
|
+
assert_equal "R", @message.type_id
|
33
|
+
end
|
34
|
+
|
35
|
+
should "have correct test id" do
|
36
|
+
assert_equal "TSH", @message.universal_test_id
|
37
|
+
end
|
38
|
+
|
39
|
+
should "have correct value" do
|
40
|
+
assert_equal "0.902", @message.result_value
|
41
|
+
end
|
42
|
+
|
43
|
+
should "have currect value and unit" do
|
44
|
+
assert_equal "mIU/L", @message.unit
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestPacketizedProtocol < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def self.gsub_nonprintable(str)
|
6
|
+
str.gsub(/<[A-Z]+?>/) do |match|
|
7
|
+
{ "<ETX>" => "\003",
|
8
|
+
"<STX>" => "\002",
|
9
|
+
"<CR>" => "\015",
|
10
|
+
"<LF>" => "\012",
|
11
|
+
"<ENQ>" => "\004",
|
12
|
+
"<EOT>" => "\005" }[match] or raise ArgumentError, "match #{match.inspect} not found"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def assert_packet_matches(expected, str)
|
17
|
+
str = self.class.gsub_nonprintable(str)
|
18
|
+
expected = self.class.gsub_nonprintable(expected)
|
19
|
+
|
20
|
+
match = LIS::Transfer::PacketizedProtocol::RX.match(str)
|
21
|
+
assert_not_nil match, expected
|
22
|
+
assert_equal expected, match[0]
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.packet_should_match(expected, str)
|
26
|
+
should "match \"#{str}\" as \"#{expected}\"" do
|
27
|
+
assert_packet_matches(expected, str)
|
28
|
+
assert_packet_matches(expected, "garbage" + str)
|
29
|
+
assert_packet_matches(expected, str + "garbage")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
packet_should_match "<ENQ>", "<ENQ>"
|
34
|
+
packet_should_match "<EOT>", "<EOT>"
|
35
|
+
packet_should_match "<STX>packet_data<CR><ETX>checksum<CR><LF>",
|
36
|
+
"<STX>packet_data<CR><ETX>checksum<CR><LF><STX>packet_data<CR><ETX>checksum<CR><LF>rest"
|
37
|
+
|
38
|
+
context "packetized protocol" do
|
39
|
+
setup do
|
40
|
+
@sent = []
|
41
|
+
@data = []
|
42
|
+
@protocol = LIS::Transfer::PacketizedProtocol.new(nil, @sent)
|
43
|
+
@protocol.on_data do |*d|
|
44
|
+
@data << d
|
45
|
+
end
|
46
|
+
end
|
47
|
+
should "fire start_of_transmission event when receiving ENQ" do
|
48
|
+
@protocol.receive("\005")
|
49
|
+
assert_equal [[:begin]], @data
|
50
|
+
end
|
51
|
+
|
52
|
+
should "fire end_of_transmission event after EOT is received" do
|
53
|
+
@protocol.receive("\005\004")
|
54
|
+
assert_equal [[:begin], [:idle]], @data
|
55
|
+
end
|
56
|
+
|
57
|
+
should "not fire end_of_transmission event after EOT is received" do
|
58
|
+
@protocol.receive("\004")
|
59
|
+
assert_equal [], @data
|
60
|
+
end
|
61
|
+
|
62
|
+
should "fire trasmission events the correct number of times" do
|
63
|
+
@protocol.receive("\005\005")
|
64
|
+
@protocol.receive("\004")
|
65
|
+
assert_equal [[:begin], [:idle]], @data
|
66
|
+
@protocol.receive("\004\005")
|
67
|
+
@protocol.receive("\004")
|
68
|
+
assert_equal [[:begin], [:idle], [:begin], [:idle]], @data
|
69
|
+
end
|
70
|
+
|
71
|
+
should "propagate only packet data" do
|
72
|
+
@str = "\0023L|1\r\0033C\r\n"
|
73
|
+
@protocol.receive("\005")
|
74
|
+
@protocol.receive(@str)
|
75
|
+
@protocol.receive("\004")
|
76
|
+
|
77
|
+
assert_equal [[:begin], [:message, "3L|1"], [:idle]], @data
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
metadata
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lis
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Levin Alexander
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-02-07 00:00:00 +01:00
|
13
|
+
default_executable: lis
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: thoughtbot-shoulda
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: yard
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: cucumber
|
37
|
+
type: :development
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: "0"
|
44
|
+
version:
|
45
|
+
description: ""
|
46
|
+
email: mail@levinalex.net
|
47
|
+
executables:
|
48
|
+
- lis
|
49
|
+
extensions: []
|
50
|
+
|
51
|
+
extra_rdoc_files:
|
52
|
+
- LICENSE
|
53
|
+
- README.rdoc
|
54
|
+
files:
|
55
|
+
- .document
|
56
|
+
- .gitignore
|
57
|
+
- LICENSE
|
58
|
+
- README.rdoc
|
59
|
+
- Rakefile
|
60
|
+
- VERSION
|
61
|
+
- bin/lis
|
62
|
+
- features/communication basics.feature
|
63
|
+
- features/lis.feature
|
64
|
+
- features/step_definitions/lis_steps.rb
|
65
|
+
- features/support/env.rb
|
66
|
+
- lib/lis.rb
|
67
|
+
- lib/lis/application_protocol.rb
|
68
|
+
- lib/lis/io_listener.rb
|
69
|
+
- lib/lis/messages.rb
|
70
|
+
- lib/lis/packetized_protocol.rb
|
71
|
+
- lib/lis/worklist_manager_interface.rb
|
72
|
+
- lis.gemspec
|
73
|
+
- test/helper.rb
|
74
|
+
- test/lib/mock_server.rb
|
75
|
+
- test/test_io_listener.rb
|
76
|
+
- test/test_messages.rb
|
77
|
+
- test/test_packetized_protocol.rb
|
78
|
+
has_rdoc: true
|
79
|
+
homepage: http://github.com/levinalex/lis
|
80
|
+
licenses: []
|
81
|
+
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options:
|
84
|
+
- --charset=UTF-8
|
85
|
+
require_paths:
|
86
|
+
- lib
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: "0"
|
92
|
+
version:
|
93
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: "0"
|
98
|
+
version:
|
99
|
+
requirements: []
|
100
|
+
|
101
|
+
rubyforge_project:
|
102
|
+
rubygems_version: 1.3.5
|
103
|
+
signing_key:
|
104
|
+
specification_version: 3
|
105
|
+
summary: LIS interface to Siemens Immulite 2000XPi or other similar analyzers
|
106
|
+
test_files:
|
107
|
+
- test/helper.rb
|
108
|
+
- test/lib/mock_server.rb
|
109
|
+
- test/test_io_listener.rb
|
110
|
+
- test/test_messages.rb
|
111
|
+
- test/test_packetized_protocol.rb
|