prremote 0.1.2 → 0.1.5

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: 9069bbf984dd62c31390570c934396581e816ad884511d248ebdf3a40657ae9d
4
- data.tar.gz: 853acaa0a69a126503b90ce6726958d194cd56fd5abaf559ff759284d26bf1a6
3
+ metadata.gz: b5d636b2157c9ef1ede45b69b9df264da77cb142529f0c93d0857e23cb681bca
4
+ data.tar.gz: e2683165f9b9c91ab1c063295eb06fa3c04b062ab8fbe7f2b574b3dc1593dfe5
5
5
  SHA512:
6
- metadata.gz: dff7ed500809d4d457b68ae7483e4d47632c5ced344d0c01237bcec981b589b57794a086508ac99ae7ac28f1b2f351e25dc3c3498b99fb87dac172c43720ebc9
7
- data.tar.gz: 58fe0f9d4911c33c8ed9456810cef1405784ab9d778d8bb14502fb9832611877e897880ebfbbd84c29b6c69a82bf42add2f020037534fe809908816cff3409ef
6
+ metadata.gz: 57617c04aa3320602e05f2f3080c4abad1cbc2639ad3a2c1017f90747ff3c5653b588280984cc0a617e0560072059ae89bb2389a460e172cfe41bb926b5589ec
7
+ data.tar.gz: 16c446180dda99c3a61a80145281cad1732321aa94016e260c6e05fba30775f874cfad3b8e3ea945cb5cc3aff0079b6beb9751fa4c2d349213966fc6bdda2511
data/README.md CHANGED
@@ -6,11 +6,17 @@
6
6
 
7
7
  Inspired by [mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html) for MicroPython.
8
8
 
