rpremote 0.1.0 → 0.2.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 +21 -17
- data/README.ja.md +43 -33
- data/README.md +41 -32
- data/RELEASING.md +23 -23
- data/THIRD_PARTY_NOTICES.md +4 -18
- data/lib/rpremote/builder.rb +9 -2
- data/lib/rpremote/cli.rb +26 -17
- data/lib/rpremote/config.rb +1 -0
- data/lib/rpremote/config_show.rb +91 -0
- data/lib/rpremote/dfu_command.rb +4 -4
- data/lib/rpremote/flash_command.rb +5 -2
- data/lib/rpremote/flasher.rb +1 -1
- data/lib/rpremote/help.rb +253 -26
- data/lib/rpremote/language_source.rb +4 -1
- data/lib/rpremote/picomodem.rb +3 -7
- data/lib/rpremote/picoruby_source_patch.rb +32 -0
- data/lib/rpremote/resetter.rb +1 -1
- data/lib/rpremote/runner.rb +2 -2
- data/lib/rpremote/shell.rb +38 -7
- data/lib/rpremote/version.rb +1 -1
- data/lib/rpremote.rb +1 -0
- data/patches/picoruby-3.4.5-ruby-exception-status.patch +59 -0
- data/patches/picoruby-4.0.3-ruby-exception-status.patch +59 -0
- data/sig/rpremote.rbs +17 -3
- data/tasks/firmware_build.rb +2 -8
- data/tasks/release_check.rb +3 -7
- metadata +5 -1
|
@@ -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
|
data/lib/rpremote/dfu_command.rb
CHANGED
|
@@ -4,7 +4,7 @@ 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: {})
|
|
@@ -30,6 +30,8 @@ module Rpremote
|
|
|
30
30
|
type = options[:type] || type_for(source)
|
|
31
31
|
port_path = services[:device].main_port(options[:port])
|
|
32
32
|
verify_rite_version!(data, port_path, options, services) if type == "RITE"
|
|
33
|
+
output.puts("staging DFU #{type.downcase} app (#{data.bytesize} bytes): #{source} -> inactive slot on #{port_path}; " \
|
|
34
|
+
"this changes the staged boot application")
|
|
33
35
|
services[:serial].open(port_path, baud: options[:baud]) do |port|
|
|
34
36
|
services[:modem].new(port, timeout: options[:timeout]).dfu(data, type: type)
|
|
35
37
|
end
|
|
@@ -122,9 +124,7 @@ module Rpremote
|
|
|
122
124
|
|
|
123
125
|
def self.verify_rite_version!(data, port_path, options, services)
|
|
124
126
|
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
|
|
127
|
+
raise ArgumentError, "DFU RITE app must start with a RITE bytecode header" unless bytecode_version&.match?(/\ARITE\d{4}\z/)
|
|
128
128
|
|
|
129
129
|
picoruby_version = services[:serial].open(port_path, baud: options[:baud]) do |port|
|
|
130
130
|
services[:runner].new(port, timeout: options[:timeout]).run("p PICORUBY_VERSION")[/\d+\.\d+\.\d+/]
|
|
@@ -9,12 +9,15 @@ module Rpremote
|
|
|
9
9
|
raise ArgumentError, "flash does not accept arguments; use --firmware FILE" unless args.empty?
|
|
10
10
|
|
|
11
11
|
target = Target.new(**options.slice(:language, :language_version, :board, :cache_dir, :firmware))
|
|
12
|
+
firmware_path = target.firmware_path(root: Dir.pwd)
|
|
13
|
+
mount = options[:mount] || "an automatically detected RP2350 BOOTSEL volume"
|
|
14
|
+
output.puts("flashing #{firmware_path} to #{mount}; this replaces persistent board firmware")
|
|
12
15
|
result = flasher.new(timeout: options[:timeout]).flash(
|
|
13
|
-
|
|
16
|
+
firmware_path,
|
|
14
17
|
mount: options[:mount],
|
|
15
18
|
port: options[:port]
|
|
16
19
|
)
|
|
17
|
-
output.puts("flashed firmware #{File.basename(
|
|
20
|
+
output.puts("flashed firmware #{File.basename(firmware_path)}: #{result.port}")
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
def self.parse_options(args, defaults)
|
data/lib/rpremote/flasher.rb
CHANGED
data/lib/rpremote/help.rb
CHANGED
|
@@ -1,35 +1,60 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rubocop:disable Metrics/ModuleLength, Metrics/CyclomaticComplexity
|
|
4
|
+
|
|
3
5
|
module Rpremote
|
|
4
6
|
module Help
|
|
5
|
-
|
|
7
|
+
COMMAND_USAGE = {
|
|
8
|
+
setup: "rpremote setup [--language LANGUAGE] [--language-version VERSION] [--force] [--cache DIR]",
|
|
9
|
+
build: "rpremote build [--language LANGUAGE] [--language-version VERSION] [--board BOARD] " \
|
|
10
|
+
"[--firmware FILE] [--cache DIR] [--mrbgems FILE|--no-mrbgems]",
|
|
11
|
+
build_clean: "rpremote build clean",
|
|
12
|
+
dfu_app: "rpremote dfu app FILE [--type ruby|rite] [--port PORT] [--baud RATE] [--timeout SEC]",
|
|
13
|
+
dfu_compile: "rpremote dfu compile FILE [--output FILE] [--language LANGUAGE] [--language-version VERSION] [--cache DIR]",
|
|
14
|
+
dfu_status: "rpremote dfu status [--port PORT] [--baud RATE] [--timeout SEC]",
|
|
15
|
+
mrbgems: "rpremote mrbgems SUBCOMMAND [--file FILE] [--lockfile FILE]",
|
|
16
|
+
flash: "rpremote flash [--firmware FILE] [--language LANGUAGE] [--language-version VERSION] " \
|
|
17
|
+
"[--board BOARD] [--cache DIR] [--mount DIR] [--port PORT] [--timeout SEC]",
|
|
18
|
+
config_show: "rpremote config show [--language LANGUAGE] [--language-version VERSION] [--board BOARD] " \
|
|
19
|
+
"[--cache DIR] [--firmware FILE] [--mrbgems FILE|--no-mrbgems] [--mount DIR] " \
|
|
20
|
+
"[--port PORT] [--baud RATE] [--timeout SEC]",
|
|
21
|
+
ports: "rpremote ports",
|
|
22
|
+
run: "rpremote run FILE [--port PORT] [--baud RATE] [--timeout SEC] [--language LANGUAGE]",
|
|
23
|
+
monitor: "rpremote monitor [--port PORT] [--baud RATE] [--timeout SEC]",
|
|
24
|
+
repl: "rpremote repl [--port PORT] [--baud RATE] [--timeout SEC]",
|
|
25
|
+
exec: "rpremote exec CODE [--port PORT] [--baud RATE] [--timeout SEC] [--language LANGUAGE]",
|
|
26
|
+
reset: "rpremote reset [--port PORT] [--baud RATE] [--timeout SEC]",
|
|
27
|
+
fs_cp: "rpremote fs cp SOURCE DESTINATION [--port PORT] [--baud RATE] [--timeout SEC]",
|
|
28
|
+
fs_cat: "rpremote fs cat :/REMOTE/PATH [--port PORT] [--baud RATE] [--timeout SEC]",
|
|
29
|
+
fs_ls: "rpremote fs ls :/REMOTE/PATH [--port PORT] [--baud RATE] [--timeout SEC]",
|
|
30
|
+
fs_rm: "rpremote fs rm :/REMOTE/PATH [--port PORT] [--baud RATE] [--timeout SEC]",
|
|
31
|
+
fs_mkdir: "rpremote fs mkdir :/REMOTE/PATH [--port PORT] [--baud RATE] [--timeout SEC]"
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
TEXT = <<~HELP.freeze
|
|
6
35
|
rpremote - R2P2 remote control for Raspberry Pi Pico 2
|
|
7
36
|
|
|
8
37
|
Usage:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
rpremote fs cat :/REMOTE/PATH [--port PORT]
|
|
30
|
-
rpremote fs ls :/REMOTE/PATH [--port PORT]
|
|
31
|
-
rpremote fs rm :/REMOTE/PATH [--port PORT]
|
|
32
|
-
rpremote fs mkdir :/REMOTE/PATH [--port PORT]
|
|
38
|
+
#{COMMAND_USAGE.fetch(:setup)}
|
|
39
|
+
#{COMMAND_USAGE.fetch(:build)}
|
|
40
|
+
#{COMMAND_USAGE.fetch(:build_clean)}
|
|
41
|
+
#{COMMAND_USAGE.fetch(:dfu_app)}
|
|
42
|
+
#{COMMAND_USAGE.fetch(:dfu_compile)}
|
|
43
|
+
#{COMMAND_USAGE.fetch(:dfu_status)}
|
|
44
|
+
#{COMMAND_USAGE.fetch(:mrbgems).sub("SUBCOMMAND", "check|list|lock|update")}
|
|
45
|
+
#{COMMAND_USAGE.fetch(:flash)}
|
|
46
|
+
#{COMMAND_USAGE.fetch(:config_show)}
|
|
47
|
+
#{COMMAND_USAGE.fetch(:ports)}
|
|
48
|
+
#{COMMAND_USAGE.fetch(:run)}
|
|
49
|
+
#{COMMAND_USAGE.fetch(:monitor)}
|
|
50
|
+
#{COMMAND_USAGE.fetch(:repl)}
|
|
51
|
+
#{COMMAND_USAGE.fetch(:exec)}
|
|
52
|
+
#{COMMAND_USAGE.fetch(:reset)}
|
|
53
|
+
#{COMMAND_USAGE.fetch(:fs_cp)}
|
|
54
|
+
#{COMMAND_USAGE.fetch(:fs_cat)}
|
|
55
|
+
#{COMMAND_USAGE.fetch(:fs_ls)}
|
|
56
|
+
#{COMMAND_USAGE.fetch(:fs_rm)}
|
|
57
|
+
#{COMMAND_USAGE.fetch(:fs_mkdir)}
|
|
33
58
|
|
|
34
59
|
`rpremote build clean` removes only the project's generated `build/` directory.
|
|
35
60
|
|
|
@@ -52,9 +77,211 @@ module Rpremote
|
|
|
52
77
|
--mount DIR use an explicit RP2350 BOOTSEL drive
|
|
53
78
|
--port PORT use an explicit R2P2 CDC 0 device
|
|
54
79
|
--baud RATE serial baud rate (default: 115200)
|
|
55
|
-
--timeout SEC timeout in seconds (default:
|
|
80
|
+
--timeout SEC timeout in seconds (default: 20)
|
|
56
81
|
-h, --help show this help
|
|
57
82
|
-V, --version show the version
|
|
58
83
|
HELP
|
|
84
|
+
|
|
85
|
+
def self.requested?(command, args)
|
|
86
|
+
%w[help --help -h].include?(command) || args.include?("--help") || args.include?("-h")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.command_text(command, args)
|
|
90
|
+
command = args.shift if command == "help"
|
|
91
|
+
return TEXT if command.nil? || %w[help --help -h].include?(command)
|
|
92
|
+
|
|
93
|
+
case command
|
|
94
|
+
when "setup" then setup_text
|
|
95
|
+
when "build" then args.first == "clean" ? build_clean_text : build_text
|
|
96
|
+
when "dfu" then dfu_text(args.first)
|
|
97
|
+
when "mrbgems" then mrbgems_text(args.first)
|
|
98
|
+
when "flash" then flash_text
|
|
99
|
+
when "config" then config_text(args.first)
|
|
100
|
+
when "ports" then ports_text
|
|
101
|
+
when "run" then run_text
|
|
102
|
+
when "exec" then exec_text
|
|
103
|
+
when "monitor" then monitor_text
|
|
104
|
+
when "repl" then repl_text
|
|
105
|
+
when "reset" then reset_text
|
|
106
|
+
when "fs" then fs_text(args.first)
|
|
107
|
+
else TEXT
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.setup_text
|
|
112
|
+
<<~HELP
|
|
113
|
+
Usage: #{COMMAND_USAGE.fetch(:setup)}
|
|
114
|
+
|
|
115
|
+
Creates config/setting.json when it does not exist, then downloads and prepares PicoRuby source.
|
|
116
|
+
Options: --language LANGUAGE (picoruby), --language-version VERSION (4.0.3), --cache DIR (firmware), --force.
|
|
117
|
+
This changes the project configuration and source cache but does not connect to a board.
|
|
118
|
+
HELP
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.build_text
|
|
122
|
+
<<~HELP
|
|
123
|
+
Usage: #{COMMAND_USAGE.fetch(:build)}
|
|
124
|
+
|
|
125
|
+
Builds a custom UF2 for the selected target. It writes generated files under build/ and the selected firmware path.
|
|
126
|
+
Options: --language LANGUAGE, --language-version VERSION, --board BOARD (pico2), --cache DIR (firmware),
|
|
127
|
+
--firmware FILE, --mrbgems FILE, --no-mrbgems. A project Mrbgems file is used automatically by default.
|
|
128
|
+
HELP
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def self.build_clean_text
|
|
132
|
+
<<~HELP
|
|
133
|
+
Usage: #{COMMAND_USAGE.fetch(:build_clean)}
|
|
134
|
+
|
|
135
|
+
Removes only the project's generated build/ directory. It does not remove firmware/, Mrbgems, or Mrbgems.lock.
|
|
136
|
+
HELP
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.dfu_text(subcommand)
|
|
140
|
+
return dfu_app_text if subcommand == "app"
|
|
141
|
+
return dfu_compile_text if subcommand == "compile"
|
|
142
|
+
return dfu_status_text if subcommand == "status"
|
|
143
|
+
|
|
144
|
+
<<~HELP
|
|
145
|
+
Usage: #{COMMAND_USAGE.fetch(:dfu_app)}
|
|
146
|
+
#{COMMAND_USAGE.fetch(:dfu_compile)}
|
|
147
|
+
#{COMMAND_USAGE.fetch(:dfu_status)}
|
|
148
|
+
|
|
149
|
+
Stages and inspects PicoModem DFU applications. Run `rpremote dfu SUBCOMMAND --help` for details.
|
|
150
|
+
HELP
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def self.dfu_app_text
|
|
154
|
+
<<~HELP
|
|
155
|
+
Usage: #{COMMAND_USAGE.fetch(:dfu_app)}
|
|
156
|
+
|
|
157
|
+
Transfers a Ruby source or matching RITE bytecode application to the inactive DFU slot. The next R2P2 restart tries it.
|
|
158
|
+
Options default to the configured or automatically selected CDC 0 port, 115200 baud, and 20 seconds.
|
|
159
|
+
The transfer changes the staged boot application; use `rpremote reset` to restart and require DFU.confirm after a successful boot.
|
|
160
|
+
HELP
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def self.dfu_compile_text
|
|
164
|
+
<<~HELP
|
|
165
|
+
Usage: #{COMMAND_USAGE.fetch(:dfu_compile)}
|
|
166
|
+
|
|
167
|
+
Compiles Ruby source to bytecode that matches the selected PicoRuby version. The output defaults beside FILE.
|
|
168
|
+
It requires prepared PicoRuby source and does not connect to a board.
|
|
169
|
+
HELP
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def self.dfu_status_text
|
|
173
|
+
<<~HELP
|
|
174
|
+
Usage: #{COMMAND_USAGE.fetch(:dfu_status)}
|
|
175
|
+
|
|
176
|
+
Prints the active and candidate DFU A/B slots. Defaults are the configured or automatically selected CDC 0 port,
|
|
177
|
+
115200 baud, and 20 seconds. This command reads board state without changing it.
|
|
178
|
+
HELP
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def self.mrbgems_text(subcommand)
|
|
182
|
+
action = subcommand || "check|list|lock|update"
|
|
183
|
+
<<~HELP
|
|
184
|
+
Usage: #{COMMAND_USAGE.fetch(:mrbgems).sub("SUBCOMMAND", action)}
|
|
185
|
+
|
|
186
|
+
Checks or lists build dependencies, writes a reproducible lock file, or updates locked GitHub commits.
|
|
187
|
+
`check` and `list` are read-only. `lock` writes Mrbgems.lock; `update` resolves new commits and rewrites it.
|
|
188
|
+
HELP
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def self.flash_text
|
|
192
|
+
<<~HELP
|
|
193
|
+
Usage: #{COMMAND_USAGE.fetch(:flash)}
|
|
194
|
+
|
|
195
|
+
Copies the selected UF2 to an RP2350 BOOTSEL volume and waits for R2P2 to reconnect. It replaces persistent board firmware.
|
|
196
|
+
Defaults select pico2, firmware/picoruby-4.0.3-pico2.uf2, an automatic BOOTSEL mount and CDC 0 port, and 20 seconds.
|
|
197
|
+
HELP
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def self.config_text(subcommand)
|
|
201
|
+
unless subcommand == "show"
|
|
202
|
+
return "Usage: #{COMMAND_USAGE.fetch(:config_show)}\n\nUse `rpremote config show --help` for the supported overrides.\n"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
<<~HELP
|
|
206
|
+
Usage: #{COMMAND_USAGE.fetch(:config_show)}
|
|
207
|
+
|
|
208
|
+
Prints the configuration file, defaults, and command-line overrides after resolution. It does not connect to a board or change state.
|
|
209
|
+
HELP
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def self.ports_text
|
|
213
|
+
"Usage: #{COMMAND_USAGE.fetch(:ports)}\n\nPrints each detected R2P2 CDC 0 serial path, one per line. It does not connect to a board.\n"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def self.run_text
|
|
217
|
+
<<~HELP
|
|
218
|
+
Usage: #{COMMAND_USAGE.fetch(:run)}
|
|
219
|
+
|
|
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.
|
|
222
|
+
HELP
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def self.exec_text
|
|
226
|
+
<<~HELP
|
|
227
|
+
Usage: #{COMMAND_USAGE.fetch(:exec)}
|
|
228
|
+
|
|
229
|
+
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.
|
|
231
|
+
HELP
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def self.monitor_text
|
|
235
|
+
<<~HELP
|
|
236
|
+
Usage: #{COMMAND_USAGE.fetch(:monitor)}
|
|
237
|
+
|
|
238
|
+
Opens a serial monitor for the selected CDC 0 port. Defaults are 115200 baud and 20 seconds; exit with Ctrl-].
|
|
239
|
+
HELP
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def self.repl_text
|
|
243
|
+
<<~HELP
|
|
244
|
+
Usage: #{COMMAND_USAGE.fetch(:repl)}
|
|
245
|
+
|
|
246
|
+
Opens PicoIRB on the selected CDC 0 port. Defaults are 115200 baud and 20 seconds; exit with Ctrl-].
|
|
247
|
+
HELP
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def self.reset_text
|
|
251
|
+
<<~HELP
|
|
252
|
+
Usage: #{COMMAND_USAGE.fetch(:reset)}
|
|
253
|
+
|
|
254
|
+
Reboots R2P2 and waits for reconnection. Defaults are the automatic CDC 0 port, 115200 baud, and 20 seconds.
|
|
255
|
+
HELP
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def self.fs_text(subcommand)
|
|
259
|
+
return fs_subcommand_text(subcommand) if %w[cp cat ls rm mkdir].include?(subcommand)
|
|
260
|
+
|
|
261
|
+
<<~HELP
|
|
262
|
+
Usage: #{COMMAND_USAGE.fetch(:fs_cp)}
|
|
263
|
+
#{COMMAND_USAGE.fetch(:fs_cat)}
|
|
264
|
+
#{COMMAND_USAGE.fetch(:fs_ls)}
|
|
265
|
+
#{COMMAND_USAGE.fetch(:fs_rm)}
|
|
266
|
+
#{COMMAND_USAGE.fetch(:fs_mkdir)}
|
|
267
|
+
|
|
268
|
+
Uses :/REMOTE/PATH for R2P2 paths. Run `rpremote fs SUBCOMMAND --help` for details.
|
|
269
|
+
HELP
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def self.fs_subcommand_text(subcommand)
|
|
273
|
+
usage = COMMAND_USAGE.fetch(:"fs_#{subcommand}")
|
|
274
|
+
effect = case subcommand
|
|
275
|
+
when "rm" then "Deletes the remote path permanently."
|
|
276
|
+
when "mkdir" then "Creates a remote directory."
|
|
277
|
+
else "Reads or transfers remote files."
|
|
278
|
+
end
|
|
279
|
+
<<~HELP
|
|
280
|
+
Usage: #{usage}
|
|
281
|
+
|
|
282
|
+
#{effect} Defaults are the automatic CDC 0 port, 115200 baud, and 20 seconds.
|
|
283
|
+
HELP
|
|
284
|
+
end
|
|
59
285
|
end
|
|
60
286
|
end
|
|
287
|
+
# rubocop:enable Metrics/ModuleLength, Metrics/CyclomaticComplexity
|
|
@@ -4,6 +4,7 @@ require "fileutils"
|
|
|
4
4
|
require "net/http"
|
|
5
5
|
require "timeout"
|
|
6
6
|
require "uri"
|
|
7
|
+
require_relative "picoruby_source_patch"
|
|
7
8
|
|
|
8
9
|
module Rpremote
|
|
9
10
|
class LanguageSource
|
|
@@ -26,6 +27,7 @@ module Rpremote
|
|
|
26
27
|
@fetcher = fetcher || method(:download)
|
|
27
28
|
@extractor = extractor || method(:extract)
|
|
28
29
|
@preparer = preparer || method(:prepare_repository)
|
|
30
|
+
@patcher = PicoRubySourcePatch.new(version: version).method(:apply)
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
def setup(force: false)
|
|
@@ -38,6 +40,7 @@ module Rpremote
|
|
|
38
40
|
raise ExtractError, "PicoRuby source was not extracted: #{source_dir}" unless File.directory?(source_dir)
|
|
39
41
|
|
|
40
42
|
preparer.call(source_dir)
|
|
43
|
+
patcher.call(source_dir)
|
|
41
44
|
source_dir
|
|
42
45
|
end
|
|
43
46
|
|
|
@@ -55,7 +58,7 @@ module Rpremote
|
|
|
55
58
|
|
|
56
59
|
private
|
|
57
60
|
|
|
58
|
-
attr_reader :fetcher, :extractor, :preparer
|
|
61
|
+
attr_reader :fetcher, :extractor, :preparer, :patcher
|
|
59
62
|
|
|
60
63
|
def fetch_archive
|
|
61
64
|
temporary = "#{archive_path}.part"
|
data/lib/rpremote/picomodem.rb
CHANGED
|
@@ -24,7 +24,7 @@ module Rpremote
|
|
|
24
24
|
READY = 0x01
|
|
25
25
|
|
|
26
26
|
CHUNK_SIZE = 480
|
|
27
|
-
DEFAULT_TIMEOUT =
|
|
27
|
+
DEFAULT_TIMEOUT = 20.0
|
|
28
28
|
MAX_FRAME_BODY = 65_535
|
|
29
29
|
DFU_MAGIC = "DFU\0".b
|
|
30
30
|
DFU_VERSION = 1
|
|
@@ -87,9 +87,7 @@ module Rpremote
|
|
|
87
87
|
when DONE_ACK
|
|
88
88
|
status, remote_crc = parse_completion(payload)
|
|
89
89
|
raise DeviceError, format("download failed with status 0x%02x", status) unless status == OK
|
|
90
|
-
if total && data.bytesize != total
|
|
91
|
-
raise ProtocolError, "file size mismatch: expected #{total}, received #{data.bytesize}"
|
|
92
|
-
end
|
|
90
|
+
raise ProtocolError, "file size mismatch: expected #{total}, received #{data.bytesize}" if total && data.bytesize != total
|
|
93
91
|
|
|
94
92
|
local_crc = Checksum.crc32(data)
|
|
95
93
|
raise ChecksumError, checksum_mismatch("CRC32", local_crc, remote_crc, 8) unless remote_crc == local_crc
|
|
@@ -194,9 +192,7 @@ module Rpremote
|
|
|
194
192
|
def expect_status(expected_command, expected_status, context)
|
|
195
193
|
command, payload = receive_frame
|
|
196
194
|
raise DeviceError, payload.force_encoding(Encoding::UTF_8) if command == ERROR
|
|
197
|
-
unless command == expected_command
|
|
198
|
-
raise ProtocolError, "unexpected response #{hex(command, 2)} while waiting for #{context}"
|
|
199
|
-
end
|
|
195
|
+
raise ProtocolError, "unexpected response #{hex(command, 2)} while waiting for #{context}" unless command == expected_command
|
|
200
196
|
raise ProtocolError, "#{context} has no status" if payload.empty?
|
|
201
197
|
return if payload.getbyte(0) == expected_status
|
|
202
198
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rpremote
|
|
4
|
+
class PicoRubySourcePatch
|
|
5
|
+
JOB_PATH = "mrbgems/picoruby-shell/mrblib/job.rb"
|
|
6
|
+
PATCH_ALIASES = { "3.4.2" => "3.4.5" }.freeze
|
|
7
|
+
|
|
8
|
+
class Error < Rpremote::Error; end
|
|
9
|
+
|
|
10
|
+
attr_reader :version
|
|
11
|
+
|
|
12
|
+
def initialize(version:)
|
|
13
|
+
@version = version
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def apply(source)
|
|
17
|
+
patch_version = PATCH_ALIASES.fetch(version, version)
|
|
18
|
+
patch = File.expand_path("../../patches/picoruby-#{patch_version}-ruby-exception-status.patch", __dir__)
|
|
19
|
+
return unless File.file?(patch) && File.file?(File.join(source, JOB_PATH))
|
|
20
|
+
return if git_apply?(source, patch, "--reverse", "--check")
|
|
21
|
+
|
|
22
|
+
raise Error, "cannot apply rpremote patch to PicoRuby #{version}" unless git_apply?(source, patch, "--check")
|
|
23
|
+
raise Error, "cannot apply rpremote patch to PicoRuby #{version}" unless git_apply?(source, patch)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def git_apply?(source, patch, *options)
|
|
29
|
+
system("git", "apply", *options, patch, chdir: source, out: File::NULL, err: File::NULL)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/rpremote/resetter.rb
CHANGED
data/lib/rpremote/runner.rb
CHANGED
|
@@ -20,14 +20,14 @@ module Rpremote
|
|
|
20
20
|
@path_factory = path_factory || method(:temporary_path)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
def run(data)
|
|
23
|
+
def run(data, output: nil)
|
|
24
24
|
remote_path = @path_factory.call
|
|
25
25
|
shell = @shell_class.new(io, timeout: timeout)
|
|
26
26
|
shell.synchronize!
|
|
27
27
|
@modem_class.new(io, timeout: timeout).upload(remote_path, data)
|
|
28
28
|
shell.synchronize!
|
|
29
29
|
shell_ready = true
|
|
30
|
-
shell.execute("./#{File.basename(remote_path)}")
|
|
30
|
+
output ? shell.execute("./#{File.basename(remote_path)}", output: output) : shell.execute("./#{File.basename(remote_path)}")
|
|
31
31
|
ensure
|
|
32
32
|
cleanup(shell, remote_path) if shell_ready && remote_path
|
|
33
33
|
end
|
data/lib/rpremote/shell.rb
CHANGED
|
@@ -4,13 +4,14 @@ require "io/wait"
|
|
|
4
4
|
|
|
5
5
|
module Rpremote
|
|
6
6
|
class Shell
|
|
7
|
-
DEFAULT_TIMEOUT =
|
|
7
|
+
DEFAULT_TIMEOUT = 20.0
|
|
8
8
|
PROMPT_START = "\e[?25l\e[1G$> \e[0K".b
|
|
9
9
|
PROMPT_END = "\e[?25h".b
|
|
10
10
|
ECHO_START = "\e[?25l\e[1G$> ".b
|
|
11
11
|
ERASE_LINE = "\e[0K".b
|
|
12
12
|
CURSOR_POSITION_QUERY = "\e[6n".b
|
|
13
13
|
CURSOR_POSITION_RESPONSE = "\e[1;1R".b
|
|
14
|
+
RUBY_EXCEPTION_STATUS = "\x1eR2P2:RUBY_EXCEPTION\x1f".b
|
|
14
15
|
|
|
15
16
|
class Error < Rpremote::Error; end
|
|
16
17
|
class TimeoutError < Error; end
|
|
@@ -42,11 +43,18 @@ module Rpremote
|
|
|
42
43
|
nil
|
|
43
44
|
end
|
|
44
45
|
|
|
45
|
-
def execute(command)
|
|
46
|
+
def execute(command, output: nil)
|
|
46
47
|
command = validate_command(command)
|
|
47
48
|
send_command(command)
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
echo = ECHO_START + command.b + ERASE_LINE
|
|
50
|
+
stream_state = {}
|
|
51
|
+
response = read_until_prompt(after: echo) { |buffer| stream_output(output, buffer, echo, stream_state) }
|
|
52
|
+
command_output = extract_output(response, command)
|
|
53
|
+
ruby_exception = command_output.include?(RUBY_EXCEPTION_STATUS)
|
|
54
|
+
command_output = remove_ruby_exception_status(command_output)
|
|
55
|
+
raise CommandError, "Ruby exception reported by R2P2" if ruby_exception
|
|
56
|
+
|
|
57
|
+
command_output
|
|
50
58
|
rescue TimeoutError
|
|
51
59
|
interrupt
|
|
52
60
|
raise
|
|
@@ -78,6 +86,7 @@ module Rpremote
|
|
|
78
86
|
|
|
79
87
|
buffer << read_available(deadline)
|
|
80
88
|
answer_terminal_queries(buffer)
|
|
89
|
+
yield buffer if block_given?
|
|
81
90
|
end
|
|
82
91
|
end
|
|
83
92
|
|
|
@@ -95,6 +104,30 @@ module Rpremote
|
|
|
95
104
|
response.byteslice((output_at + 1)...prompt_at) || +"".b
|
|
96
105
|
end
|
|
97
106
|
|
|
107
|
+
def stream_output(output, response, echo, state)
|
|
108
|
+
return unless output
|
|
109
|
+
|
|
110
|
+
echo_at = response.rindex(echo)
|
|
111
|
+
return unless echo_at
|
|
112
|
+
|
|
113
|
+
output_at = response.index("\n", echo_at + echo.bytesize)
|
|
114
|
+
return unless output_at
|
|
115
|
+
|
|
116
|
+
output_at += 1
|
|
117
|
+
prompt_at = response.index(PROMPT_START, output_at)
|
|
118
|
+
safe_end = prompt_at || (response.rindex("\n", response.bytesize - 1).to_i + 1)
|
|
119
|
+
position = state.fetch(:position, output_at)
|
|
120
|
+
return if safe_end <= position
|
|
121
|
+
|
|
122
|
+
output.write(remove_ruby_exception_status(response.byteslice(position...safe_end)))
|
|
123
|
+
output.flush if output.respond_to?(:flush)
|
|
124
|
+
state[:position] = safe_end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def remove_ruby_exception_status(data)
|
|
128
|
+
data.gsub(RUBY_EXCEPTION_STATUS, "")
|
|
129
|
+
end
|
|
130
|
+
|
|
98
131
|
def drain_input
|
|
99
132
|
deadline = monotonic_time + 0.1
|
|
100
133
|
buffer = +"".b
|
|
@@ -114,9 +147,7 @@ module Rpremote
|
|
|
114
147
|
def read_available(deadline)
|
|
115
148
|
remaining = deadline - monotonic_time
|
|
116
149
|
raise TimeoutError, "timed out waiting for the R2P2 Shell after #{timeout} seconds" unless remaining.positive?
|
|
117
|
-
unless selectable_io.wait_readable(remaining)
|
|
118
|
-
raise TimeoutError, "timed out waiting for the R2P2 Shell after #{timeout} seconds"
|
|
119
|
-
end
|
|
150
|
+
raise TimeoutError, "timed out waiting for the R2P2 Shell after #{timeout} seconds" unless selectable_io.wait_readable(remaining)
|
|
120
151
|
|
|
121
152
|
data = io.read_nonblock(4096)
|
|
122
153
|
raise IOError, "serial connection closed" if data.nil? || data.empty?
|
data/lib/rpremote/version.rb
CHANGED
data/lib/rpremote.rb
CHANGED
|
@@ -19,6 +19,7 @@ require_relative "rpremote/terminal"
|
|
|
19
19
|
require_relative "rpremote/shell"
|
|
20
20
|
require_relative "rpremote/runner"
|
|
21
21
|
require_relative "rpremote/resetter"
|
|
22
|
+
require_relative "rpremote/picoruby_source_patch"
|
|
22
23
|
require_relative "rpremote/language_source"
|
|
23
24
|
require_relative "rpremote/mrbgems"
|
|
24
25
|
require_relative "rpremote/mrbgems_command"
|