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.
@@ -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