lignite 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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