.. include:: /index.rst
:start-after: start_hello_message
:end-before: end_hello_message
.. _py_hue_knob:
4.10 Hue Knob
==============================
**はじめに**
このレッスンでは、 **Hue Knob** (色相ノブ)を作成します。これは、ロータリーエンコーダーを使用して円形 WS2812 LED モジュールの色相(Hue)を調整できるインタラクティブなカラーコントローラーです。
この WS2812 LED モジュールには、12 個の個別に制御可能な WS2812 RGB LED が搭載されており、Fusion HAT+ の SPI ベース NeoPixel インターフェースを通して制御されます。
外部のロータリーエンコーダーは、標準の GPIO ピンを介してリアルタイムのユーザー入力を提供します。
エンコーダーを回転させると、12 個の LED が RGB カラースペクトル全体を滑らかに循環します。
エンコーダーの内蔵ボタンを押すと、色相が初期値にリセットされます。
----------------------------------------------
**必要なもの**
このプロジェクトで必要なコンポーネントは以下のとおりです。
.. list-table::
:widths: 30 20
:header-rows: 1
* - COMPONENT INTRODUCTION
- PURCHASE LINK
* - :ref:`cpn_wires`
- |link_wires_buy|
* - :ref:`cpn_rotary_encoder`
- |link_rotary_encoder_buy|
* - :ref:`cpn_circular_ws2812_module`
- \-
* - :ref:`cpn_fusion_hat`
- \-
* - Raspberry Pi
- \-
----------------------------------------------
**配線図**
コンポーネントの組み立ては、以下の配線図を参照してください。
.. image:: img/fzz/4.10_hub_color_bb.png
:width: 80%
:align: center
----------------------------------------------
**セットアップ手順**
#. コードを実行する前に、必要なライブラリをインストールします。
このライブラリは、SPI 通信を使用して NeoPixel LED を制御するための機能を提供します。
.. raw:: html
.. code-block:: shell
sudo pip3 install adafruit-circuitpython-neopixel-spi --break
#. このチュートリアルで使用するすべてのサンプルコードは ``ai-lab-kit`` ディレクトリに含まれています。以下の手順でサンプルを実行してください。
.. raw:: html
.. code-block:: shell
cd ~/ai-lab-kit/python/
sudo python3 4.10_Hue_Knob.py
#. スクリプトを実行すると、WS2812 LED リングがロータリーエンコーダーに応答します。
* LED は赤色(Hue 0°)で開始します。
* エンコーダーを回すと RGB カラーホイール全体を滑らかに循環します(赤 → 黄 → 緑 → 青 → 紫 → 赤)。端末には現在の Hue と RGB 値が表示されます。
* エンコーダーのボタンを押すと Hue が 0°(赤)にリセットされます。
* Ctrl+C を押すと終了し、プログラム終了前にすべての LED が消灯します。
----------------------------------------------
**コード**
以下は、このプロジェクトで使用する Python スクリプトです。
.. raw:: html
.. code-block:: python
#!/usr/bin/env python3
from fusion_hat.motor import Motor
from fusion_hat.pin import Pin, Mode, Pull
from fusion_hat.adc import ADC
from time import sleep, time
import math
BtnPin = Pin(22, mode=Mode.IN, pull=Pull.DOWN)
motor = Motor("M0")
thermistor = ADC("A3")
level = 0
currentTemp = None
markTemp = None
PRINT_INTERVAL = 1.0
_last_print = 0.0
button_event = False # flag: button was pressed
def temperature(samples=5, delay=0.01):
"""Read thermistor multiple times and return averaged Celsius (float) or None."""
vals = []
for _ in range(samples):
analogVal = thermistor.read()
Vr = 3.3 * float(analogVal) / 4095.0
if (3.3 - Vr) <= 0.1:
return None
Rt = 10000.0 * Vr / (3.3 - Vr)
tempK = 1.0 / (((math.log(Rt / 10000.0)) / 3950.0) + (1.0 / (273.15 + 25.0)))
vals.append(tempK - 273.15)
sleep(delay)
return sum(vals) / len(vals)
def motor_run(lv):
lv = max(0, min(4, lv))
motor.power(0 if lv == 0 else lv * 25)
return lv
def changeLevel():
"""Button press: cycle level 0~4 and set a flag for main loop to print."""
global level, button_event
level = (level + 1) % 5
button_event = True
BtnPin.when_activated = changeLevel
def main():
global level, currentTemp, markTemp, _last_print, button_event
markTemp = temperature()
while True:
currentTemp = temperature()
if currentTemp is None:
print("Sensor read failed. Please check the sensor.")
sleep(0.5)
continue
# Handle button event in main loop (stable timing)
if button_event:
button_event = False
markTemp = currentTemp
print(f"[Button] Level -> {level} | Temp: {currentTemp:.2f} °C | Mark: {markTemp:.2f} °C")
# Periodic temperature print
now = time()
if now - _last_print >= PRINT_INTERVAL:
if markTemp is None:
markTemp = currentTemp
print(f"Temp: {currentTemp:.2f} °C | Mark: {markTemp:.2f} °C | Level: {level}")
_last_print = now
# Auto adjust level based on ±5°C
if markTemp is None:
markTemp = currentTemp
if level != 0:
diff = currentTemp - markTemp
if diff <= -5:
level = max(0, level - 1)
markTemp = currentTemp
print(f"[Auto] Temp down -> Level {level} (Temp: {currentTemp:.2f} °C)")
elif diff >= 5:
level = min(4, level + 1)
markTemp = currentTemp
print(f"[Auto] Temp up -> Level {level} (Temp: {currentTemp:.2f} °C)")
level = motor_run(level)
sleep(0.5)
try:
main()
except KeyboardInterrupt:
print("\nExiting...")
finally:
motor.stop()
sleep(0.1)
-------------------------
**コードの解説**
1. **NeoPixel の初期化**
- スクリプトは SPI を使用して NeoPixel LED リング/ストリップ( ``LED_COUNT = 12`` )を制御します。
- ``auto_write=False`` を設定しているため、LED の更新は ``strip.show()`` が呼ばれたときのみ実行されます。これによりちらつきを防ぎ、パフォーマンスが向上します。
- 起動時には ``strip.fill(0)`` と ``strip.show()`` によって LED をすべて消灯します。
2. **ロータリーエンコーダーのハードウェア設定**
- 3 本の GPIO ピンを使用します:
- ``CLK_PIN`` と ``DT_PIN`` は回転方向とステップを検出するためのクアドラチャ信号です。
- ``SW_PIN`` はエンコーダーボタン入力です。
- すべてのピンは内部プルアップ( ``Pull.UP`` )を使用し、ボタンは **アクティブ LOW** (押されたとき ``0`` )です。
3. **主要パラメータ(動作調整)**
- ``DETENTS_PER_CYCLE`` は、色相を 0~360° 一周させるのに必要な物理クリック数を定義します。
- 値を大きくすると、より細かい色調整が可能になります。
- ``TRANSITIONS_PER_DETENT`` は、生のクアドラチャ信号を 1 クリックとして換算するための係数です。
- 多くのエンコーダーは 1 クリックにつき 2 つの遷移を出力しますが、4 つ出力するものもあります。この値を調整すると精度が向上します。
4. ``hue_to_rgb()``
- ``0.0 ~ 1.0`` の範囲の色相値を RGB タプル ``(R, G, B)`` ( ``0 ~ 255`` )に変換します。
- HSV カラーモデルを利用して滑らかな色変化を実現します。
5. ``apply_color_from_detent()``
- エンコーダーのクリック数を色相インデックスに変換します:
- ``hue_idx = detent % DETENTS_PER_CYCLE``
- 色相を RGB に変換し、 ``strip.fill(color)`` と ``strip.show()`` で LED を更新します。
- ``last_hue_idx`` を使用して、色が変化していない場合の不要な更新を防ぎます。
- デバッグ用に、現在の色相角度、クリック値、RGB カラーを端末へ出力します。
6. ``reset_all()``
- 内部カウンターをリセットします:
- ``raw`` (生の遷移カウント)
- ``last_detent`` (最後に表示されたクリック値)
- ``last_hue_idx`` (最後に表示された色相インデックス)
- ``apply_color_from_detent(0)`` を呼び出し、LED を初期色(Hue = 0°)に戻します。
7. **メインループ(ポーリング + 回転方向検出)**
- プログラムは ``CLK`` を継続的にポーリングして変化を監視します。
- ``CLK`` が変化した場合、エンコーダーがクアドラチャシーケンス上で 1 ステップ移動したことを意味します。
- 回転方向は DT と CLK を比較して判定します。
- ``dt.value() != c`` の場合は一方の回転方向(増加)
- それ以外の場合は減少
- 生の遷移カウントはクリック数に変換されます。
- ``detent = raw // TRANSITIONS_PER_DETENT``
- LED の色は、クリック値が変化したときのみ更新されます。
8. **ボタンリセットとデバウンス**
- ボタンが押された場合( ``sw.value() == 0`` )、 ``reset_all()`` が呼び出されます。
- 短いデバウンス遅延を入れ、ボタンが離されるまで待機することで、1 回の押下で複数回リセットされるのを防ぎます。
9. **安全な終了**
- ``Ctrl + C`` を押すとプログラムが終了します。
- ``finally`` ブロックで ``strip.fill(0)`` と ``strip.show()`` を実行し、すべての LED を消灯して安全な状態で終了します。
**トラブルシューティング**
----------------------------------
- **LED が点灯しない**
- WS2812 LED モジュールの配線を確認してください。
- Fusion HAT+ の SPI NeoPixel インターフェースが有効になっているか確認してください。
- 対応している WS2812 / WS2812B LED モジュールを使用していることを確認してください。
- **色の変化が速すぎる/遅すぎる**
- ``STEPS_PER_CYCLE`` を調整して感度を変更してください。
- **ボタンを押しても色相がリセットされない**
- SW ピンが GPIO27 に接続されていることを確認してください。
- ピン設定が ``pull=Pin.PULL_UP`` になっているか確認してください。
- **スクリプトがすぐ終了する**
- ``pause()`` が ``signal`` から正しくインポートされているか確認してください。
- SPI を使用している別のプロセスが動作していないか確認してください。
**試してみよう**
-------------------
このプロジェクトをさらに拡張してみましょう。例えば次のようなアイデアがあります。
1. **明るさ調整の追加**
別の変数(例:エンコーダーの押し込み+回転)を使い、LED の明るさを 0~255 で調整できるようにします。
2. **複数の表示モードを追加**
エンコーダーを押すことでモードを切り替えます。
- 単色表示(デフォルト)
- レインボーアニメーション
- ブリージング(呼吸)エフェクト
- カラー ワイプ
3. **電源トグル機能**
エンコーダーボタンを長押しすると LED リングを ON/OFF できるようにします。
4. **色変化をより滑らかにする**
``STEPS_PER_CYCLE`` を増やすか、補間処理を追加してより滑らかな色遷移を実現します。
5. **回転方向フィードバック**
時計回りに回すと LED を緑、反時計回りでは赤に点灯させるなどのフィードバックを追加します。
これらの小さな拡張により、シンプルな「Hue Knob」を多用途な RGB コントローラーへ発展させることができます。