mcpField
M
macbook-desktop-mode

macbook-desktop-mode

github.com/terrylicaその他164 回インストール1 日前に更新
リポジトリ

macbook-desktop-mode について

MacBook Desktop Modeは、MacBookを常時AC接続のデスクトップとして使うための設定ガイドです。スリープ復帰後のUSB切断やバッテリーの充放電を扱い、電源設定、USBハブ、音声デバイス監視デーモンを説明します。

主な機能

  1. 01USBのスリープ復帰時の障害を診断
  2. 02AC接続時のスリープ設定
  3. 03充電上限を80%に設定
  4. 04uhubctlによるUSBポートのリセット
  5. 05音声監視・復旧デーモンの導入

活用シーン

  1. 01MacBookを常設ワークステーションに設定
  2. 02マイクや音声デバイスの切断を調査
  3. 03接続中のバッテリー充放電を抑制
  4. 04USBポートの自動復旧を設定

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/terrylica/cc-skills --skill macbook-desktop-mode

インストール後、Claude Code で/macbook-desktop-modeを入力して読み込まれたか確認します。

同梱ファイルreferences/evolution-log.md

詳細

MacBook デスクトップモード

MacBook を常時 AC 電源に接続したデスクトップワークステーションとして使用するための、包括的な設定ガイドです。相互に関連する 2 つの問題に対処します。スリープ/復帰サイクル中に USB デバイス(特に USB 1.1 オーディオデバイス)が認識されなくなることと、充電サイクルの繰り返しによる不要なバッテリー劣化です。

自己進化型スキル:このスキルは使用を通じて改善されます。手順が間違っている、パラメーターが変わっている、回避策が必要になった場合は、先送りせず、すぐにこのファイルを修正してください。実際に再現できる問題がある場合にのみ更新してください。

このスキルを使用する状況

  • スリープ/復帰後に USB マイクまたはオーディオデバイスが認識されなくなる
  • 電源に接続した Mac でバッテリーが不要に充放電を繰り返している
  • MacBook を常設のデスクトップワークステーションとして設定する
  • 常時 AC 電源で使用するために pmset を設定する
  • uhubctl を使ったソフトウェア制御による USB リセットに対応する、電源付き USB ハブを設定する
  • AudioDeviceMonitor Swift デーモンを強化し、自己修復による復旧を行う

根本原因の診断フレームワーク

修正を適用する前に、具体的な障害モードを診断してください。このフレームワークは、Apple Silicon 搭載 MacBook を常時 AC 電源に接続して使用した実測分析から作成されました。その環境でバスから繰り返し切断されたデバイスは、シリアル番号のないバスパワー駆動の USB 1.1 オーディオクラスマイクでした。これはスリープ/復帰時に最も脆弱な種類の USB 周辺機器であり、したがって、この修正が想定する最悪のケースです。ベンダー ID や製品 ID ではなく、以下の ioreg コマンドでご自身のデバイスを特定してください。ここではどちらも必要ありません。

DarkWake の繰り返し

macOS は頻繁に DarkWake(一部のメンテナンス用の復帰)を実行します。通常、夜間は 15 分ごとです。DarkWake 中は次のようになります。

  • Power Nap、ネットワークのキープアライブ、Siri のために CPU が復帰する
  • USB バスには部分的に電力が供給されますが、デバイスは完全には再認識されません
  • USB 1.1 のフルスピードデバイスにはリンク電源管理(LPM)がなく、正常な復帰をネゴシエートできません
  • このサイクルが複数回続くと、XHCI コントローラーが IO レジストリからデバイスを切り離します

診断コマンド — スリープ/復帰の履歴を確認します。

pmset -g log | grep -E "Sleep |Wake |DarkWake" | tail -30
USB 1.1 デバイスの制限

USB 1.1 デバイス(12 Mbps、USBSpeed = 1)は、スリープ/復帰時に最も脆弱です。

  • リンク電源管理プロトコルがありません
  • USB セレクティブサスペンドのネゴシエーションに対応していません
  • シリアル番号がないことが多く(iSerialNumber = 0)、バスリセット後の再識別が不確実です

診断コマンド — デバイスのプロパティを確認します。

