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