prremote 0.1.4 → 0.1.6

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: 46762e433d6de80bfe8f614bbdd3ee25fdbf976a235d69b3aecfe2039bd180f8
4
- data.tar.gz: bcff6b310ed0056d0298a6511cdefe9ffb77aadbcbf0522a379b62d6e3fcc872
3
+ metadata.gz: 45e633df22e18c19d4c78f58c93580d3f8bfee065806067415004220b50957d2
4
+ data.tar.gz: 291cfe851b01f38dad3d6bb10a827713d247b5ff0b8942043cbb2d2a57f40038
5
5
  SHA512:
6
- metadata.gz: 9ad10419ea1caafefbdbbeaf803dc80d2a39caf148d0ad545f069661acd46010ba45eecba4c1325235b06d0e43d8cc695bc042e03906d48a820fd6c14b658f53
7
- data.tar.gz: 40ba90421406056fdde9b2849b46a849bd6d642c00784c14f03b62ce12e09724d343b61d89eb97d6d9a035350f1dbd445b253cefe602a5faacc56698df36ff4c
6
+ metadata.gz: 1f1af87f2ae1c9b72ad86dbf14d7a8cc05a94078c9913cafa294bea45c71844baf3bfad804cbb3a9c5cc3bf726866695b8d4685eb658906e35ebaba9181e46e0
7
+ data.tar.gz: 4cf41d9ea379ba497d70c648100c907c7a51cdd452034a7fead5ba0a2d3021e60d5817f9fcaaa84b7254cd49cf7b4634a030af56f9228a9312b54c9284f33822
data/README.md CHANGED
@@ -69,27 +69,34 @@ Put the device into BOOTSEL mode (hold BOOTSEL, connect USB, release) when promp
69
69
 
70
70
  ---
71
71
 
72
- ### `run FILE`
72
+ ### `run FILE [FILE ...]`
73
73
 
74
- 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).
75
75
 
76
76
  ```bash
77
77
  prremote run app.rb
78
78
  prremote run blink.rb --port /dev/tty.usbmodem101
79
79
  ```
80
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
+
81
87
  The device responds with `RUNNING`, streams any output, then `DONE`.
82
88
 
83
89
  You can find some examples in [test/samples](test/samples/).
84
90
 
85
91
  ---
86
92
 
87
- ### `deploy FILE`
93
+ ### `deploy FILE [FILE ...]`
88
94
 
89
- 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.
90
96
 
91
97
  ```bash
92
98
  prremote deploy app.rb
99
+ prremote deploy lib.rb main.rb
93
100
  ```
94
101
 
95
102
  The device responds with `DEPLOYED` when the write is complete.
@@ -127,15 +134,16 @@ prremote reset
127
134
 
128
135
  ---
129
136
 
130
- ### `watch FILE`
137
+ ### `watch FILE [FILE ...]`
131
138
 
132
- 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.
133
140
 
134
141
  ```bash
135
142
  prremote watch app.rb
143
+ prremote watch lib.rb main.rb
136
144
  ```
137
145
 
138
- 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.
139
147
 
140
148
  ---
141
149
 
@@ -155,9 +163,9 @@ Show the gem version, mrbc version, and the connected device's runtime version.
155
163
 
156
164
  ```bash
157
165
  prremote version
158
- # prremote: 0.1.1
159
- # mrbc: 3.3.0 (/usr/local/bin/mrbc)
160
- # runtime: 0.1.3
166
+ # prremote: 0.1.6
167
+ # runtime: 0.1.6 (/dev/tty.usbmodem101)
168
+ # mrbc: mruby 4.0.0 (2026-04-20) (/opt/homebrew/bin/mrbc)
161
169
  ```
162
170
 
163
171
  ---
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.4
1
+ 0.1.6
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'
@@ -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
@@ -116,7 +131,6 @@ module Prremote
116
131
  rescue StandardError => e
117
132
  puts "mrbc: (#{e.message})"
118
133
  end
119
-
120
134
  end
121
135
 
122
136
  private
@@ -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
@@ -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,27 @@
1
+ module Prremote
2
+ module Commands
3
+ module SerialHelpers
4
+ def wait_for_ready(serial)
5
+ # On macOS, USB CDC TX buffers can be dropped when the host reopens the port,
6
+ # leaving the device stuck in getchar() without re-sending READY.
7
+ # Sending Ctrl+C forces the device to restart its READY loop.
8
+ sleep 0.1
9
+ serial.write("\x03") rescue nil
10
+
11
+ buf = +''
12
+ deadline = Time.now + 10
13
+ loop do
14
+ buf << normalize(serial.read(256) || '')
15
+ return if buf.include?('READY ')
16
+ raise 'Timeout waiting for device. Run `prremote reset` if a script is running.' if Time.now > deadline
17
+
18
+ sleep 0.05
19
+ end
20
+ end
21
+
22
+ def normalize(str)
23
+ str.gsub("\r\n", "\n").gsub("\r", '')
24
+ end
25
+ end
26
+ end
27
+ 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
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.4
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - ITO Yosei
@@ -110,7 +110,9 @@ files:
110
110
  - lib/prremote/commands/deploy.rb
111
111
  - lib/prremote/commands/eval_cmd.rb
112
112
  - lib/prremote/commands/install.rb
113
+ - lib/prremote/commands/ls.rb
113
114
  - lib/prremote/commands/run.rb
115
+ - lib/prremote/commands/serial_helpers.rb
114
116
  - lib/prremote/commands/undeploy.rb
115
117
  - lib/prremote/commands/watch.rb
116
118
  - lib/prremote/detector.rb