prremote 0.4.1 → 0.4.3
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/README.md +43 -5
- data/VERSION +1 -1
- data/lib/prremote/cli.rb +2 -0
- data/lib/prremote/commands/install.rb +133 -17
- data/lib/prremote/detector.rb +55 -5
- data/lib/prremote/runtime_manager.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 27b1b34fbd6bbb46d3adb571005dafe869b286b7bc2babc547af1c72cd7f9cd6
|
|
4
|
+
data.tar.gz: 6f35395ede42d344d2b5de27502fe20631551a94ba1c6ea115abefccd4cda9d0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9097fa9e4df160a9bf2bf4b4667d56a0fc3c8600e359d568b9aa028a9f3affb899b3e7a7ad2f9ae0204b824df378cbaa4f6c2935214807b504d70d59014eeee7
|
|
7
|
+
data.tar.gz: 0be3d04ce9b4bd1b07cb1d690b4a844d33c9f6e76c6594b270714214eef1c9662a2eb2151d26ee1e1123a8c6ea0c6c8b8b11caf552d1f62901d5cdd394a2038f
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> ⚠️ This project is in early development. APIs and commands are subject to change.
|
|
4
4
|
|
|
5
|
-
**prremote** is a command-line tool for deploying and running Ruby scripts on a Raspberry Pi Pico W or ESP32 (e.g. M5Stack) over USB serial. It ships a minimal [mruby/c](https://github.com/mrubyc/mrubyc) runtime firmware and lets you compile and send `.rb` files from your Mac or
|
|
5
|
+
**prremote** is a command-line tool for deploying and running Ruby scripts on a Raspberry Pi Pico W or ESP32 (e.g. M5Stack) over USB serial. It ships a minimal [mruby/c](https://github.com/mrubyc/mrubyc) runtime firmware and lets you compile and send `.rb` files from your Mac, Linux or WSL2 machine directly to the device.
|
|
6
6
|
|
|
7
7
|
Inspired by [mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html) for MicroPython.
|
|
8
8
|
|
|
@@ -17,8 +17,10 @@ Inspired by [mpremote](https://docs.micropython.org/en/latest/reference/mpremote
|
|
|
17
17
|
## Requirements
|
|
18
18
|
|
|
19
19
|
- Ruby 3.4 or later
|
|
20
|
+
- Supported hosts: macOS, Linux, and WSL2 — see [WSL](#wsl) for the extra setup WSL needs
|
|
20
21
|
- Supported boards:
|
|
21
22
|
- [Raspberry Pi Pico W](https://www.raspberrypi.com/products/raspberry-pi-pico-w/) / [Pico](https://www.raspberrypi.com/products/raspberry-pi-pico/)
|
|
23
|
+
- [Raspberry Pi Pico 2](https://www.raspberrypi.com/products/raspberry-pi-pico-2/) (RP2350) — `install` not yet verified on real hardware; reports welcome
|
|
22
24
|
- ESP32 (classic) — e.g. [M5GO / M5Stack Core gen1](https://docs.m5stack.com/en/core/m5go), [M5StickC PLUS](https://docs.m5stack.com/en/core/m5stickc_plus), generic [ESP32](https://www.espressif.com/en/products/socs/esp32) dev boards
|
|
23
25
|
- ESP32-C6 (RISC-V) — e.g. [Seeed Studio XIAO ESP32C6](https://wiki.seeedstudio.com/xiao_esp32c6_getting_started/)
|
|
24
26
|
- Per-board feature status (GPIO / ADC / PWM / I2C / SPI / WiFi / sockets / LCD / IR): [docs/SUPPORT.md](docs/SUPPORT.md)
|
|
@@ -64,6 +66,7 @@ Flash the prremote runtime firmware to a supported board.
|
|
|
64
66
|
prremote install # show supported boards
|
|
65
67
|
prremote install -b picow # Pico W
|
|
66
68
|
prremote install -b pico # Pico (no wireless)
|
|
69
|
+
prremote install -b pico2 # Pico 2 (RP2350)
|
|
67
70
|
prremote install -b esp32 # ESP32 (M5GO / M5Stack Core, etc.)
|
|
68
71
|
prremote install -b m5stickc_plus # M5StickC PLUS (esp32 runtime + board library)
|
|
69
72
|
prremote install -b esp32c6 # ESP32-C6 (e.g. XIAO ESP32C6)
|
|
@@ -74,7 +77,7 @@ prremote install -b picow --version 0.1.1 # specify a runtime version
|
|
|
74
77
|
|
|
75
78
|
The firmware is downloaded from GitHub Releases on first use and cached in `~/.prremote/runtime/`. Subsequent installs use the cache.
|
|
76
79
|
|
|
77
|
-
Pico boards:
|
|
80
|
+
Pico boards: if the board is already running the prremote runtime, `install` reboots it into BOOTSEL mode by itself (a 1200 bps touch on its serial port). Otherwise — a fresh board, or one running something else — put it into BOOTSEL mode (hold BOOTSEL, connect USB, release) when prompted. The drive appears as `RPI-RP2` on Pico / Pico W and as `RP2350` on Pico 2.
|
|
78
81
|
|
|
79
82
|
ESP32 (classic): no button dance and no extra tools needed — the firmware is written over the serial port by prremote's pure-Ruby implementation of the Espressif bootloader protocol (the chip is reset into its boot ROM automatically, and the write is verified with an on-chip MD5).
|
|
80
83
|
|
|
@@ -180,13 +183,48 @@ Show the gem version, mrbc version, and the connected device's runtime version.
|
|
|
180
183
|
|
|
181
184
|
```bash
|
|
182
185
|
prremote version
|
|
183
|
-
# prremote: 0.4.
|
|
184
|
-
# runtime: 0.4.
|
|
186
|
+
# prremote: 0.4.3
|
|
187
|
+
# runtime: 0.4.3 board=picow (/dev/tty.usbmodem101)
|
|
185
188
|
# mrbc: mruby 4.0.0 (2026-04-20) (/opt/homebrew/bin/mrbc)
|
|
186
189
|
```
|
|
187
190
|
|
|
188
191
|
---
|
|
189
192
|
|
|
193
|
+
## WSL
|
|
194
|
+
|
|
195
|
+
prremote runs under WSL2, but two things are not automatic there.
|
|
196
|
+
|
|
197
|
+
**USB serial devices are not forwarded.** WSL2 does not pass USB through to the guest, so a board Windows sees perfectly well has no `/dev/ttyACM*` on the Linux side. Install [usbipd-win](https://github.com/dorssel/usbipd-win) on the Windows side and attach the board:
|
|
198
|
+
|
|
199
|
+
```powershell
|
|
200
|
+
winget install --exact dorssel.usbipd-win
|
|
201
|
+
usbipd list # note the BUSID of your board
|
|
202
|
+
usbipd bind --busid <BUSID> # once, as Administrator
|
|
203
|
+
usbipd attach --wsl --busid <BUSID> # after every reconnect
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Then, in WSL:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
prremote list # /dev/ttyACM0 should now show up
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
If the port is there but cannot be opened, add yourself to the `dialout` group (`sudo usermod -aG dialout $USER`) and restart WSL with `wsl --shutdown`.
|
|
213
|
+
|
|
214
|
+
**Removable drives are not mounted.** For Pico boards, the BOOTSEL volume arrives as a Windows drive letter, which WSL does not mount on its own. Mount it before running `install`:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
sudo mkdir -p /mnt/d
|
|
218
|
+
sudo mount -t drvfs D: /mnt/d # use the drive letter RPI-RP2 was assigned
|
|
219
|
+
prremote install -b picow
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`install` scans `/mnt/d` through `/mnt/z` for the BOOTSEL volume, so the mount point only has to match the drive letter.
|
|
223
|
+
|
|
224
|
+
ESP32 boards need neither step in WSL beyond the usbipd attach, since they are flashed over the serial port.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
190
228
|
## Global Options
|
|
191
229
|
|
|
192
230
|
| Option | Description |
|
|
@@ -200,7 +238,7 @@ prremote version
|
|
|
200
238
|
|
|
201
239
|
```bash
|
|
202
240
|
# First-time setup
|
|
203
|
-
prremote install -b picow # or: pico / esp32 / esp32c6
|
|
241
|
+
prremote install -b picow # or: pico / pico2 / esp32 / esp32c6
|
|
204
242
|
|
|
205
243
|
# Manual cycle
|
|
206
244
|
prremote run app.rb # compile + run (one-shot)
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.4.
|
|
1
|
+
0.4.3
|
data/lib/prremote/cli.rb
CHANGED
|
@@ -9,6 +9,23 @@ module Prremote
|
|
|
9
9
|
|
|
10
10
|
BOARD_MAGIC = 'BORD'.freeze
|
|
11
11
|
|
|
12
|
+
DEVICE_LABELS = { 'pico' => 'Pico', 'picow' => 'Pico W', 'pico2' => 'Pico 2' }.freeze
|
|
13
|
+
|
|
14
|
+
# The UF2 bootloader exposes this file on the BOOTSEL volume.
|
|
15
|
+
BOOTSEL_MARKER = 'INFO_UF2.TXT'.freeze
|
|
16
|
+
|
|
17
|
+
# pico-sdk's stdio_usb reboots into the BOOTSEL boot ROM when the host
|
|
18
|
+
# sets this baud rate (PICO_STDIO_USB_RESET_MAGIC_BAUD_RATE), so a board
|
|
19
|
+
# already running the runtime can be flashed without the button.
|
|
20
|
+
RESET_BAUD = 1200
|
|
21
|
+
|
|
22
|
+
# How long to wait for the BOOTSEL drive after the 1200 bps touch before
|
|
23
|
+
# falling back to the manual instructions.
|
|
24
|
+
AUTO_BOOTSEL_TIMEOUT = 10
|
|
25
|
+
|
|
26
|
+
# Boards whose install path has not been run on real hardware yet.
|
|
27
|
+
UNVERIFIED_BOARDS = %w[pico2].freeze
|
|
28
|
+
|
|
12
29
|
# board may be a runtime image name (esp32) or a board sharing one
|
|
13
30
|
# (m5stickc_plus → esp32 image); see Boards::FIRMWARE.
|
|
14
31
|
def initialize(version: VERSION, board: 'picow', port: nil, verbose: false)
|
|
@@ -29,15 +46,8 @@ module Prremote
|
|
|
29
46
|
|
|
30
47
|
uf2_path = RuntimeManager.fetch(@version, @board)
|
|
31
48
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
puts ' 1. Hold the BOOTSEL button'
|
|
35
|
-
puts ' 2. Connect USB (or press RUN while holding BOOTSEL)'
|
|
36
|
-
puts ' 3. Release BOOTSEL — RPI-RP2 should appear as a USB drive'
|
|
37
|
-
puts
|
|
38
|
-
puts 'Waiting for RPI-RP2...'
|
|
39
|
-
|
|
40
|
-
volume = wait_for_volume
|
|
49
|
+
warn_unverified
|
|
50
|
+
volume = mounted_volume || auto_bootsel || manual_bootsel
|
|
41
51
|
puts "Copying firmware to #{volume}..."
|
|
42
52
|
FileUtils.cp(uf2_path, File.join(volume, File.basename(uf2_path)))
|
|
43
53
|
|
|
@@ -49,12 +59,65 @@ module Prremote
|
|
|
49
59
|
|
|
50
60
|
private
|
|
51
61
|
|
|
62
|
+
# A board already sitting in BOOTSEL needs no reboot.
|
|
63
|
+
def mounted_volume
|
|
64
|
+
volume = volume_paths.find { |p| mounted?(p) }
|
|
65
|
+
puts "Found #{volume_name} at #{volume}." if volume
|
|
66
|
+
volume
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Reboots a board running the runtime into BOOTSEL with a 1200 bps
|
|
70
|
+
# touch. nil when there is no Pico port or the drive never shows up
|
|
71
|
+
# (no runtime on the board, a wedged runtime, or a WSL drive that still
|
|
72
|
+
# needs mounting), so the caller falls back to the button.
|
|
73
|
+
def auto_bootsel
|
|
74
|
+
port = @port || Detector.find_pico_port
|
|
75
|
+
return nil unless port
|
|
76
|
+
|
|
77
|
+
puts "Rebooting #{device_label} on #{port} into BOOTSEL mode..."
|
|
78
|
+
touch_reset_baud(port)
|
|
79
|
+
puts "Waiting for #{volume_name}..."
|
|
80
|
+
wait_for_volume(timeout: AUTO_BOOTSEL_TIMEOUT)
|
|
81
|
+
rescue RuntimeError
|
|
82
|
+
puts "#{volume_name} did not appear; falling back to manual BOOTSEL."
|
|
83
|
+
puts
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Opening the port at the magic baud rate is the whole trigger. The board
|
|
88
|
+
# drops off USB right away, so errors while closing are expected.
|
|
89
|
+
def touch_reset_baud(port)
|
|
90
|
+
serial = Serial.new(port, RESET_BAUD)
|
|
91
|
+
rescue RubySerial::Error, SystemCallError => e
|
|
92
|
+
puts "Could not open #{port} at #{RESET_BAUD} bps: #{e.message}" if @verbose
|
|
93
|
+
ensure
|
|
94
|
+
begin
|
|
95
|
+
serial&.close
|
|
96
|
+
rescue RubySerial::Error, SystemCallError
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def manual_bootsel
|
|
102
|
+
puts "Put the #{device_label} into BOOTSEL mode:"
|
|
103
|
+
puts ' 1. Hold the BOOTSEL button'
|
|
104
|
+
puts ' 2. Connect USB (or press RUN while holding BOOTSEL)'
|
|
105
|
+
puts " 3. Release BOOTSEL — #{volume_name} should appear as a USB drive"
|
|
106
|
+
puts
|
|
107
|
+
puts "Waiting for #{volume_name}..."
|
|
108
|
+
wait_for_volume
|
|
109
|
+
end
|
|
110
|
+
|
|
52
111
|
# No BOOTSEL dance on ESP32: the flasher toggles DTR/RTS to enter the
|
|
53
112
|
# boot ROM by itself, so flashing works over the normal serial port.
|
|
54
113
|
def install_esp32
|
|
55
114
|
image = RuntimeManager.fetch(@version, @firmware)
|
|
56
115
|
port = @port || Detector.find_device
|
|
57
|
-
|
|
116
|
+
unless port
|
|
117
|
+
message = 'No serial device found. Connect the board or pass --port.'
|
|
118
|
+
hint = Detector.no_device_hint
|
|
119
|
+
raise [message, hint].compact.join("\n")
|
|
120
|
+
end
|
|
58
121
|
|
|
59
122
|
puts "Flashing #{File.basename(image)} to #{port}..."
|
|
60
123
|
EspFlasher.flash(port: port, image_path: image, board: @firmware, verbose: @verbose)
|
|
@@ -94,34 +157,87 @@ module Prremote
|
|
|
94
157
|
end
|
|
95
158
|
end
|
|
96
159
|
|
|
160
|
+
def warn_unverified
|
|
161
|
+
return unless UNVERIFIED_BOARDS.include?(@board)
|
|
162
|
+
|
|
163
|
+
warn "NOTE: installing on #{device_label} has not been verified on real hardware yet."
|
|
164
|
+
warn ' Please report the result at https://github.com/lumbermill/prremote/issues'
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def device_label
|
|
168
|
+
DEVICE_LABELS.fetch(@board, 'Pico')
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# The RP2040 boot ROM mounts its BOOTSEL drive as RPI-RP2; the RP2350
|
|
172
|
+
# (Pico 2) boot ROM uses RP2350 instead. INFO_UF2.TXT carries the same
|
|
173
|
+
# name as its Board-ID.
|
|
174
|
+
def volume_name
|
|
175
|
+
@board == 'pico2' ? 'RP2350' : 'RPI-RP2'
|
|
176
|
+
end
|
|
177
|
+
|
|
97
178
|
def volume_paths
|
|
179
|
+
user = ENV.fetch('USER', nil)
|
|
98
180
|
[
|
|
99
|
-
|
|
100
|
-
"/run/media/#{
|
|
101
|
-
"/media/#{
|
|
102
|
-
]
|
|
181
|
+
"/Volumes/#{volume_name}",
|
|
182
|
+
"/run/media/#{user}/#{volume_name}",
|
|
183
|
+
"/media/#{user}/#{volume_name}"
|
|
184
|
+
] + drive_letter_volume_paths
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# WSL reaches the Windows drive letter the BOOTSEL volume lands on through
|
|
188
|
+
# drvfs at /mnt/<letter>. That path carries no volume name, so the
|
|
189
|
+
# marker file is what identifies the board.
|
|
190
|
+
def drive_letter_volume_paths
|
|
191
|
+
drive_letter_roots.select { |root| bootsel_volume?(root) }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# /mnt/c is the Windows system drive; the board never lands there, so the
|
|
195
|
+
# scan starts at D.
|
|
196
|
+
def drive_letter_roots
|
|
197
|
+
('d'..'z').map { |letter| "/mnt/#{letter}" }
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Other UF2 bootloaders (and the other RP2 chip) expose INFO_UF2.TXT
|
|
201
|
+
# too, so the Board-ID line is what tells the target chip apart.
|
|
202
|
+
def bootsel_volume?(root)
|
|
203
|
+
marker = File.join(root, BOOTSEL_MARKER)
|
|
204
|
+
File.file?(marker) && /^Board-ID:\s*#{Regexp.escape(volume_name)}\s*$/.match?(File.read(marker))
|
|
205
|
+
rescue SystemCallError
|
|
206
|
+
false
|
|
103
207
|
end
|
|
104
208
|
|
|
209
|
+
# The marker, not the directory, is the test: macOS can leave an empty
|
|
210
|
+
# /Volumes/RPI-RP2 behind after the board reboots off a copy.
|
|
105
211
|
def wait_for_volume(timeout: 60)
|
|
106
212
|
deadline = Time.now + timeout
|
|
107
213
|
loop do
|
|
108
|
-
path = volume_paths.find { |p|
|
|
214
|
+
path = volume_paths.find { |p| mounted?(p) }
|
|
109
215
|
return path if path
|
|
110
|
-
raise "Timed out waiting for
|
|
216
|
+
raise "Timed out waiting for #{volume_name} volume (#{timeout}s)" if Time.now > deadline
|
|
111
217
|
|
|
112
218
|
sleep 1
|
|
113
219
|
end
|
|
114
220
|
end
|
|
115
221
|
|
|
222
|
+
# The board reboots as soon as the UF2 lands, which tears the volume down.
|
|
223
|
+
# Testing the marker rather than the directory covers the WSL case, where
|
|
224
|
+
# the /mnt/<letter> mountpoint outlives the drive it pointed at.
|
|
116
225
|
def wait_for_unmount(volume, timeout: 30)
|
|
117
226
|
deadline = Time.now + timeout
|
|
118
227
|
loop do
|
|
119
|
-
return unless
|
|
228
|
+
return unless mounted?(volume)
|
|
120
229
|
raise "Timed out waiting for device to reboot (#{timeout}s)" if Time.now > deadline
|
|
121
230
|
|
|
122
231
|
sleep 1
|
|
123
232
|
end
|
|
124
233
|
end
|
|
234
|
+
|
|
235
|
+
def mounted?(volume)
|
|
236
|
+
File.directory?(volume) && File.file?(File.join(volume, BOOTSEL_MARKER))
|
|
237
|
+
rescue SystemCallError
|
|
238
|
+
# A stale drvfs mountpoint raises instead of reporting the drive as gone.
|
|
239
|
+
false
|
|
240
|
+
end
|
|
125
241
|
end
|
|
126
242
|
end
|
|
127
243
|
end
|
data/lib/prremote/detector.rb
CHANGED
|
@@ -13,10 +13,48 @@ module Prremote
|
|
|
13
13
|
}.freeze
|
|
14
14
|
R2P2_VENDOR_IDS = %w[2e8a].freeze # Raspberry Pi USB VID (kept for compat)
|
|
15
15
|
|
|
16
|
+
# How far above the tty node the USB device sits is not fixed: ttyUSB
|
|
17
|
+
# bridges add a level that the native-CDC ttyACM nodes do not, and under
|
|
18
|
+
# WSL the chain hangs off vhci_hcd rather than a PCI controller. The lookup
|
|
19
|
+
# walks up until idVendor turns up instead of guessing a depth.
|
|
20
|
+
SYSFS_WALK_LIMIT = 6
|
|
21
|
+
|
|
16
22
|
def self.find_device
|
|
17
23
|
new.find_device
|
|
18
24
|
end
|
|
19
25
|
|
|
26
|
+
# WSL2 does not forward USB devices to the guest: a board Windows enumerates
|
|
27
|
+
# correctly still has no /dev/ttyACM* here until usbipd-win attaches it.
|
|
28
|
+
def self.wsl?
|
|
29
|
+
return @wsl unless @wsl.nil?
|
|
30
|
+
|
|
31
|
+
@wsl = RbConfig::CONFIG['host_os'].match?(/linux/) &&
|
|
32
|
+
File.file?('/proc/version') &&
|
|
33
|
+
File.read('/proc/version').downcase.include?('microsoft')
|
|
34
|
+
rescue SystemCallError
|
|
35
|
+
@wsl = false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Appended wherever a missing device would otherwise look like a prremote
|
|
39
|
+
# bug, since under WSL the cause is almost always the missing attach.
|
|
40
|
+
def self.no_device_hint
|
|
41
|
+
return nil unless wsl?
|
|
42
|
+
|
|
43
|
+
'Running under WSL: attach the board with usbipd-win first — ' \
|
|
44
|
+
'`usbipd list`, then `usbipd attach --wsl --busid <BUSID>`.'
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.find_pico_port
|
|
48
|
+
new.find_pico_port
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# A port whose USB vendor is Raspberry Pi — the only kind the 1200 bps
|
|
52
|
+
# BOOTSEL touch applies to. nil when none (or the vendor can't be read).
|
|
53
|
+
def find_pico_port
|
|
54
|
+
label = KNOWN_VENDORS.dig(R2P2_VENDOR_IDS.first, :label)
|
|
55
|
+
serial_ports.find { |p| port_label(p) == label }
|
|
56
|
+
end
|
|
57
|
+
|
|
20
58
|
def find_device
|
|
21
59
|
candidates = serial_ports
|
|
22
60
|
return candidates.first if candidates.size == 1
|
|
@@ -66,13 +104,25 @@ module Prremote
|
|
|
66
104
|
end
|
|
67
105
|
nil
|
|
68
106
|
when /linux/
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
107
|
+
vid = linux_vendor_id(port)
|
|
108
|
+
vid && KNOWN_VENDORS.dig(vid, :label)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def linux_vendor_id(port)
|
|
113
|
+
dir = File.realpath("/sys/class/tty/#{File.basename(port)}/device")
|
|
114
|
+
SYSFS_WALK_LIMIT.times do
|
|
115
|
+
vid_path = File.join(dir, 'idVendor')
|
|
116
|
+
return File.read(vid_path).strip.downcase if File.file?(vid_path)
|
|
117
|
+
|
|
118
|
+
parent = File.dirname(dir)
|
|
119
|
+
break if parent == dir
|
|
72
120
|
|
|
73
|
-
|
|
74
|
-
KNOWN_VENDORS.dig(vid, :label)
|
|
121
|
+
dir = parent
|
|
75
122
|
end
|
|
123
|
+
nil
|
|
124
|
+
rescue SystemCallError
|
|
125
|
+
nil
|
|
76
126
|
end
|
|
77
127
|
|
|
78
128
|
# Vendor IDs of all connected USB devices as 4-digit hex strings.
|
|
@@ -4,7 +4,7 @@ require 'fileutils'
|
|
|
4
4
|
|
|
5
5
|
module Prremote
|
|
6
6
|
module RuntimeManager
|
|
7
|
-
BOARDS = %w[pico picow esp32 esp32c6].freeze
|
|
7
|
+
BOARDS = %w[pico picow pico2 esp32 esp32c6].freeze
|
|
8
8
|
ESP32_BOARDS = %w[esp32 esp32c6].freeze
|
|
9
9
|
|
|
10
10
|
# Pico boards ship as UF2 (copied to the BOOTSEL drive); ESP32 family ships
|