rpremote 0.5.0 → 0.7.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.ja.md +30 -46
  4. data/README.md +30 -47
  5. data/RELEASING.md +2 -2
  6. data/THIRD_PARTY_NOTICES.md +1 -1
  7. data/lib/rpremote/bootsel_command.rb +8 -6
  8. data/lib/rpremote/build_command.rb +3 -3
  9. data/lib/rpremote/builder.rb +15 -9
  10. data/lib/rpremote/cli/filesystem_commands.rb +105 -0
  11. data/lib/rpremote/cli.rb +20 -105
  12. data/lib/rpremote/config.rb +6 -5
  13. data/lib/rpremote/config_show.rb +13 -5
  14. data/lib/rpremote/deploy_command.rb +85 -55
  15. data/lib/rpremote/help/dfu_commands.rb +59 -0
  16. data/lib/rpremote/help/filesystem_commands.rb +36 -0
  17. data/lib/rpremote/help/project_commands.rb +89 -0
  18. data/lib/rpremote/help/remote_commands.rb +54 -0
  19. data/lib/rpremote/help/root_text.rb +63 -0
  20. data/lib/rpremote/help.rb +27 -300
  21. data/lib/rpremote/language_source.rb +19 -2
  22. data/lib/rpremote/mrbgems/definition.rb +116 -0
  23. data/lib/rpremote/mrbgems/file_writer.rb +26 -0
  24. data/lib/rpremote/mrbgems/lockfile.rb +131 -0
  25. data/lib/rpremote/mrbgems.rb +95 -155
  26. data/lib/rpremote/mrbgems_command.rb +19 -4
  27. data/lib/rpremote/picoruby_compiler.rb +28 -0
  28. data/lib/rpremote/picoruby_source_patch.rb +0 -4
  29. data/lib/rpremote/target.rb +1 -1
  30. data/lib/rpremote/terminal.rb +0 -2
  31. data/lib/rpremote/version.rb +1 -1
  32. data/lib/rpremote.rb +1 -0
  33. data/patches/picoruby-4.0.4-pwm-sleep-clock.patch +27 -0
  34. data/sig/rpremote.rbs +20 -4
  35. data/tasks/firmware_build.rb +10 -2
  36. metadata +12 -3
  37. data/patches/picoruby-3.4.5-bootsel.patch +0 -71
  38. data/patches/picoruby-4.0.3-bootsel.patch +0 -71
data/lib/rpremote/cli.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "optparse"
4
4
  require_relative "build_command"
5
5
  require_relative "bootsel_command"
6
+ require_relative "cli/filesystem_commands"
6
7
  require_relative "config_show"
7
8
  require_relative "dfu_command"
8
9
  require_relative "deploy_command"
@@ -12,9 +13,10 @@ require_relative "mrbgems_command"
12
13
  require_relative "recursive_copy"
13
14
  require_relative "setup_command"
14
15
 
15
- # rubocop:disable Metrics/ClassLength
16
16
  module Rpremote
17
17
  class CLI
18
+ include FilesystemCommands
19
+
18
20
  def self.start(argv = ARGV, config: Config, **options)
19
21
  new(**options).run(argv, config: config)
20
22
  rescue OptionParser::ParseError, ArgumentError, SystemCallError, Rpremote::Error => e
@@ -136,44 +138,24 @@ module Rpremote
136
138
  ConfigShow.run(args, defaults: config_options, output: stdout)
137
139
  end
138
140
 
139
- def fs(args)
140
- subcommand = args.shift
141
- recursive = subcommand == "cp" && (args.delete("--recursive") || args.delete("-r"))
142
- options = parse_connection_options(args)
143
- case subcommand
144
- when "cp"
145
- copy(args, options, recursive: recursive)
146
- when "push"
147
- copy(args, options, recursive: true, command: "push")
148
- when "cat"
149
- cat(args, options)
150
- when "ls", "rm", "mkdir"
151
- filesystem_shell_command(subcommand, args, options)
152
- else
153
- raise ArgumentError, "unknown fs command: #{subcommand || "(none)"}"
154
- end
155
- rescue Shell::TimeoutError => e
156
- raise Shell::TimeoutError, "filesystem connection failed: #{e.message}"
157
- end
158
-
159
141
  def run_file(args)
160
142
  reset_on_timeout = !args.delete("--reset-on-timeout").nil?