ioreg -r -c IOUSBHostDevice -l | grep -A 20 "DEVICE_NAME"

主なフィールド:USBSpeed(1=フルスピード、2=ハイスピード、3=スーパースピード)、iSerialNumber(0 = シリアル番号なし)、IOPowerManagement.DriverPowerState。

USB ハンドルの競合

Chrome などのアプリケーションは、WebRTC/WebAudio 用の USB ユーザークライアントハンドル(AppleUSBHostDeviceUserClient)を直接保持します。これらの古いハンドルにより、スリープ後の正常な再初期化が妨げられます。

診断コマンド — USB ハンドルを保持しているプロセスを確認します。

ioreg -r -c IOUSBHostDevice -l | grep -B5 "IOUserClientCreator"
バッテリーの微細な充放電

充電上限を設定していない常時 AC 接続の Mac では、バッテリー残量が ML 予測値と実際の充電量の間で変動します。DarkWake のサイクルで電力が消費されるため、充電と放電の微細なサイクルが繰り返されます。

診断コマンド — 1 日の充電範囲を確認します。

ioreg -r -c AppleSmartBattery -l | grep -E "DailyMinSoc|DailyMaxSoc|Temperature|CycleCount"
  • Temperature は 1/100 度単位です(摂氏にするには 100 で割ります)
  • DailyMinSoc/DailyMaxSoc は、今日の充電量の変動範囲を示します

フェーズ 1:デスクトップモードの電源設定

1.1 充電上限を 80% に設定する

システム設定 → バッテリー → 充電の最適化 →「上限を 80% にする」

Apple Silicon では、上限に達すると Mac は AC バイパスモードに入り、電力は充電器からシステムボードに直接供給されます。バッテリーは電気的に切り離され、カレンダー劣化とサイクル劣化の両方がなくなります。

1.2 AC 電源接続時に sleep=0 を設定する

常時 AC 電源に接続するデスクトップでは、システムスリープは利点よりも問題のほうが多くなります。

# Disable system sleep on AC only (battery settings unchanged)
sudo pmset -c sleep 0

# Verify
pmset -g custom | grep -A1 "AC Power" | grep sleep

維持されるもの:

  • ディスプレイのスリープは引き続き機能します(AC 電源接続時は displaysleep=10)
  • バッテリー使用時のスリープ設定は変更されません(外出時は sleep=1)
  • ttyskeepawake=1 も引き続き有効です

解消されるもの:

  • DarkWake のサイクル(USB 切断の根本原因)
  • メンテナンス時のウェイク中に発生するバッテリーの微小な充放電
  • 熱サイクル(冷却と加熱の反復)

コスト: Apple Silicon でのアイドル時消費電力は約 5~8W。ファンは回転しません。電気代は年間約 5~8 ドルです。

1.3 電源設定を確認する
# Full power settings
pmset -g custom

# Battery health snapshot
ioreg -r -c AppleSmartBattery -l | grep -E "Temperature|CycleCount|DailyMinSoc|DailyMaxSoc|MaxCapacity|IsCharging"

# Active power assertions
pmset -g assertions

フェーズ 2:ハードウェア層 — セルフパワー USB ハブ

2.1 セルフパワー USB ハブを使う理由

セルフパワー USB ハブは USB セッションの境界 を作ります。Mac の XHCI コントローラーは、ハブ(シリアル番号を持つ堅牢な USB 2.0 以降のデバイス)とのセッションを維持します。ハブは接続されたデバイスとのセッションを独立して維持します。スリープ/ウェイクで影響を受けるのは、Mac とハブ間の接続だけです。

さらに、ハブを使うと uhubctl による ソフトウェア制御の USB ポートリセット が可能になります。これは、ハードウェアに触れずに物理的な抜き差しと同等の操作を行うものです。

2.2 ハブの選定基準

要件:

  • 外部電源で動作すること(バスパワーではない)— 専用の電源アダプターが必要です
  • uhubctl 対応のチップセット — ポートごとの電源切り替えに対応していること
  • シリアル番号を備えた USB 2.0 以降のハブ — 確実に再識別するため

