levinalex-LiaisonLabor 0.1.7

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.
@@ -0,0 +1,10 @@
1
+ == 0.1.6 / 2007-09-11
2
+
3
+ * change name of configuration file to ".liaison_labor"
4
+ * change configuration keys to strings
5
+ * honor HTTP endpoint given in configuration file
6
+
7
+ == 0.1.5 / 2007-07-19
8
+
9
+ * delete patient list on initialization
10
+
@@ -0,0 +1,13 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ bin/liaison_server
6
+ liaison_labor.gemspec
7
+ lib/liaison_labor.rb
8
+ lib/liaison_labor/interface.rb
9
+ lib/liaison_labor/packets.rb
10
+ lib/liaison_server.rb
11
+ spec/liaison_interface_spec.rb
12
+ spec/liaison_packet_spec.rb
13
+ spec/mock/mock_liaison.rb
@@ -0,0 +1,49 @@
1
+ LiaisonLabor
2
+ by Levin Alexander
3
+ http://levinalex.net/src/liaison_labor
4
+
5
+ == DESCRIPTION:
6
+
7
+ An interface to the LIAISON® analyser by DiaSorin
8
+ <http://www.diasorin.com/en/productsandsystems/liaison>
9
+
10
+ == FEATURES/PROBLEMS:
11
+
12
+ * not yet written
13
+
14
+ == SYNOPSIS:
15
+
16
+ not yet written
17
+
18
+ == REQUIREMENTS:
19
+
20
+ * not yet written
21
+
22
+ == INSTALL:
23
+
24
+ * not yet written
25
+
26
+ == LICENSE:
27
+
28
+ (The MIT License)
29
+
30
+ Copyright (c) 2007-2008 Levin Alexander
31
+
32
+ Permission is hereby granted, free of charge, to any person obtaining
33
+ a copy of this software and associated documentation files (the
34
+ 'Software'), to deal in the Software without restriction, including
35
+ without limitation the rights to use, copy, modify, merge, publish,
36
+ distribute, sublicense, and/or sell copies of the Software, and to
37
+ permit persons to whom the Software is furnished to do so, subject to
38
+ the following conditions:
39
+
40
+ The above copyright notice and this permission notice shall be
41
+ included in all copies or substantial portions of the Software.
42
+
43
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
44
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
45
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
46
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
47
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
48
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
49
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ require 'rubygems'
2
+ require 'hoe'
3
+ require 'spec/rake/spectask'
4
+
5
+ require './lib/liaison_labor.rb'
6
+
7
+ Hoe.new('LiaisonLabor', LiaisonLabor::VERSION) do |p|
8
+ p.rubyforge_name = 'liaison_labor'
9
+ p.summary = 'interfaces Liaison device to worklist_manager'
10
+
11
+ p.url = 'http://levinalex.net/src/liaison'
12
+ p.developer('Levin Alexander', 'mail@levinalex.net')
13
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
14
+
15
+ # p.extra_deps ['levinalex-serial_interface']
16
+ end
17
+
18
+ Rake.application.instance_eval { @tasks["test"] = nil }
19
+
20
+ Spec::Rake::SpecTask.new do |t|
21
+ t.warning = true
22
+ t.spec_opts = %w(-c -f specdoc)
23
+ end
24
+ task :test => :spec
25
+
26
+ task :cultivate do
27
+ system "touch Manifest.txt; rake check_manifest | grep -v \"(in \" | patch"
28
+ system "rake debug_gem | grep -v \"(in \" > `basename \\`pwd\\``.gemspec"
29
+ end
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # start a process that continuously listens on a port and
4
+ # processes packets
5
+ #
6
+ # see "./liaison_server -h" for help
7
+ #
8
+ require File.join(File.dirname(__FILE__),'..','lib','liaison_server.rb')
9
+ Diasorin::LiaisonServer.new.run!
@@ -0,0 +1,36 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{LiaisonLabor}
5
+ s.version = "0.1.7"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Levin Alexander"]
9
+ s.date = %q{2009-06-11}
10
+ s.default_executable = %q{liaison_server}
11
+ s.description = %q{An interface to the LIAISON® analyser by DiaSorin <http://www.diasorin.com/en/productsandsystems/liaison>}
12
+ s.email = ["mail@levinalex.net"]
13
+ s.executables = ["liaison_server"]
14
+ s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.txt"]
15
+ s.files = ["History.txt", "Manifest.txt", "README.txt", "Rakefile", "bin/liaison_server", "liaison_labor.gemspec", "lib/liaison_labor.rb", "lib/liaison_labor/interface.rb", "lib/liaison_labor/packets.rb", "lib/liaison_server.rb", "spec/liaison_interface_spec.rb", "spec/liaison_packet_spec.rb", "spec/mock/mock_liaison.rb"]
16
+ s.has_rdoc = true
17
+ s.homepage = %q{http://levinalex.net/src/liaison}
18
+ s.rdoc_options = ["--main", "README.txt"]
19
+ s.require_paths = ["lib"]
20
+ s.rubyforge_project = %q{liaison_labor}
21
+ s.rubygems_version = %q{1.3.1}
22
+ s.summary = %q{interfaces Liaison device to worklist_manager}
23
+
24
+ if s.respond_to? :specification_version then
25
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
26
+ s.specification_version = 2
27
+
28
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
29
+ s.add_development_dependency(%q<hoe>, [">= 1.12.1"])
30
+ else
31
+ s.add_dependency(%q<hoe>, [">= 1.12.1"])
32
+ end
33
+ else
34
+ s.add_dependency(%q<hoe>, [">= 1.12.1"])
35
+ end
36
+ end
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+
3
+ require File.join(File.dirname(__FILE__),'liaison_labor','interface.rb')
4
+ require File.join(File.dirname(__FILE__),'liaison_labor','packets.rb')
5
+
6
+ class LiaisonLabor
7
+ VERSION = '0.1.7'
8
+ end
@@ -0,0 +1,236 @@
1
+ require 'rubygems'
2
+ require 'serial_interface'
3
+
4
+ module Diasorin
5
+ module Liaison
6
+
7
+ class Interface < PacketIO
8
+
9
+ def initialize(read, write=read, options = {}, &blk)
10
+ super(LiaisonProtocol, read, write, options)
11
+
12
+ # define default handlers for import/export of patients
13
+ # and results
14
+ #
15
+ @on_order_request = lambda { nil }
16
+
17
+ # is called whenever a result is sent
18
+ #
19
+ @on_result = lambda { |patient, order, result| }
20
+
21
+ yield self if block_given?
22
+
23
+ add_sender( {
24
+ :message_header => Packets::MessageHeaderRecord,
25
+ :patient => Packets::PatientInformationRecord,
26
+ :order => Packets::TestOrderRecord,
27
+ :terminator => Packets::MessageTerminatorRecord
28
+ } )
29
+
30
+ add_receiver :message_header => Packets::MessageHeaderRecord do |p|
31
+ # delete the list of patients
32
+ @patient_information ||= {}
33
+ end
34
+
35
+ add_receiver :patient_record => Packets::PatientInformationRecord do |p|
36
+ @last_order_packet = nil
37
+ @last_result_record = nil
38
+
39
+ @last_patient_packet = p
40
+ end
41
+
42
+ add_receiver :comment => Packets::CommentRecord do |p|
43
+ @on_result.call(@last_patient_packet, @last_order_packet, @last_result_record, p)
44
+ end
45
+
46
+ add_receiver :order => Packets::TestOrderRecord do |p|
47
+ @last_result_record = nil
48
+
49
+ @last_order_packet = p
50
+ end
51
+
52
+ add_receiver :test_result => Packets::ResultRecord do |p|
53
+ @last_result_record = p
54
+ @on_result.call(@last_patient_packet, @last_order_packet, @last_result_record)
55
+ end
56
+
57
+
58
+ add_receiver :request_information => Packets::RequestInformationSegment do |p|
59
+ puts "RequestInformation: #{p.inspect}"
60
+
61
+ @patient_information ||= {}
62
+ requests = @on_order_request.call(p.starting_range)
63
+ @patient_information[p.sequence_number] = requests if requests
64
+
65
+ @mode = :request_information
66
+ end
67
+
68
+ add_receiver :ack => Packets::Acknowledge do |p|
69
+ if @mode == :request_information
70
+ @mode = @transmitting
71
+ send_packet(:message_header, "HOST", "Liaison")
72
+ transmit_requests
73
+ else
74
+ # keep on sending
75
+ end
76
+ end
77
+
78
+ add_receiver :nack => Packets::NotAcknowledge do |p|
79
+ puts "Nack!"
80
+ raise "NACK received"
81
+ end
82
+
83
+ add_receiver :message_terminator => Packets::MessageTerminatorRecord do |p|
84
+ puts "got MessageTerminatorRecord"
85
+ if @mode == :request_information
86
+ puts "should now send information"
87
+ # initiate transfer (ENQ)
88
+ send_packet("\x05", :raw => true)
89
+ end
90
+ end
91
+
92
+ run
93
+ end
94
+
95
+ def on_order_request(&blk)
96
+ @on_order_request = blk
97
+ end
98
+ def on_result(&blk)
99
+ @on_result = blk
100
+ end
101
+
102
+ def transmit_requests
103
+ @patient_information.each do |sequence_nr, data|
104
+ p "sequence: #{sequence_nr.inspect}"
105
+ p "data: #{data.inspect}"
106
+ send_packet(:patient, sequence_nr, data["patient"]["number"], data["patient"]["last_name"], data["patient"]["first_name"])
107
+ data["types"].each do |request|
108
+ send_packet(:order, sequence_nr, data["id"], request)
109
+ end
110
+ end
111
+ send_packet(:terminator, "1")
112
+ # EOT
113
+ send_packet("\x04", :raw => true)
114
+
115
+ @mode = :neutral
116
+ end
117
+
118
+ end
119
+
120
+
121
+ class LiaisonProtocol
122
+ STX = 0x02 # STX
123
+ ETX = 0x03 # ETX
124
+ NACK = 0x15 # NACK
125
+ ACK = 0x06 # ACK
126
+ ENQ = 0x05 # ENQ
127
+ CR = 0x0d
128
+ LF = 0x0a
129
+ EOT = 0x04
130
+
131
+ def initialize(send_callback, receive_callback, options = {})
132
+ @send_callback, @receive_callback = send_callback, receive_callback
133
+ @packet_buffer = ""
134
+ @send_buffer = []
135
+ @checksum = 0
136
+ @frame_number = 1
137
+ @state = :neutral
138
+ end
139
+
140
+ def self.checksum_valid?(string, expected)
141
+ sum = string.to_enum(:each_byte).inject(ETX) { |s,v| s+v } % 0x100
142
+ if sum == expected
143
+ return true
144
+ else
145
+ raise ::SerialProtocol::ChecksumMismatch, "expected #{expected}, got #{sum}"
146
+ end
147
+ end
148
+
149
+ def get_packet(str)
150
+ @receive_callback.call(str)
151
+ end
152
+
153
+ def add_char_to_packet(char)
154
+
155
+ case @state
156
+ when :neutral
157
+ if char == ENQ # establishment
158
+ send_packet(ACK.chr, :raw => true)
159
+ @state = :transfer_begin
160
+ elsif char == ACK
161
+ get_packet(ACK.chr)
162
+ elsif char == NACK
163
+ get_packet(NACK.chr)
164
+ else
165
+ # receive and ignore partial data
166
+ end
167
+ when :transfer_begin
168
+ if char == STX
169
+ # begin to receive a packet
170
+ @packet_buffer = ""
171
+ @state = :packet_data
172
+ elsif char == EOT
173
+ @state = :neutral
174
+ else
175
+ raise "unexpected data after ENQ, expected STX, got #{char.to_i}"
176
+ end
177
+ when :packet_data
178
+ if char == ETX
179
+ @state = :packet_checksum_1
180
+ else
181
+ @packet_buffer << char
182
+ end
183
+ when :packet_checksum_1
184
+ @checksum_str = "" << char
185
+ @state = :packet_checksum_2
186
+ when :packet_checksum_2
187
+ @checksum_str << char
188
+ @state = :packet_cr
189
+ when :packet_cr
190
+ if char == CR
191
+ @state = :packet_lf
192
+ else
193
+ raise "unexpected data after checksum, expected CR, got #{char.to_i}"
194
+ end
195
+ when :packet_lf
196
+ if char == LF
197
+ if self.class.checksum_valid?(@packet_buffer, @checksum_str.to_i(16))
198
+ send_packet(ACK.chr, :raw => true)
199
+ # cut the sequence ID
200
+ get_packet(@packet_buffer[1..-1])
201
+ else
202
+ send_packet(NACK.chr, :raw => true)
203
+ end
204
+ @state = :transfer_begin
205
+ else
206
+ raise "unexpected data after checksum, expected LF, got #{char.to_i}"
207
+ end
208
+ end
209
+ end
210
+
211
+ def checksum(data)
212
+ val = data.to_enum(:each_byte).inject(0) { |sum,b| sum+b } % 0x100
213
+ val.to_s(16).rjust(2,'0')
214
+ end
215
+
216
+ def send_packet(data, options = {})
217
+ warn "--> #{data.inspect}"
218
+ if options[:raw]
219
+ if data == ENQ.chr
220
+ @frame_number = 1 # reset frame number on new transmission
221
+ end
222
+ @send_callback.call(data)
223
+ else
224
+ @frame_number ||= 1
225
+
226
+ data = "" << @frame_number.to_s << data
227
+ packet_str = "" << "\x02" << data << "\r" << "\x03" << checksum("" << data << "\r\x03") << "\r\n"
228
+ p "--~> #{packet_str.inspect}"
229
+ @send_callback.call(packet_str)
230
+
231
+ @frame_number = (@frame_number + 1) % 8
232
+ end
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,302 @@
1
+ require 'date'
2
+ require 'serial_interface'
3
+
4
+ module Diasorin
5
+ module Liaison
6
+ module Packets
7
+ class LiaisonPacket < SerialPacket
8
+ FieldDelimiter = '|'
9
+ RepeatDelimiter = '\\'
10
+ ComponentDelimiter = '^'
11
+ EscapeDelimiter = "&"
12
+
13
+ header_format "A"
14
+
15
+ def sequence_number=(val)
16
+ @sequence_number = val.to_i
17
+ end
18
+
19
+ def to_str
20
+ to_s
21
+ end
22
+ end
23
+
24
+ class Acknowledge < LiaisonPacket
25
+ def self.matches?(str)
26
+ str == "\x06" ? true : false
27
+ end
28
+ def to_s; "\x06"; end
29
+ end
30
+
31
+ class NotAcknowledge < LiaisonPacket
32
+ def self.matches?(str)
33
+ str == "\x15" ? true : false
34
+ end
35
+ def to_s; "\x15"; end
36
+ end
37
+
38
+ class MessageHeaderRecord < LiaisonPacket
39
+ header_format "A"
40
+ header_filter "H"
41
+ header ["H"]
42
+
43
+ attr_reader :sender_id
44
+ attr_reader :receiver_id
45
+ attr_reader :timestamp
46
+
47
+ def initialize(sender_id, receiver_id)
48
+ @sender_id = sender_id
49
+ @receiver_id = receiver_id
50
+ end
51
+
52
+ def to_s
53
+ arr = []
54
+ arr[1] = "H"
55
+ arr[2] = "\\^&"
56
+ arr[5] = @sender_id
57
+ arr[10] = @receiver_id
58
+ arr.shift
59
+
60
+ arr.join("|")
61
+ end
62
+
63
+ def initialize_from_packet(str)
64
+ records = str.split(FieldDelimiter)
65
+
66
+ @sender_id = records[4]
67
+ @receiver_id = records[9]
68
+ @timestamp = DateTime.new(*records[13].scan(/(....)(..)(..)(..)(..)(..)/)[0].map {|x| x.to_i })
69
+
70
+ @version = records[12]
71
+ raise "Version is not supported" unless @version == '1'
72
+
73
+ end
74
+ end
75
+
76
+ class MessageTerminatorRecord < LiaisonPacket
77
+ header_format "A"
78
+ header_filter "L"
79
+ header ["L"]
80
+
81
+ attr_reader :sequence_number
82
+ attr_reader :termination_code
83
+
84
+ def initialize_from_packet(str)
85
+ records = str.split(FieldDelimiter)
86
+ _, self.sequence_number, @termination_code = records
87
+ end
88
+
89
+ def initialize(sequence_number, termination_code = "N")
90
+ @sequence_number = sequence_number
91
+ @termination_code = termination_code
92
+ end
93
+
94
+ def to_s
95
+ arr = []
96
+ arr[1] = "L"
97
+ arr[2] = @sequence_number
98
+ arr[3] = @termination_code
99
+ arr.shift
100
+
101
+ arr.join("|")
102
+ end
103
+ end
104
+
105
+ class PatientInformationRecord < LiaisonPacket
106
+ header_format "A"
107
+ header_filter "P"
108
+ header ["P"]
109
+
110
+ attr_reader :sequence_number
111
+ attr_reader :patient_id
112
+ attr_reader :patient_last_name
113
+ attr_reader :patient_first_name
114
+ attr_reader :birthdate
115
+ attr_reader :sex
116
+ attr_reader :physician
117
+
118
+ def patient_name=(val)
119
+ @patient_last_name, @patient_first_name = val.split(ComponentDelimiter)
120
+ end
121
+ def birthdate=(val)
122
+ @birthdate = case val
123
+ when String
124
+ DateTime.new(*val.scan(/(....)(..)(..)/)[0].map {|x| x.to_i }) rescue nil
125
+ end
126
+ end
127
+
128
+ def initialize(sequence_nr, patient_id, lastname = "", firstname = "")
129
+ @sequence_number = sequence_nr.to_i
130
+ @patient_id = patient_id
131
+ @patient_last_name, @patient_first_name = lastname, firstname
132
+ end
133
+
134
+ def to_s
135
+ arr = []
136
+ arr[1] = "P"
137
+ arr[2] = @sequence_number
138
+ arr[4] = @patient_id.to_s
139
+ arr[6] = [@patient_last_name, @patient_first_name].join("^")
140
+ arr.shift
141
+
142
+ arr.join("|")
143
+ end
144
+
145
+ def initialize_from_packet(str)
146
+ records = str.split(FieldDelimiter)
147
+
148
+ _1,
149
+ self.sequence_number,
150
+ _3,
151
+ @patient_id,
152
+ _5,
153
+ self.patient_name,
154
+ _7,
155
+ self.birthdate,
156
+ @sex,
157
+ _10,
158
+ _11,
159
+ _12,
160
+ _13,
161
+ @physician,
162
+ _rest = records
163
+ end
164
+ end
165
+
166
+ class RequestInformationSegment < LiaisonPacket
167
+ header_format "A"
168
+ header_filter "Q"
169
+ header ["Q"]
170
+
171
+ attr_reader :sequence_number
172
+ attr_reader :starting_range
173
+
174
+ def initialize_from_packet(str)
175
+ records = str.split(FieldDelimiter)
176
+
177
+ _1, self.sequence_number, @starting_range, _rest = records
178
+ end
179
+ end
180
+
181
+ class TestOrderRecord < LiaisonPacket
182
+ header_format "A"
183
+ header_filter "O"
184
+ header ["O"]
185
+
186
+ attr_reader :sequence_number
187
+ attr_reader :specimen_id
188
+ attr_reader :test_id
189
+ attr_reader :dilution
190
+ attr_reader :priority
191
+ attr_reader :timestamp
192
+ attr_reader :record_type
193
+
194
+ def sequence_number=(val)
195
+ @sequence_number = val.to_i
196
+ end
197
+ def test_order=(data)
198
+ _1, _2, _3, @test_id, @dilution = data.split(ComponentDelimiter)
199
+ end
200
+ def timestamp=(val)
201
+ @timestamp = case val
202
+ when String
203
+ DateTime.new(*val.scan(/(....)(..)(..)/)[0].map {|x| x.to_i }) rescue nil
204
+ end
205
+ end
206
+ def initialize_from_packet(str)
207
+ records = str.split(FieldDelimiter)
208
+
209
+ _1,
210
+ self.sequence_number,
211
+ @specimen_id,
212
+ _4,
213
+ self.test_order,
214
+ @priority,
215
+ self.timestamp, _, _, _10, _, _, _, _, _, _, _, _, _, _20, _, _, _, _, _, @record_type,
216
+ _rest = records
217
+ end
218
+
219
+ def initialize(sequence_number, specimen_id, test_id)
220
+ @sequence_number = sequence_number
221
+ @specimen_id = specimen_id
222
+ @test_id = test_id
223
+ end
224
+
225
+
226
+ def to_s
227
+ arr = []
228
+ arr[1] = "O"
229
+ arr[2] = @sequence_number
230
+ arr[3] = @specimen_id
231
+ arr[5] = [nil,nil,nil,@test_id].join("^")
232
+ arr.shift
233
+
234
+ arr.join("|")
235
+ end
236
+ end
237
+
238
+ class ResultRecord < LiaisonPacket
239
+ header_format "A"
240
+ header_filter "R"
241
+ header ["R"]
242
+
243
+ attr_reader :sequence_number
244
+ attr_reader :test_id
245
+ attr_reader :value
246
+ attr_reader :unit
247
+ attr_reader :abnormal_flags
248
+ attr_reader :result_status
249
+ attr_reader :timestamp
250
+ attr_reader :instrument
251
+
252
+ def test_result=(data)
253
+ _1, _2, _3, @test_id = data.split(ComponentDelimiter)
254
+ end
255
+ def timestamp=(val)
256
+ @timestamp = case val
257
+ when String
258
+ DateTime.new(*val.scan(/(....)(..)(..)(..)(..)(..)/)[0].map {|x| x.to_i }) rescue nil
259
+ end
260
+ end
261
+
262
+ def initialize_from_packet(str)
263
+ records = str.split(FieldDelimiter)
264
+
265
+ _1,
266
+ self.sequence_number,
267
+ self.test_result,
268
+ @value,
269
+ @unit,
270
+ _6,
271
+ @abnormal_flags,
272
+ _7,
273
+ @result_status,
274
+ _10, _11, _12,
275
+ self.timestamp,
276
+ @instrument,
277
+ rest = records
278
+ end
279
+ end
280
+
281
+ class CommentRecord < LiaisonPacket
282
+ header_format "A"
283
+ header_filter "C"
284
+ header ["C"]
285
+
286
+ attr_reader :comment
287
+
288
+ def initialize_from_packet(str)
289
+ records = str.split(FieldDelimiter)
290
+
291
+ _1,
292
+ self.sequence_number,
293
+ _3,
294
+ @comment,
295
+ rest = records
296
+ end
297
+ end
298
+
299
+
300
+ end
301
+ end
302
+ end
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'net/http'
5
+ require 'yaml'
6
+
7
+ require 'rubygems'
8
+
9
+ require File.join(File.dirname(__FILE__),'liaison_labor.rb')
10
+
11
+ class Hash
12
+ def symbolize_keys
13
+ inject({}) do |options, (key, value)|
14
+ options[(key.to_sym rescue key) || key] = value
15
+ end
16
+ end
17
+
18
+ def stringify_keys
19
+ inject({}) do |options, (key, value)|
20
+ options[key.to_s] = value
21
+ options
22
+ end
23
+ end
24
+
25
+ end
26
+
27
+
28
+ module Diasorin
29
+ ConfigFilename = ".liaison_server"
30
+ ConfigFile = File.join(ENV['HOME'] || ENV['APPDATA'], ConfigFilename)
31
+
32
+ # defaults are used when they are not overwritten in a config file
33
+ # or with command line options
34
+ #
35
+ DefaultConfig = {
36
+ :serial_port => "/dev/ttyUSB0",
37
+ :baudrate => 9600,
38
+ :uri => "http://localhost/liaison/"
39
+ }
40
+
41
+ class LiaisonServer
42
+
43
+ # load configuration from configfile, merge with
44
+ # default options
45
+ #
46
+ def load_configuration
47
+ # try to read configuration from file
48
+ @options_from_file = YAML.load_file(ConfigFile) || {} rescue {}
49
+ @options_from_file = @options_from_file.symbolize_keys
50
+
51
+ # if an option is not given on the command line
52
+ # it is taken from the config file, or the default is used
53
+ @options = Hash.new() { |h,k| @options_from_file[k] || DefaultConfig[k] }
54
+ end
55
+
56
+ # parse command line options
57
+ #
58
+ def initialize
59
+ load_configuration
60
+
61
+ @opts = OptionParser.new do |opts|
62
+
63
+ opts.separator ""
64
+ opts.separator "liaison_labor is an interface between the Liaison device connected via "
65
+ opts.separator "serial port, and a worklist_manager HTTP-server"
66
+ opts.separator ""
67
+ opts.separator "configuration can be given in '#{ConfigFile}' "
68
+ opts.separator ""
69
+
70
+ opts.on "-V","--version","Display version and exit" do
71
+ puts "#{self.class} #{::LiaisonLabor::VERSION}"
72
+ exit
73
+ end
74
+ opts.on "-s", "--serial-port DEVICE", "serial port, default is /dev/ttyUSB0" do |arg|
75
+ @options[:serial_port] = arg
76
+ end
77
+ opts.on "--baudrate BAUDRATE", "baudrate of the serial connection, default is 9600" do |arg|
78
+ @options[:baudrate] = arg.to_i
79
+ end
80
+ opts.on "-u", "--uri URI", "URI of the HTTP-Endpoint where data is read or posted" do |arg|
81
+ @options[:endpoint] = arg
82
+ end
83
+ opts.on_tail "-p", "--print-config", "Print the current configuration",
84
+ "in a format that can be used as a configuration file" do
85
+ puts @options_from_file.merge(@options).stringify_keys.to_yaml
86
+ exit
87
+ end
88
+ end
89
+ end
90
+
91
+
92
+ # open the serial port for reading and writing
93
+ #
94
+ # make sure that output buffering is disabled so that data
95
+ # is sent immediately
96
+ #
97
+ def open_port(portfile, speed)
98
+ system("stty -echo raw ospeed #{speed} ispeed #{speed} < #{portfile}")
99
+ port = File.open(portfile, "w+")
100
+ port.sync = true
101
+ port
102
+ end
103
+
104
+ def communicate!
105
+ port = open_port(@options[:serial_port], @options[:baudrate])
106
+
107
+ client = Liaison::Interface.new(port) do |liaison|
108
+
109
+ # yields the barcode of a sample
110
+ #
111
+ # expects a hash consisting of patient information and a list
112
+ # of requests for this patient
113
+ #
114
+ liaison.on_order_request do |barcode|
115
+ begin
116
+ data = YAML.load(::Net::HTTP.get( URI.join(@options[:endpoint],"find_requests/",barcode) ))
117
+ rescue Exception => e
118
+ puts e
119
+ puts e.backtrace
120
+ data = nil
121
+ end
122
+ p data
123
+ data
124
+ end
125
+
126
+ liaison.on_result do |patient, order, result, comment|
127
+ if comment
128
+ comment_str = comment.comment
129
+ else
130
+ comment_str = nil
131
+ end
132
+
133
+ barcode = order.specimen_id
134
+ data = {
135
+ "test_name" => order.test_id,
136
+ "value" => result.value,
137
+ "unit" => result.unit,
138
+ "status" => result.result_status,
139
+ "flags" => result.abnormal_flags,
140
+ "comment" => comment_str,
141
+ "result_timestamp" => result.timestamp
142
+ }
143
+
144
+ ::Net::HTTP.post_form(URI.join(@options[:endpoint], "result/", "#{URI.encode(barcode)}"), data.to_hash )
145
+ end
146
+ end
147
+
148
+ client.run
149
+
150
+ puts "Listening on #{File.expand_path(port.path)}"
151
+
152
+ begin
153
+ yield client if block_given?
154
+ client.join
155
+ rescue Interrupt => e
156
+ puts "exiting"
157
+ raise
158
+ end
159
+ end
160
+
161
+ # run the application
162
+ #
163
+ def run!(args = ARGV)
164
+ @opts.parse!(args)
165
+
166
+ communicate!
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,43 @@
1
+ require 'lib/liaison_labor/interface.rb'
2
+
3
+ require 'stringio'
4
+
5
+ include Diasorin::Liaison
6
+
7
+ context "" do
8
+ setup do
9
+ @to_host = StringIO.new
10
+ @from_host = StringIO.new
11
+
12
+ @host = PacketIO.new(LiaisonProtocol, @to_host, @from_host)
13
+ @host.run
14
+ end
15
+
16
+ def transmit_string(str)
17
+ @to_host << str
18
+ @to_host.rewind
19
+ 30.times { Thread.pass }
20
+ @from_host.rewind
21
+ end
22
+
23
+ specify "ENQ should be acknowledged" do
24
+ transmit_string "\x05"
25
+ response = @from_host.read
26
+ response.should == "\x06"
27
+ end
28
+
29
+ specify "Valid packet should be acknowledged" do
30
+ s = "\x05\x026L|1|N\x0d\x0309\x0d\x0a"
31
+ transmit_string s
32
+ response = @from_host.read
33
+ response.should == "\x06\x06"
34
+ end
35
+
36
+ specify "another valid packet" do
37
+ s = "\005\0023R|1|^^^TSH|0.182|mIU/l||L||F||||20070510161808|Liaison\r\003E2\r\n"
38
+ lambda {
39
+ transmit_string s
40
+ response = @from_host.read
41
+ }.should_not raise_error
42
+ end
43
+ end
@@ -0,0 +1,234 @@
1
+ require 'lib/liaison_labor/packets.rb'
2
+
3
+ include Diasorin
4
+
5
+
6
+ def has_header(packet, char)
7
+ specify "should be matched by packets stating with #{char}" do
8
+ instance_variable_get("@#{packet}").matches?("#{char}...").should be_true
9
+ end
10
+
11
+ specify "different headers should not match" do
12
+ instance_variable_get("@#{packet}").matches?("random garbage").should be_false
13
+ end
14
+ end
15
+
16
+ def has_example(packet, example_str)
17
+ specify "example should parse without errors" do
18
+ lambda {
19
+ instance_variable_get("@#{packet}").from_str(example_str)
20
+ }.should_not raise_error
21
+ end
22
+ end
23
+
24
+ context "checksum for packets" do
25
+ setup do
26
+ @full_packet = "\0021O|1|00000023||^^^FT4^|||||||||||||||||||||F\r\003EE\r\n"
27
+ end
28
+
29
+ specify "data should have a correct checksum" do
30
+ LiaisonProtocol.checksum_valid?("1O|1|00000023||^^^FT4^|||||||||||||||||||||F\r","EE".to_i(16)).should be_true
31
+ end
32
+
33
+ end
34
+
35
+
36
+ context "The MessageHeaderRecord packet from/to Liaison" do
37
+ setup do
38
+ @packet = Liaison::Packets::MessageHeaderRecord
39
+ @str = "H|\^&|||LaborEDV|||||Liaison|||1|19971113154903"
40
+ end
41
+
42
+ has_header :packet, "H"
43
+
44
+ specify "should have a sender_id" do
45
+ @packet.from_str(@str).sender_id.should == "LaborEDV"
46
+ end
47
+
48
+ specify "should have a receiver_id" do
49
+ @packet.from_str(@str).receiver_id.should == "Liaison"
50
+ end
51
+
52
+ specify "should have a timestamp" do
53
+ @packet.from_str(@str).timestamp.should == DateTime.new(1997,11,13,15,49,03)
54
+ end
55
+ end
56
+
57
+ context "Building a MessageHeaderRecord" do
58
+ setup do
59
+ @header = Liaison::Packets::MessageHeaderRecord
60
+ @packet = @header.new("Sender","Receiver")
61
+ end
62
+
63
+ specify "should build a correct packet" do
64
+ @packet.to_s.should == "H|\\^&|||Sender|||||Receiver"
65
+ end
66
+ end
67
+
68
+ context "the MessageTerminatorRecord packet to Liaison" do
69
+ setup do
70
+ @terminator_record = Liaison::Packets::MessageTerminatorRecord
71
+ @str = "L|1|N"
72
+ @packet = @terminator_record.from_str(@str)
73
+ end
74
+
75
+ has_header :terminator_record, "L"
76
+
77
+ specify "should have sequence number" do
78
+ @packet.sequence_number.should == 1
79
+ end
80
+ specify "should have termination code" do
81
+ @packet.termination_code.should == "N"
82
+ end
83
+ end
84
+
85
+ context "the PatientInformationRecord from/to Liaison" do
86
+ setup do
87
+ @packet = Liaison::Packets::PatientInformationRecord
88
+ @example = "P|1||PatID01||Meyer^Anna||19741001|F|||||MARTINEZ"
89
+ end
90
+
91
+ has_header :packet, "P"
92
+
93
+ specify "should have a sequence number" do
94
+ @packet.from_str(@example).sequence_number.should == 1
95
+ end
96
+
97
+ specify "should have a patient ID" do
98
+ @packet.from_str(@example).patient_id.should == "PatID01"
99
+ end
100
+ specify "should have a patient name" do
101
+ @packet.from_str(@example).patient_last_name.should == "Meyer"
102
+ @packet.from_str(@example).patient_first_name.should == "Anna"
103
+ end
104
+ specify "should have a birthday" do
105
+ @packet.from_str(@example).birthdate.should == Date.new(1974,10,01)
106
+ end
107
+ specify "should have sex" do
108
+ @packet.from_str(@example).sex.should == "F"
109
+ end
110
+ specify "should have attending physician" do
111
+ @packet.from_str(@example).physician.should == "MARTINEZ"
112
+ end
113
+ end
114
+
115
+ context "building a PatientInformationRecord" do
116
+ setup do
117
+ @record = Liaison::Packets::PatientInformationRecord
118
+ @packet = @record.new(1, "000204060", "Sierra", "Rudolph")
119
+ end
120
+
121
+ specify "should just work" do
122
+ @packet.to_s.should == "P|1||000204060||Sierra^Rudolph"
123
+ end
124
+ end
125
+
126
+ context "the RequestInformationSegment" do
127
+ setup do
128
+ @request_information = Liaison::Packets::RequestInformationSegment
129
+ @str = "Q|1|Sample01||ALL||||||||O"
130
+ @packet = @request_information.from_str(@str)
131
+ end
132
+
133
+ has_header :request_information, "Q"
134
+
135
+ specify "should have a sequence number" do
136
+ @packet.sequence_number.should == 1
137
+ end
138
+
139
+ specify "should have starting range id" do
140
+ @packet.starting_range.should == "Sample01"
141
+ end
142
+ end
143
+
144
+ context "the TestOrderRecord from/to Liaison" do
145
+ setup do
146
+ @order_record = Liaison::Packets::TestOrderRecord
147
+ @example = "O|1|SampleID01||^^^AFP^1:10|N|19980506|||||||||S||||||||||X"
148
+ @packet = @order_record.from_str(@example)
149
+ end
150
+
151
+ has_header :order_record, "O"
152
+
153
+ specify "should have a sequence number" do
154
+ @packet.sequence_number.should == 1
155
+ end
156
+ specify "should have a specimen id" do
157
+ @packet.specimen_id.should == "SampleID01"
158
+ end
159
+ specify "should have a universal test id" do
160
+ @packet.test_id.should == "AFP"
161
+ end
162
+ specify "should have a dilution" do
163
+ @packet.dilution.should == "1:10"
164
+ end
165
+ specify "should have a priority" do
166
+ @packet.priority.should == "N"
167
+ end
168
+ specify "should have timestamp" do
169
+ @packet.timestamp.should == DateTime.new(1998,5,6,0,0,0)
170
+ end
171
+ specify "should have report type" do
172
+ @packet.record_type.should == "X"
173
+ end
174
+ end
175
+
176
+ context "building a TestOrderRecord" do
177
+ setup do
178
+ @order_record = Liaison::Packets::TestOrderRecord
179
+ end
180
+
181
+ specify "should generate a correct packet" do
182
+ @packet = @order_record.new(17, "BarcodeID", "TSH")
183
+ @packet.to_s.should == "O|17|BarcodeID||^^^TSH"
184
+ end
185
+ end
186
+
187
+ context "the ResultRecord" do
188
+ setup do
189
+ @result_record = Liaison::Packets::ResultRecord
190
+ @example = "R|1|^^^AFP|0.20|IU/ml||<||F||||19980506123145|Liaison"
191
+ @packet = @result_record.from_str(@example)
192
+ end
193
+
194
+ has_header :result_record, "R"
195
+
196
+ specify "should have sequence number" do
197
+ @packet.sequence_number.should == 1
198
+ end
199
+ specify "should have test id" do
200
+ @packet.test_id.should == "AFP"
201
+ end
202
+ specify "should have value" do
203
+ @packet.value.should == "0.20"
204
+ end
205
+ specify "should have unit" do
206
+ @packet.unit.should == "IU/ml"
207
+ end
208
+ specify "should have abnormal flags" do
209
+ @packet.abnormal_flags.should == "<"
210
+ end
211
+ specify "should have result status" do
212
+ @packet.result_status.should == "F"
213
+ end
214
+ specify "should have timestamp" do
215
+ @packet.timestamp.should == DateTime.new(1998,05,06,12,31,45)
216
+ end
217
+ specify "should have instrument" do
218
+ @packet.instrument.should == "Liaison"
219
+ end
220
+
221
+ end
222
+
223
+ # TODO: empty result data should be allowed
224
+
225
+ context "the CommentRecord" do
226
+ setup do
227
+ @packet = Liaison::Packets::CommentRecord
228
+ end
229
+ has_header :packet, "C"
230
+ end
231
+
232
+
233
+
234
+
@@ -0,0 +1,43 @@
1
+
2
+ module Diasorin
3
+ module Liaison
4
+ class MockInterface < PacketIO
5
+ def initialize(read, write, options = {})
6
+ super(SerialProtocol, read, write, options)
7
+
8
+ add_sender( {
9
+ :init => Packets::MessageHeaderRecord,
10
+ :next_patient => Packets::NextPatient,
11
+ :result => Packets::Result,
12
+ :end => Packets::EndOfData
13
+ } )
14
+
15
+ add_receiver :end => Packets::EndOfData do |d|
16
+ self.end_of_list
17
+ end
18
+
19
+ add_receiver :next_result => Packets::NextResult do |d|
20
+ end
21
+
22
+ run
23
+ end
24
+
25
+ def init
26
+ send_packet :init
27
+ end
28
+
29
+ def result(pat_with_results)
30
+
31
+ end
32
+
33
+ def next_patient nr
34
+ send_packet :next_patient, nr
35
+ end
36
+
37
+ def end_of_list
38
+ send_packet :end
39
+ end
40
+
41
+ end
42
+ end
43
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: levinalex-LiaisonLabor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.7
5
+ platform: ruby
6
+ authors:
7
+ - Levin Alexander
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-06-11 00:00:00 -07:00
13
+ default_executable: liaison_server
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hoe
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.12.1
24
+ version:
25
+ description: "An interface to the LIAISON\xC2\xAE analyser by DiaSorin <http://www.diasorin.com/en/productsandsystems/liaison>"
26
+ email:
27
+ - mail@levinalex.net
28
+ executables:
29
+ - liaison_server
30
+ extensions: []
31
+
32
+ extra_rdoc_files:
33
+ - History.txt
34
+ - Manifest.txt
35
+ - README.txt
36
+ files:
37
+ - History.txt
38
+ - Manifest.txt
39
+ - README.txt
40
+ - Rakefile
41
+ - bin/liaison_server
42
+ - liaison_labor.gemspec
43
+ - lib/liaison_labor.rb
44
+ - lib/liaison_labor/interface.rb
45
+ - lib/liaison_labor/packets.rb
46
+ - lib/liaison_server.rb
47
+ - spec/liaison_interface_spec.rb
48
+ - spec/liaison_packet_spec.rb
49
+ - spec/mock/mock_liaison.rb
50
+ has_rdoc: true
51
+ homepage: http://levinalex.net/src/liaison
52
+ post_install_message:
53
+ rdoc_options:
54
+ - --main
55
+ - README.txt
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: "0"
63
+ version:
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: "0"
69
+ version:
70
+ requirements: []
71
+
72
+ rubyforge_project: liaison_labor
73
+ rubygems_version: 1.2.0
74
+ signing_key:
75
+ specification_version: 2
76
+ summary: interfaces Liaison device to worklist_manager
77
+ test_files: []
78
+