昔のMac miniはスリープ状態がインジケータランプで判別できました
2014 年以前に発売されたモデルの Mac では、スリープ状態の間はステータスインジケータランプがゆっくりと点滅します
しかし最近のMac miniはスリープ状態が判別できません1
M4 Mac mini本体裏の電源ボタンが話題になりました。熟練MacユーザやAppleからのメッセージとして 「Macは電源OFFせずスリープ運用せよ」 がありますが、スリープ運用するならMac mini本体LEDでスリープ状態が分かるようになっていて欲しいです2
Macbookは分かりやすい
MacBookは「画面を閉じた状態」がスリープ状態を物理的に示しており、
閉じればスリープ、開ければスリープ解除で非常に明確です。
Mac mini運用においてもこのような明確さが欲しい
M4 -Mac miniを1ヶ月使って気になったこと
- キーボードでスリープ状態にするとMac miniがスリープから即復帰することがある
- 電源ボタンによるスリープ移行は安定するが本体裏で不便
- スリープ中もUSB電源供給されるのでUSB機器構成によってはかなりの待機電力になる
- ThunderboltのKVM切替時にMac miniがスリープ復帰する
→KVMを別PCに切替えており「スリープ復帰してること」に気づく手段がない
欲しい機能
- アクティブ状態、スリープ状態がLEDで分かる
- スイッチOFFでスリープ状態にする
- スイッチONでスリープ状態から復帰させる
- スイッチOFFの場合はスリープ状態から勝手に復帰しても再度スリープさせる
一番最後の要件はMac miniに悪い影響を及ぼす可能性もあると思います
実際の動作の様子
マイコンはRP2040、言語はCircuitPythonを採用
当初はQMKのsuspend_power_down_userとsuspend_wakeup_init_userを使ってPC側のスリープ、スリープ復帰の処理を作っていましたが、QMKはPCスリープ中にマイコン側の処理を行うことが困難なため最終的にCircuitPythonを採用しました。
python言語はよく分からないので今回はCopilotとClaudeにプログラミングを丸投げしました。IDEはThonny。AIが生成したコードをコピペして即動作できるThonnyはとても便利でした3
code.py
import time
import board
import digitalio
import neopixel
import usb_hid
import supervisor
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
# 定数の宣言
NUM_PIXELS = 1 # LEDの数
PIN_NUM = board.GP23 # LEDが接続されているピン番号
ACTIVE_BRIGHTNESS = 0.05 # PC起動中の明るさ
SLEEP_BRIGHTNESS = 0.02 # スリープ中の明るさ
DIMMED_BRIGHTNESS = 0.01 # ディムモードの明るさ
# 呼吸エフェクトの設定
BREATHING_STEPS = 200 # 呼吸エフェクトのステップ数
BREATHING_INTERVAL = 4.0 # 呼吸エフェクトの間隔(秒)
BREATHING_DARK_INTERVAL = 5.0 # 呼吸エフェクトの暗い状態の間隔(秒)
BREATHING_CYCLE_LIMIT = 3 # 呼吸エフェクトのサイクル制限
# 状態遷移の設定
TRANSITION_DURATION = 5.0 # 状態遷移の期間(秒)
SLEEP_GUARD_DURATION = 60 # スリープガードの期間(秒)
# デバイスの初期化
kbd = Keyboard(usb_hid.devices) # キーボードデバイスの初期化
pixels = neopixel.NeoPixel(PIN_NUM, NUM_PIXELS, auto_write=False) # NeoPixel LEDの初期化
switch_pin = digitalio.DigitalInOut(board.GP6) # スイッチピンの設定
switch_pin.direction = digitalio.Direction.INPUT # スイッチピンを入力モードに設定
switch_pin.pull = digitalio.Pull.UP # スイッチピンにプルアップ抵抗を設定
# グローバル変数
last_sleep_shortcut_time = 0 # 最後にスリープショートカットが押された時間
previous_switch_state = True # 前回のスイッチ状態
# LED制御関数
def pixels_color(r, g, b, brightness):
pixels.fill((int(r * brightness), int(g * brightness), int(b * brightness)))
pixels.show()
def gradient_effect(start_color, end_color, steps, duration):
r1, g1, b1 = start_color
r2, g2, b2 = end_color
step_delay = duration / steps
for i in range(steps):
r = r1 + (r2 - r1) * i // steps
g = g1 + (g2 - g1) * i // steps
b = b1 + (b2 - b1) * i // steps
pixels_color(r, g, b, ACTIVE_BRIGHTNESS)
time.sleep(step_delay)
def flash_led(is_sleep, duration, steps):
start_color = (0, 255, 0) if is_sleep else (255, 0, 0)
end_color = (255, 0, 0) if is_sleep else (0, 255, 0)
gradient_effect(start_color, end_color, steps, duration)
# 呼吸エフェクト関数
def execute_steps(num_steps, step_delay, callback, sleep=True):
for i in range(num_steps):
callback(i)
if switch_pin.value:
print("[DEBUG] Switch state changed during steps.")
return
if sleep:
time.sleep(step_delay)
def breathing_effect(r, g, b, brightness, interval, dark_interval):
steps = BREATHING_STEPS
step_delay = interval / (steps * 2)
# 上昇フェーズ
execute_steps(steps, step_delay,
lambda i: pixels_color(r, g, b, (i * brightness) / steps),
sleep=False)
# 下降フェーズ
execute_steps(steps, step_delay,
lambda i: pixels_color(r, g, b, ((steps - i) * brightness) / steps),
sleep=False)
# 消灯時間
execute_steps(int(dark_interval / step_delay), step_delay, lambda _: None)
# キーボード制御関数
def send_key_press(*keys):
print(f"[DEBUG] Sending keys: {keys}")
pixels_color(255, 0, 0, ACTIVE_BRIGHTNESS)
kbd.press(*keys)
kbd.release_all()
def check_switch_state():
global previous_switch_state, last_sleep_shortcut_time
current_time = time.monotonic()
if not switch_pin.value and previous_switch_state:
send_key_press(Keycode.ALT, Keycode.COMMAND, Keycode.POWER)
previous_switch_state = False
last_sleep_shortcut_time = current_time
flash_led(True, TRANSITION_DURATION, 50)
return True
if switch_pin.value and not previous_switch_state:
send_key_press(Keycode.ALT)
previous_switch_state = True
flash_led(False, TRANSITION_DURATION, 50)
return True
return False
def update_led_state(is_active):
if is_active:
if switch_pin.value:
pixels_color(0, 255, 0, ACTIVE_BRIGHTNESS) # 緑
else:
pixels_color(255, 0, 0, ACTIVE_BRIGHTNESS) # 赤
def perform_breathing_effect():
breathing_cycle_count = 0
while not switch_pin.value and breathing_cycle_count < BREATHING_CYCLE_LIMIT:
current_brightness = SLEEP_BRIGHTNESS if breathing_cycle_count < BREATHING_CYCLE_LIMIT else DIMMED_BRIGHTNESS
breathing_effect(255, 255, 255, current_brightness, BREATHING_INTERVAL, BREATHING_DARK_INTERVAL)
breathing_cycle_count += 1
def handle_switch_and_led():
global last_sleep_shortcut_time
is_active = supervisor.runtime.usb_connected
if not check_switch_state():
update_led_state(is_active)
if is_active:
current_time = time.monotonic()
if (current_time - last_sleep_shortcut_time > SLEEP_GUARD_DURATION and
not switch_pin.value):
time.sleep(TRANSITION_DURATION)
send_key_press(Keycode.ALT, Keycode.COMMAND, Keycode.POWER)
last_sleep_shortcut_time = current_time
flash_led(True, TRANSITION_DURATION, 50)
else:
perform_breathing_effect()
def handle_error(e):
print(f"[ERROR] An error occurred: {type(e).__name__}: {str(e)}")
pixels_color(0, 0, 0, ACTIVE_BRIGHTNESS)
time.sleep(1)
def main():
global previous_switch_state
previous_switch_state = True
while True:
try:
handle_switch_and_led()
except Exception as e:
handle_error(e)
time.sleep(0.1)
if __name__ == "__main__":
main()
もちろん上記のコードが一発でAIから出力された訳ではありません。
AIでプログラムした手順
最初の依頼文章
RP2040互換ボードを使いCircuitPythonでPCのスリープ状態を検出し、
LEDを光らせるプログラムを書いてください。
pythonのプログラムが生成されるのでThonnyに貼り付けて実行する。
エラーが出たらAIにエラー内容をコピペで丸投げすると修正してくれるので、
再度Thonnyに貼り付けて実行、これを繰り返して動くものを作る。
今回のプログラム程度なら全コードをコピペでAIに伝えることが出来るので、
import time
import board
...(中略)...
if __name__ == "__main__":
main()
----------
上記はRP2040ボードをCircuitPythonで作ったプログラムです。
スリープショートカットを送信する際にLEDを緑色から赤色にグラデーションで変化させてください
のような指示を繰り返して要件のすべてを自然言語指示でプログラム完成させました。
Claudeは賢いのですが無料だと利用制限があるので、大枠をCopilotにコーディングしてもらって、詰まった場合はClaudeに聞くやり方で進めました。人間に対してやったら嫌がられそうですね。
上記CircuitPythonプログラムに必要なライブラリ
adafruit_hidライブラリ
「Code」→「Download ZIP」で得られたZIP内の「adafruit_hid」フォルダを、
RP2040デバイス「CIRCUITPY」の「lib」フォルダにコピーする。
neopixelライブラリ
「Code」→「Download ZIP」で得られたZIP内の「neopixel.py」ファイルを、
RP2040デバイス「CIRCUITPY」の「lib」フォルダにコピーする。
今回使ったRP2040ボード
- YD-RP2040
Aliexpressなどで安く買えるUSB-CのRP2040ボード。
フルカラーLEDのWS2812が表面実装