rpremote 0.1.0 → 0.3.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.
@@ -4,15 +4,17 @@ require "rbconfig"
4
4
  require "fileutils"
5
5
  require "rubygems/version"
6
6
  require_relative "mrbgems"
7
+ require_relative "picoruby_source_patch"
7
8
 
8
9
  module Rpremote
9
10
  class Builder
10
11
  class Error < Rpremote::Error; end
11
12
 
12
- def initialize(root: Dir.pwd, runner: nil, mrbgems_class: Mrbgems)
13
+ def initialize(root: Dir.pwd, runner: nil, mrbgems_class: Mrbgems, source_patcher: nil)
13
14
  @root = File.expand_path(root)
14
15
  @runner = runner || method(:run_command)
15
16
  @mrbgems_class = mrbgems_class
17
+ @source_patcher = source_patcher || method(:patch_source)
16
18
  end
17
19
 
18
20
  def build(output: $stdout, error: $stderr, **options)
@@ -23,6 +25,7 @@ module Rpremote
23
25
  Language.validate!(target.language)
24
26
 
25
27
  source_dir = source_for(target)
28
+ source_patcher.call(source_dir, target.language_version)
26
29
  environment = {
27
30
  "RPREMOTE_LANGUAGE" => target.language,
28
31
  "RPREMOTE_LANGUAGE_VERSION" => target.language_version,
@@ -61,7 +64,11 @@ module Rpremote
61
64
 
62
65
  private
63
66
 
64
- attr_reader :root, :runner, :mrbgems_class
67
+ attr_reader :root, :runner, :mrbgems_class, :source_patcher
68
+
69
+ def patch_source(source, version)
70
+ PicoRubySourcePatch.new(version: version).apply(source)
71
+ end
65
72
 
66
73
  def add_mrbgems_environment!(environment, options, source_dir, output)
67
74
  definition = mrbgems_definition(options[:mrbgems])
data/lib/rpremote/cli.rb CHANGED
@@ -2,12 +2,17 @@
2
2
 
3
3
  require "optparse"
4
4
  require_relative "build_command"
5
+ require_relative "bootsel_command"
6
+ require_relative "config_show"
5
7
  require_relative "dfu_command"
8
+ require_relative "deploy_command"
6
9
  require_relative "flash_command"
7
10
  require_relative "help"
8
11
  require_relative "mrbgems_command"
12
+ require_relative "recursive_copy"
9
13
  require_relative "setup_command"
10
14
 
15
+ # rubocop:disable Metrics/ClassLength
11
16
  module Rpremote
12
17
  class CLI
13
18
  def self.start(argv = ARGV, config: Config, **options)
@@ -36,6 +41,8 @@ module Rpremote
36
41
  @config_class = config
37
42
  @config_filename, config_required = config.extract_option!(args)
38
43
  command = args.shift
44
+ return show_help(command, args) if Help.requested?(command, args)
45
+
39
46
  @config_options = config.load_command(command, filename: config_filename, required: config_required)
40
47
  dispatch(command, args)
41
48
  0
@@ -46,6 +53,11 @@ module Rpremote
46
53
  attr_reader :stdout, :stderr, :serial, :device, :flasher,
47
54
  :config_class, :config_filename, :config_options
48
55
 
56
+ def show_help(command, args)
57
+ stdout.puts(Help.command_text(command, args))
58
+ 0
59
+ end
60
+
49
61
  def dispatch(command, args)
50
62
  case command
51
63
  when nil, "help", "--help", "-h"
@@ -54,7 +66,9 @@ module Rpremote
54
66
  stdout.puts(Rpremote::VERSION)
55
67
  when "ports"
56
68
  ports(args)
57
- when "setup", "flash", "build", "dfu", "mrbgems"
69
+ when "config"
70
+ config_command(args)
71
+ when "setup", "flash", "build", "bootsel", "deploy", "dfu", "mrbgems"
58
72
  project_command(command, args)
59
73
  when "run", "monitor", "repl", "exec", "reset", "fs"
60
74
  remote_command(command, args)
@@ -66,21 +80,28 @@ module Rpremote
66
80
  def project_command(command, args)
67
81
  case command
68
82
  when "setup"
69
- SetupCommand.run(
70
- args,
71
- defaults: config_options,
72
- config: config_class,
73
- config_filename: config_filename,
74
- output: stdout
75
- )
83
+ SetupCommand.run(args, defaults: config_options, config: config_class, config_filename: config_filename, output: stdout)
76
84
  when "flash"
77
85
  FlashCommand.run(args, defaults: config_options, output: stdout, flasher: flasher)
78
86
  when "build"
79
87
  BuildCommand.run(args, defaults: config_options, output: stdout, error: stderr)
80
- when "dfu"
81
- DfuCommand.run(
82
- args, defaults: config_options, output: stdout, services: { serial: serial, device: device }
88
+ when "bootsel"
89
+ BootselCommand.run(
90
+ args,
91
+ defaults: config_options,
92
+ output: stdout,
93
+ services: { flasher: flasher, serial: serial, device: device }
94
+ )
95
+ when "deploy"
96
+ DeployCommand.run(
97
+ args,
98
+ defaults: config_options,
99
+ output: stdout,
100
+ error: stderr,
101
+ services: { flasher: flasher, serial: serial, device: device }
83
102
  )
103
+ when "dfu"
104
+ DfuCommand.run(args, defaults: config_options, output: stdout, services: { serial: serial, device: device })
84
105
  when "mrbgems"
85
106
  MrbgemsCommand.run(args, output: stdout)
86
107
  end
@@ -108,12 +129,22 @@ module Rpremote
108
129
  end
109
130
  end
110
131
 
132
+ def config_command(args)
133
+ subcommand = args.shift
134
+ raise ArgumentError, "unknown config command: #{subcommand || "(none)"}" unless subcommand == "show"
135
+
136
+ ConfigShow.run(args, defaults: config_options, output: stdout)
137
+ end
138
+
111
139
  def fs(args)
112
140
  subcommand = args.shift
141
+ recursive = subcommand == "cp" && (args.delete("--recursive") || args.delete("-r"))
113
142
  options = parse_connection_options(args)
114
143
  case subcommand
115
144
  when "cp"
116
- copy(args, options)
145
+ copy(args, options, recursive: recursive)
146
+ when "push"
147
+ copy(args, options, recursive: true, command: "push")
117
148
  when "cat"
118
149
  cat(args, options)
119
150
  when "ls", "rm", "mkdir"
@@ -124,12 +155,14 @@ module Rpremote
124
155
  end
125
156
 
126
157
  def run_file(args)
158
+ reset_on_timeout = !args.delete("--reset-on-timeout").nil?
127
159
  options = parse_connection_options(args)
128
160
  raise ArgumentError, "usage: rpremote run FILE [options]" unless args.length == 1
129
161
 
130
162
  source = args.first
131
- data = File.binread(source)
132
- stdout.write(execute_temporary(data, options))
163
+ source = File.join(source, "main.rb") if File.directory?(source)
164
+ data = prepend_mrbgem_requires(File.binread(source))
165
+ execute_temporary(data, options, output: stdout, reset_on_timeout: reset_on_timeout)
133
166
  rescue Errno::ENOENT => e
134
167
  raise ArgumentError, e.message
135
168
  end
@@ -138,15 +171,32 @@ module Rpremote
138
171
  options = parse_connection_options(args)
139
172
  raise ArgumentError, "usage: rpremote exec CODE [options]" unless args.length == 1
140
173
 
141
- stdout.write(execute_temporary(args.first, options))
174
+ execute_temporary(prepend_mrbgem_requires(args.first), options, output: stdout)
142
175
  end
143
176
 
144
- def execute_temporary(data, options)
177
+ def execute_temporary(data, options, output: nil, reset_on_timeout: false)
145
178
  Language.validate!(options[:language])
146
179
  port_path = device.main_port(options[:port])
147
180
  serial.open(port_path, baud: options[:baud]) do |port|
148
- Runner.new(port, timeout: options[:timeout]).run(data)
181
+ runner = Runner.new(port, timeout: options[:timeout])
182
+ output ? runner.run(data, output: output, diagnostics: stderr) : runner.run(data, diagnostics: stderr)
149
183
  end
184
+ rescue Shell::TimeoutError => e
185
+ reset_after_run_timeout(port_path, options, e) if reset_on_timeout
186
+ raise
187
+ end
188
+
189
+ def reset_after_run_timeout(port_path, options, timeout_error)
190
+ stderr.puts("rpremote: run timed out; resetting R2P2: #{port_path}")
191
+ Resetter.new(serial: serial, timeout: options[:timeout]).reset(port_path, baud: options[:baud])
192
+ stderr.puts("rpremote: reset R2P2 after run timeout: #{port_path}")
193
+ rescue Rpremote::Error, IOError, SystemCallError => e
194
+ raise Shell::TimeoutError,
195
+ "#{timeout_error.message}; automatic reset failed: #{e.message}"
196
+ end
197
+
198
+ def prepend_mrbgem_requires(data)
199
+ Mrbgems.new(cwd: Dir.pwd).prepend_requires(data)
150
200
  end
151
201
 
152
202
  def interactive(args, repl: false)
@@ -195,15 +245,15 @@ module Rpremote
195
245
  options
196
246
  end
197
247
 
198
- def copy(args, options)
199
- raise ArgumentError, "usage: rpremote fs cp SOURCE DESTINATION [options]" unless args.length == 2
248
+ def copy(args, options, recursive: false, command: "cp")
249
+ usage = command == "push" ? "rpremote fs push LOCAL_DIR :/REMOTE_DIR" : "rpremote fs cp SOURCE DESTINATION"
250
+ raise ArgumentError, "usage: #{usage} [options]" unless args.length == 2
200
251
 
201
252
  source, destination = args
202
253
  source_remote = RemotePath.remote?(source)
203
254
  destination_remote = RemotePath.remote?(destination)
204
- if source_remote == destination_remote
205
- raise ArgumentError, "exactly one cp path must be remote (prefix remote paths with :)"
206
- end
255
+ raise ArgumentError, "exactly one cp path must be remote (prefix remote paths with :)" if source_remote == destination_remote
256
+ return RecursiveCopy.new(output: stdout, serial: serial, device: device).call(source, destination, options) if recursive
207
257
 
208
258
  if source_remote
209
259
  data = with_modem(options) { |modem| modem.download(RemotePath.unwrap(source)) }
@@ -230,6 +280,7 @@ module Rpremote
230
280
  raise ArgumentError, usage unless args.length == 1
231
281
 
232
282
  path = RemotePath.validate(args.first)
283
+ stdout.puts("deleting remote path permanently: #{path}") if command == "rm"
233
284
  output = with_shell(options) do |shell|
234
285
  shell.execute("#{command} #{Shell.quote_argument(path)}")
235
286
  end
@@ -265,3 +316,4 @@ module Rpremote
265
316
  end
266
317
  end
267
318
  end
319
+ # rubocop:enable Metrics/ClassLength
@@ -21,6 +21,8 @@ module Rpremote
21
21
  COMMAND_OPTIONS = {
22
22
  "setup" => %i[cache language language_version],
23
23
  "build" => %i[cache language language_version board firmware mrbgems],
24
+ "bootsel" => %i[mount port baud timeout],
25
+ "deploy" => %i[cache language language_version board firmware mrbgems mount port baud timeout],
24
26
  "dfu" => %i[cache language language_version port baud timeout],
25
27
  "flash" => %i[cache language language_version board firmware mount port timeout],
26
28
  "run" => %i[port baud timeout language],
@@ -29,6 +31,7 @@ module Rpremote
29
31
  "monitor" => %i[port baud timeout],
30
32
  "repl" => %i[port baud timeout],
31
33
  "fs" => %i[port baud timeout],
34
+ "config" => OPTION_TYPES.keys,
32
35
  "ports" => [],
33
36
  "mrbgems" => []
34
37
  }.freeze
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "picomodem"
5
+ require_relative "serial"
6
+ require_relative "target"
7
+
8
+ module Rpremote
9
+ class ConfigShow
10
+ def self.run(args, defaults:, output:)
11
+ new(defaults: defaults, output: output).run(args)
12
+ end
13
+
14
+ def initialize(defaults:, output:)
15
+ @defaults = defaults
16
+ @output = output
17
+ end
18
+
19
+ def run(args)
20
+ options = effective_options
21
+ parser = option_parser(options)
22
+ parser.parse!(args)
23
+ raise ArgumentError, "usage: rpremote config show [options]" unless args.empty?
24
+ raise ArgumentError, "--baud must be positive" unless options[:baud].positive?
25
+ raise ArgumentError, "--timeout must be positive" unless options[:timeout].positive?
26
+
27
+ print_options(options)
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :defaults, :output
33
+
34
+ def effective_options
35
+ {
36
+ language: defaults.fetch(:language, Target::DEFAULT_LANGUAGE),
37
+ language_version: defaults.fetch(:language_version, Target::DEFAULT_LANGUAGE_VERSION),
38
+ board: defaults.fetch(:board, Target::DEFAULT_BOARD),
39
+ cache: defaults.fetch(:cache, Target::DEFAULT_CACHE_DIR),
40
+ firmware: defaults[:firmware],
41
+ mrbgems: defaults[:mrbgems],
42
+ mount: defaults[:mount],
43
+ port: defaults[:port],
44
+ baud: defaults.fetch(:baud, Serial::BAUD_RATE),
45
+ timeout: defaults.fetch(:timeout, PicoModem::DEFAULT_TIMEOUT)
46
+ }
47
+ end
48
+
49
+ def option_parser(options)
50
+ OptionParser.new do |opts|
51
+ opts.on("--language LANGUAGE") { |value| options[:language] = value }
52
+ opts.on("--language-version VERSION") { |value| options[:language_version] = value }
53
+ opts.on("--board BOARD") { |value| options[:board] = value }
54
+ opts.on("--cache DIR") { |value| options[:cache] = value }
55
+ opts.on("--firmware FILE") { |value| options[:firmware] = value }
56
+ opts.on("--mrbgems FILE") { |value| options[:mrbgems] = value }
57
+ opts.on("--no-mrbgems") { options[:mrbgems] = false }
58
+ opts.on("--mount DIR") { |value| options[:mount] = value }
59
+ opts.on("--port PORT") { |value| options[:port] = value }
60
+ opts.on("--baud RATE", Integer) { |value| options[:baud] = value }
61
+ opts.on("--timeout SECONDS", Float) { |value| options[:timeout] = value }
62
+ end
63
+ end
64
+
65
+ def print_options(options)
66
+ target = Target.new(
67
+ language: options[:language],
68
+ language_version: options[:language_version],
69
+ board: options[:board],
70
+ cache_dir: options[:cache],
71
+ firmware: options[:firmware]
72
+ )
73
+ output.puts("language=#{target.language}")
74
+ output.puts("language_version=#{target.language_version}")
75
+ output.puts("board=#{target.board}")
76
+ output.puts("cache=#{target.cache_dir}")
77
+ output.puts("firmware=#{target.firmware_path}")
78
+ output.puts("mrbgems=#{mrbgems_value(options[:mrbgems])}")
79
+ output.puts("mount=#{options[:mount] || "auto"}")
80
+ output.puts("port=#{options[:port] || "auto"}")
81
+ output.puts("baud=#{options[:baud]}")
82
+ output.puts("timeout=#{options[:timeout]}")
83
+ end
84
+
85
+ def mrbgems_value(value)
86
+ return "false" if value == false
87
+
88
+ value || "auto"
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "bootsel_command"
5
+ require_relative "recursive_copy"
6
+
7
+ module Rpremote
8
+ class DeployCommand
9
+ SHELL_READY_TIMEOUT = 2.0
10
+ RETRY_INTERVAL = 0.25
11
+
12
+ class Error < Rpremote::Error; end
13
+
14
+ # The command intentionally keeps the six deployment stages visible in execution order.
15
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
16
+ def self.run(args, defaults:, output: $stdout, error: $stderr, services: {})
17
+ options = parse_options(args, defaults)
18
+ raise ArgumentError, "usage: rpremote deploy PATH [options]" unless args.length == 1
19
+
20
+ project = File.expand_path(args.first)
21
+ library, remote_library, source = project_files(project)
22
+ Language.validate!(options[:language])
23
+
24
+ builder = services.fetch(:builder) { Builder.new }
25
+ flasher = services.fetch(:flasher, Flasher)
26
+ serial = services.fetch(:serial, Serial)
27
+ device = services.fetch(:device, Device)
28
+ runner = services.fetch(:runner, Runner)
29
+ target_options = options.slice(:language, :language_version, :board, :cache_dir, :firmware)
30
+ firmware_path = Target.new(**target_options).firmware_path(root: Dir.pwd)
31
+
32
+ output.puts("deploy build: #{firmware_path}")
33
+ builder.build(
34
+ **target_options,
35
+ mrbgems: options[:mrbgems],
36
+ output: output,
37
+ error: error
38
+ )
39
+
40
+ flash_service = flasher.new(timeout: options[:timeout])
41
+ bootsel_mount = resolve_bootsel_mount(options, flash_service, output, services)
42
+
43
+ mount = options[:mount] || "an automatically detected RP2350 BOOTSEL volume"
44
+ output.puts("deploy flash: #{firmware_path} to #{mount}; this replaces persistent board firmware")
45
+ result = flash_service.flash(
46
+ firmware_path,
47
+ mount: bootsel_mount,
48
+ port: options[:port]
49
+ )
50
+
51
+ connection_options = options.merge(port: result.port)
52
+ output.puts("deploy connect: waiting for R2P2 Shell on #{result.port}")
53
+ wait_for_shell(
54
+ port: result.port, baud: options[:baud], timeout: options[:timeout], serial: serial,
55
+ shell: services.fetch(:shell, Shell),
56
+ sleeper: services.fetch(:sleeper) { ->(seconds) { sleep(seconds) } },
57
+ clock: services.fetch(:clock) { -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) } }
58
+ )
59
+ output.puts("deploy connect: ready on #{result.port}")
60
+
61
+ if File.directory?(library)
62
+ output.puts("deploy push: #{library} -> :#{remote_library}")
63
+ recursive_copy = services.fetch(:recursive_copy) do
64
+ RecursiveCopy.new(output: output, serial: serial, device: device)
65
+ end
66
+ recursive_copy.call(library, ":#{remote_library}", connection_options)
67
+ else
68
+ output.puts("deploy push: skipped; directory not found: #{library}")
69
+ end
70
+
71
+ output.puts("deploy run: #{source} on #{result.port}")
72
+ source_data = prepend_mrbgem_requires(File.binread(source), options[:mrbgems])
73
+ run_output = serial.open(result.port, baud: options[:baud]) do |port|
74
+ runner.new(port, timeout: options[:timeout]).run(
75
+ source_data, output: output, cleanup: false, remote_path: Runner::DEPLOY_REMOTE_PATH
76
+ )
77
+ end
78
+ output.puts("deploy run: completed; output bytes: #{run_output.to_s.bytesize}")
79
+ rescue Errno::ENOENT => e
80
+ raise ArgumentError, e.message
81
+ end
82
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
83
+
84
+ def self.project_files(project)
85
+ raise ArgumentError, "project directory not found: #{project}" unless File.directory?(project)
86
+
87
+ name = File.basename(project)
88
+ library = File.join(project, "lib", name)
89
+ source = File.join(project, "main.rb")
90
+ raise ArgumentError, "project entry file not found: #{source}" unless File.file?(source)
91
+
92
+ [library, File.join("/lib", name), source]
93
+ end
94
+ private_class_method :project_files
95
+
96
+ def self.prepend_mrbgem_requires(source, definition)
97
+ return source if definition == false
98
+
99
+ Mrbgems.new(path: definition || Mrbgems::DEFAULT_PATH, cwd: Dir.pwd).prepend_requires(source)
100
+ end
101
+ private_class_method :prepend_mrbgem_requires
102
+
103
+ def self.wait_for_shell(context)
104
+ port = context.fetch(:port)
105
+ baud = context.fetch(:baud)
106
+ timeout = context.fetch(:timeout)
107
+ serial = context.fetch(:serial)
108
+ shell = context.fetch(:shell)
109
+ sleeper = context.fetch(:sleeper)
110
+ clock = context.fetch(:clock)
111
+ deadline = clock.call + timeout
112
+ loop do
113
+ serial.open(port, baud:) do |connection|
114
+ remaining = deadline - clock.call
115
+ raise Shell::TimeoutError, "R2P2 Shell is not ready" unless remaining.positive?
116
+
117
+ shell.new(connection, timeout: [remaining, SHELL_READY_TIMEOUT].min).synchronize!
118
+ end
119
+ return
120
+ rescue Serial::ConfigurationError, Shell::TimeoutError, IOError, SystemCallError
121
+ raise Error, "timed out waiting for R2P2 Shell after flash" if clock.call >= deadline
122
+
123
+ sleeper.call(RETRY_INTERVAL)
124
+ end
125
+ end
126
+ private_class_method :wait_for_shell
127
+
128
+ def self.resolve_bootsel_mount(options, flash_service, output, services)
129
+ mounted = flash_service.find_mounted(options[:mount])
130
+ if mounted
131
+ output.puts("deploy bootsel: already mounted at #{mounted}; skipping serial reset")
132
+ return mounted
133
+ end
134
+
135
+ port = options[:port] || "an automatically detected R2P2 port"
136
+ output.puts("deploy bootsel: requesting USB BOOTSEL mode through #{port}")
137
+ BootselCommand.enter(
138
+ mount: options[:mount], port: options[:port], baud: options[:baud], timeout: options[:timeout],
139
+ output: output, services: services.slice(:flasher, :serial, :device, :shell)
140
+ )
141
+ end
142
+ private_class_method :resolve_bootsel_mount
143
+
144
+ def self.parse_options(args, defaults)
145
+ options = default_options(defaults)
146
+ option_parser(options).parse!(args)
147
+ validate_options!(options)
148
+ options
149
+ end
150
+ private_class_method :parse_options
151
+
152
+ def self.default_options(defaults)
153
+ {
154
+ language: defaults.fetch(:language, Target::DEFAULT_LANGUAGE),
155
+ language_version: defaults.fetch(:language_version, Target::DEFAULT_LANGUAGE_VERSION),
156
+ board: defaults.fetch(:board, Target::DEFAULT_BOARD),
157
+ cache_dir: defaults.fetch(:cache, Target::DEFAULT_CACHE_DIR),
158
+ firmware: defaults[:firmware],
159
+ mrbgems: defaults[:mrbgems],
160
+ mount: defaults[:mount],
161
+ port: defaults[:port],
162
+ baud: defaults.fetch(:baud, Serial::BAUD_RATE),
163
+ timeout: defaults.fetch(:timeout, Flasher::DEFAULT_TIMEOUT)
164
+ }
165
+ end
166
+ private_class_method :default_options
167
+
168
+ def self.option_parser(options)
169
+ OptionParser.new do |parser|
170
+ parser.on("--language LANGUAGE") { |value| options[:language] = value }
171
+ parser.on("--language-version VERSION") { |value| options[:language_version] = value }
172
+ parser.on("--board BOARD") { |value| options[:board] = value }
173
+ parser.on("--cache DIR") { |value| options[:cache_dir] = value }
174
+ parser.on("--firmware FILE") { |value| options[:firmware] = value }
175
+ parser.on("--mrbgems FILE") { |value| options[:mrbgems] = value }
176
+ parser.on("--no-mrbgems") { options[:mrbgems] = false }
177
+ parser.on("--mount DIR") { |value| options[:mount] = value }
178
+ parser.on("--port PORT") { |value| options[:port] = value }
179
+ parser.on("--baud RATE", Integer) { |value| options[:baud] = value }
180
+ parser.on("--timeout SECONDS", Float) { |value| options[:timeout] = value }
181
+ end
182
+ end
183
+ private_class_method :option_parser
184
+
185
+ def self.validate_options!(options)
186
+ raise ArgumentError, "--baud must be positive" unless options[:baud].positive?
187
+ raise ArgumentError, "--timeout must be positive" unless options[:timeout].positive?
188
+ end
189
+ private_class_method :validate_options!
190
+ end
191
+ end
@@ -4,20 +4,25 @@ require "optparse"
4
4
 