161
- options = parse_connection_options(args)
143
+ options = parse_connection_options(args, mrbgems_lock: true)
162
144
  raise ArgumentError, "usage: rpremote run FILE [options]" unless args.length == 1
163
145
 
164
146
  source = args.first
165
147
  source = File.join(source, "main.rb") if File.directory?(source)
166
- data = prepend_mrbgem_requires(File.binread(source))
148
+ data = prepend_mrbgem_requires(File.binread(source), options[:mrbgems_lock])
167
149
  execute_temporary(data, options, output: stdout, reset_on_timeout: reset_on_timeout)
168
150
  rescue Errno::ENOENT => e
169
151
  raise ArgumentError, e.message
170
152
  end
171
153
 
172
154
  def execute_code(args)
173
- options = parse_connection_options(args)
155
+ options = parse_connection_options(args, mrbgems_lock: true)
174
156
  raise ArgumentError, "usage: rpremote exec CODE [options]" unless args.length == 1
175
157
 
176
- execute_temporary(prepend_mrbgem_requires(args.first), options, output: stdout)
158
+ execute_temporary(prepend_mrbgem_requires(args.first, options[:mrbgems_lock]), options, output: stdout)
177
159
  end
178
160
 
179
161
  def execute_temporary(data, options, output: nil, reset_on_timeout: false)
@@ -197,8 +179,13 @@ module Rpremote
197
179
  "#{timeout_error.message}; automatic reset failed: #{e.message}"
198
180
  end
199
181
 
200
- def prepend_mrbgem_requires(data)
201
- Mrbgems.new(cwd: Dir.pwd).prepend_requires(data)
182
+ def prepend_mrbgem_requires(data, lock_file)
183
+ return data if lock_file == false
184
+
185
+ path = lock_file || Mrbgems::DEFAULT_LOCK_PATH
186
+ raise ArgumentError, "mrbgems lock file does not exist: #{path}" if lock_file && !File.file?(path)
187
+
188
+ Mrbgems.new(cwd: Dir.pwd, lock_path: path).prepend_requires(data)
202
189
  end
203
190
 
204
191
  def interactive(args, repl: false)
@@ -227,18 +214,23 @@ module Rpremote
227
214
  stdout.puts("reset R2P2: #{port_path}")
228
215
  end
229
216
 
230
- def parse_connection_options(args, default_timeout: PicoModem::DEFAULT_TIMEOUT)
217
+ def parse_connection_options(args, default_timeout: PicoModem::DEFAULT_TIMEOUT, mrbgems_lock: false)
231
218
  options = {
232
219
  port: config_options[:port],
233
220
  baud: config_options.fetch(:baud, Serial::BAUD_RATE),
234
221
  timeout: config_options.fetch(:timeout, default_timeout),
235
222
  language: config_options.fetch(:language, Target::DEFAULT_LANGUAGE)
236
223
  }
224
+ options[:mrbgems_lock] = config_options[:mrbgems_lock] if mrbgems_lock
237
225
  parser = OptionParser.new do |opts|
238
226
  opts.on("--port PORT") { |value| options[:port] = value }
239
227
  opts.on("--baud RATE", Integer) { |value| options[:baud] = value }
240
228
  opts.on("--timeout SECONDS", Float) { |value| options[:timeout] = value }
241
229
  opts.on("--language LANGUAGE") { |value| options[:language] = value }
230
+ if mrbgems_lock
231
+ opts.on("--lockfile FILE") { |value| options[:mrbgems_lock] = value }
232
+ opts.on("--no-mrbgems") { options[:mrbgems_lock] = false }
233
+ end
242
234
  end
243
235
  parser.parse!(args)
244
236
  raise ArgumentError, "--baud must be positive" unless options[:baud].positive?
@@ -246,82 +238,5 @@ module Rpremote
246
238
 
247
239
  options
248
240
  end
