esp_flash 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/LICENSE +21 -0
- data/README.md +84 -0
- data/lib/esp_flash/loader.rb +441 -0
- data/lib/esp_flash/monitor.rb +268 -0
- data/lib/esp_flash/serial_port.rb +348 -0
- data/lib/esp_flash/stub/LICENSE-APACHE +201 -0
- data/lib/esp_flash/stub/LICENSE-MIT +21 -0
- data/lib/esp_flash/stub/README.md +9 -0
- data/lib/esp_flash/stub/esp32.json +8 -0
- data/lib/esp_flash/stub/esp32c3.json +8 -0
- data/lib/esp_flash/stub/esp32c6.json +8 -0
- data/lib/esp_flash/stub/esp32h2.json +8 -0
- data/lib/esp_flash/stub/esp32s3.json +30 -0
- data/lib/esp_flash/version.rb +5 -0
- data/lib/esp_flash.rb +46 -0
- metadata +59 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/console"
|
|
4
|
+
|
|
5
|
+
module EspFlash
|
|
6
|
+
# A serial terminal for the device. Keystrokes reach the device byte for byte, terminal replies included
|
|
7
|
+
# (the answer to a "ESC [5n"/"ESC [6n" query, which the picoruby-shell sends to detect the terminal), and
|
|
8
|
+
# what the device sends is written out as it arrives. Ctrl-C goes to the device too.
|
|
9
|
+
#
|
|
10
|
+
# Ctrl-] quit
|
|
11
|
+
# Ctrl-T Ctrl-R reset the device (Ctrl-T Ctrl-H lists the menu)
|
|
12
|
+
#
|
|
13
|
+
# When the ELF and the toolchain's addr2line are available, addresses in a panic's backtrace are decoded.
|
|
14
|
+
#
|
|
15
|
+
# On a terminal, a line feed from the device also returns the cursor to the start of the line (the
|
|
16
|
+
# terminal is in raw mode, which would leave that to the device), and ESP-IDF log lines are colored by
|
|
17
|
+
# their level ("I (123) tag: ..." green, W yellow, E red) as esp-idf-monitor does.
|
|
18
|
+
class Monitor
|
|
19
|
+
EXIT_KEY = 0x1d # Ctrl-]
|
|
20
|
+
MENU_KEY = 0x14 # Ctrl-T
|
|
21
|
+
LOG_COLORS = { "I" => "\e[0;32m", "W" => "\e[0;33m", "E" => "\e[1;31m" }.freeze
|
|
22
|
+
COLOR_RESET = "\e[0m"
|
|
23
|
+
LOG_HEADER = /\A([IWE]) \([\d:. -]+\)/n
|
|
24
|
+
LOG_HEADER_PREFIX = /\A(?:[IWE](?: (?:\([\d:. -]*)?)?)?\z/n # what may still become a LOG_HEADER
|
|
25
|
+
XTENSA_TARGETS = %w[esp32 esp32s2 esp32s3].freeze
|
|
26
|
+
|
|
27
|
+
def initialize(path, baud:, target: nil, elf: nil, reset: true, input: $stdin, output: $stdout, terminal: nil)
|
|
28
|
+
@path = path
|
|
29
|
+
@baud = baud
|
|
30
|
+
@reset = reset
|
|
31
|
+
@input = input
|
|
32
|
+
@output = output
|
|
33
|
+
@terminal = terminal.nil? ? output.respond_to?(:tty?) && output.tty? : terminal
|
|
34
|
+
@line_start = true # the next byte from the device starts a line
|
|
35
|
+
@held = "".b # the start of a line that may be a log header, until the rest of it arrives
|
|
36
|
+
@colored = false # a log line's color is on
|
|
37
|
+
@elf = elf if elf && File.exist?(elf)
|
|
38
|
+
@addr2line = find_addr2line(target) if @elf
|
|
39
|
+
@menu = false
|
|
40
|
+
@line = "".b
|
|
41
|
+
@out_mutex = Mutex.new
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def run
|
|
45
|
+
@port = SerialPort.open(@path, @baud)
|
|
46
|
+
status "--- Monitoring #{@path}#{" at #{@baud} baud" if @port.resettable?}. " \
|
|
47
|
+
"Quit: Ctrl-] Reset: Ctrl-T Ctrl-R Help: Ctrl-T Ctrl-H ---"
|
|
48
|
+
reset_device if @reset
|
|
49
|
+
reader = Thread.new { read_loop }
|
|
50
|
+
raw_input { input_loop }
|
|
51
|
+
ensure
|
|
52
|
+
reader&.kill
|
|
53
|
+
@port&.close
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def input_loop
|
|
59
|
+
loop do
|
|
60
|
+
data = begin
|
|
61
|
+
@input.readpartial(1024)
|
|
62
|
+
rescue EOFError
|
|
63
|
+
return
|
|
64
|
+
end
|
|
65
|
+
out = "".b
|
|
66
|
+
data.each_byte do |byte|
|
|
67
|
+
if @menu
|
|
68
|
+
@menu = false
|
|
69
|
+
case byte
|
|
70
|
+
when 0x12, 0x52, 0x72 # Ctrl-R, R, r
|
|
71
|
+
send_to_device(out)
|
|
72
|
+
out = "".b
|
|
73
|
+
reset_device
|
|
74
|
+
when MENU_KEY then out << byte # Ctrl-T Ctrl-T sends Ctrl-T itself
|
|
75
|
+
when 0x18, 0x58, 0x78 # Ctrl-X, X, x
|
|
76
|
+
send_to_device(out)
|
|
77
|
+
return
|
|
78
|
+
else help
|
|
79
|
+
end
|
|
80
|
+
elsif byte == EXIT_KEY
|
|
81
|
+
send_to_device(out)
|
|
82
|
+
return
|
|
83
|
+
elsif byte == MENU_KEY
|
|
84
|
+
@menu = true
|
|
85
|
+
else
|
|
86
|
+
out << byte
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
send_to_device(out)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def read_loop
|
|
94
|
+
loop do
|
|
95
|
+
data = @port.read(4096, 0.2)
|
|
96
|
+
next flush_held if data.empty?
|
|
97
|
+
|
|
98
|
+
@out_mutex.synchronize do
|
|
99
|
+
@output.write(@terminal ? render(data) : data)
|
|
100
|
+
@output.flush
|
|
101
|
+
end
|
|
102
|
+
decode_backtrace(data) if @addr2line
|
|
103
|
+
rescue Disconnected
|
|
104
|
+
reconnect
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# A USB serial device can vanish for a moment while the chip resets (e.g. the USB-Serial/JTAG one).
|
|
109
|
+
def reconnect
|
|
110
|
+
status "--- #{@path} disconnected; waiting for it to come back (Ctrl-] to quit) ---"
|
|
111
|
+
@port.close
|
|
112
|
+
loop do
|
|
113
|
+
sleep 0.5
|
|
114
|
+
begin
|
|
115
|
+
@port = SerialPort.open(@path, @baud)
|
|
116
|
+
break
|
|
117
|
+
rescue Error
|
|
118
|
+
next
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
status "--- Reconnected to #{@path} ---"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# For a terminal: "\n" => "\r\n" (unless after "\r"), and log lines colored by their level.
|
|
125
|
+
def render(data)
|
|
126
|
+
data = @held + data
|
|
127
|
+
@held = "".b
|
|
128
|
+
out = "".b
|
|
129
|
+
pos = 0
|
|
130
|
+
while pos < data.bytesize
|
|
131
|
+
if @line_start
|
|
132
|
+
rest = data.byteslice(pos..)
|
|
133
|
+
if (header = LOG_HEADER.match(rest))
|
|
134
|
+
out << LOG_COLORS[header[1]]
|
|
135
|
+
@colored = true
|
|
136
|
+
elsif LOG_HEADER_PREFIX.match?(rest)
|
|
137
|
+
@held = rest
|
|
138
|
+
break
|
|
139
|
+
end
|
|
140
|
+
@line_start = false
|
|
141
|
+
end
|
|
142
|
+
newline = data.index("\n", pos)
|
|
143
|
+
unless newline
|
|
144
|
+
out << data.byteslice(pos..)
|
|
145
|
+
break
|
|
146
|
+
end
|
|
147
|
+
line = data.byteslice(pos, newline - pos)
|
|
148
|
+
line = line.chomp("\r")
|
|
149
|
+
out << line
|
|
150
|
+
out << COLOR_RESET if @colored
|
|
151
|
+
out << "\r\n"
|
|
152
|
+
@colored = false
|
|
153
|
+
@line_start = true
|
|
154
|
+
pos = newline + 1
|
|
155
|
+
end
|
|
156
|
+
out
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Writes out what render held back, when no more of the line came.
|
|
160
|
+
def flush_held
|
|
161
|
+
return if @held.empty?
|
|
162
|
+
|
|
163
|
+
@out_mutex.synchronize do
|
|
164
|
+
@output.write(@held)
|
|
165
|
+
@output.flush
|
|
166
|
+
end
|
|
167
|
+
@held = "".b
|
|
168
|
+
@line_start = false
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def send_to_device(bytes)
|
|
172
|
+
@port.write(bytes) unless bytes.empty?
|
|
173
|
+
rescue Disconnected, IOError
|
|
174
|
+
nil # read_loop reconnects
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def reset_device
|
|
178
|
+
return status("--- #{@path} cannot reset the device ---") unless @port.resettable?
|
|
179
|
+
|
|
180
|
+
status "--- Resetting the device ---"
|
|
181
|
+
EspFlash.hard_reset(@port)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def help
|
|
185
|
+
status "--- Ctrl-]: quit Ctrl-T Ctrl-R: reset Ctrl-T Ctrl-X: quit Ctrl-T Ctrl-T: send Ctrl-T ---"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def status(message)
|
|
189
|
+
@out_mutex.synchronize do
|
|
190
|
+
@output.write("\r\n#{message}\r\n")
|
|
191
|
+
@output.flush
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Raw mode so that every key (and every terminal reply) is read as it comes, without echo or signals.
|
|
196
|
+
def raw_input(&block)
|
|
197
|
+
return yield unless @input.respond_to?(:tty?) && @input.tty?
|
|
198
|
+
|
|
199
|
+
@input.raw(intr: false) do
|
|
200
|
+
windows_vt_mode(&block)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# The Windows console answers "ESC [6n" and sends keys as escape sequences only in its VT modes.
|
|
205
|
+
def windows_vt_mode
|
|
206
|
+
return yield unless Gem.win_platform?
|
|
207
|
+
|
|
208
|
+
require "fiddle/import"
|
|
209
|
+
k32 = Module.new do
|
|
210
|
+
extend Fiddle::Importer
|
|
211
|
+
dlload "kernel32.dll"
|
|
212
|
+
extern "void* GetStdHandle(long)"
|
|
213
|
+
extern "int GetConsoleMode(void*, void*)"
|
|
214
|
+
extern "int SetConsoleMode(void*, unsigned long)"
|
|
215
|
+
end
|
|
216
|
+
saved = [[-10, 0x0200], [-11, 0x0004]].filter_map do |std, flag| # VT input / VT processing
|
|
217
|
+
handle = k32.GetStdHandle(std)
|
|
218
|
+
mode = [0].pack("V")
|
|
219
|
+
next if k32.GetConsoleMode(handle, mode).zero?
|
|
220
|
+
|
|
221
|
+
mode = mode.unpack1("V")
|
|
222
|
+
k32.SetConsoleMode(handle, mode | flag)
|
|
223
|
+
[handle, mode]
|
|
224
|
+
end
|
|
225
|
+
begin
|
|
226
|
+
yield
|
|
227
|
+
ensure
|
|
228
|
+
saved.each { |handle, mode| k32.SetConsoleMode(handle, mode) }
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def find_addr2line(target)
|
|
233
|
+
names = if XTENSA_TARGETS.include?(target)
|
|
234
|
+
["xtensa-esp-elf-addr2line", "xtensa-#{target}-elf-addr2line"]
|
|
235
|
+
else
|
|
236
|
+
["riscv32-esp-elf-addr2line"]
|
|
237
|
+
end
|
|
238
|
+
exts = Gem.win_platform? ? [".exe", ""] : [""]
|
|
239
|
+
ENV["PATH"].to_s.split(File::PATH_SEPARATOR).product(names, exts).each do |dir, name, ext|
|
|
240
|
+
path = File.join(dir, name + ext)
|
|
241
|
+
return path if File.file?(path) && File.executable?(path)
|
|
242
|
+
end
|
|
243
|
+
nil
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Decodes the code addresses of a panic's "Backtrace:" line (Xtensa) or "MEPC"/"RA" registers (RISC-V).
|
|
247
|
+
def decode_backtrace(data)
|
|
248
|
+
@line << data
|
|
249
|
+
while (newline = @line.index("\n"))
|
|
250
|
+
line = @line.slice!(0..newline)
|
|
251
|
+
next unless line.match?(/Backtrace:|^\s*(PC|MEPC|RA)\s*:/n)
|
|
252
|
+
|
|
253
|
+
addresses = line.scan(/0x4[0-9a-f]{7}/in).uniq
|
|
254
|
+
next if addresses.empty?
|
|
255
|
+
|
|
256
|
+
decoded = IO.popen([@addr2line, "-pfiaC", "-e", @elf, *addresses], err: File::NULL, &:read).to_s
|
|
257
|
+
lines = decoded.lines.map(&:chomp).reject { |l| l.include?("?? ??:0") }
|
|
258
|
+
@out_mutex.synchronize do
|
|
259
|
+
lines.each { |l| @output.write("--- #{l}\r\n") }
|
|
260
|
+
@output.flush
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
@line = @line.byteslice(-1024..) if @line.bytesize > 4096
|
|
264
|
+
rescue SystemCallError
|
|
265
|
+
@addr2line = nil
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/wait"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
module EspFlash
|
|
7
|
+
# A serial port without any gem or C extension. Every backend has the same interface:
|
|
8
|
+
# read(max, timeout) up to +max+ bytes, waiting up to +timeout+ seconds for the first one ("" if none)
|
|
9
|
+
# write(bytes)
|
|
10
|
+
# baud = rate
|
|
11
|
+
# set_lines(dtr:, rts:) asserts (true) / deasserts (false) DTR and RTS; nil leaves a line as it is
|
|
12
|
+
# discard_input drops whatever has been received so far
|
|
13
|
+
# resettable? false when DTR/RTS do not reach a chip (a TCP socket)
|
|
14
|
+
# close
|
|
15
|
+
#
|
|
16
|
+
# "socket://HOST:PORT" opens a TCP connection instead (e.g. QEMU's UART).
|
|
17
|
+
module SerialPort
|
|
18
|
+
def self.open(path, baud)
|
|
19
|
+
if path.start_with?("socket://")
|
|
20
|
+
TcpPort.new(path, baud)
|
|
21
|
+
elsif Gem.win_platform?
|
|
22
|
+
WindowsPort.new(path, baud)
|
|
23
|
+
else
|
|
24
|
+
PosixPort.new(path, baud)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Serial ports that are likely to be a USB-connected board.
|
|
29
|
+
def self.candidates
|
|
30
|
+
if Gem.win_platform?
|
|
31
|
+
WindowsPort.list
|
|
32
|
+
elsif RUBY_PLATFORM.include?("darwin")
|
|
33
|
+
Dir["/dev/cu.{usbserial,usbmodem,SLAB_USBtoUART,wchusbserial}*"].sort
|
|
34
|
+
else
|
|
35
|
+
Dir["/dev/tty{USB,ACM}*"].sort
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The port to use: +given+ (e.g. ENV['PORT']) if set, the only candidate, or one the user picks when
|
|
40
|
+
# there are several and stdin is a terminal.
|
|
41
|
+
def self.resolve(given)
|
|
42
|
+
return given if given && !given.empty?
|
|
43
|
+
|
|
44
|
+
ports = candidates
|
|
45
|
+
case ports.size
|
|
46
|
+
when 0
|
|
47
|
+
raise Error, "No serial port found. Connect the board, or set PORT (e.g. PORT=/dev/ttyUSB0)."
|
|
48
|
+
when 1
|
|
49
|
+
ports.first
|
|
50
|
+
else
|
|
51
|
+
raise Error, "Several serial ports found (#{ports.join(", ")}); set PORT to pick one." unless $stdin.tty?
|
|
52
|
+
|
|
53
|
+
$stderr.puts "Several serial ports found:"
|
|
54
|
+
ports.each_with_index { |port, i| $stderr.puts " #{i + 1}) #{port}" }
|
|
55
|
+
$stderr.print "Select one [1-#{ports.size}]: "
|
|
56
|
+
index = Integer($stdin.gets.to_s.strip, exception: false)
|
|
57
|
+
raise Error, "No port selected." unless index && (1..ports.size).cover?(index)
|
|
58
|
+
|
|
59
|
+
ports[index - 1]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# A USB serial device on Linux/macOS, set up with stty(1) and its modem lines driven with ioctl(2).
|
|
64
|
+
class PosixPort
|
|
65
|
+
LINUX = RUBY_PLATFORM.include?("linux")
|
|
66
|
+
TIOCMGET = LINUX ? 0x5415 : 0x4004746a
|
|
67
|
+
TIOCMSET = LINUX ? 0x5418 : 0x8004746d
|
|
68
|
+
TIOCM_DTR = 0x002
|
|
69
|
+
TIOCM_RTS = 0x004
|
|
70
|
+
|
|
71
|
+
# struct termios2 (asm-generic/termbits.h): 4 x u32 flags, c_line, c_cc[19], u32 c_ispeed/c_ospeed.
|
|
72
|
+
# BOTHER takes any rate from c_ispeed/c_ospeed.
|
|
73
|
+
TCGETS2 = 0x802c542a
|
|
74
|
+
TCSETS2 = 0x402c542b
|
|
75
|
+
TERMIOS2 = "V4Ca19VV"
|
|
76
|
+
CBAUD = 0o010017
|
|
77
|
+
BOTHER = 0o010000
|
|
78
|
+
CSIZE = 0o000060
|
|
79
|
+
CS8 = 0o000060
|
|
80
|
+
CSTOPB = 0o000100
|
|
81
|
+
CREAD = 0o000200
|
|
82
|
+
PARENB = 0o000400
|
|
83
|
+
CLOCAL = 0o004000
|
|
84
|
+
CRTSCTS = 0o20000000000
|
|
85
|
+
VTIME = 5
|
|
86
|
+
VMIN = 6
|
|
87
|
+
|
|
88
|
+
attr_reader :path
|
|
89
|
+
|
|
90
|
+
def initialize(path, baud)
|
|
91
|
+
@path = path
|
|
92
|
+
@io = File.open(path, File::RDWR | File::NOCTTY | File::NONBLOCK)
|
|
93
|
+
@io.binmode
|
|
94
|
+
@io.sync = true
|
|
95
|
+
self.baud = baud
|
|
96
|
+
rescue SystemCallError => e
|
|
97
|
+
@io&.close
|
|
98
|
+
raise Error, "Cannot open #{path}: #{e.message}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def resettable? = true
|
|
102
|
+
|
|
103
|
+
# ESP-IDF's USB-Serial/JTAG controller (VID:PID 303a:1001), known on Linux only (nil elsewhere).
|
|
104
|
+
def usb_jtag_serial?
|
|
105
|
+
return nil unless LINUX
|
|
106
|
+
|
|
107
|
+
dir = File.realpath("/sys/class/tty/#{File.basename(File.realpath(@path))}/device")
|
|
108
|
+
dir = File.dirname(dir) until dir == "/" || File.exist?(File.join(dir, "idVendor"))
|
|
109
|
+
return false if dir == "/"
|
|
110
|
+
|
|
111
|
+
File.read(File.join(dir, "idVendor")).strip == "303a" && File.read(File.join(dir, "idProduct")).strip == "1001"
|
|
112
|
+
rescue SystemCallError
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Raw 8N1 without flow control at +baud+.
|
|
117
|
+
def baud=(baud)
|
|
118
|
+
LINUX ? linux_termios2(baud) : stty(baud)
|
|
119
|
+
rescue SystemCallError => e
|
|
120
|
+
raise Error, "Cannot set #{@path} to #{baud} baud: #{e.message}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def read(max, timeout)
|
|
124
|
+
return "".b unless @io.wait_readable(timeout)
|
|
125
|
+
|
|
126
|
+
data = @io.read_nonblock(max, exception: false)
|
|
127
|
+
data.is_a?(String) ? data : "".b
|
|
128
|
+
rescue EOFError, Errno::EIO, Errno::ENXIO => e
|
|
129
|
+
raise Disconnected, "#{@path}: #{e.message}"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def write(bytes)
|
|
133
|
+
@io.write(bytes)
|
|
134
|
+
rescue Errno::EIO, Errno::ENXIO => e
|
|
135
|
+
raise Disconnected, "#{@path}: #{e.message}"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Both lines change in one ioctl when both are given (what esptool's "tight" reset relies on).
|
|
139
|
+
def set_lines(dtr: nil, rts: nil)
|
|
140
|
+
buf = [0].pack("i")
|
|
141
|
+
@io.ioctl(TIOCMGET, buf)
|
|
142
|
+
bits = buf.unpack1("i")
|
|
143
|
+
bits = dtr ? bits | TIOCM_DTR : bits & ~TIOCM_DTR unless dtr.nil?
|
|
144
|
+
bits = rts ? bits | TIOCM_RTS : bits & ~TIOCM_RTS unless rts.nil?
|
|
145
|
+
@io.ioctl(TIOCMSET, [bits].pack("i"))
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def discard_input
|
|
149
|
+
loop { break if read(4096, 0).empty? }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def close
|
|
153
|
+
@io.close unless @io.closed?
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
private
|
|
157
|
+
|
|
158
|
+
def linux_termios2(baud)
|
|
159
|
+
buf = "\0".b * 44
|
|
160
|
+
@io.ioctl(TCGETS2, buf)
|
|
161
|
+
_iflag, _oflag, cflag, _lflag, line, cc, = buf.unpack(TERMIOS2)
|
|
162
|
+
cflag &= ~(CBAUD | CSIZE | CSTOPB | PARENB | CRTSCTS)
|
|
163
|
+
cflag |= BOTHER | CS8 | CREAD | CLOCAL
|
|
164
|
+
cc = cc.b
|
|
165
|
+
cc.setbyte(VTIME, 0)
|
|
166
|
+
cc.setbyte(VMIN, 1)
|
|
167
|
+
@io.ioctl(TCSETS2, [0, 0, cflag, 0, line, cc, baud, baud].pack(TERMIOS2))
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# stty(1) sets up its stdin, which is this port (kept open, so the settings stay).
|
|
171
|
+
def stty(baud)
|
|
172
|
+
ok = system("stty", baud.to_s, "raw", "-echo", "cs8", "-cstopb", "-parenb", "-crtscts", "clocal", "cread",
|
|
173
|
+
in: @io, err: File::NULL)
|
|
174
|
+
raise Error, "Cannot set #{@path} to #{baud} baud (stty failed)" unless ok
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# A TCP connection, e.g. to QEMU's UART. There are no modem lines, so the chip cannot be reset.
|
|
179
|
+
class TcpPort
|
|
180
|
+
attr_reader :path
|
|
181
|
+
|
|
182
|
+
def initialize(path, _baud)
|
|
183
|
+
@path = path
|
|
184
|
+
host, port = path.delete_prefix("socket://").split(":", 2)
|
|
185
|
+
@io = Socket.tcp(host, Integer(port), connect_timeout: 5)
|
|
186
|
+
@io.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
|
187
|
+
rescue SystemCallError, SocketError, ArgumentError, TypeError => e
|
|
188
|
+
raise Error, "Cannot connect to #{path}: #{e.message}"
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def resettable? = false
|
|
192
|
+
def usb_jtag_serial? = false
|
|
193
|
+
def baud=(_baud); end
|
|
194
|
+
def set_lines(dtr: nil, rts: nil); end
|
|
195
|
+
|
|
196
|
+
def read(max, timeout)
|
|
197
|
+
return "".b unless @io.wait_readable(timeout)
|
|
198
|
+
|
|
199
|
+
@io.read_nonblock(max)
|
|
200
|
+
rescue IO::WaitReadable
|
|
201
|
+
"".b
|
|
202
|
+
rescue EOFError, SystemCallError => e
|
|
203
|
+
raise Disconnected, "#{@path}: #{e.message}"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def write(bytes)
|
|
207
|
+
@io.write(bytes)
|
|
208
|
+
rescue SystemCallError => e
|
|
209
|
+
raise Disconnected, "#{@path}: #{e.message}"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def discard_input
|
|
213
|
+
loop { break if read(4096, 0).empty? }
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def close
|
|
217
|
+
@io.close unless @io.closed?
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# A COM port on Windows, through kernel32 with Fiddle.
|
|
222
|
+
class WindowsPort
|
|
223
|
+
GENERIC_READ_WRITE = 0xC0000000
|
|
224
|
+
OPEN_EXISTING = 3
|
|
225
|
+
MAXDWORD = 0xFFFFFFFF
|
|
226
|
+
SETRTS = 3
|
|
227
|
+
CLRRTS = 4
|
|
228
|
+
SETDTR = 5
|
|
229
|
+
CLRDTR = 6
|
|
230
|
+
PURGE_RXCLEAR = 0x0008
|
|
231
|
+
|
|
232
|
+
def self.kernel32
|
|
233
|
+
@kernel32 ||= begin
|
|
234
|
+
require "fiddle/import"
|
|
235
|
+
Module.new do
|
|
236
|
+
extend Fiddle::Importer
|
|
237
|
+
dlload "kernel32.dll"
|
|
238
|
+
extern "void* CreateFileW(void*, unsigned long, unsigned long, void*, unsigned long, unsigned long, void*)"
|
|
239
|
+
extern "int GetCommState(void*, void*)"
|
|
240
|
+
extern "int SetCommState(void*, void*)"
|
|
241
|
+
extern "int SetCommTimeouts(void*, void*)"
|
|
242
|
+
extern "int ReadFile(void*, void*, unsigned long, void*, void*)"
|
|
243
|
+
extern "int WriteFile(void*, void*, unsigned long, void*, void*)"
|
|
244
|
+
extern "int EscapeCommFunction(void*, unsigned long)"
|
|
245
|
+
extern "int PurgeComm(void*, unsigned long)"
|
|
246
|
+
extern "int CloseHandle(void*)"
|
|
247
|
+
extern "unsigned long GetLastError()"
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def self.list
|
|
253
|
+
require "win32/registry"
|
|
254
|
+
Win32::Registry::HKEY_LOCAL_MACHINE.open('HARDWARE\DEVICEMAP\SERIALCOMM') do |key|
|
|
255
|
+
key.map { |_name, _type, value| value }.sort_by { |name| name[/\d+/].to_i }
|
|
256
|
+
end
|
|
257
|
+
rescue LoadError, StandardError
|
|
258
|
+
[]
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
attr_reader :path
|
|
262
|
+
|
|
263
|
+
def initialize(path, baud)
|
|
264
|
+
@path = path
|
|
265
|
+
@k = self.class.kernel32
|
|
266
|
+
name = path.start_with?("\\\\.\\") ? path : "\\\\.\\#{path}"
|
|
267
|
+
@handle = @k.CreateFileW("#{name}\0".encode("UTF-16LE"), GENERIC_READ_WRITE, 0, nil, OPEN_EXISTING, 0, nil)
|
|
268
|
+
if @handle.to_i == (1 << (8 * Fiddle::SIZEOF_VOIDP)) - 1 || @handle.to_i == -1
|
|
269
|
+
raise Error, "Cannot open #{path} (error #{@k.GetLastError})"
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
@dtr = @rts = false
|
|
273
|
+
@read_timeout = nil
|
|
274
|
+
self.baud = baud
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def resettable? = true
|
|
278
|
+
def usb_jtag_serial? = nil
|
|
279
|
+
|
|
280
|
+
# The DCB also carries the DTR/RTS state, so a baud change keeps the lines as they are.
|
|
281
|
+
def baud=(baud)
|
|
282
|
+
@baud = baud
|
|
283
|
+
dcb = "\0".b * 28
|
|
284
|
+
dcb[0, 4] = [28].pack("V")
|
|
285
|
+
check(@k.GetCommState(@handle, dcb), "GetCommState")
|
|
286
|
+
flags = 0x0001 # fBinary; no parity check, no flow control
|
|
287
|
+
flags |= 0x0010 if @dtr # fDtrControl = DTR_CONTROL_ENABLE
|
|
288
|
+
flags |= 0x1000 if @rts # fRtsControl = RTS_CONTROL_ENABLE
|
|
289
|
+
dcb[4, 4] = [baud].pack("V")
|
|
290
|
+
dcb[8, 4] = [flags].pack("V")
|
|
291
|
+
dcb[18, 3] = [8, 0, 0].pack("C3") # 8 data bits, no parity, 1 stop bit
|
|
292
|
+
check(@k.SetCommState(@handle, dcb), "SetCommState")
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def read(max, timeout)
|
|
296
|
+
ms = (timeout * 1000).ceil
|
|
297
|
+
if @read_timeout != ms
|
|
298
|
+
# Return as soon as any byte is there, or after +ms+ when nothing comes.
|
|
299
|
+
timeouts = ms.zero? ? [MAXDWORD, 0, 0, 0, 0] : [MAXDWORD, MAXDWORD, ms, 0, 0]
|
|
300
|
+
check(@k.SetCommTimeouts(@handle, timeouts.pack("V5")), "SetCommTimeouts")
|
|
301
|
+
@read_timeout = ms
|
|
302
|
+
end
|
|
303
|
+
buf = "\0".b * max
|
|
304
|
+
count = [0].pack("V")
|
|
305
|
+
raise Disconnected, "#{@path}: ReadFile failed" if @k.ReadFile(@handle, buf, max, count, nil).zero?
|
|
306
|
+
|
|
307
|
+
buf.byteslice(0, count.unpack1("V"))
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def write(bytes)
|
|
311
|
+
bytes = bytes.b
|
|
312
|
+
until bytes.empty?
|
|
313
|
+
count = [0].pack("V")
|
|
314
|
+
raise Disconnected, "#{@path}: WriteFile failed" if @k.WriteFile(@handle, bytes, bytes.bytesize, count, nil).zero?
|
|
315
|
+
|
|
316
|
+
bytes = bytes.byteslice(count.unpack1("V")..)
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# DTR first: usbser.sys only passes a DTR change on together with the next RTS one.
|
|
321
|
+
def set_lines(dtr: nil, rts: nil)
|
|
322
|
+
unless dtr.nil?
|
|
323
|
+
@dtr = dtr
|
|
324
|
+
@k.EscapeCommFunction(@handle, dtr ? SETDTR : CLRDTR)
|
|
325
|
+
end
|
|
326
|
+
return if rts.nil? && dtr.nil?
|
|
327
|
+
|
|
328
|
+
@rts = rts unless rts.nil?
|
|
329
|
+
@k.EscapeCommFunction(@handle, @rts ? SETRTS : CLRRTS)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def discard_input
|
|
333
|
+
@k.PurgeComm(@handle, PURGE_RXCLEAR)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def close
|
|
337
|
+
@k.CloseHandle(@handle) if @handle
|
|
338
|
+
@handle = nil
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
private
|
|
342
|
+
|
|
343
|
+
def check(result, what)
|
|
344
|
+
raise Error, "#{what} failed on #{@path} (error #{@k.GetLastError})" if result.zero?
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|