prremote 0.2.1 → 0.3.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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -13
  3. data/VERSION +1 -1
  4. data/lib/prremote/esp_flasher.rb +80 -15
  5. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c315e2104e35b23c87d7565249c2695f0bc8b9f7d0075564dda98b8fcc11491
4
- data.tar.gz: 3e8ae2b89ca93b33f7830d45413dadef6f7c2d1d0a19d51499649214d626f1a6
3
+ metadata.gz: ddaba1909af7ae567dda45d75d78c240347062d0e00a5dab966190b22f0b28eb
4
+ data.tar.gz: e9f933d93ca77aa8324f2502958150370afe8f61b902c4420d06cfeb3edfcabd
5
5
  SHA512:
6
- metadata.gz: 8bc5946b3d296fd7cfac88817b7e212f9713b13fa4a1f1748cacf84e278b0e269b0552f46b9eb0804e2ca319e50fd9e1bb48a4d81583c96f21bbcf468aa9d735
7
- data.tar.gz: 8a467d706edf4600859d7563ab4dae1a29efd3eaaeff7d4f274d0be57412129eb44777fe099620733d0e773b0d17517751c7cb41e76714655803e038e8c5629a
6
+ metadata.gz: d885014b3034e5bcbb02203f12c994544a147f5d2439af7fd2162f39591104ce220f697732e6a62a455374508f62fa8685eb2a2f4d9e67f8593d83b9ec4676db
7
+ data.tar.gz: dc99149a4481721d2347f73345684ff1dd780a9ce96e4f861376a3077e0cce5be11a3be6598e4728cf1659bab1403e9678248ab7ffbce2c17091db9e08fd2695
data/README.md CHANGED
@@ -18,9 +18,9 @@ Inspired by [mpremote](https://docs.micropython.org/en/latest/reference/mpremote
18
18
 
19
19
  - Ruby 3.4 or later
20
20
  - Supported boards:
21
- - Raspberry Pi Pico W / Pico
22
- - ESP32 (classic) — e.g. M5GO / M5Stack Core gen1, generic dev boards
23
- - ESP32-C6 (RISC-V) — e.g. Seeed Studio XIAO ESP32C6
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
23
+ - ESP32-C6 (RISC-V) — e.g. [Seeed Studio XIAO ESP32C6](https://wiki.seeedstudio.com/xiao_esp32c6_getting_started/)
24
24
  - `mrbc` (mruby 4.x) for `run`, `deploy`, and `eval`
25
25
  - macOS: `brew install mruby`
26
26
  - Linux: build from source — [github.com/mruby/mruby/releases](https://github.com/mruby/mruby/releases)
@@ -74,7 +74,11 @@ The firmware is downloaded from GitHub Releases on first use and cached in `~/.p
74
74
 
75
75
  Pico boards: put the device into BOOTSEL mode (hold BOOTSEL, connect USB, release) when prompted.
76
76
 
77
- ESP32 / ESP32-C6 boards: 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). Reflashing the runtime does not erase a deployed script.
77
+ 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
+
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`), and put the board into bootloader mode when prompted — on the XIAO ESP32C6, hold **BOOT**, press **RST**, then release both.
80
+
81
+ Reflashing the runtime does not erase a deployed script on either chip.
78
82
 
79
83
  ---
80
84
 
@@ -128,7 +132,7 @@ Evaluate a Ruby one-liner on the device.
128
132
 
129
133
  ```bash
130
134
  prremote eval "puts 1 + 1"
131
- prremote eval "CYW43.init; CYW43::GPIO.new(CYW43::GPIO::LED_PIN).write 1"
135
+ prremote eval "GPIO.led.write 1"
132
136
  ```
133
137
 
134
138
  ---
@@ -172,8 +176,8 @@ Show the gem version, mrbc version, and the connected device's runtime version.
172
176
 
173
177
  ```bash
174
178
  prremote version
175
- # prremote: 0.2.1
176
- # runtime: 0.2.1 (/dev/tty.usbmodem101)
179
+ # prremote: 0.3.1
180
+ # runtime: 0.3.1 (/dev/tty.usbmodem101)
177
181
  # mrbc: mruby 4.0.0 (2026-04-20) (/opt/homebrew/bin/mrbc)
178
182
  ```
179
183
 
@@ -218,7 +222,7 @@ prremote flashes a minimal C firmware (built on mruby/c) onto the Pico W. The fi
218
222
  - `DPLY` + `.mrb` bytecode → save to flash and confirm with `DEPLOYED` (`deploy`)
219
223
  3. Waits for the next command
220
224
 
221
- 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 (CYW43) on the Pico W; an `LCD` class (ILI9342C) on ESP32 / M5Stack.
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; a `WiFi` module on boards with a radio (Pico W and ESP32); an `LCD` class (ILI9342C) on ESP32 / M5Stack.
222
226
 
223
227
  ---
224
228
 
@@ -284,8 +288,4 @@ bundle exec rake test
284
288
 
285
289
  - [mruby/c](https://github.com/mrubyc/mrubyc) — Lightweight mruby implementation used in the runtime
286
290
  - [picotool](https://github.com/raspberrypi/picotool) — Official Raspberry Pi tool for inspecting and managing Pico devices; useful for checking what's on flash or force-rebooting outside of prremote
287
- - [mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html) — MicroPython equivalent (inspiration)
288
-
289
- https://wiki.seeedstudio.com/ja/xiao_esp32c6_getting_started/
290
-
291
- brew install esptool
291
+ - [mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html) — MicroPython equivalent (inspiration)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.1
1
+ 0.3.1
@@ -20,6 +20,7 @@ module Prremote
20
20
  FLASH_DATA = 0x03
21
21
  FLASH_END = 0x04
22
22
  SYNC = 0x08
23
+ READ_REG = 0x0A
23
24
  SPI_SET_PARAMS = 0x0B
24
25
  SPI_ATTACH = 0x0D
25
26
  CHANGE_BAUD = 0x0F
@@ -32,6 +33,16 @@ module Prremote
32
33
  # Boards with built-in USB Serial/JTAG — flashed via esptool subprocess.
33
34
  USB_JTAG_SERIAL_BOARDS = %w[esp32c6].freeze
34
35
 
36
+ # Classic ESP32 ROM only auto-attaches flash on modules with pads wired
37
+ # to the default HSPI pins. SiP packages with in-package flash (e.g.
38
+ # ESP32-PICO-D4, used on M5StickC/PLUS) route flash through pins burned
39
+ # into eFuse instead, and FLASH_BEGIN silently hangs forever (no error
40
+ # response) if SPI_ATTACH(0) is sent on those. Mirrors esptool's
41
+ # attach_flash()/get_chip_spi_pads() (cmds.py / targets/esp32.py).
42
+ EFUSE_RD_REG_BASE = 0x3FF5A000
43
+ EFUSE_BLK0_RDATA3_REG_OFFS = EFUSE_RD_REG_BASE + 0x00C
44
+ EFUSE_BLK0_RDATA5_REG_OFFS = EFUSE_RD_REG_BASE + 0x014
45
+
35
46
  # ROM status-byte count: classic ESP32 appends 4 bytes, RISC-V chips 2.
36
47
  STATUS_BYTES_BY_BOARD = Hash.new(4).freeze
37
48
 
@@ -47,14 +58,13 @@ module Prremote
47
58
  TIOCM_RTS = 0x0004
48
59
  DARWIN = RbConfig::CONFIG['host_os'] =~ /darwin/ ? true : false
49
60
  TIOCMGET = DARWIN ? 0x4004746A : 0x5415
50
- TIOCMBIS = DARWIN ? 0x8004746E : 0x5416
51
- TIOCMBIC = DARWIN ? 0x8004746F : 0x5417
61
+ TIOCMSET = DARWIN ? 0x8004746D : 0x5418
52
62
 
53
63
  class Error < RuntimeError; end
54
64
 
55
65
  # Entry point. Routes USB-JTAG/Serial boards through esptool; handles
56
66
  # classic ESP32 with the pure-Ruby protocol implementation.
57
- def self.flash(port:, image_path:, baud: 230_400, board: nil, verbose: false)
67
+ def self.flash(port:, image_path:, baud: ROM_BAUD, board: nil, verbose: false)
58
68
  if USB_JTAG_SERIAL_BOARDS.include?(board)
59
69
  return flash_via_esptool(port: port, image_path: image_path, board: board,
60
70
  verbose: verbose)
@@ -104,21 +114,42 @@ module Prremote
104
114
  MSG
105
115
  end
106
116
 
117
+ ctx = { esptool: esptool, port: port, image_path: image_path, board: board, verbose: verbose }
118
+
119
+ # USB-Serial/JTAG chips (e.g. XIAO ESP32C6) can be dropped into the
120
+ # download ROM over USB, so esptool's usb-reset flashes hands-free — no
121
+ # BOOT/RST button dance. --after hard-reset then reboots straight into the
122
+ # freshly flashed firmware. Verified on a physical XIAO ESP32C6.
123
+ return if esptool_write(ctx, before: 'usb-reset', after: 'hard-reset', connect_attempts: 7)
124
+
125
+ # Fallback for hosts/boards where usb-reset doesn't take: enter the
126
+ # bootloader by hand and let esptool retry forever. --before no-reset never
127
+ # resets the chip, so each attempt just re-sends SYNC until the manual
128
+ # BOOT/RST lands the board in download mode — no timeout to race against.
107
129
  warn ''
108
- warn 'Put the board in bootloader mode while "Connecting..." is shown:'
130
+ warn "Couldn't reset #{board} automatically. Put it in bootloader mode by hand:"
109
131
  warn ' XIAO ESP32C6: hold BOOT, press RST, release both.'
110
-
111
- cmd = [*esptool,
112
- '--chip', board, '--port', port,
113
- '--before', 'no-reset', '--after', 'no-reset',
114
- 'write-flash', '0x0', image_path]
115
- warn "[flash] #{cmd.join(' ')}" if verbose
116
- system(*cmd) or raise Error, 'esptool exited with an error'
132
+ warn 'No rush — esptool keeps retrying the SYNC handshake until it connects.'
133
+ esptool_write(ctx, before: 'no-reset', after: 'no-reset', connect_attempts: 0) or
134
+ raise Error, 'esptool exited with an error'
117
135
 
118
136
  warn ''
119
137
  warn 'Flash complete. Press RST to start the firmware.'
120
138
  end
121
139
 
140
+ # Runs one `esptool write-flash 0x0 <image>` pass, returning esptool's
141
+ # success boolean. connect_attempts 0 = retry the connect forever.
142
+ # `ctx` carries the fixed invocation context (esptool/port/image_path/board/verbose).
143
+ def self.esptool_write(ctx, before:, after:, connect_attempts:)
144
+ cmd = [*ctx[:esptool],
145
+ '--chip', ctx[:board], '--port', ctx[:port],
146
+ '--connect-attempts', connect_attempts.to_s,
147
+ '--before', before, '--after', after,
148
+ 'write-flash', '0x0', ctx[:image_path]]
149
+ warn "[flash] #{cmd.join(' ')}" if ctx[:verbose]
150
+ system(*cmd)
151
+ end
152
+
122
153
  def self.find_esptool
123
154
  dirs = ENV.fetch('PATH', '').split(File::PATH_SEPARATOR)
124
155
  %w[esptool esptool.py].each do |exe|
@@ -193,7 +224,8 @@ module Prremote
193
224
  end
194
225
 
195
226
  def write_flash(image, offset: 0)
196
- spi_attach_payload = SPI_ATTACH_LEGACY_BOARDS.include?(@board) ? [0, 0].pack('V2') : [0].pack('V')
227
+ hspi_arg = SPI_ATTACH_LEGACY_BOARDS.include?(@board) ? efuse_spi_attach_arg : 0
228
+ spi_attach_payload = SPI_ATTACH_LEGACY_BOARDS.include?(@board) ? [hspi_arg, 0].pack('V2') : [hspi_arg].pack('V')
197
229
  command(SPI_ATTACH, spi_attach_payload)
198
230
  # id, total_size, block_size, sector_size, page_size, status_mask
199
231
  command(SPI_SET_PARAMS, [0, 4 * 1024 * 1024, 64 * 1024, 4096, 256, 0xFFFF].pack('V6'))
@@ -226,6 +258,30 @@ module Prremote
226
258
  raise Error, "MD5 mismatch after flashing (device #{device_md5}, local #{local_md5})"
227
259
  end
228
260
 
261
+ # SPI_ATTACH(0) only works for modules with flash on the default HSPI
262
+ # pins. In-package flash (ESP32-PICO-D4/V3, e.g. M5StickC/PLUS) needs its
263
+ # actual pin numbers, burned into eFuse, packed into SPI_ATTACH's arg.
264
+ # Returns 0 (the "use default pins" value) when eFuse has nothing burned.
265
+ def efuse_spi_attach_arg
266
+ rdata5 = read_reg(EFUSE_BLK0_RDATA5_REG_OFFS)
267
+ clk = rdata5 & 0x1F
268
+ q = (rdata5 >> 5) & 0x1F
269
+ d = (rdata5 >> 10) & 0x1F
270
+ cs = (rdata5 >> 15) & 0x1F
271
+ hd = (read_reg(EFUSE_BLK0_RDATA3_REG_OFFS) >> 4) & 0x1F
272
+
273
+ return 0 if [clk, q, d, hd, cs].all?(&:zero?)
274
+
275
+ vlog format('in-package flash detected (CLK:%<clk>d Q:%<q>d D:%<d>d HD:%<hd>d CS:%<cs>d)',
276
+ clk: clk, q: q, d: d, hd: hd, cs: cs)
277
+ (hd << 24) | (cs << 18) | (d << 12) | (q << 6) | clk
278
+ end
279
+
280
+ def read_reg(addr)
281
+ value, = command(READ_REG, [addr].pack('V'))
282
+ value
283
+ end
284
+
229
285
  # ── request/response plumbing ──────────────────────────────────────────
230
286
 
231
287
  def command(op, payload, checksum: 0, timeout: 3)
@@ -314,12 +370,21 @@ module Prremote
314
370
  loop { break if read_frame(deadline).nil? }
315
371
  end
316
372
 
373
+ # Sets DTR and RTS together via a single TIOCMSET, matching esptool's
374
+ # UnixTightReset. Two separate TIOCMBIS/TIOCMBIC calls (the classic-reset
375
+ # approach) send DTR and RTS as separate USB control-line-state requests;
376
+ # some USB-serial adapters (seen with CP2104 on M5StickC PLUS) glitch the
377
+ # boot ROM's auto-reset during that gap.
317
378
  def set_lines(dtr:, rts:)
318
379
  return if @fd.nil?
319
380
 
320
- io = IO.for_fd(@fd, autoclose: false)
321
- io.ioctl(dtr ? TIOCMBIS : TIOCMBIC, [TIOCM_DTR].pack('L'))
322
- io.ioctl(rts ? TIOCMBIS : TIOCMBIC, [TIOCM_RTS].pack('L'))
381
+ io = IO.for_fd(@fd, autoclose: false)
382
+ buf = [0].pack('L')
383
+ io.ioctl(TIOCMGET, buf)
384
+ status = buf.unpack1('L')
385
+ status = dtr ? (status | TIOCM_DTR) : (status & ~TIOCM_DTR)
386
+ status = rts ? (status | TIOCM_RTS) : (status & ~TIOCM_RTS)
387
+ io.ioctl(TIOCMSET, [status].pack('L'))
323
388
  rescue StandardError
324
389
  nil
325
390
  end
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.2.1
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - ITO Yosei
@@ -139,7 +139,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
139
139
  - !ruby/object:Gem::Version
140
140
  version: '0'
141
141
  requirements: []
142
- rubygems_version: 4.0.10
142
+ rubygems_version: 4.0.19
143
143
  specification_version: 4
144
144
  summary: CLI tool for deploying and running mruby/c scripts on a Raspberry Pi Pico
145
145
  W over USB serial