249
-
250
- def copy(args, options, recursive: false, command: "cp")
251
- usage = command == "push" ? "rpremote fs push LOCAL_DIR :/REMOTE_DIR" : "rpremote fs cp SOURCE DESTINATION"
252
- raise ArgumentError, "usage: #{usage} [options]" unless args.length == 2
253
-
254
- source, destination = args
255
- source_remote = RemotePath.remote?(source)
256
- destination_remote = RemotePath.remote?(destination)
257
- raise ArgumentError, "exactly one cp path must be remote (prefix remote paths with :)" if source_remote == destination_remote
258
- return RecursiveCopy.new(output: stdout, serial: serial, device: device).call(source, destination, options) if recursive
259
-
260
- ensure_filesystem_connection!(options)
261
- if source_remote
262
- data = with_modem(options) { |modem| modem.download(RemotePath.unwrap(source)) }
263
- File.binwrite(local_destination(destination, source), data)
264
- stdout.puts("downloaded #{data.bytesize} bytes: #{source} -> #{destination}")
265
- else
266
- data = File.binread(source)
267
- with_modem(options) { |modem| modem.upload(RemotePath.unwrap(destination), data) }
268
- stdout.puts("uploaded #{data.bytesize} bytes: #{source} -> #{destination}")
269
- end
270
- rescue Errno::ENOENT => e
271
- raise ArgumentError, e.message
272
- end
273
-
274
- def cat(args, options)
275
- raise ArgumentError, "usage: rpremote fs cat :/REMOTE/PATH [options]" unless args.length == 1
276
- raise ArgumentError, "cat path must be remote (prefix it with :)" unless RemotePath.remote?(args.first)
277
-
278
- stdout.write(with_modem(options) { |modem| modem.download(RemotePath.unwrap(args.first)) })
279
- end
280
-
281
- def filesystem_shell_command(command, args, options)
282
- usage = "usage: rpremote fs #{command} :/REMOTE/PATH [options]"
283
- raise ArgumentError, usage unless args.length == 1
284
-
285
- path = RemotePath.validate(args.first)
286
- stdout.puts("deleting remote path permanently: #{path}") if command == "rm"
287
- output = with_shell(options) do |shell|
288
- shell.execute("#{command} #{Shell.quote_argument(path)}")
289
- end
290
- ensure_filesystem_success!(command, output)
291
- stdout.write(output)
292
- end
293
-
294
- def ensure_filesystem_success!(command, output)
295
- failed = command == "ls" ? output.start_with?("ls:") : !output.empty?
296
- raise Shell::CommandError, output.strip if failed
297
- end
298
-
299
- def ensure_filesystem_connection!(options)
300
- output = with_shell(options) { |shell| shell.execute("ls '/'") }
301
- ensure_filesystem_success!("ls", output)
302
- end
303
-
304
- def with_modem(options)
305
- port_path = device.main_port(options[:port])
306
- serial.open(port_path, baud: options[:baud]) do |port|
307
- yield PicoModem.new(port, timeout: options[:timeout])
308
- end
309
- end
310
-
311
- def with_shell(options)
312
- port_path = device.main_port(options[:port])
313
- serial.open(port_path, baud: options[:baud]) do |port|
314
- shell = Shell.new(port, timeout: options[:timeout])
315
- shell.synchronize!
316
- yield shell
317
- end
318
- end
319
-
320
- def local_destination(destination, source)
321
- return destination unless File.directory?(destination)
322
-
323
- File.join(destination, File.basename(RemotePath.unwrap(source)))
324
- end
325
241
  end
326
242
  end
327
- # rubocop:enable Metrics/ClassLength
@@ -16,17 +16,17 @@ module Rpremote
16
16
  baud: Integer,
17
17
  timeout: Numeric,
18
18
  language: String,
19
- mrbgems: [String, FalseClass].freeze
19
+ mrbgems_lock: [String, FalseClass].freeze
20
20
  }.freeze
