lignite 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,4 @@
1
+ module Lignite
2
+ # Lignite version (uses [semantic versioning](http://semver.org/)).
3
+ VERSION = File.read(File.dirname(__FILE__) + "/../../VERSION").strip
4
+ 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