rpremote 0.2.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +16 -0
- data/README.ja.md +17 -3
- data/README.md +17 -3
- data/RELEASING.md +2 -2
- data/THIRD_PARTY_NOTICES.md +4 -0
- data/lib/rpremote/bootsel_command.rb +124 -0
- data/lib/rpremote/cli.rb +52 -9
- data/lib/rpremote/config.rb +2 -0
- data/lib/rpremote/deploy_command.rb +191 -0
- data/lib/rpremote/dfu_command.rb +65 -3
- data/lib/rpremote/flash_command.rb +4 -1
- data/lib/rpremote/flasher.rb +49 -11
- data/lib/rpremote/help.rb +64 -11
- data/lib/rpremote/mrbgems.rb +41 -12
- data/lib/rpremote/nuke_firmware.rb +71 -0
- data/lib/rpremote/picoruby_source_patch.rb +23 -6
- data/lib/rpremote/recursive_copy.rb +84 -0
- data/lib/rpremote/runner.rb +33 -6
- data/lib/rpremote/setup_command.rb +4 -1
- data/lib/rpremote/shell.rb +9 -5
- data/lib/rpremote/terminal.rb +13 -1
- data/lib/rpremote/version.rb +1 -1
- data/lib/rpremote.rb +4 -0
- data/patches/picoruby-3.4.5-bootsel.patch +71 -0
- data/patches/picoruby-4.0.3-bootsel.patch +71 -0
- data/sig/rpremote.rbs +50 -2
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 900c268660317e50ca480983f9993698adb35ab699a82d3e0539dfb583e03723
|
|
4
|
+
data.tar.gz: 5e506856f83d7a48f91481cb0ef377df70d4e6f348e922d3412d068c5012fa15
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7fe7454fe617b0a31004b17ca7e38c681711e90d4bb7889a801092fad8089246e7a1a93d89ff48058a320d60488bd825400b0bfded5380bd8f77d5bde8bded45
|
|
7
|
+
data.tar.gz: cd79792554f9174f2c416794f5973a3e5090f9494ea54ce22ca96bd21cbb72ef1dda7188b78eb9d5939f3dc44b8fcbd003bac772d41e7b13c24436bc9315f034
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.3.0 - 2026-08-28
|
|
6
|
+
|
|
7
|
+
- Add `bootsel` to ask supported R2P2 firmware to enter USB BOOTSEL mode without pressing the button.
|
|
8
|
+
- Add `bootsel --reset-flash-memory` to erase Raspberry Pi Pico external flash memory with the official universal erase UF2, whether BOOTSEL is already mounted or entered through R2P2.
|
|
9
|
+
- Have `setup` download Raspberry Pi's official `nuke_universal.uf2` into `firmware/`; `--force` refreshes the downloaded file.
|
|
10
|
+
- Change `deploy FILE` to `deploy PATH`: build and flash mrbgem-enabled firmware, recursively copy `PATH/lib/NAME` to `:/lib/NAME`, then temporarily run `PATH/main.rb`.
|
|
11
|
+
- Add `fs cp --recursive LOCAL_DIR :/REMOTE_DIR` and its `fs push` alias to create missing remote directories and upload a local directory tree in one command.
|
|
12
|
+
- Add `dfu remove` to permanently clear both DFU boot-application slots; a separate `reset` stops an application already running in RAM.
|
|
13
|
+
- Improve `run`: accept a project directory and run its `main.rb`, treat `--timeout` as an idle timeout while output continues, add `--reset-on-timeout`, and print upload, execution, cleanup, and error diagnostics to stderr.
|
|
14
|
+
- Reuse one temporary remote path for `run` and `exec`, so a failed cleanup cannot accumulate temporary files on the device.
|
|
15
|
+
- Read mrbgem `require_name` values from `Mrbgems.lock` and automatically prepend the corresponding `require` calls to `run`, `exec`, and `deploy`; allow `Mrbgems` entries to specify `require:` explicitly.
|
|
16
|
+
- Change `flash` to accept an explicit UF2 only through `--firmware FILE`; positional UF2 arguments are no longer supported.
|
|
17
|
+
- Improve RP2350 BOOTSEL flashing on macOS by accepting the expected `ENXIO` volume-detach race after a successful copy.
|
|
18
|
+
- Add PicoRuby mrbgem examples for BMI270, HC-SR04 temperature, MAX30102, MPU6050, and WS2812 SPI, plus oximeter and Processing project examples.
|
|
19
|
+
|
|
5
20
|
## 0.2.0 - 2026-08-26
|
|
6
21
|
|
|
7
22
|
- Make `run` and `exec` exit nonzero when compatible R2P2 firmware reports a Ruby exception, while preserving real-time program output.
|
|
@@ -24,5 +39,6 @@ All notable changes to this project will be documented in this file.
|
|
|
24
39
|
- rpremote currently supports PicoRuby only.
|
|
25
40
|
- Supported boards are Raspberry Pi Pico 2 and Pico 2 W; serial commands currently require macOS.
|
|
26
41
|
- `rpremote run` and `exec` are temporary R2P2 executions, not persistent deployments. Use `build` and `flash` to replace firmware.
|
|
42
|
+
- `rpremote deploy` requires PicoRuby 4.x firmware; use `build`, `flash`, `fs push`, and `run` separately with PicoRuby 3.4.x.
|
|
27
43
|
- PicoModem DFU updates only the boot application. Changes to PicoRuby, R2P2, or embedded mrbgems require rebuilding and flashing a UF2.
|
|
28
44
|
- DFU retains the complete application in RAM before writing flash. Verify available memory, bytecode compatibility, boot confirmation, and rollback on target hardware.
|
data/README.ja.md
CHANGED
|
@@ -30,6 +30,14 @@ rpremote flash --mount /Volumes/RP2350
|
|
|
30
30
|
rpremote run main.rb
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
`setup`はRaspberry Pi公式のリセット用`nuke_universal.uf2`も`firmware/`へダウンロードします。
|
|
34
|
+
|
|
35
|
+
再利用するRubyコードを`lib/NAME`へ置くプロジェクトでは、`deploy`がファームウェアをビルドして書き込み、そのディレクトリをR2P2へコピーしてからエントリーファイルを実行します。
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
rpremote deploy path/to/project
|
|
39
|
+
```
|
|
40
|
+
|
|
33
41
|
既定では`firmware/picoruby-4.0.3/`にソースを準備し、`firmware/picoruby-4.0.3-pico2.uf2`を生成します。対象ボードを明示する必要がある場合は、`rpremote ports`でR2P2のCDC 0ポートを確認してください。
|
|
34
42
|
|
|
35
43
|
## mrbgemを追加する
|
|
@@ -69,18 +77,22 @@ rpremote flash --language picoruby --language-version 3.4.2 --board pico2 --moun
|
|
|
69
77
|
| `rpremote setup` | 設定を作成し、言語ソースを準備します。 |
|
|
70
78
|
| `rpremote build` | mrbgemを含むカスタムUF2をビルドします。 |
|
|
71
79
|
| `rpremote build clean` | 中間ビルドファイルを削除します。 |
|
|
80
|
+
| `rpremote bootsel` | 実行中のR2P2へBOOTSEL移行を要求し、USBボリュームの出現を待ちます。 |
|
|
81
|
+
| `rpremote deploy PATH` | ファームウェアをビルドして書き込み、存在する場合は`PATH/lib/NAME`を`:/lib/NAME`へコピーしてから`PATH/main.rb`を実行します。ハードウェア出力は次のコマンドまで維持されます。 |
|
|
72
82
|
| `rpremote dfu app FILE` | PicoModem DFUでRubyまたは版を照合したバイトコードのアプリを更新します。 |
|
|
73
83
|
| `rpremote dfu compile FILE` | DFU用にPicoRuby版と一致する`.mrb`を生成します。 |
|
|
74
84
|
| `rpremote dfu status` | DFUのアクティブおよび起動候補スロットを表示します。 |
|
|
85
|
+
| `rpremote dfu remove` | DFU起動アプリを両スロットから削除します。実行中のアプリを停止するには別途リセットします。 |
|
|
75
86
|
| `rpremote mrbgems …` | mrbgemを検査、表示、固定、更新します。 |
|
|
76
87
|
| `rpremote flash` | 選択したUF2をBOOTSEL経由で書き込みます。 |
|
|
88
|
+
| `rpremote bootsel --reset-flash-memory` | BOOTSELへ移行し、Pico 2の外部フラッシュメモリ全体を消去します。 |
|
|
77
89
|
| `rpremote config show` | 設定ファイルとコマンドラインオプションを反映した実効設定を表示します。 |
|
|
78
90
|
| `rpremote ports` | R2P2シリアルポートを表示します。 |
|
|
79
|
-
| `rpremote run FILE` | Ruby
|
|
91
|
+
| `rpremote run FILE` | Rubyファイルを転送して実行します。ディレクトリ指定時は`main.rb`を実行し、出力をリアルタイム表示します。タイムアウトは無出力の継続時間として扱い、Ruby例外時は非0で終了します。`--reset-on-timeout`を指定すると実行タイムアウト後にR2P2をリセットします。 |
|
|
80
92
|
| `rpremote exec CODE` | 短いRubyコードを実行します。Ruby例外時は非0で終了します。 |
|
|
81
93
|
| `rpremote monitor` / `repl` | 対話型シリアルセッションを開きます。 |
|
|
82
94
|
| `rpremote reset` | R2P2を再起動して再接続まで待ちます。 |
|
|
83
|
-
| `rpremote fs cp/cat/ls/rm/mkdir` | R2P2ファイルシステムを操作します。 |
|
|
95
|
+
| `rpremote fs cp/push/cat/ls/rm/mkdir` | R2P2ファイルシステムを操作します。 |
|
|
84
96
|
|
|
85
97
|
コマンド一覧は`rpremote --help`で確認できます。コマンド固有の構文、既定値、影響は`rpremote <command> --help`で確認できます。
|
|
86
98
|
|
|
@@ -96,9 +108,11 @@ rpremote dfu app --help
|
|
|
96
108
|
## 操作モデル
|
|
97
109
|
|
|
98
110
|
- `run`と`exec`はRubyコードを一時的に転送・実行して出力を表示し、一時リモートファイルを削除します。対応するR2P2ファームウェアがRuby例外を報告した場合は、非0で終了します。
|
|
111
|
+
- `deploy PATH`はファームウェアをビルドし、BOOTSELへ移行して書き込んだ後、R2P2 Shellの起動完了を待ちます。存在する場合は`PATH/lib/NAME`を`:/lib/NAME`へコピーし、実行直前に読み込んだ`PATH/main.rb`を実行します。Shellジョブを保持するため、ハードウェア出力は次のコマンドまで維持されます。正常終了と出力バイト数を表示し、R2P2のRuby例外はコマンド失敗として扱います。ライブラリディレクトリがない場合、コピー処理はスキップします。PicoRuby 4系のファームウェアが必要です。
|
|
99
112
|
- `flash`はUF2をRP2350 BOOTSELボリュームへコピーし、永続的なR2P2ファームウェアを置き換えます。
|
|
100
113
|
- `dfu app`はRubyソースまたは対応するバイトコードのアプリを非アクティブDFUスロットへ登録します。R2P2を再起動して試行し、正常に起動したアプリは`DFU.confirm`を呼び出します。
|
|
101
|
-
-
|
|
114
|
+
- `dfu remove`はDFUのA/B両アプリスロットを完全に空にします。RAM上ですでに動作しているアプリは`rpremote reset`を実行するまで継続するため、起動アプリのログを混ぜずに`rpremote run`を使う場合は両方のコマンドを実行します。その他の`/home`ファイルとR2P2ファームウェアは削除しません。
|
|
115
|
+
- リモートパスには`:/REMOTE/PATH`を使います。`fs cp`はローカルパスとリモートパスの間で転送します。`fs push LOCAL_DIR :/REMOTE_DIR`は`fs cp --recursive`の別名で、不足しているリモートディレクトリを作成し、ローカルディレクトリの内容を一括転送します。リモートにだけ存在するファイルは削除しません。`fs rm`は指定したリモートパスを完全に削除します。
|
|
102
116
|
|
|
103
117
|
## ドキュメントとサンプル
|
|
104
118
|
|
data/README.md
CHANGED
|
@@ -30,6 +30,14 @@ rpremote flash --mount /Volumes/RP2350
|
|
|
30
30
|
rpremote run main.rb
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
`setup` also downloads the official Raspberry Pi `nuke_universal.uf2` reset firmware into `firmware/`.
|
|
34
|
+
|
|
35
|
+
For a project that stores reusable Ruby code in `lib/NAME`, `deploy` builds and flashes the firmware, copies that directory to R2P2, and then runs its entry file:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
rpremote deploy path/to/project
|
|
39
|
+
```
|
|
40
|
+
|
|
33
41
|
The defaults prepare `firmware/picoruby-4.0.3/` and create `firmware/picoruby-4.0.3-pico2.uf2`. Use `rpremote ports` to locate the R2P2 CDC 0 port when a board must be selected explicitly.
|
|
34
42
|
|
|
35
43
|
## Add mrbgems
|
|
@@ -69,18 +77,22 @@ Command-line options override `config/setting.json`. PicoRuby is implemented tod
|
|
|
69
77
|
| `rpremote setup` | Create configuration and prepare language sources. |
|
|
70
78
|
| `rpremote build` | Build a custom UF2 with project mrbgems. |
|
|
71
79
|
| `rpremote build clean` | Remove intermediate build files. |
|
|
80
|
+
| `rpremote bootsel` | Ask the running R2P2 firmware to enter BOOTSEL and wait for its USB volume. |
|
|
81
|
+
| `rpremote deploy PATH` | Build and flash firmware, copy `PATH/lib/NAME` to `:/lib/NAME` when present, then run `PATH/main.rb` while preserving its hardware output until the next command. |
|
|
72
82
|
| `rpremote dfu app FILE` | Stage a Ruby or version-checked bytecode app through PicoModem DFU. |
|
|
73
83
|
| `rpremote dfu compile FILE` | Compile `.rb` to matching PicoRuby bytecode for DFU. |
|
|
74
84
|
| `rpremote dfu status` | Show the active and candidate DFU slots. |
|
|
85
|
+
| `rpremote dfu remove` | Remove both DFU boot applications; reset separately to stop one already running. |
|
|
75
86
|
| `rpremote mrbgems …` | Check, list, lock, or update mrbgems. |
|
|
76
87
|
| `rpremote flash` | Flash the selected UF2 through BOOTSEL. |
|
|
88
|
+
| `rpremote bootsel --reset-flash-memory` | Enter BOOTSEL and erase all Pico 2 external flash memory. |
|
|
77
89
|
| `rpremote config show` | Show the effective configuration after file and command-line options are resolved. |
|
|
78
90
|
| `rpremote ports` | List detected R2P2 serial ports. |
|
|
79
|
-
| `rpremote run FILE` | Upload and run a Ruby file with real-time output; exit nonzero on a Ruby exception. |
|
|
91
|
+
| `rpremote run FILE` | Upload and run a Ruby file, or `main.rb` when FILE is a directory, with real-time output; exit nonzero on a Ruby exception. The timeout measures idle time without output. Use `--reset-on-timeout` to reset R2P2 after a run timeout. |
|
|
80
92
|
| `rpremote exec CODE` | Run short Ruby code and exit nonzero on a Ruby exception. |
|
|
81
93
|
| `rpremote monitor` / `repl` | Open an interactive serial session. |
|
|
82
94
|
| `rpremote reset` | Reboot R2P2 and wait for reconnection. |
|
|
83
|
-
| `rpremote fs cp/cat/ls/rm/mkdir` | Operate on the R2P2 filesystem. |
|
|
95
|
+
| `rpremote fs cp/push/cat/ls/rm/mkdir` | Operate on the R2P2 filesystem. |
|
|
84
96
|
|
|
85
97
|
Run `rpremote --help` for the complete command list. Run `rpremote <command> --help` for command-specific syntax, defaults, and effects.
|
|
86
98
|
|
|
@@ -97,9 +109,11 @@ UF2 files built with `rpremote build` from the [GitHub repository](https://githu
|
|
|
97
109
|
## Operation model
|
|
98
110
|
|
|
99
111
|
- `run` and `exec` upload Ruby code temporarily, relay output, then remove the temporary remote file. A Ruby exception reported by compatible R2P2 firmware makes the command exit nonzero.
|
|
112
|
+
- `deploy PATH` builds the selected firmware, enters BOOTSEL, flashes it, waits until the R2P2 Shell is ready, copies `PATH/lib/NAME` to `:/lib/NAME` when present, then runs the current contents of `PATH/main.rb`. Its Shell job is retained so hardware output remains active until the next command. It reports successful completion and the output byte count; an R2P2 Ruby exception remains a command failure. If the library directory is absent, the copy step is skipped. It requires PicoRuby 4.x firmware.
|
|
100
113
|
- `flash` copies a UF2 to the RP2350 BOOTSEL volume and replaces persistent R2P2 firmware.
|
|
101
114
|
- `dfu app` stages a Ruby source or matching bytecode app in the inactive DFU slot. Restart R2P2 to try it; a successful app must call `DFU.confirm`.
|
|
102
|
-
-
|
|
115
|
+
- `dfu remove` permanently clears both DFU A/B application slots. The application already loaded in RAM continues until you run `rpremote reset`; use both commands before `rpremote run` when boot-app output would be unwanted. It does not remove other `/home` files or R2P2 firmware.
|
|
116
|
+
- Remote paths use `:/REMOTE/PATH`. `fs cp` transfers between one local and one remote path. `fs push LOCAL_DIR :/REMOTE_DIR` is an alias of `fs cp --recursive`; it creates missing remote directories and uploads the local directory contents. It does not delete remote files. `fs rm` permanently deletes the selected remote path.
|
|
103
117
|
|
|
104
118
|
## Documentation and examples
|
|
105
119
|
|
data/RELEASING.md
CHANGED
|
@@ -48,7 +48,7 @@ bundle exec rake release
|
|
|
48
48
|
After the tag exists on GitHub, create a GitHub Release and attach the exact gem that passed validation. `--verify-tag` prevents GitHub CLI from silently creating a tag at another commit:
|
|
49
49
|
|
|
50
50
|
```sh
|
|
51
|
-
gh release create v0.
|
|
51
|
+
gh release create v0.3.0 rpremote-0.3.0.gem --verify-tag --generate-notes
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
Replace `0.
|
|
54
|
+
Replace `0.3.0` in both places for later versions. Finally, install from RubyGems.org in a fresh environment and run `rpremote --version`.
|
data/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
`rpremote` does not bundle R2P2 firmware. The `setup` command downloads a selected PicoRuby/R2P2 4.0.3 or 3.4.2 release from the PicoRuby project and verifies its published SHA-256 digest.
|
|
4
4
|
|
|
5
|
+
`setup` also downloads Raspberry Pi's `nuke_universal.uf2` from the official Pico SDK prebuilts release. It is used only by `rpremote bootsel --reset-flash-memory` to erase external flash memory.
|
|
6
|
+
|
|
7
|
+
Source: <https://github.com/raspberrypi/pico-sdk-prebuilts/releases/latest/download/nuke_universal.uf2>
|
|
8
|
+
|
|
5
9
|
## PicoRuby and R2P2
|
|
6
10
|
|
|
7
11
|
Source: <https://github.com/picoruby/picoruby>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module Rpremote
|
|
6
|
+
class BootselCommand
|
|
7
|
+
SHELL_READY_TIMEOUT = 2.0
|
|
8
|
+
RETRY_INTERVAL = 0.25
|
|
9
|
+
RESET_FLASH_FIRMWARE = File.join("firmware", "nuke_universal.uf2").freeze
|
|
10
|
+
|
|
11
|
+
class Error < Rpremote::Error; end
|
|
12
|
+
|
|
13
|
+
def self.run(args, defaults:, output: $stdout, services: {})
|
|
14
|
+
options = parse_options(args, defaults)
|
|
15
|
+
raise ArgumentError, "bootsel does not accept arguments" unless args.empty?
|
|
16
|
+
|
|
17
|
+
reset_flasher = nil
|
|
18
|
+
if options[:reset_flash_memory]
|
|
19
|
+
flasher = services.fetch(:flasher, Flasher)
|
|
20
|
+
reset_flasher = flasher.new(timeout: options[:timeout])
|
|
21
|
+
target = reset_flasher.find_mounted(options[:mount])
|
|
22
|
+
output.puts("BOOTSEL ready: #{target}") if target
|
|
23
|
+
end
|
|
24
|
+
target ||= enter(**options.slice(:mount, :port, :baud, :timeout), output: output, services: services)
|
|
25
|
+
reset_flash_memory(target, output: output, flasher: reset_flasher) if options[:reset_flash_memory]
|
|
26
|
+
target
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.reset_flash_memory(target, output:, flasher:)
|
|
30
|
+
firmware_path = File.expand_path(RESET_FLASH_FIRMWARE, Dir.pwd)
|
|
31
|
+
raise Error, "reset firmware not found: #{firmware_path}; run `rpremote setup` first" unless File.file?(firmware_path)
|
|
32
|
+
|
|
33
|
+
output.puts("resetting Raspberry Pi Pico 2 external flash memory through #{target}; this erases all stored data and firmware")
|
|
34
|
+
flasher.flash(firmware_path, mount: target, wait_for_port: false)
|
|
35
|
+
output.puts("Pico 2 external flash memory reset; wait for the RP2350 BOOTSEL drive, then run `rpremote flash` to install R2P2")
|
|
36
|
+
end
|
|
37
|
+
private_class_method :reset_flash_memory
|
|
38
|
+
|
|
39
|
+
def self.enter(mount:, port:, baud:, timeout:, output:, services: {})
|
|
40
|
+
serial = services.fetch(:serial, Serial)
|
|
41
|
+
device = services.fetch(:device, Device)
|
|
42
|
+
shell = services.fetch(:shell, Shell)
|
|
43
|
+
flasher = services.fetch(:flasher, Flasher)
|
|
44
|
+
sleeper = services.fetch(:sleeper) { ->(seconds) { sleep(seconds) } }
|
|
45
|
+
clock = services.fetch(:clock) { -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) } }
|
|
46
|
+
deadline = clock.call + timeout
|
|
47
|
+
|
|
48
|
+
request_bootsel(
|
|
49
|
+
port: port, baud: baud, deadline: deadline, output: output,
|
|
50
|
+
serial: serial, device: device, shell: shell, sleeper: sleeper, clock: clock
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
remaining = deadline - clock.call
|
|
54
|
+
raise Error, "timed out waiting for R2P2 Shell to accept a BOOTSEL request" unless remaining.positive?
|
|
55
|
+
|
|
56
|
+
target = flasher.new(timeout: remaining).wait_for_mount(mount: mount)
|
|
57
|
+
output.puts("BOOTSEL ready: #{target}")
|
|
58
|
+
target
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.request_bootsel(context)
|
|
62
|
+
port = context.fetch(:port)
|
|
63
|
+
baud = context.fetch(:baud)
|
|
64
|
+
deadline = context.fetch(:deadline)
|
|
65
|
+
output = context.fetch(:output)
|
|
66
|
+
serial = context.fetch(:serial)
|
|
67
|
+
device = context.fetch(:device)
|
|
68
|
+
shell = context.fetch(:shell)
|
|
69
|
+
sleeper = context.fetch(:sleeper)
|
|
70
|
+
clock = context.fetch(:clock)
|
|
71
|
+
announced = false
|
|
72
|
+
|
|
73
|
+
loop do
|
|
74
|
+
port_path = device.main_port(port)
|
|
75
|
+
unless announced
|
|
76
|
+
output.puts("entering BOOTSEL mode: #{port_path}")
|
|
77
|
+
announced = true
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
requested = false
|
|
81
|
+
serial.open(port_path, baud: baud) do |connection|
|
|
82
|
+
remaining = deadline - clock.call
|
|
83
|
+
raise Shell::TimeoutError, "R2P2 Shell is not ready" unless remaining.positive?
|
|
84
|
+
|
|
85
|
+
remote_shell = shell.new(connection, timeout: [remaining, SHELL_READY_TIMEOUT].min)
|
|
86
|
+
remote_shell.synchronize!
|
|
87
|
+
unless remote_shell.execute("type bootsel").include?("bootsel is")
|
|
88
|
+
raise Error, "connected R2P2 firmware does not support BOOTSEL reset; flash a current rpremote UF2 once while holding BOOTSEL"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
remote_shell.send_command("bootsel")
|
|
92
|
+
requested = true
|
|
93
|
+
end
|
|
94
|
+
break if requested
|
|
95
|
+
rescue Device::NotFoundError, Serial::ConfigurationError, Shell::TimeoutError, IOError, SystemCallError
|
|
96
|
+
raise Error, "timed out waiting for R2P2 Shell to accept a BOOTSEL request" if clock.call >= deadline
|
|
97
|
+
|
|
98
|
+
sleeper.call(RETRY_INTERVAL)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
private_class_method :request_bootsel
|
|
102
|
+
|
|
103
|
+
def self.parse_options(args, defaults)
|
|
104
|
+
options = {
|
|
105
|
+
mount: defaults[:mount],
|
|
106
|
+
port: defaults[:port],
|
|
107
|
+
baud: defaults.fetch(:baud, Serial::BAUD_RATE),
|
|
108
|
+
timeout: defaults.fetch(:timeout, Flasher::DEFAULT_TIMEOUT)
|
|
109
|
+
}
|
|
110
|
+
OptionParser.new do |parser|
|
|
111
|
+
parser.on("--mount DIR") { |value| options[:mount] = value }
|
|
112
|
+
parser.on("--port PORT") { |value| options[:port] = value }
|
|
113
|
+
parser.on("--baud RATE", Integer) { |value| options[:baud] = value }
|
|
114
|
+
parser.on("--timeout SECONDS", Float) { |value| options[:timeout] = value }
|
|
115
|
+
parser.on("--reset-flash-memory") { options[:reset_flash_memory] = true }
|
|
116
|
+
end.parse!(args)
|
|
117
|
+
raise ArgumentError, "--baud must be positive" unless options[:baud].positive?
|
|
118
|
+
raise ArgumentError, "--timeout must be positive" unless options[:timeout].positive?
|
|
119
|
+
|
|
120
|
+
options
|
|
121
|
+
end
|
|
122
|
+
private_class_method :parse_options
|
|
123
|
+
end
|
|
124
|
+
end
|
data/lib/rpremote/cli.rb
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
require "optparse"
|
|
4
4
|
require_relative "build_command"
|
|
5
|
+
require_relative "bootsel_command"
|
|
5
6
|
require_relative "config_show"
|
|
6
7
|
require_relative "dfu_command"
|
|
8
|
+
require_relative "deploy_command"
|
|
7
9
|
require_relative "flash_command"
|
|
8
10
|
require_relative "help"
|
|
9
11
|
require_relative "mrbgems_command"
|
|
12
|
+
require_relative "recursive_copy"
|
|
10
13
|
require_relative "setup_command"
|
|
11
14
|
|
|
15
|
+
# rubocop:disable Metrics/ClassLength
|
|
12
16
|
module Rpremote
|
|
13
17
|
class CLI
|
|
14
18
|
def self.start(argv = ARGV, config: Config, **options)
|
|
@@ -64,7 +68,7 @@ module Rpremote
|
|
|
64
68
|
ports(args)
|
|
65
69
|
when "config"
|
|
66
70
|
config_command(args)
|
|
67
|
-
when "setup", "flash", "build", "dfu", "mrbgems"
|
|
71
|
+
when "setup", "flash", "build", "bootsel", "deploy", "dfu", "mrbgems"
|
|
68
72
|
project_command(command, args)
|
|
69
73
|
when "run", "monitor", "repl", "exec", "reset", "fs"
|
|
70
74
|
remote_command(command, args)
|
|
@@ -81,6 +85,21 @@ module Rpremote
|
|
|
81
85
|
FlashCommand.run(args, defaults: config_options, output: stdout, flasher: flasher)
|
|
82
86
|
when "build"
|
|
83
87
|
BuildCommand.run(args, defaults: config_options, output: stdout, error: stderr)
|
|
88
|
+
when "bootsel"
|
|
89
|
+
BootselCommand.run(
|
|
90
|
+
args,
|
|
91
|
+
defaults: config_options,
|
|
92
|
+
output: stdout,
|
|
93
|
+
services: { flasher: flasher, serial: serial, device: device }
|
|
94
|
+
)
|
|
95
|
+
when "deploy"
|
|
96
|
+
DeployCommand.run(
|
|
97
|
+
args,
|
|
98
|
+
defaults: config_options,
|
|
99
|
+
output: stdout,
|
|
100
|
+
error: stderr,
|
|
101
|
+
services: { flasher: flasher, serial: serial, device: device }
|
|
102
|
+
)
|
|
84
103
|
when "dfu"
|
|
85
104
|
DfuCommand.run(args, defaults: config_options, output: stdout, services: { serial: serial, device: device })
|
|
86
105
|
when "mrbgems"
|
|
@@ -119,10 +138,13 @@ module Rpremote
|
|
|
119
138
|
|
|
120
139
|
def fs(args)
|
|
121
140
|
subcommand = args.shift
|
|
141
|
+
recursive = subcommand == "cp" && (args.delete("--recursive") || args.delete("-r"))
|
|
122
142
|
options = parse_connection_options(args)
|
|
123
143
|
case subcommand
|
|
124
144
|
when "cp"
|
|
125
|
-
copy(args, options)
|
|
145
|
+
copy(args, options, recursive: recursive)
|
|
146
|
+
when "push"
|
|
147
|
+
copy(args, options, recursive: true, command: "push")
|
|
126
148
|
when "cat"
|
|
127
149
|
cat(args, options)
|
|
128
150
|
when "ls", "rm", "mkdir"
|
|
@@ -133,12 +155,14 @@ module Rpremote
|
|
|
133
155
|
end
|
|
134
156
|
|
|
135
157
|
def run_file(args)
|
|
158
|
+
reset_on_timeout = !args.delete("--reset-on-timeout").nil?
|
|
136
159
|
options = parse_connection_options(args)
|
|
137
160
|
raise ArgumentError, "usage: rpremote run FILE [options]" unless args.length == 1
|
|
138
161
|
|
|
139
162
|
source = args.first
|
|
140
|
-
|
|
141
|
-
|
|
163
|
+
source = File.join(source, "main.rb") if File.directory?(source)
|
|
164
|
+
data = prepend_mrbgem_requires(File.binread(source))
|
|
165
|
+
execute_temporary(data, options, output: stdout, reset_on_timeout: reset_on_timeout)
|
|
142
166
|
rescue Errno::ENOENT => e
|
|
143
167
|
raise ArgumentError, e.message
|
|
144
168
|
end
|
|
@@ -147,16 +171,32 @@ module Rpremote
|
|
|
147
171
|
options = parse_connection_options(args)
|
|
148
172
|
raise ArgumentError, "usage: rpremote exec CODE [options]" unless args.length == 1
|
|
149
173
|
|
|
150
|
-
execute_temporary(args.first, options, output: stdout)
|
|
174
|
+
execute_temporary(prepend_mrbgem_requires(args.first), options, output: stdout)
|
|
151
175
|
end
|
|
152
176
|
|
|
153
|
-
def execute_temporary(data, options, output: nil)
|
|
177
|
+
def execute_temporary(data, options, output: nil, reset_on_timeout: false)
|
|
154
178
|
Language.validate!(options[:language])
|
|
155
179
|
port_path = device.main_port(options[:port])
|
|
156
180
|
serial.open(port_path, baud: options[:baud]) do |port|
|
|
157
181
|
runner = Runner.new(port, timeout: options[:timeout])
|
|
158
|
-
output ? runner.run(data, output: output) : runner.run(data)
|
|
182
|
+
output ? runner.run(data, output: output, diagnostics: stderr) : runner.run(data, diagnostics: stderr)
|
|
159
183
|
end
|
|
184
|
+
rescue Shell::TimeoutError => e
|
|
185
|
+
reset_after_run_timeout(port_path, options, e) if reset_on_timeout
|
|
186
|
+
raise
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def reset_after_run_timeout(port_path, options, timeout_error)
|
|
190
|
+
stderr.puts("rpremote: run timed out; resetting R2P2: #{port_path}")
|
|
191
|
+
Resetter.new(serial: serial, timeout: options[:timeout]).reset(port_path, baud: options[:baud])
|
|
192
|
+
stderr.puts("rpremote: reset R2P2 after run timeout: #{port_path}")
|
|
193
|
+
rescue Rpremote::Error, IOError, SystemCallError => e
|
|
194
|
+
raise Shell::TimeoutError,
|
|
195
|
+
"#{timeout_error.message}; automatic reset failed: #{e.message}"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def prepend_mrbgem_requires(data)
|
|
199
|
+
Mrbgems.new(cwd: Dir.pwd).prepend_requires(data)
|
|
160
200
|
end
|
|
161
201
|
|
|
162
202
|
def interactive(args, repl: false)
|
|
@@ -205,13 +245,15 @@ module Rpremote
|
|
|
205
245
|
options
|
|
206
246
|
end
|
|
207
247
|
|
|
208
|
-
def copy(args, options)
|
|
209
|
-
|
|
248
|
+
def copy(args, options, recursive: false, command: "cp")
|
|
249
|
+
usage = command == "push" ? "rpremote fs push LOCAL_DIR :/REMOTE_DIR" : "rpremote fs cp SOURCE DESTINATION"
|
|
250
|
+
raise ArgumentError, "usage: #{usage} [options]" unless args.length == 2
|
|
210
251
|
|
|
211
252
|
source, destination = args
|
|
212
253
|
source_remote = RemotePath.remote?(source)
|
|
213
254
|
destination_remote = RemotePath.remote?(destination)
|
|
214
255
|
raise ArgumentError, "exactly one cp path must be remote (prefix remote paths with :)" if source_remote == destination_remote
|
|
256
|
+
return RecursiveCopy.new(output: stdout, serial: serial, device: device).call(source, destination, options) if recursive
|
|
215
257
|
|
|
216
258
|
if source_remote
|
|
217
259
|
data = with_modem(options) { |modem| modem.download(RemotePath.unwrap(source)) }
|
|
@@ -274,3 +316,4 @@ module Rpremote
|
|
|
274
316
|
end
|
|
275
317
|
end
|
|
276
318
|
end
|
|
319
|
+
# rubocop:enable Metrics/ClassLength
|
data/lib/rpremote/config.rb
CHANGED
|
@@ -21,6 +21,8 @@ module Rpremote
|
|
|
21
21
|
COMMAND_OPTIONS = {
|
|
22
22
|
"setup" => %i[cache language language_version],
|
|
23
23
|
"build" => %i[cache language language_version board firmware mrbgems],
|
|
24
|
+
"bootsel" => %i[mount port baud timeout],
|
|
25
|
+
"deploy" => %i[cache language language_version board firmware mrbgems mount port baud timeout],
|
|
24
26
|
"dfu" => %i[cache language language_version port baud timeout],
|
|
25
27
|
"flash" => %i[cache language language_version board firmware mount port timeout],
|
|
26
28
|
"run" => %i[port baud timeout language],
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require_relative "bootsel_command"
|
|
5
|
+
require_relative "recursive_copy"
|
|
6
|
+
|
|
7
|
+
module Rpremote
|
|
8
|
+
class DeployCommand
|
|
9
|
+
SHELL_READY_TIMEOUT = 2.0
|
|
10
|
+
RETRY_INTERVAL = 0.25
|
|
11
|
+
|
|
12
|
+
class Error < Rpremote::Error; end
|
|
13
|
+
|
|
14
|
+
# The command intentionally keeps the six deployment stages visible in execution order.
|
|
15
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
16
|
+
def self.run(args, defaults:, output: $stdout, error: $stderr, services: {})
|
|
17
|
+
options = parse_options(args, defaults)
|
|
18
|
+
raise ArgumentError, "usage: rpremote deploy PATH [options]" unless args.length == 1
|
|
19
|
+
|
|
20
|
+
project = File.expand_path(args.first)
|
|
21
|
+
library, remote_library, source = project_files(project)
|
|
22
|
+
Language.validate!(options[:language])
|
|
23
|
+
|
|
24
|
+
builder = services.fetch(:builder) { Builder.new }
|
|
25
|
+
flasher = services.fetch(:flasher, Flasher)
|
|
26
|
+
serial = services.fetch(:serial, Serial)
|
|
27
|
+
device = services.fetch(:device, Device)
|
|
28
|
+
runner = services.fetch(:runner, Runner)
|
|
29
|
+
target_options = options.slice(:language, :language_version, :board, :cache_dir, :firmware)
|
|
30
|
+
firmware_path = Target.new(**target_options).firmware_path(root: Dir.pwd)
|
|
31
|
+
|
|
32
|
+
output.puts("deploy build: #{firmware_path}")
|
|
33
|
+
builder.build(
|
|
34
|
+
**target_options,
|
|
35
|
+
mrbgems: options[:mrbgems],
|
|
36
|
+
output: output,
|
|
37
|
+
error: error
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
flash_service = flasher.new(timeout: options[:timeout])
|
|
41
|
+
bootsel_mount = resolve_bootsel_mount(options, flash_service, output, services)
|
|
42
|
+
|
|
43
|
+
mount = options[:mount] || "an automatically detected RP2350 BOOTSEL volume"
|
|
44
|
+
output.puts("deploy flash: #{firmware_path} to #{mount}; this replaces persistent board firmware")
|
|
45
|
+
result = flash_service.flash(
|
|
46
|
+
firmware_path,
|
|
47
|
+
mount: bootsel_mount,
|
|
48
|
+
port: options[:port]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
connection_options = options.merge(port: result.port)
|
|
52
|
+
output.puts("deploy connect: waiting for R2P2 Shell on #{result.port}")
|
|
53
|
+
wait_for_shell(
|
|
54
|
+
port: result.port, baud: options[:baud], timeout: options[:timeout], serial: serial,
|
|
55
|
+
shell: services.fetch(:shell, Shell),
|
|
56
|
+
sleeper: services.fetch(:sleeper) { ->(seconds) { sleep(seconds) } },
|
|
57
|
+
clock: services.fetch(:clock) { -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) } }
|
|
58
|
+
)
|
|
59
|
+
output.puts("deploy connect: ready on #{result.port}")
|
|
60
|
+
|
|
61
|
+
if File.directory?(library)
|
|
62
|
+
output.puts("deploy push: #{library} -> :#{remote_library}")
|
|
63
|
+
recursive_copy = services.fetch(:recursive_copy) do
|
|
64
|
+
RecursiveCopy.new(output: output, serial: serial, device: device)
|
|
65
|
+
end
|
|
66
|
+
recursive_copy.call(library, ":#{remote_library}", connection_options)
|
|
67
|
+
else
|
|
68
|
+
output.puts("deploy push: skipped; directory not found: #{library}")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
output.puts("deploy run: #{source} on #{result.port}")
|
|
72
|
+
source_data = prepend_mrbgem_requires(File.binread(source), options[:mrbgems])
|
|
73
|
+
run_output = serial.open(result.port, baud: options[:baud]) do |port|
|
|
74
|
+
runner.new(port, timeout: options[:timeout]).run(
|
|
75
|
+
source_data, output: output, cleanup: false, remote_path: Runner::DEPLOY_REMOTE_PATH
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
output.puts("deploy run: completed; output bytes: #{run_output.to_s.bytesize}")
|
|
79
|
+
rescue Errno::ENOENT => e
|
|
80
|
+
raise ArgumentError, e.message
|
|
81
|
+
end
|
|
82
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
83
|
+
|
|
84
|
+
def self.project_files(project)
|
|
85
|
+
raise ArgumentError, "project directory not found: #{project}" unless File.directory?(project)
|
|
86
|
+
|
|
87
|
+
name = File.basename(project)
|
|
88
|
+
library = File.join(project, "lib", name)
|
|
89
|
+
source = File.join(project, "main.rb")
|
|
90
|
+
raise ArgumentError, "project entry file not found: #{source}" unless File.file?(source)
|
|
91
|
+
|
|
92
|
+
[library, File.join("/lib", name), source]
|
|
93
|
+
end
|
|
94
|
+
private_class_method :project_files
|
|
95
|
+
|
|
96
|
+
def self.prepend_mrbgem_requires(source, definition)
|
|
97
|
+
return source if definition == false
|
|
98
|
+
|
|
99
|
+
Mrbgems.new(path: definition || Mrbgems::DEFAULT_PATH, cwd: Dir.pwd).prepend_requires(source)
|
|
100
|
+
end
|
|
101
|
+
private_class_method :prepend_mrbgem_requires
|
|
102
|
+
|
|
103
|
+
def self.wait_for_shell(context)
|
|
104
|
+
port = context.fetch(:port)
|
|
105
|
+
baud = context.fetch(:baud)
|
|
106
|
+
timeout = context.fetch(:timeout)
|
|
107
|
+
serial = context.fetch(:serial)
|
|
108
|
+
shell = context.fetch(:shell)
|
|
109
|
+
sleeper = context.fetch(:sleeper)
|
|
110
|
+
clock = context.fetch(:clock)
|
|
111
|
+
deadline = clock.call + timeout
|
|
112
|
+
loop do
|
|
113
|
+
serial.open(port, baud:) do |connection|
|
|
114
|
+
remaining = deadline - clock.call
|
|
115
|
+
raise Shell::TimeoutError, "R2P2 Shell is not ready" unless remaining.positive?
|
|
116
|
+
|
|
117
|
+
shell.new(connection, timeout: [remaining, SHELL_READY_TIMEOUT].min).synchronize!
|
|
118
|
+
end
|
|
119
|
+
return
|
|
120
|
+
rescue Serial::ConfigurationError, Shell::TimeoutError, IOError, SystemCallError
|
|
121
|
+
raise Error, "timed out waiting for R2P2 Shell after flash" if clock.call >= deadline
|
|
122
|
+
|
|
123
|
+
sleeper.call(RETRY_INTERVAL)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
private_class_method :wait_for_shell
|
|
127
|
+
|
|
128
|
+
def self.resolve_bootsel_mount(options, flash_service, output, services)
|
|
129
|
+
mounted = flash_service.find_mounted(options[:mount])
|
|
130
|
+
if mounted
|
|
131
|
+
output.puts("deploy bootsel: already mounted at #{mounted}; skipping serial reset")
|
|
132
|
+
return mounted
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
port = options[:port] || "an automatically detected R2P2 port"
|
|
136
|
+
output.puts("deploy bootsel: requesting USB BOOTSEL mode through #{port}")
|
|
137
|
+
BootselCommand.enter(
|
|
138
|
+
mount: options[:mount], port: options[:port], baud: options[:baud], timeout: options[:timeout],
|
|
139
|
+
output: output, services: services.slice(:flasher, :serial, :device, :shell)
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
private_class_method :resolve_bootsel_mount
|
|
143
|
+
|
|
144
|
+
def self.parse_options(args, defaults)
|
|
145
|
+
options = default_options(defaults)
|
|
146
|
+
option_parser(options).parse!(args)
|
|
147
|
+
validate_options!(options)
|
|
148
|
+
options
|
|
149
|
+
end
|
|
150
|
+
private_class_method :parse_options
|
|
151
|
+
|
|
152
|
+
def self.default_options(defaults)
|
|
153
|
+
{
|
|
154
|
+
language: defaults.fetch(:language, Target::DEFAULT_LANGUAGE),
|
|
155
|
+
language_version: defaults.fetch(:language_version, Target::DEFAULT_LANGUAGE_VERSION),
|
|
156
|
+
board: defaults.fetch(:board, Target::DEFAULT_BOARD),
|
|
157
|
+
cache_dir: defaults.fetch(:cache, Target::DEFAULT_CACHE_DIR),
|
|
158
|
+
firmware: defaults[:firmware],
|
|
159
|
+
mrbgems: defaults[:mrbgems],
|
|
160
|
+
mount: defaults[:mount],
|
|
161
|
+
port: defaults[:port],
|
|
162
|
+
baud: defaults.fetch(:baud, Serial::BAUD_RATE),
|
|
163
|
+
timeout: defaults.fetch(:timeout, Flasher::DEFAULT_TIMEOUT)
|
|
164
|
+
}
|
|
165
|
+
end
|
|
166
|
+
private_class_method :default_options
|
|
167
|
+
|
|
168
|
+
def self.option_parser(options)
|
|
169
|
+
OptionParser.new do |parser|
|
|
170
|
+
parser.on("--language LANGUAGE") { |value| options[:language] = value }
|
|
171
|
+
parser.on("--language-version VERSION") { |value| options[:language_version] = value }
|
|
172
|
+
parser.on("--board BOARD") { |value| options[:board] = value }
|
|
173
|
+
parser.on("--cache DIR") { |value| options[:cache_dir] = value }
|
|
174
|
+
parser.on("--firmware FILE") { |value| options[:firmware] = value }
|
|
175
|
+
parser.on("--mrbgems FILE") { |value| options[:mrbgems] = value }
|
|
176
|
+
parser.on("--no-mrbgems") { options[:mrbgems] = false }
|
|
177
|
+
parser.on("--mount DIR") { |value| options[:mount] = value }
|
|
178
|
+
parser.on("--port PORT") { |value| options[:port] = value }
|
|
179
|
+
parser.on("--baud RATE", Integer) { |value| options[:baud] = value }
|
|
180
|
+
parser.on("--timeout SECONDS", Float) { |value| options[:timeout] = value }
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
private_class_method :option_parser
|
|
184
|
+
|
|
185
|
+
def self.validate_options!(options)
|
|
186
|
+
raise ArgumentError, "--baud must be positive" unless options[:baud].positive?
|
|
187
|
+
raise ArgumentError, "--timeout must be positive" unless options[:timeout].positive?
|
|
188
|
+
end
|
|
189
|
+
private_class_method :validate_options!
|
|
190
|
+
end
|
|
191
|
+
end
|