rpremote 0.1.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 +7 -0
- data/CHANGELOG.md +24 -0
- data/LICENSE +21 -0
- data/README.ja.md +122 -0
- data/README.md +124 -0
- data/RELEASING.md +54 -0
- data/THIRD_PARTY_NOTICES.md +29 -0
- data/exe/rpremote +7 -0
- data/lib/rpremote/build_command.rb +38 -0
- data/lib/rpremote/builder.rb +123 -0
- data/lib/rpremote/checksum.rb +27 -0
- data/lib/rpremote/cli.rb +267 -0
- data/lib/rpremote/config.rb +137 -0
- data/lib/rpremote/device.rb +56 -0
- data/lib/rpremote/dfu_command.rb +155 -0
- data/lib/rpremote/dfu_compiler.rb +50 -0
- data/lib/rpremote/flash_command.rb +45 -0
- data/lib/rpremote/flasher.rb +131 -0
- data/lib/rpremote/help.rb +60 -0
- data/lib/rpremote/language.rb +15 -0
- data/lib/rpremote/language_source.rb +126 -0
- data/lib/rpremote/mrbgems.rb +288 -0
- data/lib/rpremote/mrbgems_command.rb +48 -0
- data/lib/rpremote/picomodem.rb +293 -0
- data/lib/rpremote/remote_path.rb +27 -0
- data/lib/rpremote/resetter.rb +73 -0
- data/lib/rpremote/runner.rb +48 -0
- data/lib/rpremote/serial.rb +72 -0
- data/lib/rpremote/setup_command.rb +34 -0
- data/lib/rpremote/shell.rb +161 -0
- data/lib/rpremote/target.rb +46 -0
- data/lib/rpremote/terminal.rb +91 -0
- data/lib/rpremote/version.rb +5 -0
- data/lib/rpremote.rb +30 -0
- data/sig/rpremote.rbs +404 -0
- data/tasks/firmware_build.rb +108 -0
- data/tasks/release.rake +12 -0
- data/tasks/release_check.rb +83 -0
- metadata +82 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/wait"
|
|
4
|
+
|
|
5
|
+
module Rpremote
|
|
6
|
+
class Shell
|
|
7
|
+
DEFAULT_TIMEOUT = 10.0
|
|
8
|
+
PROMPT_START = "\e[?25l\e[1G$> \e[0K".b
|
|
9
|
+
PROMPT_END = "\e[?25h".b
|
|
10
|
+
ECHO_START = "\e[?25l\e[1G$> ".b
|
|
11
|
+
ERASE_LINE = "\e[0K".b
|
|
12
|
+
CURSOR_POSITION_QUERY = "\e[6n".b
|
|
13
|
+
CURSOR_POSITION_RESPONSE = "\e[1;1R".b
|
|
14
|
+
|
|
15
|
+
class Error < Rpremote::Error; end
|
|
16
|
+
class TimeoutError < Error; end
|
|
17
|
+
class ProtocolError < Error; end
|
|
18
|
+
class CommandError < Error; end
|
|
19
|
+
|
|
20
|
+
def self.quote_argument(argument)
|
|
21
|
+
argument = String(argument)
|
|
22
|
+
raise ArgumentError, "shell argument contains a control character" if argument.match?(/[\x00-\x1f\x7f]/)
|
|
23
|
+
return "'#{argument}'" unless argument.include?("'")
|
|
24
|
+
return %("#{argument}") unless argument.include?('"')
|
|
25
|
+
|
|
26
|
+
raise ArgumentError, "shell argument cannot contain both quote characters"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
attr_reader :io, :timeout
|
|
30
|
+
|
|
31
|
+
def initialize(io, timeout: DEFAULT_TIMEOUT)
|
|
32
|
+
raise ArgumentError, "timeout must be positive" unless timeout.positive?
|
|
33
|
+
|
|
34
|
+
@io = io
|
|
35
|
+
@timeout = timeout
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def synchronize!
|
|
39
|
+
drain_input
|
|
40
|
+
write_all("\r".b)
|
|
41
|
+
read_until_prompt
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def execute(command)
|
|
46
|
+
command = validate_command(command)
|
|
47
|
+
send_command(command)
|
|
48
|
+
response = read_until_prompt(after: ECHO_START + command.b + ERASE_LINE)
|
|
49
|
+
extract_output(response, command)
|
|
50
|
+
rescue TimeoutError
|
|
51
|
+
interrupt
|
|
52
|
+
raise
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def send_command(command)
|
|
56
|
+
command = validate_command(command)
|
|
57
|
+
write_all("#{command}\r".b)
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def validate_command(command)
|
|
64
|
+
command = String(command)
|
|
65
|
+
raise ArgumentError, "shell command must not be empty" if command.empty?
|
|
66
|
+
raise ArgumentError, "shell command contains a control character" if command.match?(/[\x00-\x1f\x7f]/)
|
|
67
|
+
|
|
68
|
+
command
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def read_until_prompt(wait: timeout, after: nil)
|
|
72
|
+
deadline = monotonic_time + wait
|
|
73
|
+
buffer = +"".b
|
|
74
|
+
loop do
|
|
75
|
+
boundary = after ? buffer.index(after) : 0
|
|
76
|
+
prompt_at = buffer.index(PROMPT_START, boundary + (after&.bytesize || 0)) if boundary
|
|
77
|
+
return buffer if prompt_at && buffer.index(PROMPT_END, prompt_at + PROMPT_START.bytesize)
|
|
78
|
+
|
|
79
|
+
buffer << read_available(deadline)
|
|
80
|
+
answer_terminal_queries(buffer)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def extract_output(response, command)
|
|
85
|
+
echo = ECHO_START + command.b + ERASE_LINE
|
|
86
|
+
echo_at = response.rindex(echo)
|
|
87
|
+
raise ProtocolError, "R2P2 Shell did not echo the command" unless echo_at
|
|
88
|
+
|
|
89
|
+
prompt_at = response.index(PROMPT_START, echo_at + echo.bytesize)
|
|
90
|
+
raise ProtocolError, "R2P2 Shell prompt was not found after the command" unless prompt_at
|
|
91
|
+
|
|
92
|
+
output_at = response.index("\n", echo_at + echo.bytesize)
|
|
93
|
+
raise ProtocolError, "R2P2 Shell response has no command boundary" unless output_at
|
|
94
|
+
|
|
95
|
+
response.byteslice((output_at + 1)...prompt_at) || +"".b
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def drain_input
|
|
99
|
+
deadline = monotonic_time + 0.1
|
|
100
|
+
buffer = +"".b
|
|
101
|
+
loop do
|
|
102
|
+
remaining = deadline - monotonic_time
|
|
103
|
+
break unless remaining.positive? && selectable_io.wait_readable(remaining)
|
|
104
|
+
|
|
105
|
+
buffer << io.read_nonblock(4096)
|
|
106
|
+
rescue IO::WaitReadable
|
|
107
|
+
next
|
|
108
|
+
rescue EOFError
|
|
109
|
+
raise IOError, "serial connection closed"
|
|
110
|
+
end
|
|
111
|
+
answer_terminal_queries(buffer)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def read_available(deadline)
|
|
115
|
+
remaining = deadline - monotonic_time
|
|
116
|
+
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
|
|
120
|
+
|
|
121
|
+
data = io.read_nonblock(4096)
|
|
122
|
+
raise IOError, "serial connection closed" if data.nil? || data.empty?
|
|
123
|
+
|
|
124
|
+
data
|
|
125
|
+
rescue IO::WaitReadable
|
|
126
|
+
retry
|
|
127
|
+
rescue EOFError
|
|
128
|
+
raise IOError, "serial connection closed"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def interrupt
|
|
132
|
+
write_all("\x03".b)
|
|
133
|
+
read_until_prompt(wait: [timeout, 1.0].min)
|
|
134
|
+
rescue IOError, SystemCallError, Error
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def answer_terminal_queries(buffer)
|
|
139
|
+
write_all(CURSOR_POSITION_RESPONSE) while buffer.sub!(CURSOR_POSITION_QUERY, "")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def write_all(data)
|
|
143
|
+
offset = 0
|
|
144
|
+
while offset < data.bytesize
|
|
145
|
+
written = io.write(data.byteslice(offset..))
|
|
146
|
+
raise IOError, "serial connection closed while writing" unless written&.positive?
|
|
147
|
+
|
|
148
|
+
offset += written
|
|
149
|
+
end
|
|
150
|
+
io.flush if io.respond_to?(:flush)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def selectable_io
|
|
154
|
+
io.respond_to?(:to_io) ? io.to_io : io
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def monotonic_time
|
|
158
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rpremote
|
|
4
|
+
class Target
|
|
5
|
+
DEFAULT_LANGUAGE = "picoruby"
|
|
6
|
+
DEFAULT_LANGUAGE_VERSION = "4.0.3"
|
|
7
|
+
DEFAULT_BOARD = "pico2"
|
|
8
|
+
DEFAULT_CACHE_DIR = "firmware"
|
|
9
|
+
IDENTIFIER_PATTERN = /\A[a-zA-Z0-9][a-zA-Z0-9._-]*\z/
|
|
10
|
+
|
|
11
|
+
attr_reader :language, :language_version, :board, :cache_dir
|
|
12
|
+
|
|
13
|
+
def initialize(
|
|
14
|
+
language: DEFAULT_LANGUAGE,
|
|
15
|
+
language_version: DEFAULT_LANGUAGE_VERSION,
|
|
16
|
+
board: DEFAULT_BOARD,
|
|
17
|
+
cache_dir: DEFAULT_CACHE_DIR,
|
|
18
|
+
firmware: nil
|
|
19
|
+
)
|
|
20
|
+
@language = validate_identifier(:language, language)
|
|
21
|
+
@language_version = validate_identifier(:language_version, language_version)
|
|
22
|
+
@board = validate_identifier(:board, board)
|
|
23
|
+
@cache_dir = String(cache_dir).gsub("{version}", @language_version)
|
|
24
|
+
@firmware = firmware
|
|
25
|
+
raise ArgumentError, "cache must not be empty" if @cache_dir.empty?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def source_dir(root: Dir.pwd)
|
|
29
|
+
File.expand_path(File.join(cache_dir, "#{language}-#{language_version}"), root)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def firmware_path(root: nil)
|
|
33
|
+
path = @firmware || File.join(cache_dir, "#{language}-#{language_version}-#{board}.uf2")
|
|
34
|
+
root ? File.expand_path(path, root) : path
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def validate_identifier(name, value)
|
|
40
|
+
value = String(value)
|
|
41
|
+
return value if IDENTIFIER_PATTERN.match?(value)
|
|
42
|
+
|
|
43
|
+
raise ArgumentError, "invalid #{name}: #{value.inspect}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/console"
|
|
4
|
+
|
|
5
|
+
module Rpremote
|
|
6
|
+
class Terminal
|
|
7
|
+
EXIT_BYTE = 0x1D
|
|
8
|
+
BUFFER_SIZE = 4096
|
|
9
|
+
|
|
10
|
+
attr_reader :serial_io, :input, :output
|
|
11
|
+
|
|
12
|
+
def initialize(serial_io, input: $stdin, output: $stdout, selector: nil)
|
|
13
|
+
@serial_io = serial_io
|
|
14
|
+
@input = input
|
|
15
|
+
@output = output
|
|
16
|
+
@selector = selector || ->(readers) { IO.select(readers) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run(exit_sequence: nil)
|
|
20
|
+
with_raw_input { relay }
|
|
21
|
+
ensure
|
|
22
|
+
leave_interactive_mode(exit_sequence)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def with_raw_input(&)
|
|
28
|
+
if input.respond_to?(:tty?) && input.tty? && input.respond_to?(:raw)
|
|
29
|
+
input.raw(&)
|
|
30
|
+
else
|
|
31
|
+
yield
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def relay
|
|
36
|
+
readers = [selectable_serial, input]
|
|
37
|
+
loop do
|
|
38
|
+
ready = @selector.call(readers)
|
|
39
|
+
break unless ready
|
|
40
|
+
|
|
41
|
+
return if ready.first.any? { |stream| !relay_stream(stream) }
|
|
42
|
+
end
|
|
43
|
+
rescue EOFError, Errno::EIO
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def relay_stream(stream)
|
|
48
|
+
if stream.equal?(selectable_serial)
|
|
49
|
+
data = serial_io.read_nonblock(BUFFER_SIZE)
|
|
50
|
+
return false if data.nil? || data.empty?
|
|
51
|
+
|
|
52
|
+
output.write(data)
|
|
53
|
+
output.flush if output.respond_to?(:flush)
|
|
54
|
+
true
|
|
55
|
+
else
|
|
56
|
+
relay_input?
|
|
57
|
+
end
|
|
58
|
+
rescue IO::WaitReadable
|
|
59
|
+
true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def relay_input?
|
|
63
|
+
data = input.read_nonblock(BUFFER_SIZE)
|
|
64
|
+
return false if data.nil? || data.empty?
|
|
65
|
+
|
|
66
|
+
exit_at = data.index(EXIT_BYTE.chr)
|
|
67
|
+
if exit_at&.positive?
|
|
68
|
+
serial_io.write(data.byteslice(0...exit_at))
|
|
69
|
+
serial_io.flush if serial_io.respond_to?(:flush)
|
|
70
|
+
end
|
|
71
|
+
return false if exit_at
|
|
72
|
+
|
|
73
|
+
serial_io.write(data)
|
|
74
|
+
serial_io.flush if serial_io.respond_to?(:flush)
|
|
75
|
+
true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def selectable_serial
|
|
79
|
+
@selectable_serial ||= serial_io.respond_to?(:to_io) ? serial_io.to_io : serial_io
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def leave_interactive_mode(sequence)
|
|
83
|
+
return unless sequence
|
|
84
|
+
|
|
85
|
+
serial_io.write(sequence)
|
|
86
|
+
serial_io.flush if serial_io.respond_to?(:flush)
|
|
87
|
+
rescue IOError, SystemCallError
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
data/lib/rpremote.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rpremote/version"
|
|
4
|
+
|
|
5
|
+
module Rpremote
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require_relative "rpremote/checksum"
|
|
10
|
+
require_relative "rpremote/config"
|
|
11
|
+
require_relative "rpremote/target"
|
|
12
|
+
require_relative "rpremote/language"
|
|
13
|
+
require_relative "rpremote/remote_path"
|
|
14
|
+
require_relative "rpremote/picomodem"
|
|
15
|
+
require_relative "rpremote/device"
|
|
16
|
+
require_relative "rpremote/dfu_compiler"
|
|
17
|
+
require_relative "rpremote/serial"
|
|
18
|
+
require_relative "rpremote/terminal"
|
|
19
|
+
require_relative "rpremote/shell"
|
|
20
|
+
require_relative "rpremote/runner"
|
|
21
|
+
require_relative "rpremote/resetter"
|
|
22
|
+
require_relative "rpremote/language_source"
|
|
23
|
+
require_relative "rpremote/mrbgems"
|
|
24
|
+
require_relative "rpremote/mrbgems_command"
|
|
25
|
+
require_relative "rpremote/flasher"
|
|
26
|
+
require_relative "rpremote/builder"
|
|
27
|
+
require_relative "rpremote/dfu_command"
|
|
28
|
+
require_relative "rpremote/setup_command"
|
|
29
|
+
require_relative "rpremote/build_command"
|
|
30
|
+
require_relative "rpremote/flash_command"
|