対応チップセット(uhubctl プロジェクトより):

  • VIA VL805/VL812/VL817
  • Realtek RTS5411
  • Genesys Logic GL850G/GL3510
2.3 uhubctl のセットアップ
# Install
brew install uhubctl

# List compatible hubs (must have powered hub connected)
uhubctl

# Cycle all ports (2-second off period)
uhubctl -a cycle -d 2

# Cycle specific port on specific hub
uhubctl -a cycle -p 1 -l 1-1 -d 2

フェーズ 3:ソフトウェア層 — AudioDeviceMonitor の機能強化

リファレンス実装は、macOS の launchd KeepAlive エージェントとしてデプロイされる Swift デーモン(AudioDeviceMonitorRunner.swift)です。次の機能を組み合わせています。

  1. 優先順位の適用(元の機能)— 優先度が最も高い利用可能なデバイスをデフォルトの入力/出力に設定します
  2. デバイスガーディアン(v2)— 状態マシンによる追跡、消失検出、段階的な復旧
  3. ウェイク検出 — IORegisterForSystemPower による IOKit 電源通知
  4. ハートビート — サイレントな切断を検出する 60 秒ごとの定期チェック
  5. 段階的な復旧 — uhubctl によるポートの電源サイクル → 再試行 → Telegram 通知
3.1 状態マシン
                    ┌──────────┐
          ┌────────▶│ PRESENT  │◀──────────────┐
          │         └────┬─────┘               │
          │              │ device gone          │
          │              ▼                      │
          │     ┌──────────────┐               │
          │     │ DISAPPEARED  │               │
          │     │  (3s debounce)│               │
          │     └──────┬───────┘               │
          │            │ still missing          │
          │            ▼                        │
          │     ┌──────────────┐     found     │
          │     │  RECOVERING  │───────────────┘
          │     │ uhubctl cycle│
          │     └──────┬───────┘
          │            │ max attempts
          │            ▼
          │     ┌──────────────┐
          │     │     DEAD     │
          └─────│  (Telegram)  │
          replug└──────────────┘
3.2 主なアーキテクチャ上の判断
  • NSWorkspace ではなく IOKit の電源コールバック — AppKit への依存がなく、ヘッドレスの launchd デーモンでも動作します
  • IOKit メッセージ定数を手動で定義 — Swift では iokit_common_msg() C マクロをインポートできないため、値は sys_iokit (0xe0000000) | message_code から計算します
  • uhubctl/curl の同期呼び出し — ブロッキング中もメイン RunLoop が CoreAudio コールバックをキューに入れるため問題ありません。イベントは失われず、復旧にかかった時間だけ遅延します
  • curl サブプロセス経由の Telegram — ライブラリへの依存がありません。認証情報は起動時に dotenv ファイルから読み込みます
  • メインキューによるシングルスレッド化 — すべての CoreAudio コールバック、ハートビート、電源通知を .main にディスパッチするため、ロックは不要です
3.3 ビルドとデプロイ

デーモンは、単一の Swift ファイルからコンパイルされる自己完結型バイナリです。

# Compile
swiftc -O -framework CoreAudio -framework IOKit \
    -o /path/to/output/audio-device-monitor \
    AudioDeviceMonitorRunner.swift

# Codesign for launchd (ad-hoc, no Apple Developer account needed)
codesign -s - -f -i com.yourorg.audio-device-monitor /path/to/output/audio-device-monitor

launchd エージェントとしてデプロイします(ユーザーレベル、KeepAlive)。

<?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>Label</key>
    <string>com.yourorg.audio-device-monitor</string>
    <key>ProgramArguments</key>
    <array>
        <string>/path/to/output/audio-device-monitor</string>
    </array>
    <key>KeepAlive</key>
    <true/>
    <key>RunAtLoad</key>
    <true/>
    <key>ProcessType</key>
    <string>Background</string>
    <key>EnvironmentVariables</key>
    <dict>
        <key>HOME</key>
        <string>/Users/yourusername</string>
    </dict>
    <key>StandardOutPath</key>
    <string>/path/to/logs/audio-device-monitor-stdout.log</string>
    <key>StandardErrorPath</key>
    <string>/path/to/logs/audio-device-monitor-stderr.log</string>
