picoride 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.
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "picoride" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
+
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
9
+
10
+ If you have any concerns about behaviour within this project, please contact us at ["youchan@gmail.com"](mailto:"youchan@gmail.com").
data/Dockerfile ADDED
@@ -0,0 +1,76 @@
1
+ # ビルド専用イメージ(移行プラン フェーズ1)。
2
+ #
3
+ # 旧来の Dockerfile はESP-IDFに加えてRuby/rbenv・R2P2-ESP32本体・Sinatraアプリまで
4
+ # 1つのイメージに焼き込んでいたが、「Sinatraはホストでネイティブに動かし、Dockerは
5
+ # idf.py build / rake setup_xxx の実行専用にする」という移行方針(CLAUDE.md参照)に
6
+ # 沿って、ここではESP-IDFツールチェインだけを持つ最小限のイメージにしてある。
7
+ #
8
+ # R2P2-ESP32本体は焼き込まない。プロジェクトごとに独立したビルド状態(sdkconfig・
9
+ # build/・managed_components等)を持たせたいため、実行時にホスト側の状態ディレクトリを
10
+ # `docker run -v <state_dir>:/R2P2-ESP32` でbind mountして使う想定(R2P2-ESP32の
11
+ # git clone・build_config.rbフックのパッチ・idf_component.ymlのバージョン固定は、
12
+ # ホスト側のRubyコードがそのstate_dirを初めて使うときに行う。イメージビルド時の
13
+ # `RUN`では実行できないため)。
14
+ FROM espressif/idf:v5.5.1
15
+
16
+ SHELL ["/bin/bash", "-c"]
17
+
18
+ ENV LANG=C.UTF-8
19
+ ENV LC_ALL=C.UTF-8
20
+
21
+ # R2P2-ESP32のrakelib/setup.rake(`rake setup_xxx`)は内部で`bundle install`を
22
+ # 呼び、そこでracc/ffi/io-console/json等ネイティブ拡張を持つgemをビルドしようと
23
+ # する。ベースイメージ(espressif/idf)にはコンパイラもRubyのヘッダファイルも
24
+ # 無いため、`mkmf.rb can't find header files for ruby`で失敗する(実際に
25
+ # `rake setup_esp32`を実行して発覚した)。ビルド専用イメージなので、
26
+ # Rubyそのものはベースイメージのシステムruby(apt由来)をそのまま使いつつ、
27
+ # 拡張のビルドに必要な最小限だけ追加する。
28
+ RUN apt-get update && apt-get install -y --no-install-recommends \
29
+ build-essential \
30
+ ruby-dev \
31
+ libssl-dev \
32
+ libreadline-dev \
33
+ zlib1g-dev \
34
+ libyaml-dev \
35
+ libffi-dev \
36
+ && rm -rf /var/lib/apt/lists/*
37
+
38
+ # main/CMakeLists.txt の littlefs_create_partition_image(joltwallet/esp_littlefs、
39
+ # storageパーティションのイメージ作成に使う)は、ビルド時にvenvを作ってPyPIから
40
+ # littlefs-python をインストールする。ビルド実行時にネットワークが不安定だと
41
+ # ここでタイムアウトして失敗する(実際に発生した)。R2P2-ESP32側のmain/idf_component.yml
42
+ # で joltwallet/littlefs を 1.20.4 に固定する前提(この固定処理自体はR2P2-ESP32を
43
+ # プロジェクトごとにcloneするRuby側の処理に移した)で、image-building-requirements.txt
44
+ # の中身が littlefs-python==0.15.0 に一意に確定していることを利用し、これを
45
+ # イメージビルド時に取得して /opt/pip-wheels に置いておく。PIP_NO_INDEX/PIP_FIND_LINKS
46
+ # で「常にこのローカルディレクトリだけを見る」ようコンテナ全体のpipに指示することで、
47
+ # ビルド実行時はPyPIへの通信自体(バージョン解決の問い合わせも含めて)を発生させない。
48
+ #
49
+ # pip3(python3含む)はこのイメージだとentrypoint.sh(コンテナ起動時にのみ実行される)
50
+ # 経由でPATHに追加される、export.shが用意するESP-IDF専用のvenvにしか無い。
51
+ # `docker build`のRUNはentrypointを経由しない生のシェルなので、同じRUN内で
52
+ # 明示的にexport.shをsourceしないとpip3が見つからない(idf.py/rakeの実行時に
53
+ # 毎回export.shをsourceしているのと同じ理由)。
54
+ RUN . "${IDF_PATH}/export.sh" > /dev/null && pip3 download littlefs-python==0.15.0 -d /opt/pip-wheels
55
+
56
+ ENV PIP_NO_INDEX=1
57
+ ENV PIP_FIND_LINKS=/opt/pip-wheels
58
+
59
+ # bind mountしたディレクトリ(ホスト側の実UIDのまま見える)に対してgitコマンドを
60
+ # 実行すると、コンテナ内のユーザーとの所有者不一致でgitが「dubious ownership」
61
+ # エラーを出して拒否する(git 2.35.2以降のCVE-2022-24765対策)。実際に
62
+ # `rake setup_xxx`実行時に発生して発覚した。bind mountされたパスは実行のたびに
63
+ # 変わりうる(プロジェクトごとの状態ディレクトリ)ので、個別に許可するのではなく
64
+ # 全パスを許可しておく(このイメージはビルド専用の使い捨てコンテナ内でしか
65
+ # 使わないため、安全性への影響は無視できる)。
66
+ RUN git config --system --add safe.directory '*'
67
+
68
+ # R2P2-ESP32のrakelib/setup.rake(`rake setup_xxx`でmruby/picorubyをビルドする処理)は
69
+ # 内部で`bundle install`を呼ぶ。このイメージにはベースイメージ(espressif/idf)が
70
+ # 標準で持つRuby(bundlerはデフォルトgemとしてライブラリだけは入っているが、
71
+ # `bundle`コマンドの実行ファイルは生成されていない)がある。実際に`rake setup_esp32`
72
+ # を実行して`bundle: command not found`(exit 127)になったことで発覚した。
73
+ RUN gem install bundler --no-document
74
+
75
+ # R2P2-ESP32の状態ディレクトリをここへbind mountして使う想定のデフォルトWORKDIR。
76
+ WORKDIR /R2P2-ESP32
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 youchan
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # PicoRuby/FemtoRuby IDE for ESP32 / Raspberry Pi Pico
2
+
3
+ This is PicoRuby/FemtoRuby development environment for ESP32 and Raspberry Pi Pico
4
+ (R2P2-ESP32 / R2P2).
5
+ This IDE can do as following
6
+
7
+ - Edit source codes(.c .h .rb) for mrbgem as a project
8
+ - Build image
9
+ - Install image to your device
10
+
11
+ ## Setup
12
+
13
+ The app itself runs natively on your machine (Ruby). Docker is only used to run
14
+ the firmware builds. There is one builder image per target family; build the
15
+ one(s) you need once up front:
16
+
17
+ ```bash
18
+ bundle install
19
+ docker build -t picoride-builder-esp32 docker/esp32 # ESP32 (ESP-IDF)
20
+ docker build -t picoride-builder-pico docker/pico # Raspberry Pi Pico (pico-sdk)
21
+ ```
22
+
23
+ The server picks the image from the project's target (Settings dialog):
24
+ `esp32*` targets use `picoride-builder-esp32`, `pico` / `pico_w` / `pico2` /
25
+ `pico2_w` use `picoride-builder-pico`. Image names can be overridden with
26
+ `PICORIDE_ESP32_BUILDER_IMAGE` / `PICORIDE_PICO_BUILDER_IMAGE`.
27
+
28
+ If PicoRuby bumps its required pico-sdk version, the Pico build stops with
29
+ "pico-sdk X is not checked out!". Rebuild the image with
30
+ `docker build --build-arg PICO_SDK_VERSION=X -t picoride-builder-pico docker/pico`.
31
+
32
+ ### Raspberry Pi Pico notes
33
+
34
+ - A Pico build produces a `.uf2`. Use "UF2をダウンロード" in the menu bar, then
35
+ hold BOOTSEL while plugging in the Pico and copy the file to the drive that
36
+ appears (`RPI-RP2` / `RP2350`).
37
+ - Unlike ESP32, `app/` is not baked into the firmware. Transfer it with
38
+ "app/ をアップロード" on the Terminal tab.
39
+ - `pico` / `pico_w` (RP2040) support only FemtoRuby (mruby/c);
40
+ `pico2` / `pico2_w` support both FemtoRuby and PicoRuby (mruby).
41
+
42
+ ## Run
43
+
44
+ ```bash
45
+ ./bin/server
46
+ ```
47
+
48
+ Open http://localhost:4567 . On first load, open a project via File > Open
49
+ Project (or the dropdown in the menu bar) and browse to, or create, a
50
+ directory on disk — it gets an `app/` and `build_config.rb` automatically.
51
+ Opened projects are remembered in `~/.picoride/project.toml`, so the last one
52
+ you had open reopens automatically on the next server start.
53
+
54
+ ## Troubleshooting
55
+
56
+ ### `Failed to execute 'open' on 'SerialPort': Failed to open serial port.`
57
+
58
+ This happens when connecting to the device from the Terminal tab (Web Serial
59
+ API). On Linux, check `chrome://device-log` right after the failed attempt to
60
+ see the actual OS-level error:
61
+
62
+ - **`FILE_ERROR_ACCESS_DENIED`**: your user isn't (yet) in the `dialout` group
63
+ with an active session, so the browser process can't open `/dev/ttyACM*` /
64
+ `/dev/ttyUSB*`.
65
+ ```bash
66
+ sudo usermod -aG dialout $USER
67
+ ```
68
+ Adding yourself to the group is not enough by itself — an already-running
69
+ login session (and any browser already open in it) still has the old group
70
+ list. **Log out and back in (or reboot)** so the new group membership takes
71
+ effect, then try connecting again.
72
+ - If the port isn't held by anything and permissions look right but it still
73
+ fails, check whether another process/service is holding the port open:
74
+ `sudo lsof /dev/ttyACM0` (or `/dev/ttyUSB0`), and whether `ModemManager` or
75
+ `brltty` is running and probing it (`systemctl status ModemManager brltty`).
76
+
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ task default: :test
@@ -0,0 +1,82 @@
1
+ # ESP32(R2P2-ESP32)用のビルド専用イメージ(移行プラン フェーズ1)。
2
+ #
3
+ # docker build -t picoride-builder-esp32 docker/esp32
4
+ #
5
+ # Raspberry Pi Pico用は docker/pico/Dockerfile(別イメージ)。picorideサーバーが
6
+ # プロジェクト設定のターゲットに応じてどちらのイメージで docker run するかを
7
+ # 切り替える(lib/picoride/server.rb の BUILDER_IMAGES 参照)。
8
+ #
9
+ # 旧来の Dockerfile はESP-IDFに加えてRuby/rbenv・R2P2-ESP32本体・Sinatraアプリまで
10
+ # 1つのイメージに焼き込んでいたが、「Sinatraはホストでネイティブに動かし、Dockerは
11
+ # idf.py build / rake setup_xxx の実行専用にする」という移行方針(CLAUDE.md参照)に
12
+ # 沿って、ここではESP-IDFツールチェインだけを持つ最小限のイメージにしてある。
13
+ #
14
+ # R2P2-ESP32本体は焼き込まない。プロジェクトごとに独立したビルド状態(sdkconfig・
15
+ # build/・managed_components等)を持たせたいため、実行時にホスト側の状態ディレクトリを
16
+ # `docker run -v <state_dir>:/R2P2-ESP32` でbind mountして使う想定(R2P2-ESP32の
17
+ # git clone・build_config.rbフックのパッチ・idf_component.ymlのバージョン固定は、
18
+ # ホスト側のRubyコードがそのstate_dirを初めて使うときに行う。イメージビルド時の
19
+ # `RUN`では実行できないため)。
20
+ FROM espressif/idf:v5.5.1
21
+
22
+ SHELL ["/bin/bash", "-c"]
23
+
24
+ ENV LANG=C.UTF-8
25
+ ENV LC_ALL=C.UTF-8
26
+
27
+ # R2P2-ESP32のrakelib/setup.rake(`rake setup_xxx`)は内部で`bundle install`を
28
+ # 呼び、そこでracc/ffi/io-console/json等ネイティブ拡張を持つgemをビルドしようと
29
+ # する。ベースイメージ(espressif/idf)にはコンパイラもRubyのヘッダファイルも
30
+ # 無いため、`mkmf.rb can't find header files for ruby`で失敗する(実際に
31
+ # `rake setup_esp32`を実行して発覚した)。ビルド専用イメージなので、
32
+ # Rubyそのものはベースイメージのシステムruby(apt由来)をそのまま使いつつ、
33
+ # 拡張のビルドに必要な最小限だけ追加する。
34
+ RUN apt-get update && apt-get install -y --no-install-recommends \
35
+ build-essential \
36
+ ruby-dev \
37
+ libssl-dev \
38
+ libreadline-dev \
39
+ zlib1g-dev \
40
+ libyaml-dev \
41
+ libffi-dev \
42
+ && rm -rf /var/lib/apt/lists/*
43
+
44
+ # main/CMakeLists.txt の littlefs_create_partition_image(joltwallet/esp_littlefs、
45
+ # storageパーティションのイメージ作成に使う)は、ビルド時にvenvを作ってPyPIから
46
+ # littlefs-python をインストールする。ビルド実行時にネットワークが不安定だと
47
+ # ここでタイムアウトして失敗する(実際に発生した)。R2P2-ESP32側のmain/idf_component.yml
48
+ # で joltwallet/littlefs を 1.20.4 に固定する前提(この固定処理自体はR2P2-ESP32を
49
+ # プロジェクトごとにcloneするRuby側の処理に移した)で、image-building-requirements.txt
50
+ # の中身が littlefs-python==0.15.0 に一意に確定していることを利用し、これを
51
+ # イメージビルド時に取得して /opt/pip-wheels に置いておく。PIP_NO_INDEX/PIP_FIND_LINKS
52
+ # で「常にこのローカルディレクトリだけを見る」ようコンテナ全体のpipに指示することで、
53
+ # ビルド実行時はPyPIへの通信自体(バージョン解決の問い合わせも含めて)を発生させない。
54
+ #
55
+ # pip3(python3含む)はこのイメージだとentrypoint.sh(コンテナ起動時にのみ実行される)
56
+ # 経由でPATHに追加される、export.shが用意するESP-IDF専用のvenvにしか無い。
57
+ # `docker build`のRUNはentrypointを経由しない生のシェルなので、同じRUN内で
58
+ # 明示的にexport.shをsourceしないとpip3が見つからない(idf.py/rakeの実行時に
59
+ # 毎回export.shをsourceしているのと同じ理由)。
60
+ RUN . "${IDF_PATH}/export.sh" > /dev/null && pip3 download littlefs-python==0.15.0 -d /opt/pip-wheels
61
+
62
+ ENV PIP_NO_INDEX=1
63
+ ENV PIP_FIND_LINKS=/opt/pip-wheels
64
+
65
+ # bind mountしたディレクトリ(ホスト側の実UIDのまま見える)に対してgitコマンドを
66
+ # 実行すると、コンテナ内のユーザーとの所有者不一致でgitが「dubious ownership」
67
+ # エラーを出して拒否する(git 2.35.2以降のCVE-2022-24765対策)。実際に
68
+ # `rake setup_xxx`実行時に発生して発覚した。bind mountされたパスは実行のたびに
69
+ # 変わりうる(プロジェクトごとの状態ディレクトリ)ので、個別に許可するのではなく
70
+ # 全パスを許可しておく(このイメージはビルド専用の使い捨てコンテナ内でしか
71
+ # 使わないため、安全性への影響は無視できる)。
72
+ RUN git config --system --add safe.directory '*'
73
+
74
+ # R2P2-ESP32のrakelib/setup.rake(`rake setup_xxx`でmruby/picorubyをビルドする処理)は
75
+ # 内部で`bundle install`を呼ぶ。このイメージにはベースイメージ(espressif/idf)が
76
+ # 標準で持つRuby(bundlerはデフォルトgemとしてライブラリだけは入っているが、
77
+ # `bundle`コマンドの実行ファイルは生成されていない)がある。実際に`rake setup_esp32`
78
+ # を実行して`bundle: command not found`(exit 127)になったことで発覚した。
79
+ RUN gem install bundler --no-document
80
+
81
+ # R2P2-ESP32の状態ディレクトリをここへbind mountして使う想定のデフォルトWORKDIR。
82
+ WORKDIR /R2P2-ESP32
@@ -0,0 +1,77 @@
1
+ # Raspberry Pi Pico(R2P2)用のビルド専用イメージ。
2
+ #
3
+ # docker build -t picoride-builder-pico docker/pico
4
+ #
5
+ # ESP32用(docker/esp32/Dockerfile)と同じく、ツールチェインだけを持つ使い捨て
6
+ # コンテナ用のイメージ。picorideサーバーがプロジェクト設定のターゲットが
7
+ # pico / pico_w / pico2 / pico2_w のときにこのイメージで docker run する
8
+ # (lib/picoride/server.rb の BUILDER_IMAGES 参照)。
9
+ #
10
+ # R2P2(Pico版)は picoruby 本体に `picoruby-r2p2` gem として統合されているので、
11
+ # ビルド対象のソースは picoruby のチェックアウト。これはイメージには焼き込まず、
12
+ # プロジェクトごとの状態ディレクトリ(<project>/.r2p2-pico)をホスト側のRubyコードが
13
+ # 初回に git clone し、`docker run -v <state_dir>:/picoruby` でbind mountして使う
14
+ # (lib/picoride/r2p2_state.rb の ensure_pico_checkout 参照)。ビルドは
15
+ # `rake r2p2:{vm}:{board}:prod`(picoruby/tasks/picoruby/r2p2.rake)。
16
+ FROM ubuntu:24.04
17
+
18
+ SHELL ["/bin/bash", "-c"]
19
+
20
+ ENV LANG=C.UTF-8
21
+ ENV LC_ALL=C.UTF-8
22
+ ENV DEBIAN_FRONTEND=noninteractive
23
+
24
+ # - gcc-arm-none-eabi 一式: libmruby.a と R2P2 本体のクロスコンパイル
25
+ # - build-essential: mrbc(ホスト向け)や pioasm・picotool のビルド
26
+ # - cmake(>= 3.22)/ python3: pico-sdk のビルドシステム
27
+ # - ruby / rake: picoruby のビルド(rake)。r2p2 のビルドは `bundle exec` を使わない
28
+ # 素の `rake` なので Bundler / gem のインストールは不要
29
+ # - zip: r2p2 の cmake がリンク後に成果物(.uf2 等)を zip にまとめる。無いと
30
+ # .uf2 自体はできているのにビルドが失敗扱いになる(実際のビルドで確認)
31
+ RUN apt-get update && apt-get install -y --no-install-recommends \
32
+ build-essential \
33
+ ca-certificates \
34
+ cmake \
35
+ gcc-arm-none-eabi \
36
+ git \
37
+ libnewlib-arm-none-eabi \
38
+ libstdc++-arm-none-eabi-newlib \
39
+ python3 \
40
+ rake \
41
+ ruby \
42
+ zip \
43
+ && rm -rf /var/lib/apt/lists/*
44
+
45
+ # pico-sdk はイメージに焼き込む。picoruby の rake タスク(r2p2:check_pico_sdk)は
46
+ # PICO_SDK_PATH の pico-sdk が `git describe --tags --exact-match` で
47
+ # picoruby 側の PICO_SDK_TAG(mrbgems/picoruby-r2p2/r2p2_config.rb)と一致することを
48
+ # 要求する。picoruby 側が SDK のバージョンを上げたらビルドがその旨のエラーで
49
+ # 止まるので、そのときは --build-arg PICO_SDK_VERSION=<新しいタグ> でイメージを
50
+ # 作り直す。
51
+ ARG PICO_SDK_VERSION=2.3.0
52
+ RUN git clone --depth 1 --branch "${PICO_SDK_VERSION}" \
53
+ --recurse-submodules --shallow-submodules \
54
+ https://github.com/raspberrypi/pico-sdk.git /opt/pico-sdk
55
+ ENV PICO_SDK_PATH=/opt/pico-sdk
56
+
57
+ # picotool(.elf → .uf2 変換に使う)。pico-sdk 2.x は SDK と同じバージョンの
58
+ # picotool がインストールされていないと、cmake の構成時に GitHub から picotool を
59
+ # 取ってきてビルドしようとする(ビルドのたびにネットワークが必要になる)。
60
+ # それを避けるため、同じバージョンをイメージビルド時に /usr/local へ入れておく
61
+ # (find_package(picotool) が /usr/local/lib/cmake/picotool を見つける)。
62
+ # USB 経由でボードを操作する機能は使わないので libusb 無しでビルドする。
63
+ RUN git clone --depth 1 --branch "${PICO_SDK_VERSION}" \
64
+ https://github.com/raspberrypi/picotool.git /tmp/picotool \
65
+ && cmake -S /tmp/picotool -B /tmp/picotool/build \
66
+ -DPICO_SDK_PATH=/opt/pico-sdk -DPICOTOOL_NO_LIBUSB=1 -DCMAKE_BUILD_TYPE=Release \
67
+ && cmake --build /tmp/picotool/build -j"$(nproc)" \
68
+ && cmake --install /tmp/picotool/build \
69
+ && rm -rf /tmp/picotool
70
+
71
+ # bind mount したディレクトリ(ホスト側の実UIDのまま見える)に対して git を
72
+ # 実行すると「dubious ownership」で拒否される(ESP32 用イメージと同じ問題。
73
+ # r2p2 の cmake は `git rev-parse` でリビジョンをファイル名に埋め込む)。
74
+ RUN git config --system --add safe.directory '*'
75
+
76
+ # picoruby の状態ディレクトリをここへ bind mount して使う想定のデフォルト WORKDIR。
77
+ WORKDIR /picoruby
data/exe/picoride ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "optparse"
5
+ require "picoride"
6
+ require "picoride/server"
7
+
8
+ options = {}
9
+ OptionParser.new do |op|
10
+ op.banner = "Usage: picoride [options]"
11
+ op.on("-p port", "set the port (default is 4567)") { |val| options[:port] = Integer(val) }
12
+ op.on("-o addr", "set the bind address (default is localhost)") { |val| options[:bind] = val }
13
+ op.on("-e env", "set the environment (default is development)") { |val| options[:environment] = val.to_sym }
14
+ end.parse!(ARGV)
15
+
16
+ Picoride::Server.run!(options)
@@ -0,0 +1,71 @@
1
+ module Picoride
2
+ class BackgroundJob
3
+ # ブラウザ(PicoRuby.wasm)側でのJSONパース/描画が重くなりすぎないよう、
4
+ # API経由で返すログは末尾のみに切り詰める。全量は @state[:log] に残る。
5
+ LOG_TAIL_LIMIT = 8_000
6
+
7
+ def initialize
8
+ @mutex = Mutex.new
9
+ @state = { status: "idle", log: "", started_at: nil, finished_at: nil }
10
+ end
11
+
12
+ # 実行中でなければバックグラウンドスレッドでcmdを開始してtrueを返す。
13
+ # 実行中ならなにもせずfalseを返す(呼び出し側で409にする)。
14
+ def start(cmd)
15
+ started = @mutex.synchronize do
16
+ break false if @state[:status] == "running"
17
+
18
+ @state[:status] = "running"
19
+ @state[:log] = ""
20
+ @state[:started_at] = Time.now.to_i
21
+ @state[:finished_at] = nil
22
+ true
23
+ end
24
+
25
+ Thread.new { run(cmd) } if started
26
+ started
27
+ end
28
+
29
+ def running?
30
+ @mutex.synchronize { @state[:status] == "running" }
31
+ end
32
+
33
+ def to_response_json
34
+ @mutex.synchronize do
35
+ full_log = @state[:log]
36
+ truncated = full_log.length > LOG_TAIL_LIMIT
37
+
38
+ {
39
+ status: @state[:status],
40
+ log: truncated ? full_log[-LOG_TAIL_LIMIT..-1] : full_log,
41
+ log_truncated: truncated,
42
+ started_at: @state[:started_at],
43
+ finished_at: @state[:finished_at]
44
+ }.to_json
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def run(cmd)
51
+ Open3.popen2e("bash", "-c", cmd) do |stdin, stdout_and_stderr, wait_thread|
52
+ stdin.close
53
+ stdout_and_stderr.each_line do |line|
54
+ @mutex.synchronize { @state[:log] << line }
55
+ end
56
+
57
+ success = wait_thread.value.success?
58
+ @mutex.synchronize do
59
+ @state[:status] = success ? "success" : "failed"
60
+ @state[:finished_at] = Time.now.to_i
61
+ end
62
+ end
63
+ rescue StandardError => e
64
+ @mutex.synchronize do
65
+ @state[:status] = "failed"
66
+ @state[:log] << "\n[#{e.class}] #{e.message}\n"
67
+ @state[:finished_at] = Time.now.to_i
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,133 @@
1
+ require "fileutils"
2
+ require "perfect_toml"
3
+
4
+ module Picoride
5
+ # ~/.picoride/project.toml への永続化。
6
+ #
7
+ # 「一度開いたら覚えておいてほしい、サーバーを再起動しても開かれている状態で
8
+ # 起動してほしい」という要望を受けて追加した。保存先はプロジェクトディレクトリの
9
+ # 外(ホームディレクトリ配下の`.picoride/`。r2p2_state.rbの状態ディレクトリと
10
+ # 同じ「プロジェクト本体の外に置く」発想)。
11
+ #
12
+ # その後「設定(ターゲット・VM・USB Console)はプロジェクト単位で持たせてほしい」
13
+ # というフィードバックを受けて、各プロジェクトの設定も同じファイルに持たせる
14
+ # ようにした(`platform`/`vm`/`usb_console`。server.rbのread_project_config/
15
+ # project_settings参照)。
16
+ #
17
+ # なお、このファイルはかつて「ワークスペース」(複数プロジェクトを束ねる
18
+ # 親ディレクトリ)を保存する`workspace.toml`という名前だった。「ワークスペース=
19
+ # プロジェクトだった」というフィードバックにより、プロジェクトを束ねる上位概念
20
+ # 自体が不要と判明したため、`project.toml`へ改名し、内容もプロジェクト直接を
21
+ # 指す形に変わっている(旧workspace.tomlの自動移行は行わない)。
22
+ #
23
+ # TOMLの読み書きは自前実装せず、PerfectTOML(mame/perfect_toml、単一ファイル・
24
+ # 無依存でTOML v1.0.0に完全準拠するパーサ/ジェネレータ)を使う。ファイルは
25
+ # active = "最後にアクティブだったプロジェクトの絶対パス"
26
+ #
27
+ # [名前]
28
+ # path = "絶対パス"
29
+ # platform = "esp32" # 未設定なら書かない(TOMLにnullは無いため)
30
+ # vm = "femtoruby" # 同上
31
+ # usb_console = false
32
+ # という形(nameはPerfectTOML.generateがHashのキーから自動でTOMLのテーブル名に
33
+ # 変換してくれる。空白を含む名前なども自動でクォートされる)。
34
+ module ProjectStore
35
+ DIR = File.join(Dir.home, ".picoride")
36
+ PATH = File.join(DIR, "project.toml")
37
+
38
+ module_function
39
+
40
+ # 保存されているプロジェクト一覧(存在しなくなったディレクトリは除外)、
41
+ # 最後にアクティブだったプロジェクトの絶対パス、各プロジェクトの設定を返す。
42
+ # 戻り値: {
43
+ # active: "絶対パス" または nil,
44
+ # paths: [絶対パスの配列(出現順)],
45
+ # settings: { "絶対パス" => {"platform"=>, "vm"=>, "usb_console"=>}, ... }
46
+ # }
47
+ def load
48
+ data =
49
+ begin
50
+ PerfectTOML.load_file(PATH)
51
+ rescue Errno::ENOENT, PerfectTOML::ParseError
52
+ # ファイルが無い(初回起動)、あるいはユーザーが手で編集して壊した
53
+ # 場合でも、ここで落ちずに「何も保存されていない」扱いにする。
54
+ {}
55
+ end
56
+
57
+ active = data["active"]
58
+ paths = []
59
+ settings = {}
60
+
61
+ data.each do |name, value|
62
+ next if name == "active" || !value.is_a?(Hash)
63
+
64
+ path = value["path"]
65
+ next unless path.is_a?(String) && File.directory?(path)
66
+
67
+ paths << path
68
+ settings[path] = {
69
+ "platform" => value["platform"],
70
+ "vm" => value["vm"],
71
+ "usb_console" => value["usb_console"] == true
72
+ }
73
+ end
74
+
75
+ active = nil unless active.is_a?(String) && paths.include?(active)
76
+
77
+ { active: active, paths: paths, settings: settings }
78
+ end
79
+
80
+ # 現在の状態をまるごと書き込む(差分更新ではなく毎回全体を作り直す。
81
+ # プロジェクトの開閉・設定変更はそう頻繁な操作ではないので、素朴な
82
+ # 全体書き直しで十分という判断)。
83
+ #
84
+ # settingsはキーワード必須にしてある(デフォルト値を用意して省略を許すと、
85
+ # 「プロジェクトを開くだけ」の呼び出し箇所で既存の設定を空のHashで
86
+ # 上書きしてしまうミスを構造的に防げないため。実際に一度このミスを
87
+ # 作りかけた)。
88
+ def save(active:, paths:, settings:)
89
+ FileUtils.mkdir_p(DIR)
90
+
91
+ data = {}
92
+ data["active"] = active if active
93
+
94
+ used_names = {}
95
+ paths.each do |path|
96
+ entry = { "path" => path }
97
+ s = settings[path]
98
+ if s
99
+ # TOMLにnullは無いため、未設定(nil)のキーは書かない。
100
+ entry["platform"] = s["platform"] if s["platform"]
101
+ entry["vm"] = s["vm"] if s["vm"]
102
+ entry["usb_console"] = true if s["usb_console"] == true
103
+ end
104
+ data[unique_name(path, used_names)] = entry
105
+ end
106
+
107
+ PerfectTOML.save_file(PATH, data)
108
+ end
109
+
110
+ # プロジェクト名は既定でディレクトリ名(パスの最後のセグメント)にする
111
+ # (menu_bar.rbのproject_labelと同じロジック。フロントとサーバで共有
112
+ # できないので両側に同じものを持たせてある)。
113
+ def display_name(path)
114
+ name = path.to_s.split("/").last
115
+ name.nil? || name.empty? ? path.to_s : name
116
+ end
117
+
118
+ # 別のパスのプロジェクトが同じディレクトリ名(例: 違う場所にある2つの
119
+ # "projects"ディレクトリ)だった場合に備えて、TOMLのテーブル名として
120
+ # 一意になるよう連番を振る。
121
+ def unique_name(path, used_names)
122
+ base = display_name(path)
123
+ name = base
124
+ counter = 2
125
+ while used_names.key?(name)
126
+ name = "#{base}_#{counter}"
127
+ counter += 1
128
+ end
129
+ used_names[name] = true
130
+ name
131
+ end
132
+ end
133
+ end