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.
@@ -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
@@ -1,6 +1,7 @@
1
1
  require "logger"
2
2
 
3
3
  module Lignite
4
+ # Include this to provide a simple `logger.debug` etc.
4
5
  module Logger
5
6
  def self.default_logger
6
7
  logger = ::Logger.new(STDERR)
@@ -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
@@ -144,7 +144,7 @@ module Lignite
144
144
  dc.output_clr_count(layer, nos)
145
145
  end
146
146
 
147
- def get_count
147
+ def get_count # rubocop:disable Naming/AccessorMethodName, upstream API name
148
148
  layer = @layer
149
149
  nos_as_indices.map do |no|
150
150
  tachos = dc.with_reply do
@@ -1,137 +1,18 @@
1
- require "yaml"
1
+ require "lignite/defines"
2
+ require "lignite/enums"
3
+ require "lignite/ev3_ops"
2
4
 
3
5
  module Lignite
4
- # Dynamically constructs methods for all the instructions in ev3.yml
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
- extend Logger
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: real: just like an Integer above, but imag tells how many bytes to use for it
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
- # TODO: parse it with return_handlers
56
- replies = return_handlers.map do |h|
57
- parsed, reply = h.call(reply)
58
- parsed
59
- end
60
- raise "Unparsed reply #{reply.inspect}" unless reply.empty?
61
- # A single reply is returned as a scalar, not an array
62
- replies.size == 1 ? replies.first : replies
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
- ->(x) { u8(x) }
84
- when "U16"
85
- ->(x) { u16(x) }
86
- when "U32"
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
- ->(i) { [unpack_u8(i[0, 1]), i[1..-1]] }
103
- when "U16"
104
- ->(i) { [unpack_u16(i[0, 2]), i[2..-1]] }
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