prremote 0.4.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a30a56541a311e708b79d18eb2f3a07b1007df6fe5c971d307ecbf069b64a78a
4
- data.tar.gz: 75bd5a9439f0eacb9237e375010e7d65423c6f8c5556d82b352d946ba3702f88
3
+ metadata.gz: fa8044b474d22090a340385b2995d1e242b3aa44f820e0f7dbd3486b4b141a77
4
+ data.tar.gz: 9af86c303d859f714b0f566d1910f200d1e3c70a5e56b8b9e799d61299c10aa6
5
5
  SHA512:
6
- metadata.gz: bb40ffda0d41b8fa8863eb330597e9f46d4dd0c963763f66a8e543e11fce491b5f969ce84a8986cc6564908f07a9d37951c5e9ed8d9a06591032e4eb7248b0bd
7
- data.tar.gz: b740c328761c6c31f0a2bd400b0baf8a537bd4d5e04637223588b9e56e695f870d2d8557790d1a92794a71f3c6be1903f31cb84a0b13ba7e5829edbcc6893dc9
6
+ metadata.gz: ab718d4431f723ed4af6d17ad744654bf547cf167afa7f1e5bd60aebb96c2d7bd6470a14898a769c7660a5c959331dff5a71527297b4245e9418abe02511c727
7
+ data.tar.gz: 220d6bf77f25475dd25ecf84ffd09a06ae592469e6c2bc90e4af117086b44a5f408ae90a75666d3cd26a79b38b1fd9832f1470cccd838c9edd4bf54176786ec6
data/README.md CHANGED
@@ -64,7 +64,8 @@ Flash the prremote runtime firmware to a supported board.
64
64
  prremote install # show supported boards
65
65
  prremote install -b picow # Pico W
66
66
  prremote install -b pico # Pico (no wireless)
67
- prremote install -b esp32 # ESP32 (M5GO / M5Stack Core, M5StickC PLUS, etc.)
67
+ prremote install -b esp32 # ESP32 (M5GO / M5Stack Core, etc.)
68
+ prremote install -b m5stickc_plus # M5StickC PLUS (esp32 runtime + board library)
68
69
  prremote install -b esp32c6 # ESP32-C6 (e.g. XIAO ESP32C6)
69
70
  prremote install -b picow --version 0.1.1 # specify a runtime version
70
71
  ```
@@ -81,6 +82,8 @@ ESP32-C6: flashing is delegated to [esptool](https://docs.espressif.com/projects
81
82
 
82
83
  Reflashing the runtime does not erase a deployed script on either chip.
83
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
+
84
87
  ---
85
88
 
86
89
  ### `run FILE [FILE ...]`
@@ -177,8 +180,8 @@ Show the gem version, mrbc version, and the connected device's runtime version.
177
180
 
178
181
  ```bash
179
182
  prremote version
180
- # prremote: 0.4.0
181
- # runtime: 0.4.0 (/dev/tty.usbmodem101)
183
+ # prremote: 0.4.1
184
+ # runtime: 0.4.1 board=picow (/dev/tty.usbmodem101)
182
185
  # mrbc: mruby 4.0.0 (2026-04-20) (/opt/homebrew/bin/mrbc)
183
186
  ```
184
187
 
@@ -213,6 +216,25 @@ prremote undeploy # remove from flash
213
216
 
214
217
  ---
215
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
+
216
238
  ## How It Works
217
239
 
218
240
  prremote flashes a minimal C firmware (built on mruby/c) onto the board. The firmware:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.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: #{RuntimeManager::BOARDS.join(', ')}"
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
- RuntimeManager::BOARDS.each { |b| puts " #{b}" }
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 RuntimeManager::BOARDS.include?(board)
46
- raise Thor::Error, "Unknown board '#{board}'. Supported boards: #{RuntimeManager::BOARDS.join(', ')}"
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
- return "#{::Regexp.last_match(1)} (#{port})" if buf =~ %r{READY prremote-runtime/([\d.]+)}
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 = compile(*rb_paths)
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 = version
8
- @board = board
9
- @port = port
10
- @verbose = verbose
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?(@board)
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, @board)
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: @board, verbose: @verbose)
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 = compile(*rb_paths)
17
+ mrb_data = compile_with_boards(rb_paths)
21
18
 
22
- warn 'Running...'
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 compile(*rb_paths)
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
- if buf.include?('READY ')
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.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