ruby_astm 0.0.0

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