21
21
  COMMAND_OPTIONS = {
22
22
  "setup" => %i[cache language language_version],
23
- "build" => %i[cache language language_version board firmware mrbgems],
23
+ "build" => %i[cache language language_version board firmware mrbgems_lock],
24
24
  "bootsel" => %i[mount port baud timeout],
25
- "deploy" => %i[cache language language_version board firmware mrbgems mount port baud timeout],
25
+ "deploy" => %i[cache language language_version board firmware mrbgems_lock mount port baud timeout],
26
26
  "dfu" => %i[cache language language_version port baud timeout],
27
27
  "flash" => %i[cache language language_version board firmware mount port timeout],
28
- "run" => %i[port baud timeout language],
29
- "exec" => %i[port baud timeout language],
28
+ "run" => %i[port baud timeout language mrbgems_lock],
29
+ "exec" => %i[port baud timeout language mrbgems_lock],
30
30
  "reset" => %i[port baud timeout],
31
31
  "monitor" => %i[port baud timeout],
32
32
  "repl" => %i[port baud timeout],
@@ -106,6 +106,7 @@ module Rpremote
106
106
  expected = OPTION_TYPES[key]
107
107
  raise Error, "unknown config option: #{key}" unless expected
108
108
  raise Error, "config option #{key} must be a #{type_name(expected)}" unless valid_value?(value, expected)
109
+
109
110
  next unless %i[baud timeout].include?(key)
110
111
  next if value.positive?
111
112
 
@@ -38,7 +38,7 @@ module Rpremote
38
38
  board: defaults.fetch(:board, Target::DEFAULT_BOARD),
39
39
  cache: defaults.fetch(:cache, Target::DEFAULT_CACHE_DIR),
40
40
  firmware: defaults[:firmware],
41
- mrbgems: defaults[:mrbgems],
41
+ mrbgems_lock: defaults[:mrbgems_lock],
42
42
  mount: defaults[:mount],
43
43
  port: defaults[:port],
44
44
  baud: defaults.fetch(:baud, Serial::BAUD_RATE),
@@ -53,8 +53,8 @@ module Rpremote
53
53
  opts.on("--board BOARD") { |value| options[:board] = value }
54
54
  opts.on("--cache DIR") { |value| options[:cache] = value }
55
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 }
56
+ opts.on("--lockfile FILE") { |value| options[:mrbgems_lock] = value }
57
+ opts.on("--no-mrbgems") { options[:mrbgems_lock] = false }
58
58
  opts.on("--mount DIR") { |value| options[:mount] = value }
59
59
  opts.on("--port PORT") { |value| options[:port] = value }
60
60
  opts.on("--baud RATE", Integer) { |value| options[:baud] = value }
@@ -70,19 +70,27 @@ module Rpremote
70
70
  cache_dir: options[:cache],
71
71
  firmware: options[:firmware]
72
72
  )
73
+ print_target_options(target)
74
+ print_runtime_options(options)
75
+ end
76
+
77
+ def print_target_options(target)
73
78
  output.puts("language=#{target.language}")
74
79
  output.puts("language_version=#{target.language_version}")
75
80
  output.puts("board=#{target.board}")
76
81
  output.puts("cache=#{target.cache_dir}")
77
82
  output.puts("firmware=#{target.firmware_path}")
78
- output.puts("mrbgems=#{mrbgems_value(options[:mrbgems])}")
83
+ end
84
+
85
+ def print_runtime_options(options)
86
+ output.puts("mrbgems_lock=#{mrbgems_lock_value(options[:mrbgems_lock])}")
79
87
  output.puts("mount=#{options[:mount] || "auto"}")
80
88
  output.puts("port=#{options[:port] || "auto"}")
81
89
  output.puts("baud=#{options[:baud]}")
82
90
  output.puts("timeout=#{options[:timeout]}")
83
91
  end
84
92
 
85
- def mrbgems_value(value)
93
+ def mrbgems_lock_value(value)
86
94
  return "false" if value == false
87
95
 
88
96
  value || "auto"
@@ -9,79 +9,106 @@ module Rpremote
9
9
  SHELL_READY_TIMEOUT = 2.0
10
10
  RETRY_INTERVAL = 0.25
11
11
 
12
+ Context = Data.define(:options, :output, :error, :services, :target_options, :firmware_path)
13
+
12
14
  class Error < Rpremote::Error; end
13
15
 
14
- # The command intentionally keeps the deployment stages visible in execution order.
15
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
16
16
  def self.run(args, defaults:, output: $stdout, error: $stderr, services: {})
17
17
  options = parse_options(args, defaults)
18
18
  raise ArgumentError, "usage: rpremote deploy PATH [options]" unless args.length == 1
19
19
 
20
- project = File.expand_path(args.first)
21
- library, remote_library, source = project_files(project)
20
+ library, remote_library, source = project_files(File.expand_path(args.first))
22
21
  Language.validate!(options[:language])
22
+ context = deployment_context(options, output, error, services)
23
+ build_firmware(context)
24
+ result = flash_firmware(context)
25
+ connect_to_shell(context, result)
26
+ push_library(context, result, library, remote_library)
27
+ run_source(context, result, source)
28
+ rescue Errno::ENOENT => e
29
+ raise ArgumentError, e.message
30
+ end
23
31
 