5
5
  module Rpremote
6
6
  class DfuCommand
7
- DEFAULT_TIMEOUT = 10.0
7
+ DEFAULT_TIMEOUT = 20.0
8
8
  TYPES = { ".rb" => "RUBY", ".mrb" => "RITE" }.freeze
9
9
 
10
10
  def self.run(args, defaults:, output: $stdout, services: {})
11
- default_services = { serial: Serial, device: Device, modem: PicoModem, runner: Runner, compiler: DfuCompiler }
11
+ default_services = {
12
+ serial: Serial, device: Device, modem: PicoModem, runner: Runner,
13
+ compiler: DfuCompiler
14
+ }
12
15
  services = default_services.merge(services)
13
16
  subcommand = args.shift
14
17
  case subcommand
15
18
  when "app" then stage(args, defaults, output, services)
16
19
  when "compile" then compile(args, defaults, output, services[:compiler])
17
20
  when "status" then status(args, defaults, output, services)
21
+ when "remove" then remove(args, defaults, output, services)
18
22
  else
19
23
  raise ArgumentError,
20
- "usage: rpremote dfu app FILE [options], dfu compile FILE [options], or dfu status [options]"
24
+ "usage: rpremote dfu app FILE [options], dfu compile FILE [options], " \
25
+ "dfu status [options], or dfu remove [options]"
21
26
  end
