prremote 0.4.0 → 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 +66 -6
- data/VERSION +1 -1
- data/lib/prremote/boards/bm8563.rb +55 -0
- data/lib/prremote/boards/m5stickc_plus.rb +102 -0
- data/lib/prremote/boards.rb +85 -0
- data/lib/prremote/cli.rb +18 -5
- data/lib/prremote/commands/deploy.rb +4 -17
- data/lib/prremote/commands/install.rb +127 -17
- data/lib/prremote/commands/run.rb +5 -19
- data/lib/prremote/commands/serial_helpers.rb +32 -2
- data/lib/prremote/detector.rb +44 -5
- data/lib/prremote/mrbc.rb +13 -0
- data/lib/prremote/runtime_manager.rb +1 -1
- metadata +4 -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,7 +66,9 @@ 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)
|
|
67
|
-
prremote install -b
|
|
69
|
+
prremote install -b pico2 # Pico 2 (RP2350)
|
|
70
|
+
prremote install -b esp32 # ESP32 (M5GO / M5Stack Core, etc.)
|
|
71
|
+
prremote install -b m5stickc_plus # M5StickC PLUS (esp32 runtime + board library)
|
|
68
72
|
prremote install -b esp32c6 # ESP32-C6 (e.g. XIAO ESP32C6)
|
|
69
73
|
prremote install -b picow --version 0.1.1 # specify a runtime version
|
|
70
74
|
```
|
|
@@ -73,7 +77,7 @@ prremote install -b picow --version 0.1.1 # specify a runtime version
|
|
|
73
77
|
|
|
74
78
|
The firmware is downloaded from GitHub Releases on first use and cached in `~/.prremote/runtime/`. Subsequent installs use the cache.
|
|
75
79
|
|
|
76
|
-
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.
|
|
77
81
|
|
|
78
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).
|
|
79
83
|
|
|
@@ -81,6 +85,8 @@ ESP32-C6: flashing is delegated to [esptool](https://docs.espressif.com/projects
|
|
|
81
85
|
|
|
82
86
|
Reflashing the runtime does not erase a deployed script on either chip.
|
|
83
87
|
|
|
88
|
+
Board names such as `m5stickc_plus` install the runtime image of their chip (esp32) and record the board name on the device, which the runtime then reports (`prremote version` shows `board=m5stickc_plus`). That name decides which [board libraries](#board-libraries) scripts can use; installing with the plain chip name (`-b esp32`) clears it.
|
|
89
|
+
|
|
84
90
|
---
|
|
85
91
|
|
|
86
92
|
### `run FILE [FILE ...]`
|
|
@@ -177,13 +183,48 @@ Show the gem version, mrbc version, and the connected device's runtime version.
|
|
|
177
183
|
|
|
178
184
|
```bash
|
|
179
185
|
prremote version
|
|
180
|
-
# prremote: 0.4.
|
|
181
|
-
# runtime: 0.4.
|
|
186
|
+
# prremote: 0.4.2
|
|
187
|
+
# runtime: 0.4.2 board=picow (/dev/tty.usbmodem101)
|
|
182
188
|
# mrbc: mruby 4.0.0 (2026-04-20) (/opt/homebrew/bin/mrbc)
|
|
183
189
|
```
|
|
184
190
|
|
|
185
191
|
---
|
|
186
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
|
+
|
|
187
228
|
## Global Options
|
|
188
229
|
|
|
189
230
|
| Option | Description |
|
|
@@ -197,7 +238,7 @@ prremote version
|
|
|
197
238
|
|
|
198
239
|
```bash
|
|
199
240
|
# First-time setup
|
|
200
|
-
prremote install -b picow # or: pico / esp32 / esp32c6
|
|
241
|
+
prremote install -b picow # or: pico / pico2 / esp32 / esp32c6
|
|
201
242
|
|
|
202
243
|
# Manual cycle
|
|
203
244
|
prremote run app.rb # compile + run (one-shot)
|
|
@@ -213,6 +254,25 @@ prremote undeploy # remove from flash
|
|
|
213
254
|
|
|
214
255
|
---
|
|
215
256
|
|
|
257
|
+
## Board libraries
|
|
258
|
+
|
|
259
|
+
Some boards come with a Ruby library for their built-in hardware. You don't load it yourself: when a script mentions one of its classes, `run` / `deploy` / `watch` / `eval` compile the library in ahead of your files (it prints `Adding board library: ...`). Scripts that don't mention it pay nothing on the device.
|
|
260
|
+
|
|
261
|
+
| Board (`install -b`) | Classes | Covers |
|
|
262
|
+
|---|---|---|
|
|
263
|
+
| `m5stickc_plus` | `M5StickCPlus`, `BM8563` | AXP192 power-on, LCD preset per rotation, buttons A/B, red LED, buzzer, IR LED, BM8563 RTC (as `Time`) |
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
stick = M5StickCPlus.new # powers the LCD
|
|
267
|
+
lcd = stick.lcd(rotation: 1) # pins / size / RAM offset / color order preset
|
|
268
|
+
lcd.text(4, 4, stick.rtc.time.to_s, scale: 1)
|
|
269
|
+
stick.led.on if stick.button_a.pressed?
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
A board library is refused if the device reports a different board (e.g. `M5StickCPlus` on an esp32c6). Chip drivers such as `BM8563` work on any board. The library sources live in [lib/prremote/boards/](lib/prremote/boards/) and double as the reference for driving the hardware directly.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
216
276
|
## How It Works
|
|
217
277
|
|
|
218
278
|
prremote flashes a minimal C firmware (built on mruby/c) onto the board. The firmware:
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.4.
|
|
1
|
+
0.4.2
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# BM8563 / PCF8563 real-time clock (I2C address 0x51).
|
|
2
|
+
# Board library for prremote — compiled into the script by the host when it
|
|
3
|
+
# references BM8563 (see lib/prremote/boards.rb); this file runs on the device.
|
|
4
|
+
#
|
|
5
|
+
# rtc = BM8563.new(i2c)
|
|
6
|
+
# rtc.valid? # false until the time has been set (VL flag)
|
|
7
|
+
# puts rtc.time.to_s # => "2026-09-28T08:30:42+9" (a Time)
|
|
8
|
+
# rtc.time = Time.new(offset: 9).sync("ntp.nict.jp")
|
|
9
|
+
#
|
|
10
|
+
# The chip holds local time; `time(offset:)` says which zone that is (JST by
|
|
11
|
+
# default) and `time=` writes the given Time in its own offset.
|
|
12
|
+
class BM8563
|
|
13
|
+
ADDR = 0x51
|
|
14
|
+
|
|
15
|
+
def initialize(i2c)
|
|
16
|
+
@i2c = i2c
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# false while the VL (voltage-low) flag says the clock lost integrity —
|
|
20
|
+
# never set, or the backup battery ran out.
|
|
21
|
+
def valid?
|
|
22
|
+
r = @i2c.read(ADDR, 1, 0x02)
|
|
23
|
+
!r.nil? && r.getbyte(0) < 0x80
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def time(offset: 9)
|
|
27
|
+
r = @i2c.read(ADDR, 7, 0x02).bytes
|
|
28
|
+
t = Time.new(offset: offset)
|
|
29
|
+
year = _dec(r[6]) + (r[5] < 0x80 ? 2000 : 1900) # month bit7 = century
|
|
30
|
+
days = t._days_since_epoch(year, _dec(r[5] & 0x1F), _dec(r[3] & 0x3F))
|
|
31
|
+
t._set_from_epoch((days * 86_400) + _seconds_of_day(r) - (offset * 3600))
|
|
32
|
+
t
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Also clears the VL flag and restarts the clock if it was stopped.
|
|
36
|
+
def time=(t)
|
|
37
|
+
wday = (((t.epoch + (t.utc_offset * 3600)) / 86_400) + 4) % 7 # 1970-01-01 = Thu
|
|
38
|
+
@i2c.write(ADDR, 0x00, 0x00, 0x00) # control 1/2: running, no alarm/timer IRQ
|
|
39
|
+
@i2c.write(ADDR, 0x02, _bcd(t.sec), _bcd(t.min), _bcd(t.hour), _bcd(t.day),
|
|
40
|
+
wday, _bcd(t.month), _bcd(t.year % 100))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# r: registers 0x02.. (sec, min, hour, ...)
|
|
44
|
+
def _seconds_of_day(r)
|
|
45
|
+
(_dec(r[2] & 0x3F) * 3600) + (_dec(r[1] & 0x7F) * 60) + _dec(r[0] & 0x7F)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def _dec(b)
|
|
49
|
+
((b >> 4) * 10) + (b & 0x0F)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def _bcd(n)
|
|
53
|
+
((n / 10) << 4) | (n % 10)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# M5StickC PLUS board support: power-on, LCD preset and built-in devices.
|
|
2
|
+
# Board library for prremote — compiled into the script by the host when it
|
|
3
|
+
# references M5StickCPlus (see lib/prremote/boards.rb); this file runs on the
|
|
4
|
+
# device (esp32 runtime) together with bm8563.rb.
|
|
5
|
+
#
|
|
6
|
+
# stick = M5StickCPlus.new # powers the LCD via the AXP192 PMIC
|
|
7
|
+
# lcd = stick.lcd(rotation: 1) # landscape; 0-3 all preset
|
|
8
|
+
# stick.button_a.pressed? # front button; button_b = side button
|
|
9
|
+
# stick.led.on # red LED
|
|
10
|
+
# stick.tone(440, 0.2) # passive buzzer
|
|
11
|
+
# stick.rtc.time.to_s # BM8563 RTC (see bm8563.rb)
|
|
12
|
+
# stick.ir.send_nec(0x00, 0x45) # IR LED
|
|
13
|
+
#
|
|
14
|
+
# Built-in devices share one I2C bus (SDA=21/SCL=22): AXP192 0x34, BM8563
|
|
15
|
+
# 0x51, MPU6886 0x68 — use stick.i2c for anything else on it.
|
|
16
|
+
class M5StickCPlus
|
|
17
|
+
AXP192 = 0x34
|
|
18
|
+
|
|
19
|
+
# ST7789v2 135x240 inside a 240x320 RAM window. Per rotation: MADCTL (the
|
|
20
|
+
# generic table minus its BGR bit — this panel is RGB) and the RAM offset
|
|
21
|
+
# of the 135-px side, which is 52 px from one edge and 53 from the other,
|
|
22
|
+
# so it changes when a rotation mirrors that axis.
|
|
23
|
+
LCD_MADCTL = [0x00, 0x60, 0xC0, 0xA0]
|
|
24
|
+
LCD_OFFSET_X = [52, 53, 53, 52]
|
|
25
|
+
|
|
26
|
+
attr_reader :i2c
|
|
27
|
+
|
|
28
|
+
def initialize
|
|
29
|
+
@i2c = I2C.new(sda_pin: 21, scl_pin: 22)
|
|
30
|
+
@i2c.write(AXP192, 0x28, 0xCC) # LDO2 (backlight) / LDO3 (panel) = 3.0V
|
|
31
|
+
cur = @i2c.read(AXP192, 1, 0x12).getbyte(0)
|
|
32
|
+
@i2c.write(AXP192, 0x12, cur | 0x4D) # enable EXTEN (Grove 5V), LDO2, LDO3, DCDC1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# rotation: 0 = portrait (USB at the bottom), 1/3 = landscape, 2 = portrait
|
|
36
|
+
# upside down. Returns a plain LCD (see lcd_wrap.rb for drawing methods).
|
|
37
|
+
def lcd(rotation: 0)
|
|
38
|
+
r = rotation % 4
|
|
39
|
+
LCD.new(rotation: r, sck_pin: 13, mosi_pin: 15, miso_pin: -1, cs_pin: 5,
|
|
40
|
+
dc_pin: 23, rst_pin: 18, bl_pin: -1, invert: true,
|
|
41
|
+
madctl: LCD_MADCTL[r], width: 135, height: 240,
|
|
42
|
+
offset_x: LCD_OFFSET_X[r], offset_y: 40)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def rtc
|
|
46
|
+
@rtc ||= BM8563.new(@i2c)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Front (big) button.
|
|
50
|
+
def button_a
|
|
51
|
+
@button_a ||= Button.new(37)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Side button, opposite the power button.
|
|
55
|
+
def button_b
|
|
56
|
+
@button_b ||= Button.new(39)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def led
|
|
60
|
+
@led ||= LED.new(10)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Plays `hz` on the passive buzzer (GPIO2) for `duration` seconds.
|
|
64
|
+
def tone(hz, duration)
|
|
65
|
+
@buzzer ||= PWM.new(2, frequency: hz, duty_u16: 0)
|
|
66
|
+
@buzzer.frequency = hz
|
|
67
|
+
@buzzer.duty_u16 = 32_768
|
|
68
|
+
sleep duration
|
|
69
|
+
@buzzer.duty_u16 = 0
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def ir
|
|
73
|
+
@ir ||= IR.new(9)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# External pull-up on the board; pressed reads 0.
|
|
77
|
+
class Button
|
|
78
|
+
def initialize(pin)
|
|
79
|
+
@gpio = GPIO.new(pin, GPIO::IN)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def pressed?
|
|
83
|
+
@gpio.read == 0
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Active-low red LED.
|
|
88
|
+
class LED
|
|
89
|
+
def initialize(pin)
|
|
90
|
+
@gpio = GPIO.new(pin, GPIO::OUT)
|
|
91
|
+
off
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def on
|
|
95
|
+
@gpio.write(0)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def off
|
|
99
|
+
@gpio.write(1)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
module Prremote
|
|
2
|
+
# Board support libraries: Ruby files under lib/prremote/boards/ that run on
|
|
3
|
+
# the device (not on the host). They are compiled into the user's script
|
|
4
|
+
# only when the script references one of their constants, so scripts that
|
|
5
|
+
# don't use them pay nothing in device heap / symbol table.
|
|
6
|
+
#
|
|
7
|
+
# A board name is what `install -b NAME` records on the device and what the
|
|
8
|
+
# runtime reports in its READY banner (`READY prremote-runtime/X board=NAME`).
|
|
9
|
+
# Several board names can share one runtime image (FIRMWARE).
|
|
10
|
+
module Boards
|
|
11
|
+
LIB_DIR = File.expand_path('boards', __dir__)
|
|
12
|
+
|
|
13
|
+
# Board name => runtime image it runs. Boards not listed here are their
|
|
14
|
+
# own image (pico, picow, esp32, esp32c6, ...).
|
|
15
|
+
FIRMWARE = {
|
|
16
|
+
'm5stickc_plus' => 'esp32'
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
# file: path under LIB_DIR
|
|
20
|
+
# constants: referencing any of these pulls the library in
|
|
21
|
+
# boards: board names it is written for (nil = any board)
|
|
22
|
+
# requires: other library files it needs, compiled before it
|
|
23
|
+
Library = Struct.new(:file, :constants, :boards, :requires, keyword_init: true)
|
|
24
|
+
|
|
25
|
+
LIBRARIES = [
|
|
26
|
+
Library.new(file: 'bm8563.rb', constants: %w[BM8563], boards: nil, requires: []),
|
|
27
|
+
Library.new(file: 'm5stickc_plus.rb', constants: %w[M5StickCPlus],
|
|
28
|
+
boards: %w[m5stickc_plus], requires: %w[bm8563.rb])
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
def firmware_for(board)
|
|
34
|
+
FIRMWARE.fetch(board, board)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Board names `install` accepts on top of the plain runtime images.
|
|
38
|
+
def names
|
|
39
|
+
FIRMWARE.keys
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Libraries the sources reference directly.
|
|
43
|
+
def referenced(sources)
|
|
44
|
+
LIBRARIES.select { |lib| referenced?(lib, sources) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Library file paths to compile ahead of the user's sources, in dependency
|
|
48
|
+
# order. Depends only on the sources, so it can run before connecting.
|
|
49
|
+
def library_paths(sources)
|
|
50
|
+
files = []
|
|
51
|
+
referenced(sources).each { |lib| add_with_requires(lib.file, files) }
|
|
52
|
+
files.map { |f| File.join(LIB_DIR, f) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Raises if a referenced library is for another board than the device's
|
|
56
|
+
# (`board` is nil when the runtime is too old to report one).
|
|
57
|
+
def check!(sources, board)
|
|
58
|
+
referenced(sources).each { |lib| check_board!(lib, board) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def referenced?(lib, sources)
|
|
62
|
+
lib.constants.any? { |c| sources.any? { |src| src.match?(/\b#{c}\b/) } }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# A board library also runs on a device that only reports its runtime
|
|
66
|
+
# image name (installed with the plain `-b esp32`, or a runtime older than
|
|
67
|
+
# board names), since the host cannot tell those boards apart.
|
|
68
|
+
def check_board!(lib, board)
|
|
69
|
+
return if lib.boards.nil? || board.nil?
|
|
70
|
+
return if lib.boards.include?(board)
|
|
71
|
+
return if lib.boards.any? { |b| firmware_for(b) == board }
|
|
72
|
+
|
|
73
|
+
raise "#{lib.constants.join('/')} is for #{lib.boards.join('/')}, " \
|
|
74
|
+
"but the device reports board=#{board}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def add_with_requires(file, files)
|
|
78
|
+
return if files.include?(file)
|
|
79
|
+
|
|
80
|
+
lib = LIBRARIES.find { |l| l.file == file }
|
|
81
|
+
lib&.requires&.each { |dep| add_with_requires(dep, files) }
|
|
82
|
+
files << file
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/prremote/cli.rb
CHANGED
|
@@ -27,7 +27,7 @@ module Prremote
|
|
|
27
27
|
desc 'install', 'Flash prremote runtime firmware to a supported board'
|
|
28
28
|
option :version, type: :string, desc: "Firmware version to install (default: #{VERSION})"
|
|
29
29
|
option :board, aliases: '-b', type: :string,
|
|
30
|
-
desc: "Board type: #{
|
|
30
|
+
desc: "Board type: #{Commands::Install.board_names.join(', ')}"
|
|
31
31
|
option :verbose, aliases: '-V', type: :boolean, default: false,
|
|
32
32
|
desc: 'Print step-by-step flash diagnostics'
|
|
33
33
|
def install
|
|
@@ -36,14 +36,14 @@ module Prremote
|
|
|
36
36
|
|
|
37
37
|
unless board
|
|
38
38
|
puts "Specify a board with --board / -b. Supported boards:"
|
|
39
|
-
|
|
39
|
+
Commands::Install.board_names.each { |b| puts " #{b}" }
|
|
40
40
|
puts
|
|
41
41
|
puts "Example: prremote install -b esp32c6"
|
|
42
42
|
return
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
unless
|
|
46
|
-
raise Thor::Error, "Unknown board '#{board}'. Supported boards: #{
|
|
45
|
+
unless Commands::Install.board_names.include?(board)
|
|
46
|
+
raise Thor::Error, "Unknown board '#{board}'. Supported boards: #{Commands::Install.board_names.join(', ')}"
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
Commands::Install.new(version: version, board: board, port: options[:port],
|
|
@@ -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
|
|
@@ -188,7 +190,8 @@ module Prremote
|
|
|
188
190
|
next
|
|
189
191
|
end
|
|
190
192
|
|
|
191
|
-
|
|
193
|
+
ready = format_ready(buf, port)
|
|
194
|
+
return ready if ready
|
|
192
195
|
return '(not responding)' if Time.now > deadline
|
|
193
196
|
|
|
194
197
|
sleep 0.05
|
|
@@ -199,6 +202,16 @@ module Prremote
|
|
|
199
202
|
serial&.close
|
|
200
203
|
end
|
|
201
204
|
|
|
205
|
+
# "0.4.1 board=m5stickc_plus (/dev/...)" from a complete READY line, else
|
|
206
|
+
# nil. Runtimes older than board names omit the board= field.
|
|
207
|
+
def format_ready(buf, port)
|
|
208
|
+
m = buf.match(%r{READY prremote-runtime/([\d.]+)(?: board=(\S+))?\s*\n})
|
|
209
|
+
return nil unless m
|
|
210
|
+
|
|
211
|
+
board = m[2] ? " board=#{m[2]}" : ''
|
|
212
|
+
"#{m[1]}#{board} (#{port})"
|
|
213
|
+
end
|
|
214
|
+
|
|
202
215
|
def resolve_port
|
|
203
216
|
return options[:port] if options[:port]
|
|
204
217
|
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
require 'open3'
|
|
2
|
-
require 'tempfile'
|
|
3
|
-
require_relative '../mrbc'
|
|
4
1
|
require_relative 'serial_helpers'
|
|
5
2
|
|
|
6
3
|
module Prremote
|
|
@@ -20,29 +17,19 @@ module Prremote
|
|
|
20
17
|
rb_paths.each { |f| raise "File not found: #{f}" unless File.exist?(f) }
|
|
21
18
|
|
|
22
19
|
warn "Compiling #{rb_paths.map { |f| File.basename(f) }.join(', ')}..."
|
|
23
|
-
mrb_data =
|
|
20
|
+
mrb_data = compile_with_boards(rb_paths)
|
|
24
21
|
|
|
25
|
-
warn 'Deploying to flash...'
|
|
26
22
|
deploy_to_device(mrb_data, rb_paths)
|
|
27
23
|
warn 'Deployed. Script will run automatically on next boot.'
|
|
28
24
|
end
|
|
29
25
|
|
|
30
26
|
private
|
|
31
27
|
|
|
32
|
-
def compile(*rb_paths)
|
|
33
|
-
Mrbc.check_version!
|
|
34
|
-
tmp = Tempfile.new(['prremote', '.mrb'])
|
|
35
|
-
out, status = Open3.capture2e(Mrbc.bin, '-o', tmp.path, *rb_paths)
|
|
36
|
-
raise "mrbc failed:\n#{out.chomp}" unless status.success?
|
|
37
|
-
|
|
38
|
-
File.binread(tmp.path)
|
|
39
|
-
ensure
|
|
40
|
-
tmp&.close!
|
|
41
|
-
end
|
|
42
|
-
|
|
43
28
|
def deploy_to_device(mrb_data, rb_paths)
|
|
44
29
|
serial = Serial.new(@port, @baud)
|
|
45
|
-
wait_for_ready(serial)
|
|
30
|
+
check_board!(wait_for_ready(serial), rb_paths)
|
|
31
|
+
|
|
32
|
+
warn 'Deploying to flash...'
|
|
46
33
|
write_chunked(serial, DEPLOY_MAGIC + build_meta_packet(rb_paths) + mrb_data)
|
|
47
34
|
wait_for_deployed(serial)
|
|
48
35
|
ensure
|
|
@@ -1,27 +1,49 @@
|
|
|
1
1
|
require 'fileutils'
|
|
2
|
+
require_relative '../boards'
|
|
3
|
+
require_relative 'serial_helpers'
|
|
2
4
|
|
|
3
5
|
module Prremote
|
|
4
6
|
module Commands
|
|
5
7
|
class Install
|
|
8
|
+
include SerialHelpers
|
|
9
|
+
|
|
10
|
+
BOARD_MAGIC = 'BORD'.freeze
|
|
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
|
+
|
|
20
|
+
# board may be a runtime image name (esp32) or a board sharing one
|
|
21
|
+
# (m5stickc_plus → esp32 image); see Boards::FIRMWARE.
|
|
6
22
|
def initialize(version: VERSION, board: 'picow', port: nil, verbose: false)
|
|
7
|
-
@version
|
|
8
|
-
@board
|
|
9
|
-
@
|
|
10
|
-
@
|
|
23
|
+
@version = version
|
|
24
|
+
@board = board
|
|
25
|
+
@firmware = Boards.firmware_for(board)
|
|
26
|
+
@port = port
|
|
27
|
+
@verbose = verbose
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Runtime images plus boards that share one (m5stickc_plus).
|
|
31
|
+
def self.board_names
|
|
32
|
+
RuntimeManager::BOARDS + Boards.names
|
|
11
33
|
end
|
|
12
34
|
|
|
13
35
|
def call
|
|
14
|
-
return install_esp32 if RuntimeManager::ESP32_BOARDS.include?(@
|
|
36
|
+
return install_esp32 if RuntimeManager::ESP32_BOARDS.include?(@firmware)
|
|
15
37
|
|
|
16
38
|
uf2_path = RuntimeManager.fetch(@version, @board)
|
|
17
39
|
|
|
18
|
-
|
|
40
|
+
warn_unverified
|
|
19
41
|
puts "Put the #{device_label} into BOOTSEL mode:"
|
|
20
42
|
puts ' 1. Hold the BOOTSEL button'
|
|
21
43
|
puts ' 2. Connect USB (or press RUN while holding BOOTSEL)'
|
|
22
|
-
puts
|
|
44
|
+
puts " 3. Release BOOTSEL — #{volume_name} should appear as a USB drive"
|
|
23
45
|
puts
|
|
24
|
-
puts
|
|
46
|
+
puts "Waiting for #{volume_name}..."
|
|
25
47
|
|
|
26
48
|
volume = wait_for_volume
|
|
27
49
|
puts "Copying firmware to #{volume}..."
|
|
@@ -38,21 +60,99 @@ module Prremote
|
|
|
38
60
|
# No BOOTSEL dance on ESP32: the flasher toggles DTR/RTS to enter the
|
|
39
61
|
# boot ROM by itself, so flashing works over the normal serial port.
|
|
40
62
|
def install_esp32
|
|
41
|
-
image = RuntimeManager.fetch(@version, @
|
|
63
|
+
image = RuntimeManager.fetch(@version, @firmware)
|
|
42
64
|
port = @port || Detector.find_device
|
|
43
|
-
|
|
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
|
|
44
70
|
|
|
45
71
|
puts "Flashing #{File.basename(image)} to #{port}..."
|
|
46
|
-
EspFlasher.flash(port: port, image_path: image, board: @
|
|
72
|
+
EspFlasher.flash(port: port, image_path: image, board: @firmware, verbose: @verbose)
|
|
73
|
+
record_board(port)
|
|
47
74
|
puts "Done. Runtime #{@version} installed."
|
|
48
75
|
end
|
|
49
76
|
|
|
77
|
+
# One ESP32 image serves several boards, so tell the runtime which one
|
|
78
|
+
# this is (kept in NVS; reported as board= in READY, which picks the
|
|
79
|
+
# board libraries). Installing the plain image clears it.
|
|
80
|
+
def record_board(port)
|
|
81
|
+
name = @board == @firmware ? '' : @board
|
|
82
|
+
serial = Serial.new(port, 115_200)
|
|
83
|
+
wait_for_ready(serial)
|
|
84
|
+
serial.write(BOARD_MAGIC + [name.bytesize].pack('C') + name)
|
|
85
|
+
reply = wait_for_board_reply(serial)
|
|
86
|
+
if reply.nil?
|
|
87
|
+
warn "WARNING: runtime #{@version} does not record board names; board=#{@board} not set."
|
|
88
|
+
elsif !name.empty?
|
|
89
|
+
puts "Board: #{reply}"
|
|
90
|
+
end
|
|
91
|
+
ensure
|
|
92
|
+
serial&.close
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# "BOARD <name>" on success; nil if the runtime predates the command.
|
|
96
|
+
def wait_for_board_reply(serial)
|
|
97
|
+
buf = +''
|
|
98
|
+
deadline = Time.now + 3
|
|
99
|
+
loop do
|
|
100
|
+
buf << normalize(safe_read(serial, 256))
|
|
101
|
+
return Regexp.last_match(1) if buf =~ /^BOARD (\S+)\n/
|
|
102
|
+
raise "Device rejected board name '#{@board}'" if buf.include?("ERROR board\n")
|
|
103
|
+
return nil if Time.now > deadline
|
|
104
|
+
|
|
105
|
+
sleep 0.05
|
|
106
|
+
end
|
|
107
|
+
end
|
|
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
|
+
|
|
50
127
|
def volume_paths
|
|
128
|
+
user = ENV.fetch('USER', nil)
|
|
51
129
|
[
|
|
52
|
-
|
|
53
|
-
"/run/media/#{
|
|
54
|
-
"/media/#{
|
|
55
|
-
]
|
|
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
|
|
56
156
|
end
|
|
57
157
|
|
|
58
158
|
def wait_for_volume(timeout: 60)
|
|
@@ -60,21 +160,31 @@ module Prremote
|
|
|
60
160
|
loop do
|
|
61
161
|
path = volume_paths.find { |p| File.directory?(p) }
|
|
62
162
|
return path if path
|
|
63
|
-
raise "Timed out waiting for
|
|
163
|
+
raise "Timed out waiting for #{volume_name} volume (#{timeout}s)" if Time.now > deadline
|
|
64
164
|
|
|
65
165
|
sleep 1
|
|
66
166
|
end
|
|
67
167
|
end
|
|
68
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.
|
|
69
172
|
def wait_for_unmount(volume, timeout: 30)
|
|
70
173
|
deadline = Time.now + timeout
|
|
71
174
|
loop do
|
|
72
|
-
return unless
|
|
175
|
+
return unless mounted?(volume)
|
|
73
176
|
raise "Timed out waiting for device to reboot (#{timeout}s)" if Time.now > deadline
|
|
74
177
|
|
|
75
178
|
sleep 1
|
|
76
179
|
end
|
|
77
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
|
|
78
188
|
end
|
|
79
189
|
end
|
|
80
190
|
end
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
require 'open3'
|
|
2
|
-
require 'tempfile'
|
|
3
|
-
require_relative '../mrbc'
|
|
4
1
|
require_relative 'serial_helpers'
|
|
5
2
|
|
|
6
3
|
module Prremote
|
|
@@ -17,31 +14,20 @@ module Prremote
|
|
|
17
14
|
rb_paths.each { |f| raise "File not found: #{f}" unless File.exist?(f) }
|
|
18
15
|
|
|
19
16
|
warn "Compiling #{rb_paths.map { |f| File.basename(f) }.join(', ')}..."
|
|
20
|
-
mrb_data =
|
|
17
|
+
mrb_data = compile_with_boards(rb_paths)
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
run_on_device(mrb_data)
|
|
19
|
+
run_on_device(mrb_data, rb_paths)
|
|
24
20
|
rescue Interrupt
|
|
25
21
|
warn ''
|
|
26
22
|
end
|
|
27
23
|
|
|
28
24
|
private
|
|
29
25
|
|
|
30
|
-
def
|
|
31
|
-
Mrbc.check_version!
|
|
32
|
-
tmp = Tempfile.new(['prremote', '.mrb'])
|
|
33
|
-
out, status = Open3.capture2e(Mrbc.bin, '-o', tmp.path, *rb_paths)
|
|
34
|
-
raise "mrbc failed:\n#{out.chomp}" unless status.success?
|
|
35
|
-
|
|
36
|
-
File.binread(tmp.path)
|
|
37
|
-
ensure
|
|
38
|
-
tmp&.close!
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def run_on_device(mrb_data)
|
|
26
|
+
def run_on_device(mrb_data, rb_paths)
|
|
42
27
|
serial = Serial.new(@port, @baud)
|
|
43
|
-
wait_for_ready(serial)
|
|
28
|
+
check_board!(wait_for_ready(serial), rb_paths)
|
|
44
29
|
|
|
30
|
+
warn 'Running...'
|
|
45
31
|
write_chunked(serial, mrb_data)
|
|
46
32
|
debug "sent #{mrb_data.bytesize} bytes (first 4: #{mrb_data[0, 4].inspect})"
|
|
47
33
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
require_relative '../boards'
|
|
2
|
+
require_relative '../mrbc'
|
|
3
|
+
|
|
1
4
|
module Prremote
|
|
2
5
|
module Commands
|
|
3
6
|
module SerialHelpers
|
|
@@ -16,9 +19,10 @@ module Prremote
|
|
|
16
19
|
next_ctrl = Time.now + 1
|
|
17
20
|
loop do
|
|
18
21
|
buf << normalize(safe_read(serial, 256))
|
|
19
|
-
|
|
22
|
+
# Wait for the whole line: the board= field follows the version.
|
|
23
|
+
if buf.match?(/READY [^\n]*\n/)
|
|
20
24
|
warn_if_runtime_outdated(buf)
|
|
21
|
-
return
|
|
25
|
+
return parse_ready(buf)
|
|
22
26
|
end
|
|
23
27
|
raise 'Timeout waiting for device. Run `prremote reset` if a script is running.' if Time.now > deadline
|
|
24
28
|
|
|
@@ -30,6 +34,32 @@ module Prremote
|
|
|
30
34
|
end
|
|
31
35
|
end
|
|
32
36
|
|
|
37
|
+
# READY banner fields. board is nil on runtimes older than board names.
|
|
38
|
+
Ready = Struct.new(:version, :board)
|
|
39
|
+
|
|
40
|
+
def parse_ready(buf)
|
|
41
|
+
m = buf.match(%r{READY prremote-runtime/(\S+)(?: board=(\S+))?})
|
|
42
|
+
m ? Ready.new(m[1], m[2]) : Ready.new(nil, nil)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Compiles the user's files, prepending the board libraries they
|
|
46
|
+
# reference (see Boards). Done before connecting: an open ESP32 port can
|
|
47
|
+
# reset the chip, so the gap between READY and sending stays minimal.
|
|
48
|
+
def compile_with_boards(rb_paths)
|
|
49
|
+
libs = Boards.library_paths(sources_of(rb_paths))
|
|
50
|
+
warn "Adding board library: #{libs.map { |f| File.basename(f) }.join(', ')}" unless libs.empty?
|
|
51
|
+
Mrbc.compile(*libs, *rb_paths)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# After READY: refuse a board library meant for another board.
|
|
55
|
+
def check_board!(ready, rb_paths)
|
|
56
|
+
Boards.check!(sources_of(rb_paths), ready.board)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def sources_of(rb_paths)
|
|
60
|
+
rb_paths.map { |f| File.read(f, encoding: 'UTF-8') }
|
|
61
|
+
end
|
|
62
|
+
|
|
33
63
|
def normalize(str)
|
|
34
64
|
str.gsub("\r\n", "\n").gsub("\r", '')
|
|
35
65
|
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.
|
data/lib/prremote/mrbc.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'open3'
|
|
2
|
+
require 'tempfile'
|
|
2
3
|
|
|
3
4
|
module Prremote
|
|
4
5
|
# mrbc lookup order:
|
|
@@ -43,6 +44,18 @@ module Prremote
|
|
|
43
44
|
@version_ok = true
|
|
44
45
|
end
|
|
45
46
|
|
|
47
|
+
# Compiles the given .rb files (in order) into one .mrb; returns its bytes.
|
|
48
|
+
def self.compile(*rb_paths)
|
|
49
|
+
check_version!
|
|
50
|
+
tmp = Tempfile.new(['prremote', '.mrb'])
|
|
51
|
+
out, status = Open3.capture2e(bin, '-o', tmp.path, *rb_paths)
|
|
52
|
+
raise "mrbc failed:\n#{out.chomp}" unless status.success?
|
|
53
|
+
|
|
54
|
+
File.binread(tmp.path)
|
|
55
|
+
ensure
|
|
56
|
+
tmp&.close!
|
|
57
|
+
end
|
|
58
|
+
|
|
46
59
|
private_class_method def self.path_candidates
|
|
47
60
|
ENV['PATH'].split(File::PATH_SEPARATOR)
|
|
48
61
|
.grep_v(SHIMS_RE)
|
|
@@ -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.
|
|
4
|
+
version: 0.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ITO Yosei
|
|
@@ -106,6 +106,9 @@ files:
|
|
|
106
106
|
- VERSION
|
|
107
107
|
- bin/prremote
|
|
108
108
|
- lib/prremote.rb
|
|
109
|
+
- lib/prremote/boards.rb
|
|
110
|
+
- lib/prremote/boards/bm8563.rb
|
|
111
|
+
- lib/prremote/boards/m5stickc_plus.rb
|
|
109
112
|
- lib/prremote/cli.rb
|
|
110
113
|
- lib/prremote/commands/deploy.rb
|
|
111
114
|
- lib/prremote/commands/eval_cmd.rb
|