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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa8044b474d22090a340385b2995d1e242b3aa44f820e0f7dbd3486b4b141a77
4
- data.tar.gz: 9af86c303d859f714b0f566d1910f200d1e3c70a5e56b8b9e799d61299c10aa6
3
+ metadata.gz: e8eeb5a287e2d90f2c3fdab1324a778be063eee3bb8eeb5577803113478f7afe
4
+ data.tar.gz: f58b759474589038e3c4f5b4fc5c0504543fa2c03f4fde9957a30fe2d6f198f1
5
5
  SHA512:
6
- metadata.gz: ab718d4431f723ed4af6d17ad744654bf547cf167afa7f1e5bd60aebb96c2d7bd6470a14898a769c7660a5c959331dff5a71527297b4245e9418abe02511c727
7
- data.tar.gz: 220d6bf77f25475dd25ecf84ffd09a06ae592469e6c2bc90e4af117086b44a5f408ae90a75666d3cd26a79b38b1fd9832f1470cccd838c9edd4bf54176786ec6
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 Linux machine directly to the device.
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.1
184
- # runtime: 0.4.1 board=picow (/dev/tty.usbmodem101)
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
1
+ 0.4.2
data/lib/prremote/cli.rb CHANGED
@@ -112,6 +112,8 @@ module Prremote
112
112
  devices = Detector.new.list_devices
113
113
  if devices.empty?
114
114
  puts 'No serial devices found.'
115
+ hint = Detector.no_device_hint
116
+ puts hint if hint
115
117
  else
116
118
  devices.each { |d| puts "#{d[:port]} (#{d[:label]})" }
117
119
  end
@@ -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
- device_label = @board == 'picow' ? 'Pico W' : 'Pico'
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 ' 3. Release BOOTSEL — RPI-RP2 should appear as a USB drive'
44
+ puts " 3. Release BOOTSEL — #{volume_name} should appear as a USB drive"
37
45
  puts
38
- puts 'Waiting for RPI-RP2...'
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
- raise 'No serial device found. Connect the board or pass --port.' unless port
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
- '/Volumes/RPI-RP2',
100
- "/run/media/#{ENV.fetch('USER', nil)}/RPI-RP2",
101
- "/media/#{ENV.fetch('USER', nil)}/RPI-RP2"
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 RPI-RP2 volume (#{timeout}s)" if Time.now > deadline
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 File.directory?(volume)
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
@@ -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
- port_name = File.basename(port)
70
- vid_path = "/sys/class/tty/#{port_name}/device/../../../idVendor"
71
- return nil unless File.exist?(vid_path)
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
- vid = File.read(vid_path).strip.downcase
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prremote
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - ITO Yosei