rpremote 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -19
- data/README.ja.md +58 -34
- data/README.md +56 -33
- data/RELEASING.md +23 -23
- data/THIRD_PARTY_NOTICES.md +10 -20
- data/lib/rpremote/bootsel_command.rb +124 -0
- data/lib/rpremote/builder.rb +9 -2
- data/lib/rpremote/cli.rb +74 -22
- data/lib/rpremote/config.rb +3 -0
- data/lib/rpremote/config_show.rb +91 -0
- data/lib/rpremote/deploy_command.rb +191 -0
- data/lib/rpremote/dfu_command.rb +69 -7
- data/lib/rpremote/flash_command.rb +9 -3
- data/lib/rpremote/flasher.rb +50 -12
- data/lib/rpremote/help.rb +309 -29
- data/lib/rpremote/language_source.rb +4 -1
- data/lib/rpremote/mrbgems.rb +41 -12
- data/lib/rpremote/nuke_firmware.rb +71 -0
- data/lib/rpremote/picomodem.rb +3 -7
- data/lib/rpremote/picoruby_source_patch.rb +49 -0
- data/lib/rpremote/recursive_copy.rb +84 -0
- data/lib/rpremote/resetter.rb +1 -1
- data/lib/rpremote/runner.rb +33 -6
- data/lib/rpremote/setup_command.rb +4 -1
- data/lib/rpremote/shell.rb +45 -10
- data/lib/rpremote/terminal.rb +13 -1
- data/lib/rpremote/version.rb +1 -1
- data/lib/rpremote.rb +5 -0
- data/patches/picoruby-3.4.5-bootsel.patch +71 -0
- data/patches/picoruby-3.4.5-ruby-exception-status.patch +59 -0
- data/patches/picoruby-4.0.3-bootsel.patch +71 -0
- data/patches/picoruby-4.0.3-ruby-exception-status.patch +59 -0
- data/sig/rpremote.rbs +67 -5
- data/tasks/firmware_build.rb +2 -8
- data/tasks/release_check.rb +3 -7
- metadata +11 -1
|
@@ -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
|
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,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rpremote
|
|
4
|
+
class PicoRubySourcePatch
|
|
5
|
+
JOB_PATH = "mrbgems/picoruby-shell/mrblib/job.rb"
|
|
6
|
+
BOOTSEL_PATH = "mrbgems/picoruby-machine/include/machine.h"
|
|
7
|
+
PATCH_ALIASES = { "3.4.2" => "3.4.5" }.freeze
|
|
8
|
+
|
|
9
|
+
class Error < Rpremote::Error; end
|
|
10
|
+
|
|
11
|
+
attr_reader :version
|
|
12
|
+
|
|
13
|
+
def initialize(version:)
|
|
14
|
+
@version = version
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def apply(source)
|
|
18
|
+
return unless Dir.exist?(source)
|
|
19
|
+
|
|
20
|
+
patch_version = PATCH_ALIASES.fetch(version, version)
|
|
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")
|
|
26
|
+
|
|
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
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
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
|
+
|
|
45
|
+
def git_apply?(source, patch, *options)
|
|
46
|
+
system("git", "apply", "--unidiff-zero", *options, patch, chdir: source, out: File::NULL, err: File::NULL)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rpremote
|
|
4
|
+
class RecursiveCopy
|
|
5
|
+
def initialize(output:, serial: Serial, device: Device)
|
|
6
|
+
@output = output
|
|
7
|
+
@serial = serial
|
|
8
|
+
@device = device
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call(source, destination, options)
|
|
12
|
+
validate!(source, destination)
|
|
13
|
+
remote_root = RemotePath.validate(destination)
|
|
14
|
+
directories, files = collect(source, remote_root)
|
|
15
|
+
create_directories(directories, options)
|
|
16
|
+
upload_files(files, options)
|
|
17
|
+
output.puts("uploaded #{files.length} files: #{source} -> #{destination}")
|
|
18
|
+
rescue Errno::ENOENT => e
|
|
19
|
+
raise ArgumentError, e.message
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :output, :serial, :device
|
|
25
|
+
|
|
26
|
+
def validate!(source, destination)
|
|
27
|
+
raise ArgumentError, "recursive cp only supports a local source and remote destination" if RemotePath.remote?(source)
|
|
28
|
+
raise ArgumentError, "recursive cp source must be a directory: #{source}" unless File.directory?(source)
|
|
29
|
+
|
|
30
|
+
RemotePath.validate(destination)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def collect(source, remote_root)
|
|
34
|
+
directories = [remote_root]
|
|
35
|
+
files = []
|
|
36
|
+
local_entries(source).each do |path|
|
|
37
|
+
remote_path = File.join(remote_root, path.delete_prefix("#{source}/"))
|
|
38
|
+
File.directory?(path) ? directories << remote_path : files << [path, remote_path]
|
|
39
|
+
end
|
|
40
|
+
[directories.sort_by { |path| [path.count("/"), path] }.uniq, files.sort]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def local_entries(source)
|
|
44
|
+
entries = Dir.glob(File.join(source, "**", "*"), File::FNM_DOTMATCH).reject do |path|
|
|
45
|
+
%w[. ..].include?(File.basename(path))
|
|
46
|
+
end
|
|
47
|
+
symbolic_link = entries.find { |path| File.symlink?(path) }
|
|
48
|
+
raise ArgumentError, "recursive cp does not support symbolic links: #{symbolic_link}" if symbolic_link
|
|
49
|
+
|
|
50
|
+
entries
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def create_directories(directories, options)
|
|
54
|
+
with_port(options) do |port|
|
|
55
|
+
shell = Shell.new(port, timeout: options[:timeout])
|
|
56
|
+
shell.synchronize!
|
|
57
|
+
directories.each do |path|
|
|
58
|
+
quoted_path = Shell.quote_argument(path)
|
|
59
|
+
next unless shell.execute("ls #{quoted_path}").start_with?("ls:")
|
|
60
|
+
|
|
61
|
+
result = shell.execute("mkdir #{quoted_path}")
|
|
62
|
+
raise Shell::CommandError, result.strip unless result.empty?
|
|
63
|
+
|
|
64
|
+
output.puts("created remote directory: :#{path}")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def upload_files(files, options)
|
|
70
|
+
with_port(options) do |port|
|
|
71
|
+
modem = PicoModem.new(port, timeout: options[:timeout])
|
|
72
|
+
files.each do |local_path, remote_path|
|
|
73
|
+
size = modem.upload(remote_path, File.binread(local_path))
|
|
74
|
+
output.puts("uploaded #{size} bytes: #{local_path} -> :#{remote_path}")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def with_port(options, &)
|
|
80
|
+
port_path = device.main_port(options[:port])
|
|
81
|
+
serial.open(port_path, baud: options[:baud], &)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
data/lib/rpremote/resetter.rb
CHANGED
data/lib/rpremote/runner.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module Rpremote
|
|
4
4
|
class Runner
|
|
5
5
|
REMOTE_DIRECTORY = "/home"
|
|
6
|
+
RUN_REMOTE_PATH = "#{REMOTE_DIRECTORY}/.rpremote-run.rb".freeze
|
|
7
|
+
DEPLOY_REMOTE_PATH = "#{REMOTE_DIRECTORY}/.rpremote-deploy.rb".freeze
|
|
6
8
|
|
|
7
9
|
attr_reader :io, :timeout
|
|
8
10
|
|
|
@@ -20,16 +22,33 @@ module Rpremote
|
|
|
20
22
|
@path_factory = path_factory || method(:temporary_path)
|
|
21
23
|
end
|
|
22
24
|
|
|
23
|
-
def run(data)
|
|
24
|
-
remote_path
|
|
25
|
+
def run(data, output: nil, cleanup: true, remote_path: nil, diagnostics: nil)
|
|
26
|
+
remote_path ||= @path_factory.call
|
|
25
27
|
shell = @shell_class.new(io, timeout: timeout)
|
|
28
|
+
trace(diagnostics, "SYNC_BEFORE_UPLOAD_START")
|
|
26
29
|
shell.synchronize!
|
|
30
|
+
trace(diagnostics, "SYNC_BEFORE_UPLOAD_DONE")
|
|
31
|
+
trace(diagnostics, "UPLOAD_START", "bytes=#{data.bytesize},path=#{remote_path}")
|
|
27
32
|
@modem_class.new(io, timeout: timeout).upload(remote_path, data)
|
|
33
|
+
trace(diagnostics, "UPLOAD_DONE")
|
|
34
|
+
trace(diagnostics, "SYNC_BEFORE_RUN_START")
|
|
28
35
|
shell.synchronize!
|
|
36
|
+
trace(diagnostics, "SYNC_BEFORE_RUN_DONE")
|
|
29
37
|
shell_ready = true
|
|
30
|
-
|
|
38
|
+
command = "./#{File.basename(remote_path)}"
|
|
39
|
+
trace(diagnostics, "EXECUTE_START", "command=#{command}")
|
|
40
|
+
result = output ? shell.execute(command, output: output, idle_timeout: true) : shell.execute(command, idle_timeout: true)
|
|
41
|
+
trace(diagnostics, "EXECUTE_DONE", "bytes=#{result.to_s.bytesize}")
|
|
42
|
+
result
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
trace(diagnostics, "ERROR", "class=#{e.class},message=#{e.message.to_s.gsub("\n", " ")}")
|
|
45
|
+
raise
|
|
31
46
|
ensure
|
|
32
|
-
cleanup
|
|
47
|
+
if cleanup && shell_ready && remote_path
|
|
48
|
+
trace(diagnostics, "CLEANUP_START", "path=#{remote_path}")
|
|
49
|
+
cleanup(shell, remote_path)
|
|
50
|
+
trace(diagnostics, "CLEANUP_DONE")
|
|
51
|
+
end
|
|
33
52
|
end
|
|
34
53
|
|
|
35
54
|
private
|
|
@@ -40,9 +59,17 @@ module Rpremote
|
|
|
40
59
|
nil
|
|
41
60
|
end
|
|
42
61
|
|
|
62
|
+
def trace(diagnostics, event, details = nil)
|
|
63
|
+
return unless diagnostics
|
|
64
|
+
|
|
65
|
+
line = "rpremote: run event=#{event}"
|
|
66
|
+
line += ",#{details}" if details
|
|
67
|
+
diagnostics.puts(line)
|
|
68
|
+
diagnostics.flush if diagnostics.respond_to?(:flush)
|
|
69
|
+
end
|
|
70
|
+
|
|
43
71
|
def temporary_path
|
|
44
|
-
|
|
45
|
-
"#{REMOTE_DIRECTORY}/.rpremote-run-#{Process.pid}-#{stamp}.rb"
|
|
72
|
+
RUN_REMOTE_PATH
|
|
46
73
|
end
|
|
47
74
|
end
|
|
48
75
|
end
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "optparse"
|
|
4
|
+
require_relative "nuke_firmware"
|
|
4
5
|
|
|
5
6
|
module Rpremote
|
|
6
7
|
class SetupCommand
|
|
7
|
-
def self.run(args, defaults:, config:, config_filename:, output: $stdout)
|
|
8
|
+
def self.run(args, defaults:, config:, config_filename:, output: $stdout, nuke_firmware: NukeFirmware)
|
|
8
9
|
options = {
|
|
9
10
|
force: false,
|
|
10
11
|
cache_dir: defaults.fetch(:cache, Target::DEFAULT_CACHE_DIR),
|
|
@@ -29,6 +30,8 @@ module Rpremote
|
|
|
29
30
|
cache_dir: target.cache_dir
|
|
30
31
|
).setup(force: options[:force])
|
|
31
32
|
output.puts("installed #{target.language} #{target.language_version}: #{path}")
|
|
33
|
+
nuke_path = nuke_firmware.new.setup(force: options[:force])
|
|
34
|
+
output.puts("installed Raspberry Pi nuke firmware: #{nuke_path}")
|
|
32
35
|
end
|
|
33
36
|
end
|
|
34
37
|
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
|
|
@@ -28,11 +29,12 @@ module Rpremote
|
|
|
28
29
|
|
|
29
30
|
attr_reader :io, :timeout
|
|
30
31
|
|
|
31
|
-
def initialize(io, timeout: DEFAULT_TIMEOUT)
|
|
32
|
+
def initialize(io, timeout: DEFAULT_TIMEOUT, clock: nil)
|
|
32
33
|
raise ArgumentError, "timeout must be positive" unless timeout.positive?
|
|
33
34
|
|
|
34
35
|
@io = io
|
|
35
36
|
@timeout = timeout
|
|
37
|
+
@clock = clock || -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
def synchronize!
|
|
@@ -42,11 +44,20 @@ module Rpremote
|
|
|
42
44
|
nil
|
|
43
45
|
end
|
|
44
46
|
|
|
45
|
-
def execute(command)
|
|
47
|
+
def execute(command, output: nil, idle_timeout: false)
|
|
46
48
|
command = validate_command(command)
|
|
47
49
|
send_command(command)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
echo = ECHO_START + command.b + ERASE_LINE
|
|
51
|
+
stream_state = {}
|
|
52
|
+
response = read_until_prompt(after: echo, idle_timeout: idle_timeout) do |buffer|
|
|
53
|
+
stream_output(output, buffer, echo, stream_state)
|
|
54
|
+
end
|
|
55
|
+
command_output = extract_output(response, command)
|
|
56
|
+
ruby_exception = command_output.include?(RUBY_EXCEPTION_STATUS)
|
|
57
|
+
command_output = remove_ruby_exception_status(command_output)
|
|
58
|
+
raise CommandError, "Ruby exception reported by R2P2" if ruby_exception
|
|
59
|
+
|
|
60
|
+
command_output
|
|
50
61
|
rescue TimeoutError
|
|
51
62
|
interrupt
|
|
52
63
|
raise
|
|
@@ -68,7 +79,7 @@ module Rpremote
|
|
|
68
79
|
command
|
|
69
80
|
end
|
|
70
81
|
|
|
71
|
-
def read_until_prompt(wait: timeout, after: nil)
|
|
82
|
+
def read_until_prompt(wait: timeout, after: nil, idle_timeout: false)
|
|
72
83
|
deadline = monotonic_time + wait
|
|
73
84
|
buffer = +"".b
|
|
74
85
|
loop do
|
|
@@ -77,7 +88,9 @@ module Rpremote
|
|
|
77
88
|
return buffer if prompt_at && buffer.index(PROMPT_END, prompt_at + PROMPT_START.bytesize)
|
|
78
89
|
|
|
79
90
|
buffer << read_available(deadline)
|
|
91
|
+
deadline = monotonic_time + wait if idle_timeout
|
|
80
92
|
answer_terminal_queries(buffer)
|
|
93
|
+
yield buffer if block_given?
|
|
81
94
|
end
|
|
82
95
|
end
|
|
83
96
|
|
|
@@ -95,6 +108,30 @@ module Rpremote
|
|
|
95
108
|
response.byteslice((output_at + 1)...prompt_at) || +"".b
|
|
96
109
|
end
|
|
97
110
|
|
|
111
|
+
def stream_output(output, response, echo, state)
|
|
112
|
+
return unless output
|
|
113
|
+
|
|
114
|
+
echo_at = response.rindex(echo)
|
|
115
|
+
return unless echo_at
|
|
116
|
+
|
|
117
|
+
output_at = response.index("\n", echo_at + echo.bytesize)
|
|
118
|
+
return unless output_at
|
|
119
|
+
|
|
120
|
+
output_at += 1
|
|
121
|
+
prompt_at = response.index(PROMPT_START, output_at)
|
|
122
|
+
safe_end = prompt_at || (response.rindex("\n", response.bytesize - 1).to_i + 1)
|
|
123
|
+
position = state.fetch(:position, output_at)
|
|
124
|
+
return if safe_end <= position
|
|
125
|
+
|
|
126
|
+
output.write(remove_ruby_exception_status(response.byteslice(position...safe_end)))
|
|
127
|
+
output.flush if output.respond_to?(:flush)
|
|
128
|
+
state[:position] = safe_end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def remove_ruby_exception_status(data)
|
|
132
|
+
data.gsub(RUBY_EXCEPTION_STATUS, "")
|
|
133
|
+
end
|
|
134
|
+
|
|
98
135
|
def drain_input
|
|
99
136
|
deadline = monotonic_time + 0.1
|
|
100
137
|
buffer = +"".b
|
|
@@ -114,9 +151,7 @@ module Rpremote
|
|
|
114
151
|
def read_available(deadline)
|
|
115
152
|
remaining = deadline - monotonic_time
|
|
116
153
|
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
|
|
154
|
+
raise TimeoutError, "timed out waiting for the R2P2 Shell after #{timeout} seconds" unless selectable_io.wait_readable(remaining)
|
|
120
155
|
|
|
121
156
|
data = io.read_nonblock(4096)
|
|
122
157
|
raise IOError, "serial connection closed" if data.nil? || data.empty?
|
|
@@ -155,7 +190,7 @@ module Rpremote
|
|
|
155
190
|
end
|
|
156
191
|
|
|
157
192
|
def monotonic_time
|
|
158
|
-
|
|
193
|
+
@clock.call
|
|
159
194
|
end
|
|
160
195
|
end
|
|
161
196
|
end
|
data/lib/rpremote/terminal.rb
CHANGED
|
@@ -49,7 +49,7 @@ module Rpremote
|
|
|
49
49
|
data = serial_io.read_nonblock(BUFFER_SIZE)
|
|
50
50
|
return false if data.nil? || data.empty?
|
|
51
51
|
|
|
52
|
-
output.write(data)
|
|
52
|
+
output.write(normalize_line_endings(data))
|
|
53
53
|
output.flush if output.respond_to?(:flush)
|
|
54
54
|
true
|
|
55
55
|
else
|
|
@@ -75,6 +75,18 @@ module Rpremote
|
|
|
75
75
|
true
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
# input.raw disables the terminal's usual LF-to-CRLF output conversion.
|
|
79
|
+
# Preserve device CRLF while making a bare LF start at the left margin.
|
|
80
|
+
def normalize_line_endings(data)
|
|
81
|
+
normalized = +"".b
|
|
82
|
+
data.each_byte do |byte|
|
|
83
|
+
normalized << "\r" if byte == 0x0A && !@previous_output_was_cr
|
|
84
|
+
normalized << byte
|
|
85
|
+
@previous_output_was_cr = byte == 0x0D
|
|
86
|
+
end
|
|
87
|
+
normalized
|
|
88
|
+
end
|
|
89
|
+
|
|
78
90
|
def selectable_serial
|
|
79
91
|
@selectable_serial ||= serial_io.respond_to?(:to_io) ? serial_io.to_io : serial_io
|
|
80
92
|
end
|
data/lib/rpremote/version.rb
CHANGED
data/lib/rpremote.rb
CHANGED
|
@@ -19,7 +19,10 @@ 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/recursive_copy"
|
|
23
|
+
require_relative "rpremote/picoruby_source_patch"
|
|
22
24
|
require_relative "rpremote/language_source"
|
|
25
|
+
require_relative "rpremote/nuke_firmware"
|
|
23
26
|
require_relative "rpremote/mrbgems"
|
|
24
27
|
require_relative "rpremote/mrbgems_command"
|
|
25
28
|
require_relative "rpremote/flasher"
|
|
@@ -28,3 +31,5 @@ require_relative "rpremote/dfu_command"
|
|
|
28
31
|
require_relative "rpremote/setup_command"
|
|
29
32
|
require_relative "rpremote/build_command"
|
|
30
33
|
require_relative "rpremote/flash_command"
|
|
34
|
+
require_relative "rpremote/bootsel_command"
|
|
35
|
+
require_relative "rpremote/deploy_command"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
diff --git a/mrbgems/picoruby-machine/include/machine.h b/mrbgems/picoruby-machine/include/machine.h
|
|
2
|
+
--- a/mrbgems/picoruby-machine/include/machine.h
|
|
3
|
+
+++ b/mrbgems/picoruby-machine/include/machine.h
|
|
4
|
+
@@ -48,1 +48,2 @@
|
|
5
|
+
void Machine_reboot(void);
|
|
6
|
+
+void Machine_bootsel(void);
|
|
7
|
+
diff --git a/mrbgems/picoruby-machine/mrblib/machine.rb b/mrbgems/picoruby-machine/mrblib/machine.rb
|
|
8
|
+
--- a/mrbgems/picoruby-machine/mrblib/machine.rb
|
|
9
|
+
+++ b/mrbgems/picoruby-machine/mrblib/machine.rb
|
|
10
|
+
@@ -22,0 +23,4 @@
|
|
11
|
+
+ def self.bootsel
|
|
12
|
+
+ self._bootsel
|
|
13
|
+
+ end
|
|
14
|
+
+
|
|
15
|
+
diff --git a/mrbgems/picoruby-machine/ports/rp2040/machine.c b/mrbgems/picoruby-machine/ports/rp2040/machine.c
|
|
16
|
+
--- a/mrbgems/picoruby-machine/ports/rp2040/machine.c
|
|
17
|
+
+++ b/mrbgems/picoruby-machine/ports/rp2040/machine.c
|
|
18
|
+
@@ -12,0 +13,1 @@
|
|
19
|
+
+#include "pico/bootrom.h"
|
|
20
|
+
@@ -32,0 +34,6 @@
|
|
21
|
+
+void
|
|
22
|
+
+Machine_bootsel(void)
|
|
23
|
+
+{
|
|
24
|
+
+ rom_reset_usb_boot(0, 0);
|
|
25
|
+
+}
|
|
26
|
+
+
|
|
27
|
+
diff --git a/mrbgems/picoruby-machine/src/mruby/machine.c b/mrbgems/picoruby-machine/src/mruby/machine.c
|
|
28
|
+
--- a/mrbgems/picoruby-machine/src/mruby/machine.c
|
|
29
|
+
+++ b/mrbgems/picoruby-machine/src/mruby/machine.c
|
|
30
|
+
@@ -417,0 +418,11 @@
|
|
31
|
+
+static mrb_value
|
|
32
|
+
+mrb_s__bootsel(mrb_state *mrb, mrb_value self)
|
|
33
|
+
+{
|
|
34
|
+
+#if !defined(PICORB_PLATFORM_POSIX)
|
|
35
|
+
+ Machine_bootsel();
|
|
36
|
+
+#else
|
|
37
|
+
+ mrb_raise(mrb, E_NOTIMP_ERROR, "Machine._bootsel is not available on this platform");
|
|
38
|
+
+#endif
|
|
39
|
+
+ return mrb_nil_value();
|
|
40
|
+
+}
|
|
41
|
+
+
|
|
42
|
+
@@ -443,0 +455,1 @@
|
|
43
|
+
+ mrb_define_class_method_id(mrb, module_Machine, MRB_SYM(_bootsel), mrb_s__bootsel, MRB_ARGS_NONE());
|
|
44
|
+
diff --git a/mrbgems/picoruby-machine/src/mrubyc/machine.c b/mrbgems/picoruby-machine/src/mrubyc/machine.c
|
|
45
|
+
--- a/mrbgems/picoruby-machine/src/mrubyc/machine.c
|
|
46
|
+
+++ b/mrbgems/picoruby-machine/src/mrubyc/machine.c
|
|
47
|
+
@@ -252,0 +253,11 @@
|
|
48
|
+
+static void
|
|
49
|
+
+c_Machine__bootsel(mrbc_vm *vm, mrbc_value *v, int argc)
|
|
50
|
+
+{
|
|
51
|
+
+#if !defined(PICORB_PLATFORM_POSIX)
|
|
52
|
+
+ Machine_bootsel();
|
|
53
|
+
+#else
|
|
54
|
+
+ mrbc_raise(vm, MRBC_CLASS(RuntimeError), "Machine.bootsel is not available on this platform.");
|
|
55
|
+
+#endif
|
|
56
|
+
+ SET_NIL_RETURN();
|
|
57
|
+
+}
|
|
58
|
+
+
|
|
59
|
+
@@ -439,0 +451,1 @@
|
|
60
|
+
+ mrbc_define_method(vm, module_Machine, "_bootsel", c_Machine__bootsel);
|
|
61
|
+
diff --git a/mrbgems/picoruby-shell/shell_executables/_path.txt b/mrbgems/picoruby-shell/shell_executables/_path.txt
|
|
62
|
+
--- a/mrbgems/picoruby-shell/shell_executables/_path.txt
|
|
63
|
+
+++ b/mrbgems/picoruby-shell/shell_executables/_path.txt
|
|
64
|
+
@@ -16,0 +17,1 @@
|
|
65
|
+
+/bin/bootsel
|
|
66
|
+
diff --git a/mrbgems/picoruby-shell/shell_executables/bootsel.rb b/mrbgems/picoruby-shell/shell_executables/bootsel.rb
|
|
67
|
+
new file mode 100644
|
|
68
|
+
--- /dev/null
|
|
69
|
+
+++ b/mrbgems/picoruby-shell/shell_executables/bootsel.rb
|
|
70
|
+
@@ -0,0 +1 @@
|
|
71
|
+
+Machine.bootsel
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
diff --git a/mrbgems/picoruby-shell/mrblib/job.rb b/mrbgems/picoruby-shell/mrblib/job.rb
|
|
2
|
+
index 9351ee0..91e7e74 100644
|
|
3
|
+
--- a/mrbgems/picoruby-shell/mrblib/job.rb
|
|
4
|
+
+++ b/mrbgems/picoruby-shell/mrblib/job.rb
|
|
5
|
+
@@ -1,5 +1,7 @@
|
|
6
|
+
class Shell
|
|
7
|
+
class Job
|
|
8
|
+
+ RUBY_EXCEPTION_STATUS = "\x1eR2P2:RUBY_EXCEPTION\x1f"
|
|
9
|
+
+
|
|
10
|
+
def initialize(*params)
|
|
11
|
+
if params.empty?
|
|
12
|
+
raise ArgumentError, "Job requires at least one parameter"
|
|
13
|
+
@@ -24,12 +26,11 @@ class Shell
|
|
14
|
+
@sandbox.resume
|
|
15
|
+
@sandbox.wait(timeout: nil)
|
|
16
|
+
if error = @sandbox.error
|
|
17
|
+
- puts "#{error.message} (#{error.class})"
|
|
18
|
+
+ return report_ruby_exception(error)
|
|
19
|
+
end
|
|
20
|
+
return true
|
|
21
|
+
rescue Exception => e
|
|
22
|
+
- puts "#{e.message} (#{e.class})"
|
|
23
|
+
- return false
|
|
24
|
+
+ return report_ruby_exception(e)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def state
|
|
28
|
+
@@ -46,22 +47,28 @@ class Shell
|
|
29
|
+
trap
|
|
30
|
+
@sandbox.load_file(@exefile)
|
|
31
|
+
if error = @sandbox.error
|
|
32
|
+
- puts "\n#{error.message} (#{error.class})"
|
|
33
|
+
+ return report_ruby_exception(error, true)
|
|
34
|
+
end
|
|
35
|
+
return true
|
|
36
|
+
rescue Exception => e
|
|
37
|
+
- puts "#{e.message} (#{e.class})"
|
|
38
|
+
# backtrace uses a lot of memory
|
|
39
|
+
#if e.respond_to?(:backtrace)
|
|
40
|
+
# e.backtrace.each do |line|
|
|
41
|
+
# puts " #{line}"
|
|
42
|
+
# end
|
|
43
|
+
#end
|
|
44
|
+
- return false
|
|
45
|
+
+ return report_ruby_exception(e)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
+ def report_ruby_exception(error, leading_newline = false)
|
|
51
|
+
+ print "\n" if leading_newline
|
|
52
|
+
+ puts "#{error.message} (#{error.class})"
|
|
53
|
+
+ print RUBY_EXCEPTION_STATUS
|
|
54
|
+
+ false
|
|
55
|
+
+ end
|
|
56
|
+
+
|
|
57
|
+
def trap
|
|
58
|
+
Signal.trap(:TSTP) do
|
|
59
|
+
@sandbox.suspend
|