lignite 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.rubocop.yml +12 -0
  4. data/NEWS.md +5 -1
  5. data/README.md +2 -2
  6. data/Rakefile +1 -1
  7. data/VERSION +1 -1
  8. data/bin/ev3tool +111 -104
  9. data/examples/bobbee.rb +7 -7
  10. data/examples/hello.rb +2 -0
  11. data/examples/hello.yml +4 -0
  12. data/examples/light-sensor.rb +2 -0
  13. data/examples/light-sensor.yml +3 -0
  14. data/examples/lights.rb +7 -7
  15. data/examples/lights.yml +4 -0
  16. data/examples/sys_list_files.rb +4 -2
  17. data/examples/sys_list_files.yml +9 -0
  18. data/lib/lignite.rb +5 -1
  19. data/lib/lignite/assembler.rb +6 -6
  20. data/lib/lignite/body_compiler.rb +4 -0
  21. data/lib/lignite/connection.rb +59 -1
  22. data/lib/lignite/connection/bluetooth.rb +7 -1
  23. data/lib/lignite/connection/replay.rb +57 -0
  24. data/lib/lignite/connection/tap.rb +45 -0
  25. data/lib/lignite/connection/usb.rb +10 -7
  26. data/lib/lignite/direct_commands.rb +56 -5
  27. data/lib/lignite/message.rb +8 -5
  28. data/lib/lignite/motors.rb +5 -5
  29. data/lib/lignite/op_compiler.rb +19 -18
  30. data/lib/lignite/rbf_object.rb +1 -1
  31. data/lib/lignite/system_commands.rb +30 -2
  32. data/lib/lignite/variables.rb +2 -2
  33. data/lignite.gemspec +14 -4
  34. data/rubocop-suse.yml +74 -0
  35. data/spec/connection_usb_spec.rb +42 -0
  36. data/spec/data/ColorReadout.rb +11 -12
  37. data/spec/data/HelloWorld-subop.rb +1 -1
  38. data/spec/data/HelloWorld.rb +1 -1
  39. data/spec/data/NoDebug.rb +1 -1
  40. data/spec/data/VernierReadout.rb +4 -4
  41. data/spec/direct_commands_spec.rb +25 -0
  42. data/spec/system_commands_spec.rb +22 -0
  43. metadata +28 -3
  44. data/lib/lignite/message_sender.rb +0 -94
data/lib/lignite.rb CHANGED
@@ -5,10 +5,11 @@ require "lignite/assembler"
5
5
  require "lignite/body_compiler"
6
6
  require "lignite/connection"
7
7
  require "lignite/connection/bluetooth"
8
+ require "lignite/connection/replay"
9
+ require "lignite/connection/tap"
8
10
  require "lignite/connection/usb"
9
11
  require "lignite/direct_commands"
10
12
  require "lignite/message"
11
- require "lignite/message_sender"
12
13
  require "lignite/motors"
13
14
  require "lignite/op_compiler"
14
15
  require "lignite/rbf_object"
@@ -33,4 +34,7 @@ module Lignite
33
34
  class ByteString < String
34
35
  # empty class, just for documentation purposes
35
36
  end
37
+
38
+ class VMError < RuntimeError
39
+ end
36
40
  end
@@ -6,16 +6,16 @@ module Lignite
6
6
  include Bytes
7
7
  include Logger
8
8
 
9
- SIGNATURE = "LEGO"
9
+ SIGNATURE = "LEGO".freeze
10
10
  def image_header(image_size:, version:, object_count:, global_bytes:)
11
11
  SIGNATURE + u32(image_size) + u16(version) + u16(object_count) +
12
12
  u32(global_bytes)
13
13
  end
14
14
 
15
15
  # @return [Array<RbfObject>]
16
- attr :objects
16
+ attr_reader :objects
17
17
  # @return [Variables]
18
- attr :globals
18
+ attr_reader :globals
19
19
 
20
20
  # Assemble a complete RBF program file.
21
21
  # (it is OK to reuse an Assembler and call this several times in a sequence)
@@ -30,7 +30,7 @@ module Lignite
30
30
  instance_eval(rb_text, rb_filename, 1) # 1 is the line number
31
31
 
32
32
  File.open(rbf_filename, "w") do |f|
33
- dummy_header = image_header(image_size:0, version: 0, object_count: 0, global_bytes: 0)
33
+ dummy_header = image_header(image_size: 0, version: 0, object_count: 0, global_bytes: 0)
34
34
  f.write(dummy_header)
35
35
  @objects.each do |obj|
36
36
  h = obj.header(f.tell)