24
- flasher = services.fetch(:flasher, Flasher)
25
- serial = services.fetch(:serial, Serial)
26
- device = services.fetch(:device, Device)
27
- runner = services.fetch(:runner, Runner)
32
+ def self.deployment_context(options, output, error, services)
28
33
  target_options = options.slice(:language, :language_version, :board, :cache_dir, :firmware)
29
34
  firmware_path = Target.new(**target_options).firmware_path(root: Dir.pwd)
35
+ Context.new(options:, output:, error:, services:, target_options:, firmware_path:)
36
+ end
37
+ private_class_method :deployment_context
38
+
39
+ def self.build_firmware(context)
40
+ return unless context.options[:build]
41
+
42
+ builder = context.services.fetch(:builder) { Builder.new }
43
+ context.output.puts("deploy build: #{context.firmware_path}")
44
+ builder.build(
45
+ **context.target_options,
46
+ mrbgems_lock: context.options[:mrbgems_lock],
47
+ output: context.output,
48
+ error: context.error
49
+ )
50
+ end
51
+ private_class_method :build_firmware
30
52
 
31
- if options[:build]
32
- builder = services.fetch(:builder) { Builder.new }
33
- output.puts("deploy build: #{firmware_path}")
34
- builder.build(
35
- **target_options,
36
- mrbgems: options[:mrbgems],
37
- output: output,
38
- error: error
39
- )
40
- end
41
-
42
- flash_service = flasher.new(timeout: options[:timeout])
43
- bootsel_mount = resolve_bootsel_mount(options, flash_service, output, services)
44
-
53
+ def self.flash_firmware(context)
54
+ options = context.options
55
+ flash_service = context.services.fetch(:flasher, Flasher).new(timeout: options[:timeout])
56
+ bootsel_mount = resolve_bootsel_mount(options, flash_service, context.output, context.services)
45
57
  mount = options[:mount] || "an automatically detected RP2350 BOOTSEL volume"
46
- output.puts("deploy flash: #{firmware_path} to #{mount}; this replaces persistent board firmware")
47
- result = flash_service.flash(
48
- firmware_path,
49
- mount: bootsel_mount,
50
- port: options[:port]
58
+ context.output.puts(
59
+ "deploy flash: #{context.firmware_path} to #{mount}; this replaces persistent board firmware"
51
60
  )
61
+ flash_service.flash(context.firmware_path, mount: bootsel_mount, port: options[:port])
62
+ end
63
+ private_class_method :flash_firmware
52
64
 
53
- connection_options = options.except(:build).merge(port: result.port)
54
- output.puts("deploy connect: waiting for R2P2 Shell on #{result.port}")
65
+ def self.connect_to_shell(context, result)
66
+ options = context.options
67
+ context.output.puts("deploy connect: waiting for R2P2 Shell on #{result.port}")
55
68
  wait_for_shell(
56
- port: result.port, baud: options[:baud], timeout: options[:timeout], serial: serial,
57
- shell: services.fetch(:shell, Shell),
58
- sleeper: services.fetch(:sleeper) { ->(seconds) { sleep(seconds) } },
59
- clock: services.fetch(:clock) { -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) } }
69
+ port: result.port, baud: options[:baud], timeout: options[:timeout],
70
+ serial: context.services.fetch(:serial, Serial),
71
+ shell: context.services.fetch(:shell, Shell),
72
+ sleeper: context.services.fetch(:sleeper) { ->(seconds) { sleep(seconds) } },
73
+ clock: context.services.fetch(:clock) { -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) } }
60
74
  )
61
- output.puts("deploy connect: ready on #{result.port}")
75
+ context.output.puts("deploy connect: ready on #{result.port}")
76
+ end
77
+ private_class_method :connect_to_shell
62
78
 
63
- if File.directory?(library)
64
- output.puts("deploy push: #{library} -> :#{remote_library}")
65
- recursive_copy = services.fetch(:recursive_copy) do
66
- RecursiveCopy.new(output: output, serial: serial, device: device)
67
- end
68
- recursive_copy.call(library, ":#{remote_library}", connection_options)
69
- else
70
- output.puts("deploy push: skipped; directory not found: #{library}")
79
+ def self.push_library(context, result, library, remote_library)
80
+ unless File.directory?(library)
81
+ context.output.puts("deploy push: skipped; directory not found: #{library}")
82
+ return
71
83
  end
72
84
 
