lignite 0.2.0 → 0.3.0

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