Pan-Tilt Playground: Part 2 - Servo driver creation

In this tutorial (2nd part of the series) we'll program the first movements of the Pan-Tilt. In addition, we will rewrite the provided Python driver for the servo motors. The original drivers basically work, but is not really intended for these 300 degree servos. That sounds a bit difficult, but it's not!

 

Note: The first tutorial explains step by step how to build the pan-tilt yourself.

STEP 1
Objective

You will learn to rewrite provided Python driver and to control the pan-tilt using very basic Python code. Together we'll also create the basis for all other parts of this series.

HARDWARE LIST
1 Unihiker M10
1 micro:Driver - Driver Expansion Board
2 2Kg 300° Clutch Servos
STEP 2
The first movements

Create a new Python file and add following code.

CODE
from sys import exit
from time import sleep
from pinpong.board import Board
from pinpong.libs.microbit_motor import DFServo


PAN_PIN: int = 2
PAN_MAX_ANGLE: int = 100
PAN_START_ANGLE: int = PAN_MAX_ANGLE // 2
TILT_PIN: int = 3
TILT_MAX_ANGLE: int = 60
TILT_START_ANGLE: int = TILT_MAX_ANGLE // 2
RESET_POSITION: int = 0

ROUNDS: int = 5
DELAY: float = .5


if __name__ == '__main__':
    Board("UNIHIKER").begin()

    pan = DFServo(PAN_PIN)
    tilt = DFServo(TILT_PIN)

    try:
        for i in range(ROUNDS):
            print(f"[INFO] Round {i + 1}...")
            print("[INFO] Move to starting position...")
            pan.angle(PAN_START_ANGLE)
            tilt.angle(TILT_START_ANGLE)
            sleep(DELAY)

            print("[INFO] Move to max position...")
            pan.angle(PAN_MAX_ANGLE)
            tilt.angle(TILT_MAX_ANGLE)
            sleep(DELAY)
    except KeyboardInterrupt:
        print("[INFO] KeyboardInterrupt triggered...")
    finally:
        print('[INFO] Move to reset position...')
        pan.angle(RESET_POSITION)
        tilt.angle(RESET_POSITION)
        sleep(DELAY)

        exit(0)

Explanation of Python code

 

This Python script controls two servo motors (a pan and tilt mechanism) using a UNIHIKER board and the pinpong library.

 

The first four lines import required Python modules/packages.

 

- sys.exit: Used to gracefully exit the script.
- time.sleep: Adds delays between movements.
- pinpong.board.Board: Manages the connection to the UNIHIKER board.
- pinpong.libs.microbit_motor.DFServo: Controls servo motors (via Expansion Board).

 

Then 9 constants are defined. This helps to improve the code readability, scalability and maintainability, makes later modifications super easy and prevents magic numbers in the code.

 

- PAN_PIN and TILT_PIN: GPIO pin numbers controlling the pan and tilt servos.
- PAN_MAX_ANGLE and TILT_MAX_ANGLE: Maximum allowed angles for movement.
- PAN_START_ANGLE and TILT_START_ANGLE: Initial position.
- RESET_POSITION: Position to reset after execution.
- ROUNDS and DELAY: Defines the number of movement cycles and delay between them.

 

The rest of the code is the initialization and the actual control of the servos. Since Unihiker is used, the board object is created. Then one object each for the servos. A few movements are carried out in the for-loop. In addition, If [Ctrl + c] is pressed (KeyboardInterrupt), it catches the signal. The Finally block ensures the servos move back to a reset position before exiting.

The tiny problem

 

As you have noticed, the degrees are not 100% correct. This is because the Python driver was created for 180 degree servos. This means that the value for Pulse Width Range is always calculated incorrectly. Here is the code provided in the def angle(self, degree): method.

CODE
  def angle(self, degree):
    if degree >= 180:
      degree = 180
    if degree < 0:
      degree = 0
    v_us = degree * 10 + 600
    value = v_us * 4095 // (1000000 // 50)
    self.set_pwm(self.servo + 7, 0, value)

But this problem can be solved very easily!

STEP 3
Prepare the project

Now create a project on your computer (PC/Laptop) with a few folders and files.

 

Note: This project will be used and developed further in all further tutorials in this series.

CODE
# create and change into project folder
$ mkdir Pan-Tilt && cd Pan-Tilt

# create folder for modules
$ mkdir libs

# create empty servo driver file
$ touch libs/servo.py

# create empty main file
$ touch main.py

If you're done the structure should look like this:

CODE
# run tree command to show folders and files (optional)
$ tree Pan-Tilt/
Pan-Tilt/
|-- libs
|   `-- servo.py
`-- main.py

1 directory, 2 files

So let's start with the file: libs/servo.py. We inherit from the DFServo class and simply override the angle() method. We also add other methods that will simplify coding later.

 

I think the DocStrings and Type-Hints explain the code very clearly.

