CopyButton

2026年5月2日土曜日

tmux + Tailscale + Termius でスマホから Claude Code を動かす最短手順【母艦ハブ型・2026年版】

この記事は「外出先のスマホから自宅マシン群の Claude Code で開発したい」エンジニア向け。tmux + Tailscale + Termius を組み合わせ、古いノートPC を 接続ハブ兼 Claude 母艦にして、画像生成 GPU・iOS 開発用 Mac・常時稼働 Raspberry Pi を使い分ける構成を全部書く。プロジェクトごとに tmux セッションを分ける運用も載せる。

スマホで tmux ターミナルを開きノートPCに SSH 接続している作業デスクの写真

この記事でできること

  • 外出先のスマホ(iPhone / Android)から自宅の Claude Code セッションに接続して開発を続けられる
  • 母艦をハブにして、用途別に GPU マシン・Mac・Raspberry Pi を使い分けられる
  • プロジェクトごとに独立した tmux セッションを持って、画像生成・iOS 開発・投資ボット運用を並行で抱えても混ざらない
  • tmux で永続セッション化するので、スマホ画面をオフにしても処理が続く
  • 必要なものは古いノートPC 1 台 + スマホ。VPS 月額もポート開放も不要

3 つのアプローチ比較

「Claude Code をどこに置くか」で構成のクセが分かれる。本記事の 母艦ハブ型 は Claude Code を母艦 1 か所に集約し、重い処理や開発対象 OS は周辺マシンに ssh で逃がす。1 台に全部詰める単一マシン型と、各端末に Claude Code を常駐させて使い分ける分散型の中間に当たる。

構成 母艦ハブ型
(本記事)
単一 Mac で全部 全端末で分散
(各マシンに Claude Code を常駐)
必要マシン 用途別の周辺マシン × N + 母艦 1 台 高スペック Mac 1 台 用途別マシン × N
複数プロジェクト並行時の安定性
機能横断ワークフロー
Tailscale ノード数 2 2 N+1

表を読み解くと 3 案のトレードオフが見える。例として SDXL で画像生成 → iOS アプリのアセットに組み込み → Mac で archive という機能横断ワークフローを Claude Code に任せたい場面で考える。

  • 単一マシン型: 1 台で完結するので機能横断は強い。ただし画像生成まで賄える GPU 内蔵 Mac は M3 Max / M4 Max 級が必要で初期コストが現実的でない。重い画像生成中に同居するトレードボットや IDE がリソース競合で詰まりやすい。
  • 分散型: マシンごとに独立しているので並行時の安定性は高い。一方、各マシンの Claude Code がそれぞれの島に閉じているので、機能横断ワークフローでは 人間が「画像 ready → Mac に渡す → ビルド」のタスク分割と引き渡しを毎回手で切り出す 必要がある。
  • 母艦ハブ型: 並行も横断も両立する。1 つの Claude Code セッションが ssh + scp / rsync で各マシン間のファイルを動かせるので、機能横断ワークフローも 1 回の依頼でまるごと Claude Code に丸投げできる。コストは分散型に対して母艦 1 台分の追加が乗るが、母艦は低スペックの不要 PC の流用で足りるので増分は最小。

構成図

スマホ(iPhone / Android)
Termius アプリ

Tailscale (WireGuard)
母艦:古いノートPC(接続ハブ兼 Claude 母艦)
WSL2 + tmux × N セッション + Claude Code
┬─ ssh ──┬─ ssh ──┬
▼      ▼      ▼
GPU マシン
画像生成・LLM 推論
Mac
iOS / macOS 開発
Raspberry Pi
常時稼働ボット

スマホ → 母艦の経路だけ Tailscale。母艦から周辺マシンへは LAN 内の ssh で飛ぶ。周辺マシンを Tailscale に入れないので、ハイスペック機材に Tailscale デーモンや MagicDNS 名を増やさず、外部から触れる接続点を母艦 1 台に集約できる。

使ったもの

  • 母艦ノートPC: 第7〜10世代 Core i5 / メモリ 8GB あれば十分。バッテリーが死んでいても AC 直挿しで OK。常時電源 ON で運用
  • GPU マシン(任意): 画像生成や AI 推論を任せたい場合のみ。自分は GTX 1660 SUPER 搭載のミニ PC を使っている
  • Mac(任意): iOS / macOS アプリ開発する場合のみ。Xcode が動けば Mac mini / 旧 MacBook でも可
  • Raspberry Pi(任意): 常時稼働させたい軽量ボット用。投資シミュレーション・MQTT ブローカ・cron 系をここに集約
  • スマホ: iOS / Android 問わず。Bluetooth キーボードがあると長時間でも疲れにくい
  • Termius: 無料プランで ssh + 鍵管理ができる。複数端末で設定同期したい場合のみ Pro が必要
  • Tailscale: 母艦とスマホの 2 ノードだけに入れる。WireGuard ベースの mesh VPN で個人利用は無料(最大 100 デバイス)
  • tmux: 永続セッションを担う。Ubuntu なら apt 一発
  • WSL2(母艦が Windows の場合): Linux 環境を Windows 内に共存させる

手順

1. 母艦に WSL2 + sshd + tmux を入れる

母艦が Windows の場合、PowerShell を管理者権限で開いて WSL2 + Ubuntu をインストール。

wsl --install -d Ubuntu

WSL2 の中で systemd を有効にすると sshd が systemctl で扱える。/etc/wsl.conf に以下を追記して wsl --shutdown で再起動する。

[boot]
systemd=true

続けて WSL2 内で sshd と tmux を入れる。

$ sudo apt update
$ sudo apt install -y openssh-server tmux
$ sudo systemctl enable --now ssh

母艦が Linux ならこの 1 コマンドで終わる。Mac なら brew install tmux + システム環境設定でリモートログインを ON にする。

2. Tailscale は母艦とスマホだけに入れる

Tailscale 公式でアカウントを作る(Google / GitHub / Microsoft でログイン)。VPN ノードに加えるのは母艦とスマホの 2 台だけ。GPU マシン・Mac・Raspberry Pi は LAN 内に閉じたままで触らない。これが母艦ハブ型の核で、ハイスペック機材側に Tailscale デーモンを増やさず、外部からの侵入経路も母艦 1 点に集約できる。

母艦(Linux / WSL2)に Tailscale を入れる:

$ curl -fsSL https://tailscale.com/install.sh | sh
$ sudo tailscale up

表示された URL をブラウザで開いて承認すると母艦が Tailscale ネットワークに参加する。スマホには App Store / Google Play で Tailscale アプリを入れて同じアカウントでログインするだけ。

Tailscale 管理画面で母艦に laptop-host のような MagicDNS 名が振られ、スマホからは ssh username@laptop-host でどこからでも母艦に繋がる。GPU マシン・Mac・Raspberry Pi には MagicDNS 名は振らない。母艦から先の名前解決は次の手順 5 のとおり LAN 内で完結させる。

3. 母艦に永続 tmux セッションを用意する

母艦に ssh で入って claude という名前のセッションを作る。

$ tmux new -s claude

このセッション内で claude コマンド(Claude Code CLI)を起動する。デタッチは Ctrl-bd。スマホ側で Termius を閉じても、この tmux セッションはサーバ側で動き続ける。

再接続したいときは:

$ tmux attach -t claude

4. Termius にホスト登録

