katcp 0.0.7 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|