@@ -40,8 +40,8 @@ module Lignite
40
40
  end
41
41
  size = f.tell
42
42
  f.pos = 0
43
- header = image_header(image_size: size,
44
- version: version,
43
+ header = image_header(image_size: size,
44
+ version: version,
45
45
  object_count: @objects.size,
46
46
  global_bytes: @globals.bytesize)
47
47
  f.write(header)
@@ -51,5 +51,9 @@ module Lignite
51
51
 
52
52
  @bytes += @op_compiler.send(name, *args)
53
53
  end
54
+
55
+ def respond_to_missing?(name, _include_private)
56
+ @op_compiler.respond_to?(name) || super
57
+ end
54
58
  end
55
59
  end
@@ -1,13 +1,71 @@
1
1
  module Lignite
2
2
  class Connection
3
+ include Bytes
4
+ include Logger
5
+
3
6
  # @return [Connection] Try a {Usb} connection first, then a {Bluetooth} one.
4
7
  def self.create
8
+ @c ||= Replay.new(ENV["LIGNITE_REPLAY"]) if ENV["LIGNITE_REPLAY"]
9
+
5
10
  @c ||= begin
6
11
  Usb.new
7
12
  rescue NoUsbDevice
8
13
  Bluetooth.new
9
14
  end
15
+
16
+ @c = Tap.new(@c, ENV["LIGNITE_TAP"]) if ENV["LIGNITE_TAP"]
17
+ @c
18
+ end
19
+
20
+ def self.reset
21
+ @c = nil
22
+ end
23
+
24
+ def initialize
25
+ @buf = ""
26
+ end
27
+
28
+ def close
29
+ Connection.reset
30
+ end
31
+
32
+ # @param payload [ByteString]
33
+ def send(payload)
34
+ packet = u16(payload.bytesize) + payload
35
+ logger.debug "-> #{packet.inspect}"
36
+
37
+ write(packet)
38
+ end
39
+
40
+ # @return [ByteString] a complete message
41
+ def receive
42
+ size = nil
43
+ loop do
44
+ lenbuf = bufread(2)
45
+ size = unpack_u16(lenbuf)
46
+ break unless size.zero?
47
+ # leftover data?
48
+ @buf = ""
49
+ end
50
+
51
+ res = bufread(size)
52
+ res
53
+ end
54
+
55
+ # @!group Subclasses must implement
56
+ # @!method read(maxlen)
57
+ # @!method write(data)
58
+ # @!method close
59
+
60
+ private
61
+
62
+ # read must not be called with a too low value :-/
63
+ def bufread(n)
64
+ @buf += read(10000) while @buf.bytesize < n
65
+ ret = @buf[0, n]
66
+ @buf = @buf[n..-1]
67
+ logger.debug "R<-(#{ret.bytesize})#{ret.inspect}"
68
+ ret
10
69
  end
11
- # FIXME: how to close and reopen a connection?
12
70
  end
13
71
  end
@@ -8,6 +8,7 @@ module Lignite
8
8
 
9
9
  # @param address [String] "11:22:33:44:55:66"
10
10
  def initialize(address = address_from_file)
11
+ super()
11
12
  @sock = Socket.new(AF_BLUETOOTH, :STREAM, BTPROTO_RFCOMM)
12
13
  addr_b = address.split(/:/).map { |x| x.to_i(16) }
13
14
  channel = 1
@@ -24,7 +25,7 @@ module Lignite
24
25
  end
25
26
 
26
27
  def self.config_filename
27
- "#{ENV['HOME']}/.config/lignite-btaddr"
28
+ "#{ENV["HOME"]}/.config/lignite-btaddr"
28
29
  end
29
30
 
30
31
  def self.template_config_filename
@@ -44,6 +45,11 @@ module Lignite
44
45
  def write(s)
45
46
  @sock.write(s)
46
47
  end
48
+
49
+ def close
50
+ @sock.shutdown
51
+ super
52
+ end
47
53
  end
48
54
  end
49
55
  end
@@ -0,0 +1,57 @@
1
+ require "yaml"
2
+
3
+ module Lignite
4
+ class Connection
5
+ class ReplayError < RuntimeError
6
+ end
7
+
8
+ # Replays a recorded communication.
9
+ # It checks that #send matches the stored sends, replays the #receive.
10
+ class Replay < Connection
11
+ def initialize(filename)
12
+ @filename = filename
13
+
14
+ # [
15
+ # {"SEND" => "foo"},
16
+ # {"SEND" => "foo2"},
17
+ # {"RECV" => "adfafd"},
18
+ # {"SEND" => "foo2"},
19
+ # {"RECV" => "adfafd"}
20
+ # ]
21
+ @stream = YAML.load_file(filename)
22
+ end
23
+
24
+ # @param payload [ByteString]
25
+ def send(payload)
26
+ recorded = @stream.shift
27
+ raise ReplayError, "Nothing left in the recording" if recorded.nil?
28
+ hex = recorded["SEND"]
29
+ raise ReplayError, "Called SEND but the recording says RECV" if hex.nil?
30
+ data = hex_to_bin(hex)
31
+ raise ReplayError, "Called SEND but the recorded data does not match" if data != payload
32
+ end
33
+
34
+ # @return [ByteString] a complete message
35
+ def receive
36
+ recorded = @stream.shift
37
+ raise ReplayError, "Nothing left in the recording" if recorded.nil?
38
+ hex = recorded["RECV"]
39
+ raise ReplayError, "Called RECV but the recording says SEND" if hex.nil?
40
+ hex_to_bin(hex)
41
+ end
42
+
43
+ def close
44
+ super
45
+ raise ReplayError, "Called close but the recording has leftover data" unless @stream.empty?
46
+ end
47
+
48
+ private
49
+
50
+ # @param hex [String] "413432"
51
+ # @return [ByteString] "A42"
52
+ def hex_to_bin(hex)
53
+ [hex].pack("H*")
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,45 @@
1
+ require "yaml"
2
+
3
+ module Lignite
4
+ class Connection
5
+ # An adapter that delegates to another connection
6
+ # and records the communication
7
+ class Tap < Connection
8
+ def initialize(conn, filename)
9
+ raise "File #{filename} exists, will not overwrite" if File.exist?(filename)
10
+ @conn = conn
11
+ @filename = filename
12
+ @packets = []
13
+ end
14
+
15
+ # @param payload [ByteString]
16
+ def send(payload)
17
+ r = @conn.send(payload)
18
+ @packets << { "SEND" => bin_to_hex(payload) }
19
+ r
20
+ end
21
+
22
+ # @return [ByteString] a complete message
23
+ def receive
24
+ s = @conn.receive
25
+ @packets << { "RECV" => bin_to_hex(s) }
26
+ s
27
+ end
28
+
29
+ def close
30
+ y = YAML.dump(@packets)
31
+ File.write(@filename, y)
32
+ super
33
+ @conn.close
34
+ end
35
+
36
+ private
37
+
38
+ # @param bin [ByteString] "A42"
39
+ # @return [String] "413432"
40
+ def bin_to_hex(bin)
41
+ bin.unpack("H*").first
42
+ end
43
+ end
44
+ end
45
+ end
@@ -25,6 +25,7 @@ module Lignite
25
25
  attr_reader :device, :interface, :out_ep, :in_ep
26
26
 
27
27
  def initialize
28
+ super
28
29
  usb = LIBUSB::Context.new
29
30
  @device = usb.devices(idVendor: VENDOR_LEGO, idProduct: PRODUCT_EV3).first
30
31
  raise Lignite::NoUsbDevice if @device.nil?
@@ -33,8 +34,8 @@ module Lignite
33
34
  ## device.set_configuration(CONFIGURATION_EV3)
34
35
  @interface = @device.interfaces[INTERFACE_EV3]
35
36
  eps = @interface.endpoints
36
- @out_ep = eps.find { |e| e.direction == :out}
37
- @in_ep = eps.find { |e| e.direction == :in}
37
+ @out_ep = eps.find { |e| e.direction == :out }
38
+ @in_ep = eps.find { |e| e.direction == :in }
38
39
  end
39
40
 
40
41
  # @return [Integer] number of bytes written
@@ -58,17 +59,19 @@ module Lignite
58
59
  begin
59
60
  got = devh.interrupt_transfer(endpoint: @in_ep, dataIn: bytes)
60
61
  rescue LIBUSB::Error => e
61
- if e.transferred.is_a? String
62
- got = e.transferred
63
- else
64
- raise
65
- end
62
+ got = e.transferred
63
+ raise unless got.is_a? String
66
64
  end
67
65
  end
68
66
  end
69
67
  logger.debug "Read returning #{got.bytesize} bytes"
70
68
  got
71
69
  end
70
+
71
+ def close
72
+ super
73
+ # do nothing: read and write open and close the handle each time
74
+ end
72
75
  end
73
76
  end
74
77
  end
@@ -1,12 +1,24 @@
1
1
  module Lignite
2
2
  class DirectCommands
3
+ include Bytes
4
+
5
+ def self.run(conn = Connection.create, &block)
6
+ dc = new(conn)
7
+ dc.instance_exec(&block)
8
+ dc.close
9
+ end
10
+
3
11
  # @param conn [Connection]
4
12
  def initialize(conn = Connection.create)
5
13
  @op_compiler = OpCompiler.new
6
- @sender = MessageSender.new(conn)
14
+ @conn = conn
7
15
  @globals = nil
8
16
  end
9
17
 
18
+ def close
19
+ @conn.close
20
+ end
21
+
10
22
  def variables
11
23
  @globals
12
24
  end
@@ -17,7 +29,7 @@ module Lignite
17
29
  ret_bytes = instance_exec(&body)
18
30
  ret = @globals.unpack(ret_bytes)
19
31
  @globals = nil
20
- ret # TODO decode according to type
32
+ ret # TODO: decode according to type
21
33
  end
22
34
 
23
35
  def block(&body)
@@ -28,19 +40,58 @@ module Lignite
28
40
  bs = bodyc.bytes
29
41
  lsize = locals.bytesize
30
42
  if @globals
31
- @sender.direct_command_with_reply(bs, global_size: @globals.bytesize, local_size: lsize)
43
+ direct_command_with_reply(bs, global_size: @globals.bytesize, local_size: lsize)
32
44
  else
33
- @sender.direct_command(bs, global_size: 0, local_size: lsize)
45
+ direct_command(bs, global_size: 0, local_size: lsize)
34
46
  end
35
47
  end
36
48
 
37
49
  def method_missing(name, *args)
38
50
  if @op_compiler.respond_to?(name)
39
51
  insb = @op_compiler.send(name, *args)
40
- @sender.direct_command(insb)
52
+ direct_command(insb)
41
53
  else
42
54
  super
43
55
  end
44
56
  end
57
+
58
+ def respond_to_missing?(name, _include_private)
59
+ @op_compiler.respond_to?(name) || super
60
+ end
61
+
62
+ private
63
+
64
+ def direct_command(instr_bytes, global_size: 0, local_size: 0)
65
+ body = u16(var_alloc(global_size: global_size, local_size: local_size)) +
66
+ instr_bytes
67
+ cmd = Message.direct_command_no_reply(body)
68
+ @conn.send(cmd.bytes)
69
+ end
70
+
71
+ def direct_command_with_reply(instr_bytes, global_size: 0, local_size: 0)
72
+ body = u16(var_alloc(global_size: global_size, local_size: local_size)) +
73
+ instr_bytes
74
+ cmd = Message.direct_command_with_reply(body)
75
+ @conn.send(cmd.bytes)
76
+
77
+ reply = Message.reply_from_bytes(@conn.receive)
78
+ assert_match(reply.msgid, cmd.msgid, "Reply id")
79
+ if reply.error?
80
+ raise VMError # no details?
81
+ end
82
+
83
+ reply.globals
84
+ end
85
+
86
+ def var_alloc(global_size:, local_size:)
87
+ var_alloc = global_size & 0x3ff
88
+ var_alloc |= (local_size & 0x3f) << 10
89
+ var_alloc
90
+ end
91
+
92
+ def assert_match(actual, expected, description)
93
+ return if actual == expected
94
+ raise "#{description} does not match, expected #{expected}, actual #{actual}"
95
+ end
45
96
  end
46
97
  end
@@ -1,17 +1,20 @@
1
1
  module Lignite
2
2
  # A Message has 3 common parts:
3
3
  # - length u16, (not including the length itself);
4
- # this is added by {MessageSender#send}
5
- # and stripped by {MessageSender#receive}
4
+ # this is added by {Connection#send}
5
+ # and stripped by {Connection#receive}
6
6
  # - msgid, u16
7
7
  # - type, u8
8
8
  # and then a type-specific body.
9
- # It is sent or received via {MessageSender}
10
9
  class Message
11
10
  include Bytes
12
11
  extend Bytes
13
12
  extend Logger
14
- @msg_counter = rand(65535)
13
+
14
+ def self.reset_msgid
15
+ @msg_counter = 0
16
+ end
17
+ reset_msgid
15
18
 
16
19
  def self.msgid
17
20
  @msg_counter += 1
@@ -63,7 +66,7 @@ module Lignite
63
66
  when 0x04 # DIRECT_REPLY_ERROR
64
67
  DirectReply.new(msgid: msgid, error: true, body: body)
65
68
  else
66
- raise "Unexpected reply type %x" % type
69
+ raise format("Unexpected reply type %x", type)
67
70
  end
68
71
  end
69
72
  end