lignite 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/COPYING +674 -0
- data/Gemfile +3 -0
- data/README.md +51 -0
- data/Rakefile +6 -0
- data/VERSION +1 -0
- data/bin/ev3tool +70 -0
- data/data/ev3.yml +10103 -0
- data/data/lignite-btaddr +7 -0
- data/data/sysops.yml +290 -0
- data/examples/hello.rb +8 -0
- data/examples/lights.rb +10 -0
- data/examples/motors.rb +19 -0
- data/examples/sound.rb +9 -0
- data/examples/sys_list_files.rb +16 -0
- data/lib/lignite.rb +30 -0
- data/lib/lignite/assembler.rb +49 -0
- data/lib/lignite/body_compiler.rb +42 -0
- data/lib/lignite/bytes.rb +35 -0
- data/lib/lignite/connection.rb +13 -0
- data/lib/lignite/connection/bluetooth.rb +37 -0
- data/lib/lignite/connection/usb.rb +74 -0
- data/lib/lignite/direct_commands.rb +26 -0
- data/lib/lignite/logger.rb +15 -0
- data/lib/lignite/message.rb +100 -0
- data/lib/lignite/message_sender.rb +92 -0
- data/lib/lignite/op_compiler.rb +224 -0
- data/lib/lignite/rbf_object.rb +33 -0
- data/lib/lignite/system_commands.rb +103 -0
- data/lib/lignite/variables.rb +27 -0
- data/lib/lignite/version.rb +4 -0
- data/lignite.gemspec +74 -0
- data/spec/assembler_spec.rb +24 -0
- data/spec/data/HelloWorld-subop.rb +6 -0
- data/spec/data/HelloWorld-subop.rbf +0 -0
- data/spec/data/HelloWorld.lms +7 -0
- data/spec/data/HelloWorld.rb +6 -0
- data/spec/data/HelloWorld.rbf +0 -0
- data/spec/data/VernierReadout.lms +31 -0
- data/spec/data/VernierReadout.rb +27 -0
- data/spec/data/VernierReadout.rbf +0 -0
- data/spec/spec_helper.rb +26 -0
- metadata +158 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
module Lignite
|
2
|
+
# Extends {OpCompiler} by
|
3
|
+
# - variable declarations: {VariableDeclarer}
|
4
|
+
# - high level flow control: {#loop}
|
5
|
+
class BodyCompiler
|
6
|
+
|
7
|
+
# {#locals} are {Variables}
|
8
|
+
module VariableDeclarer
|
9
|
+
def data32(id)
|
10
|
+
locals.add(id, 4)
|
11
|
+
end
|
12
|
+
|
13
|
+
def datas(id, size)
|
14
|
+
locals.add(id, size)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
include VariableDeclarer
|
19
|
+
attr_reader :bytes
|
20
|
+
attr_reader :locals
|
21
|
+
|
22
|
+
def initialize(locals)
|
23
|
+
@bytes = ""
|
24
|
+
@locals = locals
|
25
|
+
@op_compiler = OpCompiler.new(nil, @locals)
|
26
|
+
end
|
27
|
+
|
28
|
+
def loop(&body)
|
29
|
+
subc = BodyCompiler.new(@locals)
|
30
|
+
subc.instance_exec(&body)
|
31
|
+
@bytes << subc.bytes
|
32
|
+
# the jump takes up 4 bytes: JR, LC2, LO, HI
|
33
|
+
jr(Complex(- (subc.bytes.bytesize + 4), 2))
|
34
|
+
end
|
35
|
+
|
36
|
+
def method_missing(name, *args)
|
37
|
+
super unless @op_compiler.respond_to?(name)
|
38
|
+
|
39
|
+
@bytes += @op_compiler.send(name, *args)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Lignite
|
2
|
+
module Bytes
|
3
|
+
def u8(n)
|
4
|
+
(n & 0xff).chr
|
5
|
+
end
|
6
|
+
|
7
|
+
def u16(n)
|
8
|
+
u8(n & 0xff) + u8(n >> 8)
|
9
|
+
end
|
10
|
+
|
11
|
+
def u32(n)
|
12
|
+
u16(n & 0xffff) + u16(n >> 16)
|
13
|
+
end
|
14
|
+
|
15
|
+
def f32(float)
|
16
|
+
[float].pack("e")
|
17
|
+
end
|
18
|
+
|
19
|
+
def unpack_u8(s)
|
20
|
+
s.unpack("C").first
|
21
|
+
end
|
22
|
+
|
23
|
+
def unpack_u16(s)
|
24
|
+
s.unpack("S<").first
|
25
|
+
end
|
26
|
+
|
27
|
+
def unpack_u32(s)
|
28
|
+
s.unpack("L<").first
|
29
|
+
end
|
30
|
+
|
31
|
+
def hexdump(s)
|
32
|
+
s.unpack("H*").first
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Lignite
|
2
|
+
class Connection
|
3
|
+
# @return [Connection] Try a {Usb} connection first, then a {Bluetooth} one.
|
4
|
+
def self.create
|
5
|
+
@c ||= begin
|
6
|
+
Usb.new
|
7
|
+
rescue NoUsbDevice
|
8
|
+
Bluetooth.new
|
9
|
+
end
|
10
|
+
end
|
11
|
+
# FIXME: how to close and reopen a connection?
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "socket"
|
2
|
+
|
3
|
+
module Lignite
|
4
|
+
class Connection
|
5
|
+
class Bluetooth < Connection
|
6
|
+
AF_BLUETOOTH = 31
|
7
|
+
BTPROTO_RFCOMM = 3
|
8
|
+
|
9
|
+
# @param address [String] "11:22:33:44:55:66"
|
10
|
+
def initialize(address = address_from_file)
|
11
|
+
@sock = Socket.new(AF_BLUETOOTH, :STREAM, BTPROTO_RFCOMM)
|
12
|
+
addr_b = address.split(/:/).map { |x| x.to_i(16) }
|
13
|
+
channel = 1
|
14
|
+
sockaddr = [AF_BLUETOOTH, 0, *addr_b.reverse, channel, 0].pack("C*")
|
15
|
+
# common exceptions:
|
16
|
+
# "Errno::EHOSTUNREACH: No route to host": BT is disabled;
|
17
|
+
# use `hciconfig hci0 up`
|
18
|
+
# "Errno::EHOSTDOWN: Host is down": Turn the brick on, enable BT
|
19
|
+
@sock.connect(sockaddr)
|
20
|
+
end
|
21
|
+
|
22
|
+
def address_from_file
|
23
|
+
fn = "#{ENV['HOME']}/.config/lignite-btaddr"
|
24
|
+
s = File.read(fn)
|
25
|
+
s.lines.grep(/^[0-9a-fA-F]/).first.strip
|
26
|
+
end
|
27
|
+
|
28
|
+
def read(n)
|
29
|
+
@sock.recv(n)
|
30
|
+
end
|
31
|
+
|
32
|
+
def write(s)
|
33
|
+
@sock.write(s)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# https://github.com/larskanis/libusb
|
2
|
+
require "libusb"
|
3
|
+
|
4
|
+
module Lignite
|
5
|
+
class NoUsbDevice < RuntimeError
|
6
|
+
end
|
7
|
+
|
8
|
+
class Connection
|
9
|
+
class Usb < Connection
|
10
|
+
include Logger
|
11
|
+
|
12
|
+
# To get to the endpoint we need to descend down the hierarchy of
|
13
|
+
# 1) Device
|
14
|
+
VENDOR_LEGO = 0x0694
|
15
|
+
PRODUCT_EV3 = 5
|
16
|
+
# 2) Configuration, 1-based
|
17
|
+
CONFIGURATION_EV3 = 1
|
18
|
+
# 3) Interface, 0-based
|
19
|
+
INTERFACE_EV3 = 0
|
20
|
+
# 4) Alternate setting, 0-based
|
21
|
+
SETTING_EV3 = 0
|
22
|
+
# 5) Endpoint, 0-based
|
23
|
+
ENDPOINT_EV3 = 1
|
24
|
+
|
25
|
+
attr_reader :device, :interface, :out_ep, :in_ep
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
usb = LIBUSB::Context.new
|
29
|
+
@device = usb.devices(idVendor: VENDOR_LEGO, idProduct: PRODUCT_EV3).first
|
30
|
+
raise Lignite::NoUsbDevice if @device.nil?
|
31
|
+
|
32
|
+
## Because multiple configs are rare, the library allows to omit this:
|
33
|
+
## device.set_configuration(CONFIGURATION_EV3)
|
34
|
+
@interface = @device.interfaces[INTERFACE_EV3]
|
35
|
+
eps = @interface.endpoints
|
36
|
+
@out_ep = eps.find { |e| e.direction == :out}
|
37
|
+
@in_ep = eps.find { |e| e.direction == :in}
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [Integer] number of bytes written
|
41
|
+
def write(data)
|
42
|
+
written = nil
|
43
|
+
@device.open do |devh|
|
44
|
+
devh.auto_detach_kernel_driver = true
|
45
|
+
devh.claim_interface(@interface) do
|
46
|
+
written = devh.interrupt_transfer(endpoint: @out_ep, dataOut: data)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
written
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [String]
|
53
|
+
def read(bytes = nil)
|
54
|
+
got = nil
|
55
|
+
@device.open do |devh|
|
56
|
+
devh.auto_detach_kernel_driver = true
|
57
|
+
devh.claim_interface(@interface) do
|
58
|
+
begin
|
59
|
+
got = devh.interrupt_transfer(endpoint: @in_ep, dataIn: bytes)
|
60
|
+
rescue LIBUSB::Error => e
|
61
|
+
if e.transferred.is_a? String
|
62
|
+
got = e.transferred
|
63
|
+
else
|
64
|
+
raise
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
logger.debug "Read returning #{got.bytesize} bytes"
|
70
|
+
got
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Lignite
|
2
|
+
# FIXME: cannot handle replies
|
3
|
+
class DirectCommands
|
4
|
+
# @param conn [Connection]
|
5
|
+
def initialize(conn = Connection.create)
|
6
|
+
@op_compiler = OpCompiler.new
|
7
|
+
@sender = MessageSender.new(conn)
|
8
|
+
end
|
9
|
+
|
10
|
+
def block(&body)
|
11
|
+
locals = Variables.new
|
12
|
+
bodyc = BodyCompiler.new(locals)
|
13
|
+
bodyc.instance_exec(&body)
|
14
|
+
@sender.direct_command(bodyc.bytes, local_size: locals.bytesize)
|
15
|
+
end
|
16
|
+
|
17
|
+
def method_missing(name, *args)
|
18
|
+
if @op_compiler.respond_to?(name)
|
19
|
+
insb = @op_compiler.send(name, *args)
|
20
|
+
@sender.direct_command(insb)
|
21
|
+
else
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "logger"
|
2
|
+
|
3
|
+
module Lignite
|
4
|
+
module Logger
|
5
|
+
def self.default_logger
|
6
|
+
logger = ::Logger.new(STDERR)
|
7
|
+
logger.level = $VERBOSE ? ::Logger::DEBUG : ::Logger::INFO
|
8
|
+
logger
|
9
|
+
end
|
10
|
+
|
11
|
+
def logger
|
12
|
+
@logger ||= Logger.default_logger
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Lignite
|
2
|
+
# A Message has 3 common parts:
|
3
|
+
# - length u16, (not including the length itself);
|
4
|
+
# this is added by {MessageSender#send}
|
5
|
+
# and stripped by {MessageSender#receive}
|
6
|
+
# - msgid, u16
|
7
|
+
# - type, u8
|
8
|
+
# and then a type-specific body.
|
9
|
+
# It is sent or received via {MessageSender}
|
10
|
+
class Message
|
11
|
+
include Bytes
|
12
|
+
extend Bytes
|
13
|
+
extend Logger
|
14
|
+
@msg_counter = rand(65535)
|
15
|
+
|
16
|
+
def self.msgid
|
17
|
+
@msg_counter += 1
|
18
|
+
logger.debug "MSGID #{@msg_counter}"
|
19
|
+
@msg_counter
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :msgid, :type, :body
|
23
|
+
|
24
|
+
def initialize(type:, body:)
|
25
|
+
@msgid = self.class.msgid
|
26
|
+
@type = type
|
27
|
+
@body = body
|
28
|
+
end
|
29
|
+
|
30
|
+
# not including the length
|
31
|
+
def bytes
|
32
|
+
u16(@msgid) + u8(@type) + @body
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.system_command_with_reply(body)
|
36
|
+
new(type: 0x01, body: body)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.system_command_no_reply(body)
|
40
|
+
new(type: 0x81, body: body)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.direct_command_with_reply(body)
|
44
|
+
new(type: 0x00, body: body)
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.direct_command_no_reply(body)
|
48
|
+
new(type: 0x80, body: body)
|
49
|
+
end
|
50
|
+
|
51
|
+
# @param bytes [ByteString] does not include the length field
|
52
|
+
def self.reply_from_bytes(bytes)
|
53
|
+
msgid = unpack_u16(bytes[0..1])
|
54
|
+
type = unpack_u8(bytes[2])
|
55
|
+
body = bytes[3..-1]
|
56
|
+
case type
|
57
|
+
when 0x03 # SYSTEM_REPLY
|
58
|
+
SystemReply.new(msgid: msgid, error: false, body: body)
|
59
|
+
when 0x05 # SYSTEM_REPLY_ERROR
|
60
|
+
SystemReply.new(msgid: msgid, error: true, body: body)
|
61
|
+
when 0x02 # DIRECT_REPLY
|
62
|
+
DirectReply.new(msgid: msgid, error: false, body: body)
|
63
|
+
when 0x04 # DIRECT_REPLY_ERROR
|
64
|
+
DirectReply.new(msgid: msgid, error: true, body: body)
|
65
|
+
else
|
66
|
+
raise "Unexpected reply type %x" % type
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class SystemReply < Message
|
72
|
+
def initialize(msgid:, error:, body:)
|
73
|
+
@msgid = msgid
|
74
|
+
@error = error
|
75
|
+
@command = unpack_u8(body[0])
|
76
|
+
@status = unpack_u8(body[1])
|
77
|
+
@data = body[2..-1]
|
78
|
+
end
|
79
|
+
|
80
|
+
attr_reader :msgid, :error, :command, :status, :data
|
81
|
+
|
82
|
+
def error?
|
83
|
+
@error
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class DirectReply < Message
|
88
|
+
def new(msgid:, error:, body:)
|
89
|
+
@msgid = msgid
|
90
|
+
@error = error
|
91
|
+
@globals = body
|
92
|
+
end
|
93
|
+
|
94
|
+
attr_reader :msgid, :error, :globals
|
95
|
+
|
96
|
+
def error?
|
97
|
+
@error
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Lignite
|
2
|
+
|
3
|
+
# FIXME: Possibly merge with Connection (UsbConnection)
|
4
|
+
class MessageSender
|
5
|
+
include Bytes
|
6
|
+
include Logger
|
7
|
+
|
8
|
+
def initialize(connection)
|
9
|
+
@c = connection
|
10
|
+
@buf = ""
|
11
|
+
end
|
12
|
+
|
13
|
+
def direct_command(instr_bytes, global_size: 0, local_size: 0)
|
14
|
+
body = u16(var_alloc(global_size: global_size, local_size: local_size)) +
|
15
|
+
instr_bytes
|
16
|
+
cmd = Message.direct_command_no_reply(body)
|
17
|
+
send(cmd.bytes)
|
18
|
+
end
|
19
|
+
|
20
|
+
def direct_command_with_reply(instr_bytes, global_size: 0, local_size: 0)
|
21
|
+
body = u16(var_alloc(global_size: global_size, local_size: local_size)) +
|
22
|
+
instr_bytes
|
23
|
+
cmd = Message.direct_command_with_reply(body)
|
24
|
+
send(cmd.bytes)
|
25
|
+
|
26
|
+
reply = Message.reply_from_bytes(receive)
|
27
|
+
assert_match(reply.msgid, cmd.msgid, "Reply id")
|
28
|
+
if reply.error?
|
29
|
+
raise "VMError" # no details?
|
30
|
+
end
|
31
|
+
|
32
|
+
reply.data
|
33
|
+
end
|
34
|
+
|
35
|
+
def system_command_with_reply(instr_bytes)
|
36
|
+
cmd = Message.system_command_with_reply(instr_bytes)
|
37
|
+
send(cmd.bytes)
|
38
|
+
|
39
|
+
reply = Message.reply_from_bytes(receive)
|
40
|
+
assert_match(reply.msgid, cmd.msgid, "Reply id")
|
41
|
+
assert_match(reply.command, unpack_u8(instr_bytes[0]), "Command num")
|
42
|
+
if reply.error?
|
43
|
+
raise "VMError, %u" % reply.status
|
44
|
+
end
|
45
|
+
|
46
|
+
reply.data
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def send(payload)
|
52
|
+
packet = u16(payload.bytesize) + payload
|
53
|
+
logger.debug "-> #{packet.inspect}"
|
54
|
+
@c.write(packet)
|
55
|
+
end
|
56
|
+
|
57
|
+
# read must not be called with a too low value :-/
|
58
|
+
def bufread(n)
|
59
|
+
while n > @buf.bytesize
|
60
|
+
@buf += @c.read(10000)
|
61
|
+
end
|
62
|
+
ret = @buf[0, n]
|
63
|
+
@buf = @buf[n..-1]
|
64
|
+
logger.debug "R<-(#{ret.bytesize})#{ret.inspect}"
|
65
|
+
ret
|
66
|
+
end
|
67
|
+
|
68
|
+
def receive
|
69
|
+
size = nil
|
70
|
+
loop do
|
71
|
+
lenbuf = bufread(2)
|
72
|
+
size = unpack_u16(lenbuf)
|
73
|
+
break unless size.zero?
|
74
|
+
# leftover data?
|
75
|
+
@buf = ""
|
76
|
+
end
|
77
|
+
|
78
|
+
res = bufread(size)
|
79
|
+
res
|
80
|
+
end
|
81
|
+
|
82
|
+
def var_alloc(global_size:, local_size:)
|
83
|
+
var_alloc = global_size & 0x3ff
|
84
|
+
var_alloc |= (local_size & 0x3f) << 10
|
85
|
+
end
|
86
|
+
|
87
|
+
def assert_match(actual, expected, description)
|
88
|
+
return if actual == expected
|
89
|
+
raise "#{description} does not match, expected #{expected}, actual #{actual}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|