prremote 0.2.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c370f33a2cd268d2ab4eba26c7123d195e71fc5a927c386d561e55ad5a2adb0e
4
- data.tar.gz: 181302d7b8d2cd6e78f3a76b98171bddd5fb4c9d66f8c0aacd925f4911cb4920
3
+ metadata.gz: 5c315e2104e35b23c87d7565249c2695f0bc8b9f7d0075564dda98b8fcc11491
4
+ data.tar.gz: 3e8ae2b89ca93b33f7830d45413dadef6f7c2d1d0a19d51499649214d626f1a6
5
5
  SHA512:
6
- metadata.gz: f411813a2aa5c91a59bb7ce05b74a1bbd6f85bfb4b8329f7a1df146d8a014eb65ee6750643c63d2bdb3d09ec8bd0b3943e28e14d48a459cb3d3f645bf6206a68
7
- data.tar.gz: 47f84f1d2277a04b1739f7c72f3565d9c3e8f6ba2700abc38142370d90ef928e577eb931d3ec8a8dafc5019b0ac15bd5da857cc33a1c400f4ca3ae28f59109d4
6
+ metadata.gz: 8bc5946b3d296fd7cfac88817b7e212f9713b13fa4a1f1748cacf84e278b0e269b0552f46b9eb0804e2ca319e50fd9e1bb48a4d81583c96f21bbcf468aa9d735
7
+ data.tar.gz: 8a467d706edf4600859d7563ab4dae1a29efd3eaaeff7d4f274d0be57412129eb44777fe099620733d0e773b0d17517751c7cb41e76714655803e038e8c5629a
data/README.md CHANGED
@@ -17,9 +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
- - A supported board:
20
+ - Supported boards:
21
21
  - Raspberry Pi Pico W / Pico
22
22
  - ESP32 (classic) — e.g. M5GO / M5Stack Core gen1, generic dev boards
23
+ - ESP32-C6 (RISC-V) — e.g. Seeed Studio XIAO ESP32C6
23
24
  - `mrbc` (mruby 4.x) for `run`, `deploy`, and `eval`
24
25
  - macOS: `brew install mruby`
