prremote 0.4.1 → 0.4.2
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 +73 -10
- data/lib/prremote/detector.rb +44 -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: e8eeb5a287e2d90f2c3fdab1324a778be063eee3bb8eeb5577803113478f7afe
|
|
4
|
+
data.tar.gz: f58b759474589038e3c4f5b4fc5c0504543fa2c03f4fde9957a30fe2d6f198f1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bec989326521193f9abcdd90794d79d2acefe8f9d41721507188cd267dc9fbb085c75bc953e7c14960f6ccdb6106da8f5cc276f4a8a7b861038092beeb74af57
|
|
7
|
+
data.tar.gz: f5087a5ab44764509e10b7c70b4b1214223cebdf3ff8f72adee7740297a8deeb7b37c8729a27511484630521ba8da20c55102174e8333728ffd36c39fbd37b12
|
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: put the device into BOOTSEL mode (hold BOOTSEL, connect USB, release) when prompted.
|
|
80
|
+
Pico boards: put the device 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.2
|
|
187
|
+
# runtime: 0.4.2 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.2
|
data/lib/prremote/cli.rb
CHANGED
|
@@ -9,6 +9,14 @@ 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
|
+
# Boards whose install path has not been run on real hardware yet.
|
|
18
|
+
UNVERIFIED_BOARDS = %w[pico2].freeze
|
|
19
|
+
|
|
12
20
|
# board may be a runtime image name (esp32) or a board sharing one
|
|
13
21
|
# (m5stickc_plus → esp32 image); see Boards::FIRMWARE.
|
|
14
22
|
def initialize(version: VERSION, board: 'picow', port: nil, verbose: false)
|
|
@@ -29,13 +37,13 @@ module Prremote
|
|
|
29
37
|
|
|
30
38
|
uf2_path = RuntimeManager.fetch(@version, @board)
|
|
31
39
|
|
|
32
|
-
|
|
40
|
+
warn_unverified
|
|
33
41
|
puts "Put the #{device_label} into BOOTSEL mode:"
|
|
34
42
|
puts ' 1. Hold the BOOTSEL button'
|
|
35
43
|
puts ' 2. Connect USB (or press RUN while holding BOOTSEL)'
|
|
36
|
-
puts
|
|
44
|
+
puts " 3. Release BOOTSEL — #{volume_name} should appear as a USB drive"
|
|
37
45
|
puts
|
|
38
|
-
puts
|
|
46
|
+
puts "Waiting for #{volume_name}..."
|
|
39
47
|
|
|
40
48
|
volume = wait_for_volume
|
|
41
49
|
puts "Copying firmware to #{volume}..."
|
|
@@ -54,7 +62,11 @@ module Prremote
|
|
|
54
62
|
def install_esp32
|
|
55
63
|
image = RuntimeManager.fetch(@version, @firmware)
|
|
56
64
|
port = @port || Detector.find_device
|
|
57
|
-
|
|
65
|
+
unless port
|
|
66
|
+
message = 'No serial device found. Connect the board or pass --port.'
|
|
67
|
+
hint = Detector.no_device_hint
|
|
68
|
+
raise [message, hint].compact.join("\n")
|
|
69
|
+
end
|
|
58
70
|
|
|
59
71
|
puts "Flashing #{File.basename(image)} to #{port}..."
|
|
60
72
|
EspFlasher.flash(port: port, image_path: image, board: @firmware, verbose: @verbose)
|
|
@@ -94,12 +106,53 @@ module Prremote
|
|
|
94
106
|
end
|
|
95
107
|
end
|
|
96
108
|
|
|
109
|
+
def warn_unverified
|
|
110
|
+
return unless UNVERIFIED_BOARDS.include?(@board)
|
|
111
|
+
|
|
112
|
+
warn "NOTE: installing on #{device_label} has not been verified on real hardware yet."
|
|
113
|
+
warn ' Please report the result at https://github.com/lumbermill/prremote/issues'
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def device_label
|
|
117
|
+
DEVICE_LABELS.fetch(@board, 'Pico')
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# The RP2040 boot ROM mounts its BOOTSEL drive as RPI-RP2; the RP2350
|
|
121
|
+
# (Pico 2) boot ROM uses RP2350 instead. INFO_UF2.TXT carries the same
|
|
122
|
+
# name as its Board-ID.
|
|
123
|
+
def volume_name
|
|
124
|
+
@board == 'pico2' ? 'RP2350' : 'RPI-RP2'
|
|
125
|
+
end
|
|
126
|
+
|
|
97
127
|
def volume_paths
|
|
128
|
+
user = ENV.fetch('USER', nil)
|
|
98
129
|
[
|
|
99
|
-
|
|
100
|
-
"/run/media/#{
|
|
101
|
-
"/media/#{
|
|
102
|
-
]
|
|
130
|
+
"/Volumes/#{volume_name}",
|
|
131
|
+
"/run/media/#{user}/#{volume_name}",
|
|
132
|
+
"/media/#{user}/#{volume_name}"
|
|
133
|
+
] + drive_letter_volume_paths
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# WSL reaches the Windows drive letter the BOOTSEL volume lands on through
|
|
137
|
+
# drvfs at /mnt/<letter>. That path carries no volume name, so the
|
|
138
|
+
# marker file is what identifies the board.
|
|
139
|
+
def drive_letter_volume_paths
|
|
140
|
+
drive_letter_roots.select { |root| bootsel_volume?(root) }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# /mnt/c is the Windows system drive; the board never lands there, so the
|
|
144
|
+
# scan starts at D.
|
|
145
|
+
def drive_letter_roots
|
|
146
|
+
('d'..'z').map { |letter| "/mnt/#{letter}" }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Other UF2 bootloaders (and the other RP2 chip) expose INFO_UF2.TXT
|
|
150
|
+
# too, so the Board-ID line is what tells the target chip apart.
|
|
151
|
+
def bootsel_volume?(root)
|
|
152
|
+
marker = File.join(root, BOOTSEL_MARKER)
|
|
153
|
+
File.file?(marker) && /^Board-ID:\s*#{Regexp.escape(volume_name)}\s*$/.match?(File.read(marker))
|
|
154
|
+
rescue SystemCallError
|
|
155
|
+
false
|
|
103
156
|
end
|
|
104
157
|
|
|
105
158
|
def wait_for_volume(timeout: 60)
|
|
@@ -107,21 +160,31 @@ module Prremote
|
|
|
107
160
|
loop do
|
|
108
161
|
path = volume_paths.find { |p| File.directory?(p) }
|
|
109
162
|
return path if path
|
|
110
|
-
raise "Timed out waiting for
|
|
163
|
+
raise "Timed out waiting for #{volume_name} volume (#{timeout}s)" if Time.now > deadline
|
|
111
164
|
|
|
112
165
|
sleep 1
|
|
113
166
|
end
|
|
114
167
|
end
|
|
115
168
|
|
|
169
|
+
# The board reboots as soon as the UF2 lands, which tears the volume down.
|
|
170
|
+
# Testing the marker rather than the directory covers the WSL case, where
|
|
171
|
+
# the /mnt/<letter> mountpoint outlives the drive it pointed at.
|
|
116
172
|
def wait_for_unmount(volume, timeout: 30)
|
|
117
173
|
deadline = Time.now + timeout
|
|
118
174
|
loop do
|
|
119
|
-
return unless
|
|
175
|
+
return unless mounted?(volume)
|
|
120
176
|
raise "Timed out waiting for device to reboot (#{timeout}s)" if Time.now > deadline
|
|
121
177
|
|
|
122
178
|
sleep 1
|
|
123
179
|
end
|
|
124
180
|
end
|
|
181
|
+
|
|
182
|
+
def mounted?(volume)
|
|
183
|
+
File.directory?(volume) && File.file?(File.join(volume, BOOTSEL_MARKER))
|
|
184
|
+
rescue SystemCallError
|
|
185
|
+
# A stale drvfs mountpoint raises instead of reporting the drive as gone.
|
|
186
|
+
false
|
|
187
|
+
end
|
|
125
188
|
end
|
|
126
189
|
end
|
|
127
190
|
end
|
data/lib/prremote/detector.rb
CHANGED
|
@@ -13,10 +13,37 @@ 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
|
+
|
|
20
47
|
def find_device
|
|
21
48
|
candidates = serial_ports
|
|
22
49
|
return candidates.first if candidates.size == 1
|
|
@@ -66,13 +93,25 @@ module Prremote
|
|
|
66
93
|
end
|
|
67
94
|
nil
|
|
68
95
|
when /linux/
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
96
|
+
vid = linux_vendor_id(port)
|
|
97
|
+
vid && KNOWN_VENDORS.dig(vid, :label)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def linux_vendor_id(port)
|
|
102
|
+
dir = File.realpath("/sys/class/tty/#{File.basename(port)}/device")
|
|
103
|
+
SYSFS_WALK_LIMIT.times do
|
|
104
|
+
vid_path = File.join(dir, 'idVendor')
|
|
105
|
+
return File.read(vid_path).strip.downcase if File.file?(vid_path)
|
|
106
|
+
|
|
107
|
+
parent = File.dirname(dir)
|
|
108
|
+
break if parent == dir
|
|
72
109
|
|
|
73
|
-
|
|
74
|
-
KNOWN_VENDORS.dig(vid, :label)
|
|
110
|
+
dir = parent
|
|
75
111
|
end
|
|
112
|
+
nil
|
|
113
|
+
rescue SystemCallError
|
|
114
|
+
nil
|
|
76
115
|
end
|
|
77
116
|
|
|
78
117
|
# 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
|