lis 0.0.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/.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
|