73
- output.puts("deploy run: #{source} on #{result.port}")
74
- source_data = prepend_mrbgem_requires(File.binread(source), options[:mrbgems])
85
+ context.output.puts("deploy push: #{library} -> :#{remote_library}")
86
+ recursive_copy = context.services.fetch(:recursive_copy) do
87
+ RecursiveCopy.new(
88
+ output: context.output,
89
+ serial: context.services.fetch(:serial, Serial),
90
+ device: context.services.fetch(:device, Device)
91
+ )
92
+ end
93
+ connection_options = context.options.except(:build).merge(port: result.port)
94
+ recursive_copy.call(library, ":#{remote_library}", connection_options)
95
+ end
96
+ private_class_method :push_library
97
+
98
+ def self.run_source(context, result, source)
99
+ options = context.options
100
+ context.output.puts("deploy run: #{source} on #{result.port}")
101
+ source_data = prepend_mrbgem_requires(File.binread(source), options[:mrbgems_lock])
102
+ serial = context.services.fetch(:serial, Serial)
103
+ runner = context.services.fetch(:runner, Runner)
75
104
  run_output = serial.open(result.port, baud: options[:baud]) do |port|
76
105
  runner.new(port, timeout: options[:timeout]).run(
77
- source_data, output: output, cleanup: false, remote_path: Runner::DEPLOY_REMOTE_PATH
106
+ source_data, output: context.output, cleanup: false, remote_path: Runner::DEPLOY_REMOTE_PATH
78
107
  )
79
108
  end
80
- output.puts("deploy run: completed; output bytes: #{run_output.to_s.bytesize}")
81
- rescue Errno::ENOENT => e
82
- raise ArgumentError, e.message
109
+ context.output.puts("deploy run: completed; output bytes: #{run_output.to_s.bytesize}")
83
110
  end
84
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
111
+ private_class_method :run_source
85
112
 
86
113
  def self.project_files(project)
87
114
  raise ArgumentError, "project directory not found: #{project}" unless File.directory?(project)
@@ -95,10 +122,13 @@ module Rpremote
95
122
  end
96
123
  private_class_method :project_files
97
124
 
98
- def self.prepend_mrbgem_requires(source, definition)
99
- return source if definition == false
125
+ def self.prepend_mrbgem_requires(source, lock_file)
126
+ return source if lock_file == false
127
+
128
+ path = lock_file || Mrbgems::DEFAULT_LOCK_PATH
129
+ raise ArgumentError, "mrbgems lock file does not exist: #{path}" if lock_file && !File.file?(path)
100
130
 
101
- Mrbgems.new(path: definition || Mrbgems::DEFAULT_PATH, cwd: Dir.pwd).prepend_requires(source)
131
+ Mrbgems.new(lock_path: path, cwd: Dir.pwd).prepend_requires(source)
102
132
  end
103
133
  private_class_method :prepend_mrbgem_requires
104
134
 
@@ -158,7 +188,7 @@ module Rpremote
158
188
  board: defaults.fetch(:board, Target::DEFAULT_BOARD),
159
189
  cache_dir: defaults.fetch(:cache, Target::DEFAULT_CACHE_DIR),
160
190
  firmware: defaults[:firmware],
161
- mrbgems: defaults[:mrbgems],
191
+ mrbgems_lock: defaults[:mrbgems_lock],
162
192
  mount: defaults[:mount],
163
193
  port: defaults[:port],
164
194
  baud: defaults.fetch(:baud, Serial::BAUD_RATE),
@@ -176,8 +206,8 @@ module Rpremote
176
206
  parser.on("--board BOARD") { |value| options[:board] = value }
177
207
  parser.on("--cache DIR") { |value| options[:cache_dir] = value }
178
208
  parser.on("--firmware FILE") { |value| options[:firmware] = value }
179
- parser.on("--mrbgems FILE") { |value| options[:mrbgems] = value }
180
- parser.on("--no-mrbgems") { options[:mrbgems] = false }
209
+ parser.on("--lockfile FILE") { |value| options[:mrbgems_lock] = value }
210
+ parser.on("--no-mrbgems") { options[:mrbgems_lock] = false }
181
211
  parser.on("--mount DIR") { |value| options[:mount] = value }
182
212
  parser.on("--port PORT") { |value| options[:port] = value }
