lis 0.0.0 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +1 -0
- data/README.markdown +9 -0
- data/VERSION +1 -1
- data/bin/lis2http +6 -0
- data/features/lis.feature +49 -8
- data/lib/lis/application_protocol.rb +43 -8
- data/lib/lis/commands/application.rb +55 -0
- data/lib/lis/interface_server.rb +23 -0
- data/lib/lis/io_listener.rb +51 -13
- data/lib/lis/messages/header.rb +23 -0
- data/lib/lis/messages/order.rb +35 -0
- data/lib/lis/messages/patient.rb +22 -0
- data/lib/lis/messages/query.rb +13 -0
- data/lib/lis/messages/result.rb +20 -0
- data/lib/lis/messages/terminator.rb +16 -0
- data/lib/lis/messages.rb +76 -79
- data/lib/lis/packetized_protocol.rb +46 -10
- data/lib/lis/worklist_manager_interface.rb +20 -9
- data/lib/lis.rb +4 -0
- data/lis.gemspec +26 -7
- data/test/helper.rb +1 -0
- data/test/messages/test_header.rb +19 -0
- data/test/messages/test_order.rb +10 -0
- data/test/messages/test_patients.rb +10 -0
- data/test/messages/test_terminator.rb +25 -0
- data/test/test_application_protocol.rb +15 -0
- data/test/test_messages.rb +21 -12
- data/test/test_packetized_protocol.rb +18 -1
- metadata +26 -7
- data/README.rdoc +0 -7
- data/bin/lis +0 -32
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.
|
1
|
+
0.1.0
|
data/bin/lis2http
ADDED
data/features/lis.feature
CHANGED
@@ -1,9 +1,50 @@
|
|
1
|
-
Feature:
|
2
|
-
In order to
|
3
|
-
|
4
|
-
|
1
|
+
Feature:
|
2
|
+
In order to value
|
3
|
+
As a role
|
4
|
+
I want feature
|
5
5
|
|
6
|
-
Scenario:
|
7
|
-
Given
|
8
|
-
When
|
9
|
-
|
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
|
-
@
|
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
|
-
@
|
37
|
-
requests = @on_request_callback.call(
|
38
|
-
@
|
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
|
-
|
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
|
data/lib/lis/io_listener.rb
CHANGED
@@ -2,41 +2,72 @@ require 'strscan'
|
|
2
2
|
|
3
3
|
module LIS::Transfer
|
4
4
|
|
5
|
-
# a chainable IO-Listener that provides
|
5
|
+
# a chainable IO-Listener that provides two methods:
|
6
6
|
#
|
7
|
-
#
|
8
|
-
#
|
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
|
-
#
|
13
|
-
#
|
14
|
-
#
|
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
|
-
#
|
17
|
-
#
|
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
|
-
|
32
|
-
@writer << message if @writer
|
33
|
-
end
|
49
|
+
# @see #write
|
34
50
|
def <<(*args)
|
35
51
|
write(*args)
|
36
52
|
end
|
37
53
|
|
38
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
16
|
-
|
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
|
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
|
-
|
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
|
-
|
69
|
+
type, data = string.scan(/^(.)\|(.*)$/)[0]
|
36
70
|
data = data.split(/\|/)
|
37
71
|
|
38
|
-
return [
|
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
|
47
|
-
|
48
|
-
|
81
|
+
def set_index_for(field_name, idx)
|
82
|
+
@field_indices ||= {}
|
83
|
+
@field_indices[field_name] = idx
|
49
84
|
end
|
50
85
|
|
51
|
-
def
|
52
|
-
|
53
|
-
val
|
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
|
-
|
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
|
99
|
+
def set_field_attributes(index, hash)
|
61
100
|
@field_names ||= {}
|
62
|
-
@field_names[
|
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
|
-
|
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
|
-
|
102
|
-
|
103
|
-
|
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
|
-
|
122
|
-
|
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
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
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.
|
23
|
-
"value" => result.
|
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.
|
32
|
+
"result_timestamp" => result.test_completed_at
|
28
33
|
}
|
29
34
|
|
30
|
-
|
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, '
|
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.
|
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-
|
13
|
-
s.default_executable = %q{
|
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 = ["
|
16
|
+
s.executables = ["lis2http"]
|
17
17
|
s.extra_rdoc_files = [
|
18
18
|
"LICENSE",
|
19
|
-
"README.
|
19
|
+
"README.markdown"
|
20
20
|
]
|
21
21
|
s.files = [
|
22
22
|
".document",
|
23
23
|
".gitignore",
|
24
|
+
".yardopts",
|
24
25
|
"LICENSE",
|
25
|
-
"README.
|
26
|
+
"README.markdown",
|
26
27
|
"Rakefile",
|
27
28
|
"VERSION",
|
28
|
-
"bin/
|
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
@@ -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 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
|
data/test/test_messages.rb
CHANGED
@@ -1,37 +1,42 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
|
-
class
|
3
|
+
class TestMessages < Test::Unit::TestCase
|
4
4
|
|
5
|
-
context "message
|
5
|
+
context "parsing an order message" do
|
6
6
|
setup do
|
7
|
-
@
|
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
|
11
|
-
assert_equal
|
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
|
15
|
-
assert_equal
|
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 "
|
20
|
-
assert_equal
|
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 = "
|
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.
|
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, "
|
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.
|
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-
|
13
|
-
default_executable:
|
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
|
-
-
|
48
|
+
- lis2http
|
49
49
|
extensions: []
|
50
50
|
|
51
51
|
extra_rdoc_files:
|
52
52
|
- LICENSE
|
53
|
-
- README.
|
53
|
+
- README.markdown
|
54
54
|
files:
|
55
55
|
- .document
|
56
56
|
- .gitignore
|
57
|
+
- .yardopts
|
57
58
|
- LICENSE
|
58
|
-
- README.
|
59
|
+
- README.markdown
|
59
60
|
- Rakefile
|
60
61
|
- VERSION
|
61
|
-
- bin/
|
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
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!
|