22
27
  end
23
28
 
@@ -30,6 +35,8 @@ module Rpremote
30
35
  type = options[:type] || type_for(source)
31
36
  port_path = services[:device].main_port(options[:port])
32
37
  verify_rite_version!(data, port_path, options, services) if type == "RITE"
38
+ output.puts("staging DFU #{type.downcase} app (#{data.bytesize} bytes): #{source} -> inactive slot on #{port_path}; " \
39
+ "this changes the staged boot application")
33
40
  services[:serial].open(port_path, baud: options[:baud]) do |port|
34
41
  services[:modem].new(port, timeout: options[:timeout]).dfu(data, type: type)
35
42
  end
@@ -50,7 +57,7 @@ module Rpremote
50
57
  private_class_method :compile
51
58
 
52
59
  def self.status(args, defaults, output, services)
53
- options = parse_app_options(args, defaults)
60
+ options = parse_connection_options(args, defaults)
54
61
  raise ArgumentError, "usage: rpremote dfu status [options]" unless args.empty?
55
62
 
56
63
  port_path = services[:device].main_port(options[:port])
@@ -69,6 +76,45 @@ module Rpremote
69
76
  end
70
77
  private_class_method :status
71
78
 
79
+ def self.remove(args, defaults, output, services)
80
+ options = parse_connection_options(args, defaults)
81
+ raise ArgumentError, "usage: rpremote dfu remove [options]" unless args.empty?
82
+
83
+ port_path = services[:device].main_port(options[:port])
84
+ code = <<~'RUBY'
85
+ require "dfu"
86
+ # A confirmed DFU launcher may have returned after leaving a Sandbox
87
+ # worker alive. Stop known Processing workers before waiting for the
88
+ # Shell prompt; deleting their source files does not stop RAM tasks.
89
+ begin
90
+ $imu_processing_stream.close if $imu_processing_stream
91
+ $imu_processing_stream = nil
92
+ rescue Exception
93
+ $imu_processing_stream = nil
94
+ end
95
+ begin
96
+ $mpu6050_processing_stream.close if $mpu6050_processing_stream
97
+ $mpu6050_processing_stream = nil
98
+ rescue Exception
99
+ $mpu6050_processing_stream = nil
100
+ end
101
+ DFU::Meta.recover
102
+ DFU::Meta.save(DFU::Meta.deep_copy(DFU::Meta::DEFAULT))
103
+ %w[a b].each do |slot|
104
+ %w[rb mrb].each do |ext|
105
+ path = "#{ENV['HOME']}/app_#{slot}.#{ext}"
106
+ File.unlink(path) if File.exist?(path)
107
+ end
108
+ end
109
+ RUBY
110
+ output.puts("removing all DFU boot applications from #{port_path}; this permanently clears both A/B slots")
111
+ services[:serial].open(port_path, baud: options[:baud]) do |port|
112
+ services[:runner].new(port, timeout: options[:timeout]).run(code)
113
+ end
114
+ output.puts("removed all DFU boot applications; run `rpremote reset` to stop the running application")
115
+ end
116
+ private_class_method :remove
117
+
72
118
  def self.parse_app_options(args, defaults)