183
213
  parser.on("--baud RATE", Integer) { |value| options[:baud] = value }
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rpremote
4
+ module Help
5
+ def self.dfu_text(subcommand)
6
+ return dfu_app_text if subcommand == "app"
7
+ return dfu_compile_text if subcommand == "compile"
8
+ return dfu_status_text if subcommand == "status"
9
+ return dfu_remove_text if subcommand == "remove"
10
+
11
+ <<~HELP
12
+ Usage: #{COMMAND_USAGE.fetch(:dfu_app)}
13
+ #{COMMAND_USAGE.fetch(:dfu_compile)}
14
+ #{COMMAND_USAGE.fetch(:dfu_status)}
15
+ #{COMMAND_USAGE.fetch(:dfu_remove)}
16
+
17
+ Stages and inspects PicoModem DFU applications. Run `rpremote dfu SUBCOMMAND --help` for details.
18
+ HELP
19
+ end
20
+
21
+ def self.dfu_app_text
22
+ <<~HELP
23
+ Usage: #{COMMAND_USAGE.fetch(:dfu_app)}
24
+
25
+ Transfers a Ruby source or matching RITE bytecode application to the inactive DFU slot. The next R2P2 restart tries it.
26
+ Options default to the configured or automatically selected CDC 0 port, 115200 baud, and 20 seconds.
27
+ The transfer changes the staged boot application; use `rpremote reset` to restart and require DFU.confirm after a successful boot.
28
+ HELP
29
+ end
30
+
31
+ def self.dfu_compile_text
32
+ <<~HELP
33
+ Usage: #{COMMAND_USAGE.fetch(:dfu_compile)}
34
+
35
+ Compiles Ruby source to bytecode that matches the selected PicoRuby version. The output defaults beside FILE.
36
+ It requires prepared PicoRuby source and does not connect to a board.
37
+ HELP
38
+ end
39
+
40
+ def self.dfu_status_text
41
+ <<~HELP
42
+ Usage: #{COMMAND_USAGE.fetch(:dfu_status)}
43
+
44
+ Prints the active and candidate DFU A/B slots. Defaults are the configured or automatically selected CDC 0 port,
45
+ 115200 baud, and 20 seconds. This command reads board state without changing it.
46
+ HELP
47
+ end
48
+
49
+ def self.dfu_remove_text
50
+ <<~HELP
51
+ Usage: #{COMMAND_USAGE.fetch(:dfu_remove)}
52
+
53
+ Permanently removes the Ruby source and bytecode applications from both DFU A/B slots and resets their metadata.
54
+ Run `rpremote reset` afterward to stop the application already running in RAM. It does not remove /home/app.rb,
55
+ /home/app.mrb, other files, embedded mrbgems, or R2P2 firmware.
56
+ HELP
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rpremote
4
+ module Help
5
+ def self.fs_text(subcommand)
6
+ return fs_subcommand_text(subcommand) if %w[cp push cat ls rm mkdir].include?(subcommand)
7
+
8
+ <<~HELP
9
+ Usage: #{COMMAND_USAGE.fetch(:fs_cp)}
10
+ #{COMMAND_USAGE.fetch(:fs_push)}
11
+ #{COMMAND_USAGE.fetch(:fs_cat)}
12
+ #{COMMAND_USAGE.fetch(:fs_ls)}
13
+ #{COMMAND_USAGE.fetch(:fs_rm)}
14
+ #{COMMAND_USAGE.fetch(:fs_mkdir)}
15
+
16
+ Uses :/REMOTE/PATH for R2P2 paths. Run `rpremote fs SUBCOMMAND --help` for details.
17
+ HELP
18
+ end
19
+
20
+ def self.fs_subcommand_text(subcommand)
21
+ usage = COMMAND_USAGE.fetch(:"fs_#{subcommand}")
22
+ effect = case subcommand
23
+ when "rm" then "Deletes the remote path permanently."
24
+ when "mkdir" then "Creates a remote directory."
25
+ when "cp" then "Transfers one file, or recursively uploads a local directory with --recursive."
26
+ when "push" then "Creates missing remote directories and recursively uploads a local directory."
27
+ else "Reads remote files."
28
+ end
29
+ <<~HELP
30
+ Usage: #{usage}
31
+
32
+ #{effect} Defaults are the automatic CDC 0 port, 115200 baud, and 20 seconds.
33
+ HELP
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rpremote
4
+ module Help
5
+ def self.setup_text
6
+ <<~HELP
7
+ Usage: #{COMMAND_USAGE.fetch(:setup)}
8
+
9
+ Creates config/setting.json when it does not exist, then downloads and prepares PicoRuby source and the official Raspberry Pi nuke_universal.uf2 firmware.
10
+ Options: --language LANGUAGE (picoruby), --language-version VERSION (latest), --cache DIR (firmware), --force.
11
+ This changes the project configuration and source cache but does not connect to a board.
12
+ HELP
13
+ end
14
+
15
+ def self.build_text
16
+ <<~HELP
17
+ Usage: #{COMMAND_USAGE.fetch(:build)}
18
+
19
+ Builds a custom UF2 for the selected target. It writes generated files under build/ and the selected firmware path.
20
+ Options: --language LANGUAGE, --language-version VERSION, --board BOARD (pico2), --cache DIR (firmware),
21
+ --firmware FILE, --lockfile FILE, --no-mrbgems. A project Mrbgems.lock file is used automatically by default.
22
+ HELP
23
+ end
24
+
25
+ def self.build_clean_text
26
+ <<~HELP
27
+ Usage: #{COMMAND_USAGE.fetch(:build_clean)}
28
+
29
+ Removes only the project's generated build/ directory. It does not remove firmware/, Mrbgems, or Mrbgems.lock.
30
+ HELP
31
+ end
32
+
33
+ def self.deploy_text
34
+ <<~HELP
35
+ Usage: #{COMMAND_USAGE.fetch(:deploy)}
36
+
37
+ Enters BOOTSEL when needed, flashes the existing selected UF2, and waits for the R2P2 Shell to become ready.
38
+ With --build, builds the selected custom UF2 before entering BOOTSEL and flashing it.
39
+ 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.
40
+ The stages run in order and stop at the first failure. Flashing replaces persistent board firmware.
41
+ deploy requires current PicoRuby 4.x firmware.
42
+ Options combine build, BOOTSEL, flash, library-copy, and run settings. The first install of firmware with automatic BOOTSEL still requires holding BOOTSEL.
43
+ HELP
44
+ end
45
+
46
+ def self.bootsel_text
47
+ <<~HELP
48
+ Usage: #{COMMAND_USAGE.fetch(:bootsel)}
49
+
50
+ Asks the running R2P2 firmware to restart as an RP2350 BOOTSEL USB volume, then waits for that volume to appear.
51
+ By default it does not write firmware. This requires firmware built by a current rpremote; use physical BOOTSEL once to install it first.
52
+ --reset-flash-memory copies the official Raspberry Pi nuke_universal.uf2 in BOOTSEL mode and erases all external flash memory.
53
+ After resetting flash memory, wait for BOOTSEL to reappear and run `rpremote flash` to install R2P2 again.
54
+ HELP
55
+ end
56
+
57
+ def self.mrbgems_text(subcommand)
58
+ action = subcommand || "check|list|lock|update"
59
+ <<~HELP
60
+ Usage: #{COMMAND_USAGE.fetch(:mrbgems).sub("SUBCOMMAND", action)}
61
+
62
+ Checks or lists build dependencies, writes a reproducible lock file, or updates locked GitHub commits.
63
+ `lock --with production --without test` includes common and production gems. Exclusions take precedence.
64
+ `update` resolves new commits using the same group options.
65
+ HELP
66
+ end
67
+
68
+ def self.flash_text
69
+ <<~HELP
70
+ Usage: #{COMMAND_USAGE.fetch(:flash)}
71
+
72
+ Copies the selected UF2 to an RP2350 BOOTSEL volume and waits for R2P2 to reconnect. It replaces persistent board firmware.
73
+ Defaults select pico2, firmware/picoruby-latest-pico2.uf2, an automatic BOOTSEL mount and CDC 0 port, and 20 seconds.
74
+ HELP
75
+ end
76
+
77
+ def self.config_text(subcommand)
78
+ unless subcommand == "show"
79
+ return "Usage: #{COMMAND_USAGE.fetch(:config_show)}\n\nUse `rpremote config show --help` for the supported overrides.\n"
80
+ end
81
+
82
+ <<~HELP
83
+ Usage: #{COMMAND_USAGE.fetch(:config_show)}
84
+
85
+ Prints the configuration file, defaults, and command-line overrides after resolution. It does not connect to a board or change state.
86
+ HELP
87
+ end
88
+ end
89
+ end