In this tutorial, you'll learn how to set up a Python Tkinter application on the UNIHIKER device that interacts with GR10-30 gesture sensor. The sensor detects gestures and changes the displayed images accordingly. You'll walk through the necessary hardware, assembly, code creation, and deployment steps.
UNIHIKER Device - A single board computer compatible with Python.
Gesture Sensor - An I2C-enabled gesture sensor, specifically the DFRobot GR10-30.
Connecting Wires - To connect the sensor to the UNIHIKER device.
Connect the Gesture Sensor to UNIHIKER. Make sure that you only turn on the power supply after the connection! Double-check the connections for secure and correct placement.
Create a directory for your project named “ImageViewer”. Inside this directory create two more directories named “lib” and “demo”. The directory “lib” will contain a file named "GR10_30.py". Inside the directory “demo” all pictures will be stored. In addition, a file called “main.py” is required in the project folder.
Here is an example of what everything will look like in the end:
The file "GR10_30.py" serves as a driver for the sensor. I modified this for you from the original (Raspberry) for UNIHIKER. I was a bit lazy and the Python DocStrings are missing. But that shouldn't be a problem.
from pinpong.board import I2C
from time import sleep
GR10_30_I2C_ADDR = 0x73
GR30_10_INPUT_REG_ADDR = 0x02
GR30_10_INPUT_REG_DATA_READY = 0x06
GR30_10_INPUT_REG_INTERRUPT_STATE = 0x07
GR30_10_INPUT_REG_EXIST_STATE = 0x08
GR30_10_HOLDING_REG_INTERRUPT_MODE = 0x09
GR30_10_HOLDING_REG_RESET = 0x18
GESTURE_UP = (1 << 0)
GESTURE_DOWN = (1 << 1)
GESTURE_LEFT = (1 << 2)
GESTURE_RIGHT = (1 << 3)
GESTURE_FORWARD = (1 << 4)
GESTURE_BACKWARD = (1 << 5)
GESTURE_CLOCKWISE = (1 << 6)
GESTURE_COUNTERCLOCKWISE = (1 << 7)
GESTURE_WAVE = (1 << 8)
GESTURE_HOVER = (1 << 9)
GESTURE_UNKNOWN = (1 << 10)
GESTURE_CLOCKWISE_C = (1 << 14)
GESTURE_COUNTERCLOCKWISE_C = (1 << 15)
class DFRobot_GR10_30_I2C:
def __init__(self, i2c_addr=GR10_30_I2C_ADDR, i2c_bus=0):
self._addr = i2c_addr
self._temp_buffer = [0] * 2
try:
self._i2c = I2C(i2c_bus)
except Exception as err:
print(f'Could not initialize i2c! bus: {i2c_bus}, error: {err}')
def _write_reg(self, reg, data) -> None:
if isinstance(data, int):
data = [data]
try:
self._i2c.writeto_mem(self._addr, reg, bytearray(data))
except Exception as err:
print(f'Write issue: {err}')
def _read_reg(self, reg, length) -> bytes:
try:
result = self._i2c.readfrom_mem(self._addr, reg, length)
except Exception as err:
print(f'Read issue: {err}')
result = [0, 0]
return result
def _detect_device_address(self) -> int:
r_buf = self._read_reg(GR30_10_INPUT_REG_ADDR, 2)
data = r_buf[0] << 8 | r_buf[1]
return data
def _reset_sensor(self) -> None:
self._temp_buffer[0] = 0x55
self._temp_buffer[1] = 0x00
self._write_reg(GR30_10_HOLDING_REG_RESET, self._temp_buffer)
sleep(0.1)
def begin(self) -> bool:
if self._detect_device_address() != GR10_30_I2C_ADDR:
return False
self._reset_sensor()
sleep(0.5)
return True
def en_gestures(self, gestures) -> None:
gestures = gestures & 0xc7ff
self._temp_buffer[0] = (gestures >> 8) & 0xC7
self._temp_buffer[1] = gestures & 0x00ff
self._write_reg(GR30_10_HOLDING_REG_INTERRUPT_MODE, self._temp_buffer)
sleep(0.1)
def get_exist(self) -> bool:
r_buf = self._read_reg(GR30_10_INPUT_REG_EXIST_STATE, 2)
data = r_buf[0] * 256 + r_buf[1]
return bool(data)
def get_data_ready(self) -> bool:
r_buf = self._read_reg(GR30_10_INPUT_REG_DATA_READY, 2)
data = r_buf[0] * 256 + r_buf[1]
if data == 0x01:
return True
else:
return False
def get_gestures(self) -> int:
r_buf = self._read_reg(GR30_10_INPUT_REG_INTERRUPT_STATE, 2)
data = (r_buf[0] & 0xff) * 256 + r_buf[1]
return int(data)
The application logic is located in the “main.py” file. All necessary packages/libraries are imported here, constants are defined, functions are created, etc. Here I was a little more diligent and created the Python DocStrings. These should help you understand the code better.
from os import listdir
from os.path import dirname, join
from tkinter import Tk, BOTH, Frame, Label, PhotoImage
from typing import List, Tuple
from PIL import Image, ImageTk
from time import sleep
from sys import exit
from threading import Thread
from pinpong.board import Board
from lib.GR10_30 import DFRobot_GR10_30_I2C, GESTURE_LEFT, GESTURE_RIGHT, GESTURE_UP, GESTURE_DOWN
SCREEN_WIDTH: int = 240
SCREEN_HEIGHT: int = 320
FONT: tuple = ("Arial", 14, "bold")
IMAGE_PATH: str = 'demo/'
IMAGE_EXTENSIONS: tuple = ('.png', '.jpg', '.jpeg')
def load_image(image_path: str, image_size: Tuple[int, int]) -> ImageTk.PhotoImage:
"""
Load an image from the specified path and resize it to the given size.
:param image_path: The path to the image file.
:type image_path: str
:param image_size: The desired size of the image as a tuple of width and height.
:type image_size: Tuple[int, int]
:return: The loaded and resized image as an `ImageTk.PhotoImage` object.
:rtype: ImageTk.PhotoImage
"""
image = Image.open(image_path)
image = image.resize(image_size)
return ImageTk.PhotoImage(image)
def load_all_images(image_dir: str, image_size: Tuple[int, int]) -> List[ImageTk.PhotoImage]:
"""
Load all the images in the specified directory.
:param image_dir: The directory where the images are located.
:type image_dir: str
:param image_size: The desired size of the images.
:type image_size: Tuple[int, int]
:return: A list of loaded images as `ImageTk.PhotoImage`.
:rtype: List[ImageTk.PhotoImage]
"""
filenames = [join(image_dir, f) for f in listdir(image_dir) if f.endswith(IMAGE_EXTENSIONS)]
return [load_image(filename, image_size) for filename in filenames]
def update_label(label: Label, image_list: List[PhotoImage], index: int) -> None:
"""
Updates the image and text displayed in a tkinter Label widget.
:param label: The tkinter Label widget to update.
:type label: Label
:param image_list: A list of PhotoImage objects representing the images to cycle through.
:type image_list: List[ImageTk.PhotoImage]
:param index: The index of the image in the image_list to display in the label.
:type index: int
:return: None
"""
total_images = len(image_list)
if 0 <= index < total_images:
message = f'Image: {index + 1} of {total_images}'
print(f'[INFO] Display {message.lower()}')
label.config(image=image_list[index], text=message, font=FONT, fg='white')
label.image = image_list[index]
def handle_gestures(sensor: DFRobot_GR10_30_I2C, image_list: List[PhotoImage], label: Label) -> None:
"""
Handles gestures detected by the given sensor.
:param sensor: The instance of the DFRobot_GR10_30_I2C sensor.
:type sensor: DFRobot_GR10_30_I2C
:param image_list: A list of PhotoImage objects representing the images to cycle through.
:type image_list: List[ImageTk.PhotoImage]
:param label: The tkinter Label widget to update.
:type label: Label
:return: None
"""
image_index = 0
total_images = len(image_list)
while True:
if gesture_sensor.get_data_ready():
gesture = sensor.get_gestures()
if gesture & GESTURE_LEFT:
print(f'[INFO] Gesture: {gesture} left detected')
if image_index < total_images:
image_index += 1
if gesture & GESTURE_RIGHT:
print(f'[INFO] Gesture: {gesture} right detected')
if image_index > 0:
image_index -= 1
if gesture & GESTURE_UP:
print(f'[INFO] Gesture: {gesture} up detected')
image_index = 0
if gesture & GESTURE_DOWN:
print(f'[INFO] Gesture: {gesture} down detected')
image_index = total_images - 1
update_label(label, images, image_index)
sleep(.05)
if __name__ == '__main__':
Board("UNIHIKER").begin()
gesture_sensor = DFRobot_GR10_30_I2C()
while not gesture_sensor.begin():
print('[INFO] Initializing gesture sensor...')
sleep(1)
gesture_sensor.en_gestures(GESTURE_LEFT | GESTURE_RIGHT | GESTURE_UP | GESTURE_DOWN)
root = Tk()
root.resizable(False, False)
root.geometry(f'{SCREEN_WIDTH}x{SCREEN_HEIGHT}+0+0')
frame = Frame(root, height=SCREEN_HEIGHT, width=SCREEN_WIDTH, bg='black')
frame.pack(fill=BOTH, expand=True)
img_label = Label(frame, text='', font=FONT, fg='white', compound='center')
img_label.pack(fill=BOTH, expand=True)
current_dir = dirname(__file__)
target_path = join(current_dir, IMAGE_PATH)
images = load_all_images(join(dirname(__file__), IMAGE_PATH), (SCREEN_WIDTH, SCREEN_HEIGHT))
if not images:
print(f'[INFO] No images found: {target_path}')
exit(1)
else:
print(f'[INFO] Found {len(images)} images')
update_label(img_label, images, 0)
gesture_thread = Thread(target=handle_gestures, args=(gesture_sensor, images, img_label))
gesture_thread.daemon = True
gesture_thread.start()
root.mainloop()
Ensure UNIHIKER is Connected: Connect the UNIHIKER device to your local environment via USB. To send the project files to the UNIHIKER board, you can use tools like SCP or SMB. Here's an example using SCP:
$ scp -r ImageViewer [email protected]:/root/
You can start the application via the touch display or via command line. Here the example via command-line.
$ python3 /root/ImageViewer/main.py
You can now change the images with hand gestures. Left, right, up and down...
Gesture Customization: Enhance the application by adding more gestures or customizing actions for existing gestures.
User Interface: Improve the Tkinter UI with additional features.
Performance Optimization: Optimize image loading and memory usage for better performance.
Interactive Features: Integrate interactive features such as touch input or additional sensors for a more engaging experience.
Feel free to modify and expand upon this tutorial based on your project's needs and creativity!