73
119
  options = {
74
120
  port: defaults[:port],
@@ -89,6 +135,24 @@ module Rpremote
89
135
  end
90
136
  private_class_method :parse_app_options
91
137
 
138
+ def self.parse_connection_options(args, defaults)
139
+ options = {
140
+ port: defaults[:port],
141
+ baud: defaults.fetch(:baud, Serial::BAUD_RATE),
142
+ timeout: defaults.fetch(:timeout, DEFAULT_TIMEOUT)
143
+ }
144
+ OptionParser.new do |parser|
145
+ parser.on("--port PORT") { |value| options[:port] = value }
146
+ parser.on("--baud RATE", Integer) { |value| options[:baud] = value }
147
+ parser.on("--timeout SECONDS", Float) { |value| options[:timeout] = value }
148
+ end.parse!(args)
149
+ raise ArgumentError, "--baud must be positive" unless options[:baud].positive?
150
+ raise ArgumentError, "--timeout must be positive" unless options[:timeout].positive?
151
+
152
+ options
153
+ end
154
+ private_class_method :parse_connection_options
155
+
92
156
  def self.parse_compile_options(args, defaults)
93
157
  options = {
94
158
  language: defaults.fetch(:language, Target::DEFAULT_LANGUAGE),
@@ -122,9 +186,7 @@ module Rpremote
122
186
 
123
187
  def self.verify_rite_version!(data, port_path, options, services)
124
188
  bytecode_version = data.byteslice(0, 8)
125
- unless bytecode_version&.match?(/\ARITE\d{4}\z/)
126
- raise ArgumentError, "DFU RITE app must start with a RITE bytecode header"
127
- end
189
+ raise ArgumentError, "DFU RITE app must start with a RITE bytecode header" unless bytecode_version&.match?(/\ARITE\d{4}\z/)
128
190
 
129
191
  picoruby_version = services[:serial].open(port_path, baud: options[:baud]) do |port|
130
192
  services[:runner].new(port, timeout: options[:timeout]).run("p PICORUBY_VERSION")[/\d+\.\d+\.\d+/]