katcp 0.0.7 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- def initialize(remote_host, remote_port=7147, local_host=nil, local_port=nil)
39
- super(remote_host, remote_port, local_host, local_port)
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
- # Save attr name
64
- @device_attrs << dev
65
- # Dynamically define methods
66
- instance_eval <<-"_end"
67
- def #{dev}(*args)
68
- read('#{dev}', *args)
69
- end
70
- def #{dev}=(*args)
71
- write('#{dev}', 0, *args)
72
- end
73
- _end
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 defined
82
- # dynamically.
83
- def undefine_device_attrs # :nodoc
84
- @device_attrs.each do |dev|
85
- instance_eval <<-"_end"
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, but uses a bulkread request rather than a 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
- request(:progdev, *args)
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>status.ok?</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(:status)
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
- def initialize(remote_host, remote_port=7147, local_host=nil, local_port=nil)
17
-
18
- # Save remote_host and remote_port for #inspect
19
- @remote_host = remote_host
20
- @remote_port = remote_port
21
-
22
- # @socket is the socket connecting to the KATCP server
23
- @socket = TCPSocket.new(remote_host, remote_port, local_host, local_port)
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
- # TODO: Monkey-patch gets so that it recognizes "\r" or "\n" as line
44
- # endings. Currently only recognizes fixed strings, so for now go with
45
- # "\n".
46
- while line = @socket.gets("\n") do
47
- # Split line into words and unescape each word
48
- words = line.chomp.split(/[ \t]+/).map! {|w| w.katcp_unescape!}
49
- # Handle requests, replies, and informs based on first character of
50
- # first word.
51
- case words[0][0,1]
52
- # Request
53
- when '?'
54
- # TODO Send 'unsupported' reply (or support requests from server?)
55
- # Reply
56
- when '!'
57
- # TODO: Raise exception if name is not same as @reqname?
58
- # TODO: Raise exception on non-ok?
59
- # Enqueue words to @rxq
60
- @rxq.enq(words)
61
- # Inform
62
- when '#'
63
- # If the name is same as @reqname
64
- if @reqname && @reqname == words[0][1..-1]
65
- # Enqueue words to @rxq
66
- @rxq.enq(words)
67
- else
68
- # Must be asynchronous inform message, add to list.
69
- line.katcp_unescape!
70
- line.chomp!
71
- @informs << line
72
- end
73
- else
74
- # Malformed line
75
- # TODO: Log error better?
76
- warn "malformed line: #{line.inspect}"
77
- end
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
- end # @socket.each_line block
197
+ self
198
+ end #connect
80
199
 
81
- warn "Read on socket returned EOF"
82
- #TODO Close socket? Push EOF flag into @rxq?
83
- end # Thread.new block
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
- name = name.to_s.gsub('_','-')
238
+ reqname = name.to_s.gsub('_','-')
109
239
 
110
240
  # Escape arguments
111
- arguments.map! {|arg| arg.to_s.katcp_escape}
112
-
113
- # Create response
114
- resp = Response.new
115
-
116
- # Get lock on @reqlock
117
- @reqlock.synchronize do
118
- # Store request name
119
- @reqname = name
120
- # Send request
121
- req = "?#{[name, *arguments].join(' ')}\n"
122
- @socket.print req
123
- # Loop on reply queue until done or error
124
- begin
125
- words = @rxq.deq
126
- resp << words
127
- end until words[0][0,1] == '!'
128
- # Clear request name
129
- @reqname = nil
130
- end
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
- s = "#{@remote_host}:#{@remote_port}"
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
@@ -3,5 +3,5 @@
3
3
  #++
4
4
 
5
5
  module KATCP
6
- VERSION = "0.0.7"
6
+ VERSION = "0.1.4"
7
7
  end
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: 17
4
+ hash: 19
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 0
9
- - 7
10
- version: 0.0.7
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: 2012-09-06 00:00:00 Z
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.0.7 Documentation
62
+ - Ruby/KATCP 0.1.4 Documentation
63
63
  require_paths:
64
64
  - lib
65
65
  required_ruby_version: !ruby/object:Gem::Requirement