rpremote 0.2.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,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
@@ -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 = { 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
 
@@ -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 = parse_app_options(args, defaults)
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") { |value| options[:firmware] = value }
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 }
@@ -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
- return validate_mount!(File.expand_path(explicit_mount)) if explicit_mount
52
+ mounted = find_mounted(explicit_mount)
53
+ return mounted if mounted
51
54
 
52
- matches = Dir.glob(File.join(volumes_root, "*")).select do |path|
53
- Dir.exist?(path) && valid_target?(path)
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
- case matches.length
56
- when 0
57
- raise MountNotFoundError,
58
- "Pico 2 BOOTSEL drive not found; reconnect it while holding BOOTSEL or use --mount DIR"
59
- when 1
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 seconds, and picoruby. It exits nonzero when compatible R2P2 firmware reports a Ruby exception.
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 seconds, and picoruby. It exits nonzero when compatible R2P2 firmware reports a Ruby exception.
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
- else "Reads or transfers remote files."
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}