lignite 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.rubocop.yml +12 -0
- data/NEWS.md +5 -1
- data/README.md +2 -2
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/bin/ev3tool +111 -104
- data/examples/bobbee.rb +7 -7
- data/examples/hello.rb +2 -0
- data/examples/hello.yml +4 -0
- data/examples/light-sensor.rb +2 -0
- data/examples/light-sensor.yml +3 -0
- data/examples/lights.rb +7 -7
- data/examples/lights.yml +4 -0
- data/examples/sys_list_files.rb +4 -2
- data/examples/sys_list_files.yml +9 -0
- data/lib/lignite.rb +5 -1
- data/lib/lignite/assembler.rb +6 -6
- data/lib/lignite/body_compiler.rb +4 -0
- data/lib/lignite/connection.rb +59 -1
- data/lib/lignite/connection/bluetooth.rb +7 -1
- data/lib/lignite/connection/replay.rb +57 -0
- data/lib/lignite/connection/tap.rb +45 -0
- data/lib/lignite/connection/usb.rb +10 -7
- data/lib/lignite/direct_commands.rb +56 -5
- data/lib/lignite/message.rb +8 -5
- data/lib/lignite/motors.rb +5 -5
- data/lib/lignite/op_compiler.rb +19 -18
- data/lib/lignite/rbf_object.rb +1 -1
- data/lib/lignite/system_commands.rb +30 -2
- data/lib/lignite/variables.rb +2 -2
- data/lignite.gemspec +14 -4
- data/rubocop-suse.yml +74 -0
- data/spec/connection_usb_spec.rb +42 -0
- data/spec/data/ColorReadout.rb +11 -12
- data/spec/data/HelloWorld-subop.rb +1 -1
- data/spec/data/HelloWorld.rb +1 -1
- data/spec/data/NoDebug.rb +1 -1
- data/spec/data/VernierReadout.rb +4 -4
- data/spec/direct_commands_spec.rb +25 -0
- data/spec/system_commands_spec.rb +22 -0
- metadata +28 -3
- 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
|
data/lib/lignite/assembler.rb
CHANGED
@@ -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
|
-
|
16
|
+
attr_reader :objects
|
17
17
|
# @return [Variables]
|
18
|
-
|
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:
|
44
|
-
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)
|
data/lib/lignite/connection.rb
CHANGED
@@ -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[
|
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
|
-
|
62
|
-
|
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
|
-
@
|
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
|
-
|
43
|
+
direct_command_with_reply(bs, global_size: @globals.bytesize, local_size: lsize)
|
32
44
|
else
|
33
|
-
|
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
|
-
|
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
|
data/lib/lignite/message.rb
CHANGED
@@ -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 {
|
5
|
-
# and stripped by {
|
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
|
-
|
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"
|
69
|
+
raise format("Unexpected reply type %x", type)
|
67
70
|
end
|
68
71
|
end
|
69
72
|
end
|