prremote 0.1.0
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +202 -0
- data/bin/prremote +6 -0
- data/lib/prremote/cli.rb +140 -0
- data/lib/prremote/commands/deploy.rb +75 -0
- data/lib/prremote/commands/eval_cmd.rb +22 -0
- data/lib/prremote/commands/install.rb +62 -0
- data/lib/prremote/commands/run.rb +102 -0
- data/lib/prremote/commands/undeploy.rb +41 -0
- data/lib/prremote/commands/watch.rb +42 -0
- data/lib/prremote/detector.rb +67 -0
- data/lib/prremote/mrbc.rb +25 -0
- data/lib/prremote/runtime_manager.rb +57 -0
- data/lib/prremote/version.rb +4 -0
- data/lib/prremote.rb +2 -0
- metadata +142 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 38c2f68eda963d1330c36a056cd57d7f40aa20916aab7b254b190aea3f9cde11
|
|
4
|
+
data.tar.gz: 6ef03b383f689c40a43a5449300e648cc693a1c7ebc974661398e2f290272396
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 67e8c6d8ce84d1721f23671269460fa366ab3ceaf4316f535e185a8d4395b919d6cb746197934074a8942752ed389476a0e979d41705f22cb5053a0fbade6531
|
|
7
|
+
data.tar.gz: 88100fc592979327716cf747bebbd06c90087c8193c2bb7fc692fab36fb7a3f9d38d68b97c5b5533c3a682ca0ac4e55b39c311ff28c2ac0643aaea3fe1fd3a2f
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ITO Yosei
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# prremote
|
|
2
|
+
|
|
3
|
+
> ⚠️ This project is in early development. APIs and commands are subject to change.
|
|
4
|
+
|
|
5
|
+
**prremote** is a command-line tool for deploying and running Ruby scripts on a Raspberry Pi Pico W over USB serial. It ships a minimal [mruby/c](https://github.com/mrubyc/mrubyc) runtime firmware and lets you compile and send `.rb` files from your Mac or Linux machine directly to the device.
|
|
6
|
+
|
|
7
|
+
Inspired by [mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html) for MicroPython.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- Ruby 3.x or later
|
|
14
|
+
- Raspberry Pi Pico W
|
|
15
|
+
- `mrbc` in your PATH (for `run`, `deploy`, and `eval`) — install via `brew install mruby` on macOS
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gem install prremote
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# 1. Flash the prremote runtime to your Pico W (one-time setup)
|
|
31
|
+
prremote install
|
|
32
|
+
|
|
33
|
+
# 2. Write your app
|
|
34
|
+
echo 'puts "Hello from Pico W!"' > app.rb
|
|
35
|
+
|
|
36
|
+
# 3. Run it
|
|
37
|
+
prremote run app.rb
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Commands
|
|
43
|
+
|
|
44
|
+
### `install`
|
|
45
|
+
|
|
46
|
+
Flash the prremote runtime firmware to a Pico W.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
prremote install
|
|
50
|
+
prremote install --version 0.1.1 # specify a runtime version
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The firmware is downloaded from GitHub Releases on first use and cached in `~/.prremote/runtime/`. Subsequent installs use the cache.
|
|
54
|
+
|
|
55
|
+
Put the Pico W into BOOTSEL mode (hold BOOTSEL, connect USB, release) when prompted.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
### `run FILE`
|
|
60
|
+
|
|
61
|
+
Compile a local `.rb` file to mruby bytecode and run it on the device immediately (one-shot).
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
prremote run app.rb
|
|
65
|
+
prremote run blink.rb --port /dev/tty.usbmodem101
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The device responds with `RUNNING`, streams any output, then `DONE`.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### `deploy FILE`
|
|
73
|
+
|
|
74
|
+
Compile a local `.rb` file and save it to the device's flash. The script runs automatically on every boot.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
prremote deploy app.rb
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The device responds with `DEPLOYED` when the write is complete.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
### `undeploy`
|
|
85
|
+
|
|
86
|
+
Erase the deployed script from flash. After this, the device boots into idle mode.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
prremote undeploy
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
### `eval EXPR`
|
|
95
|
+
|
|
96
|
+
Evaluate a Ruby one-liner on the device.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
prremote eval "puts 1 + 1"
|
|
100
|
+
prremote eval "CYW43.init; CYW43::GPIO.new(CYW43::GPIO::LED_PIN).write 1"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### `reset`
|
|
106
|
+
|
|
107
|
+
Send `Ctrl+C` to interrupt a running program.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
prremote reset
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
### `watch FILE`
|
|
116
|
+
|
|
117
|
+
Watch a local file for changes and automatically re-run it on the device on every save.
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
prremote watch app.rb
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Useful during development — save your file and the device immediately runs the updated code.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
### `list`
|
|
128
|
+
|
|
129
|
+
List USB serial devices that may be prremote-compatible.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
prremote list
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
### `version`
|
|
138
|
+
|
|
139
|
+
Show the gem version, mrbc version, and the connected device's runtime version.
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
prremote version
|
|
143
|
+
# prremote: 0.1.0
|
|
144
|
+
# mrbc: 3.3.0 (/usr/local/bin/mrbc)
|
|
145
|
+
# runtime: 0.1.2
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Global Options
|
|
151
|
+
|
|
152
|
+
| Option | Description |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `--port`, `-p PORT` | Serial port (default: auto-detect) |
|
|
155
|
+
| `--baud`, `-b N` | Baud rate (default: `115200`) |
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Typical Development Workflow
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# First-time setup
|
|
163
|
+
prremote install
|
|
164
|
+
|
|
165
|
+
# Manual cycle
|
|
166
|
+
prremote run app.rb # compile + run (one-shot)
|
|
167
|
+
prremote reset # interrupt a running program
|
|
168
|
+
|
|
169
|
+
# Automated cycle (recommended)
|
|
170
|
+
prremote watch app.rb # auto-run on every file save
|
|
171
|
+
|
|
172
|
+
# Persistent deployment (auto-runs on boot)
|
|
173
|
+
prremote deploy app.rb
|
|
174
|
+
prremote undeploy # remove from flash
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## How It Works
|
|
180
|
+
|
|
181
|
+
prremote flashes a minimal C firmware (built on mruby/c) onto the Pico W. The firmware:
|
|
182
|
+
|
|
183
|
+
1. Waits for a USB serial connection and sends `READY prremote-runtime/VERSION`
|
|
184
|
+
2. Receives a command from the host:
|
|
185
|
+
- Raw `.mrb` bytecode → execute immediately and stream output (`run` / `eval` / `watch`)
|
|
186
|
+
- `DPLY` + `.mrb` bytecode → save to flash and confirm with `DEPLOYED` (`deploy`)
|
|
187
|
+
3. Waits for the next command
|
|
188
|
+
|
|
189
|
+
Scripts saved via `deploy` are stored in flash and run automatically on every boot. GPIO and WiFi (CYW43) C bindings are available in Ruby code running on the device.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
[MIT License](LICENSE)
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Related Projects
|
|
200
|
+
|
|
201
|
+
- [mruby/c](https://github.com/mrubyc/mrubyc) — Lightweight mruby implementation used in the runtime
|
|
202
|
+
- [mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html) — MicroPython equivalent (inspiration)
|
data/bin/prremote
ADDED
data/lib/prremote/cli.rb
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
require 'thor'
|
|
2
|
+
require 'rubyserial'
|
|
3
|
+
require_relative 'version'
|
|
4
|
+
require_relative 'detector'
|
|
5
|
+
require_relative 'mrbc'
|
|
6
|
+
require_relative 'runtime_manager'
|
|
7
|
+
require_relative 'commands/install'
|
|
8
|
+
require_relative 'commands/deploy'
|
|
9
|
+
require_relative 'commands/undeploy'
|
|
10
|
+
require_relative 'commands/run'
|
|
11
|
+
require_relative 'commands/eval_cmd'
|
|
12
|
+
require_relative 'commands/watch'
|
|
13
|
+
|
|
14
|
+
module Prremote
|
|
15
|
+
class CLI < Thor
|
|
16
|
+
class_option :port, aliases: '-p', desc: 'Serial port (default: auto-detect)'
|
|
17
|
+
class_option :baud, aliases: '-b', type: :numeric, default: 115_200, desc: 'Baud rate'
|
|
18
|
+
|
|
19
|
+
def self.exit_on_failure?
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
desc 'install', 'Flash prremote runtime firmware to Pico W'
|
|
24
|
+
option :version, type: :string, desc: "Firmware version to install (default: #{RUNTIME_VERSION})"
|
|
25
|
+
def install
|
|
26
|
+
version = options[:version] || RUNTIME_VERSION
|
|
27
|
+
Commands::Install.new(version: version).call
|
|
28
|
+
rescue RuntimeError => e
|
|
29
|
+
raise Thor::Error, e.message
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
desc 'run FILE', 'Compile and run a Ruby script on the device (one-shot)'
|
|
33
|
+
def run_script(file)
|
|
34
|
+
port = resolve_port
|
|
35
|
+
Commands::Run.new(port: port, baud: options[:baud]).call(file)
|
|
36
|
+
rescue RuntimeError => e
|
|
37
|
+
raise Thor::Error, e.message
|
|
38
|
+
end
|
|
39
|
+
map 'run' => :run_script
|
|
40
|
+
|
|
41
|
+
desc 'deploy FILE', 'Compile and save a Ruby script to flash (auto-runs on boot)'
|
|
42
|
+
def deploy(file)
|
|
43
|
+
port = resolve_port
|
|
44
|
+
Commands::Deploy.new(port: port, baud: options[:baud]).call(file)
|
|
45
|
+
rescue RuntimeError => e
|
|
46
|
+
raise Thor::Error, e.message
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
desc 'undeploy', 'Erase the deployed script from flash (disables auto-run on boot)'
|
|
50
|
+
def undeploy
|
|
51
|
+
port = resolve_port
|
|
52
|
+
Commands::Undeploy.new(port: port, baud: options[:baud]).call
|
|
53
|
+
rescue RuntimeError => e
|
|
54
|
+
raise Thor::Error, e.message
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
desc 'eval EXPR', 'Compile and run a one-liner Ruby expression on the device'
|
|
58
|
+
def eval(expr)
|
|
59
|
+
port = resolve_port
|
|
60
|
+
Commands::EvalCmd.new(port: port, baud: options[:baud]).call(expr)
|
|
61
|
+
rescue RuntimeError => e
|
|
62
|
+
raise Thor::Error, e.message
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
desc 'watch FILE', 'Watch a Ruby file for changes and re-run on the device automatically'
|
|
66
|
+
def watch(file)
|
|
67
|
+
port = resolve_port
|
|
68
|
+
Commands::Watch.new(port: port, baud: options[:baud]).call(file)
|
|
69
|
+
rescue RuntimeError => e
|
|
70
|
+
raise Thor::Error, e.message
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
desc 'list', 'Show available serial devices'
|
|
74
|
+
def list
|
|
75
|
+
devices = Detector.new.list_devices
|
|
76
|
+
if devices.empty?
|
|
77
|
+
puts 'No serial devices found.'
|
|
78
|
+
else
|
|
79
|
+
devices.each { |d| puts "#{d[:port]} (#{d[:label]})" }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
desc 'reset', 'Send Ctrl+C to interrupt the running program'
|
|
84
|
+
def reset
|
|
85
|
+
port = resolve_port
|
|
86
|
+
serial = Serial.new(port, options[:baud])
|
|
87
|
+
serial.write("\x03")
|
|
88
|
+
sleep 0.1
|
|
89
|
+
puts 'Reset signal sent.'
|
|
90
|
+
rescue RuntimeError => e
|
|
91
|
+
raise Thor::Error, e.message
|
|
92
|
+
ensure
|
|
93
|
+
serial&.close
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
desc 'version', 'Show prremote, mrbc, and device firmware version'
|
|
97
|
+
def version
|
|
98
|
+
puts "prremote: #{VERSION}"
|
|
99
|
+
|
|
100
|
+
begin
|
|
101
|
+
puts "mrbc: #{Mrbc.version} (#{Mrbc.bin})"
|
|
102
|
+
rescue RuntimeError => e
|
|
103
|
+
puts "mrbc: (#{e.message})"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
puts "runtime: #{fetch_runtime_version}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def fetch_runtime_version
|
|
112
|
+
port = options[:port] || Detector.find_device
|
|
113
|
+
return '(no device connected)' unless port
|
|
114
|
+
|
|
115
|
+
serial = Serial.new(port, options[:baud])
|
|
116
|
+
serial.write("\x03")
|
|
117
|
+
buf = +''
|
|
118
|
+
deadline = Time.now + 5
|
|
119
|
+
loop do
|
|
120
|
+
buf << (serial.read(256) || '').gsub("\r\n", "\n").gsub("\r", '')
|
|
121
|
+
return ::Regexp.last_match(1) if buf =~ %r{READY prremote-runtime/([\d.]+)}
|
|
122
|
+
break if Time.now > deadline
|
|
123
|
+
|
|
124
|
+
sleep 0.05
|
|
125
|
+
end
|
|
126
|
+
'(not responding)'
|
|
127
|
+
ensure
|
|
128
|
+
serial&.close
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def resolve_port
|
|
132
|
+
return options[:port] if options[:port]
|
|
133
|
+
|
|
134
|
+
port = Detector.find_device
|
|
135
|
+
raise Thor::Error, 'No device found. Use --port to specify one.' unless port
|
|
136
|
+
|
|
137
|
+
port
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require 'open3'
|
|
2
|
+
require 'tempfile'
|
|
3
|
+
require_relative '../mrbc'
|
|
4
|
+
|
|
5
|
+
module Prremote
|
|
6
|
+
module Commands
|
|
7
|
+
class Deploy
|
|
8
|
+
DEPLOY_MAGIC = 'DPLY'.freeze
|
|
9
|
+
|
|
10
|
+
def initialize(port:, baud:)
|
|
11
|
+
@port = port
|
|
12
|
+
@baud = baud
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(rb_path)
|
|
16
|
+
raise "File not found: #{rb_path}" unless File.exist?(rb_path)
|
|
17
|
+
|
|
18
|
+
warn "Compiling #{rb_path}..."
|
|
19
|
+
mrb_data = compile(rb_path)
|
|
20
|
+
|
|
21
|
+
warn 'Deploying to flash...'
|
|
22
|
+
deploy_to_device(mrb_data)
|
|
23
|
+
warn 'Deployed. Script will run automatically on next boot.'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def compile(rb_path)
|
|
29
|
+
tmp = Tempfile.new(['prremote', '.mrb'])
|
|
30
|
+
out, status = Open3.capture2e(Mrbc.bin, '-o', tmp.path, rb_path)
|
|
31
|
+
raise "mrbc failed:\n#{out.chomp}" unless status.success?
|
|
32
|
+
|
|
33
|
+
File.binread(tmp.path)
|
|
34
|
+
ensure
|
|
35
|
+
tmp&.close!
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def deploy_to_device(mrb_data)
|
|
39
|
+
serial = Serial.new(@port, @baud)
|
|
40
|
+
sleep 0.5
|
|
41
|
+
serial.read(4096)
|
|
42
|
+
|
|
43
|
+
serial.write(DEPLOY_MAGIC + mrb_data)
|
|
44
|
+
debug "sent DPLY + #{mrb_data.bytesize} bytes"
|
|
45
|
+
|
|
46
|
+
wait_for_deployed(serial)
|
|
47
|
+
ensure
|
|
48
|
+
serial&.close
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def wait_for_deployed(serial)
|
|
52
|
+
buf = +''
|
|
53
|
+
deadline = Time.now + 30
|
|
54
|
+
loop do
|
|
55
|
+
chunk = normalize(serial.read(256) || '')
|
|
56
|
+
buf << chunk unless chunk.empty?
|
|
57
|
+
|
|
58
|
+
return if buf.include?("DEPLOYED\n")
|
|
59
|
+
raise "Device error: #{buf.strip}" if buf.match?(/^ERROR /)
|
|
60
|
+
raise 'Timeout waiting for deploy confirmation' if Time.now > deadline
|
|
61
|
+
|
|
62
|
+
sleep 0.05
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def normalize(str)
|
|
67
|
+
str.gsub("\r\n", "\n").gsub("\r", '')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def debug(msg)
|
|
71
|
+
warn "[debug] #{msg}" if ENV['PRREMOTE_DEBUG']
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'tempfile'
|
|
2
|
+
require_relative 'run'
|
|
3
|
+
|
|
4
|
+
module Prremote
|
|
5
|
+
module Commands
|
|
6
|
+
class EvalCmd
|
|
7
|
+
def initialize(port:, baud:)
|
|
8
|
+
@port = port
|
|
9
|
+
@baud = baud
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call(expr)
|
|
13
|
+
tmp = Tempfile.new(['prremote_eval', '.rb'])
|
|
14
|
+
tmp.write(expr)
|
|
15
|
+
tmp.flush
|
|
16
|
+
Run.new(port: @port, baud: @baud).call(tmp.path)
|
|
17
|
+
ensure
|
|
18
|
+
tmp&.close!
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
|
|
3
|
+
module Prremote
|
|
4
|
+
module Commands
|
|
5
|
+
class Install
|
|
6
|
+
def initialize(version: RUNTIME_VERSION)
|
|
7
|
+
@version = version
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
uf2_path = RuntimeManager.fetch(@version)
|
|
12
|
+
|
|
13
|
+
puts 'Put the Pico W into BOOTSEL mode:'
|
|
14
|
+
puts ' 1. Hold the BOOTSEL button'
|
|
15
|
+
puts ' 2. Connect USB (or press RUN while holding BOOTSEL)'
|
|
16
|
+
puts ' 3. Release BOOTSEL — RPI-RP2 should appear as a USB drive'
|
|
17
|
+
puts
|
|
18
|
+
puts 'Waiting for RPI-RP2...'
|
|
19
|
+
|
|
20
|
+
volume = wait_for_volume
|
|
21
|
+
puts "Copying firmware to #{volume}..."
|
|
22
|
+
FileUtils.cp(uf2_path, File.join(volume, File.basename(uf2_path)))
|
|
23
|
+
|
|
24
|
+
puts 'Waiting for device to reboot...'
|
|
25
|
+
wait_for_unmount(volume)
|
|
26
|
+
|
|
27
|
+
puts "Done. Runtime #{@version} installed."
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def volume_paths
|
|
33
|
+
[
|
|
34
|
+
'/Volumes/RPI-RP2',
|
|
35
|
+
"/run/media/#{ENV.fetch('USER', nil)}/RPI-RP2",
|
|
36
|
+
"/media/#{ENV.fetch('USER', nil)}/RPI-RP2"
|
|
37
|
+
]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def wait_for_volume(timeout: 60)
|
|
41
|
+
deadline = Time.now + timeout
|
|
42
|
+
loop do
|
|
43
|
+
path = volume_paths.find { |p| File.directory?(p) }
|
|
44
|
+
return path if path
|
|
45
|
+
raise "Timed out waiting for RPI-RP2 volume (#{timeout}s)" if Time.now > deadline
|
|
46
|
+
|
|
47
|
+
sleep 1
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def wait_for_unmount(volume, timeout: 30)
|
|
52
|
+
deadline = Time.now + timeout
|
|
53
|
+
loop do
|
|
54
|
+
return unless File.directory?(volume)
|
|
55
|
+
raise "Timed out waiting for device to reboot (#{timeout}s)" if Time.now > deadline
|
|
56
|
+
|
|
57
|
+
sleep 1
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
require 'open3'
|
|
2
|
+
require 'tempfile'
|
|
3
|
+
require_relative '../mrbc'
|
|
4
|
+
|
|
5
|
+
module Prremote
|
|
6
|
+
module Commands
|
|
7
|
+
class Run
|
|
8
|
+
def initialize(port:, baud:)
|
|
9
|
+
@port = port
|
|
10
|
+
@baud = baud
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(rb_path)
|
|
14
|
+
raise "File not found: #{rb_path}" unless File.exist?(rb_path)
|
|
15
|
+
|
|
16
|
+
warn "Compiling #{rb_path}..."
|
|
17
|
+
mrb_data = compile(rb_path)
|
|
18
|
+
|
|
19
|
+
warn 'Running...'
|
|
20
|
+
run_on_device(mrb_data)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def compile(rb_path)
|
|
26
|
+
tmp = Tempfile.new(['prremote', '.mrb'])
|
|
27
|
+
out, status = Open3.capture2e(Mrbc.bin, '-o', tmp.path, rb_path)
|
|
28
|
+
raise "mrbc failed:\n#{out.chomp}" unless status.success?
|
|
29
|
+
|
|
30
|
+
File.binread(tmp.path)
|
|
31
|
+
ensure
|
|
32
|
+
tmp&.close!
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def run_on_device(mrb_data)
|
|
36
|
+
serial = Serial.new(@port, @baud)
|
|
37
|
+
sleep 0.5
|
|
38
|
+
drained = serial.read(4096) || ''
|
|
39
|
+
debug "drained #{drained.bytesize} bytes: #{drained.inspect}"
|
|
40
|
+
|
|
41
|
+
serial.write(mrb_data)
|
|
42
|
+
debug "sent #{mrb_data.bytesize} bytes (first 4: #{mrb_data[0, 4].inspect})"
|
|
43
|
+
|
|
44
|
+
post_running = wait_for_running(serial)
|
|
45
|
+
stream_until_done(serial, post_running)
|
|
46
|
+
ensure
|
|
47
|
+
serial&.close
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def wait_for_running(serial)
|
|
51
|
+
buf = +''
|
|
52
|
+
deadline = Time.now + 10
|
|
53
|
+
loop do
|
|
54
|
+
chunk = normalize(serial.read(256) || '')
|
|
55
|
+
unless chunk.empty?
|
|
56
|
+
debug "recv: #{chunk.inspect}"
|
|
57
|
+
buf << chunk
|
|
58
|
+
end
|
|
59
|
+
raise "Device error: #{buf.strip}" if buf.match?(/^ERROR /)
|
|
60
|
+
|
|
61
|
+
if (idx = buf.index("RUNNING\n"))
|
|
62
|
+
return buf[(idx + "RUNNING\n".length)..]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
raise 'Timeout waiting for device to start execution' if Time.now > deadline
|
|
66
|
+
|
|
67
|
+
sleep 0.05
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def stream_until_done(serial, initial = +'')
|
|
72
|
+
buf = initial
|
|
73
|
+
loop do
|
|
74
|
+
buf << normalize(serial.read(256) || '')
|
|
75
|
+
|
|
76
|
+
if (done_pos = buf.index("DONE\n"))
|
|
77
|
+
$stdout.print buf[0, done_pos] unless done_pos.zero?
|
|
78
|
+
$stdout.flush
|
|
79
|
+
return
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
if buf.length > 512
|
|
83
|
+
safe = buf.length - 5
|
|
84
|
+
$stdout.print buf[0, safe]
|
|
85
|
+
$stdout.flush
|
|
86
|
+
buf = buf[safe..]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
sleep 0.01
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def normalize(str)
|
|
94
|
+
str.gsub("\r\n", "\n").gsub("\r", '')
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def debug(msg)
|
|
98
|
+
warn "[debug] #{msg}" if ENV['PRREMOTE_DEBUG']
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Prremote
|
|
2
|
+
module Commands
|
|
3
|
+
class Undeploy
|
|
4
|
+
ERASE_MAGIC = 'ERSE'.freeze
|
|
5
|
+
|
|
6
|
+
def initialize(port:, baud:)
|
|
7
|
+
@port = port
|
|
8
|
+
@baud = baud
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
serial = Serial.new(@port, @baud)
|
|
13
|
+
sleep 0.5
|
|
14
|
+
serial.read(4096)
|
|
15
|
+
|
|
16
|
+
serial.write(ERASE_MAGIC)
|
|
17
|
+
wait_for_erased(serial)
|
|
18
|
+
warn 'Flash erased. Device will no longer auto-run a script on boot.'
|
|
19
|
+
ensure
|
|
20
|
+
serial&.close
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def wait_for_erased(serial)
|
|
26
|
+
buf = +''
|
|
27
|
+
deadline = Time.now + 30
|
|
28
|
+
loop do
|
|
29
|
+
chunk = (serial.read(256) || '').gsub("\r\n", "\n").gsub("\r", '')
|
|
30
|
+
buf << chunk unless chunk.empty?
|
|
31
|
+
|
|
32
|
+
return if buf.include?("ERASED\n")
|
|
33
|
+
raise "Device error: #{buf.strip}" if buf.match?(/^ERROR /)
|
|
34
|
+
raise 'Timeout waiting for erase confirmation' if Time.now > deadline
|
|
35
|
+
|
|
36
|
+
sleep 0.05
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require_relative 'run'
|
|
2
|
+
|
|
3
|
+
module Prremote
|
|
4
|
+
module Commands
|
|
5
|
+
class Watch
|
|
6
|
+
POLL_INTERVAL = 0.5
|
|
7
|
+
|
|
8
|
+
def initialize(port:, baud:)
|
|
9
|
+
@port = port
|
|
10
|
+
@baud = baud
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(rb_path)
|
|
14
|
+
raise "File not found: #{rb_path}" unless File.exist?(rb_path)
|
|
15
|
+
|
|
16
|
+
warn "Watching #{rb_path} (Ctrl+C to stop)..."
|
|
17
|
+
last_mtime = File.mtime(rb_path)
|
|
18
|
+
run(rb_path)
|
|
19
|
+
|
|
20
|
+
loop do
|
|
21
|
+
sleep POLL_INTERVAL
|
|
22
|
+
mtime = File.mtime(rb_path)
|
|
23
|
+
next if mtime == last_mtime
|
|
24
|
+
|
|
25
|
+
last_mtime = mtime
|
|
26
|
+
warn "\n--- #{rb_path} changed, re-running ---"
|
|
27
|
+
run(rb_path)
|
|
28
|
+
end
|
|
29
|
+
rescue Interrupt
|
|
30
|
+
warn "\nStopped watching."
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def run(rb_path)
|
|
36
|
+
Run.new(port: @port, baud: @baud).call(rb_path)
|
|
37
|
+
rescue RuntimeError => e
|
|
38
|
+
warn "Error: #{e.message}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require 'rbconfig'
|
|
2
|
+
|
|
3
|
+
module Prremote
|
|
4
|
+
class Detector
|
|
5
|
+
R2P2_VENDOR_IDS = %w[2e8a].freeze # Raspberry Pi USB VID
|
|
6
|
+
|
|
7
|
+
def self.find_device
|
|
8
|
+
new.find_device
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def find_device
|
|
12
|
+
candidates = serial_ports
|
|
13
|
+
return candidates.first if candidates.size == 1
|
|
14
|
+
|
|
15
|
+
r2p2 = candidates.select { |p| r2p2_port?(p) }
|
|
16
|
+
r2p2.first || candidates.first
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def list_devices
|
|
20
|
+
serial_ports.map do |port|
|
|
21
|
+
label = r2p2_port?(port) ? 'R2P2/PicoRuby' : 'unknown'
|
|
22
|
+
{ port: port, label: label }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def serial_ports
|
|
29
|
+
case RbConfig::CONFIG['host_os']
|
|
30
|
+
when /darwin/
|
|
31
|
+
# Use cu.* (call-out) devices — tty.* blocks on carrier and causes EBUSY
|
|
32
|
+
Dir.glob('/dev/cu.usbmodem*') + Dir.glob('/dev/cu.usbserial*')
|
|
33
|
+
when /linux/
|
|
34
|
+
Dir.glob('/dev/ttyACM*') + Dir.glob('/dev/ttyUSB*')
|
|
35
|
+
when /mswin|mingw|cygwin/
|
|
36
|
+
# Enumerate COM ports via registry on Windows
|
|
37
|
+
require 'win32/registry'
|
|
38
|
+
ports = []
|
|
39
|
+
Win32::Registry::HKEY_LOCAL_MACHINE.open('HARDWARE\DEVICEMAP\SERIALCOMM') do |reg|
|
|
40
|
+
reg.each_value { |_name, _type, data| ports << data }
|
|
41
|
+
end
|
|
42
|
+
ports
|
|
43
|
+
else
|
|
44
|
+
[]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def r2p2_port?(port)
|
|
49
|
+
# On macOS/Linux, check sysfs or ioreg for the Raspberry Pi VID
|
|
50
|
+
case RbConfig::CONFIG['host_os']
|
|
51
|
+
when /darwin/
|
|
52
|
+
ioreg_output = `ioreg -p IOUSB -l 2>/dev/null`
|
|
53
|
+
R2P2_VENDOR_IDS.any? { |vid| ioreg_output.include?(vid) } &&
|
|
54
|
+
port.match?(/usbmodem/)
|
|
55
|
+
when /linux/
|
|
56
|
+
port_name = File.basename(port)
|
|
57
|
+
vid_path = "/sys/class/tty/#{port_name}/device/../../../idVendor"
|
|
58
|
+
return false unless File.exist?(vid_path)
|
|
59
|
+
|
|
60
|
+
vid = File.read(vid_path).strip.downcase
|
|
61
|
+
R2P2_VENDOR_IDS.include?(vid)
|
|
62
|
+
else
|
|
63
|
+
false
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require 'open3'
|
|
2
|
+
|
|
3
|
+
module Prremote
|
|
4
|
+
module Mrbc
|
|
5
|
+
SHIMS_RE = %r{\.rbenv/shims}
|
|
6
|
+
|
|
7
|
+
def self.bin
|
|
8
|
+
return ENV['MRBC'] if ENV['MRBC'] && File.executable?(ENV['MRBC'])
|
|
9
|
+
|
|
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')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.version
|
|
19
|
+
out, = Open3.capture2e(bin, '--version')
|
|
20
|
+
out.strip
|
|
21
|
+
rescue RuntimeError
|
|
22
|
+
'(mrbc not found)'
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'uri'
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module Prremote
|
|
6
|
+
module RuntimeManager
|
|
7
|
+
REPO = 'lumbermill/prremote'.freeze
|
|
8
|
+
BOARD = 'picow'.freeze
|
|
9
|
+
|
|
10
|
+
def self.uf2_filename(version)
|
|
11
|
+
"prremote-#{BOARD}-runtime-#{version}.uf2"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.release_url(version)
|
|
15
|
+
"https://github.com/#{REPO}/releases/download/runtime-#{version}/#{uf2_filename(version)}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.cache_dir
|
|
19
|
+
File.join(Dir.home, '.prremote', 'runtime')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.cached_path(version)
|
|
23
|
+
File.join(cache_dir, uf2_filename(version))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.fetch(version)
|
|
27
|
+
path = cached_path(version)
|
|
28
|
+
return path if File.exist?(path)
|
|
29
|
+
|
|
30
|
+
FileUtils.mkdir_p(cache_dir)
|
|
31
|
+
$stderr.print "Downloading #{uf2_filename(version)}..."
|
|
32
|
+
$stderr.flush
|
|
33
|
+
download(release_url(version), path)
|
|
34
|
+
warn ' done.'
|
|
35
|
+
path
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.download(url, dest, redirects = 5)
|
|
39
|
+
raise 'Too many redirects' if redirects.zero?
|
|
40
|
+
|
|
41
|
+
uri = URI.parse(url)
|
|
42
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
43
|
+
http.request(Net::HTTP::Get.new(uri))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
case response
|
|
47
|
+
when Net::HTTPRedirection
|
|
48
|
+
download(response['Location'], dest, redirects - 1)
|
|
49
|
+
when Net::HTTPSuccess
|
|
50
|
+
File.binwrite(dest, response.body)
|
|
51
|
+
else
|
|
52
|
+
FileUtils.rm_f(dest)
|
|
53
|
+
raise "Download failed: #{response.code} #{response.message}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/prremote.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: prremote
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ITO Yosei
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: base64
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rubyserial
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.6'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.6'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: thor
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.3'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.3'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: minitest
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '5.25'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '5.25'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rake
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '13.0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '13.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: rubocop
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '1.70'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '1.70'
|
|
96
|
+
description: Compile and run mruby/c scripts on a Raspberry Pi Pico W over USB serial.
|
|
97
|
+
email:
|
|
98
|
+
- y-itou@lumber-mill.co.jp
|
|
99
|
+
executables:
|
|
100
|
+
- prremote
|
|
101
|
+
extensions: []
|
|
102
|
+
extra_rdoc_files: []
|
|
103
|
+
files:
|
|
104
|
+
- LICENSE
|
|
105
|
+
- README.md
|
|
106
|
+
- bin/prremote
|
|
107
|
+
- lib/prremote.rb
|
|
108
|
+
- lib/prremote/cli.rb
|
|
109
|
+
- lib/prremote/commands/deploy.rb
|
|
110
|
+
- lib/prremote/commands/eval_cmd.rb
|
|
111
|
+
- lib/prremote/commands/install.rb
|
|
112
|
+
- lib/prremote/commands/run.rb
|
|
113
|
+
- lib/prremote/commands/undeploy.rb
|
|
114
|
+
- lib/prremote/commands/watch.rb
|
|
115
|
+
- lib/prremote/detector.rb
|
|
116
|
+
- lib/prremote/mrbc.rb
|
|
117
|
+
- lib/prremote/runtime_manager.rb
|
|
118
|
+
- lib/prremote/version.rb
|
|
119
|
+
homepage: https://github.com/lumbermill/prremote
|
|
120
|
+
licenses:
|
|
121
|
+
- MIT
|
|
122
|
+
metadata:
|
|
123
|
+
rubygems_mfa_required: 'true'
|
|
124
|
+
rdoc_options: []
|
|
125
|
+
require_paths:
|
|
126
|
+
- lib
|
|
127
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - ">="
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '3.4'
|
|
132
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
133
|
+
requirements:
|
|
134
|
+
- - ">="
|
|
135
|
+
- !ruby/object:Gem::Version
|
|
136
|
+
version: '0'
|
|
137
|
+
requirements: []
|
|
138
|
+
rubygems_version: 4.0.3
|
|
139
|
+
specification_version: 4
|
|
140
|
+
summary: CLI tool for deploying and running mruby/c scripts on a Raspberry Pi Pico
|
|
141
|
+
W over USB serial
|
|
142
|
+
test_files: []
|