25
26
  - Linux: build from source — [github.com/mruby/mruby/releases](https://github.com/mruby/mruby/releases)
@@ -56,21 +57,24 @@ prremote run app.rb
56
57
 
57
58
  ### `install`
58
59
 
59
- Flash the prremote runtime firmware to a Pico W, Pico, or ESP32.
60
+ Flash the prremote runtime firmware to a supported board.
60
61
 
61
62
  ```bash
62
- prremote install # Pico W (default)
63
- prremote install --board pico # Pico (no wireless)
64
- prremote install --board esp32 # ESP32 (M5GO / M5Stack Core, etc.)
65
- prremote install --version 0.1.1 # specify a runtime version
66
- prremote install --board pico --version 0.1.1
63
+ prremote install # show supported boards
64
+ prremote install -b picow # Pico W
65
+ prremote install -b pico # Pico (no wireless)
66
+ prremote install -b esp32 # ESP32 (M5GO / M5Stack Core, etc.)
67
+ prremote install -b esp32c6 # ESP32-C6 (e.g. XIAO ESP32C6)
68
+ prremote install -b picow --version 0.1.1 # specify a runtime version
67
69
  ```
68
70
 
71
+ `-b` / `--board` selects the target board. Running `install` without `--board` prints the list of supported boards.
72
+
69
73
  The firmware is downloaded from GitHub Releases on first use and cached in `~/.prremote/runtime/`. Subsequent installs use the cache.
70
74
 
71
75
  Pico boards: put the device into BOOTSEL mode (hold BOOTSEL, connect USB, release) when prompted.
72
76
 
73
- ESP32 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 / 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.
74
78
 
75
79
  ---
76
80
 
@@ -91,7 +95,7 @@ prremote run lib.rb main.rb
91
95
 
92
96
  The device responds with `RUNNING`, streams any output, then `DONE`.
93
97
 
94
- You can find some examples in [test/samples](test/samples/).
98
+ You can find some examples in [examples](examples/).
95
99
 
96
100
  ---
97
101
 
@@ -168,8 +172,8 @@ Show the gem version, mrbc version, and the connected device's runtime version.
168
172
 
169
173
  ```bash
170
174
  prremote version
171
- # prremote: 0.2.0
172
- # runtime: 0.2.0 (/dev/tty.usbmodem101)
175
+ # prremote: 0.2.1
176
+ # runtime: 0.2.1 (/dev/tty.usbmodem101)
173
177
  # mrbc: mruby 4.0.0 (2026-04-20) (/opt/homebrew/bin/mrbc)
174
178
  ```
175
179
 
@@ -180,7 +184,7 @@ prremote version
180
184
  | Option | Description |
181
185
  |---|---|
182
186
  | `--port`, `-p PORT` | Serial port (default: auto-detect) |
183
- | `--baud`, `-b N` | Baud rate (default: `115200`) |
187
+ | `--baud N` | Baud rate (default: `115200`) |
184
188
 
185
189
  ---
186
190
 
@@ -188,7 +192,7 @@ prremote version
188
192
 
189
193
  ```bash
190
194
  # First-time setup
191
- prremote install
195
+ prremote install -b picow # or: pico / esp32 / esp32c6
192
196
 
193
197
  # Manual cycle
194
198
  prremote run app.rb # compile + run (one-shot)
@@ -281,3 +285,7 @@ bundle exec rake test
281
285
  - [mruby/c](https://github.com/mrubyc/mrubyc) — Lightweight mruby implementation used in the runtime
282
286
  - [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
283
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
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.0
1
+ 0.2.1
data/lib/prremote/cli.rb CHANGED
@@ -16,7 +16,7 @@ require_relative 'commands/watch'
16
16
  module Prremote
17
17
  class CLI < Thor
18
18
  class_option :port, aliases: '-p', desc: 'Serial port (default: auto-detect)'
19
- class_option :baud, aliases: '-b', type: :numeric, default: 115_200, desc: 'Baud rate'
19
+ class_option :baud, type: :numeric, default: 115_200, desc: 'Baud rate'
20
20
 
21
21
  def self.exit_on_failure?
22
22
  true
@@ -24,17 +24,30 @@ module Prremote
24
24
 
25
25
  remove_command :tree
26
26
 
27
- desc 'install', 'Flash prremote runtime firmware to Pico W, Pico, or ESP32'
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
- option :board, type: :string, desc: 'Board type: picow, pico, or esp32 (default: picow)'
29
+ option :board, aliases: '-b', type: :string,
30
+ desc: "Board type: #{RuntimeManager::BOARDS.join(', ')}"
31
+ option :verbose, aliases: '-V', type: :boolean, default: false,
32
+ desc: 'Print step-by-step flash diagnostics'
30
33
  def install
31
34
  version = options[:version] || VERSION
32
- board = options[:board] || 'picow'
35
+ board = options[:board]
36
+
37
+ unless board
38
+ puts "Specify a board with --board / -b. Supported boards:"
39
+ RuntimeManager::BOARDS.each { |b| puts " #{b}" }
40
+ puts
41
+ puts "Example: prremote install -b esp32c6"
42
+ return
43
+ end
44
+
33
45
  unless RuntimeManager::BOARDS.include?(board)
34
- raise Thor::Error, "Unknown board '#{board}'. Valid values: #{RuntimeManager::BOARDS.join(', ')}"
46
+ raise Thor::Error, "Unknown board '#{board}'. Supported boards: #{RuntimeManager::BOARDS.join(', ')}"
35
47
  end
36
48
 
37
- Commands::Install.new(version: version, board: board, port: options[:port]).call
49
+ Commands::Install.new(version: version, board: board, port: options[:port],
50
+ verbose: options[:verbose]).call
38
51
  rescue StandardError => e
39
52
  raise Thor::Error, e.message
40
53
  end
@@ -43,7 +43,7 @@ module Prremote
43
43
  def deploy_to_device(mrb_data, rb_paths)
44
44
  serial = Serial.new(@port, @baud)
45
45
  wait_for_ready(serial)
46
- serial.write(DEPLOY_MAGIC + build_meta_packet(rb_paths) + mrb_data)
46
+ write_chunked(serial, DEPLOY_MAGIC + build_meta_packet(rb_paths) + mrb_data)
47
47
  wait_for_deployed(serial)
48
48
  ensure
49
49
  serial&.close
@@ -3,14 +3,15 @@ require 'fileutils'
3
3
  module Prremote
4
4
  module Commands
5
5
  class Install
6
- def initialize(version: VERSION, board: 'picow', port: nil)
6
+ def initialize(version: VERSION, board: 'picow', port: nil, verbose: false)
7
7
  @version = version
8
- @board = board
9
- @port = port
8
+ @board = board
9
+ @port = port
10
+ @verbose = verbose
10
11
  end
11
12
 
12
13
  def call
13
- return install_esp32 if @board == 'esp32'
14
+ return install_esp32 if RuntimeManager::ESP32_BOARDS.include?(@board)
14
15
 
15
16
  uf2_path = RuntimeManager.fetch(@version, @board)
16
17
 
@@ -42,7 +43,7 @@ module Prremote
42
43
  raise 'No serial device found. Connect the board or pass --port.' unless port
43
44
 
44
45
  puts "Flashing #{File.basename(image)} to #{port}..."
45
- EspFlasher.flash(port: port, image_path: image)
46
+ EspFlasher.flash(port: port, image_path: image, board: @board, verbose: @verbose)
46
47
  puts "Done. Runtime #{@version} installed."
47
48
  end
48
49
 
@@ -42,7 +42,7 @@ module Prremote
42
42
  serial = Serial.new(@port, @baud)
43
43
  wait_for_ready(serial)
44
44
 
45
- serial.write(mrb_data)
45
+ write_chunked(serial, mrb_data)
46
46
  debug "sent #{mrb_data.bytesize} bytes (first 4: #{mrb_data[0, 4].inspect})"
47
47
 
48
48
  post_running = wait_for_running(serial)
@@ -34,6 +34,25 @@ module Prremote
34
34
  str.gsub("\r\n", "\n").gsub("\r", '')
35
35
  end
36
36
 
37
+ # ESP32-C6's native USB Serial/JTAG drops bytes when a large payload is
38
+ # written in a single burst: its 256-byte driver RX ring buffer overflows
39
+ # faster than the firmware drains it byte-by-byte, and the controller does
40
+ # not apply USB backpressure. Writing in <=256-byte chunks with a short
41
+ # gap keeps the device from overrunning. The pacing is negligible on the
42
+ # baud-limited transports (Pico USB-CDC / ESP32 UART bridge), where a
43
+ # 256-byte chunk already takes ~22 ms to clock out at 115200 baud.
44
+ SERIAL_CHUNK_SIZE = 256
45
+ SERIAL_CHUNK_DELAY = 0.004
46
+
47
+ def write_chunked(serial, data)
48
+ i = 0
49
+ while i < data.bytesize
50
+ serial.write(data.byteslice(i, SERIAL_CHUNK_SIZE))
51
+ i += SERIAL_CHUNK_SIZE
52
+ sleep SERIAL_CHUNK_DELAY if i < data.bytesize
53
+ end
54
+ end
55
+
37
56
  # Wraps serial.read so that a device disconnect (e.g. ENXIO on macOS when
38
57
  # the Pico resets) surfaces as a human-readable error instead of a bare errno name.
39
58
  def safe_read(serial, size)
@@ -3,20 +3,19 @@ require 'digest'
3
3
  require 'rbconfig'
4
4
 
5
5
  module Prremote
6
- # Pure-Ruby ESP32 flasher speaks the Espressif serial bootloader protocol
7
- # directly so `install --board esp32` needs no esptool / Python.
6
+ # Pure-Ruby flasher for classic ESP32 (Xtensa) boards.
7
+ # Speaks the Espressif serial bootloader protocol directly so
8
+ # `install --board esp32` needs no external tools.
9
+ #
10
+ # For ESP32-C6 and other USB-JTAG/Serial chips the ROM does not support
11
+ # direct flash write/erase (FLASH_BEGIN returns error 0x38 regardless of
12
+ # parameters). esptool uploads a RAM stub before writing; we delegate those
13
+ # boards to the `esptool` CLI instead of reimplementing the stub protocol.
8
14
  #
9
- # Talks to the ESP32 (classic) ROM loader only (no stub upload): SYNC,
10
- # SPI_ATTACH, SPI_SET_PARAMS, CHANGE_BAUDRATE, FLASH_BEGIN/DATA/END and
11
- # SPI_FLASH_MD5 — enough to write a merged image at offset 0x0 and verify it.
12
15
  # Protocol reference:
13
16
  # https://docs.espressif.com/projects/esptool/en/latest/esp32/advanced-topics/serial-protocol.html
14
- #
15
- # The chip is put into (and out of) the boot ROM by toggling DTR/RTS through
16
- # the USB-UART bridge's auto-reset circuit, via ioctl on the serial fd —
17
- # macOS and Linux only.
18
17
  class EspFlasher
19
- # Command opcodes (ROM loader subset)
18
+ # Command opcodes
20
19
  FLASH_BEGIN = 0x02
21
20
  FLASH_DATA = 0x03
22
21
  FLASH_END = 0x04
@@ -26,41 +25,58 @@ module Prremote
26
25
  CHANGE_BAUD = 0x0F
27
26
  SPI_FLASH_MD5 = 0x13
28
27
 
29
- FLASH_WRITE_SIZE = 0x400 # ROM loader max data per FLASH_DATA packet
30
- STATUS_BYTES = 4 # ESP32 ROM appends 4 status bytes to responses
28
+ FLASH_WRITE_SIZE = 0x400 # max data per FLASH_DATA packet
31
29
  CHECKSUM_SEED = 0xEF
30
+ ROM_BAUD = 115_200
31
+
32
+ # Boards with built-in USB Serial/JTAG — flashed via esptool subprocess.
33
+ USB_JTAG_SERIAL_BOARDS = %w[esp32c6].freeze
32
34
 
33
- ROM_BAUD = 115_200
35
+ # ROM status-byte count: classic ESP32 appends 4 bytes, RISC-V chips 2.
36
+ STATUS_BYTES_BY_BOARD = Hash.new(4).freeze
37
+
38
+ # Classic ESP32 SPI_ATTACH takes [hspi_arg, extended_arg] (8 bytes);
39
+ # newer RISC-V chips take only [hspi_arg] (4 bytes).
40
+ SPI_ATTACH_LEGACY_BOARDS = %w[esp32].freeze
41
+
42
+ MD5_HEX_LENGTH = 32
43
+ MD5_RAW_LENGTH = 16
34
44
 
35
45
  # ioctl modem-control constants
36
46
  TIOCM_DTR = 0x0002
37
47
  TIOCM_RTS = 0x0004
38
48
  DARWIN = RbConfig::CONFIG['host_os'] =~ /darwin/ ? true : false
39
49
  TIOCMGET = DARWIN ? 0x4004746A : 0x5415
40
- TIOCMSET = DARWIN ? 0x8004746D : 0x5418
50
+ TIOCMBIS = DARWIN ? 0x8004746E : 0x5416
51
+ TIOCMBIC = DARWIN ? 0x8004746F : 0x5417
41
52
 
42
53
  class Error < RuntimeError; end
43
54
 
44
- # Flashes `image_path` at offset 0x0 and verifies it with an on-chip MD5.
45
- # The transfer runs at `baud` when rubyserial supports it locally
46
- # (macOS termios caps out at 230400), otherwise at the ROM's 115200.
47
- def self.flash(port:, image_path:, baud: 230_400)
55
+ # Entry point. Routes USB-JTAG/Serial boards through esptool; handles
56
+ # classic ESP32 with the pure-Ruby protocol implementation.
57
+ def self.flash(port:, image_path:, baud: 230_400, board: nil, verbose: false)
58
+ if USB_JTAG_SERIAL_BOARDS.include?(board)
59
+ return flash_via_esptool(port: port, image_path: image_path, board: board,
60
+ verbose: verbose)
61
+ end
62
+
48
63
  unless RbConfig::CONFIG['host_os'] =~ /darwin|linux/
49
64
  raise Error, 'pure-Ruby flashing supports macOS/Linux only; ' \
50
- 'on other systems flash with esptool: ' \
51
- "esptool write_flash 0x0 #{image_path}"
65
+ 'on other systems use esptool: ' \
66
+ "esptool write-flash 0x0 #{image_path}"
52
67
  end
53
68
 
54
- image = File.binread(image_path)
55
- serial = Serial.new(port, ROM_BAUD)
56
- flasher = new(serial, fd: serial.instance_variable_get(:@fd))
69
+ image = File.binread(image_path)
70
+ status_bytes = STATUS_BYTES_BY_BOARD[board]
71
+ serial = Serial.new(port, ROM_BAUD)
72
+ flasher = new(serial, fd: serial.instance_variable_get(:@fd),
73
+ status_bytes: status_bytes, board: board, verbose: verbose)
57
74
  begin
58
75
  flasher.enter_bootloader
59
76
  flasher.sync!
60
- serial = flasher.upgrade_baud(port, baud) if baud != ROM_BAUD && baud_supported?(baud)
77
+ upgrade = baud != ROM_BAUD && baud_supported?(baud)
78
+ serial = flasher.upgrade_baud(port, baud) if upgrade
61
79
  flasher.write_flash(image, offset: 0)
62
- # Verify before FLASH_END: the ROM loader has been seen going quiet
63
- # after that command, while MD5 right after the last block is reliable.
64
80
  flasher.verify_md5(image, offset: 0)
65
81
  flasher.finish_flash
66
82
  flasher.hard_reset
@@ -69,28 +85,68 @@ module Prremote
69
85
  end
70
86
  end
71
87
 
72
- # The local termios layer must support the baud before CHANGE_BAUDRATE is
73
- # sent to the chip — once the chip switches, there is no way back without
74
- # a re-sync, so never request a speed we cannot reopen at.
75
88
  def self.baud_supported?(baud)
76
89
  RubySerial::Posix::BAUDE_RATES.key?(baud)
77
90
  rescue NameError
78
91
  false
79
92
  end
80
93
 
81
- # `serial` needs #read/#write; `fd` enables DTR/RTS control and may be
82
- # nil in tests.
83
- def initialize(serial, fd: nil)
84
- @serial = serial
85
- @fd = fd
86
- @rxbuf = +''.b
94
+ # Flash via the `esptool` CLI (required for USB-JTAG/Serial boards whose
95
+ # ROM does not support direct write). esptool handles stub upload
96
+ # internally; we just need it installed.
97
+ def self.flash_via_esptool(port:, image_path:, board:, verbose:)
98
+ esptool = find_esptool
99
+ unless esptool
100
+ raise Error, <<~MSG.strip
101
+ Flashing #{board} requires esptool.
102
+ Install: brew install esptool
103
+ or: pip3 install esptool
104
+ MSG
105
+ end
106
+
107
+ warn ''
108
+ warn 'Put the board in bootloader mode while "Connecting..." is shown:'
109
+ 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'
117
+
118
+ warn ''
119
+ warn 'Flash complete. Press RST to start the firmware.'
87
120
  end
88
121
 
89
- # ── chip reset control (DTR/RTS via the auto-download circuit) ────────
122
+ def self.find_esptool
123
+ dirs = ENV.fetch('PATH', '').split(File::PATH_SEPARATOR)
124
+ %w[esptool esptool.py].each do |exe|
125
+ return [exe] if dirs.any? { |d| (f = File.join(d, exe)) && File.executable?(f) && !File.directory?(f) }
126
+ end
127
+ # python3 -m esptool fallback: must actually import the module to verify
128
+ return ['python3', '-m', 'esptool'] if
129
+ system('python3', '-c', 'import esptool', out: File::NULL, err: File::NULL)
130
+
131
+ nil
132
+ rescue StandardError
133
+ nil
134
+ end
135
+
136
+ # ── instance ──────────────────────────────────────────────────────────
90
137
 
138
+ # `serial` needs #read/#write; `fd` enables DTR/RTS control.
139
+ def initialize(serial, fd: nil, status_bytes: 4, board: nil, verbose: false)
140
+ @serial = serial
141
+ @fd = fd
142
+ @rxbuf = +''.b
143
+ @status_bytes = status_bytes
144
+ @board = board.to_s
145
+ @verbose = verbose
146
+ end
147
+
148
+ # Classic auto-reset: RTS→EN, DTR→IO0 (via external UART bridge).
91
149
  def enter_bootloader
92
- # esptool's "classic reset": hold EN low, release it with IO0 low so
93
- # the chip starts the ROM loader, then release IO0.
94
150
  set_lines(dtr: false, rts: true)
95
151
  sleep 0.1
96
152
  set_lines(dtr: true, rts: false)
@@ -108,27 +164,28 @@ module Prremote
108
164
 
109
165
  def sync!
110
166
  payload = [0x07, 0x07, 0x12, 0x20].pack('C4') + ([0x55] * 32).pack('C32')
111
- synced = 8.times.any? do
167
+ synced = 8.times.any? do |i|
112
168
  @rxbuf.clear
113
169
  begin
170
+ vlog format('sync: attempt %<n>d/8 (status_bytes=%<sb>d)', n: i + 1, sb: @status_bytes)
114
171
  command(SYNC, payload, timeout: 0.5)
115
172
  drain_responses
173
+ vlog 'sync: success'
116
174
  true
117
- rescue Error
175
+ rescue Error => e
176
+ vlog format('sync: attempt %<n>d failed (%<msg>s)', n: i + 1, msg: e.message)
118
177
  false
119
178
  end
120
179
  end
121
- raise Error, 'could not sync with the ESP32 boot ROM' unless synced
180
+ raise Error, 'could not sync with the ESP boot ROM' unless synced
122
181
  end
123
182
 
124
- # CHANGE_BAUDRATE, then reopen the port at the new speed. Plain open does
125
- # not touch DTR/RTS (verified with rubyserial), so the chip stays in the
126
- # bootloader across the reopen.
183
+ # CHANGE_BAUDRATE, then reopen the port at the new speed.
127
184
  def upgrade_baud(port, baud)
128
185
  command(CHANGE_BAUD, [baud, 0].pack('V2'))
129
186
  @serial.close
130
187
  sleep 0.05
131
- serial = Serial.new(port, baud)
188
+ serial = Serial.new(port, baud)
132
189
  @serial = serial
133
190
  @fd = serial.instance_variable_get(:@fd)
134
191
  @rxbuf.clear
@@ -136,36 +193,33 @@ module Prremote
136
193
  end
137
194
 
138
195
  def write_flash(image, offset: 0)
139
- command(SPI_ATTACH, [0, 0].pack('V2'))
140
- # id, total size, block size, sector size, page size, status mask
141
- command(SPI_SET_PARAMS,
142
- [0, 4 * 1024 * 1024, 64 * 1024, 4096, 256, 0xFFFF].pack('V6'))
143
-
144
- blocks = (image.bytesize + FLASH_WRITE_SIZE - 1) / FLASH_WRITE_SIZE
145
- # The ROM erases the region inside FLASH_BEGIN; allow ~30 s per MB.
146
- erase_timeout = 30 * (1 + (image.bytesize / (1024 * 1024)))
147
- command(FLASH_BEGIN,
148
- [image.bytesize, blocks, FLASH_WRITE_SIZE, offset].pack('V4'),
196
+ spi_attach_payload = SPI_ATTACH_LEGACY_BOARDS.include?(@board) ? [0, 0].pack('V2') : [0].pack('V')
197
+ command(SPI_ATTACH, spi_attach_payload)
198
+ # id, total_size, block_size, sector_size, page_size, status_mask
199
+ command(SPI_SET_PARAMS, [0, 4 * 1024 * 1024, 64 * 1024, 4096, 256, 0xFFFF].pack('V6'))
200
+
201
+ blocks = (image.bytesize + FLASH_WRITE_SIZE - 1) / FLASH_WRITE_SIZE
202
+ erase_size = blocks * FLASH_WRITE_SIZE
203
+ erase_timeout = 30 * (1 + (erase_size / (1024 * 1024)))
204
+ command(FLASH_BEGIN, [erase_size, blocks, FLASH_WRITE_SIZE, offset].pack('V4'),
149
205
  timeout: erase_timeout)
150
206
  stream_blocks(image, blocks)
151
207
  end
152
208
 
153
- # Every block is already committed once its FLASH_DATA is acked, so
154
- # FLASH_END is only a courtesy "done" (we leave the loader via hard_reset
155
- # anyway). The ESP32 ROM has been seen answering it with error 0x06 —
156
- # ignore it; the MD5 check is the source of truth.
209
+ # FLASH_END is a courtesy; hard_reset resets the chip anyway.
210
+ # The ROM has been seen returning error 0x06 here ignore it.
157
211
  def finish_flash
158
- command(FLASH_END, [1].pack('V')) # 1 = stay in the loader
212
+ command(FLASH_END, [1].pack('V'))
159
213
  rescue Error
160
214
  nil
161
215
  end
162
216
 
163
217
  def verify_md5(image, offset: 0)
164
- timeout = 8 * (1 + (image.bytesize / (1024 * 1024)))
165
- _value, data = command(SPI_FLASH_MD5,
166
- [offset, image.bytesize, 0, 0].pack('V4'),
167
- timeout: timeout)
168
- device_md5 = data[0, 32] # the ROM loader answers as 32 hex chars
218
+ timeout = 8 * (1 + (image.bytesize / (1024 * 1024)))
219
+ _v, data = command(SPI_FLASH_MD5, [offset, image.bytesize, 0, 0].pack('V4'),
220
+ timeout: timeout)
221
+ # Classic ESP32 ROM returns 32 hex ASCII chars; detect by length.
222
+ device_md5 = data.bytesize >= MD5_HEX_LENGTH ? data[0, MD5_HEX_LENGTH] : data[0, MD5_RAW_LENGTH].unpack1('H*')
169
223
  local_md5 = Digest::MD5.hexdigest(image)
170
224
  return if device_md5 == local_md5
171
225
 
@@ -174,8 +228,6 @@ module Prremote
174
228
 
175
229
  # ── request/response plumbing ──────────────────────────────────────────
176
230
 
177
- # Sends one command packet and waits for its response.
178
- # Returns [value, data] from the response (status bytes stripped).
179
231
  def command(op, payload, checksum: 0, timeout: 3)
180
232
  packet = [0x00, op, payload.bytesize].pack('CCv') +
181
233
  [checksum].pack('V') + payload
@@ -206,6 +258,10 @@ module Prremote
206
258
 
207
259
  private
208
260
 
261
+ def vlog(msg)
262
+ warn "[flash] #{msg}" if @verbose
263
+ end
264
+
209
265
  def stream_blocks(image, blocks)
210
266
  blocks.times do |seq|
211
267
  block = image.byteslice(seq * FLASH_WRITE_SIZE, FLASH_WRITE_SIZE)
@@ -218,25 +274,21 @@ module Prremote
218
274
  $stderr.print "\n"
219
275
  end
220
276
 
221
- # Returns [value, data] when the frame is the response to `op`, raises on
222
- # an error status, and returns nil for unrelated frames (stale responses).
223
277
  def parse_response(frame, op)
224
- return nil if frame.bytesize < 8 + STATUS_BYTES || frame.getbyte(0) != 0x01 ||
278
+ return nil if frame.bytesize < 8 + @status_bytes || frame.getbyte(0) != 0x01 ||
225
279
  frame.getbyte(1) != op
226
280
 
227
281
  value = frame.byteslice(4, 4).unpack1('V')
228
282
  body = frame.byteslice(8..)
229
- status = body.byteslice(-STATUS_BYTES, STATUS_BYTES)
283
+ status = body.byteslice(-@status_bytes, @status_bytes)
230
284
  if status.getbyte(0) != 0
231
285
  raise Error, format('command 0x%<op>02x failed (error 0x%<err>02x)',
232
286
  op: op, err: status.getbyte(1))
233
287
  end
234
288
 
235
- [value, body.byteslice(0...-STATUS_BYTES)]
289
+ [value, body.byteslice(0...-@status_bytes)]
236
290
  end
237
291
 
238
- # Reads from the serial port until a complete 0xC0 ... 0xC0 frame is
239
- # available or the deadline passes. Returns the decoded frame or nil.
240
292
  def read_frame(deadline)
241
293
  loop do
242
294
  start = @rxbuf.index("\xC0".b)
@@ -245,7 +297,7 @@ module Prremote
245
297
  if stop
246
298
  frame = @rxbuf.byteslice((start + 1)...stop)
247
299
  @rxbuf = @rxbuf.byteslice((stop + 1)..) || +''.b
248
- next if frame.empty? # back-to-back C0 markers
300
+ next if frame.empty?
249
301
 
250
302
  return slip_decode(frame)
251
303
  end
@@ -257,8 +309,6 @@ module Prremote
257
309
  end
258
310
  end
259
311
 
260
- # The ROM answers a successful SYNC with a burst of identical responses;
261
- # swallow them so they are not mistaken for the next command's reply.
262
312
  def drain_responses
263
313
  deadline = Time.now + 0.3
264
314
  loop { break if read_frame(deadline).nil? }
@@ -268,12 +318,10 @@ module Prremote
268
318
  return if @fd.nil?
269
319
 
270
320
  io = IO.for_fd(@fd, autoclose: false)
271
- buf = [0].pack('L')
272
- io.ioctl(TIOCMGET, buf)
273
- bits = buf.unpack1('L')
274
- bits = dtr ? (bits | TIOCM_DTR) : (bits & ~TIOCM_DTR)
275
- bits = rts ? (bits | TIOCM_RTS) : (bits & ~TIOCM_RTS)
276
- io.ioctl(TIOCMSET, [bits].pack('L'))
321
+ io.ioctl(dtr ? TIOCMBIS : TIOCMBIC, [TIOCM_DTR].pack('L'))
322
+ io.ioctl(rts ? TIOCMBIS : TIOCMBIC, [TIOCM_RTS].pack('L'))
323
+ rescue StandardError
324
+ nil
277
325
  end
278
326
 
279
327
  def progress(done, total)
@@ -4,13 +4,14 @@ require 'fileutils'
4
4
 
5
5
  module Prremote
6
6
  module RuntimeManager
7
- BOARDS = %w[pico picow esp32].freeze
7
+ BOARDS = %w[pico picow esp32 esp32c6].freeze
8
+ ESP32_BOARDS = %w[esp32 esp32c6].freeze
8
9
 
9
- # Pico boards ship as UF2 (copied to the BOOTSEL drive); ESP32 ships as a
10
- # single merged .bin (bootloader + partition table + app) flashed at 0x0
11
- # with esptool.
10
+ # Pico boards ship as UF2 (copied to the BOOTSEL drive); ESP32 family ships
11
+ # as a single merged .bin (bootloader + partition table + app) flashed at
12
+ # 0x0 with esptool.
12
13
  def self.artifact_filename(version, board)
13
- ext = board == 'esp32' ? 'bin' : 'uf2'
14
+ ext = ESP32_BOARDS.include?(board) ? 'bin' : 'uf2'
14
15
  "prremote-#{board}-runtime-#{version}.#{ext}"
15
16
  end
16
17
 
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.0
4
+ version: 0.2.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.3
142
+ rubygems_version: 4.0.10
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