levinalex-LiaisonLabor 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+