</dict>
</plist>

読み込み:launchctl load ~/Library/LaunchAgents/com.yourorg.audio-device-monitor.plist

3.4 設定(Swift ソース内)

優先順位リストと監視対象デバイスは、Swift ソースの先頭に定数として定義します。

  1. inputPriorities / outputPriorities — 優先度の高い順に並べた配列
  2. guardedDevices — 消失監視を行う GuardedDevice(name:, isInput:) の配列
  3. 調整用定数:heartbeatInterval(60s)、disappearDebounce(3s)、maxRecoveryAttempts(3)
  4. 通知用認証情報:dotenv ファイルから読み込みます(パスは loadCredentials() で設定)

変更後は、再コンパイルして launchd エージェントを再起動します。

フェーズ 4:検証と監視

4.1 バッテリーの健全性監視
# Quick battery snapshot
ioreg -r -c AppleSmartBattery -l | grep -E "Temperature|CycleCount|DailyMinSoc|DailyMaxSoc|MaxCapacity"

# Full power state
pmset -g batt
system_profiler SPPowerDataType | grep -E "Cycle|Condition|Maximum|Charging"

充電上限を80%に設定し、電源アダプター接続時に sleep=0 を設定すると、次のようになります。

  • DailyMinSoc と DailyMaxSoc が収束します(変動しません)。
  • 定常状態では Temperature が3500(35°C)未満に保たれます。
  • CycleCount の増加が大幅に緩やかになります。
4.2 USB デバイスの健全性
# Current USB device tree
system_profiler SPUSBDataType

# IOKit details for a specific device
ioreg -r -c IOUSBHostDevice -l | grep -A 25 "DEVICE_NAME"

# Kernel USB assertions (shows which devices prevent sleep)
pmset -g assertions | grep USB
4.3 オーディオモニターのログ
# Tail live logs (adjust path to your log location)
tail -50 /path/to/logs/audio-device-monitor-stderr.log

# Key log patterns to watch for:
# "Starting v2 — device guardian mode"     → v2 features active
# "System woke — checking devices"         → wake detection working
# "ALERT: '<device>' disappeared"          → device dropout detected
# "recovered via uhubctl!"                 → automated recovery succeeded
# "recovery FAILED"                        → manual replug needed

アンチパターン

アンチパターン失敗する理由正しい方法
DarkWakeを減らすために powernap=0 に変更するUSBの安定性と引き換えに、iCloud同期や「探す」などが実行されなくなります。電源アダプター接続時に sleep=0 を設定すると、DarkWakeを完全になくせます。
最適化されたバッテリー充電を無効にするバッテリーが100%まで充電されるため、寿命に悪影響があります。充電上限を明示的に80%に設定します。
USBハンドルを解放するためにChromeを終了するいたちごっこになります。WebRTCアプリならどれでもハンドルを取得できます。電源付きハブでセッションの境界を作ります。
USBの状態を確認するために system_profiler をポーリングするコストの高いサブプロセスが起動し、2〜3秒の遅延が発生します。CoreAudioのプロパティリスナーなら即時に検知できます。
NSWorkspace.didWakeNotification を使用するAppKit/NSApplicationが必要なため、launchdデーモンでは動作しません。IORegisterForSystemPower コールバックを使用します。
デバイスが見つからないときにIOKit経由でUSBをリセットするIOレジストリからすでに切断されたデバイスはリセットできません。uhubctlで物理ポートの電源を入れ直します。

関連項目

実行後の振り返り

このスキルの実行後、タスクを終了する前に振り返ってください。

  1. 現在地を確認します。 — 編集する前に、このSKILL.mdの正規パスを見つけます。
  2. 何が失敗しましたか? — 原因となった指示を修正します。
  3. 予想以上にうまくいったことは何ですか? — 推奨事項として反映します。
  4. 何が実態とずれていますか? — 現状に合わなくなったスクリプト、参照情報、依存関係を修正します。
  5. 記録します。 — 発生のきっかけ、修正内容、根拠を含む進化ログのエントリーを追加します。

先送りしないでください。次回の実行では、あなたが残したものを引き継ぎます。

コメント