lignite 0.1.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 +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
|