.. 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.