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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -19
- data/README.ja.md +58 -34
- data/README.md +56 -33
- data/RELEASING.md +23 -23
- data/THIRD_PARTY_NOTICES.md +10 -20
- data/lib/rpremote/bootsel_command.rb +124 -0
- data/lib/rpremote/builder.rb +9 -2
- data/lib/rpremote/cli.rb +74 -22
- data/lib/rpremote/config.rb +3 -0
- data/lib/rpremote/config_show.rb +91 -0
- data/lib/rpremote/deploy_command.rb +191 -0
- data/lib/rpremote/dfu_command.rb +69 -7
- data/lib/rpremote/flash_command.rb +9 -3
- data/lib/rpremote/flasher.rb +50 -12
- data/lib/rpremote/help.rb +309 -29
- data/lib/rpremote/language_source.rb +4 -1
- data/lib/rpremote/mrbgems.rb +41 -12
- data/lib/rpremote/nuke_firmware.rb +71 -0
- data/lib/rpremote/picomodem.rb +3 -7
- data/lib/rpremote/picoruby_source_patch.rb +49 -0
- data/lib/rpremote/recursive_copy.rb +84 -0
- data/lib/rpremote/resetter.rb +1 -1
- data/lib/rpremote/runner.rb +33 -6
- data/lib/rpremote/setup_command.rb +4 -1
- data/lib/rpremote/shell.rb +45 -10
- data/lib/rpremote/terminal.rb +13 -1
- data/lib/rpremote/version.rb +1 -1
- data/lib/rpremote.rb +5 -0
- data/patches/picoruby-3.4.5-bootsel.patch +71 -0
- data/patches/picoruby-3.4.5-ruby-exception-status.patch +59 -0
- data/patches/picoruby-4.0.3-bootsel.patch +71 -0
- data/patches/picoruby-4.0.3-ruby-exception-status.patch +59 -0
- data/sig/rpremote.rbs +67 -5
- data/tasks/firmware_build.rb +2 -8
- data/tasks/release_check.rb +3 -7
- metadata +11 -1
data/lib/rpremote/builder.rb
CHANGED
|
@@ -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 "
|
|
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 "
|
|
81
|
-
|
|
82
|
-
args,
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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])
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/rpremote/config.rb
CHANGED
|
@@ -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
|
data/lib/rpremote/dfu_command.rb
CHANGED
|
@@ -4,20 +4,25 @@ require "optparse"
|
|
|
4
4
|
|
|
5
5
|
module Rpremote
|
|
6
6
|
class DfuCommand
|
|
7
|
-
DEFAULT_TIMEOUT =
|
|
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 = {
|
|
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],
|
|
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 =
|
|
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+/]
|