.. include:: /index.rst
:start-after: start_hello_message
:end-before: end_hello_message
.. _py_servo_angle_meter:
4.11 サーボ角度メーター
==============================
**はじめに**
このレッスンでは、 **サーボ角度メーター** を作成します。
これは、ポテンショメータでサーボモーターの角度を制御し、その現在の角度を OLED 画面に表示するビジュアルインジケーターです。
ポテンショメータは Fusion HAT+ の ADC インターフェースを通じてアナログ電圧を出力します。
サーボはこの読み取り値に基づいて角度を制御され、128×64 の I2C OLED ディスプレイには数値によるサーボ角度と、画面上を滑らかに移動するグラフィカルなバーが表示されます。
ポテンショメータを回すと、サーボはおよそ -90° から +90° の範囲で動作し、OLED の表示もリアルタイムで更新されます。
----------------------------------------------
**必要なもの**
このプロジェクトで必要なコンポーネントは以下のとおりです。
.. list-table::
:widths: 30 20
:header-rows: 1
* - COMPONENT INTRODUCTION
- PURCHASE LINK
* - :ref:`cpn_wires`
- |link_wires_buy|
* - :ref:`cpn_potentiometer`
- |link_potentiometer_buy|
* - :ref:`cpn_servo`
- |link_servo_buy|
* - :ref:`cpn_oled`
- \-
* - :ref:`cpn_fusion_hat`
- \-
* - Raspberry Pi
- \-
.. ----------------------------------------------
.. **Circuit Diagram**
.. .. image:: img/fzz/4.11_servo_oled_sch.png
.. :width: 80%
.. :align: center
----------------------------------------------
**配線図**
以下の配線図を参考にして、各コンポーネントを接続してください。
.. image:: img/fzz/4.11_servo_angle_meter_bb.png
:width: 100%
:align: center
----------------------------------------------
**セットアップ手順**
#. 必要なライブラリをインストールします。
.. raw:: html
.. code-block:: shell
sudo pip3 install adafruit-circuitpython-ssd1306 --break
#. このチュートリアルで使用するすべてのサンプルコードは ``ai-lab-kit`` ディレクトリにあります。
.. raw:: html
.. code-block:: shell
cd ~/ai-lab-kit/python/
sudo python3 4.11_ServoAngleMeter.py
#. スクリプトを実行すると次のように動作します。
* ポテンショメータを回すと、サーボが -90° から +90° の範囲で回転します。
* OLED には数値の角度と、移動するバー状のポインタが表示されます。
* Ctrl+C を押すとプログラムが終了し、サーボは 0° に戻り、OLED 画面はクリアされます。
----------------------------------------------
**コード**
以下は Servo Angle Meter の Python スクリプトです。
.. raw:: html
.. code-block:: python
from fusion_hat.adc import ADC
from fusion_hat.servo import Servo
from PIL import Image, ImageDraw, ImageFont
import adafruit_ssd1306
import board, time
# ==== OLED setup ====
WIDTH, HEIGHT = 128, 64
i2c = board.I2C()
oled = adafruit_ssd1306.SSD1306_I2C(WIDTH, HEIGHT, i2c, addr=0x3C)
oled.fill(0)
oled.show()
# Framebuffer for drawing
image = Image.new("1", (WIDTH, HEIGHT))
draw = ImageDraw.Draw(image)
font = ImageFont.load_default()
def text_size(font, text):
l, t, r, b = font.getbbox(text)
return (r - l, b - t)
# ==== Servo & potentiometer ====
servo = Servo('P0') # servo on port P0
pot = ADC('A0') # potentiometer on A0 (0..4095)
def linear_map(x, in_min, in_max, out_min, out_max):
"""Map x from one range to another."""
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
# ---- bar layout ----
BAR_TOP = 40
BAR_HEIGHT = 10
BAR_MARGINX = 6
BAR_WIDTH = WIDTH - BAR_MARGINX * 2
BAR_CENTERX = BAR_MARGINX + BAR_WIDTH // 2
def draw_bar(angle_deg):
"""Draw a centered horizontal bar and pointer for -90..90 degrees."""
draw.rectangle((0, 0, WIDTH, HEIGHT), outline=0, fill=0)
# Title
title = "Servo Angle"
tw, th = text_size(font, title)
draw.text(((WIDTH - tw) // 2, 4), title, font=font, fill=255)
# Numeric angle
txt = f"{angle_deg:>4} deg"
nw, nh = text_size(font, txt)
draw.text(((WIDTH - nw) // 2, 20), txt, font=font, fill=255)
# Bar outline
draw.rectangle(
(BAR_MARGINX, BAR_TOP, BAR_MARGINX + BAR_WIDTH - 1, BAR_TOP + BAR_HEIGHT),
outline=255, fill=0
)
# Ticks
for x in (BAR_MARGINX, BAR_CENTERX, BAR_MARGINX + BAR_WIDTH - 1):
draw.line((x, BAR_TOP - 3, x, BAR_TOP + BAR_HEIGHT + 3), fill=255)
# Map angle to pixel position
pos = int(linear_map(angle_deg, -90, 90, BAR_MARGINX, BAR_MARGINX + BAR_WIDTH - 1))
draw.line((pos, BAR_TOP - 2, pos, BAR_TOP + BAR_HEIGHT + 2), fill=255)
# Fill direction highlight
if pos >= BAR_CENTERX:
draw.rectangle((BAR_CENTERX, BAR_TOP + 1, pos, BAR_TOP + BAR_HEIGHT - 1), fill=255)
else:
draw.rectangle((pos, BAR_TOP + 1, BAR_CENTERX, BAR_TOP + BAR_HEIGHT - 1), fill=255)
try:
while True:
raw = pot.read()
angle = int(linear_map(raw, 0, 4095, -90, 90))
servo.angle(angle)
draw_bar(angle)
oled.image(image)
oled.show()
time.sleep(0.05)
except KeyboardInterrupt:
servo.angle(0)
oled.fill(0)
oled.show()
print("\nExited.")
----------------------------------------------
**コードの解説**
1. **Imports**
- ``ADC`` はポテンショメータからアナログ値を読み取ります
- ``Servo`` はサーボモーターの回転を制御します
- ``PIL`` は OLED に表示するグラフィックを描画します
- ``adafruit_ssd1306`` は I2C OLED ディスプレイを制御します
- ``board`` はハードウェア I/O を提供します
- ``time`` はループ速度を制御します
2. **OLED Setup**
128×64 の SSD1306 OLED を初期化してクリアします。
描画はオフスクリーンのフレームバッファに作成され、その後ディスプレイに転送されます。
.. code-block:: python
# ==== OLED setup ====
WIDTH, HEIGHT = 128, 64
i2c = board.I2C()
oled = adafruit_ssd1306.SSD1306_I2C(WIDTH, HEIGHT, i2c, addr=0x3C)
oled.fill(0)
oled.show()
# Framebuffer for drawing
image = Image.new("1", (WIDTH, HEIGHT))
draw = ImageDraw.Draw(image)
font = ImageFont.load_default()
3. **Servo & Potentiometer**
- サーボは ``P0`` ポートに接続
- ポテンショメータはアナログ入力 ``A0`` に接続
- ADC の範囲は ``0..4095``
.. code-block:: python
# ==== Servo & potentiometer ====
servo = Servo('P0') # servo on port P0
pot = ADC('A0') # potentiometer on A0 (0..4095)
4. **Mapping Values**
``linear_map()`` 関数は、ポテンショメータの読み取り値を ``-90..90`` のサーボ角度に変換します。
.. code-block:: python
def linear_map(x, in_min, in_max, out_min, out_max):
"""Map x from one range to another."""
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
5. **Drawing the UI**
``draw_bar()`` 関数は次の処理を行います。
* 画面をクリア
* タイトルを描画
* 数値の角度を表示
* 横方向のバーと目盛りを描画
* ポインタと方向を示す塗りつぶしバーを描画
.. code-block:: python
def draw_bar(angle_deg):
"""
Draw a centered horizontal bar with a moving pointer.
-90° maps to the far left, +90° to the far right.
0° is at the bar center.
"""
# Clear screen
draw.rectangle((0, 0, WIDTH, HEIGHT), outline=0, fill=0)
# Title
title = "Servo Angle"
tw, th = text_size(font, title)
draw.text(((WIDTH - tw) // 2, 4), title, font=font, fill=255)
# Numeric angle
txt = f"{angle_deg:>4} deg"
nw, nh = text_size(font, txt)
draw.text(((WIDTH - nw) // 2, 20), txt, font=font, fill=255)
# Static bar background
draw.rectangle(
(BAR_MARGINX, BAR_TOP, BAR_MARGINX + BAR_WIDTH - 1, BAR_TOP + BAR_HEIGHT),
outline=255, fill=0
)
# Ticks: left (-90), center (0), right (+90)
for x in (BAR_MARGINX, BAR_CENTERX, BAR_MARGINX + BAR_WIDTH - 1):
draw.line((x, BAR_TOP - 3, x, BAR_TOP + BAR_HEIGHT + 3), fill=255)
# Map angle (-90..90) to bar position
pos = int(linear_map(angle_deg, -90, 90, BAR_MARGINX, BAR_MARGINX + BAR_WIDTH - 1))
# Pointer: a solid vertical line
draw.line((pos, BAR_TOP - 2, pos, BAR_TOP + BAR_HEIGHT + 2), fill=255)
# Optional: filled segment from center to pointer (visualize direction)
if pos >= BAR_CENTERX:
draw.rectangle((BAR_CENTERX, BAR_TOP + 1, pos, BAR_TOP + BAR_HEIGHT - 1), outline=0, fill=255)
else:
draw.rectangle((pos, BAR_TOP + 1, BAR_CENTERX, BAR_TOP + BAR_HEIGHT - 1), outline=0, fill=255)
6. **Main Loop**
プログラムは次の処理を繰り返します。
* ADC を読み取る
* サーボ角度を計算する
* サーボを更新する
* UI を描画する
* OLED を更新する
.. code-block:: python
while True:
# Read potentiometer (0..4095) and map to angle (-90..90)
raw = pot.read()
angle = int(linear_map(raw, 0, 4095, -90, 90))
# Drive servo
servo.angle(angle)
# Draw UI and push to OLED
draw_bar(angle)
oled.image(image)
oled.show()
# Optional: print for debugging
# print(f"pot={raw:4d} -> angle={angle:4d} deg")
time.sleep(0.05) # ~20 FPS
7. **Graceful Exit**
Ctrl+C を押すと次の処理が行われます。
- サーボが 0° に戻る
- OLED 画面がクリアされる
----------------------------------------------
**トラブルシューティング**
- **OLED に何も表示されない**
- I2C 配線を確認してください
- デバイスアドレスが ``0x3C`` であることを確認してください
- 必要なライブラリがインストールされていることを確認してください
- **サーボが動作しない**
- サーボの電源を確認してください
- サーボが ``P0`` に接続されていることを確認してください
- サーボの信号線が正しく接続されていることを確認してください
- **動作範囲が正しくない**
以下のマッピング範囲を調整してください。
.. code-block:: python
angle = int(linear_map(raw, 0, 4095, -90, 90))
- **OLED がちらつく**
遅延時間を増やします。
.. code-block:: python
time.sleep(0.1)
----------------------------------------------
**試してみよう**
1. **サーボ角度制限を追加する**
機械的な過回転を防ぎます。
2. **キャリブレーションを追加する**
ポテンショメータの最小値・最大値を自動検出します。
3. **動きを滑らかにする**
イージングやローパスフィルタを適用します。
4. **表示情報を追加する**
角度とともに ADC の生データも表示します。
5. **警告表示を追加する**
角度が ±75° 付近に達したときポインタを点滅させます。
これらの拡張を行うことで、Servo Angle Meter はより高機能な入力可視化ツールへと発展させることができます。