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