CODE
from typing import Optional
from pinpong.libs.microbit_motor import DFServo


class DFServo300(DFServo):
    """
    This class provides functionality to control a DFRobt 300° servo.

    :ivar _MIN_ANGLE: The minimum angular position that can be set.
    :ivar _MAX_ANGLE: The maximum angular position that can be set.
    :ivar _MIN_PULSE_WIDTH: The minimum pulse width in microseconds for the servo.
    :ivar _MAX_PULSE_WIDTH: The maximum pulse width in microseconds for the servo.
    :ivar _FREQUENCY: Frequency of the PWM signal in Hz.
    :ivar _MAX_DUTY: Maximum duty cycle value used in PWM generation.
    """
    _MIN_ANGLE: int = 1
    _MAX_ANGLE: int = 299
    _MIN_PULSE_WIDTH: int = 500
    _MAX_PULSE_WIDTH: int = 2500
    _FREQUENCY: int = 50
    _MAX_DUTY: int = 4095

    def __init__(self, pin: int):
        """
        Initialize the object with a specific GPIO pin in order to control the servo motor.

        :param pin: The GPIO pin associated with servo motor.
        :type pin: int
        """
        super().__init__(pin)
        self.__pulse_width_range = self._MAX_PULSE_WIDTH - self._MIN_PULSE_WIDTH
        self.__degree = None

    def _set_pwm(self, value: int) -> None:
        """
        Sets the Pulse Width Modulation (PWM) value for a servo.

        :param value: The PWM value for the servo.
        :type value: int
        :return: None
        """
        self.set_pwm(self.servo + 7, 0, value)

    def reset(self) -> None:
        """
        Resets the servo to the angle 1 degree.

        :return: None
        """
        self.angle(self._MIN_ANGLE)

    def angle(self, degree: int) -> None:
        """
        Set the servo motor to a specific angle (from 1 to 299 degrees).

        :param degree: Angle (in degrees) to set for the servo.
        :type degree: int
        :return: None
        """
        self.__degree = max(self._MIN_ANGLE, min(self._MAX_ANGLE, degree))

        pulse_width_us = self._MIN_PULSE_WIDTH + (self.__degree * self.__pulse_width_range // self._MAX_ANGLE)
        pwm_value = pulse_width_us * self._MAX_DUTY // (1000000 // self._FREQUENCY)

        self._set_pwm(value=pwm_value)

    def get_angle(self) -> Optional[int]:
        """
        Retrieves current angle value (in degrees) for servo motor.

        :return: The current angle value or None if the angle is not defined.
        :rtype: Optional[int]
        """
        return self.__degree

In the next step we create the code for main.py. This code is similar to the structure of the first example and I think you will find your way around very quickly. 

 

The only important difference is that we now import our own driver.

 

"from libs.servo import DFServo300" instead of "from pinpong.libs.microbit_motor import DFServo"

CODE
from sys import exit
from time import sleep
from pinpong.board import Board
from libs.servo import DFServo300


PAN_PIN: int = 2
PAN_END_ANGLE: int = 180
PAN_START_ANGLE: int = PAN_END_ANGLE // 2
TILT_PIN: int = 3
TILT_END_ANGLE: int = 90
TILT_START_ANGLE: int = TILT_END_ANGLE // 2

ROUNDS: int = 5
DELAY: float = .5


if __name__ == '__main__':
    Board("UNIHIKER").begin()

    pan = DFServo300(PAN_PIN)
    tilt = DFServo300(TILT_PIN)

    try:
        for i in range(ROUNDS):
            print(f"[INFO] Round {i + 1}...")
            print("[INFO] Move to starting position...")
            pan.angle(PAN_START_ANGLE)
            tilt.angle(TILT_START_ANGLE)
            print(f"[INFO] Moved Pan: {pan.get_angle()} / Tilt: {tilt.get_angle()} degrees.")
            sleep(DELAY)

            print("[INFO] Move to max position...")
            pan.angle(PAN_END_ANGLE)
            tilt.angle(TILT_END_ANGLE)
            print(f"[INFO] Moved Pan: {pan.get_angle()} / Tilt: {tilt.get_angle()} degrees.")
            sleep(DELAY)

    except KeyboardInterrupt:
        print("[INFO] KeyboardInterrupt triggered...")
    finally:
        print('[INFO] Move to reset position...')
        pan.reset()
        tilt.reset()
        sleep(DELAY)
        print(f"[INFO] Moved Pan: {pan.get_angle()} / Tilt: {tilt.get_angle()} degrees.")

        exit(0)

Important: Before you upload and run the new code, you should loosen the screw on the tilt and realign it after execution!

 

STEP 4
Annotation

If you have successfully completed all steps so far, you already have a very good basis for all further tutorials in this series. But play around and try it yourself!

License
All Rights
Reserved
licensBg
0