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,224 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
module Lignite
|
4
|
+
class OpCompiler
|
5
|
+
include Bytes
|
6
|
+
include Logger
|
7
|
+
extend Logger
|
8
|
+
|
9
|
+
class TODO < StandardError
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.load_const(name, value)
|
13
|
+
raise "duplicate constant #{name}" if Lignite.const_defined?(name)
|
14
|
+
Lignite.const_set(name, value)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.load_op(oname, odata)
|
18
|
+
ovalue = odata["value"]
|
19
|
+
oparams = odata["params"]
|
20
|
+
p1 = oparams.first
|
21
|
+
if p1 && p1["type"] == "SUBP"
|
22
|
+
commands = p1["commands"]
|
23
|
+
commands.each do |cname, cdata|
|
24
|
+
cvalue = cdata["value"]
|
25
|
+
load_const(cname, cvalue)
|
26
|
+
cparams = cdata["params"]
|
27
|
+
define_op("#{oname}_#{cname}", ovalue, cvalue, cparams)
|
28
|
+
end
|
29
|
+
define_multiop(oname, commands)
|
30
|
+
else
|
31
|
+
define_op(oname, ovalue, nil, oparams)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.define_multiop(oname, commands)
|
36
|
+
names = commands.map do |cname, cdata|
|
37
|
+
csym = cname.downcase.to_sym
|
38
|
+
cvalue = cdata["value"]
|
39
|
+
[cvalue, csym]
|
40
|
+
end.to_h
|
41
|
+
|
42
|
+
osym = oname.downcase.to_sym
|
43
|
+
define_method(osym) do |*args|
|
44
|
+
logger.debug "called #{osym} with #{args.inspect}"
|
45
|
+
cvalue = args.shift
|
46
|
+
csym = names.fetch(cvalue)
|
47
|
+
send("#{osym}_#{csym}", *args)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.define_op(oname, ovalue, cvalue, params)
|
52
|
+
check_arg_count = true
|
53
|
+
param_handlers = params.map do |par|
|
54
|
+
case par["type"]
|
55
|
+
when "PARLAB" # a label, only one opcode
|
56
|
+
raise TODO
|
57
|
+
when "PARNO" # the value says how many other params follow
|
58
|
+
check_arg_count = false
|
59
|
+
->(x) { param_simple(x) }
|
60
|
+
when "PARS" # string
|
61
|
+
raise TODO
|
62
|
+
when "PARV" # value, type depends
|
63
|
+
raise TODO
|
64
|
+
when "PARVALUES"
|
65
|
+
raise TODO
|
66
|
+
when "PAR8", "PAR16", "PAR32", "PARF"
|
67
|
+
->(x) { param_simple(x) }
|
68
|
+
else
|
69
|
+
raise "Unhandled param type #{par["type"]}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
osym = oname.downcase.to_sym
|
74
|
+
define_method(osym) do |*args|
|
75
|
+
logger.debug "called #{osym} with #{args.inspect}"
|
76
|
+
if check_arg_count && args.size != param_handlers.size
|
77
|
+
raise ArgumentError, "expected #{param_handlers.size} arguments, got #{args.size}"
|
78
|
+
end
|
79
|
+
|
80
|
+
bytes = u8(ovalue)
|
81
|
+
bytes += param_simple(cvalue) unless cvalue.nil?
|
82
|
+
|
83
|
+
bytes += args.zip(param_handlers).map do |a, h|
|
84
|
+
h ||= ->(x) { param_simple(x) }
|
85
|
+
# h.call(a) would have self = Op instead of #<Op>
|
86
|
+
instance_exec(a, &h)
|
87
|
+
end.join("")
|
88
|
+
logger.debug "returning bytecode: #{bytes.inspect}"
|
89
|
+
bytes
|
90
|
+
end
|
91
|
+
rescue TODO
|
92
|
+
logger.debug "Could not define #{oname}"
|
93
|
+
end
|
94
|
+
|
95
|
+
@loaded = false
|
96
|
+
|
97
|
+
def self.load_yml
|
98
|
+
return if @loaded
|
99
|
+
fname = File.expand_path("../../../data/ev3.yml", __FILE__)
|
100
|
+
yml = YAML.load_file(fname)
|
101
|
+
op_hash = yml["ops"]
|
102
|
+
op_hash.each do |oname, odata|
|
103
|
+
load_op(oname, odata)
|
104
|
+
end
|
105
|
+
|
106
|
+
defines = yml["defines"]
|
107
|
+
defines.each do |dname, ddata|
|
108
|
+
load_const(dname, ddata["value"])
|
109
|
+
end
|
110
|
+
|
111
|
+
enums = yml["enums"]
|
112
|
+
enums.each do |ename, edata|
|
113
|
+
edata["members"].each do |mname, mdata|
|
114
|
+
load_const(mname, mdata["value"])
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
@loaded = true
|
119
|
+
end
|
120
|
+
|
121
|
+
def initialize(globals = nil, locals = nil)
|
122
|
+
self.class.load_yml
|
123
|
+
@globals = globals
|
124
|
+
@locals = locals
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
PRIMPAR_SHORT = 0x00
|
130
|
+
PRIMPAR_LONG = 0x80
|
131
|
+
|
132
|
+
PRIMPAR_CONST = 0x00
|
133
|
+
PRIMPAR_VARIABEL = 0x40
|
134
|
+
PRIMPAR_LOCAL = 0x00
|
135
|
+
PRIMPAR_GLOBAL = 0x20
|
136
|
+
PRIMPAR_HANDLE = 0x10
|
137
|
+
PRIMPAR_ADDR = 0x08
|
138
|
+
|
139
|
+
PRIMPAR_INDEX = 0x1F
|
140
|
+
PRIMPAR_CONST_SIGN = 0x20
|
141
|
+
PRIMPAR_VALUE = 0x3F
|
142
|
+
|
143
|
+
PRIMPAR_BYTES = 0x07
|
144
|
+
|
145
|
+
PRIMPAR_STRING_OLD = 0
|
146
|
+
PRIMPAR_1_BYTE = 1
|
147
|
+
PRIMPAR_2_BYTES = 2
|
148
|
+
PRIMPAR_4_BYTES = 3
|
149
|
+
PRIMPAR_STRING = 4
|
150
|
+
|
151
|
+
PRIMPAR_LABEL = 0x20
|
152
|
+
|
153
|
+
def make_lcn(n, bytes)
|
154
|
+
case bytes
|
155
|
+
when 0
|
156
|
+
[n & PRIMPAR_VALUE]
|
157
|
+
when 1
|
158
|
+
[PRIMPAR_LONG | PRIMPAR_1_BYTE, n & 0xff]
|
159
|
+
when 2
|
160
|
+
[PRIMPAR_LONG | PRIMPAR_2_BYTES, n & 0xff, (n >> 8) & 0xff]
|
161
|
+
else
|
162
|
+
[PRIMPAR_LONG | PRIMPAR_4_BYTES,
|
163
|
+
n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff]
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def make_lc(n, bytes = nil)
|
168
|
+
bytes ||= if (-31 .. 31).include? n
|
169
|
+
0
|
170
|
+
elsif (-127 .. 127).include? n
|
171
|
+
1
|
172
|
+
elsif (-32767 .. 32767).include? n
|
173
|
+
2
|
174
|
+
else
|
175
|
+
4
|
176
|
+
end
|
177
|
+
make_lcn(n, bytes)
|
178
|
+
end
|
179
|
+
|
180
|
+
def make_v(n, local_or_global)
|
181
|
+
vartag = PRIMPAR_VARIABEL | local_or_global
|
182
|
+
if (0 .. 31).include? n
|
183
|
+
return [vartag | (n & PRIMPAR_VALUE)]
|
184
|
+
elsif (0 .. 255).include? n
|
185
|
+
return [vartag | PRIMPAR_LONG | PRIMPAR_1_BYTE, n & 0xff]
|
186
|
+
elsif (0 .. 65535).include? n
|
187
|
+
return [vartag | PRIMPAR_LONG | PRIMPAR_2_BYTES, n & 0xff, (n >> 8) & 0xff]
|
188
|
+
end
|
189
|
+
[vartag | PRIMPAR_LONG | PRIMPAR_4_BYTES,
|
190
|
+
n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff]
|
191
|
+
end
|
192
|
+
|
193
|
+
def make_var(sym)
|
194
|
+
raise "No variables declared, cannot process symbols" if @locals.nil? && @globals.nil?
|
195
|
+
if @locals.key?(sym)
|
196
|
+
o = @locals.offset(sym)
|
197
|
+
make_v(o, PRIMPAR_LOCAL)
|
198
|
+
elsif @globals.key?(sym)
|
199
|
+
o = @globals.offset(sym)
|
200
|
+
make_v(o, PRIMPAR_GLOBAL)
|
201
|
+
else
|
202
|
+
raise "Variable #{sym} not found"
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# @return [ByteString]
|
207
|
+
def param_simple(x)
|
208
|
+
case x
|
209
|
+
when Integer
|
210
|
+
make_lc(x).map(&:chr).join("")
|
211
|
+
when Complex
|
212
|
+
make_lc(x.real, x.imag).map(&:chr).join("")
|
213
|
+
when String
|
214
|
+
u8(0x80) + x + u8(0x00)
|
215
|
+
when Float
|
216
|
+
u8(0x83) + f32(x)
|
217
|
+
when Symbol
|
218
|
+
make_var(x).map(&:chr).join("")
|
219
|
+
else
|
220
|
+
raise ArgumentError, "Unexpected type: #{x.class}"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Lignite
|
2
|
+
# Part of an assembled RBF file
|
3
|
+
class RbfObject
|
4
|
+
include Bytes
|
5
|
+
def self.vmthread(body:, local_bytes:)
|
6
|
+
new(owner: 0, triggers: 0, local_bytes: local_bytes, body: body)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.subcall(body:, local_bytes:)
|
10
|
+
new(owner: 0, triggers: 1, local_bytes: local_bytes, body: body)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.block(owner:, triggers:, body:)
|
14
|
+
new(owner: owner, triggers: triggers, local_bytes: 0, body: body)
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(owner:, triggers:, local_bytes:, body:)
|
18
|
+
@owner = owner
|
19
|
+
@triggers = triggers
|
20
|
+
@local_bytes = local_bytes
|
21
|
+
@body = body
|
22
|
+
end
|
23
|
+
|
24
|
+
def header(pos_before_header = 0)
|
25
|
+
u32(pos_before_header + 12) + # size of header
|
26
|
+
u16(@owner) + u16(@triggers) + u32(@local_bytes)
|
27
|
+
end
|
28
|
+
|
29
|
+
def body
|
30
|
+
@body
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Lignite
|
2
|
+
class SystemCommands
|
3
|
+
include Bytes
|
4
|
+
include Logger
|
5
|
+
extend Logger
|
6
|
+
|
7
|
+
# @param conn [Connection]
|
8
|
+
def initialize(conn = Connection.create)
|
9
|
+
@message_sender = MessageSender.new(conn)
|
10
|
+
load_yml
|
11
|
+
end
|
12
|
+
|
13
|
+
def load_yml
|
14
|
+
fname = File.expand_path("../../../data/sysops.yml", __FILE__)
|
15
|
+
op_hash = YAML.load_file(fname)["sysops"]
|
16
|
+
op_hash.each do |oname, odata|
|
17
|
+
load_op(oname, odata)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# oname LIST_FILES
|
22
|
+
def load_op(oname, odata)
|
23
|
+
ovalue = odata["value"]
|
24
|
+
|
25
|
+
param_handlers, return_handlers = handlers(odata)
|
26
|
+
|
27
|
+
osym = oname.downcase.to_sym
|
28
|
+
self.class.send(:define_method, osym) do |*args|
|
29
|
+
logger.debug "called #{osym} with #{args.inspect}"
|
30
|
+
if args.size != param_handlers.size
|
31
|
+
raise ArgumentError, "expected #{param_handlers.size} arguments, got #{args.size}"
|
32
|
+
end
|
33
|
+
|
34
|
+
bytes = u8(ovalue)
|
35
|
+
bytes += param_handlers.zip(args).map do |h, a|
|
36
|
+
# h.call(a) would have self = Op instead of #<Op>
|
37
|
+
instance_exec(a, &h)
|
38
|
+
end.join("")
|
39
|
+
logger.debug "sysop to execute: #{bytes.inspect}"
|
40
|
+
|
41
|
+
reply = @message_sender.system_command_with_reply(bytes)
|
42
|
+
|
43
|
+
# TODO: parse it with return_handlers
|
44
|
+
replies = return_handlers.map do |h|
|
45
|
+
parsed, reply = h.call(reply)
|
46
|
+
parsed
|
47
|
+
end
|
48
|
+
raise "Unparsed reply #{reply.inspect}" unless reply.empty?
|
49
|
+
# A single reply is returned as a scalar, not an array
|
50
|
+
replies.size == 1 ? replies.first : replies
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def handlers(odata)
|
55
|
+
oparams = odata["params"]
|
56
|
+
param_handlers = []
|
57
|
+
return_handlers = []
|
58
|
+
oparams.each do |p|
|
59
|
+
if p["dir"] == "in"
|
60
|
+
param_handlers << param_handler(p)
|
61
|
+
else
|
62
|
+
return_handlers << return_handler(p)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
[param_handlers, return_handlers]
|
66
|
+
end
|
67
|
+
|
68
|
+
def param_handler(oparam)
|
69
|
+
case oparam["type"]
|
70
|
+
when "U8"
|
71
|
+
->(x) { u8(x) }
|
72
|
+
when "U16"
|
73
|
+
->(x) { u16(x) }
|
74
|
+
when "U32"
|
75
|
+
->(x) { u32(x) }
|
76
|
+
when "BYTES"
|
77
|
+
->(x) { x }
|
78
|
+
when "ZBYTES"
|
79
|
+
->(x) { x + u8(0) }
|
80
|
+
else
|
81
|
+
raise
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# the handler is a lambda returning a pair:
|
86
|
+
# a parsed value and the rest of the input
|
87
|
+
def return_handler(oparam)
|
88
|
+
case oparam["type"]
|
89
|
+
when "U8"
|
90
|
+
->(i) { [unpack_u8(i[0, 1]), i[1..-1]] }
|
91
|
+
when "U16"
|
92
|
+
->(i) { [unpack_u16(i[0, 2]), i[2..-1]] }
|
93
|
+
when "U32"
|
94
|
+
->(i) { [unpack_u32(i[0, 4]), i[4..-1]] }
|
95
|
+
when "BYTES"
|
96
|
+
->(i) { [i, ""] }
|
97
|
+
else
|
98
|
+
raise
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Lignite
|
2
|
+
# Allocate local or global variables
|
3
|
+
class Variables
|
4
|
+
def initialize
|
5
|
+
@offset = 0
|
6
|
+
@vars = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def add(id, size)
|
10
|
+
raise "Duplicate variable #{id}" if @vars.key?(id)
|
11
|
+
@vars[id] = {offset: @offset, size: size}
|
12
|
+
@offset += size
|
13
|
+
end
|
14
|
+
|
15
|
+
def bytesize
|
16
|
+
@offset
|
17
|
+
end
|
18
|
+
|
19
|
+
def key?(sym)
|
20
|
+
@vars.key?(sym)
|
21
|
+
end
|
22
|
+
|
23
|
+
def offset(sym)
|
24
|
+
@vars[sym][:offset]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lignite.gemspec
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require File.expand_path(File.dirname(__FILE__) + "/lib/lignite/version")
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "lignite"
|
7
|
+
s.version = Lignite::VERSION
|
8
|
+
s.summary = "Program LEGO Mindstorms EV3 in Ruby"
|
9
|
+
s.description = <<TXT
|
10
|
+
Lignite is a set of Ruby tools to interact with LEGO Mindstorms EV3.
|
11
|
+
It uses the original LMS2012 firmware, so ev3dev is not required.
|
12
|
+
TXT
|
13
|
+
|
14
|
+
s.author = "Martin Vidner"
|
15
|
+
s.email = "martin@vidner.net"
|
16
|
+
s.homepage = "https://github.com/mvidner/lignite"
|
17
|
+
s.license = "GPL-3.0-only"
|
18
|
+
|
19
|
+
# ruby -e 'puts `git ls-files`.lines.map { |f| " %s,\n" % f.strip.inspect }'
|
20
|
+
s.files = [
|
21
|
+
".gitignore",
|
22
|
+
"COPYING",
|
23
|
+
"Gemfile",
|
24
|
+
"README.md",
|
25
|
+
"Rakefile",
|
26
|
+
"VERSION",
|
27
|
+
"bin/ev3tool",
|
28
|
+
"data/ev3.yml",
|
29
|
+
"data/lignite-btaddr",
|
30
|
+
"data/sysops.yml",
|
31
|
+
"examples/hello.rb",
|
32
|
+
"examples/lights.rb",
|
33
|
+
"examples/motors.rb",
|
34
|
+
"examples/sound.rb",
|
35
|
+
"examples/sys_list_files.rb",
|
36
|
+
"lib/lignite.rb",
|
37
|
+
"lib/lignite/assembler.rb",
|
38
|
+
"lib/lignite/body_compiler.rb",
|
39
|
+
"lib/lignite/bytes.rb",
|
40
|
+
"lib/lignite/connection.rb",
|
41
|
+
"lib/lignite/connection/bluetooth.rb",
|
42
|
+
"lib/lignite/connection/usb.rb",
|
43
|
+
"lib/lignite/direct_commands.rb",
|
44
|
+
"lib/lignite/logger.rb",
|
45
|
+
"lib/lignite/message.rb",
|
46
|
+
"lib/lignite/message_sender.rb",
|
47
|
+
"lib/lignite/op_compiler.rb",
|
48
|
+
"lib/lignite/rbf_object.rb",
|
49
|
+
"lib/lignite/system_commands.rb",
|
50
|
+
"lib/lignite/variables.rb",
|
51
|
+
"lib/lignite/version.rb",
|
52
|
+
"lignite.gemspec",
|
53
|
+
"spec/assembler_spec.rb",
|
54
|
+
"spec/data/HelloWorld-subop.rb",
|
55
|
+
"spec/data/HelloWorld-subop.rbf",
|
56
|
+
"spec/data/HelloWorld.lms",
|
57
|
+
"spec/data/HelloWorld.rb",
|
58
|
+
"spec/data/HelloWorld.rbf",
|
59
|
+
"spec/data/VernierReadout.lms",
|
60
|
+
"spec/data/VernierReadout.rb",
|
61
|
+
"spec/data/VernierReadout.rbf",
|
62
|
+
"spec/spec_helper.rb"
|
63
|
+
]
|
64
|
+
|
65
|
+
s.executables = s.files.grep(/^bin\//) { |f| File.basename(f) }
|
66
|
+
|
67
|
+
s.required_ruby_version = ">= 2.1" # mandatory keyword arguments
|
68
|
+
s.add_dependency "libusb", "~> 0.6"
|
69
|
+
|
70
|
+
s.add_development_dependency "coveralls", "~> 0"
|
71
|
+
s.add_development_dependency "simplecov", "~> 0"
|
72
|
+
s.add_development_dependency "rspec", "~> 3"
|
73
|
+
s.add_development_dependency "yard", "~> 0"
|
74
|
+
end
|