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