lignite 0.3.0 → 0.4.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 +4 -4
- data/.rubocop.yml +28 -0
- data/NEWS.md +6 -0
- data/Rakefile +22 -1
- data/VERSION +1 -1
- data/bin/ev3tool +2 -149
- data/examples/bobbee.rb +95 -73
- data/lib/lignite.rb +2 -0
- data/lib/lignite/assembler.rb +1 -1
- data/lib/lignite/bytes.rb +10 -2
- data/lib/lignite/connection.rb +13 -5
- data/lib/lignite/connection/bluetooth.rb +1 -0
- data/lib/lignite/connection/replay.rb +8 -10
- data/lib/lignite/connection/tap.rb +2 -8
- data/lib/lignite/connection/usb.rb +2 -0
- data/lib/lignite/defines.rb +244 -0
- data/lib/lignite/defines.rb.erb +14 -0
- data/lib/lignite/direct_commands.rb +1 -0
- data/lib/lignite/enums.rb +659 -0
- data/lib/lignite/enums.rb.erb +21 -0
- data/lib/lignite/ev3_ops.rb +9401 -0
- data/lib/lignite/ev3_tool.rb +175 -0
- data/lib/lignite/logger.rb +1 -0
- data/lib/lignite/message.rb +2 -0
- data/lib/lignite/motors.rb +1 -1
- data/lib/lignite/op_compiler.rb +14 -126
- data/lib/lignite/system_commands.rb +25 -26
- data/lignite.gemspec +17 -2
- data/spec/assembler_spec.rb +0 -2
- data/spec/data/ev3tool_download.yml +3 -0
- data/spec/data/ev3tool_list_files.yml +3 -0
- data/spec/data/ev3tool_start.yml +4 -0
- data/spec/data/ev3tool_stop.yml +2 -0
- data/spec/data/ev3tool_upload.yml +5 -0
- data/spec/data/everstorm.rbf +0 -0
- data/spec/direct_commands_spec.rb +1 -0
- data/spec/ev3_tool_spec.rb +71 -0
- data/spec/spec_helper.rb +6 -1
- data/tools/ops_from_yml +176 -0
- metadata +44 -16
@@ -0,0 +1,175 @@
|
|
1
|
+
require "lignite"
|
2
|
+
require "thor"
|
3
|
+
require "fileutils"
|
4
|
+
require "objspace"
|
5
|
+
|
6
|
+
module Lignite
|
7
|
+
# Implements the `ev3tool` command line interface
|
8
|
+
class Ev3Tool < Thor
|
9
|
+
# The VM current working directory is /home/root/lms2012/sys
|
10
|
+
# which is not very useful. A better default is /home/root/lms2012/prjs
|
11
|
+
# which is displayed on the 2nd tab of the brick UI.
|
12
|
+
EV3TOOL_HOME = "../prjs".freeze
|
13
|
+
|
14
|
+
include Lignite::Bytes
|
15
|
+
|
16
|
+
def self.exit_on_failure?
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
desc "upload LOCAL_FILENAME [BRICK_FILENAME]", "upload a program or a file"
|
21
|
+
map "ul" => "upload"
|
22
|
+
def upload(local_filename, brick_filename = nil)
|
23
|
+
data = File.read(local_filename, encoding: Encoding::BINARY)
|
24
|
+
unless brick_filename
|
25
|
+
prj = File.basename(local_filename, ".rbf")
|
26
|
+
brick_filename = "#{EV3TOOL_HOME}/#{prj}/#{prj}.rbf"
|
27
|
+
end
|
28
|
+
handle = sc.begin_download(data.bytesize, brick_filename)
|
29
|
+
sc.continue_download(handle, data)
|
30
|
+
end
|
31
|
+
|
32
|
+
desc "download BRICK_FILENAME [LOCAL_FILENAME]", "download a file"
|
33
|
+
map "dl" => "download"
|
34
|
+
def download(brick_filename, local_filename = nil)
|
35
|
+
local_filename ||= File.basename(brick_filename)
|
36
|
+
fsize, handle, data = sc.begin_upload(4096, brick_filename)
|
37
|
+
File.open(local_filename, "w") do |f|
|
38
|
+
loop do
|
39
|
+
f.write(data)
|
40
|
+
fsize -= data.bytesize
|
41
|
+
break if fsize.zero?
|
42
|
+
handle, data = sc.continue_upload(handle, 4096)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
desc "list-files DIRNAME", "list DIRNAME in a long format"
|
48
|
+
map "ls-l" => "list-files"
|
49
|
+
def list_files(name)
|
50
|
+
puts raw_list_files(name)
|
51
|
+
end
|
52
|
+
|
53
|
+
desc "ls DIRNAME", "list DIRNAME in a short format"
|
54
|
+
def ls(name)
|
55
|
+
puts raw_ls(name)
|
56
|
+
end
|
57
|
+
|
58
|
+
no_commands do
|
59
|
+
def raw_list_files(name)
|
60
|
+
name ||= EV3TOOL_HOME
|
61
|
+
name = "#{EV3TOOL_HOME}/#{name}" unless name.start_with?("/")
|
62
|
+
|
63
|
+
result = ""
|
64
|
+
fsize, handle, data = sc.list_files(4096, name)
|
65
|
+
loop do
|
66
|
+
result += data
|
67
|
+
fsize -= data.bytesize
|
68
|
+
break if fsize.zero?
|
69
|
+
handle, data = sc.continue_list_files(handle, 4096)
|
70
|
+
end
|
71
|
+
result
|
72
|
+
rescue Lignite::VMError
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
|
76
|
+
def raw_ls(name)
|
77
|
+
raw = raw_list_files(name)
|
78
|
+
return nil if raw.nil?
|
79
|
+
|
80
|
+
raw.lines.map do |l|
|
81
|
+
l = l.chomp
|
82
|
+
next nil if ["./", "../"].include?(l)
|
83
|
+
next l if l.end_with?("/")
|
84
|
+
# skip checksum + space + size + space
|
85
|
+
l[32 + 1 + 8 + 1..-1]
|
86
|
+
end.compact
|
87
|
+
end
|
88
|
+
|
89
|
+
def runnable_name(name)
|
90
|
+
if name.start_with?("/")
|
91
|
+
name
|
92
|
+
elsif name.include?("/")
|
93
|
+
"#{EV3TOOL_HOME}/#{name}"
|
94
|
+
else
|
95
|
+
"#{EV3TOOL_HOME}/#{name}/#{name}.rbf"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
desc "start NAME", "start a program"
|
101
|
+
def start(name)
|
102
|
+
name = runnable_name(name)
|
103
|
+
|
104
|
+
raise Thor::Error, "File #{name.inspect} not found on the brick" unless file_exist?(name)
|
105
|
+
|
106
|
+
slot = Lignite::USER_SLOT
|
107
|
+
no_debug = 0
|
108
|
+
dc.block do
|
109
|
+
# these are local variables
|
110
|
+
data32 :size
|
111
|
+
data32 :ip
|
112
|
+
file_load_image(slot, name, :size, :ip)
|
113
|
+
program_start(slot, :size, :ip, no_debug)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
desc "stop", "stop a running program"
|
118
|
+
def stop
|
119
|
+
dc.program_stop(Lignite::USER_SLOT)
|
120
|
+
end
|
121
|
+
|
122
|
+
no_commands do
|
123
|
+
def file_exist?(name)
|
124
|
+
dirname = File.dirname(name)
|
125
|
+
filename = File.basename(name)
|
126
|
+
files = raw_ls(dirname) || []
|
127
|
+
files.include?(filename)
|
128
|
+
end
|
129
|
+
|
130
|
+
def assisted_connection
|
131
|
+
c = Lignite::Connection.create
|
132
|
+
# When invoking via Thor we can't get at the instance otherwise :-/
|
133
|
+
ObjectSpace.define_finalizer(self, ->(_id) { close })
|
134
|
+
c
|
135
|
+
rescue StandardError => e
|
136
|
+
fn = Lignite::Connection::Bluetooth.config_filename
|
137
|
+
$stderr.puts <<MSG
|
138
|
+
Could not connect to EV3.
|
139
|
+
Use a USB cable or configure a Bluetooth address in #{fn.inspect}.
|
140
|
+
Details:
|
141
|
+
#{e.message}
|
142
|
+
MSG
|
143
|
+
|
144
|
+
try_config_from_template(fn, Lignite::Connection::Bluetooth.template_config_filename)
|
145
|
+
raise Thor::Error, ""
|
146
|
+
end
|
147
|
+
|
148
|
+
def try_config_from_template(config_fn, template_fn)
|
149
|
+
return unless !File.exist?(config_fn) && File.exist?(template_fn)
|
150
|
+
FileUtils.mkdir_p(File.dirname(config_fn))
|
151
|
+
FileUtils.install(template_fn, config_fn)
|
152
|
+
$stderr.puts "(A template config file has been copied for your convenience)"
|
153
|
+
end
|
154
|
+
|
155
|
+
def sc
|
156
|
+
return @sc if @sc
|
157
|
+
@sc = Lignite::SystemCommands.new(conn)
|
158
|
+
end
|
159
|
+
|
160
|
+
def dc
|
161
|
+
return @dc if @dc
|
162
|
+
@dc = Lignite::DirectCommands.new(conn)
|
163
|
+
end
|
164
|
+
|
165
|
+
def conn
|
166
|
+
@conn ||= assisted_connection
|
167
|
+
end
|
168
|
+
|
169
|
+
def close
|
170
|
+
@conn.close if @conn
|
171
|
+
@conn = nil
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
data/lib/lignite/logger.rb
CHANGED
data/lib/lignite/message.rb
CHANGED
@@ -71,6 +71,7 @@ module Lignite
|
|
71
71
|
end
|
72
72
|
end
|
73
73
|
|
74
|
+
# A reply to a SystemCommand
|
74
75
|
class SystemReply < Message
|
75
76
|
def initialize(msgid:, error:, body:)
|
76
77
|
@msgid = msgid
|
@@ -87,6 +88,7 @@ module Lignite
|
|
87
88
|
end
|
88
89
|
end
|
89
90
|
|
91
|
+
# A reply to a DirectCommand
|
90
92
|
class DirectReply < Message
|
91
93
|
def initialize(msgid:, error:, body:)
|
92
94
|
@msgid = msgid
|
data/lib/lignite/motors.rb
CHANGED
data/lib/lignite/op_compiler.rb
CHANGED
@@ -1,137 +1,18 @@
|
|
1
|
-
require "
|
1
|
+
require "lignite/defines"
|
2
|
+
require "lignite/enums"
|
3
|
+
require "lignite/ev3_ops"
|
2
4
|
|
3
5
|
module Lignite
|
4
|
-
#
|
6
|
+
# Compiles methods for all the instructions in ev3.yml
|
5
7
|
# The methods return the {ByteString}s corresponding to the ops.
|
6
8
|
class OpCompiler
|
7
|
-
# TODO: doing it dynamically
|
8
|
-
# - is slow
|
9
|
-
# - makes the implementation harder to understand
|
10
|
-
# - means we cannot use YARD to document the API
|
11
|
-
# Therefore we should generate (most of) op_compiler.rb statically from
|
12
|
-
# ev3.yml ahead of the time.
|
13
|
-
|
14
|
-
include Bytes
|
15
9
|
include Logger
|
16
|
-
|
17
|
-
|
18
|
-
# A marker for features that are not implemented yet
|
19
|
-
class TODO < StandardError
|
20
|
-
end
|
21
|
-
|
22
|
-
def self.load_const(name, value)
|
23
|
-
raise "duplicate constant #{name}" if Lignite.const_defined?(name)
|
24
|
-
Lignite.const_set(name, value)
|
25
|
-
end
|
26
|
-
|
27
|
-
def self.load_op(oname, odata)
|
28
|
-
ovalue = odata["value"]
|
29
|
-
oparams = odata["params"]
|
30
|
-
p1 = oparams.first
|
31
|
-
if p1 && p1["type"] == "SUBP"
|
32
|
-
commands = p1["commands"]
|
33
|
-
commands.each do |cname, cdata|
|
34
|
-
cvalue = cdata["value"]
|
35
|
-
load_const(cname, cvalue)
|
36
|
-
cparams = cdata["params"]
|
37
|
-
define_op("#{oname}_#{cname}", ovalue, cvalue, cparams)
|
38
|
-
end
|
39
|
-
define_multiop(oname, commands)
|
40
|
-
else
|
41
|
-
define_op(oname, ovalue, nil, oparams)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def self.define_multiop(oname, commands)
|
46
|
-
names = commands.map do |cname, cdata|
|
47
|
-
csym = cname.downcase.to_sym
|
48
|
-
cvalue = cdata["value"]
|
49
|
-
[cvalue, csym]
|
50
|
-
end.to_h
|
51
|
-
|
52
|
-
osym = oname.downcase.to_sym
|
53
|
-
define_method(osym) do |*args|
|
54
|
-
logger.debug "called #{osym} with #{args.inspect}"
|
55
|
-
cvalue = args.shift
|
56
|
-
csym = names.fetch(cvalue)
|
57
|
-
send("#{osym}_#{csym}", *args)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
def self.define_op(oname, ovalue, cvalue, params)
|
62
|
-
check_arg_count = true
|
63
|
-
param_handlers = params.map do |par|
|
64
|
-
case par["type"]
|
65
|
-
when "PARLAB" # a label, only one opcode
|
66
|
-
raise TODO
|
67
|
-
when "PARNO" # the value says how many other params follow
|
68
|
-
check_arg_count = false
|
69
|
-
->(x) { param_simple(x) }
|
70
|
-
when "PARS" # string
|
71
|
-
raise TODO
|
72
|
-
when "PARV" # value, type depends
|
73
|
-
raise TODO
|
74
|
-
when "PARVALUES"
|
75
|
-
raise TODO
|
76
|
-
when "PAR8", "PAR16", "PAR32", "PARF"
|
77
|
-
->(x) { param_simple(x) }
|
78
|
-
else
|
79
|
-
raise "Unhandled param type #{par["type"]}"
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
osym = oname.downcase.to_sym
|
84
|
-
define_method(osym) do |*args|
|
85
|
-
logger.debug "called #{osym} with #{args.inspect}"
|
86
|
-
if check_arg_count && args.size != param_handlers.size
|
87
|
-
raise ArgumentError, "expected #{param_handlers.size} arguments, got #{args.size}"
|
88
|
-
end
|
89
|
-
|
90
|
-
bytes = u8(ovalue)
|
91
|
-
bytes += param_simple(cvalue) unless cvalue.nil?
|
92
|
-
|
93
|
-
bytes += args.zip(param_handlers).map do |a, h|
|
94
|
-
h ||= ->(x) { param_simple(x) }
|
95
|
-
# h.call(a) would have self = Op instead of #<Op>
|
96
|
-
instance_exec(a, &h)
|
97
|
-
end.join("")
|
98
|
-
logger.debug "returning bytecode: #{bytes.inspect}"
|
99
|
-
bytes
|
100
|
-
end
|
101
|
-
rescue TODO
|
102
|
-
logger.debug "Could not define #{oname}"
|
103
|
-
end
|
104
|
-
|
105
|
-
@loaded = false
|
106
|
-
|
107
|
-
def self.load_yml
|
108
|
-
return if @loaded
|
109
|
-
fname = File.expand_path("../../../data/ev3.yml", __FILE__)
|
110
|
-
yml = YAML.load_file(fname)
|
111
|
-
op_hash = yml["ops"]
|
112
|
-
op_hash.each do |oname, odata|
|
113
|
-
load_op(oname, odata)
|
114
|
-
end
|
115
|
-
|
116
|
-
defines = yml["defines"]
|
117
|
-
defines.each do |dname, ddata|
|
118
|
-
load_const(dname, ddata["value"])
|
119
|
-
end
|
120
|
-
|
121
|
-
enums = yml["enums"]
|
122
|
-
enums.each_value do |edata|
|
123
|
-
edata["members"].each do |mname, mdata|
|
124
|
-
load_const(mname, mdata["value"])
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
|
-
@loaded = true
|
129
|
-
end
|
10
|
+
include Bytes
|
11
|
+
include Ev3Ops
|
130
12
|
|
131
13
|
# @param globals [Variables,nil]
|
132
14
|
# @param locals [Variables,nil]
|
133
15
|
def initialize(globals = nil, locals = nil)
|
134
|
-
self.class.load_yml
|
135
16
|
@globals = globals
|
136
17
|
@locals = locals
|
137
18
|
end
|
@@ -218,13 +99,20 @@ module Lignite
|
|
218
99
|
end
|
219
100
|
end
|
220
101
|
|
102
|
+
# @return [ByteString]
|
103
|
+
def param_multiple(*args)
|
104
|
+
args.map { |a| param_simple(a) }.join("")
|
105
|
+
end
|
106
|
+
|
221
107
|
# @return [ByteString]
|
222
108
|
def param_simple(x)
|
223
109
|
case x
|
224
110
|
when Integer
|
225
111
|
make_lc(x).map(&:chr).join("")
|
226
112
|
when Complex
|
227
|
-
# a Complex number:
|
113
|
+
# a Complex number:
|
114
|
+
# #real: just like an Integer above, but
|
115
|
+
# #imag tells how many bytes to use for it
|
228
116
|
make_lc(x.real, x.imag).map(&:chr).join("")
|
229
117
|
when String
|
230
118
|
u8(0x80) + x + u8(0x00)
|
@@ -1,4 +1,6 @@
|
|
1
1
|
module Lignite
|
2
|
+
# The commands that cannot appear in a .rbf program,
|
3
|
+
# used mostly for program manipulation
|
2
4
|
class SystemCommands
|
3
5
|
include Bytes
|
4
6
|
include Logger
|
@@ -52,15 +54,21 @@ module Lignite
|
|
52
54
|
|
53
55
|
reply = system_command_with_reply(bytes)
|
54
56
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
57
|
+
parse_reply(reply, return_handlers)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param reply [ByteString]
|
62
|
+
# @param return_handlers [Array<Proc>]
|
63
|
+
# @return [Object,Array<Object>]
|
64
|
+
def parse_reply(reply, return_handlers)
|
65
|
+
replies = return_handlers.map do |h|
|
66
|
+
parsed, reply = h.call(reply)
|
67
|
+
parsed
|
63
68
|
end
|
69
|
+
raise "Unparsed reply #{reply.inspect}" unless reply.empty?
|
70
|
+
# A single reply is returned as a scalar, not an array
|
71
|
+
replies.size == 1 ? replies.first : replies
|
64
72
|
end
|
65
73
|
|
66
74
|
def handlers(odata)
|
@@ -79,16 +87,11 @@ module Lignite
|
|
79
87
|
|
80
88
|
def param_handler(oparam)
|
81
89
|
case oparam["type"]
|
82
|
-
when "U8"
|
83
|
-
|
84
|
-
when "
|
85
|
-
|
86
|
-
when "
|
87
|
-
->(x) { u32(x) }
|
88
|
-
when "BYTES"
|
89
|
-
->(x) { x }
|
90
|
-
when "ZBYTES"
|
91
|
-
->(x) { x + u8(0) }
|
90
|
+
when "U8" then ->(x) { u8(x) }
|
91
|
+
when "U16" then ->(x) { u16(x) }
|
92
|
+
when "U32" then ->(x) { u32(x) }
|
93
|
+
when "BYTES" then ->(x) { x }
|
94
|
+
when "ZBYTES" then ->(x) { x + u8(0) }
|
92
95
|
else
|
93
96
|
raise
|
94
97
|
end
|
@@ -98,14 +101,10 @@ module Lignite
|
|
98
101
|
# a parsed value and the rest of the input
|
99
102
|
def return_handler(oparam)
|
100
103
|
case oparam["type"]
|
101
|
-
when "U8"
|
102
|
-
|
103
|
-
when "
|
104
|
-
|
105
|
-
when "U32"
|
106
|
-
->(i) { [unpack_u32(i[0, 4]), i[4..-1]] }
|
107
|
-
when "BYTES"
|
108
|
-
->(i) { [i, ""] }
|
104
|
+
when "U8" then ->(i) { [unpack_u8(i[0, 1]), i[1..-1]] }
|
105
|
+
when "U16" then ->(i) { [unpack_u16(i[0, 2]), i[2..-1]] }
|
106
|
+
when "U32" then ->(i) { [unpack_u32(i[0, 4]), i[4..-1]] }
|
107
|
+
when "BYTES" then ->(i) { [i, ""] }
|
109
108
|
else
|
110
109
|
raise
|
111
110
|
end
|