.. include:: /index.rst
:start-after: start_hello_message
:end-before: end_hello_message
.. _py_servo_angle_meter:
4.11 Servo-Winkelmesser
==============================
**Einführung**
In dieser Lektion bauen Sie einen **Servo-Winkelmesser** – eine visuelle Servo-Winkelanzeige, die ein Potentiometer verwendet, um einen Servomotor zu steuern, während der aktuelle Winkel auf einem OLED-Bildschirm angezeigt wird.
Das Potentiometer liefert eine analoge Spannung über die ADC-Schnittstelle des Fusion HAT+.
Der Servo erhält auf Grundlage dieses Messwerts Steuerbefehle, und ein 128×64-I2C-OLED-Bildschirm zeigt den numerischen Servowinkel sowie einen grafischen Balken an, der sich flüssig über das Display bewegt.
Wenn Sie das Potentiometer drehen, bewegt sich der Servo ungefähr zwischen -90° und +90°, und das OLED wird in Echtzeit aktualisiert.
----------------------------------------------
**Was Sie benötigen**
Für dieses Projekt werden die folgenden Komponenten benötigt:
.. list-table::
:widths: 30 20
:header-rows: 1
* - KOMPONENTENBESCHREIBUNG
- KAUFLINK
* - :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
- \-
.. ----------------------------------------------
.. **Schaltplan**
.. .. image:: img/fzz/4.11_servo_oled_sch.png
.. :width: 80%
.. :align: center
----------------------------------------------
**Verdrahtungsdiagramm**
Verwenden Sie das folgende Verdrahtungsdiagramm, um die Komponenten korrekt zu verbinden:
.. image:: img/fzz/4.11_servo_angle_meter_bb.png
:width: 100%
:align: center
----------------------------------------------
**Einrichtungsschritte**
#. Installieren Sie die erforderlichen Bibliotheken:
.. raw:: html
.. code-block:: shell
sudo pip3 install adafruit-circuitpython-ssd1306 --break
#. Der gesamte in diesem Tutorial verwendete Beispielcode befindet sich im Verzeichnis ``ai-lab-kit``:
.. raw:: html
.. code-block:: shell
cd ~/ai-lab-kit/python/
sudo python3 4.11_ServoAngleMeter.py
#. Wenn das Skript ausgeführt wird:
* Durch Drehen des Potentiometers bewegt sich der Servo zwischen -90° und +90°.
* Das OLED zeigt den numerischen Winkel und einen beweglichen Balkenzeiger an.
* Mit Ctrl+C wird das Programm beendet, der Servo auf 0° zurückgesetzt und das Display gelöscht.
----------------------------------------------
**Code**
Hier ist das Python-Skript für den Servo-Winkelmesser:
.. 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.")
----------------------------------------------
**Understanding the Code**
1. **Imports**
- ``ADC`` reads analog values from the potentiometer
- ``Servo`` controls servo rotation
- ``PIL`` handles all OLED graphics
- ``adafruit_ssd1306`` drives the I2C OLED display
- ``board`` provides hardware I/O
- ``time`` controls loop speed
2. **OLED Setup**
A 128×64 SSD1306 OLED is initialized and cleared.
An off-screen framebuffer holds the graphics for each frame before being pushed to the display.
.. 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**
- Servo connected to port ``P0``
- Potentiometer connected to analog input ``A0``
- ADC range: ``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()`` converts the potentiometer reading into a servo angle in the range ``-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**
The ``draw_bar()`` function:
* Clears the display
* Draws the title
* Shows numeric angle
* Draws a horizontal bar and tick marks
* Draws a pointer and filled segment indicating the angle direction
.. 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. **Hauptschleife**
Das Skript führt wiederholt folgende Schritte aus:
* Es liest den ADC-Wert.
* Es berechnet den Servowinkel.
* Es aktualisiert den Servo.
* Es zeichnet die aktualisierte Benutzeroberfläche.
* Es aktualisiert das OLED-Display.
.. 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. **Sauberes Beenden**
Mit ``Ctrl+C``:
- kehrt der Servo auf 0° zurück
- wird das OLED-Display gelöscht
----------------------------------------------
**Fehlerbehebung**
- **OLED zeigt nichts an**
- Überprüfen Sie die I2C-Verdrahtung.
- Stellen Sie sicher, dass die Geräteadresse ``0x3C`` lautet.
- Vergewissern Sie sich, dass die erforderlichen Bibliotheken installiert sind.
- **Servo reagiert nicht**
- Überprüfen Sie die Stromversorgung des Servos.
- Stellen Sie sicher, dass der Servo mit ``P0`` verbunden ist.
- Prüfen Sie, ob das Signalkabel des Servos korrekt angeschlossen ist.
- **Bewegungsbereich ist falsch**
Passen Sie Folgendes an:
.. code-block:: python
angle = int(linear_map(raw, 0, 4095, -90, 90))
- **OLED flackert**
Erhöhen Sie die Verzögerung:
.. code-block:: python
time.sleep(0.1)
----------------------------------------------
**Probieren Sie es selbst aus**
1. **Servowinkelbegrenzung hinzufügen**
Verhindern Sie eine mechanische Übersteuerung.
2. **Kalibrierung hinzufügen**
Ermitteln Sie die minimalen und maximalen Potentiometerwerte dynamisch.
3. **Sanftere Bewegung**
Wenden Sie Easing oder Tiefpassfilterung an.
4. **Mehr Anzeigeinformationen**
Zeigen Sie zusätzlich zum Winkel auch den rohen ADC-Wert an.
5. **Warnhinweise**
Lassen Sie den Zeiger in der Nähe der Grenzwerte (±75°) blinken.
Diese Erweiterungen machen den Servo-Winkelmesser zu einem leistungsfähigen Werkzeug zur Visualisierung von Eingaben.