prremote 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 38c2f68eda963d1330c36a056cd57d7f40aa20916aab7b254b190aea3f9cde11
4
- data.tar.gz: 6ef03b383f689c40a43a5449300e648cc693a1c7ebc974661398e2f290272396
3
+ metadata.gz: 9069bbf984dd62c31390570c934396581e816ad884511d248ebdf3a40657ae9d
4
+ data.tar.gz: 853acaa0a69a126503b90ce6726958d194cd56fd5abaf559ff759284d26bf1a6
5
5
  SHA512:
6
- metadata.gz: 67e8c6d8ce84d1721f23671269460fa366ab3ceaf4316f535e185a8d4395b919d6cb746197934074a8942752ed389476a0e979d41705f22cb5053a0fbade6531
7
- data.tar.gz: 88100fc592979327716cf747bebbd06c90087c8193c2bb7fc692fab36fb7a3f9d38d68b97c5b5533c3a682ca0ac4e55b39c311ff28c2ac0643aaea3fe1fd3a2f
6
+ metadata.gz: dff7ed500809d4d457b68ae7483e4d47632c5ced344d0c01237bcec981b589b57794a086508ac99ae7ac28f1b2f351e25dc3c3498b99fb87dac172c43720ebc9
7
+ data.tar.gz: 58fe0f9d4911c33c8ed9456810cef1405784ab9d778d8bb14502fb9832611877e897880ebfbbd84c29b6c69a82bf42add2f020037534fe809908816cff3409ef
data/README.md CHANGED
@@ -12,7 +12,12 @@ Inspired by [mpremote](https://docs.micropython.org/en/latest/reference/mpremote
12
12
 
13
13
  - Ruby 3.x or later
14
14
  - Raspberry Pi Pico W
15
- - `mrbc` in your PATH (for `run`, `deploy`, and `eval`) — install via `brew install mruby` on macOS
15
+ - `mrbc` (mruby 4.x) for `run`, `deploy`, and `eval`
16
+ - macOS: `brew install mruby`
17
+ - Linux: build from source — [github.com/mruby/mruby/releases](https://github.com/mruby/mruby/releases)
18
+ (`sudo apt install mruby` installs mruby 3.x which is **not compatible**)
19
+ - If `mrbc` is not on your PATH, set the `MRBC` environment variable:
20
+ `MRBC=/path/to/mrbc prremote run app.rb`
16
21
 
17
22
  ---
18
23
 
@@ -43,16 +48,18 @@ prremote run app.rb
43
48
 
44
49
  ### `install`
45
50
 
46
- Flash the prremote runtime firmware to a Pico W.
51
+ Flash the prremote runtime firmware to a Pico W or Pico.
47
52
 
48
53
  ```bash
49
- prremote install
50
- prremote install --version 0.1.1 # specify a runtime version
54
+ prremote install # Pico W (default)
55
+ prremote install --board pico # Pico (no wireless)
56
+ prremote install --version 0.1.1 # specify a runtime version
57
+ prremote install --board pico --version 0.1.1
51
58
  ```
52
59
 
53
60
  The firmware is downloaded from GitHub Releases on first use and cached in `~/.prremote/runtime/`. Subsequent installs use the cache.
54
61
 
55
- Put the Pico W into BOOTSEL mode (hold BOOTSEL, connect USB, release) when prompted.
62
+ Put the device into BOOTSEL mode (hold BOOTSEL, connect USB, release) when prompted.
56
63
 
57
64
  ---
58
65
 
@@ -140,9 +147,9 @@ Show the gem version, mrbc version, and the connected device's runtime version.
140
147
 
141
148
  ```bash
142
149
  prremote version
143
- # prremote: 0.1.0
150
+ # prremote: 0.1.1
144
151
  # mrbc: 3.3.0 (/usr/local/bin/mrbc)
145
- # runtime: 0.1.2
152
+ # runtime: 0.1.3
146
153
  ```
147
154
 
148
155
  ---
@@ -199,4 +206,5 @@ Scripts saved via `deploy` are stored in flash and run automatically on every bo
199
206
  ## Related Projects
200
207
 
201
208
  - [mruby/c](https://github.com/mrubyc/mrubyc) — Lightweight mruby implementation used in the runtime
209
+ - [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
202
210
  - [mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html) — MicroPython equivalent (inspiration)
data/lib/prremote/cli.rb CHANGED
@@ -20,12 +20,20 @@ module Prremote
20
20
  true
21
21
  end
22
22
 
23
- desc 'install', 'Flash prremote runtime firmware to Pico W'
23
+ remove_command :tree
24
+
25
+ desc 'install', 'Flash prremote runtime firmware to Pico W or Pico'
24
26
  option :version, type: :string, desc: "Firmware version to install (default: #{RUNTIME_VERSION})"
27
+ option :board, type: :string, desc: 'Board type: pico or picow (default: picow)'
25
28
  def install
26
29
  version = options[:version] || RUNTIME_VERSION
27
- Commands::Install.new(version: version).call
28
- rescue RuntimeError => e
30
+ board = options[:board] || 'picow'
31
+ unless RuntimeManager::BOARDS.include?(board)
32
+ raise Thor::Error, "Unknown board '#{board}'. Valid values: #{RuntimeManager::BOARDS.join(', ')}"
33
+ end
34
+
35
+ Commands::Install.new(version: version, board: board).call
36
+ rescue StandardError => e
29
37
  raise Thor::Error, e.message
30
38
  end
31
39
 
@@ -33,7 +41,7 @@ module Prremote
33
41
  def run_script(file)
34
42
  port = resolve_port
35
43
  Commands::Run.new(port: port, baud: options[:baud]).call(file)
36
- rescue RuntimeError => e
44
+ rescue StandardError => e
37
45
  raise Thor::Error, e.message
38
46
  end
39
47
  map 'run' => :run_script
@@ -42,7 +50,7 @@ module Prremote
42
50
  def deploy(file)
43
51
  port = resolve_port
44
52
  Commands::Deploy.new(port: port, baud: options[:baud]).call(file)
45
- rescue RuntimeError => e
53
+ rescue StandardError => e
46
54
  raise Thor::Error, e.message
47
55
  end
48
56
 
@@ -50,7 +58,7 @@ module Prremote
50
58
  def undeploy
51
59
  port = resolve_port
52
60
  Commands::Undeploy.new(port: port, baud: options[:baud]).call
53
- rescue RuntimeError => e
61
+ rescue StandardError => e
54
62
  raise Thor::Error, e.message
55
63
  end
56
64
 
@@ -58,7 +66,7 @@ module Prremote
58
66
  def eval(expr)
59
67
  port = resolve_port
60
68
  Commands::EvalCmd.new(port: port, baud: options[:baud]).call(expr)
61
- rescue RuntimeError => e
69
+ rescue StandardError => e
62
70
  raise Thor::Error, e.message
63
71
  end
64
72
 
@@ -66,7 +74,7 @@ module Prremote
66
74
  def watch(file)
67
75
  port = resolve_port
68
76
  Commands::Watch.new(port: port, baud: options[:baud]).call(file)
69
- rescue RuntimeError => e
77
+ rescue StandardError => e
70
78
  raise Thor::Error, e.message
71
79
  end
72
80
 
@@ -87,7 +95,7 @@ module Prremote
87
95
  serial.write("\x03")
88
96
  sleep 0.1
89
97
  puts 'Reset signal sent.'
90
- rescue RuntimeError => e
98
+ rescue StandardError => e
91
99
  raise Thor::Error, e.message
92
100
  ensure
93
101
  serial&.close
@@ -98,8 +106,13 @@ module Prremote
98
106
  puts "prremote: #{VERSION}"
99
107
 
100
108
  begin
109
+ major = Mrbc.major_version
101
110
  puts "mrbc: #{Mrbc.version} (#{Mrbc.bin})"
102
- rescue RuntimeError => e
111
+ unless major && major >= Mrbc::REQUIRED_MAJOR
112
+ puts " ** mrbc is too old (need #{Mrbc::REQUIRED_MAJOR}.x) **"
113
+ puts " Hint: the path can be set via MRBC environment variable."
114
+ end
115
+ rescue StandardError => e
103
116
  puts "mrbc: (#{e.message})"
104
117
  end
105
118
 
@@ -108,7 +121,7 @@ module Prremote
108
121
 
109
122
  private
110
123
 
111
- def fetch_runtime_version
124
+ def fetch_runtime_version # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
112
125
  port = options[:port] || Detector.find_device
113
126
  return '(no device connected)' unless port
114
127
 
@@ -116,14 +129,44 @@ module Prremote
116
129
  serial.write("\x03")
117
130
  buf = +''
118
131
  deadline = Time.now + 5
132
+
119
133
  loop do
120
- buf << (serial.read(256) || '').gsub("\r\n", "\n").gsub("\r", '')
134
+ begin
135
+ buf << (serial.read(256) || '').gsub("\r\n", "\n").gsub("\r", '')
136
+ rescue StandardError
137
+ # Watchdog reboot dropped USB; wait for re-enumeration then reopen.
138
+ begin
139
+ serial.close
140
+ rescue StandardError
141
+ nil
142
+ end
143
+ serial = nil
144
+ reopen_deadline = [deadline, Time.now + 8].min
145
+ loop do
146
+ return '(not responding)' if Time.now > reopen_deadline
147
+
148
+ p = options[:port] || Detector.find_device
149
+ if p && File.exist?(p)
150
+ serial = begin
151
+ Serial.new(p, options[:baud])
152
+ rescue StandardError
153
+ nil
154
+ end
155
+ break if serial
156
+ end
157
+ sleep 0.3
158
+ end
159
+ buf = +''
160
+ next
161
+ end
162
+
121
163
  return ::Regexp.last_match(1) if buf =~ %r{READY prremote-runtime/([\d.]+)}
122
- break if Time.now > deadline
164
+ return '(not responding)' if Time.now > deadline
123
165
 
124
166
  sleep 0.05
125
167
  end
126
- '(not responding)'
168
+ rescue StandardError => e
169
+ "(#{e.message})"
127
170
  ensure
128
171
  serial&.close
129
172
  end
@@ -26,6 +26,7 @@ module Prremote
26
26
  private
27
27
 
28
28
  def compile(rb_path)
29
+ Mrbc.check_version!
29
30
  tmp = Tempfile.new(['prremote', '.mrb'])
30
31
  out, status = Open3.capture2e(Mrbc.bin, '-o', tmp.path, rb_path)
31
32
  raise "mrbc failed:\n#{out.chomp}" unless status.success?
@@ -3,14 +3,16 @@ require 'fileutils'
3
3
  module Prremote
4
4
  module Commands
5
5
  class Install
6
- def initialize(version: RUNTIME_VERSION)
6
+ def initialize(version: RUNTIME_VERSION, board: 'picow')
7
7
  @version = version
8
+ @board = board
8
9
  end
9
10
 
10
11
  def call
11
- uf2_path = RuntimeManager.fetch(@version)
12
+ uf2_path = RuntimeManager.fetch(@version, @board)
12
13
 
13
- puts 'Put the Pico W into BOOTSEL mode:'
14
+ device_label = @board == 'picow' ? 'Pico W' : 'Pico'
15
+ puts "Put the #{device_label} into BOOTSEL mode:"
14
16
  puts ' 1. Hold the BOOTSEL button'
15
17
  puts ' 2. Connect USB (or press RUN while holding BOOTSEL)'
16
18
  puts ' 3. Release BOOTSEL — RPI-RP2 should appear as a USB drive'
@@ -23,6 +23,7 @@ module Prremote
23
23
  private
24
24
 
25
25
  def compile(rb_path)
26
+ Mrbc.check_version!
26
27
  tmp = Tempfile.new(['prremote', '.mrb'])
27
28
  out, status = Open3.capture2e(Mrbc.bin, '-o', tmp.path, rb_path)
28
29
  raise "mrbc failed:\n#{out.chomp}" unless status.success?
@@ -10,18 +10,67 @@ module Prremote
10
10
 
11
11
  def call
12
12
  serial = Serial.new(@port, @baud)
13
- sleep 0.5
14
- serial.read(4096)
13
+ serial.write("\x03")
14
+
15
+ # \x03 may trigger a watchdog reboot (if a script was running).
16
+ # wait_for_ready returns the active serial (original or reopened).
17
+ serial = wait_for_ready(serial)
15
18
 
16
19
  serial.write(ERASE_MAGIC)
17
20
  wait_for_erased(serial)
18
21
  warn 'Flash erased. Device will no longer auto-run a script on boot.'
19
22
  ensure
20
- serial&.close
23
+ begin
24
+ serial&.close
25
+ rescue StandardError
26
+ nil
27
+ end
21
28
  end
22
29
 
23
30
  private
24
31
 
32
+ # Returns the serial object in READY state (may be a new object after reboot).
33
+ def wait_for_ready(serial) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
34
+ buf = +''
35
+ deadline = Time.now + 10
36
+ loop do
37
+ begin
38
+ buf << (serial.read(256) || '').gsub("\r\n", "\n").gsub("\r", '')
39
+ rescue StandardError
40
+ # Watchdog reboot dropped USB; wait for device to re-enumerate.
41
+ begin
42
+ serial.close
43
+ rescue StandardError
44
+ nil
45
+ end
46
+ serial = nil
47
+ reopen_deadline = [deadline, Time.now + 8].min
48
+ loop do
49
+ raise 'Timeout waiting for device to reconnect' if Time.now > reopen_deadline
50
+
51
+ candidate = Detector.find_device || @port
52
+ if candidate && File.exist?(candidate)
53
+ begin
54
+ serial = Serial.new(candidate, @baud)
55
+ @port = candidate
56
+ break
57
+ rescue StandardError
58
+ nil
59
+ end
60
+ end
61
+ sleep 0.3
62
+ end
63
+ buf = +''
64
+ next
65
+ end
66
+
67
+ return serial if buf.include?('READY prremote-runtime/')
68
+ raise 'Timeout waiting for READY' if Time.now > deadline
69
+
70
+ sleep 0.05
71
+ end
72
+ end
73
+
25
74
  def wait_for_erased(serial)
26
75
  buf = +''
27
76
  deadline = Time.now + 30
@@ -34,7 +34,7 @@ module Prremote
34
34
 
35
35
  def run(rb_path)
36
36
  Run.new(port: @port, baud: @baud).call(rb_path)
37
- rescue RuntimeError => e
37
+ rescue StandardError => e
38
38
  warn "Error: #{e.message}"
39
39
  end
40
40
  end
data/lib/prremote/mrbc.rb CHANGED
@@ -1,18 +1,21 @@
1
1
  require 'open3'
2
2
 
3
3
  module Prremote
4
+ # mrbc lookup order:
5
+ # 1. MRBC env var if set (use this to point at a non-PATH binary)
6
+ # 2. PATH entries, excluding rbenv shims — shims delegate to the project's
7
+ # .ruby-version (typically CRuby) and do not work as a standalone mrbc
8
+ # 3. rbenv direct installs (~/.rbenv/versions/mruby-*/bin/mrbc)
9
+ # The first candidate wins; version compatibility is checked in check_version!.
4
10
  module Mrbc
5
11
  SHIMS_RE = %r{\.rbenv/shims}
12
+ REQUIRED_MAJOR = 4
6
13
 
7
14
  def self.bin
8
15
  return ENV['MRBC'] if ENV['MRBC'] && File.executable?(ENV['MRBC'])
9
16
 
10
- found = ENV['PATH'].split(File::PATH_SEPARATOR)
11
- .grep_v(SHIMS_RE)
12
- .map { |d| File.join(d, 'mrbc') }
13
- .find { |f| File.executable?(f) }
14
-
15
- found || raise('mrbc not found. Install mruby: brew install mruby')
17
+ (path_candidates + rbenv_candidates).first ||
18
+ raise('mrbc not found. Install mruby 4.x: brew install mruby')
16
19
  end
17
20
 
18
21
  def self.version
@@ -21,5 +24,35 @@ module Prremote
21
24
  rescue RuntimeError
22
25
  '(mrbc not found)'
23
26
  end
27
+
28
+ def self.major_version
29
+ version[/\bmruby (\d+)\./, 1]&.to_i
30
+ end
31
+
32
+ def self.check_version!
33
+ return if @version_ok
34
+
35
+ major = major_version
36
+ unless major && major >= REQUIRED_MAJOR
37
+ raise "mrbc is mruby #{major || '?'}.x but mruby #{REQUIRED_MAJOR}.x or newer is required.\n" \
38
+ "Installed: #{version}\n" \
39
+ "Install mruby 4.x: brew install mruby\n" \
40
+ "On Linux: build from source (https://github.com/mruby/mruby/releases)"
41
+ end
42
+
43
+ @version_ok = true
44
+ end
45
+
46
+ private_class_method def self.path_candidates
47
+ ENV['PATH'].split(File::PATH_SEPARATOR)
48
+ .grep_v(SHIMS_RE)
49
+ .map { |d| File.join(d, 'mrbc') }
50
+ .select { |f| File.executable?(f) }
51
+ end
52
+
53
+ private_class_method def self.rbenv_candidates
54
+ Dir.glob(File.expand_path('~/.rbenv/versions/mruby-*/bin/mrbc'))
55
+ .select { |f| File.executable?(f) }
56
+ end
24
57
  end
25
58
  end
@@ -4,33 +4,32 @@ require 'fileutils'
4
4
 
5
5
  module Prremote
6
6
  module RuntimeManager
7
- REPO = 'lumbermill/prremote'.freeze
8
- BOARD = 'picow'.freeze
7
+ BOARDS = %w[pico picow].freeze
9
8
 
10
- def self.uf2_filename(version)
11
- "prremote-#{BOARD}-runtime-#{version}.uf2"
9
+ def self.uf2_filename(version, board)
10
+ "prremote-#{board}-runtime-#{version}.uf2"
12
11
  end
13
12
 
14
- def self.release_url(version)
15
- "https://github.com/#{REPO}/releases/download/runtime-#{version}/#{uf2_filename(version)}"
13
+ def self.release_url(version, board)
14
+ "https://github.com/lumbermill/prremote/releases/download/runtime-#{version}/#{uf2_filename(version, board)}"
16
15
  end
17
16
 
18
17
  def self.cache_dir
19
18
  File.join(Dir.home, '.prremote', 'runtime')
20
19
  end
21
20
 
22
- def self.cached_path(version)
23
- File.join(cache_dir, uf2_filename(version))
21
+ def self.cached_path(version, board)
22
+ File.join(cache_dir, uf2_filename(version, board))
24
23
  end
25
24
 
26
- def self.fetch(version)
27
- path = cached_path(version)
25
+ def self.fetch(version, board)
26
+ path = cached_path(version, board)
28
27
  return path if File.exist?(path)
29
28
 
30
29
  FileUtils.mkdir_p(cache_dir)
31
- $stderr.print "Downloading #{uf2_filename(version)}..."
30
+ $stderr.print "Downloading #{uf2_filename(version, board)}..."
32
31
  $stderr.flush
33
- download(release_url(version), path)
32
+ download(release_url(version, board), path)
34
33
  warn ' done.'
35
34
  path
36
35
  end
@@ -1,4 +1,4 @@
1
1
  module Prremote
2
- VERSION = '0.1.0'.freeze
3
- RUNTIME_VERSION = '0.1.2'.freeze
2
+ VERSION = '0.1.2'
3
+ RUNTIME_VERSION = '0.1.3'
4
4
  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.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - ITO Yosei