9
+
10
+ [![Gem Version](https://img.shields.io/gem/v/prremote)](https://rubygems.org/gems/prremote)
11
+ [![CI](https://github.com/lumbermill/prremote/actions/workflows/ci.yml/badge.svg)](https://github.com/lumbermill/prremote/actions/workflows/ci.yml)
12
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
13
+ [![Ruby](https://img.shields.io/badge/ruby-3.4%20%7C%204.0-red)](https://github.com/lumbermill/prremote)
14
+
9
15
  ---
10
16
 
11
17
  ## Requirements
12
18
 
13
- - Ruby 3.x or later
19
+ - Ruby 3.4 or later
14
20
  - Raspberry Pi Pico W
15
21
  - `mrbc` (mruby 4.x) for `run`, `deploy`, and `eval`
16
22
  - macOS: `brew install mruby`
@@ -63,25 +69,34 @@ Put the device into BOOTSEL mode (hold BOOTSEL, connect USB, release) when promp
63
69
 
64
70
  ---
65
71
 
66
- ### `run FILE`
72
+ ### `run FILE [FILE ...]`
67
73
 
68
- Compile a local `.rb` file to mruby bytecode and run it on the device immediately (one-shot).
74
+ Compile one or more local `.rb` files to mruby bytecode and run them on the device immediately (one-shot).
69
75
 
70
76
  ```bash
71
77
  prremote run app.rb
72
78
  prremote run blink.rb --port /dev/tty.usbmodem101
73
79
  ```
74
80
 
81
+ Multiple files are compiled in order into a single `.mrb`. Classes and methods defined in earlier files are available to later ones — this is the recommended alternative to `require`, which is not available in mruby/c.
82
+
83
+ ```bash
84
+ prremote run lib.rb main.rb
85
+ ```
86
+
75
87
  The device responds with `RUNNING`, streams any output, then `DONE`.
76
88
 
89
+ You can find some examples in [test/samples](test/samples/).
90
+
77
91
  ---
78
92
 
79
- ### `deploy FILE`
93
+ ### `deploy FILE [FILE ...]`
80
94
 
81
- Compile a local `.rb` file and save it to the device's flash. The script runs automatically on every boot.
95
+ Compile one or more local `.rb` files and save them to the device's flash. The script runs automatically on every boot.
82
96
 
83
97
  ```bash
84
98
  prremote deploy app.rb
99
+ prremote deploy lib.rb main.rb
85
100
  ```
86
101
 
87
102
  The device responds with `DEPLOYED` when the write is complete.
@@ -119,15 +134,16 @@ prremote reset
119
134
 
120
135
  ---
121
136
 
122
- ### `watch FILE`
137
+ ### `watch FILE [FILE ...]`
123
138
 
124
- Watch a local file for changes and automatically re-run it on the device on every save.
139
+ Watch one or more local files for changes and automatically re-run them on the device on every save.
125
140
 
126
141
  ```bash
127
142
  prremote watch app.rb
143
+ prremote watch lib.rb main.rb
128
144
  ```
129
145
 
130
- Useful during development — save your file and the device immediately runs the updated code.
146
+ Useful during development — save any watched file and the device immediately runs the updated code.
131
147
 
132
148
  ---
133
149
 
@@ -147,9 +163,9 @@ Show the gem version, mrbc version, and the connected device's runtime version.
147
163
 
148
164
  ```bash
149
165
  prremote version
150
- # prremote: 0.1.1
151
- # mrbc: 3.3.0 (/usr/local/bin/mrbc)
152
- # runtime: 0.1.3
166
+ # prremote: 0.1.5
167
+ # runtime: 0.1.5 (/dev/tty.usbmodem101)
168
+ # mrbc: mruby 4.0.0 (2026-04-20) (/opt/homebrew/bin/mrbc)
153
169
  ```
154
170
 
155
171
  ---
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.5
data/lib/prremote/cli.rb CHANGED
@@ -7,6 +7,7 @@ require_relative 'runtime_manager'
7
7
  require_relative 'commands/install'
8
8
  require_relative 'commands/deploy'
9
9
  require_relative 'commands/undeploy'
10
+ require_relative 'commands/ls'
10
11
  require_relative 'commands/run'
11
12
  require_relative 'commands/eval_cmd'
12
13
  require_relative 'commands/watch'
@@ -23,10 +24,10 @@ module Prremote
23
24
  remove_command :tree
24
25
 
25
26
  desc 'install', 'Flash prremote runtime firmware to Pico W or Pico'
26
- option :version, type: :string, desc: "Firmware version to install (default: #{RUNTIME_VERSION})"
27
+ option :version, type: :string, desc: "Firmware version to install (default: #{VERSION})"
27
28
  option :board, type: :string, desc: 'Board type: pico or picow (default: picow)'
28
29
  def install
29
- version = options[:version] || RUNTIME_VERSION
30
+ version = options[:version] || VERSION
30
31
  board = options[:board] || 'picow'
31
32
  unless RuntimeManager::BOARDS.include?(board)
32
33
  raise Thor::Error, "Unknown board '#{board}'. Valid values: #{RuntimeManager::BOARDS.join(', ')}"
@@ -37,19 +38,31 @@ module Prremote
37
38
  raise Thor::Error, e.message
38
39
  end
39
40
 
40
- desc 'run FILE', 'Compile and run a Ruby script on the device (one-shot)'
41
- def run_script(file)
41
+ desc 'run FILE [FILE ...]', 'Compile and run Ruby scripts on the device (one-shot)'
42
+ def run_script(*files)
43
+ raise Thor::Error, 'At least one file is required.' if files.empty?
44
+
42
45
  port = resolve_port
43
- Commands::Run.new(port: port, baud: options[:baud]).call(file)
46
+ Commands::Run.new(port: port, baud: options[:baud]).call(*files)
44
47
  rescue StandardError => e
45
48
  raise Thor::Error, e.message
46
49
  end
47
50
  map 'run' => :run_script
48
51
 
49
- desc 'deploy FILE', 'Compile and save a Ruby script to flash (auto-runs on boot)'
50
- def deploy(file)
52
+ desc 'deploy FILE [FILE ...]', 'Compile and save Ruby scripts to flash (auto-runs on boot)'
53
+ def deploy(*files)
54
+ raise Thor::Error, 'At least one file is required.' if files.empty?
55
+
51
56
  port = resolve_port
52
- Commands::Deploy.new(port: port, baud: options[:baud]).call(file)
57
+ Commands::Deploy.new(port: port, baud: options[:baud]).call(*files)
58
+ rescue StandardError => e
59
+ raise Thor::Error, e.message
60
+ end
61
+
62
+ desc 'ls', 'Show the script deployed to flash (use `reset` first if a script is running)'
63
+ def ls
64
+ port = resolve_port
65
+ Commands::Ls.new(port: port, baud: options[:baud]).call
53
66
  rescue StandardError => e
54
67
  raise Thor::Error, e.message
55
68
  end
@@ -70,10 +83,12 @@ module Prremote
70
83
  raise Thor::Error, e.message
71
84
  end
72
85
 
73
- desc 'watch FILE', 'Watch a Ruby file for changes and re-run on the device automatically'
74
- def watch(file)
86
+ desc 'watch FILE [FILE ...]', 'Watch Ruby files for changes and re-run on the device automatically'
87
+ def watch(*files)
88
+ raise Thor::Error, 'At least one file is required.' if files.empty?
89
+
75
90
  port = resolve_port
76
- Commands::Watch.new(port: port, baud: options[:baud]).call(file)
91
+ Commands::Watch.new(port: port, baud: options[:baud]).call(*files)
77
92
  rescue StandardError => e
78
93
  raise Thor::Error, e.message
79
94
  end
@@ -104,10 +119,11 @@ module Prremote
104
119
  desc 'version', 'Show prremote, mrbc, and device firmware version'
105
120
  def version
106
121
  puts "prremote: #{VERSION}"
122
+ puts "runtime: #{fetch_runtime_version}"
107
123
 
108
124
  begin
109
125
  major = Mrbc.major_version
110
- puts "mrbc: #{Mrbc.version} (#{Mrbc.bin})"
126
+ puts "mrbc: #{Mrbc.version} (#{Mrbc.bin})"
111
127
  unless major && major >= Mrbc::REQUIRED_MAJOR
112
128
  puts " ** mrbc is too old (need #{Mrbc::REQUIRED_MAJOR}.x) **"
113
129
  puts " Hint: the path can be set via MRBC environment variable."
@@ -115,8 +131,6 @@ module Prremote
115
131
  rescue StandardError => e
116
132
  puts "mrbc: (#{e.message})"
117
133
  end
118
-
119
- puts "runtime: #{fetch_runtime_version}"
120
134
  end
121
135
 
122
136
  private
@@ -160,7 +174,7 @@ module Prremote
160
174
  next
161
175
  end
162
176
 
163
- return ::Regexp.last_match(1) if buf =~ %r{READY prremote-runtime/([\d.]+)}
177
+ return "#{::Regexp.last_match(1)} (#{port})" if buf =~ %r{READY prremote-runtime/([\d.]+)}
164
178
  return '(not responding)' if Time.now > deadline
165
179
 
166
180
  sleep 0.05
@@ -1,34 +1,38 @@
1
1
  require 'open3'
2
2
  require 'tempfile'
3
3
  require_relative '../mrbc'
4
+ require_relative 'serial_helpers'
4
5
 
5
6
  module Prremote
6
7
  module Commands
7
8
  class Deploy
9
+ include SerialHelpers
10
+
8
11
  DEPLOY_MAGIC = 'DPLY'.freeze
12
+ META_MAGIC = 'META'.freeze
9
13
 
10
14
  def initialize(port:, baud:)
11
15
  @port = port
12
16
  @baud = baud
13
17
  end
14
18
 
15
- def call(rb_path)
16
- raise "File not found: #{rb_path}" unless File.exist?(rb_path)
19
+ def call(*rb_paths)
20
+ rb_paths.each { |f| raise "File not found: #{f}" unless File.exist?(f) }
17
21
 
18
- warn "Compiling #{rb_path}..."
19
- mrb_data = compile(rb_path)
22
+ warn "Compiling #{rb_paths.map { |f| File.basename(f) }.join(', ')}..."
23
+ mrb_data = compile(*rb_paths)
20
24
 
21
25
  warn 'Deploying to flash...'
22
- deploy_to_device(mrb_data)
26
+ deploy_to_device(mrb_data, rb_paths)
23
27
  warn 'Deployed. Script will run automatically on next boot.'
24
28
  end
25
29
 
26
30
  private
27
31
 
28
- def compile(rb_path)
32
+ def compile(*rb_paths)
29
33
  Mrbc.check_version!
30
34
  tmp = Tempfile.new(['prremote', '.mrb'])
31
- out, status = Open3.capture2e(Mrbc.bin, '-o', tmp.path, rb_path)
35
+ out, status = Open3.capture2e(Mrbc.bin, '-o', tmp.path, *rb_paths)
32
36
  raise "mrbc failed:\n#{out.chomp}" unless status.success?
33
37
 
34
38
  File.binread(tmp.path)
@@ -36,19 +40,23 @@ module Prremote
36
40
  tmp&.close!
37
41
  end
38
42
 
39
- def deploy_to_device(mrb_data)
43
+ def deploy_to_device(mrb_data, rb_paths)
40
44
  serial = Serial.new(@port, @baud)
41
- sleep 0.5
42
- serial.read(4096)
43
-
44
- serial.write(DEPLOY_MAGIC + mrb_data)
45
- debug "sent DPLY + #{mrb_data.bytesize} bytes"
46
-
45
+ wait_for_ready(serial)
46
+ serial.write(DEPLOY_MAGIC + build_meta_packet(rb_paths) + mrb_data)
47
47
  wait_for_deployed(serial)
48
48
  ensure
49
49
  serial&.close
50
50
  end
51
51
 
52
+ def build_meta_packet(rb_paths)
53
+ names_bytes = rb_paths.map { |f| File.basename(f) }.join(' ').encode('UTF-8').b
54
+ name_len = [names_bytes.bytesize, 240].min
55
+ ts = Time.now.to_i
56
+ debug "meta: #{name_len} bytes, ts=#{ts}"
57
+ META_MAGIC + [name_len].pack('C') + names_bytes[0, name_len] + [ts].pack('N')
58
+ end
59
+
52
60
  def wait_for_deployed(serial)
53
61
  buf = +''
54
62
  deadline = Time.now + 30
@@ -64,10 +72,6 @@ module Prremote
64
72
  end
65
73
  end
66
74
 
67
- def normalize(str)
68
- str.gsub("\r\n", "\n").gsub("\r", '')
69
- end
70
-
71
75
  def debug(msg)
72
76
  warn "[debug] #{msg}" if ENV['PRREMOTE_DEBUG']
73
77
  end
@@ -3,7 +3,7 @@ require 'fileutils'
3
3
  module Prremote
4
4
  module Commands
5
5
  class Install
6
- def initialize(version: RUNTIME_VERSION, board: 'picow')
6
+ def initialize(version: VERSION, board: 'picow')
7
7
  @version = version
8
8
  @board = board
9
9
  end
@@ -0,0 +1,61 @@
1
+ require_relative 'serial_helpers'
2
+
3
+ module Prremote
4
+ module Commands
5
+ class Ls
6
+ include SerialHelpers
7
+
8
+ QUERY_MAGIC = 'QURY'.freeze
9
+
10
+ def initialize(port:, baud:)
11
+ @port = port
12
+ @baud = baud
13
+ end
14
+
15
+ def call
16
+ result = query_device
17
+ if result
18
+ ts_str = if result[:timestamp].positive?
19
+ Time.at(result[:timestamp]).strftime('%Y-%m-%d %H:%M:%S')
20
+ else
21
+ '(unknown)'
22
+ end
23
+ puts 'Deployed script:'
24
+ puts " Files: #{result[:names]}"
25
+ puts " Deployed: #{ts_str}"
26
+ else
27
+ puts 'No script deployed.'
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def query_device
34
+ serial = Serial.new(@port, @baud)
35
+ wait_for_ready(serial)
36
+ serial.write(QUERY_MAGIC)
37
+
38
+ buf = +''
39
+ deadline = Time.now + 5
40
+ loop do
41
+ chunk = normalize(serial.read(256) || '')
42
+ buf << chunk
43
+
44
+ if (m = buf.match(/^DEPLOYED (\d+) (.+)\n/))
45
+ return { timestamp: m[1].to_i, names: m[2].strip }
46
+ end
47
+ return nil if buf.include?("NONE\n")
48
+
49
+ if Time.now > deadline
50
+ raise 'Timeout waiting for device response. ' \
51
+ 'If a script is running, use `prremote reset` first.'
52
+ end
53
+
54
+ sleep 0.05
55
+ end
56
+ ensure
57
+ serial&.close
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,31 +1,36 @@
1
1
  require 'open3'
2
2
  require 'tempfile'
3
3
  require_relative '../mrbc'
4
+ require_relative 'serial_helpers'
4
5
 
5
6
  module Prremote
6
7
  module Commands
7
8
  class Run
9
+ include SerialHelpers
10
+
8
11
  def initialize(port:, baud:)
9
12
  @port = port
10
13
  @baud = baud
11
14
  end
12
15
 
13
- def call(rb_path)
14
- raise "File not found: #{rb_path}" unless File.exist?(rb_path)
16
+ def call(*rb_paths)
17
+ rb_paths.each { |f| raise "File not found: #{f}" unless File.exist?(f) }
15
18
 
16
- warn "Compiling #{rb_path}..."
17
- mrb_data = compile(rb_path)
19
+ warn "Compiling #{rb_paths.map { |f| File.basename(f) }.join(', ')}..."
20
+ mrb_data = compile(*rb_paths)
18
21
 
19
22
  warn 'Running...'
20
23
  run_on_device(mrb_data)
24
+ rescue Interrupt
25
+ warn ''
21
26
  end
22
27
 
23
28
  private
24
29
 
25
- def compile(rb_path)
30
+ def compile(*rb_paths)
26
31
  Mrbc.check_version!
27
32
  tmp = Tempfile.new(['prremote', '.mrb'])
28
- out, status = Open3.capture2e(Mrbc.bin, '-o', tmp.path, rb_path)
33
+ out, status = Open3.capture2e(Mrbc.bin, '-o', tmp.path, *rb_paths)
29
34
  raise "mrbc failed:\n#{out.chomp}" unless status.success?
30
35
 
31
36
  File.binread(tmp.path)
@@ -35,15 +40,17 @@ module Prremote
35
40
 
36
41
  def run_on_device(mrb_data)
37
42
  serial = Serial.new(@port, @baud)
38
- sleep 0.5
39
- drained = serial.read(4096) || ''
40
- debug "drained #{drained.bytesize} bytes: #{drained.inspect}"
43
+ wait_for_ready(serial)
41
44
 
42
45
  serial.write(mrb_data)
43
46
  debug "sent #{mrb_data.bytesize} bytes (first 4: #{mrb_data[0, 4].inspect})"
44
47
 
45
48
  post_running = wait_for_running(serial)
46
49
  stream_until_done(serial, post_running)
50
+ rescue Interrupt
51
+ serial&.write("\x03")
52
+ sleep 0.1
53
+ raise
47
54
  ensure
48
55
  serial&.close
49
56
  end
@@ -91,10 +98,6 @@ module Prremote
91
98
  end
92
99
  end
93
100
 
94
- def normalize(str)
95
- str.gsub("\r\n", "\n").gsub("\r", '')
96
- end
97
-
98
101
  def debug(msg)
99
102
  warn "[debug] #{msg}" if ENV['PRREMOTE_DEBUG']
100
103
  end
@@ -0,0 +1,21 @@
1
+ module Prremote
2
+ module Commands
3
+ module SerialHelpers
4
+ def wait_for_ready(serial)
5
+ buf = +''
6
+ deadline = Time.now + 10
7
+ loop do
8
+ buf << normalize(serial.read(256) || '')
9
+ return if buf.include?('READY ')
10
+ raise 'Timeout waiting for device. Run `prremote reset` if a script is running.' if Time.now > deadline
11
+
12
+ sleep 0.05
13
+ end
14
+ end
15
+
16
+ def normalize(str)
17
+ str.gsub("\r\n", "\n").gsub("\r", '')
18
+ end
19
+ end
20
+ end
21
+ end
@@ -10,21 +10,22 @@ module Prremote
10
10
  @baud = baud
11
11
  end
12
12
 
13
- def call(rb_path)
14
- raise "File not found: #{rb_path}" unless File.exist?(rb_path)
13
+ def call(*rb_paths)
14
+ rb_paths.each { |f| raise "File not found: #{f}" unless File.exist?(f) }
15
15
 
16
- warn "Watching #{rb_path} (Ctrl+C to stop)..."
17
- last_mtime = File.mtime(rb_path)
18
- run(rb_path)
16
+ label = rb_paths.map { |f| File.basename(f) }.join(', ')
17
+ warn "Watching #{label} (Ctrl+C to stop)..."
18
+ mtimes = rb_paths.map { |f| File.mtime(f) }
19
+ run(*rb_paths)
19
20
 
20
21
  loop do
21
22
  sleep POLL_INTERVAL
22
- mtime = File.mtime(rb_path)
23
- next if mtime == last_mtime
23
+ new_mtimes = rb_paths.map { |f| File.mtime(f) }
24
+ next if new_mtimes == mtimes
24
25
 
25
- last_mtime = mtime
26
- warn "\n--- #{rb_path} changed, re-running ---"
27
- run(rb_path)
26
+ mtimes = new_mtimes
27
+ warn "\n--- #{label} changed, re-running ---"
28
+ run(*rb_paths)
28
29
  end
29
30
  rescue Interrupt
30
31
  warn "\nStopped watching."
@@ -32,8 +33,8 @@ module Prremote
32
33
 
33
34
  private
34
35
 
35
- def run(rb_path)
36
- Run.new(port: @port, baud: @baud).call(rb_path)
36
+ def run(*rb_paths)
37
+ Run.new(port: @port, baud: @baud).call(*rb_paths)
37
38
  rescue StandardError => e
38
39
  warn "Error: #{e.message}"
39
40
  end
@@ -1,4 +1,3 @@
1
1
  module Prremote
2
- VERSION = '0.1.2'
3
- RUNTIME_VERSION = '0.1.3'
2
+ VERSION = File.read(File.expand_path('../../VERSION', __dir__)).strip
4
3
  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.2
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - ITO Yosei
@@ -103,13 +103,16 @@ extra_rdoc_files: []
103
103
  files:
104
104
  - LICENSE
105
105
  - README.md
106
+ - VERSION
106
107
  - bin/prremote
107
108
  - lib/prremote.rb
108
109
  - lib/prremote/cli.rb
109
110
  - lib/prremote/commands/deploy.rb
110
111
  - lib/prremote/commands/eval_cmd.rb
111
112
  - lib/prremote/commands/install.rb
113
+ - lib/prremote/commands/ls.rb
112
114
  - lib/prremote/commands/run.rb
115
+ - lib/prremote/commands/serial_helpers.rb
113
116
  - lib/prremote/commands/undeploy.rb
114
117
  - lib/prremote/commands/watch.rb
115
118
  - lib/prremote/detector.rb