ruby_astm 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9583ca630b54cc83f9bffdb91b5094b7cc01145b
4
+ data.tar.gz: 7695f9fe20e298629f5eb5af67d8ea8f5c345616
5
+ SHA512:
6
+ metadata.gz: ec25812e078a96cf28ecd54ed6cf2325ef84ce46e15087d49b55341ca9a9be8542e5848bc493933dd33fd856c2c5a958a9855e0ca7158d4cd579a439d604760d
7
+ data.tar.gz: 210f8f1ff0d433b688af770419592f4966dedac076499a0913193cd948ee71f7a545b185e007206ced54c3bb5642f780d5f53ead36c06e8575727cfbd0d1e631
@@ -0,0 +1,5 @@
1
+ class Adapter
2
+
3
+
4
+
5
+ end
@@ -0,0 +1 @@
1
+ {"installed":{"client_id":"555057029489-sel4u1t4tnv3qicdhsssp9phn3r8tvrj.apps.googleusercontent.com","project_id":"project-id-4907840994004404941","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://www.googleapis.com/oauth2/v3/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"L6DjrWGteAcwJwh7YdfzYwDp","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}
@@ -0,0 +1,135 @@
1
+ require 'google/apis/script_v1'
2
+ require 'googleauth'
3
+ require 'googleauth/stores/file_token_store'
4
+ require 'fileutils'
5
+ require 'publisher/poller'
6
+
7
+ class Google_Lab_Interface < Poller
8
+
9
+
10
+ OOB_URI = 'urn:ietf:wg:oauth:2.0:oob'.freeze
11
+ APPLICATION_NAME = 'Google Apps Script API Ruby Quickstart'.freeze
12
+ CREDENTIALS_PATH = 'credentials.json'.freeze
13
+ # The file token.yaml stores the user's access and refresh tokens, and is
14
+ # created automatically when the authorization flow completes for the first
15
+ # time.
16
+ TOKEN_PATH = 'token.yaml'.freeze
17
+ SCOPE = 'https://www.googleapis.com/auth/script.projects'.freeze
18
+
19
+ SCOPES = ["https://www.googleapis.com/auth/documents","https://www.googleapis.com/auth/drive","https://www.googleapis.com/auth/script.projects","https://www.googleapis.com/auth/spreadsheets"]
20
+
21
+ $service = nil
22
+ SCRIPT_ID = "M7JDg7zmo0Xldo4RTWFGCsI2yotVzKYhk"
23
+
24
+
25
+ ##
26
+ # Ensure valid credentials, either by restoring from the saved credentials
27
+ # files or intitiating an OAuth2 authorization. If authorization is required,
28
+ # the user's default browser will be launched to approve the request.
29
+ #
30
+ # @return [Google::Auth::UserRefreshCredentials] OAuth2 credentials
31
+ def authorize
32
+
33
+ client_id = Google::Auth::ClientId.from_file(root_path + "/publisher/" + CREDENTIALS_PATH)
34
+ token_store = Google::Auth::Stores::FileTokenStore.new(file: (root_path + "/publisher/" + TOKEN_PATH))
35
+ authorizer = Google::Auth::UserAuthorizer.new(client_id, SCOPES, token_store)
36
+ user_id = 'default'
37
+ credentials = authorizer.get_credentials(user_id)
38
+ if credentials.nil?
39
+ url = authorizer.get_authorization_url(base_url: OOB_URI)
40
+ puts 'Open the following URL in the browser and enter the ' \
41
+ "resulting code after authorization:\n" + url
42
+ code = gets
43
+ credentials = authorizer.get_and_store_credentials_from_code(
44
+ user_id: user_id, code: code, base_url: OOB_URI
45
+ )
46
+ end
47
+ credentials
48
+ end
49
+
50
+ ## @param[String] mpg : path to mappings file. Defaults to nil.
51
+ def initialize(mpg=nil)
52
+ AstmServer.log("Initialized Google Lab Interface")
53
+ super(mpg)
54
+ $service = Google::Apis::ScriptV1::ScriptService.new
55
+ $service.client_options.application_name = APPLICATION_NAME
56
+ $service.authorization = authorize
57
+ end
58
+
59
+
60
+ def poll_LIS_for_requisition
61
+
62
+ pre_poll_LIS
63
+
64
+ AstmServer.log("polling LIS for new requisitions")
65
+
66
+ epoch = (Time.now - 5.days).to_i*1000
67
+
68
+ pp = {
69
+ :input => JSON.generate([epoch])
70
+ }
71
+
72
+ request = Google::Apis::ScriptV1::ExecutionRequest.new(
73
+ function: 'get_latest_test_information',
74
+ parameters: pp
75
+ )
76
+
77
+ begin
78
+ resp = $service.run_script(SCRIPT_ID, request)
79
+ if resp.error
80
+ AstmServer.log("Response Error polling LIS for requisitions: #{resp.error.message}: #{resp.error.code}")
81
+ else
82
+ process_LIS_response(resp.response["result"])
83
+ AstmServer.log("Successfully polled lis for requisitions: #{resp.response}")
84
+ end
85
+ rescue => e
86
+ AstmServer.log("Rescue Error polling LIS for requisitions: #{e.to_s}")
87
+ AstmServer.log("Error backtrace")
88
+ AstmServer.log(e.backtrace.to_s)
89
+ ensure
90
+ post_poll_LIS
91
+ end
92
+
93
+ end
94
+
95
+
96
+ # method overriden from adapter.
97
+ # data should be an array of objects.
98
+ # see adapter for the recommended structure.
99
+ def update(data)
100
+
101
+ orders = JSON.generate(data)
102
+
103
+ pp = {
104
+ :input => orders
105
+ }
106
+
107
+ request = Google::Apis::ScriptV1::ExecutionRequest.new(
108
+ function: 'update_report',
109
+ parameters: pp
110
+ )
111
+
112
+ begin
113
+ AstmServer.log("updating following results to LIS")
114
+ AstmServer.log(request.parameters.to_s)
115
+ resp = $service.run_script(SCRIPT_ID, request)
116
+ if resp.error
117
+ AstmServer.log("Error updating results to LIS, message follows")
118
+ AstmServer.log("error: #{resp.error.message} : code: #{resp.error.code}")
119
+ #puts "there was an error."
120
+ else
121
+ AstmServer.log("Updating results to LIS successfull")
122
+ end
123
+ rescue => e
124
+ AstmServer.log("Error updating results to LIS, backtrace follows")
125
+ AstmServer.log(e.backtrace.to_s)
126
+ end
127
+
128
+ end
129
+
130
+
131
+ def poll
132
+ super
133
+ end
134
+
135
+ end
@@ -0,0 +1,339 @@
1
+ require 'rufus-scheduler'
2
+ require 'time'
3
+ require 'redis'
4
+
5
+ class Poller
6
+ #############################################################################3
7
+ ##
8
+ ##
9
+ ## require the file that implements the Adapter class.
10
+ ##
11
+ ##
12
+ ##############################################################################
13
+ EDTA = "EDTA"
14
+ SERUM = "SERUM"
15
+ PLASMA = "PLASMA"
16
+ FLUORIDE = "FLUORIDE"
17
+ ESR = "ESR"
18
+ URINE = "URINE"
19
+ REQUISITIONS_SORTED_SET = "requisitions_sorted_set"
20
+ REQUISITIONS_HASH = "requisitions_hash"
21
+ POLL_STATUS_KEY = "ruby_astm_lis_poller"
22
+ LAST_REQUEST_AT = "last_request_at"
23
+ LAST_REQUEST_STATUS = "last_request_status"
24
+ RUNNING = "running"
25
+ COMPLETED = "completed"
26
+
27
+ def initialize(mpg=nil)
28
+ $redis = Redis.new
29
+ ## this mapping is from MACHINE CODE AS THE KEY
30
+ $mappings = JSON.parse(IO.read(mpg || ("mappings.json")))
31
+ ## INVERTING THE MAPPINGS, GIVES US THE LIS CODE AS THE KEY.
32
+ $inverted_mappings = Hash[$mappings.values.map{|c| c = c["LIS_CODE"]}.zip($mappings.keys)]
33
+ end
34
+
35
+ def root_path
36
+ File.dirname __dir__
37
+ end
38
+
39
+
40
+ def prepare_redis
41
+ if ($redis.exists "processing") == 0
42
+ $redis.lpush("processing",JSON.generate([]))
43
+ end
44
+ end
45
+
46
+ def pre_poll_LIS
47
+ previous_requisition_request_status = nil
48
+
49
+ if previous_requisition_request_status = $redis.get(POLL_STATUS_KEY)
50
+
51
+ last_request_at = previous_requisition_request_status[LAST_REQUEST_AT]
52
+
53
+ last_request_status = previous_requisition_request_status[LAST_REQUEST_STATUS]
54
+ end
55
+
56
+ running_time = Time.now.to_i
57
+
58
+ $redis.watch(POLL_STATUS_KEY) do
59
+
60
+ if $redis.get(POLL_STATUS_KEY) == previous_requisition_request_status
61
+ if ((last_request_status != RUNNING) || ((Time.now.to_i - last_request_at) > 600))
62
+ $redis.multi do |multi|
63
+ multi.set(POLL_STATUS_KEY, JSON.generate({LAST_REQUEST_STATUS => RUNNING, LAST_REQUEST_AT => running_time}))
64
+ end
65
+ AstmServer.log("pre poll lis status set to running")
66
+ end
67
+ else
68
+ AstmServer.log("pre poll lis status check interrupted by another client, so exiting here")
69
+ $redis.unwatch
70
+ return
71
+ end
72
+ end
73
+ end
74
+
75
+ ## uses redis CAS to ensure that two requests don't overlap.
76
+ ## will update to the requisitions hash the specimen id -> and the
77
+ ## now lets test this.
78
+ ## how to stub it out ?
79
+ ## first we call it direct.
80
+ def post_poll_LIS
81
+
82
+ requisition_status = JSON.parse($redis.get(POLL_STATUS_KEY))
83
+
84
+ if (requisition_status[LAST_REQUEST_STATUS] == RUNNING)
85
+
86
+ $redis.watch(POLL_STATUS_KEY) do
87
+
88
+ if $redis.get(POLL_STATUS_KEY) == JSON.generate(requisition_status)
89
+
90
+ $redis.multi do |multi|
91
+ multi.set(POLL_STATUS_KEY,JSON.generate({LAST_REQUEST_STATUS => COMPLETED, LAST_REQUEST_AT => requisition_status[LAST_REQUEST_AT]}))
92
+ end
93
+ AstmServer.log("post poll LIS status set to completed")
94
+ else
95
+ AstmServer.log("post poll LIS was was interrupted by another client , so exited this thread")
96
+ $redis.unwatch(POLL_STATUS_KEY)
97
+ return
98
+ end
99
+
100
+ end
101
+
102
+ else
103
+ AstmServer.log("post poll LIS was not in running state")
104
+ end
105
+
106
+ end
107
+
108
+ def build_tests_hash(record)
109
+ tests_hash = {}
110
+
111
+ ## key -> TUBE_NAME : eg: EDTA
112
+ ## value -> its barcode id.
113
+ tube_ids = {}
114
+ ## assign.
115
+ ## lavender -> index 28
116
+ ## serum -> index 29
117
+ ## plasm -> index 30
118
+ ## fluoride -> index 31
119
+ ## urine -> index 32
120
+ ## esr -> index 33
121
+ unless record[28].blank?
122
+ tube_ids[EDTA] = record[28]
123
+ tests_hash[EDTA + ":" + record[28]] = []
124
+ end
125
+
126
+ unless record[29].blank?
127
+ tube_ids[SERUM] = record[29]
128
+ tests_hash[SERUM + ":" + record[29]] = []
129
+ end
130
+
131
+ unless record[30].blank?
132
+ tube_ids[PLASMA] = record[30]
133
+ tests_hash[PLASMA + ":" + record[30]] = []
134
+ end
135
+
136
+ unless record[31].blank?
137
+ tube_ids[FLUORIDE] = record[31]
138
+ tests_hash[FLUORIDE + ":" + record[31]] = []
139
+ end
140
+
141
+ unless record[32].blank?
142
+ tube_ids[URINE] = record[32]
143
+ tests_hash[URINE + ":" + record[32]] = []
144
+ end
145
+
146
+ unless record[33].blank?
147
+ tube_ids[ESR] = record[33]
148
+ tests_hash[ESR + ":" + record[33]] = []
149
+ end
150
+
151
+
152
+ tests = record[8].split(",")
153
+ tests.each do |test|
154
+ ## use the inverted mappings to
155
+ if machine_code = $inverted_mappings[test]
156
+ ## now get its tube type
157
+ ## mappings have to match the tubes defined in this file.
158
+ tube = $mappings[machine_code]["TUBE"]
159
+ ## now find the tests_hash which has this tube.
160
+ ## and the machine code to its array.
161
+ ## so how to find this.
162
+ tube_key = tests_hash.keys.select{|c| c=~/#{tube}/ }[0]
163
+ tests_hash[tube_key] << machine_code
164
+ else
165
+ AstmServer.log("ERROR: Test: #{test} does not have an LIS code")
166
+ end
167
+ end
168
+ AstmServer.log("tests hash generated")
169
+ AstmServer.log(JSON.generate(tests_hash))
170
+ tests_hash
171
+ end
172
+
173
+ ## @param[Integer] epoch : the epoch at which these tests were requested.
174
+ ## @param[Hash] tests : {"EDTA:barcode" => [MCV,MCH,MCHC...]}
175
+ def merge_with_requisitions_hash(epoch,tests)
176
+ ## so we basically now add this to the epoch ?
177
+ ## or a sorted set ?
178
+ ## key -> TUBE:specimen_id
179
+ ## value -> array of tests as json
180
+ ## score -> time.
181
+ $redis.multi do |multi|
182
+ $redis.zadd REQUISITIONS_SORTED_SET, epoch, JSON.generate(tests)
183
+ tests.keys.each do |tube_barcode|
184
+ $redis.hset REQUISITIONS_HASH, tube_barcode, JSON.generate(tests[tube_barcode])
185
+ end
186
+ end
187
+ end
188
+
189
+
190
+ ## @param[String] json_response : contains the response from the LIS
191
+ ## it should be the jsonified version of a hash that is structured as follows:
192
+ =begin
193
+ {
194
+ "epoch" => [
195
+ [index_8 : list_of_LIS_TEST_CODES_seperated_by_commas, index 28 => lavender, index 29 => serum, index 30 => plasma, index 31 => fluoride, index 32 => urine, index 33 => esr]
196
+ }
197
+ =end
198
+ ## @return[nil]
199
+ def process_LIS_response(json_response)
200
+ lab_results = JSON.parse(json_response)
201
+ AstmServer.log("requisitions downloaded from LIS")
202
+ AstmServer.log(JSON.generate(lab_results))
203
+ lab_results.keys.each do |epoch|
204
+ merge_with_requisitions_hash(epoch,build_tests_hash(lab_results[epoch][0]))
205
+ end
206
+ end
207
+
208
+ ## override to define how the data is updated.
209
+ def update(data)
210
+
211
+ end
212
+
213
+ ##@param[Array] data : array of objects.
214
+ ##@return[Boolean] true/false : depending on whether it was successfully updated or not.
215
+ ## recommended structure for data.
216
+ =begin
217
+ data = [
218
+ {
219
+ :id => "ARUBA",
220
+ :results => [
221
+ {
222
+ :name => "TLCparam",
223
+ :value => 10
224
+ },
225
+ {
226
+ :name => "Nparam",
227
+ :value => 23
228
+ },
229
+ {
230
+ :name => "ANCparam",
231
+ :value => 25
232
+ },
233
+ {
234
+ :name => "Lparam",
235
+ :value => 10
236
+ },
237
+ {
238
+ :name => "ALCparam",
239
+ :value => 44
240
+ },
241
+ {
242
+ :name => "Mparam",
243
+ :value => 55
244
+ },
245
+ {
246
+ :name => "AMCparam",
247
+ :value => 22
248
+ },
249
+ {
250
+ :name => "Eparam",
251
+ :value => 222
252
+ },
253
+ {
254
+ :name => "AECparam",
255
+ :value => 21
256
+ },
257
+ {
258
+ :name => "BASOparam",
259
+ :value => 222
260
+ },
261
+ {
262
+ :name => "ABCparam",
263
+ :value => 300
264
+ },
265
+ {
266
+ :name => "RBCparam",
267
+ :value => 2.22
268
+ },
269
+ {
270
+ :name => "HBparam",
271
+ :value => 19
272
+ },
273
+ {
274
+ :name => "HCTparam",
275
+ :value => 22
276
+ },
277
+ {
278
+ :name => "MCVparam",
279
+ :value => 222
280
+ },
281
+ {
282
+ :name => "MCHparam",
283
+ :value => 21
284
+ },
285
+ {
286
+ :name => "MCHCparam",
287
+ :value => 10
288
+ },
289
+ {
290
+ :name => "MCVparam",
291
+ :value => 222
292
+ },
293
+ {
294
+ :name => "RDWCVparam",
295
+ :value => 12
296
+ },
297
+ {
298
+ :name => "PCparam",
299
+ :value => 1.22322
300
+ }
301
+ ]
302
+ }
303
+ ]
304
+ =end
305
+ ## pretty simple, if the value is not already there it will be updated, otherwise it won't be.
306
+ def update_LIS
307
+ prepare_redis
308
+ patients_to_process = $redis.llen("patients") > 0
309
+ while patients_to_process == true
310
+ if patient_results = $redis.rpoplpush("patients","processing")
311
+ patient_results = JSON.parse(patient_results)
312
+ update(patient_results)
313
+ patients_to_process = $redis.llen("patients") > 0
314
+ else
315
+ patients_to_process = false
316
+ end
317
+ end
318
+ end
319
+
320
+ ## this method should be overriden.
321
+ ## will poll the lis, and store locally, in a redis sorted set the following:
322
+ ## key => specimen_id
323
+ ## value => tests designated for that specimen.
324
+ ## score => time of requisition of that specimen.
325
+ ## name of the sorted set can be defined in the class that inherits from adapter, or will default to "requisitions"
326
+ ## when a query is sent from any laboratory equipment to the local ASTMServer, it will query the redis sorted set, for the test information.
327
+ ## so this poller basically constantly replicates the cloud based test information to the local server.
328
+ def poll_LIS_for_requisition
329
+
330
+ end
331
+
332
+ def poll
333
+ pre_poll_LIS
334
+ poll_LIS_for_requisition
335
+ update_LIS
336
+ post_poll_LIS
337
+ end
338
+
339
+ end
@@ -0,0 +1,2 @@
1
+ ---
2
+ default: '{"client_id":"555057029489-sel4u1t4tnv3qicdhsssp9phn3r8tvrj.apps.googleusercontent.com","access_token":"ya29.GlxjBtK4IVi08eyrCDI6WaEgkZnjhNK7wvJIw8vd7PzzfLiMfuphIlZFk4t8UUMSROqQIsBw7RQXTifEXE2FDjvjdLoeEr-sDj1xHVifr53_V-ZcJlbOXBCUexjxTQ","refresh_token":"1/SPwjKXMExoiU43WCJxPJYPd2cj0Hhb1Ms2ONe3KgIIs","scope":["https://www.googleapis.com/auth/documents","https://www.googleapis.com/auth/drive","https://www.googleapis.com/auth/script.projects","https://www.googleapis.com/auth/spreadsheets"],"expiration_time_millis":1543480295000}'
@@ -0,0 +1,49 @@
1
+ require 'rubygems'
2
+ require 'eventmachine'
3
+ require 'em-rubyserial'
4
+ require "active_support/all"
5
+ require "json"
6
+ require "redis"
7
+
8
+ class AstmServer
9
+
10
+ include LabInterface
11
+
12
+ def self.log(message)
13
+ puts "" + message
14
+ $redis.zadd("ruby_astm_log",Time.now.to_i,message)
15
+ end
16
+
17
+ $ENQ = "[5]"
18
+ $start_text = "[2]"
19
+ $end_text = "[3]"
20
+ $record_end = "[13]"
21
+ $frame_end = "[10]"
22
+
23
+ def initialize(server_ip,server_port,mpg,respond_to_queries=false)
24
+ $redis = Redis.new
25
+ AstmServer.log("Initializing AstmServer")
26
+ self.server_ip = server_ip || "192.168.1.14"
27
+ self.server_port = server_port || 3000
28
+ self.respond_to_queries = respond_to_queries
29
+ $mappings = JSON.parse(IO.read(mpg || ("mappings.json")))
30
+ end
31
+
32
+ def start_server
33
+ EventMachine.run {
34
+ self.ethernet_server = EventMachine::start_server self.server_ip, self.server_port, LabInterface
35
+ AstmServer.log("Running ETHERNET SERVER on #{server_port}")
36
+ #serial = EventMachine.open_serial('/dev/ttyUSB0', 9600, 8)
37
+ #serial.on_data do |data|
38
+ # puts "got some data"
39
+ # puts data.to_s
40
+ # serial.send_data("\X06")
41
+ #end
42
+ }
43
+ end
44
+
45
+ ## now we need to run the server and poller.
46
+ ## thats what we need.
47
+
48
+
49
+ end
@@ -0,0 +1,25 @@
1
+ class Frame
2
+
3
+ attr_accessor :machine_model
4
+ attr_accessor :astm_version
5
+ attr_accessor :records
6
+
7
+ def initalize(args)
8
+ self.records = []
9
+ super(args)
10
+ end
11
+
12
+ def self.is_start_frame?(l)
13
+ line_without_control_char = l[1..-1]
14
+ return line_without_control_char =~ /\d+H/
15
+ #k = l[0].bytes
16
+ #if k.to_s == "[5]"
17
+ # puts "ENQ"
18
+ #end
19
+ end
20
+
21
+ def self.is_end_frame?(line)
22
+
23
+ end
24
+
25
+ end
@@ -0,0 +1,47 @@
1
+ class Header
2
+ attr_accessor :machine_name
3
+ attr_accessor :patients
4
+ attr_accessor :queries
5
+ attr_accessor :response_sent
6
+
7
+ def initialize(args)
8
+ if args[:line]
9
+ line = args[:line]
10
+ unless line.fields[4].empty?
11
+ fields = line.fields[4].split(/\^/)
12
+ self.machine_name = fields[0].strip
13
+ end
14
+ else
15
+ super
16
+ end
17
+ self.patients = []
18
+ self.queries = []
19
+ self.response_sent = false
20
+ end
21
+
22
+ ## pushes each patient into a redis list called "patients"
23
+ def commit
24
+ self.patients.map{|patient| $redis.lpush("patients",patient.to_json)}
25
+ #puts JSON.pretty_generate(JSON.parse(self.to_json))
26
+ end
27
+
28
+ ## used to respond to queries.
29
+ ## @return[String] response_to_query : response to the header query.
30
+ def build_responses
31
+ responses = self.queries.map {|query|
32
+ header_response = "1H|\`^&||||||||||P|E 1394-97|#{Time.now.strftime("%Y%m%d%H%M%S")}\r"
33
+ query.response = header_response + query.build_response
34
+ query.response
35
+ }
36
+ responses
37
+ end
38
+
39
+ def to_json
40
+ hash = {}
41
+ self.instance_variables.each do |x|
42
+ hash[x] = self.instance_variable_get x
43
+ end
44
+ return hash.to_json
45
+ end
46
+
47
+ end
@@ -0,0 +1,160 @@
1
+ require "active_support/all"
2
+
3
+ module LabInterface
4
+
5
+ ACK = "\x06"
6
+ ENQ = "\x05"
7
+ STX = "\x02"
8
+ LF = "\x10"
9
+ CR = "\x13"
10
+ ETX = "\x03"
11
+ EOT = "\x04"
12
+
13
+ mattr_accessor :headers
14
+ mattr_accessor :ethernet_server
15
+ mattr_accessor :server_ip
16
+ mattr_accessor :server_port
17
+ mattr_accessor :mapping
18
+ mattr_accessor :respond_to_queries
19
+
20
+
21
+ ## returns the root directory of the gem.
22
+ def root
23
+ File.dirname __dir__
24
+ end
25
+
26
+ ## can process a file which contains ASTM output.
27
+ ## this method is added so that you can load previously generated ASTM output into your database
28
+ ## it also simplifies testing.
29
+ def process_text_file(full_file_path)
30
+ #full_file_path ||= File.join root,'../test','resources','sysmex_550_sample.txt'
31
+ IO.read(full_file_path).each_line do |line|
32
+ process_text(line)
33
+ end
34
+ end
35
+
36
+ def terminator
37
+ "L|1|N\r"
38
+ end
39
+
40
+ def checksum(input)
41
+ strString = input
42
+ checksum = strString.sum
43
+ b = checksum.to_s(16)
44
+ strCksm = b[-2..-1]
45
+ if strCksm.length < 2
46
+ for i in strString.length..1
47
+ strCksm = "0" + strCksm
48
+ end
49
+ end
50
+ strCksm.upcase
51
+ end
52
+
53
+ def receive_data(data)
54
+
55
+ puts "incoming data bytes."
56
+
57
+ concat = ""
58
+
59
+
60
+ puts data.bytes.to_a.to_s
61
+
62
+ data.bytes.to_a.each do |byte|
63
+ x = [byte].pack('c*').force_encoding('UTF-8')
64
+ if x == "\r"
65
+ concat+="\n"
66
+ elsif x == "\n"
67
+ #puts "new line found --- "
68
+ concat+=x
69
+ #puts "last thing in concat."
70
+ #puts concat[-1].to_s
71
+ else
72
+ concat+=x
73
+ end
74
+ end
75
+
76
+ #puts "concat is:"
77
+ puts concat.to_s
78
+ process_text(concat)
79
+
80
+ if data.bytes.to_a[0] == 4
81
+ puts "sent ENQ"
82
+ send_data(ENQ)
83
+ elsif data.bytes.to_a[0] == 6
84
+ ## so now the middle part has to be
85
+ =begin
86
+ response = STX + "1H|\`^&||||||||||P|E 1394-97|#{Time.now.strftime("%Y%m%d%H%M%S")}\r" + terminator + ETX + checksum("1H|\`^&||||||||||P|E 1394-97|#{Time.now.strftime("%Y%m%d%H%M%S")}\r" + terminator) + "\r"
87
+ response = response.bytes.to_a
88
+ response << 10
89
+ send_data(response.pack('c*'))
90
+ =end
91
+ self.headers[-1].build_responses.each do |response|
92
+ message_checksum = checksum(response + terminator + ETX)
93
+ puts "Calculated checksum is: #{message_checksum}"
94
+ final_resp = STX + response + terminator + ETX + message_checksum + "\r"
95
+ final_resp_arr = final_resp.bytes.to_a
96
+ final_resp_arr << 10
97
+ puts final_resp_arr.to_s
98
+ if (self.headers[-1].response_sent == false)
99
+ send_data(final_resp_arr.pack('c*'))
100
+ self.headers[-1].response_sent = true
101
+ else
102
+ puts "response was already sent."
103
+ send_data(EOT)
104
+ end
105
+
106
+ end
107
+ else
108
+ ## send the header
109
+ puts "--------- SENT ACK -----------"
110
+ send_data(ACK)
111
+ end
112
+ end
113
+
114
+ def send_enq
115
+ puts "enq as bytes is:"
116
+ puts ENQ.unpack('c*')
117
+ send_data(ENQ)
118
+ end
119
+
120
+ def process_text(text)
121
+ text.split("\n").each do |l|
122
+ line = Line.new({:text => l})
123
+ process_type(line)
124
+ end
125
+ end
126
+
127
+ def process_type(line)
128
+ case line.type
129
+ when "Header"
130
+ header = Header.new({:line => line})
131
+ self.headers ||= []
132
+ self.headers << header
133
+ when "Query"
134
+ query = Query.new({:line => line})
135
+ self.headers[-1].queries << query
136
+ when "Patient"
137
+ patient = Patient.new({:line => line})
138
+ self.headers[-1].patients << patient
139
+ when "Order"
140
+ order = Order.new({:line => line})
141
+ self.headers[-1].patients[-1].orders << order
142
+ when "Result"
143
+ result = Result.new({:line => line})
144
+ self.headers[-1].patients[-1].orders[-1].results[result.name] = result
145
+ when "Terminator"
146
+ self.headers[-1].commit
147
+ end
148
+ end
149
+
150
+ =begin
151
+ 1.STX + response + LF + ETX
152
+ 2.response
153
+ 3.STX + response + "L|1|N\r" + ETX
154
+ 4.response + "L|1|N\r"
155
+ =end
156
+ def unbind
157
+ puts "-- someone disconnected from the echo server!"
158
+ end
159
+
160
+ end
@@ -0,0 +1,65 @@
1
+ class Line
2
+
3
+ TYPES = {
4
+ "H" => "Header",
5
+ "P" => "Patient",
6
+ "Q" => "Query",
7
+ "O" => "Order",
8
+ "R" => "Result",
9
+ "L" => "Terminator"
10
+ }
11
+
12
+ attr_accessor :text
13
+
14
+ attr_accessor :fields
15
+
16
+ attr_accessor :type
17
+
18
+ ########################################################
19
+ ##
20
+ ##
21
+ ## HEADER ATTRIBUTES
22
+ ##
23
+ ##
24
+ ########################################################
25
+
26
+
27
+
28
+ ## sets the types, and fields
29
+ ## we can have processing based on line.
30
+ def initialize(args)
31
+ self.fields = []
32
+ raise "no text provided" unless args[:text]
33
+ if args[:text]
34
+ args[:text].split(/\|/).each do |field|
35
+ self.fields << field
36
+ end
37
+ end
38
+ detect_type
39
+ end
40
+
41
+ def detect_type
42
+ #puts "detecting line type: #{self.text}"
43
+ line_type = self.fields[0]
44
+ line_type.scan(/(?<ltype>[A-Z])/) { |ltype|
45
+ #puts "got ltype as: #{ltype[0]}"
46
+ #puts Line::TYPES.to_s
47
+ if Line::TYPES[ltype[0]]
48
+ self.type = Line::TYPES[ltype[0]]
49
+ #puts "assigning type as: #{self.type}"
50
+ end
51
+ }
52
+
53
+ =begin
54
+ if self.fields[0][1..-1] =~/#{type}/
55
+ puts "got type: #{Line::TYPES[type]}"
56
+ self.type = Line::TYPES[type]
57
+ break
58
+ end
59
+ =end
60
+
61
+ end
62
+
63
+
64
+
65
+ end
@@ -0,0 +1,75 @@
1
+ class Order
2
+ ## => patient id.
3
+ ## => should match the specimen id that comes into the query.
4
+ ## index (2)
5
+ attr_accessor :id
6
+
7
+ ## this should be same as the one for patient.
8
+ ## index (1)
9
+ attr_accessor :sequence_number
10
+
11
+ ## => key : result name.
12
+ ## => value : result object
13
+ attr_accessor :results
14
+
15
+ ## the list of tests that need to be performed.
16
+ ## each should be prefixed by three carets
17
+ ## index (4)
18
+ attr_accessor :tests
19
+ ## the specimen type.
20
+
21
+ ## index (15)
22
+ attr_accessor :specimen_type
23
+
24
+
25
+ ## the order type, like stat, routine etc.
26
+ ## index (5)
27
+ attr_accessor :priority
28
+
29
+ ## the date and time of collection
30
+ ## index (7)
31
+ attr_accessor :date_time
32
+
33
+ ## action code
34
+ ## index (11)
35
+ attr_accessor :action_code
36
+
37
+ def initialize(args)
38
+ if args[:line]
39
+ line = args[:line]
40
+ if line.fields[2]
41
+ line.fields[2].strip.scan(/(?<specimen_id>[^\^]+)/) { |specimen_id|
42
+ self.id ||= specimen_id
43
+ }
44
+ elsif line.fields[3]
45
+ ## for the sysmex xn-550 this is the regex.
46
+ line.fields[3].strip.scan(/(?<tube_rack>\d+\^)+(?<patient_id>.+)\^/) { |tube_rack,patient_id| self.id = patient_id.strip}
47
+ end
48
+ else
49
+ self.sequence_number = args[:sequence_number]
50
+ self.tests = args[:tests]
51
+ self.id = args[:specimen_id]
52
+ self.specimen_type = args[:specimen_type]
53
+ self.tests = args[:tests]
54
+ self.date_time = args[:date_time]
55
+ self.priority = args[:priority]
56
+ self.action_code = args[:action_code]
57
+ end
58
+ self.results = {}
59
+ end
60
+
61
+ def build_response
62
+
63
+ raise "provide a sequence number" if self.sequence_number.blank?
64
+ raise "provide a specimen id" if self.id.blank?
65
+ raise "provide a list of tests" if self.tests.blank?
66
+ raise "provide a test priority" if self.priority.blank?
67
+
68
+ if self.specimen_type.blank?
69
+ #puts "no specimen type has been provided, sending SERUM"
70
+ end
71
+
72
+ "O|#{self.sequence_number}|#{self.id}^01||^^^#{self.tests.join('^^^')}|#{self.priority}||#{Time.now.strftime("%Y%m%d%H%M%S")}||||N||||#{self.specimen_type || 'SERUM'}\r"
73
+ end
74
+
75
+ end
@@ -0,0 +1,32 @@
1
+ class Patient
2
+
3
+ ## sequence number can only be from 0 -- 9.
4
+ attr_accessor :sequence_number
5
+ attr_accessor :patient_id
6
+ attr_accessor :orders
7
+
8
+ def initialize(args)
9
+ if args[:line]
10
+ line = args[:line]
11
+ self.sequence_number = line.fields[1].to_i
12
+ self.orders = []
13
+ else
14
+ self.sequence_number = args[:sequence_number]
15
+ self.patient_id = args[:patient_id]
16
+ end
17
+ end
18
+
19
+ ## patient id.
20
+ def build_response
21
+ "P|#{self.sequence_number}|#{self.patient_id}|||||||||||||||\r"
22
+ end
23
+
24
+ def to_json
25
+ hash = {}
26
+ self.instance_variables.each do |x|
27
+ hash[x] = self.instance_variable_get x
28
+ end
29
+ return hash.to_json
30
+ end
31
+
32
+ end
@@ -0,0 +1,34 @@
1
+ class Query
2
+
3
+ attr_accessor :sample_id
4
+
5
+ attr_accessor :response
6
+
7
+ def initialize(args)
8
+ line = args[:line]
9
+ unless line.fields[2].empty?
10
+ fields = line.fields[2].split(/\^/)
11
+ self.sample_id = fields[1].strip
12
+ end
13
+ end
14
+
15
+ ## each query will build one patient and one order inside it.
16
+ ## the order can have many tests.
17
+ def build_response(variables=nil)
18
+ variables ||= {
19
+ :sequence_number => "0",
20
+ :patient_id => "abcde",
21
+ :specimen_id => self.sample_id,
22
+ :tests => ["TRIG"],
23
+ :priority => "R"
24
+ }
25
+
26
+ patient = Patient.new({:sequence_number => "0", :patient_id => "abcde"})
27
+
28
+ order = Order.new({:sequence_number => patient.sequence_number, :specimen_id => variables[:specimen_id], :tests => variables[:tests], :priority => variables[:priority]})
29
+
30
+ return patient.build_response + order.build_response
31
+
32
+ end
33
+
34
+ end
@@ -0,0 +1,34 @@
1
+ class Result
2
+ attr_accessor :name
3
+ attr_accessor :value
4
+ attr_accessor :units
5
+ attr_accessor :flags
6
+ attr_accessor :timestamp
7
+ attr_accessor :reference_ranges
8
+ attr_accessor :dilution
9
+
10
+ ## here will call mappings and check the result correlation
11
+ def initialize(args)
12
+ if args[:line]
13
+ line = args[:line]
14
+ line.fields[2].scan(/\^+(?<name>[A-Za-z0-9\%\#]+)\^?(?<dilution>\d+)?/) { |name,dilution|
15
+ self.name = lookup_mapping(name)
16
+ self.dilution = dilution
17
+ }
18
+ self.value = line.fields[3].strip
19
+ self.reference_ranges = line.fields[5].strip
20
+ self.flags = line.fields[6].strip
21
+ line.fields[12].strip.scan(/(?<year>\d{4})(?<month>\d{2})(?<day>\d{2})(?<hours>\d{2})(?<minutes>\d{2})(?<seconds>\d{2})/) {|year,month,day,hours,minutes,seconds|
22
+ self.timestamp = Time.new(year,month,day,hours,minutes,seconds)
23
+ }
24
+ else
25
+ super
26
+ end
27
+ end
28
+
29
+ ## @return[String] the name defined in the mappings.json file, or the name that wqs passed in.
30
+ def lookup_mapping(name)
31
+ $mappings[name] ? $mappings[name]["LIS_CODE"] : name
32
+ end
33
+
34
+ end
data/lib/ruby_astm.rb ADDED
@@ -0,0 +1,12 @@
1
+ require "ruby_astm/query"
2
+ require "ruby_astm/frame"
3
+ require "ruby_astm/header"
4
+ require "ruby_astm/lab_interface"
5
+ require "ruby_astm/line"
6
+ require "ruby_astm/order"
7
+ require "ruby_astm/patient"
8
+ require "ruby_astm/result"
9
+ require "ruby_astm/astm_server"
10
+ require "publisher/adapter"
11
+ require "publisher/google_lab_interface"
12
+ require "publisher/poller"
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby_astm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Bhargav Raut
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-11-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: eventmachine
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: em-rubyserial
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: json
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: redis
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: This gem provides a server that can handle communication from medical
84
+ instruments that send/receive information on the ASTM protocol.
85
+ email: bhargav.r.raut@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - lib/publisher/adapter.rb
91
+ - lib/publisher/credentials.json
92
+ - lib/publisher/google_lab_interface.rb
93
+ - lib/publisher/poller.rb
94
+ - lib/publisher/token.yaml
95
+ - lib/ruby_astm.rb
96
+ - lib/ruby_astm/astm_server.rb
97
+ - lib/ruby_astm/frame.rb
98
+ - lib/ruby_astm/header.rb
99
+ - lib/ruby_astm/lab_interface.rb
100
+ - lib/ruby_astm/line.rb
101
+ - lib/ruby_astm/order.rb
102
+ - lib/ruby_astm/patient.rb
103
+ - lib/ruby_astm/query.rb
104
+ - lib/ruby_astm/result.rb
105
+ homepage: https://www.github/com/wordjelly/ruby_astm
106
+ licenses:
107
+ - MIT
108
+ metadata: {}
109
+ post_install_message:
110
+ rdoc_options: []
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ requirements: []
124
+ rubyforge_project:
125
+ rubygems_version: 2.6.14.1
126
+ signing_key:
127
+ specification_version: 4
128
+ summary: A Ruby gem to interface with Medical instruments that work on the ASTM protocol
129
+ test_files: []