ruby_astm 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/publisher/adapter.rb +5 -0
- data/lib/publisher/credentials.json +1 -0
- data/lib/publisher/google_lab_interface.rb +135 -0
- data/lib/publisher/poller.rb +339 -0
- data/lib/publisher/token.yaml +2 -0
- data/lib/ruby_astm/astm_server.rb +49 -0
- data/lib/ruby_astm/frame.rb +25 -0
- data/lib/ruby_astm/header.rb +47 -0
- data/lib/ruby_astm/lab_interface.rb +160 -0
- data/lib/ruby_astm/line.rb +65 -0
- data/lib/ruby_astm/order.rb +75 -0
- data/lib/ruby_astm/patient.rb +32 -0
- data/lib/ruby_astm/query.rb +34 -0
- data/lib/ruby_astm/result.rb +34 -0
- data/lib/ruby_astm.rb +12 -0
- metadata +129 -0
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 @@
|
|
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: []
|