katcp 0.0.7 → 0.1.4
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.
- data/lib/katcp/client/roach.rb +328 -35
- data/lib/katcp/client.rb +228 -69
- data/lib/katcp/version.rb +1 -1
- metadata +6 -6
data/lib/katcp/client/roach.rb
CHANGED
@@ -3,6 +3,102 @@ require 'katcp/client'
|
|
3
3
|
# Holds KATCP related classes etc.
|
4
4
|
module KATCP
|
5
5
|
|
6
|
+
# Class used to access BRAMs
|
7
|
+
class Bram
|
8
|
+
def initialize(katcp_client, bram_name)
|
9
|
+
@katcp_client = katcp_client
|
10
|
+
@bram_name = bram_name
|
11
|
+
end
|
12
|
+
|
13
|
+
# Calls @katcp_client.bulkread(@bram_name, *args)
|
14
|
+
def [](*args)
|
15
|
+
@katcp_client.bulkread(@bram_name, *args)
|
16
|
+
end
|
17
|
+
alias :get :[]
|
18
|
+
|
19
|
+
# Calls @katcp_client.write(@bram_name, *args)
|
20
|
+
def []=(*args)
|
21
|
+
@katcp_client.write(@bram_name, *args)
|
22
|
+
end
|
23
|
+
alias :set :[]=
|
24
|
+
end
|
25
|
+
|
26
|
+
# Class used to access 10 GbE cores
|
27
|
+
class TenGE < Bram
|
28
|
+
# Read a 64 bit value big endian value starting at 32-bit word offset
|
29
|
+
# +addr+.
|
30
|
+
def read64(addr)
|
31
|
+
hi, lo = get(addr,2).to_a
|
32
|
+
((hi & 0xffff) << 32) | (lo & 0xffffffff)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Write +val64+ as a 64 bit value big endian value starting at 32-bit word
|
36
|
+
# offset +addr+.
|
37
|
+
def write64(addr, val64)
|
38
|
+
hi = ((val64 >> 32) & 0xffff)
|
39
|
+
lo = val64 & 0xffffffff
|
40
|
+
set(addr, hi, lo)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Return MAC address of 10 GbE core as 48-bit value.
|
44
|
+
def mac
|
45
|
+
read64(0)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Set MAC address of 10 GbE core to 48-bit value +m+.
|
49
|
+
def mac=(m)
|
50
|
+
write64(0, m)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Get gateway IP address as 32-bit integer.
|
54
|
+
def gw ; get(3) ; end
|
55
|
+
# Set gateway IP address to 32-bit integer +a+.
|
56
|
+
def gw=(a); set(3, a); end
|
57
|
+
|
58
|
+
# Get source IP address as 32-bit integer.
|
59
|
+
def ip ; get(4) ; end
|
60
|
+
# Set source IP address to 32-bit integer +a+.
|
61
|
+
def ip=(a); set(4, a); end
|
62
|
+
|
63
|
+
# Get local rx port as 16-bit integer.
|
64
|
+
def port ; get(8) & 0xffff ; end
|
65
|
+
# Set local rx port to 16-bit integer +p+.
|
66
|
+
def port=(p); set(8, (get(8) & 0xffff0000) | (p & 0xffff)); end
|
67
|
+
|
68
|
+
# Returns xaui status word. Bits 2 through 5 are lane sync, bit 6 is
|
69
|
+
# channel bonding status.
|
70
|
+
def xaui_status; get(9); end
|
71
|
+
# Four least significant bits represent sync status for each lane.
|
72
|
+
# 1 bit = lane sync OK
|
73
|
+
# 0 bit = lane sync BAD
|
74
|
+
# Proper operation requires all four lanes to have good sync status, so 15
|
75
|
+
# (0b1111) is the desired value.
|
76
|
+
def xaui_sync; (get(9) >> 2) & 0b1111; end
|
77
|
+
# Returns true if #xaui_sync returns 15
|
78
|
+
def xaui_sync_ok?; xaui_sync == 0b1111; end
|
79
|
+
# Returns true if all four XAUI lanes are bonded
|
80
|
+
def xaui_bonded?; (get(9) >> 6) & 1; end
|
81
|
+
|
82
|
+
# Get current value of rx_eq_mix parameter.
|
83
|
+
def rx_eq_mix ; (get(10) >> 24) & 0xff; end
|
84
|
+
# Get current value of rx_eq_pol parameter.
|
85
|
+
def rx_eq_pol ; (get(10) >> 16) & 0xff; end
|
86
|
+
# Get current value of tx_preemph parameter.
|
87
|
+
def tx_preemph ; (get(10) >> 8) & 0xff; end
|
88
|
+
# Get current value of tx_diff_ctrl parameter.
|
89
|
+
def tx_diff_ctrl; (get(10) ) & 0xff; end
|
90
|
+
|
91
|
+
# Returns current value of ARP table entry +idx+.
|
92
|
+
def [](idx)
|
93
|
+
read64(0xc00+2*idx)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Sets value of ARP table entry +idx+ to +mac+.
|
97
|
+
def []=(idx, mac)
|
98
|
+
write64(0xc00+2*idx, mac)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
6
102
|
# Facilitates talking to <tt>tcpborphserver2</tt>, a KATCP server
|
7
103
|
# implementation that runs on ROACH boards. In addition to providing
|
8
104
|
# convenience wrappers around <tt>tcpborphserver2</tt> requests, it also adds
|
@@ -15,7 +111,8 @@ module KATCP
|
|
15
111
|
# writer attributes (i.e. methods) for gateware devices as the FPGA is
|
16
112
|
# programmed (or de-programmed). This not only provides for very clean and
|
17
113
|
# readable code, it also provides convenient tab completion in irb for
|
18
|
-
# gateware specific device names.
|
114
|
+
# gateware specific device names. Subclasses can exert some control over
|
115
|
+
# the dynamic method genreation. See #device_typemap for details.
|
19
116
|
#
|
20
117
|
# * Word based instead of byte based data offsets and counts. KATCP::Client
|
21
118
|
# data transfer methods #read and #write deal with byte based offsets and
|
@@ -32,25 +129,67 @@ module KATCP
|
|
32
129
|
# is currently programmed.
|
33
130
|
attr_reader :devices
|
34
131
|
|
132
|
+
# call-seq: RoachClient.new([remote_host, remote_port=7147, local_host=nil, local_port=nil,] opts={}) -> RoachClient
|
133
|
+
#
|
35
134
|
# Creates a RoachClient that connects to a KATCP server at +remote_host+ on
|
36
135
|
# +remote_port+. If +local_host+ and +local_port+ are specified, then
|
37
136
|
# those parameters are used on the local end to establish the connection.
|
38
|
-
|
39
|
-
|
137
|
+
# Positional parameters can be used OR parameters can be passed via the
|
138
|
+
# +opts+ Hash.
|
139
|
+
#
|
140
|
+
# Supported keys for the +opts+ Hash are:
|
141
|
+
#
|
142
|
+
# :remote_host Specifies hostname of KATCP server
|
143
|
+
# :remote_port Specifies port used by KATCP server (default 7147)
|
144
|
+
# :local_host Specifies local interface to bind to (default nil)
|
145
|
+
# :local_port Specifies local port to bind to (default nil)
|
146
|
+
# :typemap Provides a default device typemap (default {}).
|
147
|
+
# See #device_typemap for details.
|
148
|
+
def initialize(*args)
|
40
149
|
# List of all devices
|
41
150
|
@devices = [];
|
42
151
|
# List of dynamically defined device attrs (readers only, writers implied)
|
43
152
|
@device_attrs = [];
|
153
|
+
# @device objects is a Hash of created device objects: key is Class,
|
154
|
+
# value is a Hash mapping device name to instance of Class.
|
155
|
+
@device_objects = {}
|
156
|
+
# Call super *after* initializing subclass instance variables
|
157
|
+
super(*args)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Override KATCP::Client#connect to perform subclass specific
|
161
|
+
# post-connection setup.
|
162
|
+
def connect
|
163
|
+
super
|
164
|
+
# Determine which commands are supported by the server
|
165
|
+
commands = request(:help).to_s
|
166
|
+
# Determine whether bulkread is used by server
|
167
|
+
@bulkread = commands.index('#help bulkread') ? :bulkread : :read
|
168
|
+
# Determine whether status or fpgastatus command is used by server
|
169
|
+
@fpgastatus = commands.index('#help fpgastatus') ? :fpgastatus : :status
|
170
|
+
# Define device-specific attributes (if device is programmed)
|
44
171
|
define_device_attrs
|
172
|
+
self
|
173
|
+
end
|
174
|
+
|
175
|
+
# Override KATCP::Client#close to perform subclass specific post-close
|
176
|
+
# cleanup. Be sure to call super afterwards!
|
177
|
+
def close
|
178
|
+
# Undefine device-specific attributes (if device is programmed)
|
179
|
+
undefine_device_attrs
|
180
|
+
ensure
|
181
|
+
super
|
45
182
|
end
|
46
183
|
|
47
184
|
# Dynamically define attributes (i.e. methods) for gateware devices, if
|
48
|
-
# currently programmed.
|
49
|
-
def define_device_attrs # :nodoc
|
185
|
+
# currently programmed. See #device_typemap for more details.
|
186
|
+
def define_device_attrs # :nodoc:
|
50
187
|
# First undefine existing device attrs
|
51
188
|
undefine_device_attrs
|
52
189
|
# Define nothing if FPGA not programmed
|
53
190
|
return unless programmed?
|
191
|
+
# Get device typemap (possibly from subclass override!)
|
192
|
+
typemap = device_typemap || {}
|
54
193
|
# Dynamically define accessors for all devices (i.e. registers, BRAMs,
|
55
194
|
# etc.) except those whose names conflict with existing methods.
|
56
195
|
@devices = listdev.lines[0..-2].map {|l| l[1]}
|
@@ -60,17 +199,19 @@ module KATCP
|
|
60
199
|
|
61
200
|
# Define methods unless they conflict with existing methods
|
62
201
|
if ! respond_to?(dev) && ! respond_to?("#{dev}=")
|
63
|
-
#
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
202
|
+
# Determine type (and aliases) of this device
|
203
|
+
type, *aliases = typemap[dev] || typemap[dev.to_sym]
|
204
|
+
next if type == :skip
|
205
|
+
# Dynamically define methods and aliases
|
206
|
+
case type
|
207
|
+
when Class; device_object(type, dev, *aliases)
|
208
|
+
when :bram; device_object(Bram, dev, *aliases)
|
209
|
+
when :tenge; device_object(TenGE, dev, *aliases)
|
210
|
+
when :roreg; roreg(dev, *aliases)
|
211
|
+
# else :rwreg or nil (or anything else for that matter) so treat it
|
212
|
+
# as R/W register.
|
213
|
+
else rwreg(dev, *aliases)
|
214
|
+
end
|
74
215
|
end
|
75
216
|
end
|
76
217
|
self
|
@@ -78,24 +219,159 @@ module KATCP
|
|
78
219
|
|
79
220
|
protected :define_device_attrs
|
80
221
|
|
81
|
-
# Undefine any attributes (i.e. methods) that were previously
|
82
|
-
# dynamically.
|
83
|
-
def undefine_device_attrs # :nodoc
|
84
|
-
@device_attrs.each do |
|
85
|
-
instance_eval
|
86
|
-
class << self
|
87
|
-
remove_method '#{dev}'
|
88
|
-
remove_method '#{dev}='
|
89
|
-
end
|
90
|
-
_end
|
222
|
+
# Undefine any attributes (i.e. methods) and aliases that were previously
|
223
|
+
# defined dynamically.
|
224
|
+
def undefine_device_attrs # :nodoc:
|
225
|
+
@device_attrs.each do |name|
|
226
|
+
instance_eval "class << self; remove_method '#{name}'; end"
|
91
227
|
end
|
92
228
|
@device_attrs.clear
|
93
229
|
@devices.clear
|
230
|
+
# TODO Support cleanup call for device objects?
|
231
|
+
@device_objects.clear
|
94
232
|
self
|
95
233
|
end
|
96
234
|
|
97
235
|
protected :undefine_device_attrs
|
98
236
|
|
237
|
+
# This method's return value controls how methods and aliases are
|
238
|
+
# dynamically generated for devices within the ROACH gateware. If
|
239
|
+
# #device_typemap returns +nil+ or an empty Hash, all devices will be
|
240
|
+
# treated as read/write registers. Otherwise, #device_typemap must
|
241
|
+
# return a Hash-like object. If the object returned by #device_typemap
|
242
|
+
# contains a key (String or Symbol) for a given device name, the key's
|
243
|
+
# corresponding value specifies how to treat that device when dynamically
|
244
|
+
# generating accessor methods for it and whether to generate any aliases
|
245
|
+
# for it. If no key exists for a given device, the device will be treated
|
246
|
+
# as a read/write register. The corresponding value can be one of:
|
247
|
+
#
|
248
|
+
# :roreg (Read-only register) Only a reader method will be created.
|
249
|
+
# :rwreg (Read-write register) Both reader and writer methods will be
|
250
|
+
# created.
|
251
|
+
# :bram (Shared BRAM) A reader method returning a Bram object
|
252
|
+
# will be created. The returned Bram object
|
253
|
+
# provides convenient ways to read and write
|
254
|
+
# to the Bram device.
|
255
|
+
# :tenge (10 GbE) A reader method returning a TenGE object
|
256
|
+
# will be created. The returned TenGE object
|
257
|
+
# provides convenient ways to read and write
|
258
|
+
# to the TenGE device.
|
259
|
+
# :skip (unwanted device) No method will be created.
|
260
|
+
#
|
261
|
+
# Methods are only created for devices that actually exist on the device.
|
262
|
+
# If no device exists for a given key, no methods will be created for that
|
263
|
+
# key. In other words, regardless of the keys given, methods will not be
|
264
|
+
# created unless they are backed by an actual device. Both reader and
|
265
|
+
# writer methods are created for devices for which no key is present.
|
266
|
+
#
|
267
|
+
# The value can also be an Array whose first element is a Symbol from the
|
268
|
+
# list above. The remaining elements specify aliases to be created for the
|
269
|
+
# given attribute methods.
|
270
|
+
#
|
271
|
+
# RoachClient#device_typemap returns on empty Hash so all devices are
|
272
|
+
# treated as read/write registers by default. Gateware specific subclasses
|
273
|
+
# of RoachClient can override #device_typemap method to return a object
|
274
|
+
# containing a Hash tailored to a specific gateware design.
|
275
|
+
#
|
276
|
+
# Example: The following would lead to the creation of the following
|
277
|
+
# methods and aliases: "input_selector", "input_selector=", "insel",
|
278
|
+
# "insel=", "switch_gbe_status", "switch_gbe", "adc_rms_levels" (assuming
|
279
|
+
# the named devices all exist!). No methods would be created for the
|
280
|
+
# device named "unwanted_reg" even if it exists.
|
281
|
+
#
|
282
|
+
# class MyRoachDesign < RoachClient
|
283
|
+
# DEVICE_TYPEMAP = {
|
284
|
+
# :input_selector => [:rwreg, :insel],
|
285
|
+
# :switch_gbe_status => :roreg,
|
286
|
+
# :switch_gbe => :tenge,
|
287
|
+
# :adc_rms_levels => :bram,
|
288
|
+
# :unwanted_reg => :skip
|
289
|
+
# }
|
290
|
+
#
|
291
|
+
# def device_typemap
|
292
|
+
# DEVICE_TYPEMAP
|
293
|
+
# end
|
294
|
+
# end
|
295
|
+
#
|
296
|
+
# Returns the default device typemap Hash (either the one passed to the
|
297
|
+
# constructor or an empty Hash). Design specific subclasses can override
|
298
|
+
# this method to return a design specific device typemap.
|
299
|
+
def device_typemap
|
300
|
+
(@opts && @opts[:typemap]) || {}
|
301
|
+
end
|
302
|
+
|
303
|
+
# Allow subclasses to create read accessor method (with optional aliases)
|
304
|
+
# Create read accessor method (with optional aliases)
|
305
|
+
# for register. Converts reg_name to method name by replacing '/' with
|
306
|
+
# '_'. Typically used with read-only registers.
|
307
|
+
def roreg(reg_name, *aliases) # :nodoc:
|
308
|
+
method_name = reg_name.to_s.gsub('/', '_')
|
309
|
+
instance_eval <<-"_end"
|
310
|
+
class << self
|
311
|
+
def #{method_name}(off=0,len=1); read('#{reg_name}',off,len); end
|
312
|
+
end
|
313
|
+
_end
|
314
|
+
@device_attrs << method_name
|
315
|
+
aliases.each do |a|
|
316
|
+
a = a.to_s.gsub('/', '_')
|
317
|
+
instance_eval "class << self; alias #{a} #{method_name}; end"
|
318
|
+
@device_attrs << a
|
319
|
+
end
|
320
|
+
self
|
321
|
+
end
|
322
|
+
|
323
|
+
protected :roreg
|
324
|
+
|
325
|
+
# Allow subclasses to create read and write accessor methods (with optional
|
326
|
+
# Create read and write accessor methods (with optional
|
327
|
+
# aliases) for register. Converts reg_name to method name by replacing '/'
|
328
|
+
# with '_'.
|
329
|
+
def rwreg(reg_name, *aliases) # :nodoc:
|
330
|
+
roreg(reg_name, *aliases)
|
331
|
+
method_name = reg_name.to_s.gsub('/', '_')
|
332
|
+
instance_eval <<-"_end"
|
333
|
+
class << self
|
334
|
+
def #{method_name}=(v,off=0); write('#{reg_name}',off,v); end
|
335
|
+
end
|
336
|
+
_end
|
337
|
+
@device_attrs << "#{method_name}="
|
338
|
+
aliases.each do |a|
|
339
|
+
a = a.to_s.gsub('/', '_')
|
340
|
+
instance_eval "class << self; alias #{a}= #{method_name}=; end"
|
341
|
+
@device_attrs << "#{a}="
|
342
|
+
end
|
343
|
+
self
|
344
|
+
end
|
345
|
+
|
346
|
+
protected :rwreg
|
347
|
+
|
348
|
+
# Create accessor method (with optional aliases) for a device object backed
|
349
|
+
# by instance of Class referred to by clazz. clazz.to_s must return the
|
350
|
+
# name of a Class whose initialize method accepts two parameters: a
|
351
|
+
# KATCP::Client instance and the name of the device.
|
352
|
+
def device_object(clazz, name, *aliases) # :nodoc:
|
353
|
+
name = name.to_s
|
354
|
+
method_name = name.gsub('/', '_')
|
355
|
+
instance_eval <<-"_end"
|
356
|
+
class << self
|
357
|
+
def #{method_name}()
|
358
|
+
@device_objects[#{clazz}] ||= {}
|
359
|
+
@device_objects[#{clazz}]['#{name}'] ||= #{clazz}.new(self, '#{name}')
|
360
|
+
@device_objects[#{clazz}]['#{name}']
|
361
|
+
end
|
362
|
+
end
|
363
|
+
_end
|
364
|
+
@device_attrs << method_name
|
365
|
+
aliases.each do |a|
|
366
|
+
a = a.to_s.gsub('/', '_')
|
367
|
+
instance_eval "class << self; alias #{a} #{method_name}; end"
|
368
|
+
@device_attrs << "#{a}"
|
369
|
+
end
|
370
|
+
self
|
371
|
+
end
|
372
|
+
|
373
|
+
protected :device_object
|
374
|
+
|
99
375
|
# Returns +true+ if the current design has a device named +device+.
|
100
376
|
def has_device?(device)
|
101
377
|
@devices.include?(device.to_s)
|
@@ -114,16 +390,19 @@ module KATCP
|
|
114
390
|
# unless +word_count+ is given in which case it returns an
|
115
391
|
# NArray.int(word_count).
|
116
392
|
#
|
117
|
-
# Equivalent to #read
|
118
|
-
# request.
|
393
|
+
# Equivalent to #read (but uses a bulkread request rather than a read
|
394
|
+
# request if bulkread is supported).
|
119
395
|
def bulkread(register_name, *args)
|
396
|
+
# Defer to #read unless server provides bulkread command
|
397
|
+
return read(register_name, *args) unless @bulkread == :bulkread
|
398
|
+
|
120
399
|
byte_offset = 4 * (args[0] || 0)
|
121
400
|
byte_count = 4 * (args[1] || 1)
|
122
401
|
raise 'word count must be non-negative' if byte_count < 0
|
123
402
|
resp = request(:bulkread, register_name, byte_offset, byte_count)
|
124
403
|
raise resp.to_s unless resp.ok?
|
125
404
|
data = resp.lines[0..-2].map{|l| l[1]}.join
|
126
|
-
if args.length <= 1
|
405
|
+
if args.length <= 1 || args[1] == 1
|
127
406
|
data.unpack('N')[0]
|
128
407
|
else
|
129
408
|
data.to_na(NArray::INT).ntoh
|
@@ -162,12 +441,16 @@ module KATCP
|
|
162
441
|
request(:listdev, :size).sort!
|
163
442
|
end
|
164
443
|
|
444
|
+
# This is the default timeout to use when programming a bitstream via
|
445
|
+
# #progdev.
|
446
|
+
PROGDEV_SOCKET_TIMEOUT = 5
|
447
|
+
|
165
448
|
# call-seq:
|
166
449
|
# progdev -> KATCP::Response
|
167
450
|
# progdev(image_file) -> KATCP::Response
|
168
451
|
#
|
169
452
|
# Programs a gateware image specified by +image_file+. If +image_file+ is
|
170
|
-
# omitted, de-programs the FPGA.
|
453
|
+
# omitted or nil, de-programs the FPGA.
|
171
454
|
#
|
172
455
|
# Whenever the FPGA is programmed, reader and writer attributes (i.e.
|
173
456
|
# methods) are defined for every device listed by #listdev except for
|
@@ -177,12 +460,22 @@ module KATCP
|
|
177
460
|
# attributes that were dynamically defined for the previous design are
|
178
461
|
# removed.
|
179
462
|
def progdev(*args)
|
180
|
-
|
463
|
+
prev_socket_timeout = @socket_timeout
|
464
|
+
begin
|
465
|
+
# Adjust @socket_timeout if programming a bitstream
|
466
|
+
@socket_timeout = PROGDEV_SOCKET_TIMEOUT if args[0]
|
467
|
+
request(:progdev, *args)
|
468
|
+
ensure
|
469
|
+
@socket_timeout = prev_socket_timeout
|
470
|
+
end
|
181
471
|
define_device_attrs
|
182
472
|
end
|
183
473
|
|
184
474
|
# Returns true if currently programmed (specifically, it is equivalent to
|
185
|
-
# <tt>
|
475
|
+
# <tt>request(@fpgastatus).ok?</tt>). Older versions of tcpborphserver
|
476
|
+
# used the "status" command, while newer versions use the "fpgastatus"
|
477
|
+
# command for the same purpose. The @connect method checks which is used
|
478
|
+
# by the server and sets @fpgastatus accordingly.
|
186
479
|
def programmed?
|
187
480
|
status.ok?
|
188
481
|
end
|
@@ -208,7 +501,7 @@ module KATCP
|
|
208
501
|
resp = request(:read, register_name, byte_offset, byte_count)
|
209
502
|
raise resp.to_s unless resp.ok?
|
210
503
|
data = resp.payload
|
211
|
-
if args.length <= 1
|
504
|
+
if args.length <= 1 || args[1] == 1
|
212
505
|
data.unpack('N')[0]
|
213
506
|
else
|
214
507
|
data.to_na(NArray::INT).ntoh
|
@@ -223,7 +516,7 @@ module KATCP
|
|
223
516
|
#
|
224
517
|
# Reports if gateware has been programmed.
|
225
518
|
def status
|
226
|
-
request(
|
519
|
+
request(@fpgastatus||:status)
|
227
520
|
end
|
228
521
|
|
229
522
|
# call-seq:
|
@@ -292,7 +585,7 @@ module KATCP
|
|
292
585
|
elsif byte_count == 0
|
293
586
|
warn "writing 0 bytes to #{register_name}"
|
294
587
|
end
|
295
|
-
resp = request(:write, register_name, byte_offset, data)
|
588
|
+
resp = request(:write, register_name, byte_offset, data, byte_count)
|
296
589
|
raise resp.to_s unless resp.ok?
|
297
590
|
self
|
298
591
|
end
|
data/lib/katcp/client.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'thread'
|
1
3
|
require 'monitor'
|
2
4
|
require 'socket'
|
3
5
|
|
@@ -10,17 +12,33 @@ module KATCP
|
|
10
12
|
# Facilitates talking to a KATCP server.
|
11
13
|
class Client
|
12
14
|
|
15
|
+
# Default timeout for socket operations (in seconds)
|
16
|
+
DEFAULT_SOCKET_TIMEOUT = 0.25
|
17
|
+
|
18
|
+
# call-seq: Client.new([remote_host, remote_port=7147, local_host=nil, local_port=nil,] opts={}) -> Client
|
19
|
+
#
|
13
20
|
# Creates a KATCP client that connects to a KATCP server at +remote_host+
|
14
21
|
# on +remote_port+. If +local_host+ and +local_port+ are specified, then
|
15
22
|
# those parameters are used on the local end to establish the connection.
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
23
|
+
# Positional parameters can be used OR parameters can be passed via the
|
24
|
+
# +opts+ Hash.
|
25
|
+
#
|
26
|
+
# Supported keys for the +opts+ Hash are:
|
27
|
+
#
|
28
|
+
# :remote_host Specifies hostname of KATCP server
|
29
|
+
# :remote_port Specifies port used by KATCP server (default 7147)
|
30
|
+
# :local_host Specifies local interface to bind to (default nil)
|
31
|
+
# :local_port Specifies local port to bind to (default nil)
|
32
|
+
def initialize(*args)
|
33
|
+
# If final arg is a Hash, pop it off
|
34
|
+
@opts = (Hash === args[-1]) ? args.pop : {}
|
35
|
+
|
36
|
+
# Save parameters
|
37
|
+
remote_host, remote_port, local_host, local_port = args
|
38
|
+
@remote_host = remote_host ? remote_host.to_s : @opts[:remote_host].to_s
|
39
|
+
@remote_port = remote_port || @opts[:remote_port] || 7147
|
40
|
+
@local_host = local_host || @opts[:local_host]
|
41
|
+
@local_port = local_port || @opts[:local_port]
|
24
42
|
|
25
43
|
# Init attribute(s)
|
26
44
|
@informs = []
|
@@ -38,49 +56,158 @@ module KATCP
|
|
38
56
|
# @rxq is an inter-thread queue
|
39
57
|
@rxq = Queue.new
|
40
58
|
|
59
|
+
# Timeout value for socket operations
|
60
|
+
@socket_timeout = DEFAULT_SOCKET_TIMEOUT
|
61
|
+
|
62
|
+
# No socket yet
|
63
|
+
@socket = nil
|
64
|
+
|
65
|
+
# Try to connect socket and start listener thread, but stifle exception
|
66
|
+
# if it fails because we need object creation to succeed even if connect
|
67
|
+
# doesn't. Each request attempt will try to reconnect if needed.
|
68
|
+
# TODO Warn if connection fails?
|
69
|
+
connect rescue self
|
70
|
+
end
|
71
|
+
|
72
|
+
# Connect socket and start listener thread
|
73
|
+
def connect
|
74
|
+
# Close existing connection (if any)
|
75
|
+
close
|
76
|
+
|
77
|
+
# Create new socket.
|
78
|
+
@socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
|
79
|
+
sockaddr = Socket.sockaddr_in(@remote_port, @remote_host)
|
80
|
+
|
81
|
+
# Do non-blocking connect
|
82
|
+
begin
|
83
|
+
@socket.connect_nonblock(sockaddr)
|
84
|
+
rescue Errno::EINPROGRESS
|
85
|
+
# Wait with timeout for @socket to become writable.
|
86
|
+
# Is DEFAULT_SOCKET_TIMEOUT seconds an OK timeout for connect?
|
87
|
+
rd_wr_err = select([], [@socket], [], DEFAULT_SOCKET_TIMEOUT)
|
88
|
+
if rd_wr_err.nil?
|
89
|
+
# Timeout during connect, close socket and raise TimeoutError
|
90
|
+
@socket.close
|
91
|
+
raise TimeoutError.new(
|
92
|
+
'connection timed out in %.3f seconds' % DEFAULT_SOCKET_TIMEOUT)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Verify that we're connected
|
96
|
+
begin
|
97
|
+
@socket.connect_nonblock(sockaddr)
|
98
|
+
rescue Errno::EISCONN # expected
|
99
|
+
# ignore
|
100
|
+
rescue # anything else, unexpected
|
101
|
+
# Close socket and re-raise
|
102
|
+
@socket.close
|
103
|
+
raise
|
104
|
+
end
|
105
|
+
end # non-blocking connect
|
106
|
+
|
41
107
|
# Start thread that reads data from server.
|
42
108
|
Thread.new do
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
109
|
+
catch :giveup do
|
110
|
+
while true
|
111
|
+
begin
|
112
|
+
req_timeouts = 0
|
113
|
+
while req_timeouts < 2
|
114
|
+
# Use select to wait with timeout for data or error
|
115
|
+
rd_wr_err = select([@socket], [], [@socket], @socket_timeout)
|
116
|
+
|
117
|
+
# Handle timeout
|
118
|
+
if rd_wr_err.nil?
|
119
|
+
# Timeout, increment req_timeout if we're expecting a reply,
|
120
|
+
# then try again
|
121
|
+
req_timeouts += 1 if @reqname
|
122
|
+
next
|
123
|
+
end
|
124
|
+
|
125
|
+
# Handle error
|
126
|
+
if rd_wr_err[2][0]
|
127
|
+
# Ignore unless we're expected a reply
|
128
|
+
next unless @reqname
|
129
|
+
# Otherwise, send double-bang error response, and give up
|
130
|
+
@rxq.enq(['!!socket-error'])
|
131
|
+
throw :giveup
|
132
|
+
end
|
133
|
+
|
134
|
+
# OK to read!
|
135
|
+
|
136
|
+
# TODO: Monkey-patch gets so that it recognizes "\r" or "\n" as
|
137
|
+
# line endings. Currently only recognizes fixed strings, so for
|
138
|
+
# now go with "\n".
|
139
|
+
line = @socket.gets("\n")
|
140
|
+
|
141
|
+
# If EOF
|
142
|
+
if line.nil?
|
143
|
+
# Send double-bang error response, and give up
|
144
|
+
@rxq.enq(['!!socket-eof'])
|
145
|
+
throw :giveup
|
146
|
+
end
|
147
|
+
|
148
|
+
# Split line into words and unescape each word
|
149
|
+
words = line.chomp.split(/[ \t]+/).map! {|w| w.katcp_unescape!}
|
150
|
+
# Handle requests, replies, and informs based on first character
|
151
|
+
# of first word.
|
152
|
+
case words[0][0,1]
|
153
|
+
# Request
|
154
|
+
when '?'
|
155
|
+
# TODO Send 'unsupported' reply (or support requests from server?)
|
156
|
+
# Reply
|
157
|
+
when '!'
|
158
|
+
# TODO: Raise exception if name is not same as @reqname?
|
159
|
+
# TODO: Raise exception on non-ok?
|
160
|
+
# Enqueue words to @rxq
|
161
|
+
@rxq.enq(words)
|
162
|
+
# Inform
|
163
|
+
when '#'
|
164
|
+
# If the name is same as @reqname
|
165
|
+
if @reqname && @reqname == words[0][1..-1]
|
166
|
+
# Enqueue words to @rxq
|
167
|
+
@rxq.enq(words)
|
168
|
+
else
|
169
|
+
# Must be asynchronous inform message, add to list.
|
170
|
+
line.katcp_unescape!
|
171
|
+
line.chomp!
|
172
|
+
@informs << line
|
173
|
+
end
|
174
|
+
else
|
175
|
+
# Malformed line
|
176
|
+
# TODO: Log error better?
|
177
|
+
warn "malformed line: #{line.inspect}"
|
178
|
+
end # case words[0][0,1]
|
179
|
+
|
180
|
+
# Reset req_timeouts counter
|
181
|
+
req_timeouts = 0
|
182
|
+
|
183
|
+
end # while req_timeouts < 2
|
184
|
+
|
185
|
+
# Got 2 timeouts in a request!
|
186
|
+
# Send double-bang timeout response
|
187
|
+
@rxq.enq(['!!socket-timeout'])
|
188
|
+
throw :giveup
|
189
|
+
|
190
|
+
rescue Exception => e
|
191
|
+
$stderr.puts e; $stderr.flush
|
192
|
+
end # begin
|
193
|
+
end # while true
|
194
|
+
end # catch :giveup
|
195
|
+
end # Thread.new block
|
78
196
|
|
79
|
-
|
197
|
+
self
|
198
|
+
end #connect
|
80
199
|
|
81
|
-
|
82
|
-
|
83
|
-
|
200
|
+
# Close socket if it exists and is not already closed. Subclasses can
|
201
|
+
# override #close to perform additional cleanup as needed, but they must
|
202
|
+
# either close the socket themselves or call super.
|
203
|
+
def close
|
204
|
+
@socket.close if connected?
|
205
|
+
self
|
206
|
+
end
|
207
|
+
|
208
|
+
# Returns true if socket has been created and not closed
|
209
|
+
def connected?
|
210
|
+
!@socket.nil? && !@socket.closed?
|
84
211
|
end
|
85
212
|
|
86
213
|
# Return remote hostname
|
@@ -103,31 +230,63 @@ module KATCP
|
|
103
230
|
#
|
104
231
|
# TODO: Raise exception if reply is not OK?
|
105
232
|
def request(name, *arguments)
|
233
|
+
# (Re-)connect if @socket is in an invalid state
|
234
|
+
connect if @socket.nil? || @socket.closed?
|
235
|
+
|
106
236
|
# Massage name to allow Symbols and to allow '_' between words (since
|
107
237
|
# that is more natural for Symbols) in place of '-'
|
108
|
-
|
238
|
+
reqname = name.to_s.gsub('_','-')
|
109
239
|
|
110
240
|
# Escape arguments
|
111
|
-
arguments.map! {|arg| arg.to_s.katcp_escape}
|
112
|
-
|
113
|
-
#
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
#
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
241
|
+
reqargs = arguments.map! {|arg| arg.to_s.katcp_escape}
|
242
|
+
|
243
|
+
# TODO Find a more elegant way to code this retry loop?
|
244
|
+
attempts = 0
|
245
|
+
while true
|
246
|
+
attempts += 1
|
247
|
+
|
248
|
+
# Create response
|
249
|
+
resp = Response.new
|
250
|
+
|
251
|
+
# Give "words" scope outside of synchronize block
|
252
|
+
words = nil
|
253
|
+
|
254
|
+
# Get lock on @reqlock
|
255
|
+
@reqlock.synchronize do
|
256
|
+
# Store request name
|
257
|
+
@reqname = reqname
|
258
|
+
# Send request
|
259
|
+
req = "?#{[reqname, *reqargs].join(' ')}\n"
|
260
|
+
@socket.print req
|
261
|
+
# Loop on reply queue until done or error
|
262
|
+
begin
|
263
|
+
words = @rxq.deq
|
264
|
+
resp << words
|
265
|
+
end until words[0][0,1] == '!'
|
266
|
+
# Clear request name
|
267
|
+
@reqname = nil
|
268
|
+
end # @reqlock.synchronize
|
269
|
+
|
270
|
+
# Break out of retry loop unless double-bang reply
|
271
|
+
break unless words[0][0,2] == '!!'
|
272
|
+
|
273
|
+
# Double-bang reply!!
|
274
|
+
|
275
|
+
# If we've already attempted more than once (i.e. twice)
|
276
|
+
if attempts > 1
|
277
|
+
# Raise exception
|
278
|
+
case words[0]
|
279
|
+
when '!!socket-timeout'; raise TimeoutError.new(resp)
|
280
|
+
when '!!socket-error'; raise SocketError.new(resp)
|
281
|
+
when '!!socket-eof'; raise SocketEOF.new(resp)
|
282
|
+
else raise RuntimeError.new(resp)
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
# Reconnect and try again
|
287
|
+
connect
|
288
|
+
end # while true
|
289
|
+
|
131
290
|
resp
|
132
291
|
end
|
133
292
|
|
@@ -151,7 +310,7 @@ module KATCP
|
|
151
310
|
|
152
311
|
# Provides terse string representation of +self+.
|
153
312
|
def to_s
|
154
|
-
|
313
|
+
"#{@remote_host}:#{@remote_port}"
|
155
314
|
end
|
156
315
|
|
157
316
|
# Provides more detailed String representation of +self+
|
data/lib/katcp/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: katcp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 19
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
8
|
+
- 1
|
9
|
+
- 4
|
10
|
+
version: 0.1.4
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- David MacMahon
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date:
|
18
|
+
date: 2013-03-04 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: narray
|
@@ -59,7 +59,7 @@ rdoc_options:
|
|
59
59
|
- -m
|
60
60
|
- README
|
61
61
|
- --title
|
62
|
-
- Ruby/KATCP 0.
|
62
|
+
- Ruby/KATCP 0.1.4 Documentation
|
63
63
|
require_paths:
|
64
64
|
- lib
|
65
65
|
required_ruby_version: !ruby/object:Gem::Requirement
|