dmm_util 0.1.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.
@@ -0,0 +1,440 @@
1
+ require 'rubygems'
2
+ require 'serialport'
3
+ require 'enumerator'
4
+
5
+ ### It appears thwe are 32 bit vals:
6
+ ## Mulit-map values
7
+ # Sequence numbers
8
+ # duration
9
+
10
+ module DmmUtil
11
+ class Fluke28xDriver
12
+ MPQ_PROPS = [:company, :contact, :operator, :site]
13
+ MP_PROPS = {:dcpol => [:pos, :neg], :rsm => [:off, :on], :si => [:off, :on],
14
+ :lang => [:japanese, :english, :chinese, :german, :french, :spanish, :italian],
15
+ :apoffto => [2700, 0, 3600, 900, 1500, 2100], :hzedge => [:rising, :falling], :lcdcont => :int, :acsmooth => [:off, :on],
16
+ :numfmt => [:point, :comma], :beeper => [:off, :on], :pwpol => [:pos, :neg], :aheventth => :int,
17
+ :timefmt => [12, 24], :cusdbm => :int, :dbmref => [16, 0, 600, 50, 8, 25, 75, 4, 1000, 32],
18
+ :receventth => [5, 0, 1, 25, 20, 15, 4, 10], :contbeepos => [:short, :open], :digits => [5, 4], :tempos => :int,
19
+ :clock => :timeval, :contbeep => [:off, :on], :ablto => [0, 600, 1200, 1800, 300, 900, 1500],
20
+ :tempunit => [:c, :f], :datefmt => [:mm_dd, :dd_mm]}
21
+
22
+ include FormatConvertors
23
+
24
+ def initialize(port)
25
+ @port = port
26
+ @map_cache = {}
27
+ end
28
+
29
+ ########## High level commands ################
30
+
31
+ def valid?
32
+ 3.times do
33
+ begin
34
+ res = self.id
35
+ return true if res[:model_number] && res[:software_version] && res[:serial_number]
36
+ rescue MeterError
37
+ end
38
+ end
39
+ return false
40
+ end
41
+
42
+ ########## Fluke28xDrivercommands ##################
43
+
44
+ def id
45
+ res = meter_command("ID")
46
+ {:model_number => res[0], :software_version => res[1], :serial_number => res[2]}
47
+ end
48
+
49
+ def qdda
50
+ res = meter_command("qdda")
51
+
52
+ mode_count = Integer(res[8])
53
+ reading_count = Integer(res[9 + mode_count])
54
+ raise DmmUtil::MeterError("Error parsing qdda response") unless res.size == 10 + mode_count + reading_count * 9
55
+
56
+ reading_map = {}
57
+ res[(10 + mode_count)..-1].each_slice(9) do |reading|
58
+ reading_map[reading[0]] = {:value => Float(reading[1]),
59
+ :unit => reading[2], :unit_multiplier => Integer(reading[3]),
60
+ :decimals => reading[4].to_i,
61
+ :state => reading[6], :ts => parse_time(reading[8].to_f),
62
+ :display_digits => Integer(reading[5]), :attribute => reading[7]}
63
+ end
64
+
65
+ {:prim_function => res[0], :sec_function => res[1], :mode => res[9,mode_count],
66
+ :auto_range => res[2], :range_max => Integer(res[4]),
67
+ :unit => res[3], :unit_multiplier => Integer(res[5]),
68
+ :bolt => res[6], :ts => (res[7].to_i == 0 ? nil : parse_time(Float(res[7]))),
69
+ :readings => reading_map}
70
+ end
71
+
72
+ def qddb
73
+ bytes = meter_command("qddb")
74
+
75
+ reading_count = get_u16(bytes, 32)
76
+ raise MeterError.new("qddb parse error, expected #{reading_count * 30 + 34} bytes, got #{bytes.size}") unless bytes.size == reading_count * 30 + 34
77
+ tsval = get_double(bytes, 20)
78
+ # all bytes parsed
79
+ {
80
+ :prim_function => get_map_value(:primfunction, bytes, 0),
81
+ :sec_function => get_map_value(:secfunction, bytes, 2),
82
+ :auto_range => get_map_value(:autorange, bytes, 4),
83
+ :unit => get_map_value(:unit, bytes, 6),
84
+ :range_max => get_double(bytes, 8),
85
+ :unit_multiplier => get_s16(bytes, 16),
86
+ :bolt => get_map_value(:bolt, bytes, 18),
87
+ :ts => (tsval < 0.1) ? nil : parse_time(tsval), # 20
88
+ :mode => get_multimap_value(:mode, bytes, 28),
89
+ :un1 => get_u16(bytes, 30),
90
+ # 32 is reading count
91
+ :readings => parse_readings(bytes[34 .. -1])
92
+ }
93
+ end
94
+
95
+ def qsls
96
+ res = meter_command("qsls")
97
+ {:recording => Integer(res[0]), :min_max => Integer(res[1]),
98
+ :peak => Integer(res[2]), :measurement => Integer(res[3])}
99
+ end
100
+
101
+ def qsmr(idx)
102
+ # Get saved measurement
103
+ res = meter_command("qsmr #{idx}")
104
+
105
+ reading_count = get_u16(res, 36)
106
+ raise MeterError.new("qsmr parse error, expected at least #{reading_count * 30 + 38} bytes, got #{res.size}") unless res.size >= reading_count * 30 + 38
107
+
108
+ { :seq_no => get_u16(res,0),
109
+ :un1 => get_u16(res,2), # 32 bit?
110
+ :prim_function => get_map_value(:primfunction, res,4), # prim?
111
+ :sec_function => get_map_value(:secfunction, res,6), # sec?
112
+ :auto_range => get_map_value(:autorange, res, 8),
113
+ :unit => get_map_value(:unit, res, 10),
114
+ :range_max => get_double(res, 12),
115
+ :unit_multiplier => get_s16(res, 20),
116
+ :bolt => get_map_value(:bolt, res, 22),
117
+ :un4 => get_u16(res,24), # ts?
118
+ :un5 => get_u16(res,26),
119
+ :un6 => get_u16(res,28),
120
+ :un7 => get_u16(res,30),
121
+ :mode => get_multimap_value(:mode, res,32),
122
+ :un9 => get_u16(res,34),
123
+ # 36 is reading count
124
+ :readings => parse_readings(res[38, reading_count * 30]),
125
+ :name => res[(38 + reading_count * 30)..-1],
126
+ }
127
+ end
128
+
129
+ def qmmsi(idx)
130
+ # Get min/max
131
+ do_min_max_cmd("qmmsi", idx)
132
+ end
133
+
134
+ def qpsi(idx)
135
+ # Recorded peak
136
+ do_min_max_cmd("qpsi", idx)
137
+ end
138
+
139
+ def qrsi(idx)
140
+ # Recorded session info
141
+ res = meter_command("qrsi #{idx}")
142
+ reading_count = get_u16(res, 76)
143
+ raise MeterError.new("qrsi parse error, expected at least #{reading_count * 30 + 78} bytes, got #{res.size}") unless res.size >= reading_count * 30 + 78
144
+
145
+ # All bytes parsed
146
+ {
147
+ :seq_no => get_u16(res, 0),
148
+ :un2 => get_u16(res, 2), # 32 bits?
149
+ :start_ts => parse_time(get_double(res, 4)),
150
+ :end_ts => parse_time(get_double(res, 12)),
151
+ :sample_interval => get_double(res, 20),
152
+ :event_threshold => get_double(res, 28),
153
+ :reading_index => get_u16(res, 36), # 32 bits?
154
+ :un3 => get_u16(res, 38),
155
+ :num_samples => get_u16(res, 40), # Is this 32 bits? Whats in 42
156
+ :un4 => get_u16(res, 42),
157
+ :prim_function => get_map_value(:primfunction, res, 44), # prim?
158
+ :sec_function => get_map_value(:secfunction, res, 46), # sec?
159
+ :auto_range => get_map_value(:autorange, res, 48),
160
+ :unit => get_map_value(:unit, res, 50),
161
+ :range_max => get_double(res, 52),
162
+ :unit_multiplier => get_s16(res, 60),
163
+ :bolt => get_map_value(:bolt, res, 62), #bolt?
164
+ :un8 => get_u16(res, 64), #ts3?
165
+ :un9 => get_u16(res, 66), #ts3?
166
+ :un10 => get_u16(res, 68), #ts3?
167
+ :un11 => get_u16(res, 70), #ts3?
168
+ :mode => get_multimap_value(:mode, res, 72),
169
+ :un12 => get_u16(res, 74),
170
+ # 76 is reading count
171
+ :readings => parse_readings(res[78, reading_count * 30]),
172
+ :name => res[(78 + reading_count * 30)..-1]
173
+ }
174
+ end
175
+
176
+ def qsrr(reading_idx, sample_idx)
177
+ res = meter_command("qsrr #{reading_idx},#{sample_idx}", 149)
178
+
179
+ raise MeterError.new("Invalid block size: #{res.size} should be 146") unless res.size == 146
180
+ # All bytes parsed - except there seems to be single byte at end?
181
+ {
182
+ :start_ts => parse_time(get_double(res, 0)),
183
+ :end_ts => parse_time(get_double(res, 8)),
184
+ :readings => parse_readings(res[16, 30*3]),
185
+ :duration => get_u16(res, 106) * 0.1,
186
+ :un2 => get_u16(res, 108),
187
+ :readings2 => parse_readings(res[110,30]),
188
+ :record_type => get_map_value(:recordtype, res, 140),
189
+ :stable => get_map_value(:isstableflag, res, 142),
190
+ :transient_state => get_map_value(:transientstate, res, 144)
191
+ }
192
+ end
193
+
194
+ def qemap(map_name)
195
+ res = meter_command("qemap #{map_name.to_s}")
196
+ entry_count = Integer(res.shift)
197
+ raise MeterError.new("Error parsing qemap") unless res.size == entry_count * 2
198
+
199
+ map = {}
200
+ res.each_slice(2) do |key, val|
201
+ map[Integer(key)] = val
202
+ end
203
+ map
204
+ end
205
+
206
+ MPQ_PROPS.each do |mpq_prop|
207
+ define_method(mpq_prop) do
208
+ self.meter_command("qmpq #{mpq_prop}")[0]
209
+ end
210
+
211
+ define_method("#{mpq_prop}=") do |val|
212
+ self.meter_command("mpq #{mpq_prop},#{quote_str(val)}", 0)
213
+ end
214
+ end
215
+
216
+ MP_PROPS.each do |mp_prop, format|
217
+ define_method(mp_prop) do
218
+ val = self.meter_command("qmp #{mp_prop}")[0]
219
+ if format == :int
220
+ val = Integer(val)
221
+ elsif format == :timeval
222
+ tz_offset = Time.now.utc_offset
223
+ val = Time.at(Integer(val) - tz_offset)
224
+ end
225
+ val
226
+ end
227
+
228
+ define_method("#{mp_prop}=") do |val|
229
+ if format.is_a?(Array)
230
+ raise MeterError.new("Illegal value '#{val}' for #{mp_prop}. Legal values: #{format.join(", ")}") unless format.include?(val)
231
+ elsif format == :int
232
+ raise MeterError.new("Illegal value '#{val}' for #{mp_prop}. Must be integer.") unless val.is_a?(Integer)
233
+ elsif format == :timeval
234
+ raise MeterError.new("Clock command requires a Time object.") unless val.is_a?(Time)
235
+ tz_offset = Time.now.utc_offset
236
+ val = val.to_i + tz_offset
237
+ end
238
+ self.meter_command("mp #{mp_prop},#{val}",0)
239
+ end
240
+ end
241
+
242
+
243
+ ########## Low level stuff ##########
244
+ def do_min_max_cmd(cmd, idx)
245
+ res = meter_command("#{cmd} #{idx}")
246
+ # un8 = 0, un2 = 0, always bolt
247
+ reading_count = get_u16(res, 52)
248
+ raise MeterError.new("qsmmsi parse error, expected at least #{reading_count * 30 + 54} bytes, got #{res.size}") unless res.size >= reading_count * 30 + 54
249
+
250
+ # All bytes parsed
251
+ { :seq_no => get_u16(res, 0),
252
+ :un2 => get_u16(res, 2), # High byte of seq no?
253
+ :ts1 => parse_time(get_double(res, 4)),
254
+ :ts2 => parse_time(get_double(res, 12)),
255
+ :prim_function => get_map_value(:primfunction, res, 20),
256
+ :sec_function => get_map_value(:secfunction, res, 22),
257
+ :autorange => get_map_value(:autorange, res, 24),
258
+ :unit => get_map_value(:unit, res, 26),
259
+ :range_max => get_double(res, 28),
260
+ :unit_multiplier => get_s16(res, 36),
261
+ :bolt => get_map_value(:bolt, res, 38),
262
+ :ts3 => parse_time(get_double(res, 40)),
263
+ :mode => get_multimap_value(:mode, res, 48),
264
+ :un8 => get_u16(res, 50),
265
+ # 52 is reading_count
266
+ :readings => parse_readings(res[54, reading_count * 30]),
267
+ :name => res[(54 + reading_count * 30)..-1]
268
+ }
269
+ end
270
+
271
+ def parse_readings(reading_bytes)
272
+ readings = {}
273
+ ByteStr.new(reading_bytes).each_slice(30) do |reading_arr|
274
+ r = reading_arr.map{|b| b.chr}.join
275
+ # All bytes parsed
276
+ readings[get_map_value(:readingid, r, 0)] = {
277
+ :value => get_double(r, 2),
278
+ :unit => get_map_value(:unit, r, 10),
279
+ :unit_multiplier => get_s16(r, 12),
280
+ :decimals => get_s16(r, 14),
281
+ :display_digits => get_s16(r, 16),
282
+ :state => get_map_value(:state, r, 18),
283
+ :attribute => get_map_value(:attribute, r, 20),
284
+ :ts => get_time(r, 22)
285
+ }
286
+ end
287
+ readings
288
+ end
289
+
290
+
291
+ def get_map_value(map_name, str, offset)
292
+ map = @map_cache[map_name.to_sym] ||= qemap(map_name)
293
+ value = get_u16(str, offset)
294
+ raise MeterError.new("Can not find key #{value} in map #{map_name}") unless map.has_key?(value)
295
+ map[value]
296
+ end
297
+
298
+ def get_multimap_value(map_name, str, offset)
299
+ map = @map_cache[map_name.to_sym] ||= qemap(map_name)
300
+ value = get_u16(str, offset)
301
+ check = 0
302
+ ret = []
303
+ map.keys.sort.each do |key|
304
+ if (value & key) != 0
305
+ ret << map[key]
306
+ check |= key
307
+ end
308
+ end
309
+ raise MeterError.new("Can not find key #{value} in map #{map_name}") unless check == value
310
+ ret
311
+ end
312
+
313
+ def data_ok?(data, count)
314
+ # No status code yet
315
+ return false if data.size < 2
316
+
317
+ # Non-OK status
318
+ return true if data.size == 2 && data[0,1] != "0" && data[1,1] == "\r"
319
+
320
+ # Non-OK status with extra data on end
321
+ raise MeterError.new("Error parsing status from meter (Non-OK status with extra data on end)") if data.size > 2 && data[0,1] != "0"
322
+
323
+ # We should now be in OK state
324
+ raise MeterError.new("Error parsing status from meter (status:#{data[0,1]} size:#{data.size})") unless data[0,1] == "0" && data[1,1] == "\r"
325
+
326
+ if count
327
+ data.size == count
328
+ else
329
+ data.size >= 4 && data[-1,1] == "\r"
330
+ end
331
+
332
+ end
333
+
334
+ def read_retry(count)
335
+ retry_count = 0
336
+ data = ""
337
+
338
+ while retry_count < 500 && !data_ok?(data, count)
339
+ data += @port.read
340
+ return data if data_ok?(data, count)
341
+ sleep 0.01
342
+ retry_count += 1
343
+ end
344
+ raise MeterError.new("Error parsing status from meter: #{data[0,1]} #{data.size} #{data[1,1] == "\r"} #{data[-1,1] == "\r"}")
345
+ end
346
+
347
+ def meter_command(cmd, expected_result = nil)
348
+ @port.write "#{cmd}\r"
349
+ data = read_retry(expected_result ? (expected_result + 2) : nil )
350
+ status = data[0,1]
351
+ raise MeterError.new("Command returned error code #{status}", Integer(status)) unless status == "0"
352
+ raise MeterError.new("Did not receive complete reply from meter") unless data[-1,1] == "\r"
353
+
354
+ binary = (data[2,2] == "#0")
355
+
356
+ if binary
357
+ data[4..-2]
358
+ else
359
+ tokens = []
360
+ state = :init
361
+ current_token = []
362
+ data[2..-2].each_byte do |b|
363
+ c = b.chr
364
+ case state
365
+ when :init
366
+ case c
367
+ when ","
368
+ tokens << current_token.join
369
+ current_token = []
370
+ when "'"
371
+ raise MeterError.new("Unexpected quote") unless current_token.empty?
372
+ state = :sq
373
+ when '"'
374
+ raise MeterError.new("Unexpected double-quote") unless current_token.empty?
375
+ state = :dq
376
+ else
377
+ current_token << c
378
+ end
379
+ when :sq
380
+ case c
381
+ when "'"
382
+ state = :sqe
383
+ else
384
+ current_token << c
385
+ end
386
+ when :sqe
387
+ case c
388
+ when "'"
389
+ current_token << c
390
+ state = :sq
391
+ when ","
392
+ tokens << current_token.join
393
+ current_token = []
394
+ state = :init
395
+ else
396
+ raise MeterError.new("Expected comma after single-quoted string")
397
+ end
398
+ when :dq
399
+ case c
400
+ when '"'
401
+ state = :dqe
402
+ else
403
+ current_token << c
404
+ end
405
+ when :dqe
406
+ case c
407
+ when ","
408
+ tokens << current_token.join
409
+ current_token = []
410
+ state = :init
411
+ else
412
+ raise MeterError.new("Expected comma after double-quoted string")
413
+ end
414
+ else
415
+ raise MeterError.new("Invalid parser state")
416
+ end
417
+ end
418
+
419
+ raise MeterError.new("Did not find end of string") unless [:init, :sqe, :dqe].include?(state)
420
+ tokens << current_token.join
421
+
422
+ end
423
+ rescue MeterError => e
424
+ if e.status == 8
425
+ retry
426
+ else
427
+ raise
428
+ end
429
+ end
430
+
431
+ end
432
+
433
+ class ByteStr < String
434
+ alias :each :each_byte
435
+ end
436
+
437
+
438
+
439
+
440
+ end
@@ -0,0 +1,46 @@
1
+ module DmmUtil
2
+
3
+ module FormatConvertors
4
+
5
+ def get_u16(str, offset)
6
+ lo_byte = str[offset]
7
+ hi_byte = str[offset + 1]
8
+ hi_byte * 0x100 + lo_byte
9
+ end
10
+
11
+ def get_s16(str, offset)
12
+ val = get_u16(str, offset)
13
+ unless (val & 0x8000) == 0
14
+ val = -(0x10000 - val)
15
+ end
16
+ val
17
+ end
18
+
19
+ def get_double(str, offset)
20
+ bytestr = str[offset, 8]
21
+ endianstr = bytestr[0,4].reverse + bytestr[4,4].reverse
22
+ endianstr.unpack("G")[0]
23
+ end
24
+
25
+ def get_time(str, offset)
26
+ parse_time(get_double(str, offset))
27
+ end
28
+
29
+ def parse_time(t)
30
+ tz_offset = Time.now.utc_offset
31
+ Time.at(t - tz_offset)
32
+ end
33
+
34
+ def quote_str(str)
35
+ has_single = str.include?("'")
36
+ has_double = str.include?('"')
37
+
38
+ if has_single && !has_double
39
+ "\"#{str}\""
40
+ else
41
+ "'#{str.gsub(/'/, "''")}'"
42
+ end
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,64 @@
1
+ require 'ostruct'
2
+
3
+ module DmmUtil
4
+
5
+ class Measurement
6
+ attr_reader :raw
7
+
8
+ def initialize(attrs)
9
+ @raw = attrs
10
+ end
11
+
12
+ def name
13
+ raw[:name] || raise("Not a named measurement")
14
+ end
15
+
16
+ def ts
17
+ raw[:ts] || self.primary.ts
18
+ end
19
+
20
+ def prim_function
21
+ raw[:prim_function]
22
+ end
23
+
24
+ def reading_names
25
+ raw[:readings].keys.map{|r| r.downcase.to_sym}
26
+ end
27
+
28
+ def to_s
29
+ order = [:primary, :maximum, :average, :minimum, :rel_reference,
30
+ :secondary,
31
+ :db_ref, :temp_offset,
32
+ :live, :rel_live]
33
+ existing = reading_names
34
+ res = []
35
+
36
+ existing.delete(:live) if existing.include?(:live) && self.live == self.primary
37
+
38
+ (order - [:primary]).each do |name|
39
+ next unless existing.include?(name)
40
+ res << "#{name}: #{self.send(name).to_s}"
41
+ end
42
+
43
+ (existing - order).each do |name|
44
+ res << "#{name}: #{self.send(name).to_s}"
45
+ end
46
+
47
+ if res.empty?
48
+ primary.to_s
49
+ else
50
+ "#{primary.to_s} (#{res.join(", ")})"
51
+ end
52
+ end
53
+
54
+ def method_missing(meth, *args)
55
+ if raw[:readings].has_key?(meth.to_s.upcase)
56
+ Reading.new(raw[:readings][meth.to_s.upcase])
57
+ else
58
+ super
59
+ end
60
+ end
61
+
62
+ end
63
+
64
+ end
@@ -0,0 +1,39 @@
1
+ module DmmUtil
2
+ class Meter
3
+ attr_reader :driver
4
+
5
+ def initialize(driver)
6
+ @driver = driver
7
+ end
8
+
9
+ def recordings
10
+ Cursor.new(driver, :recording, :qrsi, Recording)
11
+ end
12
+
13
+ def saved_measurements
14
+ Cursor.new(driver, :measurement, :qsmr, Measurement)
15
+ end
16
+
17
+ def saved_min_max
18
+ Cursor.new(driver, :min_max, :qmmsi, Measurement)
19
+ end
20
+
21
+ def saved_peak
22
+ Cursor.new(driver, :peak, :qpsi, Measurement)
23
+ end
24
+
25
+ def measure_now
26
+ Measurement.new(driver.qddb)
27
+ end
28
+
29
+ end
30
+
31
+ class MeterError < RuntimeError
32
+ attr_reader :status
33
+ def initialize(msg, status = nil)
34
+ super msg
35
+ @status = status
36
+ end
37
+ end
38
+
39
+ end
@@ -0,0 +1,64 @@
1
+ module DmmUtil
2
+ class Reading
3
+ attr_reader :raw
4
+
5
+ MULTIPLIER_MAP = {
6
+ -12 => "p",
7
+ -9 => "n",
8
+ -6 => "u",
9
+ -3 => "m",
10
+ -2 => "c",
11
+ -1 => "d",
12
+ 0 => "",
13
+ 1 => "D",
14
+ 2 => "h",
15
+ 3 => "k",
16
+ 6 => "M",
17
+ 9 => "G",
18
+ 12 => "T"
19
+ }
20
+
21
+ def initialize(attrs)
22
+ @raw = attrs
23
+ end
24
+
25
+ def ts
26
+ raw[:ts]
27
+ end
28
+
29
+ def value
30
+ raw[:value]
31
+ end
32
+
33
+ def unit
34
+ raw[:unit]
35
+ end
36
+
37
+ def scaled_value
38
+ decimals = @raw[:decimals]
39
+ multiplier = @raw[:unit_multiplier]
40
+ state = @raw[:state]
41
+
42
+ if state == "NORMAL"
43
+ val = "%.#{decimals}f" % (value / (10 ** multiplier))
44
+ elsif state == "OL_MINUS"
45
+ val = "-OL"
46
+ else
47
+ val = state
48
+ end
49
+
50
+ [val, "#{MULTIPLIER_MAP[multiplier]}#{unit}"]
51
+ end
52
+
53
+ def to_s
54
+ sv = scaled_value
55
+ "#{sv.first} #{sv.last}"
56
+ end
57
+
58
+ def ==(other)
59
+ value == other.value && unit == other.unit
60
+ end
61
+
62
+
63
+ end
64
+ end
@@ -0,0 +1,35 @@
1
+ module DmmUtil
2
+ class Recording
3
+ attr_reader :raw
4
+
5
+ def initialize(driver, attrs)
6
+ @driver = driver
7
+ @raw = attrs
8
+ end
9
+
10
+ def name
11
+ raw[:name]
12
+ end
13
+
14
+ def start_ts
15
+ raw[:start_ts]
16
+ end
17
+
18
+ def end_ts
19
+ raw[:end_ts]
20
+ end
21
+
22
+ def seq_no
23
+ raw[:seq_no]
24
+ end
25
+
26
+ def num_samples
27
+ raw[:num_samples]
28
+ end
29
+
30
+ def measurements
31
+ RecordingMeasurementCursor.new(@driver, self)
32
+ end
33
+
34
+ end
35
+ end