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,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