.. 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 はより高機能な入力可視化ツールへと発展させることができます。