prremote 0.3.2 → 0.4.1
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 +29 -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 +16 -5
- data/lib/prremote/commands/deploy.rb +4 -17
- data/lib/prremote/commands/install.rb +54 -7
- data/lib/prremote/commands/run.rb +5 -19
- data/lib/prremote/commands/serial_helpers.rb +32 -2
- data/lib/prremote/mrbc.rb +13 -0
- 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: fa8044b474d22090a340385b2995d1e242b3aa44f820e0f7dbd3486b4b141a77
|
|
4
|
+
data.tar.gz: 9af86c303d859f714b0f566d1910f200d1e3c70a5e56b8b9e799d61299c10aa6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab718d4431f723ed4af6d17ad744654bf547cf167afa7f1e5bd60aebb96c2d7bd6470a14898a769c7660a5c959331dff5a71527297b4245e9418abe02511c727
|
|
7
|
+
data.tar.gz: 220d6bf77f25475dd25ecf84ffd09a06ae592469e6c2bc90e4af117086b44a5f408ae90a75666d3cd26a79b38b1fd9832f1470cccd838c9edd4bf54176786ec6
|
data/README.md
CHANGED
|
@@ -19,8 +19,9 @@ Inspired by [mpremote](https://docs.micropython.org/en/latest/reference/mpremote
|
|
|
19
19
|
- Ruby 3.4 or later
|
|
20
20
|
- Supported boards:
|
|
21
21
|
- [Raspberry Pi Pico W](https://www.raspberrypi.com/products/raspberry-pi-pico-w/) / [Pico](https://www.raspberrypi.com/products/raspberry-pi-pico/)
|
|
22
|
-
- ESP32 (classic) — e.g. [M5GO / M5Stack Core gen1](https://docs.m5stack.com/en/core/m5go), generic [ESP32](https://www.espressif.com/en/products/socs/esp32) dev boards
|
|
22
|
+
- 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
23
|
- ESP32-C6 (RISC-V) — e.g. [Seeed Studio XIAO ESP32C6](https://wiki.seeedstudio.com/xiao_esp32c6_getting_started/)
|
|
24
|
+
- Per-board feature status (GPIO / ADC / PWM / I2C / SPI / WiFi / sockets / LCD / IR): [docs/SUPPORT.md](docs/SUPPORT.md)
|
|
24
25
|
- `mrbc` (mruby 4.x) for `run`, `deploy`, and `eval`
|
|
25
26
|
- macOS: `brew install mruby`
|
|
26
27
|
- Linux: build from source — [github.com/mruby/mruby/releases](https://github.com/mruby/mruby/releases)
|
|
@@ -64,6 +65,7 @@ prremote install # show supported boards
|
|
|
64
65
|
prremote install -b picow # Pico W
|
|
65
66
|
prremote install -b pico # Pico (no wireless)
|
|
66
67
|
prremote install -b esp32 # ESP32 (M5GO / M5Stack Core, etc.)
|
|
68
|
+
prremote install -b m5stickc_plus # M5StickC PLUS (esp32 runtime + board library)
|
|
67
69
|
prremote install -b esp32c6 # ESP32-C6 (e.g. XIAO ESP32C6)
|
|
68
70
|
prremote install -b picow --version 0.1.1 # specify a runtime version
|
|
69
71
|
```
|
|
@@ -76,10 +78,12 @@ Pico boards: put the device into BOOTSEL mode (hold BOOTSEL, connect USB, releas
|
|
|
76
78
|
|
|
77
79
|
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).
|
|
78
80
|
|
|
79
|
-
ESP32-C6: flashing is delegated to [esptool](https://docs.espressif.com/projects/esptool/en/latest/), because the C6 boot ROM rejects the direct write the classic ESP32 accepts. Install it first (`brew install esptool`, or `pip3 install esptool`)
|
|
81
|
+
ESP32-C6: flashing is delegated to [esptool](https://docs.espressif.com/projects/esptool/en/latest/), because the C6 boot ROM rejects the direct write the classic ESP32 accepts. Install it first (`brew install esptool`, or `pip3 install esptool`). The C6's USB Serial/JTAG port is reset into the boot ROM over USB, so no buttons are needed; only if that fails does prremote fall back to waiting for manual bootloader mode — on the XIAO ESP32C6, hold **BOOT**, press **RST**, then release both.
|
|
80
82
|
|
|
81
83
|
Reflashing the runtime does not erase a deployed script on either chip.
|
|
82
84
|
|
|
85
|
+
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.
|
|
86
|
+
|
|
83
87
|
---
|
|
84
88
|
|
|
85
89
|
### `run FILE [FILE ...]`
|
|
@@ -176,8 +180,8 @@ Show the gem version, mrbc version, and the connected device's runtime version.
|
|
|
176
180
|
|
|
177
181
|
```bash
|
|
178
182
|
prremote version
|
|
179
|
-
# prremote: 0.
|
|
180
|
-
# runtime: 0.
|
|
183
|
+
# prremote: 0.4.1
|
|
184
|
+
# runtime: 0.4.1 board=picow (/dev/tty.usbmodem101)
|
|
181
185
|
# mrbc: mruby 4.0.0 (2026-04-20) (/opt/homebrew/bin/mrbc)
|
|
182
186
|
```
|
|
183
187
|
|
|
@@ -212,9 +216,28 @@ prremote undeploy # remove from flash
|
|
|
212
216
|
|
|
213
217
|
---
|
|
214
218
|
|
|
219
|
+
## Board libraries
|
|
220
|
+
|
|
221
|
+
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.
|
|
222
|
+
|
|
223
|
+
| Board (`install -b`) | Classes | Covers |
|
|
224
|
+
|---|---|---|
|
|
225
|
+
| `m5stickc_plus` | `M5StickCPlus`, `BM8563` | AXP192 power-on, LCD preset per rotation, buttons A/B, red LED, buzzer, IR LED, BM8563 RTC (as `Time`) |
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
stick = M5StickCPlus.new # powers the LCD
|
|
229
|
+
lcd = stick.lcd(rotation: 1) # pins / size / RAM offset / color order preset
|
|
230
|
+
lcd.text(4, 4, stick.rtc.time.to_s, scale: 1)
|
|
231
|
+
stick.led.on if stick.button_a.pressed?
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
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.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
215
238
|
## How It Works
|
|
216
239
|
|
|
217
|
-
prremote flashes a minimal C firmware (built on mruby/c) onto the
|
|
240
|
+
prremote flashes a minimal C firmware (built on mruby/c) onto the board. The firmware:
|
|
218
241
|
|
|
219
242
|
1. Waits for a USB serial connection and sends `READY prremote-runtime/VERSION`
|
|
220
243
|
2. Receives a command from the host:
|
|
@@ -222,7 +245,7 @@ prremote flashes a minimal C firmware (built on mruby/c) onto the Pico W. The fi
|
|
|
222
245
|
- `DPLY` + `.mrb` bytecode → save to flash and confirm with `DEPLOYED` (`deploy`)
|
|
223
246
|
3. Waits for the next command
|
|
224
247
|
|
|
225
|
-
Scripts saved via `deploy` are stored in flash and run automatically on every boot. GPIO / ADC / PWM / I2C / SPI bindings are available on all boards;
|
|
248
|
+
Scripts saved via `deploy` are stored in flash and run automatically on every boot. GPIO / ADC / PWM / I2C / SPI bindings are available on all boards; `WiFi`, `Time` (NTP) and `TCPSocket` on boards with a radio (Pico W, ESP32, ESP32-C6), plus `UDPSocket` on ESP32 / ESP32-C6; an `LCD` class (ILI9342C / ILI9341 / ST7789) on ESP32 / ESP32-C6; and an `IR` transmitter class on classic ESP32 (e.g. the M5StickC PLUS IR LED). See [docs/SUPPORT.md](docs/SUPPORT.md) for the full board × feature matrix and [examples/](examples/) for copy-paste samples per board.
|
|
226
249
|
|
|
227
250
|
---
|
|
228
251
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.4.1
|
|
@@ -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],
|
|
@@ -188,7 +188,8 @@ module Prremote
|
|
|
188
188
|
next
|
|
189
189
|
end
|
|
190
190
|
|
|
191
|
-
|
|
191
|
+
ready = format_ready(buf, port)
|
|
192
|
+
return ready if ready
|
|
192
193
|
return '(not responding)' if Time.now > deadline
|
|
193
194
|
|
|
194
195
|
sleep 0.05
|
|
@@ -199,6 +200,16 @@ module Prremote
|
|
|
199
200
|
serial&.close
|
|
200
201
|
end
|
|
201
202
|
|
|
203
|
+
# "0.4.1 board=m5stickc_plus (/dev/...)" from a complete READY line, else
|
|
204
|
+
# nil. Runtimes older than board names omit the board= field.
|
|
205
|
+
def format_ready(buf, port)
|
|
206
|
+
m = buf.match(%r{READY prremote-runtime/([\d.]+)(?: board=(\S+))?\s*\n})
|
|
207
|
+
return nil unless m
|
|
208
|
+
|
|
209
|
+
board = m[2] ? " board=#{m[2]}" : ''
|
|
210
|
+
"#{m[1]}#{board} (#{port})"
|
|
211
|
+
end
|
|
212
|
+
|
|
202
213
|
def resolve_port
|
|
203
214
|
return options[:port] if options[:port]
|
|
204
215
|
|
|
@@ -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,17 +1,31 @@
|
|
|
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
|
+
# board may be a runtime image name (esp32) or a board sharing one
|
|
13
|
+
# (m5stickc_plus → esp32 image); see Boards::FIRMWARE.
|
|
6
14
|
def initialize(version: VERSION, board: 'picow', port: nil, verbose: false)
|
|
7
|
-
@version
|
|
8
|
-
@board
|
|
9
|
-
@
|
|
10
|
-
@
|
|
15
|
+
@version = version
|
|
16
|
+
@board = board
|
|
17
|
+
@firmware = Boards.firmware_for(board)
|
|
18
|
+
@port = port
|
|
19
|
+
@verbose = verbose
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Runtime images plus boards that share one (m5stickc_plus).
|
|
23
|
+
def self.board_names
|
|
24
|
+
RuntimeManager::BOARDS + Boards.names
|
|
11
25
|
end
|
|
12
26
|
|
|
13
27
|
def call
|
|
14
|
-
return install_esp32 if RuntimeManager::ESP32_BOARDS.include?(@
|
|
28
|
+
return install_esp32 if RuntimeManager::ESP32_BOARDS.include?(@firmware)
|
|
15
29
|
|
|
16
30
|
uf2_path = RuntimeManager.fetch(@version, @board)
|
|
17
31
|
|
|
@@ -38,15 +52,48 @@ module Prremote
|
|
|
38
52
|
# No BOOTSEL dance on ESP32: the flasher toggles DTR/RTS to enter the
|
|
39
53
|
# boot ROM by itself, so flashing works over the normal serial port.
|
|
40
54
|
def install_esp32
|
|
41
|
-
image = RuntimeManager.fetch(@version, @
|
|
55
|
+
image = RuntimeManager.fetch(@version, @firmware)
|
|
42
56
|
port = @port || Detector.find_device
|
|
43
57
|
raise 'No serial device found. Connect the board or pass --port.' unless port
|
|
44
58
|
|
|
45
59
|
puts "Flashing #{File.basename(image)} to #{port}..."
|
|
46
|
-
EspFlasher.flash(port: port, image_path: image, board: @
|
|
60
|
+
EspFlasher.flash(port: port, image_path: image, board: @firmware, verbose: @verbose)
|
|
61
|
+
record_board(port)
|
|
47
62
|
puts "Done. Runtime #{@version} installed."
|
|
48
63
|
end
|
|
49
64
|
|
|
65
|
+
# One ESP32 image serves several boards, so tell the runtime which one
|
|
66
|
+
# this is (kept in NVS; reported as board= in READY, which picks the
|
|
67
|
+
# board libraries). Installing the plain image clears it.
|
|
68
|
+
def record_board(port)
|
|
69
|
+
name = @board == @firmware ? '' : @board
|
|
70
|
+
serial = Serial.new(port, 115_200)
|
|
71
|
+
wait_for_ready(serial)
|
|
72
|
+
serial.write(BOARD_MAGIC + [name.bytesize].pack('C') + name)
|
|
73
|
+
reply = wait_for_board_reply(serial)
|
|
74
|
+
if reply.nil?
|
|
75
|
+
warn "WARNING: runtime #{@version} does not record board names; board=#{@board} not set."
|
|
76
|
+
elsif !name.empty?
|
|
77
|
+
puts "Board: #{reply}"
|
|
78
|
+
end
|
|
79
|
+
ensure
|
|
80
|
+
serial&.close
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# "BOARD <name>" on success; nil if the runtime predates the command.
|
|
84
|
+
def wait_for_board_reply(serial)
|
|
85
|
+
buf = +''
|
|
86
|
+
deadline = Time.now + 3
|
|
87
|
+
loop do
|
|
88
|
+
buf << normalize(safe_read(serial, 256))
|
|
89
|
+
return Regexp.last_match(1) if buf =~ /^BOARD (\S+)\n/
|
|
90
|
+
raise "Device rejected board name '#{@board}'" if buf.include?("ERROR board\n")
|
|
91
|
+
return nil if Time.now > deadline
|
|
92
|
+
|
|
93
|
+
sleep 0.05
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
50
97
|
def volume_paths
|
|
51
98
|
[
|
|
52
99
|
'/Volumes/RPI-RP2',
|
|
@@ -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/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)
|
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
|
+
version: 0.4.1
|
|
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
|