In this tutorial I would like to show you how easily and quickly you can create a radar interface. At the end of these instructions you should be able to expand them yourself and use them for your DIY projects.
Connecting the hardware is super easy! Plug the UNIHIKER board into the micro:Driver expansion board (Gravity standard interface). Next, connect the ultrasonic sensor via I2C with UNIHIKER board. Then connect the servo motor to the micro:Driver expansion board (for example to pin S8). Finally you have to attach the ultrasonic sensor to the servo motor. I used some cardboard and velcro. It doesn't look very nice, but it was quick and lasted.
Now we start with the Python code. In order to ensure better maintainability and reusability, we create two Python scripts. It is recommended to create the UI as a Python module (for example as a class). In your development project you should create a “lib” folder and create a “radar.py” file in it (eq. YOUR-PROJECT/lib/radar.py).
from tkinter import Tk, Canvas, BOTH
from math import cos, sin, radians
class Radar:
"""
Radar class to represent a Radar UI via a Tk interface
"""
_DISTANCE: str = "cm"
_FONT: str = "Helvetica"
_COLORS: dict = {
'radar': 'white',
'background': 'black',
'point': 'yellow',
'line': 'lawn green',
'line_text': 'cyan'
}
_ARC_STEPS: int = 3
_SONAR_OBJECTS: dict = {}
def __init__(self, screen_width: int, screen_height: int) -> None:
"""
Radar UI interface constructor
:param screen_width: width of the screen
:param screen_height: height of the screen
"""
self._screen_width = int(screen_width)
self._screen_height = int(screen_height)
self._center_x = self._screen_width // 2
self._center_y = self._screen_height - 50
self._line_width = None
self._angle_start = None
self._angle_end = None
self._max_radius = None
self._arc_distance = None
self.screen = Tk()
self.screen.geometry(f'{self._screen_width}x{self._screen_height}+0+0')
self.screen.resizable(width=False, height=False)
self.canvas = Canvas(self.screen, bg=self._COLORS['background'])
self.canvas.pack(expand=True, fill=BOTH)
def configure(self, line_width: int, max_radius: int, arc_distance: int, start_angle: int, end_angle: int) -> None:
"""
Configure the radar
:param line_width: line width of the graphics
:param max_radius: arc max radius
:param arc_distance: arc distance in pixel
:param start_angle: start angle in degrees for arcs
:param end_angle: end angle in degrees for arcs
:return: None
"""
self._line_width = int(line_width)
self._angle_start = int(start_angle)
self._angle_end = int(end_angle)
self._max_radius = int(max_radius)
self._arc_distance = int(arc_distance)
def _draw_line(self, angle: int, color: str) -> None:
"""
Draw line with given angle and color on interface
:param angle: angle in degrees
:return: None
"""
in_radian = radians(int(angle))
x1 = self._center_x
y1 = self._center_y
x2 = x1 + self._max_radius * cos(in_radian)
y2 = y1 - self._max_radius * sin(in_radian)
self.canvas.create_line(x1, y1, x2, y2, width=self._line_width, fill=str(color))
def _draw_text(self, x: int, y: int, text: str, color: str, font_size: int = 20) -> None:
"""
Draw given text with values on interface
:param x: x position as integer
:param y: y position as integer
:param text: string with current line angle
:param color: color of the text as string
:param font_size: optional font size (default: 20)
:return: None
"""
x_pos = int(x)
y_pos = int(y)
font = (self._FONT, int(font_size))
self.canvas.create_text(x_pos, y_pos, text=str(text), font=font, fill=color)
def _draw_background(self, show_measurement: bool = True) -> None:
"""
Draw radar background graphic on interface
:param show_measurement: whether to show the measurement or not
:return: None
"""
color = self._COLORS['radar']
angle_total = self._angle_start + self._angle_end
x1 = self._center_x - self._max_radius
y1 = self._center_y - self._max_radius
x2 = self._center_x + self._max_radius
y2 = self._center_y + self._max_radius
for _ in range(self._ARC_STEPS):
self.canvas.create_arc(x1, y1, x2, y2,
start=self._angle_start,
extent=self._angle_end,
width=self._line_width,
outline=color)
x1 += self._arc_distance
y1 += self._arc_distance
x2 -= self._arc_distance
y2 -= self._arc_distance
for value in range(0, 360, 45):
if self._angle_start <= value <= angle_total:
self._draw_line(angle=value, color=color)
radius = self._max_radius
if bool(show_measurement):
for _ in range(self._ARC_STEPS):
text_start_x = self._center_x + radius * cos(radians(self._angle_start))
text_start_y = self._center_y - radius * sin(radians(self._angle_start))
text_end_x = self._center_x + radius * cos(radians(angle_total))
text_end_y = self._center_y - radius * sin(radians(angle_total))
if self._angle_start == 0:
text_start_x -= 25
text_start_y += 10
if angle_total == 180:
text_end_x += 25
text_end_y += 10
if self._angle_start <= 90:
text_start_x += 25
if angle_total >= 90:
text_end_x -= 25
self._draw_text(x=text_start_x,
y=text_start_y,
text=f'{radius}{self._DISTANCE}',
color=color,
font_size=10)
self._draw_text(x=text_end_x,
y=text_end_y,
text=f'{radius}{self._DISTANCE}',
color=color,
font_size=10)
radius -= self._arc_distance
def _draw_point(self, distance: int, angle: int) -> None:
"""
Draw point on radar with given distance and angle on interface
:param distance: distance in centimeters
:param angle: angle in degrees
:return: None
"""
color = self._COLORS['point']
in_radian = radians(int(angle))
x = self._center_x + int(distance) * cos(in_radian)
y = self._center_y - int(distance) * sin(in_radian)
self.canvas.create_oval(x - 5, y - 5, x + 5, y + 5, fill=color)
def update(self, distance: int, angle: int) -> None:
"""
Update the radar with given distance and angle
:param distance: distance in centimeters
:param angle: angle in radian
:return: None
"""
current_distance = int(distance)
current_angle = int(angle)
self._SONAR_OBJECTS[angle] = current_distance
end = self._angle_start + self._angle_end
self.canvas.delete("all")
self._draw_background(show_measurement=False)
self._draw_text(x=self._center_x,
y=self._center_y + 15,
text=f'{current_angle}°',
color=self._COLORS['line_text'])
for key, value in self._SONAR_OBJECTS.items():
if 1 <= value <= self._max_radius and self._angle_start <= key <= end:
self._draw_point(distance=value, angle=key)
if self._angle_start <= current_angle <= end:
self._draw_line(angle=current_angle, color=self._COLORS['line'])
Directly in the project folder you create another file “main.py” (eq. YOUR-PROJECT/main.py). Here all other libraries are actually just imported, the servo motor is controlled and the ultrasonic sensor is read out.
from time import sleep
from pinpong.board import Board
from pinpong.libs.dfrobot_urm09 import URM09
from pinpong.libs.microbit_motor import DFServo
from lib.radar import Radar
SCREEN_WIDTH: int = 240
SCREEN_HEIGHT: int = 320
ARC_LINE_WIDTH: int = 1
ARC_MAX_RADIUS: int = 150
ARC_DISTANCE: int = 50
ARC_START: int = 45
ARC_EXTENT: int = 90
SERVO_PIN: int = 8
DELAY_SECONDS: float = .25
def generate_numbers(minimum: int, maximum: int, step: int):
"""
Generates numbers between minimum and maximum by step
:param minimum: minimum value
:param maximum: maximum value
:param step: step value
"""
g_minimum = int(minimum)
g_maximum = int(maximum)
g_step = int(step)
number = g_minimum
direction = 1
while True:
yield number
number += g_step * direction
if number > g_maximum:
direction = -1
number = g_maximum
elif number < g_minimum:
direction = 1
number = g_minimum
if __name__ == '__main__':
Board("UNIHIKER").begin()
print('Init servo motor')
servo = DFServo(SERVO_PIN)
servo.angle(45)
sleep(.5)
print('Init ultrasonic sensor')
sensor = URM09()
sensor.set_mode_range(sensor._MEASURE_MODE_AUTOMATIC, sensor._MEASURE_RANG_150)
print('Init angle generator')
generator = generate_numbers(minimum=45, maximum=135, step=1)
print('Init radar UI')
display = Radar(screen_width=SCREEN_WIDTH, screen_height=SCREEN_HEIGHT)
display.configure(line_width=ARC_LINE_WIDTH,
max_radius=ARC_MAX_RADIUS,
arc_distance=ARC_DISTANCE,
start_angle=ARC_START,
end_angle=ARC_EXTENT)
while True:
servo_angle = next(generator)
servo.angle(servo_angle)
sensor_distance = sensor.distance_cm()
display.update(distance=sensor_distance, angle=servo_angle)
display.screen.update()
sleep(DELAY_SECONDS)
Now load all the necessary folders and files onto the UNIHIKER. To do this, the UNIHIKER must be started and accessible via USB or WLAN. I used "SCP" for this and recursively copied all files from my local computer to the UNIHIKER: “$ scp -r ~/Projects/unihiker/Radar [email protected]:/root/Projects/”. Important! The command is just an example. You have to adapt it for yourself. After a successful upload, you can test everything. Here is an example for the terminal: "$ cd Projects/Radar && python3 -B main.py". But you can also use the UNIHIKER UI to start everything!
Annotation
- You can adapt/modify the UI (for example the colors, the arcs or line width)! Take a closer look at the “radar.py” and “main.py” files. There are constants in every file that will help you with this. Also, I tried to comment on everything important or write docstrings for it. - The respective pinpong packages/modules (for servo and sensor) are already preinstalled on the UNIHIKER. - You don't have to use the same servo motor or ultrasonic sensor. DFRobot also offers other devices (depending on your needs). But modify the code for that!