スマホで Termius を開いて新規ホスト追加。

  • Hostname: laptop-host(Tailscale の MagicDNS 名)または Tailscale IP(100.x.y.z
  • Username: 母艦のユーザー名
  • Auth: Termius が生成する ssh 鍵を使うのが楽。生成した公開鍵を母艦の ~/.ssh/authorized_keys に追記する

接続したら、ログイン直後に tmux attach -t claude を打つだけで Claude Code 画面が再現する。Termius のスニペット機能に登録しておくと 1 タップで再接続できる。

5. ssh 公開鍵を周辺マシンに配る(パスワードレス)

母艦から周辺マシンへの ssh は パスワードレスの公開鍵認証に統一しておく。Claude Code は対話の途中でパスワード入力を待たされると詰まりやすく、鍵認証なら ssh gpu-host コマンド のように一行で完結する。

母艦で ed25519 鍵を 1 ペア作り(無ければ)、同じ公開鍵を GPU マシン・Mac・Raspberry Pi の ~/.ssh/authorized_keys に配る:

$ ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519     # 既にあればスキップ
$ ssh-copy-id user@gpu-host
$ ssh-copy-id user@mac-host
$ ssh-copy-id user@pi-host

母艦の鍵 1 本で GPU・Mac・Pi のどこにでも横断的に飛べるようになる。Claude Code は母艦上で動いているので、結果的に Claude Code が周辺マシンを全部使い分けられる状態になる。

逆に Claude Code に触らせたくない環境ができたら、そのマシンの authorized_keys から該当行を消すだけで良い。母艦側の鍵や ~/.ssh/config はそのままで、特定マシンだけ縁を切るという制御を後からピンポイントで効かせられる。

6. 母艦から周辺マシンへ ssh で飛ぶ

周辺マシンは Tailscale に乗っていないので、母艦からは LAN 内の ssh で繋ぐ。Linux / Mac / Raspberry Pi で avahi-daemon(mDNS)を有効にしておけば gpu-host.local / mac-host.local / pi-host.local で名前解決できる。Avahi が使えない環境では、ルータの DHCP 予約で固定 IP を振って母艦の ~/.ssh/configHost gpu-host として書いておけば、同じく ssh gpu-host の短い名前で叩ける。Claude Code で重い処理を投げたいときは、tmux の別ウィンドウや別ペインから ssh しておくとそのままコマンドを叩ける。

# 母艦の tmux 内で別ウィンドウを開いて
$ ssh gpu-host
# そこで重い処理を起動
$ python sdxl_batch.py
# detach: Ctrl-b d

iOS 開発は母艦の Claude Code から ssh mac-host 経由で Xcode プロジェクトのソースを直接編集できる(ビルドは Mac 側で xcodebuild を叩く形)。Raspberry Pi の常時稼働ボットも同じパターンで、母艦から ssh pi-host してログを覗いたりコードを直したりする。

プロジェクトごとに tmux セッションを分ける

母艦に Claude Code セッションを 1 本だけ持つ運用だと、画像生成のジョブと iOS 開発の会話が同じ tmux に流れ込んで邪魔になる。プロジェクトごとに 独立した tmux セッションを持っておくと、用途別にスマホで切り替えられる。

セッション起動スクリプト

自分は ~/start_claude_<project>.sh という起動スクリプトを母艦のホームに置いている。中身は名前だけ違う以下のテンプレート:

#!/bin/sh

NAME=claude_blog

if [ -n "$SSH_CONNECTION" ] && command -v tmux &>/dev/null; then
    if tmux has-session -t $NAME 2>/dev/null; then
        # セッションが存在すれば自動アタッチ
        tmux attach -t $NAME
    else
        # なければ新規作成
        tmux new-session -s $NAME
    fi
fi

ポイントは 2 つ。SSH_CONNECTION 環境変数で ssh 経由のときだけ tmux に入る(cron や直接ログインでは発動しない)、has-session で再接続を冪等にしている(毎回新規でも、再アタッチでも同じスクリプトでよい)。

運用しているセッション一覧

スクリプト tmux セッション名 用途 主な作業先
start_claude_blog.shclaude_blogブログ自動生成母艦内
start_claude_image.shclaude_image画像生成パイプライン保守GPU マシン
start_claude_ios.shclaude_iosiOS アプリ開発Mac
start_claude_sim.shclaude_sim投資シミュレーションRaspberry Pi

各セッションは用途と作業対象マシンを分担する。セッションに入ったら cd ~/projects/<project> したり ssh <target-host> したりして、その用途で必要な場所に移動する。

Termius 側のスニペット運用

Termius は接続後に実行する「スニペット」を保存できる。プロジェクトごとに以下のようなスニペットを作っておくと、母艦に ssh した直後の画面で 1 タップ起動できる。

# Snippet 名: blog
bash ~/start_claude_blog.sh

# Snippet 名: image
bash ~/start_claude_image.sh

# Snippet 名: ios
bash ~/start_claude_ios.sh

あるいは Termius の同一ホストに対して別エイリアスを作って、それぞれの「Startup snippet」に上記コマンドを設定すると、ホスト一覧から直接プロジェクト別セッションに入れる。

ハック拡張アイデア

  • mosh を入れる: 電車の地下区間など回線が瞬断する環境で再接続が劇的に楽になる。Termius の Pro プランが必要
  • 音声入力: スマホの IME を音声入力に切り替えると、Termius の入力欄に直接喋り込める。長文プロンプトを打つときはこれが楽
  • tmux のキーバインド変更: スマホのソフトキーボードで Ctrl-b は押しにくいので、~/.tmux.conf でプレフィックスを Ctrl-a に変更しておく
  • Tailscale ACL をスマホ → 母艦のみに制限: Tailscale ネットワーク内の許可経路を 1 本に絞る。GPU マシンや Mac は元から Tailscale に居ないので、スマホが盗まれてもアクセス可能なのは母艦だけで済む
  • 長時間ジョブの通知: tmux 内で動いているジョブの完了を Slack や LINE Notify に送ると、外出先でも完走を把握できる

詰まりポイント

  • WSL2 がアイドルで落ちる: tmux で何かが動いていれば落ちないが、空セッションだと数分で停止する。必ず tmux new -s claude 内で何かを走らせておく
  • WSL2 の sshd ポートが 22 で衝突する: Windows 側の OpenSSH と被る場合は /etc/ssh/sshd_configPort 2222 に逃がす。Termius 側のホスト設定でもポート指定する
  • Tailscale の MagicDNS が引けない: 管理画面で MagicDNS が ON になっているか確認。OFF だと数値 IP(100.x.y.z)でしか繋がらない
  • tmux のスクロールがスマホで効かない: Ctrl-b[ でコピーモードに入ると指でスクロールできる
  • start_claude_*.sh が ssh で起動しない: ~/.bash_profile~/.bashrc から呼ばないと発動しない。Termius のスニペットから明示的に呼ぶか、ホストごとの startup command に登録する
  • 同じセッションに 2 端末から同時にアタッチして操作が割れる: 共有を切りたいときは tmux attach -d -t claude_blog(既存接続を切ってからアタッチ)にする

よくある質問

Q. 母艦をハブにするメリットは?

A. 外部接続点を母艦 1 点に絞れるので、ハイスペック機材に Tailscale デーモンや MagicDNS 名を入れずに済む。スマホで覚えるホストも 1 つで済み、ACL も簡素。プロジェクトとマシンのルーティングは母艦内のスクリプトに閉じ込められるので、機材構成を入れ替えてもスマホ側の設定はいじらなくていい。

Q. プロジェクトごとに tmux セッションを分けるのは過剰では?

A. 1 プロジェクトしか持っていなければ過剰。複数プロジェクトを並行で抱える場合、Claude Code はセッション単位でコンテキストが混ざるので分けたほうが事故が減る。start_claude_<name>.sh は 10 行で済むので始めやすい。

Q. Tailscale ではなく自前の VPN や ngrok でもいい?

A. 動く。ただし Tailscale は無料・設定 5 分・MagicDNS 付きで、個人用途なら明確な乗り換え理由がない。ngrok 系は外部公開 URL を発行する性質上、攻撃面が広がるので常用は避けたい。

Q. 公式の Claude Code Remote Control があるなら、わざわざ ssh する必要は?

A. 公式 Remote Control は便利だが、ローカルファイルや別マシンへの ssh、自前のスクリプトなど「Claude Code の外側」で何かしたい場面は ssh のほうが小回りが効く。両方併用してもいい。

Q. iPad / Android タブレットでも同じ構成で動く?

A. 動く。むしろ画面が広いぶん作業効率は上がる。Termius / Tailscale ともに iPadOS / Android タブレット向けに同じアプリが提供されている。

※本記事の手順は執筆時点(2026 年 5 月)で動作確認していますが、Tailscale / Termius / WSL2 のアップデートで挙動が変わる場合があります。動かない場合は コメント欄でお知らせください。

まとめ

tmux + Tailscale + Termius の 3 点セットに「母艦をハブとして周辺マシンを使い分ける」「プロジェクトごとに tmux セッションを切る」を加えると、外出先のスマホからでも複数プロジェクトを並行して回せる。古いノートPC の再活用先としても向いていて、画像生成・iOS 開発・常時稼働ボットといった性格の違うワークロードを同居させても接続が落ちにくい。

この記事が役に立ったら X(Twitter)でシェアしてもらえると喜びます。

関連記事

参考

※この記事は Claude Code を使った自動更新を試しています。

2026年5月1日金曜日

スマホのリモート操作で iOS アプリをビルド&TestFlight 配信する【SSH + build keychain・2026年版】

この記事は「外出先のスマホからでも自宅 Mac の Xcode を叩いて iOS アプリを TestFlight に上げたい」エンジニア向け。スマホ → 母艦ノートPC(WSL2)→ 別マシンの Mac に多段 ssh で繋ぎ、archive → IPA → TestFlight アップロードまで 1 コマンドで完結させる手順。codesign が SSH で詰まる理由と回避策(専用 build keychain)、配信スクリプト 3 本の中身も全部書く。

ノートPCのターミナル・Mac mini・スマホが連携してアプリを配信している作業デスクの写真

この記事でできること

  • 外出先のスマホから(または WSL2 のターミナルから)SSH 1 行で TestFlight に新ビルドが上がる
  • Mac は Xcode と xcodebuild の実行環境としてだけ使い、普段は触らない
  • App Store Connect API Key.p8)認証で 2 段階認証ダイアログを出さない
  • VPS や GitHub Actions を使わずに自宅 LAN 内で完結(Mac mini や旧 MacBook の余生活用にも向く)

4つのアプローチ比較

構成 SSH + 自前スクリプト
(本記事)
Mac で Xcode 操作 GitHub Actions (macos) fastlane 主導
月額コスト 0 円(自宅 Mac の電気代のみ) 0 円 macOS runner は分単価高め 0 円(自宅 Mac)
エディタ WSL2 の好みのもの(VS Code / vim 等) Xcode 任意 任意
配信 1 回の所要時間 3〜6 分(archive + IPA + altool) 5〜10 分(GUI 経由) push 後 10〜15 分(CI 起動含む) 3〜6 分
秘匿情報の置き場 Mac ローカル(~/.appstoreconnect Mac ローカル GitHub Secrets Mac ローカル / Match

構成図

WSL2(コード編集マシン)
エディタ + git push + ssh の起点
▼ git push
▼ ssh + bash scripts/upload.sh
Mac(Xcode 専用 archive サーバ)
build.keychain-db / xcodebuild / altool
▼ xcrun altool --upload-app
App Store Connect / TestFlight

使ったもの

なぜ setup_build_keychain.sh が必要か

普通に Mac の Terminal で xcodebuild archive すれば通るのに、SSH 経由だと codesign が「キーチェーンを開けません」で落ちるのが本記事のスタートライン。

原因: login keychain は SSH ログインで unlock しても codesign から触れない。GUI ログインセッションに紐づいた security agent が居ないため、ACL 上の「アプリ確認なしに使ってよい」リストが働かず、対話プロンプトを出そうとして失敗する。

解決策は 専用の build keychain を作って Apple Distribution 証明書を複製、ACL を緩めて codesign から GUI 確認なしに使える状態にする。これを scripts/setup_build_keychain.sh として 1 度だけ Mac の Terminal(GUI セッション)で実行する。

以下のコード中の placeholder は自分の環境の値に置き換えてください({kcpw} は build keychain をローカルでアンロックするための任意文字列で、外部秘匿情報ではありません)。

{user}                        自分のユーザ名(Mac の login user)
{mac_addr}                    Mac の IP/ホスト名(例: 192.168.x.y)
{app_name}                    アプリ名(Xcode scheme / archive 名と一致)
{bundle_id}                   Bundle ID(例: com.example.MyApp)
{team_id}                     Apple Developer Team ID(10 文字)
{kcpw}                        build keychain ローカル鍵(任意の文字列)
{provisioning_profile_name}   Provisioning Profile 名
#!/bin/bash
set -euo pipefail

KCNAME="build.keychain"
KCPATH="$HOME/Library/Keychains/${KCNAME}-db"
KCPW="{kcpw}"                  # build keychain アンロック用(ローカル container 鍵。任意の文字列)
EXPORTPW="$(uuidgen)"          # .p12 一時暗号用(本スクリプト中だけ使用)

if [[ -f "$KCPATH" ]]; then
    echo "build keychain は既に存在: $KCPATH"; exit 1
fi

# 1. build keychain 作成
security create-keychain -p "$KCPW" "$KCNAME"
security set-keychain-settings -lut 21600 "$KCNAME"
security unlock-keychain -p "$KCPW" "$KCNAME"

# 2. login keychain から identity を export(GUI で「常に許可」を選ぶ)
TMPP12="$(mktemp -t build-cert).p12"
trap 'rm -f "$TMPP12"' EXIT
security export -k "$HOME/Library/Keychains/login.keychain-db" \
    -t identities -f pkcs12 -P "$EXPORTPW" -o "$TMPP12"

# 3. build keychain へ import + codesign / productbuild が触れるよう -T 指定
security import "$TMPP12" -k "$KCNAME" -P "$EXPORTPW" \
    -T /usr/bin/codesign -T /usr/bin/security \
    -T /usr/bin/productbuild -T /usr/bin/productsign

# 4. ACL で GUI プロンプトを抑制
security set-key-partition-list \
    -S 'apple-tool:,apple:,codesign:' -s -k "$KCPW" "$KCNAME"

# 5. search list の先頭に追加
ORIGINAL_LIST="$(security list-keychains -d user | sed -e 's/^[[:space:]]*//' -e 's/"//g' | tr '\n' ' ')"
security list-keychains -d user -s "$KCNAME" $ORIGINAL_LIST

security find-identity -v -p codesigning "$KCNAME"

ポイントは 4 番目の set-key-partition-list。ここで apple-tool: / apple: / codesign: をアクセス許可リストに入れて、codesign 系ツールが GUI 確認なしに鍵を使える状態にする。これが SSH 経由でも証明書アクセスできる正体。

archive.sh — Release archive を非対話で生成

2 本目のスクリプト。SSH 経由で xcodebuild archive を叩く。実行前に build keychain を unlock して、6 時間自動ロックを設定する。

#!/bin/bash
set -euo pipefail

APP_NAME="{app_name}"          # ← 自分のアプリ名に置き換え
KCPW="{kcpw}"                  # ← setup_build_keychain.sh で使ったのと同じローカル鍵

ARCHIVE_PATH="${ARCHIVE_PATH:-/tmp/${APP_NAME}.xcarchive}"
BUILD_KCPW="${BUILD_KCPW:-${KCPW}}"
WORKSPACE="${WORKSPACE:-$(pwd)/${APP_NAME}.xcworkspace}"

if [[ ! -f "$HOME/Library/Keychains/build.keychain-db" ]]; then
    echo "✗ build.keychain-db が無い。setup_build_keychain.sh を先に。" >&2
    exit 1
fi

# build keychain unlock(6h 自動ロック)
security unlock-keychain -p "$BUILD_KCPW" build.keychain
security set-keychain-settings -lut 21600 build.keychain

# archive
rm -rf "$ARCHIVE_PATH"
LOG=/tmp/archive.log
xcodebuild \
    -workspace "$WORKSPACE" \
    -scheme "$APP_NAME" \
    -configuration Release \
    -destination 'generic/platform=iOS' \
    -archivePath "$ARCHIVE_PATH" \
    archive \
    > "$LOG" 2>&1 || true

if grep -q "ARCHIVE SUCCEEDED" "$LOG"; then
    echo "✓ ARCHIVE SUCCEEDED -> $ARCHIVE_PATH"
else
    echo "✗ ARCHIVE FAILED (log: $LOG)"
    grep -B1 -A4 -E 'error:|errSec|FAILED' "$LOG" | tail -30
    exit 1
fi

ログを /tmp/archive.log に逃がして、成否を ARCHIVE SUCCEEDED 文字列で判定する形。xcodebuild の標準出力は冗長なので SSH 越しに見るのは消耗する。

ExportOptions.plist — Manual signing を明示

archive から IPA をエクスポートするときの設定ファイル。Auto signing で何度かハマったので、Manual signing で profile 名と Team ID を明示する形に固定した。git にコミットしても秘匿情報は含まれない。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>method</key>
    <string>app-store-connect</string>
    <key>teamID</key>
    <string>{team_id}</string>
    <key>signingStyle</key>
    <string>manual</string>
    <key>provisioningProfiles</key>
    <dict>
        <key>{bundle_id}</key>
        <string>{provisioning_profile_name}</string>
    </dict>
    <key>uploadSymbols</key><true/>
    <key>compileBitcode</key><false/>
    <key>stripSwiftSymbols</key><true/>
</dict>
</plist>

Bundle ID とプロファイル名は自分の値に置き換える。プロファイル名は Apple Developer ポータルで作成したもの。

upload.sh — IPA エクスポート + altool アップロード

3 本目。archive を再利用して IPA を吐き、xcrun altool で TestFlight に投げる。altoolApp Store Connect API Key 認証だと 2 段階認証ダイアログが出ない。

#!/bin/bash
set -euo pipefail

APP_NAME="{app_name}"          # ← 自分のアプリ名に置き換え
KCPW="{kcpw}"                  # ← setup_build_keychain.sh で使ったのと同じローカル鍵

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
ARCHIVE_PATH="${ARCHIVE_PATH:-/tmp/${APP_NAME}.xcarchive}"
EXPORT_DIR="${EXPORT_DIR:-/tmp/${APP_NAME}-export}"
EXPORT_OPTIONS="${EXPORT_OPTIONS:-$SCRIPT_DIR/ExportOptions.plist}"
ALTOOL_ENV="${ALTOOL_ENV:-$PROJECT_ROOT/Config/altool.env}"
BUILD_KCPW="${BUILD_KCPW:-${KCPW}}"

# Config/altool.env から ASC_KEY_ID / ASC_ISSUER_ID をロード
source "$ALTOOL_ENV"

# archive が無ければ自動で archive.sh を呼ぶ(再利用が効く)
if [[ ! -d "$ARCHIVE_PATH" ]]; then
    bash "$SCRIPT_DIR/archive.sh"
fi

# IPA 出力(exportArchive も再署名するので keychain unlock)
security unlock-keychain -p "$BUILD_KCPW" build.keychain

rm -rf "$EXPORT_DIR"
xcodebuild -exportArchive \
    -archivePath "$ARCHIVE_PATH" \
    -exportPath "$EXPORT_DIR" \
    -exportOptionsPlist "$EXPORT_OPTIONS" \
    > /tmp/export.log 2>&1 || true

IPA_PATH="$(find "$EXPORT_DIR" -maxdepth 1 -name '*.ipa' | head -n1)"
[[ -f "$IPA_PATH" ]] || { echo "✗ IPA 生成失敗"; exit 1; }

# TestFlight アップロード
xcrun altool --upload-app -f "$IPA_PATH" -t ios \
    --apiKey "$ASC_KEY_ID" --apiIssuer "$ASC_ISSUER_ID"

Config/altool.env.gitignore 済みで、中身は 2 行だけ。

ASC_KEY_ID=ABC123XYZ4
ASC_ISSUER_ID=12345678-90ab-cdef-1234-567890abcdef

.p8 ファイル本体は ~/.appstoreconnect/private_keys/AuthKey_<KEY_ID>.p8 に置く(altool がこのパスを自動で見に行く)。

WSL2 から 1 行で TestFlight に上げる

ここまで揃えると、WSL2 側の運用は 1 行で済む。

# CFBundleVersion を bump して push したあとに
ssh {user}@{mac_addr} 'cd ~/path/to/{app_name} && bash scripts/upload.sh'

3〜6 分後に TestFlight に新ビルドが現れる。App Store Connect の Processing が完了したらすぐ自分の iPhone で Install 可能。

詰まりポイント

  • codesign が GUI プロンプトを出して固まる: build keychain の set-key-partition-list を忘れている。apple-tool:,apple:,codesign: を必ず指定
  • Auto signing が別チームの Development cert にフォールバック: Release は Manual signing 固定 + Team ID 明示が安全。ExportOptions.plistsigningStyle = manual
  • build keychain が 6 時間で自動ロックされて翌日のビルドが落ちる: archive.sh / upload.sh の冒頭で毎回 security unlock-keychain を呼ぶ。一度の set-keychain-settings -lut 21600 だけだと足りない
  • SPM の dynamic framework が Embed されない: RealmSwift のような source-distributed dynamic framework は project.pbxproj の Embed Frameworks に手動登録が必要。Firebase / GoogleMobileAds の XCFramework binary target は auto-embed されるので忘れがち
  • altool の 2 段階認証ダイアログ: パスワード認証だと 2FA でブロックされる。必ず App Store Connect API Key(.p8)認証を使う
  • Mac の sshd が起動していない: システム設定 → 一般 → 共有 → リモートログイン を有効化

よくある質問

Q. Mac は普段スリープでも大丈夫?

A. 配信するときだけ叩くなら Wake-on-LAN + システム設定の「ネットワークアクセスによりスリープ解除」を ON にすれば SSH パケットで起きる。常時稼働させるなら蓋を閉じても寝ない設定(caffeinate + 電源接続中はスリープしない)にしておく。

Q. Apple Distribution の証明書はどう発行する?

A. Mac の Xcode で Settings → Accounts → Apple Developer の Manage Certificates から「Apple Distribution」を発行 → login keychain に登録される。setup_build_keychain.sh を走らせるとそれが build keychain に複製される。

Q. Provisioning Profile を更新したらどうする?

A. Apple Developer ポータルでダウンロード → Mac で開く → Xcode の Signing & Capabilities で読み込まれているか確認。ExportOptions.plist の profile 名は変わらないので、スクリプト側の修正は不要。

Q. Mac mini を専用 archive サーバにしたい

A. M2 / M4 の Mac mini なら Xcode 26 + SPM の archive で 3〜5 分。常時電源 ON でも消費電力 10W 前後。Mac mini + LINE Notify でビルド完了通知を飛ばせば、外出中でも完走を把握できる。

Q. fastlane を使ったほうが早い?

A. fastlane は match による証明書同期や lane の構造化が便利。本記事のスクリプトは 「Mac 1 台で完結する個人配信」に絞っているので fastlane の機能のごく一部だけを薄く再実装した形。チーム開発になったら fastlane に乗り換える価値はある。

※本記事の手順は執筆時点(2026 年 5 月)の Xcode 26 / macOS 15 系で動作確認しています。Apple Distribution 証明書の発行手続きや App Store Connect の UI は変わる場合があります。動かない場合は コメント欄でお知らせください。

まとめ

Xcode を WSL2 から完全リモートで動かすのは build keychain と App Store Connect API Keyの 2 点を押さえれば素直に組める。シェルスクリプト 3 本(setup_build_keychain.sh / archive.sh / upload.sh)と ExportOptions.plist 1 枚で完結する。Mac は Xcode 専用の archive マシンとして奥に置いて、普段の開発は Linux の好みの環境で行える。

この記事が役に立ったら X(Twitter)でシェアしてもらえると喜びます。

関連記事

参考

※この記事は Claude Code を使った自動更新を試しています。

7 年放置の iOS アプリを Claude Code で復活させて App Store に出した 6 日間【Xcode 26 / Swift 6 / v3.0.0】

この記事は「7 年前に作ったきり放置している iOS アプリを最新 Xcode / Swift で動かし直してリリースしたい個人開発者」向け。自作蔵書管理アプリ「My Books」を Claude Code とペアで 6 日間モダナイズし、Xcode 26 / Swift 6 / iOS 26 対応で v3.0.0 として TestFlight 18 ビルド経由で App Store 公開(2026-05-02)するまでの全記録。コンパイル成功の後で踏んだ実機の罠も全部書く。

古い iPhone と Xcode が起動した新しい MacBook が並ぶ作業デスクの写真

この記事は何の話か

  • 2016 年初版・My Books(蔵書管理アプリ)7 年放置で塩漬けになった Swift / CocoaPods / Realm / 各種 SDK をまとめて現行スタックに置換し、2026-05-02 に App Store 公開した過程
  • WSL2 で Claude Code、ビルドは別マシンの Mac に SSH 投げ、TestFlight 配信まで 1 コマンド化
  • コンパイルが通ったあとの 実機 TestFlight クラッシュ連戦 の中身(DGCharts vtable・楽天 API 廃止・Google Books クォータ枯渇など)
  • AI ペアプロで「人間がやるべきこと」と「AI に任せられること」がどう分かれたか

数字でみるモダナイズ

項目
作業期間2026-04-25 〜 2026-05-01(6 日)
コミット数約 60
TestFlight ビルド数18
追加したテスト20(AppUtilTests / BookTests / ItemEnumsTests / ViewUtilTests)
リリースバージョンv3.0.0 / Build 18
App Store 公開2026-05-02
対象環境Xcode 26 / Swift 6 / iOS 17+ / 旧 master ブランチは凍結

全体の流れ

大きく 3 フェーズに分かれた。前半 6 日のうち 5 日がモダナイズ、半日が配信パイプ整備、残りが TestFlight 配信中の障害対応と App Store 申請。Build 18 がそのまま審査を通過して 2026-05-02 に公開された。

① モダナイズ(Phase 0–8 / 4 月 25 – 30 日)
CocoaPods → SPM、Realm @Persisted、async/await、ViewController 分割、シークレット管理、テスト追加
② TestFlight 配信パイプ構築(4 月 30 日午後)
WSL → SSH → Mac → archive → IPA → altool → TestFlight
③ TestFlight 連戦 → App Store 申請・公開(4 月 30 日夜 – 5 月 2 日)
Build 5 → 18、実機クラッシュ・API 障害を潰したあと Build 18 で審査提出 → 2026-05-02 に v3.0.0 公開

① モダナイズの中身

計画 → 削除 → API 移行 → Realm 刷新 → ViewController 分割 → シークレット整理 → テスト追加、の順。

  • CocoaPods → SPM(Realm 20.0.4 / Firebase iOS SDK 11.15.0 / Google Mobile Ads 11.13.0 / DGCharts 5.1.0)
  • Amazon PA-API 関連を全削除: 旧 RWMAmazonProductAdvertisingManager / HMAC.m / Bridging Header / BarcodeCacher 補助アプリも一掃
  • deprecated API 撲滅: arc4random_uniform / statusBarOrientation / UILocalNotification / kCLLocationAccuracyKilometer / AVCaptureConnection.videoOrientation など総 30 箇所超
  • async/await 化: ProductSearchwithTaskGroup ベースの並列に。Google Books / 楽天 / OpenBD の 3 プロバイダを async throws 統一。delegate プロトコル群を全廃
  • Realm モダナイズ: @objc dynamic@Persisted、旧マイグレーション破棄して deleteRealmIfMigrationNeeded = true + 「データをリセットしました」アラート方式に切替
  • ViewController 分割: 1135 行 → 471 行、4 つの extension(Camera / ProductSearch / Library / Speech)に責務分離
  • シークレット管理: Config/Secrets.template.xcconfig を commit、実値の Secrets.xcconfig は gitignore。Info.plist は $(VAR) 経由で展開

② TestFlight 配信パイプ

WSL2 でコード編集 → GitHub push → 別マシンの Mac に SSH → archive → IPA → TestFlight アップロードを 1 コマンドで完了させたかった。最終的にスクリプト 3 本で運用している。

scripts/setup_build_keychain.sh   # 1 度だけ実行、専用 build keychain に証明書を複製
scripts/archive.sh                # Release 構成で xcodebuild archive を非対話実行
scripts/upload.sh                 # archive 再利用で IPA → xcrun altool で TestFlight

App Store Connect API Key(.p8)認証で altool に渡す。fastlane も併用しているがテスト・ビルド検証用で、配信本体は素の xcodebuild + altool。

③ TestFlight 連戦

ここが一番時間を食った。Build 5 → 18 で 1 日半。コンパイルが通ること実機 iOS 26 で動くことの間に、思った以上に距離がある。

Build 18 を App Store 提出に回し、Apple 審査は追加質問なしで通過。2026-05-02 に v3.0.0 として App Store 公開。バージョン番号と Build 番号は TestFlight 最終ビルドのまま据え置き。

個人的に印象に残った踏み込み 5 件

1. Xcode 26 のビルドサンドボックス

死んでいた scripts/update_storyboard_strings.sh(コメント 1 行のみ)と Run Script Phase が残っていた。Xcode 26 のビルドサンドボックスは 空 Run Script Phase でもパスが解決できないとエラーに昇格させる。コメントだけ残ったスクリプトでも sandbox がエラー判定するため、Run Script Phase ごと削除する必要があった。

2. 楽天ウェブサービスが 13 日後に廃止

書影が出ない症状を切り分けていく中で、楽天ブックス API が 2026-05-14 に旧 app.rakuten.co.jp を完全停止して新 openapi.rakuten.co.jp に移行する真っ最中であることが判明。残り 13 日。

  • 必須パラメータが applicationId + accessKey に増加
  • アプリケーションタイプが Webアプリケーション(許可ウェブサイト)か バックエンドサービス(許可IP)の 2 択
  • モバイルアプリは Webアプリケーション一択。Origin ヘッダ必須、無いと 403 REQUEST_CONTEXT_BODY_HTTP_REFERRER_MISSING

新ドメインで Webアプリケーション型として再登録 → Secrets.xcconfig に新 ApplicationId / AccessKey / 許可ドメインを格納 → ProductSearchByRakuten.swiftOrigin / Referer ヘッダを毎回付与する形に書き換え。楽天ウェブサービスを使っているアプリは早めの移行を強く勧める。

3. Google Books の匿名共有クォータ枯渇

無認証で Google Books API を叩くと 429 Quota Exceeded。エラー JSON の project_number を見ると自分のものではない。仕様を読み直すと、API キー無し呼び出しは世界中の anonymous caller が同じ匿名共有プロジェクトの 20M/day クォータを取り合うとのこと。常時枯渇。自分の GCP project で API キーを発行 → ?key= クエリで送る形に修正。

4. DGCharts vtable で 4 ビルド消費 → 自前 CoreGraphics 実装に置換

Build 5–8 が同じ症状で連続クラッシュ。ShelfViewController.setChart() 内の PieChartView.data = ... setter が EXC_BAD_INSTRUCTION (brk #1).ips を atos で symbol 解決すると、DGChartsDGChartsDynamic.framework647 個の symbol が同じ stub に aliasされていた。Xcode 26 + Swift 6 + SPM の dead-strip が暴走している疑い。

静的→動的の切替も KVC 経由の objc_msgSend 逃げ道も効かず、最終的にチャート機能(3 スライス + 中央テキストのドーナツ図)を BookStatusPieView: UIView(約 60 行)として CoreGraphics で自前実装に置換した。Storyboard の customClass を差し替えて完了。

5. SSH 経由の codesign と TestFlight 配信パイプ

Mac に SSH して xcodebuild archive しても codesign が login keychain にアクセスできず失敗する問題(login keychain は SSH で unlock しても codesign から触れない仕組み)と、専用 build keychain で ACL を緩和してパイプを 1 コマンド化した話は分量が多くなったので別記事に切り出した。WSL2 → SSH → Mac → archive → IPA → altool までスクリプト 3 本で組む全手順は 「WSL2 から SSH で Mac の Xcode を archive して TestFlight に上げる最短手順」を参照。

AI ペアプロの分担

6 日通して回した結果、人間と AI がそれぞれ得意な領域がはっきりした。

担当 タスク
Claude Code crash log(.ips)の atos symbol 解決 / curl での API 切り分け / コード修正 / SSH 経由の archive・upload / docs と memory の同期更新 / コミットメッセージ生成
人間 実機操作 / App Store Connect の各種同意・申請 / GCP / 楽天 Developers のブラウザ操作 / 物理的な耳と目(音量・スピーカー挙動・バナー表示の確認)

AI に向かないタスクを無理に押しつけない切り分けが効率に直結した。逆に crash log の symbol 解決と API の curl 切り分けは AI が極端に速い

学び

  • コンパイル成功 ≠ 動作: 7 年ぶんの API 変更を Xcode が静的に拾える範囲には限界がある。実機 TestFlight に流して初めて出る不具合(DGCharts vtable / Storyboard の幽霊 outlet / ATS / ATT / AVAudioSession)が大半
  • サードパーティ API は独立進化する: Google Books の匿名共有クォータ枯渇 / 楽天の旧 API 13 日後廃止 / AdMob SDK 11+ の adSize 必須化、いずれもアプリのコードと無関係に進んでいた変更
  • テストパイプ整備が前提条件: WSL → SSH → Mac → TestFlight の 1 コマンド化が無ければ Build 1 → 18 を 1 日で回せなかった
  • Xcode 26 の罠: ENABLE_DEBUG_DYLIB / sandbox / phantom scheme / SPM Embed Frameworks。test target 立ち上げと配信パイプ構築の双方で踏む
  • 「実機で 1 日触って何も起きない」までやって完了: コード変更は前段、TestFlight crash 対応が時間的にはほぼ同等の工数になる

※本記事の作業は執筆時点(2026 年 5 月)の Xcode 26 / Swift 6 / iOS 17–26 で行った。サードパーティ API の仕様変更や SDK のバージョンによってはそのまま再現しない箇所があります。気づきがあれば コメント欄でお知らせください。

まとめ

7 年放置していた個人 iOS アプリでも、Xcode 最新版と Claude Code を組み合わせれば 6 日で v3.0.0 を App Store まで届けられる。ただし「コンパイルが通る」と「実機で動く」は別物で、TestFlight に流してからの障害対応が想像以上に時間を食う。サードパーティ API の独立進化と Xcode 26 のビルドサンドボックスは、コードに触らなくても勝手に効いてくる。配信パイプを早めに整えておくとそこから速く回せる。

App Store: My Books(蔵書管理アプリ)(v3.0.0 / 2026-05-02 公開)。役に立ったら X(Twitter)でシェアしてもらえると喜びます。

関連記事

参考

※この記事は Claude Code を使った自動更新を試しています。

ブログ用 AI 画像をローカル GPU で生成する【SDXL + IP-Adapter + img2img・実生成画像で進化を全部書く】

毎週Claude Codeに投稿してもらうブログ記事のカバー画像と部品紹介イラストを、ローカル GPU で自動生成する仕組みを作った。SDXL を素直に動かす段階から、IP-Adapter で実機写真に寄せる段階、img2img で手書き風イラストにする段階まで、生成画像が実際にどう変わるかを 6 枚の比較画像で全部書く。VRAM 6GB マシンで詰まったメモリ制約と、それを回避するために選んだアプローチも含む。

本記事の手順は全てClaude Codeに構築してもらい、記事の執筆も99% Claude Codeだが、試行錯誤の経緯は実話。

img2img + 高 denoise で生成した XIAO ESP32-S3 の水彩イラスト。本記事のゴール

この記事でできること

  • GTX 1660 SUPER(VRAM 6GB)+ メインメモリ 16GB の Linux PC で SDXL を回す全体像がわかる
  • IP-Adapter で「実機の見た目に近い」AI 画像を作れる
  • img2img + 高 denoise で「手書き風水彩イラスト」を作れる
  • VRAM 6GB マシンで起こりがちなメモリ不足の回避策がわかる

使ったもの

なぜローカル生成にしたのか

毎週ブログ記事を 1 本書くペースだと、API 課金型の画像生成サービスは「試行錯誤の心理的コスト」が積み上がる。プロンプトを 5 回練り直すたびに少額が引かれていく感覚が、実験的な調整の手を止めてしまう。ローカル GPU で完結させれば、コストは電気代だけ。気が済むまで --seed を変えて回せる。

もうひとつの理由は依存関係を自分の管理下に置きたいこと。API は仕様変更や値上げ、サービス停止で動かなくなる。ローカル環境ならモデルファイルとコードを保存しておけば、5 年後でも同じ手順で動かせる。

Step 1: ComfyUI + SDXL を素のまま動かす

まず ComfyUI を 公式サイト の手順通りにインストールし、Juggernaut XL v9 を checkpoints/ に配置する。プロンプトに「watercolor illustration of XIAO ESP32-S3」と書いて txt2img を回した結果がこれ:

参照画像なしの SDXL 出力。XIAO ESP32-S3 を指定したのに、実物とは似ていない汎用ガジェット風の絵が出る

水彩のスタイル指定は通っているが、絵は架空のガジェット。SDXL のベースモデルは「具体的なボード名」までは学習していないため、それっぽい何かを出力してくる。

Step 2: 「実機の見た目」を出すには参照画像が要る

ブログで「この XIAO ESP32-S3 で温湿度モニタを作りました」と書きたいときに、絵が違うボードでは記事の信頼性が落ちる。AI が学習していない具体的なハードを正確に出すには、実物写真を参照画像として渡す仕組みが要る。それが IP-Adapter。

Step 3: IP-Adapter Plus を導入する

カスタムノード ComfyUI_IPAdapter_pluscustom_nodes/ に clone し、Hugging Face から:

  • ip-adapter-plus_sdxl_vit-h.safetensors(~700MB)→ models/ipadapter/
  • CLIP-ViT-H-14-laion2B-s32B-b79K.safetensors(~2.5GB)→ models/clip_vision/

を DL する。XIAO の公式商品写真を参照画像として渡し、weight 0.5 で生成すると:

IP-Adapter weight 0.5 で生成した XIAO ESP32-S3。実物の構造が忠実に反映されている

実物の構造が忠実に再現される。USB-C コネクタ、シールドされた無線モジュール、QR コード状のチップマーキングまで「これは確かに XIAO ESP32-S3」と認識できる。が、よく見るとプロンプトに書いた watercolor illustration の指定は完全に消えて写実調になっている。

IP-Adapter Plus は「style transfer 型」で、参照写真の色・質感・スタイルが結果に強く転写される。これはカバー画像(実物に近づけたい)には強力だが、イラスト調にしたいときは邪魔になる

Step 4: 「写真」と「イラスト」を使い分けたい

カバー画像は写実調で OK。だが本文中の「使ったもの」セクションで部品を紹介するときは、イラスト調にして視覚的なリズムを変えたい。同じ記事内で全部写実調だと単調になる。

Step 5: IP-Adapter weight を下げてみる(失敗)

素直な発想として、IP-Adapter の weight を下げれば写真感が薄まりプロンプトの指示が通るのでは、と試す。weight を 0.3 まで下げ、ネガティブプロンプトで (photorealistic:1.6) を強く指定した結果:

IP-Adapter weight 0.3 でも参照写真の style が支配的で写実調のまま

weight 0.3 でも写真スタイルが支配的。Plus 変種は 低 weight でも style が残る性質があるため、イラスト化用途には根本的に向いていないことがわかる。

Step 6: ControlNet を試したが封印

次に試したのが ControlNet (Canny)。参照写真から輪郭線だけを抽出し、色や質感はプロンプトに完全に任せる、というアプローチ。理屈の上ではイラスト化にぴったり。

動作はする。ただし VRAM 6GB + メインメモリ 16GB の構成で、SDXL + IP-Adapter + ControlNet を同時にロードすると長時間稼働後に他プロセスとのメモリ競合で詰まる事象が頻発した。短時間だけ動けば成功するが、稼働を続けるとプロセスが応答しなくなる。

ブログ更新は cron で毎週走らせたい。その間に詰まると記事が出ない。「動作はするが信頼性が足りない」機能は cron 運用には載せられないので、保守的に封印した。

Step 7: img2img + 高 denoise で解決

残された道は img2img + 高 denoise。仕組みはこうだ:

  1. 参照写真を VAE エンコードして初期 latent に変換する
  2. KSampler の denoise を高めに設定する(0.9 推奨)
  3. KSampler は初期 latent からサンプリング過程を進めるが、denoise が高いほど「初期 latent の影響が薄れ、プロンプトの指示が支配的になる」

結果、構造(部品の輪郭・配置)はうっすら残るが、色・質感・スタイルはプロンプトが完全制御する。重要なのは:

  • カスタムノード追加なし(ComfyUI の標準ノードだけで構成できる)
  • 追加モデル DL なし
  • メモリ消費は txt2img と同等

つまり ControlNet を回避しつつ「輪郭参照 + プロンプト主導のスタイル」を実現できる。VRAM 6GB マシンで安定して回せる。

Step 8: denoise の最適値を実測で探る

img2img の denoise をいくらにするかが品質を左右する。同じプロンプト・同じ seed・同じ参照画像で 0.8 / 0.85 / 0.9 を比較した。

denoise 0.8

img2img denoise 0.8。まだ参照写真の構造が支配的で写実調のまま

まだ写真寄り。「水彩」のスタイル指定はうっすら効いているが、見た目は写実画像に水彩フィルタを軽くかけた程度。

denoise 0.85

img2img denoise 0.85。0.8 とほぼ変わらず写実寄り

0.05 上げても変化はわずか。やはり初期 latent の写真感が残る。

denoise 0.9

img2img denoise 0.9。水彩イラストと部品識別可能のバランスに到達した最終形

来た。水彩の柔らかさと、XIAO ESP32-S3 の特徴的な形(USB-C コネクタの位置、無線モジュールのシールド、castellated pin の並び)が両立している。denoise 0.9 が現状ベスト値

0.95 まで上げると今度は構造が崩れて「水彩なんだけど何のボードか分からない」絵になりやすい。0.9 が「形のヒントは残し、スタイルはプロンプト主導」のバランス点だった。

Step 9: 完成したパイプラインを実際の記事にも適用

このパイプラインで、別記事用に Raspberry Pi Pico 2 W のイラストも作ってみた。参照画像は Raspberry Pi 公式 のプレス画像、denoise 0.9、生成時間は約 2 分:

Raspberry Pi Pico 2 W の水彩イラスト。同じパイプラインで別の部品にも適用できることを確認

緑色 PCB、castellated pin の金色、micro USB コネクタの位置、無線モジュールのシールド形状がそれぞれ識別できる。同じスクリプト、同じ denoise 0.9 で部品を入れ替えるだけでこの粒度の出力が再現できることが確認できた。

よくある質問

Q. GPU が VRAM 6GB しかなくても動きますか?

A. 動きます。SDXL を fp8 量子化(ComfyUI の起動オプションで --fp8_e4m3fn-unet を指定)で約 4GB に抑え、IP-Adapter を含めても VRAM 5.5GB 程度です。ただし ControlNet を同時にロードすると 6GB 上限に近づき不安定になります。

Q. 1 枚生成に何秒かかりますか?

A. GTX 1660 SUPER で 15 steps の txt2img なら約 90 秒、IP-Adapter 付きで約 120 秒、25 steps の img2img illustration なら約 120 秒です。クラウド GPU に比べると遅いですが、コストは電気代だけなので無制限に試行錯誤できます。

Q. denoise 0.9 が決め手とのことですが、なぜ 0.7 や 0.8 では効果が薄いのですか?

A. img2img では denoise が低いほど初期 latent(参照写真の構造)が結果に強く残ります。0.7-0.85 だと参照写真の写真感がうっすら残ってしまい、プロンプトのイラスト指定が押し負けます。0.9 で初期 latent の影響がほぼ「形のヒント」程度まで弱まり、スタイルはプロンプトが完全制御する状態になります。

Q. なぜ IP-Adapter Plus じゃなく素の IP-Adapter を試さないのですか?

A. 素の ip-adapter_sdxl_vit-h.safetensors は Plus に比べて style 転写が弱いと言われており、選択肢として残っています。ただし img2img + 高 denoise が「カスタムノード追加なし・追加モデル DL なし」という運用上のシンプルさで上回るので、現状はそちらを採用しています。

※本記事の手順は執筆時点(2026 年 5 月)で動作確認していますが、ComfyUI のバージョンや IP-Adapter のモデル更新によりそのままでは動かない場合があります。動かない場合は コメント欄でお知らせください。

まとめ

SDXL → IP-Adapter → img2img と機能を足すたびに、生成画像が「汎用ガジェット → 実機の写実 → 手書き風イラスト」と段階的に変化していく過程を、実際の生成画像 6 枚で見せてきた。

VRAM 6GB のマシンでは 「カスタムノード追加なし」「メモリ消費 txt2img 並み」のアプローチが結局シンプルで強かった。img2img + 高 denoise は新しい技術ではないが、IP-Adapter のスタイル転写問題を回避する手段として再評価する価値がある。

「機能を足せば足すほど良くなる」とは限らない。制約に合わせてアプローチを切り替える柔軟性が、ローカル環境での AI 画像生成では一番大事だった。

この記事が役に立ったら X(Twitter)でシェアしてもらえると喜びます。

関連記事

参考

※この記事は Claude Code を使った自動更新を試しています。

2026年4月29日水曜日

Raspberry Pi Pico 2 W + MicroPython で温湿度センサーを自作する【2026年版・Home Assistant に MQTT 送信】

※Claude Codeで記事の自動更新お試し中

Raspberry Pi Pico 2 W の水彩イラスト(緑色 PCB と castellated pin、無線モジュール、micro USB)

Raspberry Pi Pico 2 W が出てから「結局 ESP32 と何が違うんだ?」と聞かれることが増えました。RP2350 のデュアルコア Cortex-M33 + Wi-Fi/BLE が載った Pico 2 W で、温湿度センサー BME280 の値を Home Assistant に MQTT で送信し、Auto Discovery で勝手にセンサーが出てくるところまでを最短で通します。半田付け不要、ブレッドボードのみ。電子工作初学者でも 1〜2 時間で動きます。

この記事でできること

  • Raspberry Pi Pico 2 W に MicroPython を入れて Wi-Fi に繋げられる
  • BME280 で温度・湿度・気圧を I2C 経由で読める
  • Home Assistant に MQTT Discovery でセンサーが自動登録される(YAML 不要)
  • 必要なものは合計 2,500〜3,500円から
  • Pico W で詰まりがちな umqtt.simple のインストール方法と、無通信で切断される問題への対処がわかる

なぜ Pico 2 W なのか

ESP32 系で同じことができるのは事実ですが、Pico 2 W には MicroPython 公式サポートが手厚い・Thonny で REPL に即繋がる・RP2350 のデュアルコアで片方を BLE 受信に回せるといった違いがあります。Wi-Fi 4 + BLE 5.2 が両方乗っているので、後から「BLE で SwitchBot 温湿度計のデータも吸う」みたいな拡張がそのままできます。今回は MQTT 中心ですが、最後に拡張アイデアを置いておきます。

使ったもの

母艦側のソフトウェア環境:

  • MicroPython v1.24 以降(Pico 2 W 公式 UF2)
  • Thonny(REPL/ファイル転送/mip パッケージ管理を全部こなす)
  • Home Assistant(HAOS / Container どちらでも可)
  • Mosquitto(Home Assistant の Mosquitto broker アドオンが楽)

配線

BME280 は I2C で繋ぎます。Pico 2 W の I2C0 デフォルト(GP4=SDA / GP5=SCL)に合わせるとコードが短くなります。

Raspberry Pi Pico 2 W
3V3(OUT) → VIN
GND → GND
GP4 (SDA) → SDA
GP5 (SCL) → SCL
BME280 (I2C)

BME280 モジュールに 3.3V レギュレータが載っていれば 5V でも動きますが、ノイズと自己発熱(温度がやや高めに出る)を抑えるために 3V3(OUT) で給電するのを勧めます。

手順

1. MicroPython を Pico 2 W に書き込む

Pico 2 W の BOOTSEL ボタンを押しながら USB を挿すとマスストレージとして認識されます。ここに RP2350 用の最新 UF2 をドラッグ&ドロップ。

# raspberrypi.com/documentation/microcontrollers/micropython.html
# 「Raspberry Pi Pico 2 W」用 .uf2 を落として RPI-RP2 ドライブにコピー
# 自動で再起動して MicroPython が起動する

2. Thonny から umqtt.simple を入れる

古い記事だと upip でインストールしろと書いてありますが、2026 年現在は upip は廃止。代わりに mip を使います。Thonny のシェル(MicroPython 側)で:

>>> import mip
>>> mip.install("umqtt.simple")
>>> mip.install("umqtt.robust")  # 切断時の自動再接続用

Pico 2 W が Wi-Fi 接続前だと当然失敗するので、先にネットに繋ぐか、それが面倒なら PC で simple.py を落として Thonny でアップロードしても OK。

3. BME280 ドライバを入れる

MicroPython 用 BME280 ドライバはいくつかありますが、robert-hh/BME280 が安定。bme280_float.py を Thonny のファイルブラウザ経由で Pico に転送します。

4. main.py を書く

Wi-Fi 接続 → BME280 から読む → Home Assistant に Discovery 設定を一度送る → 60秒おきに値を publish、という最小構成にします。SECRETS は別ファイルに切り出すと git 管理しやすいですが、まずはベタ書きで動かします。

import network, time, ujson, machine
from umqtt.robust import MQTTClient
import bme280_float as bme280

WIFI_SSID = "YourSSID"
WIFI_PASS = "YourPass"
MQTT_HOST = "192.168.1.10"   # Home Assistant の IP
MQTT_USER = "pico"
MQTT_PASS = "picopass"
DEVICE_ID = "pico2w_livingroom"

# --- Wi-Fi ---
wlan = network.WLAN(network.STA_IF); wlan.active(True)
wlan.connect(WIFI_SSID, WIFI_PASS)
for _ in range(20):
    if wlan.isconnected(): break
    time.sleep(0.5)
print("ip:", wlan.ifconfig()[0])

# --- I2C / BME280 ---
i2c = machine.I2C(0, sda=machine.Pin(4), scl=machine.Pin(5), freq=100_000)
sensor = bme280.BME280(i2c=i2c)

# --- MQTT ---
c = MQTTClient(DEVICE_ID, MQTT_HOST, user=MQTT_USER, password=MQTT_PASS,
               keepalive=60)   # ←無通信切断対策
c.connect()

# --- HA Auto Discovery (一度だけ retain で送る) ---
def disc(component, name, key, unit, dev_class):
    topic = "homeassistant/sensor/{}/{}/config".format(DEVICE_ID, key)
    payload = {
        "name": name,
        "state_topic": "ontheh/{}/state".format(DEVICE_ID),
        "unit_of_measurement": unit,
        "value_template": "{{{{ value_json.{} }}}}".format(key),
        "unique_id": "{}_{}".format(DEVICE_ID, key),
        "device_class": dev_class,
        "device": {"identifiers":[DEVICE_ID], "name":"Pico 2W Livingroom",
                   "manufacturer":"Raspberry Pi", "model":"Pico 2 W"},
    }
    c.publish(topic, ujson.dumps(payload), retain=True)

disc("sensor", "リビング温度", "temp", "°C", "temperature")
disc("sensor", "リビング湿度", "hum",  "%",  "humidity")
disc("sensor", "リビング気圧", "pres", "hPa","pressure")

# --- メインループ ---
while True:
    t, p, h = sensor.read_compensated_data()
    payload = ujson.dumps({
        "temp": round(t, 2),
        "hum":  round(h, 2),
        "pres": round(p / 100, 2),    # Pa → hPa
    })
    c.publish("ontheh/{}/state".format(DEVICE_ID), payload)
    print(payload)
    time.sleep(60)

5. Home Assistant 側で確認

Mosquitto broker アドオンを起動してユーザーを作っておけば、main.py を実行した瞬間に Settings → Devices & services → MQTT に「Pico 2W Livingroom」がデバイスとして自動登録されます。YAML 編集も再起動も不要。

# MQTT を覗きたいときは Mosquitto アドオン経由でも OK
$ mosquitto_sub -h 127.0.0.1 -u pico -P picopass -t 'ontheh/#' -v
ontheh/pico2w_livingroom/state {"temp": 24.31, "hum": 53.78, "pres": 1011.42}

詰まりポイント

  • USB を繋いでも認識されない → 充電専用ケーブルあるある。データ線が結線されたケーブルに変える。
  • mip.installnot found → Wi-Fi 未接続。network.WLAN で接続してから実行する。
  • I2C デバイスが i2c.scan() に出てこない → BME280 のアドレスは 0x76 または 0x77。SDO の処理で変わる。bme280.BME280(i2c=i2c, address=0x77) を試す。
  • 数時間後に publish が止まる → ブローカーから keepalive で切られている。MQTTClient(..., keepalive=60) + umqtt.robust で自動再接続させるのが鉄板。
  • 温度が +2℃ ぐらい高めに出る → BME280 の自己発熱。長辺を立てる、5V→3V3 給電に変える、それでもズレるなら value_template でオフセット補正。
  • HA に出ない → Discovery トピックは必ず retain=True で送る。送り損ねた場合は HA を再起動するか、空ペイロード "" を送って消してから再送する。

ハック拡張アイデア

  • BLE モードを使って SwitchBot 温湿度計ハック: Pico 2 W の BLE 5.2 で SwitchBot のアドバタイズを直接受け、HA Bridge を経由せずに温湿度を吸い出す。OpenWonderLabs の公開仕様を読めば実装できます。
  • 0.96 インチ HiLetgo OLED ディスプレイ(SSD1306 / I2C 128×64)(Amazonで確認)を Pico の I2C1 (GP14/GP15) に追加して、温湿度をローカル表示する。BME280 と I2C アドレスを分けて同じバスに乗せても可。
  • RP2350 のもう一方のコアに _thread でリングバッファを置く: 片方のコアが I2C 読み出し、もう片方が MQTT 送信、を完全分離すると Wi-Fi 切断中もサンプリングが止まらない。Pico 1 ではここまでしんどかったのが楽になります。
  • HA の自動化と連動: 「湿度 60% を超えたら除湿器の SwitchBot プラグを ON」みたいな自動化が、Discovery で自動登録されたエンティティ名を参照するだけで書ける。

消費電力メモ

Pico 2 W を 5V USB 給電・60 秒 publish ループで放置すると、平均 70mA 前後(Wi-Fi 維持のため)。machine.deepsleep() でセンサー読み出しの瞬間だけ起こす運用にすると 10〜15mA まで下がりますが、Wi-Fi 再接続に毎回 3〜5 秒かかるので、1 分間隔のホームセンサーなら常時接続のままで実用です。

※本記事の手順・コードは執筆時点(2026 年 4 月)で動作確認していますが、ライブラリやハードのバージョンが変わるとそのままでは動かない場合があります。動かない場合は コメント欄でお知らせください。

まとめ

Raspberry Pi Pico 2 W + BME280 + MQTT Discovery で、半田付けなしの温湿度モニタを Home Assistant に組み込みました。umqtt.simple のインストール方法と keepalive を押さえれば、あとは公式チュートリアル系の罠もそんなにありません。同じ仕組みを増やしてリビング・寝室・玄関に置けば、家全体の温湿度マップが完成します。

この記事が役に立ったら X(Twitter)でシェアしてもらえると喜びます。

参考

ESP32-S3 + ESPHome で部屋のCO2モニタを自作する【2026年版・Home Assistant 連携】

この記事は、自宅の空気質を可視化したいエンジニア向けです。ESP32-S3 と CO2 センサ「SCD41」を ESPHome でつないで、CO2・温度・湿度を Home Assistant に流し込むまでを最短ルートで書きます。半田付けほぼナシ、ファームのコードも全公開です。(在宅ワーク中の「なんか頭がぼーっとする」問題を物理的に可視化したかった、というのが動機です。)

XIAO ESP32-S3 と SCD41 CO2 センサを 4 本のジャンパ線で I2C 接続した完成セットアップ

この記事でできること

  • ESP32-S3 で動くスタンドアロンの CO2 モニタが作れる(消費電力は数十 mW クラス)
  • ESPHome の YAML を書くだけで Home Assistant に自動統合される(コードは記事内で全部公開)
  • 必要なものは合計 4,000 円ちょっとから

使ったもの

全体の流れ

SCD41(CO2 + 温度 + 湿度)
▼ I2C(4 本接続)
XIAO ESP32-S3 + ESPHome
▼ Wi-Fi(ESPHome API)
Home Assistant(可視化・通知・自動化)

手順

1. 配線(4 本だけ)

SCD41 と XIAO ESP32-S3 を I2C で接続します。XIAO のデフォルト I2C は D4 = GPIO5(SDA)D5 = GPIO6(SCL)

SCD41 側 XIAO ESP32-S3 側
VDD3V3
GNDGND
SDAD4(GPIO5)
SCLD5(GPIO6)

2. ESPHome を準備

ESPHome は Home Assistant 公式の「マイコン用ファームウェア生成ツール」。Home Assistant の「ESPHome アドオン」を入れておくのが一番楽(HACS 不要)。ローカルで動かすなら pipx でも入る。

$ pipx install esphome
$ esphome version
2026.4.x

3. YAML を書く(コピペでOK)

以下を co2-monitor.yaml として保存。!secret 部分は自分の Wi-Fi に書き換えてください。

esphome:
  name: co2-monitor
  friendly_name: CO2 Monitor

esp32:
  board: seeed_xiao_esp32s3
  framework:
    type: arduino

logger:
api:
  encryption:
    key: "32 バイトの base64 キーを入れる"

ota:
  - platform: esphome
    password: "適当に"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

i2c:
  sda: GPIO5
  scl: GPIO6
  scan: true

sensor:
  - platform: scd4x
    co2:
      name: "CO2"
    temperature:
      name: "Temperature"
    humidity:
      name: "Humidity"
    update_interval: 60s

4. ファームを書き込む

USB-C で XIAO を PC につないで、初回はシリアル経由で焼きます。

$ esphome run co2-monitor.yaml
# シリアルポートを選んで Enter

2 回目以降は同じネットワーク上なら無線(OTA)で更新できます。これが ESPHome の最大の楽さ。

5. Home Assistant に統合

同じ Wi-Fi 上にあれば自動検出されます。検出されない場合は手動で:

  1. 「設定」→「デバイスとサービス」→「統合を追加」→「ESPHome」
  2. ホスト名 co2-monitor.local を入力
  3. YAML で設定した暗号化キーを入力 → 完了

CO2 / Temperature / Humidity の 3 センサが現れるので、ダッシュボードに「センサカード」「履歴グラフカード」を追加するだけで可視化完成です。

ここから先のハック

  • OLED 表示を追加: 0.96 インチ I2C OLED(SSD1306)を同じバスに繋いで現在の CO2 をその場で見せる。I2C なので配線追加なしでデバイスを増やせる
  • CO2 が高くなったら換気通知: Home Assistant のオートメーションで「1000 ppm が 3 分続いたら Slack / LINE 通知」を組む
  • L チカ警告: XIAO 内蔵 LED や WS2812B LED テープlight: コンポーネントで点灯。CO2 レベルで色を変える(青→緑→黄→赤)
  • バッテリ駆動: XIAO ESP32-S3 はリポ直結&充電回路つき。ディープスリープ+10 分おき計測で数日もつ

詰まりポイント

  • I2C で SCD41 が見えない: i2c: scan: true のログを確認。0x62 が見えれば成功。見えない場合は配線(特に SDA と SCL の逆挿し)を疑う
  • ESPHome のボード指定: board: esp32-s3-devkitc-1 でも動くが、XIAO 専用の seeed_xiao_esp32s3 のほうが GPIO マッピングが正しい
  • 初回の数値が暴れる: SCD41 は電源投入から最初の数分は校正中で値が安定しない。常時通電で運用するのが前提

※本記事の手順・コードは執筆時点(2026 年 4 月)で動作確認していますが、ライブラリやハードのバージョンが変わるとそのままでは動かない場合があります。動かない場合は コメント欄でお知らせください。

まとめ

ESP32-S3 + SCD41 の組み合わせは、半田付けゼロ・コード 80 行で「自宅の CO2 を Home Assistant に流す」までいけます。市販の CO2 モニタを買うより安く、しかもオートメーションで「換気して」と通知させたり、エアコンの強弱を切り替えたり、好きなだけ拡張できる。これが自作の楽しさ。

この記事が役に立ったら X(Twitter)でシェアしてもらえると喜びます。

よくある質問

Q. SCD41 が I2C で見えないときは?

A. i2c: scan: true のログを確認し、0x62 が見えなければ配線(SDA/SCL の逆挿し)を疑ってください。XIAO ESP32-S3 のデフォルト I2C は D4=GPIO5(SDA)D5=GPIO6(SCL) です。

Q. 数値が安定しないときは?

A. SCD41 は電源投入後の最初の数分は校正中で値が安定しません。常時通電で運用するのが前提です。「ウォームアップ中」と割り切って数分待ちましょう。

Q. ESPHome のボード指定はどれが正しい?

A. seeed_xiao_esp32s3 を使います。汎用の esp32-s3-devkitc-1 でも動きますが、XIAO 専用指定のほうが GPIO マッピングが正しく当たります。

Q. バッテリ駆動できますか?

A. はい。XIAO ESP32-S3 はリポバッテリ直結&充電回路つきで、ディープスリープ+10 分おき計測で数日もちます。

関連記事

参考

※この記事は Claude Code を使った自動更新を試しています。

SwitchBot × IFTTT で帰宅前にエアコンを自動 ON【GPS トリガー連携版】

※Claude Codeで記事の自動更新お試し中

この記事は、「帰宅前に自動でエアコンを付けたい」「外部サービスと連携したい」方向けです。IFTTT と SwitchBot を組み合わせることで、GPS 位置情報・天気・時刻など SwitchBot アプリ単体では使えないトリガーでエアコンを操作できます。

3つのアプローチ比較

同じことを実現する方法は3つあります。この記事では IFTTT × SwitchBot の連携方法を解説します。

(1) Raspberry Pi
+ cron
(2) SwitchBot
オートメーション
(3) IFTTT
連携
(本記事)
追加ハードRaspberry Pi 必要不要不要
セットアップPython 実装
+ cron 設定
アプリのみ
(最も簡単)
IFTTTアカウント
+アプレット設定
トリガー制限なし
(Python で自由に)
温度・時刻・
在宅検知など
GPS・天気・
他サービス連携が強み
コストPi の電気代のみ
(約1〜2W)
無料無料プランは
2アプレットまで
ロジックの複雑さヒステリシス・ログ・
拡張など自由
AND/OR 程度IF→THEN の
単純なルール
クラウド依存SwitchBot API のみSwitchBot
クラウドのみ
IFTTT +
SwitchBot クラウド

「アプリだけで済ませたい」なら SwitchBot オートメーション版、「複雑なロジックを書きたい」なら Raspberry Pi 版も参照してください(記事末のシリーズ一覧)。

この記事でできること

  • IFTTT と SwitchBot を連携してエアコンを操作できる
  • 「自宅から 500m 圏内に入ったらエアコン ON」のような GPS トリガーが作れる
  • 天気・時刻・他サービスなど SwitchBot アプリ外のトリガーが利用できる

使ったもの

  • SwitchBot ハブ2(Amazonで確認)— Hub シリーズが IFTTT 連携に対応
  • IFTTT アカウント(無料プランで可、ただし後述の制限あり)
  • スマートフォン(iOS / Android)

注意:IFTTT 無料プランは有効化できるアプレット数が2つまでです。3つ以上使う場合は有料プラン(Pro、月額約600円〜)が必要になります。

事前準備:SwitchBot アプリでエアコンを登録する

Hub 2 でエアコンを操作するには、まずアプリでエアコンの赤外線コードを学習させます。

  1. SwitchBot アプリ → ホーム右上の「+」→「赤外線リモコンを追加」
  2. 「エアコン」→ メーカーを選択(プリセットにあればそのまま使用)
  3. プリセットにない場合は「学習リモコン」で実機リモコンの信号を取り込む
  4. アプリからオン・オフできることを確認する

この操作でエアコンが SwitchBot クラウドに「仮想赤外線リモコン」として登録されます。どの方法でもこの準備は共通で必要です。

IFTTT と SwitchBot の連携手順

1. IFTTT アカウントを作成する

IFTTT の公式サイトでアカウントを作成します。Google アカウントでのサインアップが手軽です。

2. SwitchBot サービスを連携する

IFTTT の検索で「SwitchBot」を探し「Connect」→ SwitchBot アカウントでログインして連携を承認します。

3. アプレットを作成する(例:帰宅前エアコン ON)

「Create」→「If This」→「Location」→「You enter an area」で自宅周辺の円を設定(半径 500m 以上推奨)。「Then That」→「SwitchBot」→「Control your device」でエアコンを ON に設定します。

4. エアコンを OFF にするアプレットも作成する

「You exit an area(エリアから出たら)」または「温度が下がったら」など、状況に応じた OFF ルールを別途作ります。IFTTT 無料プランの2アプレット枠はこれで埋まります。

GPS トリガーの精度について

IFTTT の位置情報トリガーはスマートフォンの GPS を使うため、精度は 100〜500m 程度です。設定エリアの半径を 500m 以上にしておくと誤作動が減ります。また、応答に1〜5分程度の遅延が発生することがあります。「帰宅15分前に ON」を狙うなら半径を 2〜3km にするのが現実的です。

IFTTT 連携の制限

  • 無料プランはアプレット2つまで(ON/OFF で2つ使い切る)
  • IF→THEN の単純なルールのみ(AND/OR 条件は Pro プラン)
  • SwitchBot の IFTTT 連携は Hub シリーズのみ対応(Hub Mini・Hub 2 は対応、独立したセンサー系は不可)
  • IFTTT サービス障害時は動作しない(クラウド依存が2重になる)

※本記事の手順・コードは執筆時点(2026 年 4 月)で動作確認していますが、ライブラリやハードのバージョンが変わるとそのままでは動かない場合があります。動かない場合は コメント欄でお知らせください。

まとめ

IFTTT × SwitchBot で、GPS 位置情報をトリガーにした「帰宅前エアコン自動 ON」が手軽に作れます。SwitchBot アプリ単体では対応できない外部サービス連携が必要な場合に選ぶ方法です。ただし無料プランの2アプレット制限と応答遅延は事前に把握しておくことをおすすめします。

この記事が役に立ったら X(Twitter)でシェアしてもらえると喜びます。

参考

このシリーズの記事