rpremote 0.2.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 +16 -0
- data/README.ja.md +17 -3
- data/README.md +17 -3
- data/RELEASING.md +2 -2
- data/THIRD_PARTY_NOTICES.md +4 -0
- data/lib/rpremote/bootsel_command.rb +124 -0
- data/lib/rpremote/cli.rb +52 -9
- data/lib/rpremote/config.rb +2 -0
- data/lib/rpremote/deploy_command.rb +191 -0
- data/lib/rpremote/dfu_command.rb +65 -3
- data/lib/rpremote/flash_command.rb +4 -1
- data/lib/rpremote/flasher.rb +49 -11
- data/lib/rpremote/help.rb +64 -11
- data/lib/rpremote/mrbgems.rb +41 -12
- data/lib/rpremote/nuke_firmware.rb +71 -0
- data/lib/rpremote/picoruby_source_patch.rb +23 -6
- data/lib/rpremote/recursive_copy.rb +84 -0
- data/lib/rpremote/runner.rb +33 -6
- data/lib/rpremote/setup_command.rb +4 -1
- data/lib/rpremote/shell.rb +9 -5
- data/lib/rpremote/terminal.rb +13 -1
- data/lib/rpremote/version.rb +1 -1
- data/lib/rpremote.rb +4 -0
- data/patches/picoruby-3.4.5-bootsel.patch +71 -0
- data/patches/picoruby-4.0.3-bootsel.patch +71 -0
- data/sig/rpremote.rbs +50 -2
- metadata +7 -1
data/lib/rpremote/dfu_command.rb
CHANGED
|
@@ -8,16 +8,21 @@ module Rpremote
|
|
|
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
|
|
|
@@ -52,7 +57,7 @@ module Rpremote
|
|
|
52
57
|
private_class_method :compile
|
|
53
58
|
|
|
54
59
|
def self.status(args, defaults, output, services)
|
|
55
|
-
options =
|
|
60
|
+
options = parse_connection_options(args, defaults)
|
|
56
61
|
raise ArgumentError, "usage: rpremote dfu status [options]" unless args.empty?
|
|
57
62
|
|
|
58
63
|
port_path = services[:device].main_port(options[:port])
|
|
@@ -71,6 +76,45 @@ module Rpremote
|
|
|
71
76
|
end
|
|
72
77
|
private_class_method :status
|
|
73
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
|
+
|
|
74
118
|
def self.parse_app_options(args, defaults)
|
|
75
119
|
options = {
|
|
76
120
|
port: defaults[:port],
|
|
@@ -91,6 +135,24 @@ module Rpremote
|
|
|
91
135
|
end
|
|
92
136
|
private_class_method :parse_app_options
|
|
93
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
|
+
|
|
94
156
|
def self.parse_compile_options(args, defaults)
|
|
95
157
|
options = {
|
|
96
158
|
language: defaults.fetch(:language, Target::DEFAULT_LANGUAGE),
|
|
@@ -33,7 +33,10 @@ module Rpremote
|
|
|
33
33
|
}
|
|
34
34
|
OptionParser.new do |parser|
|
|
35
35
|
parser.on("--cache DIR") { |value| options[:cache_dir] = value }
|
|
36
|
-
parser.on("--firmware FILE")
|
|
36
|
+
parser.on("--firmware FILE") do |value|
|
|
37
|
+
options[:firmware] = value
|
|
38
|
+
options[:firmware_explicit] = true
|
|
39
|
+
end
|
|
37
40
|
parser.on("--language LANGUAGE") { |value| options[:language] = value }
|
|
38
41
|
parser.on("--language-version VERSION") { |value| options[:language_version] = value }
|
|
39
42
|
parser.on("--board BOARD") { |value| options[:board] = value }
|
data/lib/rpremote/flasher.rb
CHANGED
|
@@ -36,30 +36,57 @@ module Rpremote
|
|
|
36
36
|
raise ArgumentError, "timeout must be positive" unless @timeout.positive?
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
def flash(uf2_path, mount: nil, port: nil)
|
|
39
|
+
def flash(uf2_path, mount: nil, port: nil, wait_for_port: true)
|
|
40
40
|
validate_firmware!(uf2_path)
|
|
41
41
|
target = find_mount(mount)
|
|
42
42
|
destination = File.join(target, File.basename(uf2_path))
|
|
43
43
|
copy_firmware(uf2_path, destination)
|
|
44
44
|
wait_until("BOOTSEL drive to disappear") { !mount_probe.call(target) }
|
|
45
|
+
return Result.new(mount: target, destination: destination, port: nil) unless wait_for_port
|
|
46
|
+
|
|
45
47
|
detected_port = wait_until("R2P2 serial port to appear") { detect_port(port) }
|
|
46
48
|
Result.new(mount: target, destination: destination, port: detected_port)
|
|
47
49
|
end
|
|
48
50
|
|
|
49
51
|
def find_mount(explicit_mount = nil)
|
|
50
|
-
|
|
52
|
+
mounted = find_mounted(explicit_mount)
|
|
53
|
+
return mounted if mounted
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
raise MountNotFoundError,
|
|
56
|
+
"Pico 2 BOOTSEL drive not found; reconnect it while holding BOOTSEL or use --mount DIR"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def find_mounted(explicit_mount = nil)
|
|
60
|
+
if explicit_mount
|
|
61
|
+
path = File.expand_path(explicit_mount)
|
|
62
|
+
return unless Dir.exist?(path)
|
|
63
|
+
|
|
64
|
+
return validate_mount!(path)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
matches = bootsel_mounts
|
|
68
|
+
return if matches.empty?
|
|
69
|
+
return matches.first if matches.one?
|
|
70
|
+
|
|
71
|
+
raise MountNotFoundError, "multiple Pico 2 BOOTSEL drives found; use --mount DIR"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def wait_for_mount(mount: nil)
|
|
75
|
+
if mount
|
|
76
|
+
path = File.expand_path(mount)
|
|
77
|
+
return wait_until("Pico 2 BOOTSEL drive to appear") do
|
|
78
|
+
next unless Dir.exist?(path)
|
|
79
|
+
next unless valid_target?(path)
|
|
80
|
+
|
|
81
|
+
path
|
|
82
|
+
end
|
|
54
83
|
end
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
84
|
+
|
|
85
|
+
wait_until("Pico 2 BOOTSEL drive to appear") do
|
|
86
|
+
matches = bootsel_mounts
|
|
87
|
+
raise MountNotFoundError, "multiple Pico 2 BOOTSEL drives found; use --mount DIR" if matches.length > 1
|
|
88
|
+
|
|
60
89
|
matches.first
|
|
61
|
-
else
|
|
62
|
-
raise MountNotFoundError, "multiple Pico 2 BOOTSEL drives found; use --mount DIR"
|
|
63
90
|
end
|
|
64
91
|
end
|
|
65
92
|
|
|
@@ -90,8 +117,19 @@ module Rpremote
|
|
|
90
117
|
false
|
|
91
118
|
end
|
|
92
119
|
|
|
120
|
+
def bootsel_mounts
|
|
121
|
+
Dir.glob(File.join(volumes_root, "*")).select do |path|
|
|
122
|
+
Dir.exist?(path) && valid_target?(path)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
93
126
|
def copy_firmware(source, destination)
|
|
94
127
|
FileUtils.copy_file(source, destination)
|
|
128
|
+
rescue Errno::ENXIO
|
|
129
|
+
# RP2350 may reboot and detach its BOOTSEL volume before macOS finishes
|
|
130
|
+
# closing the destination. The disappearance and serial reconnect checks
|
|
131
|
+
# in #flash determine whether the transfer actually completed.
|
|
132
|
+
nil
|
|
95
133
|
rescue SystemCallError => e
|
|
96
134
|
raise Error, "failed to copy UF2 firmware: #{e.message}"
|
|
97
135
|
end
|
data/lib/rpremote/help.rb
CHANGED
|
@@ -9,9 +9,14 @@ module Rpremote
|
|
|
9
9
|
build: "rpremote build [--language LANGUAGE] [--language-version VERSION] [--board BOARD] " \
|
|
10
10
|
"[--firmware FILE] [--cache DIR] [--mrbgems FILE|--no-mrbgems]",
|
|
11
11
|
build_clean: "rpremote build clean",
|
|
12
|
+
bootsel: "rpremote bootsel [--reset-flash-memory] [--mount DIR] [--port PORT] [--baud RATE] [--timeout SEC]",
|
|
13
|
+
deploy: "rpremote deploy PATH [--language LANGUAGE] [--language-version VERSION] [--board BOARD] " \
|
|
14
|
+
"[--firmware FILE] [--cache DIR] [--mrbgems FILE|--no-mrbgems] [--mount DIR] " \
|
|
15
|
+
"[--port PORT] [--baud RATE] [--timeout SEC]",
|
|
12
16
|
dfu_app: "rpremote dfu app FILE [--type ruby|rite] [--port PORT] [--baud RATE] [--timeout SEC]",
|
|
13
17
|
dfu_compile: "rpremote dfu compile FILE [--output FILE] [--language LANGUAGE] [--language-version VERSION] [--cache DIR]",
|
|
14
18
|
dfu_status: "rpremote dfu status [--port PORT] [--baud RATE] [--timeout SEC]",
|
|
19
|
+
dfu_remove: "rpremote dfu remove [--port PORT] [--baud RATE] [--timeout SEC]",
|
|
15
20
|
mrbgems: "rpremote mrbgems SUBCOMMAND [--file FILE] [--lockfile FILE]",
|
|
16
21
|
flash: "rpremote flash [--firmware FILE] [--language LANGUAGE] [--language-version VERSION] " \
|
|
17
22
|
"[--board BOARD] [--cache DIR] [--mount DIR] [--port PORT] [--timeout SEC]",
|
|
@@ -19,12 +24,13 @@ module Rpremote
|
|
|
19
24
|
"[--cache DIR] [--firmware FILE] [--mrbgems FILE|--no-mrbgems] [--mount DIR] " \
|
|
20
25
|
"[--port PORT] [--baud RATE] [--timeout SEC]",
|
|
21
26
|
ports: "rpremote ports",
|
|
22
|
-
run: "rpremote run FILE [--port PORT] [--baud RATE] [--timeout SEC] [--language LANGUAGE]",
|
|
27
|
+
run: "rpremote run FILE [--port PORT] [--baud RATE] [--timeout SEC] [--reset-on-timeout] [--language LANGUAGE]",
|
|
23
28
|
monitor: "rpremote monitor [--port PORT] [--baud RATE] [--timeout SEC]",
|
|
24
29
|
repl: "rpremote repl [--port PORT] [--baud RATE] [--timeout SEC]",
|
|
25
30
|
exec: "rpremote exec CODE [--port PORT] [--baud RATE] [--timeout SEC] [--language LANGUAGE]",
|
|
26
31
|
reset: "rpremote reset [--port PORT] [--baud RATE] [--timeout SEC]",
|
|
27
|
-
fs_cp: "rpremote fs cp SOURCE DESTINATION [--port PORT] [--baud RATE] [--timeout SEC]",
|
|
32
|
+
fs_cp: "rpremote fs cp SOURCE DESTINATION [--recursive] [--port PORT] [--baud RATE] [--timeout SEC]",
|
|
33
|
+
fs_push: "rpremote fs push LOCAL_DIR :/REMOTE_DIR [--port PORT] [--baud RATE] [--timeout SEC]",
|
|
28
34
|
fs_cat: "rpremote fs cat :/REMOTE/PATH [--port PORT] [--baud RATE] [--timeout SEC]",
|
|
29
35
|
fs_ls: "rpremote fs ls :/REMOTE/PATH [--port PORT] [--baud RATE] [--timeout SEC]",
|
|
30
36
|
fs_rm: "rpremote fs rm :/REMOTE/PATH [--port PORT] [--baud RATE] [--timeout SEC]",
|
|
@@ -38,9 +44,12 @@ module Rpremote
|
|
|
38
44
|
#{COMMAND_USAGE.fetch(:setup)}
|
|
39
45
|
#{COMMAND_USAGE.fetch(:build)}
|
|
40
46
|
#{COMMAND_USAGE.fetch(:build_clean)}
|
|
47
|
+
#{COMMAND_USAGE.fetch(:bootsel)}
|
|
48
|
+
#{COMMAND_USAGE.fetch(:deploy)}
|
|
41
49
|
#{COMMAND_USAGE.fetch(:dfu_app)}
|
|
42
50
|
#{COMMAND_USAGE.fetch(:dfu_compile)}
|
|
43
51
|
#{COMMAND_USAGE.fetch(:dfu_status)}
|
|
52
|
+
#{COMMAND_USAGE.fetch(:dfu_remove)}
|
|
44
53
|
#{COMMAND_USAGE.fetch(:mrbgems).sub("SUBCOMMAND", "check|list|lock|update")}
|
|
45
54
|
#{COMMAND_USAGE.fetch(:flash)}
|
|
46
55
|
#{COMMAND_USAGE.fetch(:config_show)}
|
|
@@ -51,6 +60,7 @@ module Rpremote
|
|
|
51
60
|
#{COMMAND_USAGE.fetch(:exec)}
|
|
52
61
|
#{COMMAND_USAGE.fetch(:reset)}
|
|
53
62
|
#{COMMAND_USAGE.fetch(:fs_cp)}
|
|
63
|
+
#{COMMAND_USAGE.fetch(:fs_push)}
|
|
54
64
|
#{COMMAND_USAGE.fetch(:fs_cat)}
|
|
55
65
|
#{COMMAND_USAGE.fetch(:fs_ls)}
|
|
56
66
|
#{COMMAND_USAGE.fetch(:fs_rm)}
|
|
@@ -66,11 +76,11 @@ module Rpremote
|
|
|
66
76
|
--language-version VERSION
|
|
67
77
|
use R2P2/PicoRuby 4.0.3 or 3.4.2 (default: 4.0.3)
|
|
68
78
|
--cache DIR use another project cache directory
|
|
69
|
-
--mrbgems FILE use an explicit Mrbgems definition during build
|
|
70
|
-
--no-mrbgems build without the automatically detected Mrbgems
|
|
79
|
+
--mrbgems FILE use an explicit Mrbgems definition during build or deploy
|
|
80
|
+
--no-mrbgems build or deploy without the automatically detected Mrbgems
|
|
71
81
|
--board BOARD
|
|
72
82
|
select pico2 or pico2_w (default: pico2)
|
|
73
|
-
--firmware FILE build to, or flash from, this UF2 path
|
|
83
|
+
--firmware FILE build to, or deploy or flash from, this UF2 path
|
|
74
84
|
--language LANGUAGE
|
|
75
85
|
select the remote language (default: picoruby)
|
|
76
86
|
--config FILE use another configuration file
|
|
@@ -93,6 +103,8 @@ module Rpremote
|
|
|
93
103
|
case command
|
|
94
104
|
when "setup" then setup_text
|
|
95
105
|
when "build" then args.first == "clean" ? build_clean_text : build_text
|
|
106
|
+
when "bootsel" then bootsel_text
|
|
107
|
+
when "deploy" then deploy_text
|
|
96
108
|
when "dfu" then dfu_text(args.first)
|
|
97
109
|
when "mrbgems" then mrbgems_text(args.first)
|
|
98
110
|
when "flash" then flash_text
|
|
@@ -112,7 +124,7 @@ module Rpremote
|
|
|
112
124
|
<<~HELP
|
|
113
125
|
Usage: #{COMMAND_USAGE.fetch(:setup)}
|
|
114
126
|
|
|
115
|
-
Creates config/setting.json when it does not exist, then downloads and prepares PicoRuby source.
|
|
127
|
+
Creates config/setting.json when it does not exist, then downloads and prepares PicoRuby source and the official Raspberry Pi nuke_universal.uf2 firmware.
|
|
116
128
|
Options: --language LANGUAGE (picoruby), --language-version VERSION (4.0.3), --cache DIR (firmware), --force.
|
|
117
129
|
This changes the project configuration and source cache but does not connect to a board.
|
|
118
130
|
HELP
|
|
@@ -136,15 +148,40 @@ module Rpremote
|
|
|
136
148
|
HELP
|
|
137
149
|
end
|
|
138
150
|
|
|
151
|
+
def self.deploy_text
|
|
152
|
+
<<~HELP
|
|
153
|
+
Usage: #{COMMAND_USAGE.fetch(:deploy)}
|
|
154
|
+
|
|
155
|
+
Builds the selected custom UF2, enters BOOTSEL when needed, flashes the firmware, and waits for the R2P2 Shell to become ready.
|
|
156
|
+
If PATH/lib/NAME exists, it copies it to :/lib/NAME, where NAME is the final component of PATH. It then runs PATH/main.rb and preserves its Shell job, so hardware output remains active until the next command.
|
|
157
|
+
The stages run in order and stop at the first failure. Flashing replaces persistent board firmware.
|
|
158
|
+
deploy requires PicoRuby 4.x firmware (currently 4.0.3).
|
|
159
|
+
Options combine build, BOOTSEL, flash, library-copy, and run settings. The first install of firmware with automatic BOOTSEL still requires holding BOOTSEL.
|
|
160
|
+
HELP
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def self.bootsel_text
|
|
164
|
+
<<~HELP
|
|
165
|
+
Usage: #{COMMAND_USAGE.fetch(:bootsel)}
|
|
166
|
+
|
|
167
|
+
Asks the running R2P2 firmware to restart as an RP2350 BOOTSEL USB volume, then waits for that volume to appear.
|
|
168
|
+
By default it does not write firmware. This requires firmware built by a current rpremote; use physical BOOTSEL once to install it first.
|
|
169
|
+
--reset-flash-memory copies the official Raspberry Pi nuke_universal.uf2 in BOOTSEL mode and erases all external flash memory.
|
|
170
|
+
After resetting flash memory, wait for BOOTSEL to reappear and run `rpremote flash` to install R2P2 again.
|
|
171
|
+
HELP
|
|
172
|
+
end
|
|
173
|
+
|
|
139
174
|
def self.dfu_text(subcommand)
|
|
140
175
|
return dfu_app_text if subcommand == "app"
|
|
141
176
|
return dfu_compile_text if subcommand == "compile"
|
|
142
177
|
return dfu_status_text if subcommand == "status"
|
|
178
|
+
return dfu_remove_text if subcommand == "remove"
|
|
143
179
|
|
|
144
180
|
<<~HELP
|
|
145
181
|
Usage: #{COMMAND_USAGE.fetch(:dfu_app)}
|
|
146
182
|
#{COMMAND_USAGE.fetch(:dfu_compile)}
|
|
147
183
|
#{COMMAND_USAGE.fetch(:dfu_status)}
|
|
184
|
+
#{COMMAND_USAGE.fetch(:dfu_remove)}
|
|
148
185
|
|
|
149
186
|
Stages and inspects PicoModem DFU applications. Run `rpremote dfu SUBCOMMAND --help` for details.
|
|
150
187
|
HELP
|
|
@@ -178,6 +215,16 @@ module Rpremote
|
|
|
178
215
|
HELP
|
|
179
216
|
end
|
|
180
217
|
|
|
218
|
+
def self.dfu_remove_text
|
|
219
|
+
<<~HELP
|
|
220
|
+
Usage: #{COMMAND_USAGE.fetch(:dfu_remove)}
|
|
221
|
+
|
|
222
|
+
Permanently removes the Ruby source and bytecode applications from both DFU A/B slots and resets their metadata.
|
|
223
|
+
Run `rpremote reset` afterward to stop the application already running in RAM. It does not remove /home/app.rb,
|
|
224
|
+
/home/app.mrb, other files, embedded mrbgems, or R2P2 firmware.
|
|
225
|
+
HELP
|
|
226
|
+
end
|
|
227
|
+
|
|
181
228
|
def self.mrbgems_text(subcommand)
|
|
182
229
|
action = subcommand || "check|list|lock|update"
|
|
183
230
|
<<~HELP
|
|
@@ -217,8 +264,10 @@ module Rpremote
|
|
|
217
264
|
<<~HELP
|
|
218
265
|
Usage: #{COMMAND_USAGE.fetch(:run)}
|
|
219
266
|
|
|
220
|
-
Uploads FILE to R2P2, relays output, then removes the temporary remote file. Defaults are the automatic CDC 0 port,
|
|
221
|
-
115200 baud, 20
|
|
267
|
+
Uploads FILE to R2P2, or main.rb when FILE is a directory, relays output, then removes the temporary remote file. Defaults are the automatic CDC 0 port,
|
|
268
|
+
115200 baud, a 20-second idle timeout, and picoruby. Output from the running program resets the idle timeout.
|
|
269
|
+
It exits nonzero when compatible R2P2 firmware reports a Ruby exception.
|
|
270
|
+
--reset-on-timeout resets R2P2 after the run times out and the serial connection has closed.
|
|
222
271
|
HELP
|
|
223
272
|
end
|
|
224
273
|
|
|
@@ -227,7 +276,8 @@ module Rpremote
|
|
|
227
276
|
Usage: #{COMMAND_USAGE.fetch(:exec)}
|
|
228
277
|
|
|
229
278
|
Runs short Ruby CODE through a temporary remote file, then removes it. Defaults are the automatic CDC 0 port,
|
|
230
|
-
115200 baud, 20
|
|
279
|
+
115200 baud, a 20-second idle timeout, and picoruby. Output from the running program resets the idle timeout.
|
|
280
|
+
It exits nonzero when compatible R2P2 firmware reports a Ruby exception.
|
|
231
281
|
HELP
|
|
232
282
|
end
|
|
233
283
|
|
|
@@ -256,10 +306,11 @@ module Rpremote
|
|
|
256
306
|
end
|
|
257
307
|
|
|
258
308
|
def self.fs_text(subcommand)
|
|
259
|
-
return fs_subcommand_text(subcommand) if %w[cp cat ls rm mkdir].include?(subcommand)
|
|
309
|
+
return fs_subcommand_text(subcommand) if %w[cp push cat ls rm mkdir].include?(subcommand)
|
|
260
310
|
|
|
261
311
|
<<~HELP
|
|
262
312
|
Usage: #{COMMAND_USAGE.fetch(:fs_cp)}
|
|
313
|
+
#{COMMAND_USAGE.fetch(:fs_push)}
|
|
263
314
|
#{COMMAND_USAGE.fetch(:fs_cat)}
|
|
264
315
|
#{COMMAND_USAGE.fetch(:fs_ls)}
|
|
265
316
|
#{COMMAND_USAGE.fetch(:fs_rm)}
|
|
@@ -274,7 +325,9 @@ module Rpremote
|
|
|
274
325
|
effect = case subcommand
|
|
275
326
|
when "rm" then "Deletes the remote path permanently."
|
|
276
327
|
when "mkdir" then "Creates a remote directory."
|
|
277
|
-
|
|
328
|
+
when "cp" then "Transfers one file, or recursively uploads a local directory with --recursive."
|
|
329
|
+
when "push" then "Creates missing remote directories and recursively uploads a local directory."
|
|
330
|
+
else "Reads remote files."
|
|
278
331
|
end
|
|
279
332
|
<<~HELP
|
|
280
333
|
Usage: #{usage}
|
data/lib/rpremote/mrbgems.rb
CHANGED
|
@@ -14,7 +14,7 @@ module Rpremote
|
|
|
14
14
|
COMMIT_PATTERN = /\A[0-9a-f]{40,64}\z/i
|
|
15
15
|
VMS = %i[mruby mrubyc].freeze
|
|
16
16
|
|
|
17
|
-
Dependency = Data.define(:type, :source, :branch, :commit, :path)
|
|
17
|
+
Dependency = Data.define(:type, :source, :branch, :commit, :path, :require_name)
|
|
18
18
|
Overlay = Data.define(:path, :fingerprint)
|
|
19
19
|
|
|
20
20
|
class Error < Rpremote::Error; end
|
|
@@ -68,6 +68,20 @@ module Rpremote
|
|
|
68
68
|
raise LockError, "invalid mrbgems lock file #{lock_path}: #{e.message}"
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
def require_names
|
|
72
|
+
lock_data = read_lock(required: false)
|
|
73
|
+
return [] unless lock_data
|
|
74
|
+
|
|
75
|
+
lock_data.fetch("gems").filter_map { |gem| gem["require_name"] }.uniq
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def prepend_requires(source)
|
|
79
|
+
names = require_names
|
|
80
|
+
return source if names.empty?
|
|
81
|
+
|
|
82
|
+
names.map { |name| "require #{name.inspect}\n" }.join.b + source.b
|
|
83
|
+
end
|
|
84
|
+
|
|
71
85
|
def generate_overlay(base_config:, target:, directory:, update: false)
|
|
72
86
|
lock_data = lock(update: update)
|
|
73
87
|
fingerprint = build_fingerprint(base_config, lock_data)
|
|
@@ -109,10 +123,12 @@ module Rpremote
|
|
|
109
123
|
dependency.source, dependency.branch
|
|
110
124
|
)
|
|
111
125
|
{ "type" => "github", "source" => dependency.source,
|
|
112
|
-
"branch" => dependency.branch, "commit" => validate_commit!(commit)
|
|
126
|
+
"branch" => dependency.branch, "commit" => validate_commit!(commit),
|
|
127
|
+
"require_name" => dependency.require_name }.compact
|
|
113
128
|
else
|
|
114
129
|
{ "type" => "path", "source" => dependency.source,
|
|
115
|
-
"sha256" => digest_directory(dependency.path)
|
|
130
|
+
"sha256" => digest_directory(dependency.path),
|
|
131
|
+
"require_name" => dependency.require_name || local_require_name(dependency.path) }.compact
|
|
116
132
|
end
|
|
117
133
|
end
|
|
118
134
|
|
|
@@ -184,12 +200,22 @@ module Rpremote
|
|
|
184
200
|
|
|
185
201
|
def valid_github_lock?(entry)
|
|
186
202
|
GITHUB_PATTERN.match?(entry["source"].to_s) && !entry["branch"].to_s.empty? &&
|
|
187
|
-
COMMIT_PATTERN.match?(entry["commit"].to_s)
|
|
203
|
+
COMMIT_PATTERN.match?(entry["commit"].to_s) && valid_require_name?(entry["require_name"])
|
|
188
204
|
end
|
|
189
205
|
|
|
190
206
|
def valid_path_lock?(entry)
|
|
191
207
|
entry["type"] == "path" && !entry["source"].to_s.empty? &&
|
|
192
|
-
/\A[0-9a-f]{64}\z/.match?(entry["sha256"].to_s)
|
|
208
|
+
/\A[0-9a-f]{64}\z/.match?(entry["sha256"].to_s) && valid_require_name?(entry["require_name"])
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def valid_require_name?(name)
|
|
212
|
+
name.nil? || (name.is_a?(String) && !name.empty? && !name.match?(/[\x00-\x1f\x7f]/))
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def local_require_name(directory)
|
|
216
|
+
source = File.read(File.join(directory, "mrbgem.rake"))
|
|
217
|
+
match = source.match(/^\s*spec\.require_name\s*=\s*["']([^"']+)["']\s*$/)
|
|
218
|
+
match && match[1]
|
|
193
219
|
end
|
|
194
220
|
|
|
195
221
|
def build_fingerprint(base_config, lock_data)
|
|
@@ -246,14 +272,17 @@ module Rpremote
|
|
|
246
272
|
@vm_name = value
|
|
247
273
|
end
|
|
248
274
|
|
|
249
|
-
def gem(github: nil, path: nil, branch: "main", commit: nil)
|
|
275
|
+
def gem(github: nil, path: nil, branch: "main", commit: nil, require: nil)
|
|
250
276
|
sources = [github, path].compact
|
|
251
277
|
raise DefinitionError, "gem requires exactly one of github or path" unless sources.length == 1
|
|
278
|
+
unless require.nil? || (require.is_a?(String) && !require.empty? && !require.match?(/[\x00-\x1f\x7f]/))
|
|
279
|
+
raise DefinitionError, "invalid mrbgem require name: #{require.inspect}"
|
|
280
|
+
end
|
|
252
281
|
|
|
253
282
|
dependency = if github
|
|
254
|
-
github_dependency(github, branch, commit)
|
|
283
|
+
github_dependency(github, branch, commit, require)
|
|
255
284
|
else
|
|
256
|
-
path_dependency(path, branch, commit)
|
|
285
|
+
path_dependency(path, branch, commit, require)
|
|
257
286
|
end
|
|
258
287
|
key = [dependency.type, dependency.source]
|
|
259
288
|
raise DefinitionError, "duplicate mrbgem: #{dependency.source}" if dependencies.any? do |item|
|
|
@@ -267,21 +296,21 @@ module Rpremote
|
|
|
267
296
|
|
|
268
297
|
attr_reader :directory
|
|
269
298
|
|
|
270
|
-
def github_dependency(source, branch, commit)
|
|
299
|
+
def github_dependency(source, branch, commit, require_name)
|
|
271
300
|
raise DefinitionError, "invalid GitHub mrbgem: #{source.inspect}" unless GITHUB_PATTERN.match?(source.to_s)
|
|
272
301
|
raise DefinitionError, "GitHub mrbgem branch must not be empty" if branch.to_s.empty?
|
|
273
302
|
raise DefinitionError, "invalid Git commit: #{commit.inspect}" if commit && !COMMIT_PATTERN.match?(commit.to_s)
|
|
274
303
|
|
|
275
304
|
Dependency.new(type: :github, source: source, branch: branch,
|
|
276
|
-
commit: commit&.downcase, path: nil)
|
|
305
|
+
commit: commit&.downcase, path: nil, require_name: require_name)
|
|
277
306
|
end
|
|
278
307
|
|
|
279
|
-
def path_dependency(source, branch, commit)
|
|
308
|
+
def path_dependency(source, branch, commit, require_name)
|
|
280
309
|
raise DefinitionError, "local mrbgem path must not be empty" if source.to_s.empty?
|
|
281
310
|
raise DefinitionError, "local mrbgem does not accept branch or commit" if branch != "main" || commit
|
|
282
311
|
|
|
283
312
|
Dependency.new(type: :path, source: source, branch: nil, commit: nil,
|
|
284
|
-
path: File.expand_path(source, directory))
|
|
313
|
+
path: File.expand_path(source, directory), require_name: require_name)
|
|
285
314
|
end
|
|
286
315
|
end
|
|
287
316
|
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "timeout"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
module Rpremote
|
|
9
|
+
class NukeFirmware
|
|
10
|
+
URL = "https://github.com/raspberrypi/pico-sdk-prebuilts/releases/latest/download/nuke_universal.uf2"
|
|
11
|
+
FILENAME = "nuke_universal.uf2"
|
|
12
|
+
MAX_REDIRECTS = 5
|
|
13
|
+
|
|
14
|
+
class DownloadError < Rpremote::Error; end
|
|
15
|
+
|
|
16
|
+
def initialize(directory: Target::DEFAULT_CACHE_DIR, fetcher: nil)
|
|
17
|
+
@directory = File.expand_path(directory)
|
|
18
|
+
@fetcher = fetcher || method(:download)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def setup(force: false)
|
|
22
|
+
FileUtils.mkdir_p(directory)
|
|
23
|
+
return path if File.size?(path) && !force
|
|
24
|
+
|
|
25
|
+
temporary = "#{path}.part"
|
|
26
|
+
File.binwrite(temporary, fetcher.call(URL))
|
|
27
|
+
raise DownloadError, "downloaded nuke_universal.uf2 is empty" unless File.size?(temporary)
|
|
28
|
+
|
|
29
|
+
File.rename(temporary, path)
|
|
30
|
+
path
|
|
31
|
+
rescue SystemCallError => e
|
|
32
|
+
raise DownloadError, "cannot store nuke_universal.uf2: #{e.message}"
|
|
33
|
+
ensure
|
|
34
|
+
FileUtils.rm_f(temporary) if temporary
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
attr_reader :directory, :fetcher
|
|
40
|
+
|
|
41
|
+
def path
|
|
42
|
+
File.join(directory, FILENAME)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def download(url, redirects = MAX_REDIRECTS)
|
|
46
|
+
raise DownloadError, "too many redirects while downloading nuke_universal.uf2" if redirects.negative?
|
|
47
|
+
|
|
48
|
+
uri = URI(url)
|
|
49
|
+
raise DownloadError, "nuke_universal.uf2 download requires HTTPS" unless uri.is_a?(URI::HTTPS)
|
|
50
|
+
|
|
51
|
+
request = Net::HTTP::Get.new(uri)
|
|
52
|
+
request["User-Agent"] = "rpremote/#{Rpremote::VERSION}"
|
|
53
|
+
response = Net::HTTP.start(
|
|
54
|
+
uri.host, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 60
|
|
55
|
+
) { |http| http.request(request) }
|
|
56
|
+
|
|
57
|
+
case response
|
|
58
|
+
when Net::HTTPSuccess then response.body
|
|
59
|
+
when Net::HTTPRedirection
|
|
60
|
+
location = response["location"]
|
|
61
|
+
raise DownloadError, "nuke_universal.uf2 download redirect has no location" unless location
|
|
62
|
+
|
|
63
|
+
download(URI.join(url, location).to_s, redirects - 1)
|
|
64
|
+
else
|
|
65
|
+
raise DownloadError, "nuke_universal.uf2 download failed: HTTP #{response.code} #{response.message}"
|
|
66
|
+
end
|
|
67
|
+
rescue SocketError, SystemCallError, Timeout::Error => e
|
|
68
|
+
raise DownloadError, "nuke_universal.uf2 download failed: #{e.message}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module Rpremote
|
|
4
4
|
class PicoRubySourcePatch
|
|
5
5
|
JOB_PATH = "mrbgems/picoruby-shell/mrblib/job.rb"
|
|
6
|
+
BOOTSEL_PATH = "mrbgems/picoruby-machine/include/machine.h"
|
|
6
7
|
PATCH_ALIASES = { "3.4.2" => "3.4.5" }.freeze
|
|
7
8
|
|
|
8
9
|
class Error < Rpremote::Error; end
|
|
@@ -14,19 +15,35 @@ module Rpremote
|
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def apply(source)
|
|
18
|
+
return unless Dir.exist?(source)
|
|
19
|
+
|
|
17
20
|
patch_version = PATCH_ALIASES.fetch(version, version)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
patches(patch_version).each do |patch, prerequisite|
|
|
22
|
+
next unless File.file?(patch) && File.file?(File.join(source, prerequisite))
|
|
23
|
+
next if prerequisite == BOOTSEL_PATH && File.read(File.join(source, prerequisite)).include?("Machine_bootsel")
|
|
24
|
+
|
|
25
|
+
next if git_apply?(source, patch, "--reverse", "--check")
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
raise Error, "cannot apply rpremote patch to PicoRuby #{version}" unless git_apply?(source, patch, "--check")
|
|
28
|
+
raise Error, "cannot apply rpremote patch to PicoRuby #{version}" unless git_apply?(source, patch)
|
|
29
|
+
end
|
|
30
|
+
nil
|
|
24
31
|
end
|
|
25
32
|
|
|
26
33
|
private
|
|
27
34
|
|
|
35
|
+
def patches(patch_version)
|
|
36
|
+
definitions = {
|
|
37
|
+
"ruby-exception-status" => JOB_PATH,
|
|
38
|
+
"bootsel" => BOOTSEL_PATH
|
|
39
|
+
}
|
|
40
|
+
definitions.map do |name, prerequisite|
|
|
41
|
+
[File.expand_path("../../patches/picoruby-#{patch_version}-#{name}.patch", __dir__), prerequisite]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
28
45
|
def git_apply?(source, patch, *options)
|
|
29
|
-
system("git", "apply", *options, patch, chdir: source, out: File::NULL, err: File::NULL)
|
|
46
|
+
system("git", "apply", "--unidiff-zero", *options, patch, chdir: source, out: File::NULL, err: File::NULL)
|
|
30
47
|
end
|
|
31
48
|
end
|
|
32
49
|
end
|