This tutorial will guide you through building a project that connects a UNIHIKER board to an iPhone via Bluetooth, allowing you to control the volume of a USB speaker connected to UNIHIKER. The control is facilitated by the buttons on the UNIHIKER, which adjust the volume and control media playback on the iPhone.
You'll need the following components:
UNIHIKER: A single board platform with onboard Bluetooth connectivity.
USB Speaker: Any speaker with a USB connection.
iPhone: The mobile device that will be connected to the UNIHIKER via Bluetooth.
Ensure your USB speaker is connected to the UNIHIKER via the USB port.
Before starting, you'll need to ensure your UNIHIKER environment is set up with all the necessary libraries.
python3-dbus: To communicate with the system's D-Bus interface, which is used to manage Bluetooth devices.
You can install the python3-dbus library on your UNIHIKER by running:
```bash
# update package repositories
$ apt update
# install required packages
$ apt install -y python3-dbus
```
Project structure
The root folder is named iOS_Media_Controller. Inside it, there's a subfolder named lib containing the gui.py file. The main.py file is directly under the root folder.
Below is the complete Python code for the project. You will have two Python scripts: main.py and gui.py (in the lib folder).
main.py
from subprocess import run, PIPE, CalledProcessError
from sys import exit
from pinpong.board import Board, Pin
from pinpong.extension.unihiker import button_a, button_b
from lib.gui import GUI
IOS_MAC_ADDRESS: str = ''
DEFAULT_VOLUME: int = 50
SCREEN_WIDTH: int = 240
SCREEN_HEIGHT: int = 320
def get_connected_devices() -> list:
"""
Retrieves a list of connected Bluetooth devices.
:return: list of MAC addresses of the connected Bluetooth devices.
"""
output = None
devices = []
try:
result = run(['bluetoothctl', 'info'], check=True, stdout=PIPE, stderr=PIPE)
output = result.stdout.decode()
except CalledProcessError as err:
print(f'[ERROR] {err}')
return []
if output is not None:
for line in output.splitlines():
if 'Device' in line:
devices.append(line.split(' ')[1])
return devices
def is_device_connected(devices: list) -> bool:
"""
Verify if the iOS device MAC address is currently connected.
:param devices: List of device MAC addresses currently connected.
:type devices: list
:return: True if the iOS device MAC address is found in the provided list of devices, False otherwise.
:rtype: bool
"""
return IOS_MAC_ADDRESS in devices
def set_volume(volume: int) -> None:
"""
Set the volume for connected speakers.
:param volume: The desired volume level as an integer percentage (e.g., 50 for 50%).
:type volume: int
:return: None
"""
try:
run(['amixer', 'set', 'Master', f'{volume}%'], check=True, stdout=PIPE, stderr=PIPE)
except CalledProcessError as err:
print(f'[ERROR] {err}')
def increase_volume(pin: Pin=None) -> None:
"""
Increase the volume by 5%
:param pin: Optional parameter, not used in the function
:type pin: Pin
:return: None
"""
global current_volume
global window
_ = pin
if current_volume < 100:
current_volume += 5
set_volume(current_volume)
window.update_information(volume=current_volume)
def decrease_volume(pin: Pin=None) -> None:
"""
Decrease the volume by 5%
:param pin: Optional parameter, not used in the function
:type pin: Pin
:return: None
"""
global current_volume
global window
_ = pin
if current_volume > 0:
current_volume -= 5
set_volume(current_volume)
window.update_information(volume=current_volume)
if __name__ == '__main__':
connected_devices = get_connected_devices()
if not connected_devices:
exit(1)
elif not is_device_connected(devices=connected_devices):
print(f'[ERROR] {IOS_MAC_ADDRESS} not connected')
exit(1)
else:
print(f'[INFO] {IOS_MAC_ADDRESS} connected')
Board("UNIHIKER").begin()
current_volume = DEFAULT_VOLUME
set_volume(current_volume)
window = GUI(width=SCREEN_WIDTH, height=SCREEN_HEIGHT, mac_address=IOS_MAC_ADDRESS)
window.update_information(volume=current_volume)
button_a.irq(trigger=Pin.IRQ_RISING, handler=increase_volume)
button_b.irq(trigger=Pin.IRQ_RISING, handler=decrease_volume)
window.mainloop()
Important
You need to set the value for constant IOS_MAC_ADDRESS before running the application. The value is the MAC address of your iPhone. On your iPhone Bluetooth must be enabled. You can execute on UNIHIKER following Linux commands:
```bash
# scan with bluetoothctl
$ bluetoothctl scan on
# scan with hcitool
$ hcitool scan
```
lib/gui.py
from tkinter import Tk, Label, Frame, Button, LEFT
from dbus import SystemBus, Interface
class GUI(Tk):
def __init__(self, width: int, height: int, mac_address: str):
"""
Class constructor
:param width: The width of the window.
:type width: int
:param height: The height of the window.
:type height: int
:param mac_address: The MAC address of the Bluetooth device.
:type mac_address: str
"""
super().__init__()
mac = mac_address.replace(':', '_')
self.geometry(f'{width}x{height}+0+0')
self.resizable(width=False, height=False)
self._label = Label(self, pady=5, font=('Arial', 25))
self._label.pack()
self._btn_frame = Frame(self)
self._btn_frame.pack(anchor='center')
self.btn_previous = Button(self._btn_frame, text='\u25C0\u25C0', command=lambda: self._btn_action('previous'))
self.btn_previous.pack(side=LEFT)
self.btn_play = Button(self._btn_frame, text='\u25B6', command=lambda: self._btn_action('play'))
self.btn_play.pack(side=LEFT)
self.btn_pause = Button(self._btn_frame, text='\u2759\u2759', command=lambda: self._btn_action('pause'))
self.btn_pause.pack(side=LEFT)
self.btn_next = Button(self._btn_frame, text='\u25B6\u25B6', command=lambda: self._btn_action('next'))
self.btn_next.pack(side=LEFT)
bus = SystemBus()
media_player = bus.get_object('org.bluez', f'/org/bluez/hci0/dev_{mac}/player0')
self._player = Interface(media_player, dbus_interface='org.bluez.MediaPlayer1')
def update_information(self, volume: int) -> None:
"""
Update label text with new volume level
:param volume: An integer representing the new volume level to display.
:type volume: int
:return: None
"""
self._label.config(text=f'Volume: {volume}')
def _btn_action(self, action: str) -> None:
"""
Button action handler
:param action: The action to be performed by the player. Values are 'play', 'pause', 'next', and 'previous'.
:type action: str
:return: None
"""
if action == 'play':
self._player.Play()
elif action == 'pause':
self._player.Pause()
elif action == 'next':
self._player.Next()
elif action == 'previous':
self._player.Previous()
You can upload the project files to the UNIHIKER board using either SCP (secure copy) or SMB (shared folder).
SCP Method: Use the following SCP command from your local machine to copy the project files:
$ scp -r iOS_Media_Controller root@10.1.2.3:/root/
You'll need to use bluetoothctl to establish a connection between your iPhone and the UNIHIKER board.
```bash
# scan for devices
$ bluetoothctl scan on
# pair device
$ bluetoothctl pair <iPhone_MAC_Address>
# connect device
$ bluetoothctl connect <iPhone_MAC_Address>
```
Replace <iPhone_MAC_Address> with the actual MAC address you obtained during the scan.
Note: The Python script checks on start only if device is connected. Before you run the application, you need to connect manually.
Once your iPhone is connected to the UNIHIKER and the project files are uploaded, you can run the application:
$ python3 main.py
It is also possible to start the application directly over the touch screen. Whatever you like more…
You should now be able to control the volume of the USB speaker and manage iPhone media playback on the UNIHIKER.
Here are a few ways to improve and expand this project:
UI: Add more graphical elements to the GUI to enhance user interaction.
Playback Controls: Expand playback controls to include more functionalities like shuffle or repeat.
Speaker: Instead of USB use Bluetooth speaker.
This project can serve as a starting point for creating a Bluetooth-enabled media controller or smart speaker.