Note

Hello, welcome to the SunFounder Raspberry Pi & Arduino & ESP32 Enthusiasts Community on Facebook! Dive deeper into Raspberry Pi, Arduino, and ESP32 with fellow enthusiasts.

Why Join?

  • Expert Support: Solve post-sale issues and technical challenges with help from our community and team.

  • Learn & Share: Exchange tips and tutorials to enhance your skills.

  • Exclusive Previews: Get early access to new product announcements and sneak peeks.

  • Special Discounts: Enjoy exclusive discounts on our newest products.

  • Festive Promotions and Giveaways: Take part in giveaways and holiday promotions.

👉 Ready to explore and create with us? Click [here] and join today!

4.11 Servo Angle Meter

Introduction

In this lesson, you will build a Servo Angle Meter — a visual servo angle indicator that uses a potentiometer to control a servo motor, while displaying the current angle on an OLED screen. The potentiometer provides an analog voltage through the Fusion HAT+’s ADC interface. The servo receives commands based on this reading, and a 128×64 I2C OLED screen shows the numeric servo angle and a graphical bar that moves smoothly across the display.

As you rotate the potentiometer, the servo sweeps between approximately -90° and +90°, and the OLED updates in real time.


What You’ll Need

The following components are required for this project:

COMPONENT INTRODUCTION

PURCHASE LINK

Jumper Wires

BUY

Potentiometer

BUY

Servo

BUY

OLED Display Module

-

Fusion HAT+

-

Raspberry Pi

-


Wiring Diagram

Refer to the wiring diagram for assembling the components:

../_images/4.11_servo_angle_meter_bb.png

Setup Steps

  1. Install the required libraries:

    sudo pip3 install adafruit-circuitpython-ssd1306 --break
    
  2. All example code used in this tutorial is available in the ai-lab-kit directory:

    cd ~/ai-lab-kit/python/
    sudo python3 4.11_ServoAngleMeter.py
    
  3. When the script runs:

    • Rotating the potentiometer moves the servo between -90° and +90°.

    • The OLED shows the numeric angle and a moving bar pointer.

    • Ctrl+C exits, resets servo to 0°, and clears the display.


Code

Here is the Python script for the Servo Angle Meter:

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.

    # ==== 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

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

    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

    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

    The script repeatedly:

    • Reads the ADC

    • Computes servo angle

    • Updates servo

    • Draws updated UI

    • Refreshes OLED

    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:

    • Servo returns to 0°

    • OLED is cleared


Troubleshooting

  • OLED shows nothing

    • Check I2C wiring

    • Verify device address is 0x3C

    • Ensure required libraries are installed

  • Servo does not respond

    • Check servo power

    • Confirm servo is connected to P0

    • Ensure servo signal wire is correct

  • Movement range incorrect

    Adjust:

    angle = int(linear_map(raw, 0, 4095, -90, 90))
    
  • OLED flickers

    Increase delay:

    time.sleep(0.1)
    

Try It Yourself

  1. Add servo angle limits

    Prevent mechanical overdrive.

  2. Add calibration

    Detect min/max potentiometer values dynamically.

  3. Smooth motion

    Apply easing or low-pass filtering.

  4. More display info

    Show raw ADC value alongside angle.

  5. Warnings

    Blink pointer near limits (±75°).

These additions turn the Servo Angle Meter into a highly capable input-visualization tool.