import customtkinter as ctk
class Config:
"""Application configuration constants."""
# Screen dimensions (Unihiker M10)
SCREEN_WIDTH = 240
SCREEN_HEIGHT = 320
# Network settings
HOST_IP = "192.168.xx.xxx"
HOST_PORT = 9999
# Colors (Dark Modern Palette)
BG_PRIMARY = "#1a1a1a"
BG_SECONDARY = "#2b2b2b"
TEXT_PRIMARY = "#ffffff"
TEXT_SECONDARY = "#a0a0a0"
# Accent colors for app buttons
ACCENT_SPOTIFY = "#1DB954"
ACCENT_VSCODE = "#007ACC"
# Animation Settings
TARGET_FPS = 30
FRAME_TIME = 1000 // TARGET_FPS # 33ms for 30 FPS
# Basic App Structure
class RemoteDeckApp(ctk.CTk):
def __init__(self):
super().__init__()
self.title("Unihiker Remote Deck")
self.geometry(f"{Config.SCREEN_WIDTH}x{Config.SCREEN_HEIGHT}")
self.configure(fg_color=Config.BG_PRIMARY)
if __name__ == "__main__":
ctk.set_appearance_mode("dark")
app = RemoteDeckApp()
app.mainloop()
Because the code is written entirely in Python, it was a little complicated. For the foundation, we use customtkinker to configure the application's basic structure and make it executable. It became complicated to create because it only supports its libraries, but this is just the foundation—the top layer.
import customtkinter as ctk
class SplashScreen(ctk.CTkFrame):
def __init__(self, master):
super().__init__(master, fg_color="#1a1a1a")
self.setup_ui()
self.progress_value = 0.0
self.animation_running = False
def setup_ui(self):
# Center container
container = ctk.CTkFrame(self, fg_color="transparent")
container.place(relx=0.5, rely=0.5, anchor="center")
# Logo/Title
title_label = ctk.CTkLabel(
container,
text="UNIHIKER",
font=ctk.CTkFont(family="Arial", size=24, weight="bold"),
text_color="#ffffff"
)
title_label.pack(pady=(0, 30))
# Progress bar
self.progress_bar = ctk.CTkProgressBar(
container,
width=180,
height=6,
corner_radius=3,
fg_color="#2b2b2b",
progress_color="#1DB954"
)
self.progress_bar.pack(pady=(0, 10))
self.progress_bar.set(0)
# Status text
self.status_label = ctk.CTkLabel(
container,
text="System Initializing...",
font=ctk.CTkFont(family="Arial", size=10),
text_color="#666666"
)
self.status_label.pack()
def start_animation(self):
"""Start the boot animation."""
self.progress_value = 0.0
self.animation_running = True
self._animate_progress()
def _animate_progress(self):
"""Animate the progress bar."""
if not self.animation_running:
return
# Calculate step for smooth animation
step = 1.0 / (2.0 * 30) # 2 seconds at 30 FPS
self.progress_value += step
if self.progress_value >= 1.0:
self.progress_bar.set(1.0)
self.status_label.configure(text="Ready!")
else:
self.progress_bar.set(self.progress_value)
if self.progress_value > 0.7:
self.status_label.configure(text="Preparing interface...")
elif self.progress_value > 0.4:
self.status_label.configure(text="Loading components...")
self.after(33, self._animate_progress) # 30 FPS
# Demo usage
if __name__ == "__main__":
ctk.set_appearance_mode("dark")
app = ctk.CTk()
app.geometry("240x320")
splash = SplashScreen(app)
splash.pack(fill="both", expand=True)
splash.start_animation()
app.mainloop()
We attempted to include an idle animation to save time when it isn't in use. It can be altered, like a clock or something, but I wanted to include a gif, but we are unable to do so with Python. GIFs had to be converted into frames using PIL in order for it to function properly. However, it must be stable for 30 to 60 frames. Additionally, we used a loading bar and some simple splash animations for the initial UI animation. It wasn't necessary, but without it, the user interface wouldn't load smoothly, which could aid in loading the entire script. It may take some time to load.
import customtkinter as ctk
from PIL import Image
class AppLauncher(ctk.CTkFrame):
def __init__(self, master):
super().__init__(master, fg_color="#1a1a1a")
self.setup_ui()
def setup_ui(self):
# Header
header = ctk.CTkFrame(self, fg_color="transparent", height=40)
header.pack(fill="x", padx=10, pady=(10, 5))
header_label = ctk.CTkLabel(
header,
text="Remote Deck",
font=ctk.CTkFont(family="Arial", size=14, weight="bold"),
text_color="#ffffff"
)
header_label.pack(side="left", pady=5)
# Button grid container
grid_container = ctk.CTkFrame(self, fg_color="transparent")
grid_container.pack(fill="both", expand=True, padx=10, pady=5)
# Configure grid
grid_container.grid_columnconfigure(0, weight=1)
grid_container.grid_columnconfigure(1, weight=1)
# App buttons data
apps = [
{"name": "Spotify", "color": "#1DB954", "command": "OPEN_SPOTIFY"},
{"name": "VS Code", "color": "#007ACC", "command": "OPEN_VSCODE"},
{"name": "Safari", "color": "#0A84FF", "command": "OPEN_SAFARI"},
{"name": "Terminal", "color": "#6B7280", "command": "OPEN_TERMINAL"},
]
# Create buttons
for idx, app in enumerate(apps):
row = idx // 2
col = idx % 2
# Button frame
btn_frame = ctk.CTkFrame(
grid_container,
fg_color="#2b2b2b",
corner_radius=15
)
btn_frame.grid(row=row, column=col, padx=5, pady=5, sticky="nsew")
grid_container.grid_rowconfigure(row, weight=1)
# Accent color bar
accent_bar = ctk.CTkFrame(
btn_frame,
width=4,
height=30,
corner_radius=2,
fg_color=app["color"]
)
accent_bar.place(x=8, rely=0.5, anchor="w")
# App name
name_label = ctk.CTkLabel(
btn_frame,
text=app["name"],
font=ctk.CTkFont(family="Arial", size=11, weight="bold"),
text_color="#ffffff"
)
name_label.place(relx=0.5, rely=0.5, anchor="center")
# Make clickable
self._make_clickable(btn_frame, app["command"])
self._make_clickable(name_label, app["command"])
def _make_clickable(self, widget, command):
widget.bind("<Button-1>", lambda e: self._on_app_click(command))
def _on_app_click(self, command):
print(f"Launching: {command}")
# Here you would send the command to your Mac server
# Demo usage
if __name__ == "__main__":
ctk.set_appearance_mode("dark")
app = ctk.CTk()
app.geometry("240x320")
launcher = AppLauncher(app)
launcher.pack(fill="both", expand=True)
app.mainloop()
Although creating a homepage user interface can be challenging, some libraries, like PIL, are used to import images, such as the logos of apps like Safari, Spotify, and VScode. to give it a nice appearance
import socket
import threading
class NetworkManager:
def __init__(self, host_ip="192.168.xx.xxx", host_port=9999):
self.host_ip = host_ip
self.host_port = host_port
def send_command(self, command):
def send_in_thread():
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(3)
sock.connect((self.host_ip, self.host_port))
sock.send(command.encode("utf-8"))
response = sock.recv(1024).decode("utf-8")
print(f"Response: {response}")
sock.close()
except Exception as e:
print(f"Network error: {e}")
threading.Thread(target=send_in_thread, daemon=True).start()
def check_connection(self):
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(2)
result = sock.connect_ex((self.host_ip, self.host_port))
sock.close()
return result == 0
except:
return False
# Usage example
if __name__ == "__main__":
network = NetworkManager()
# Test connection
if network.check_connection():
print("Connected to Mac!")
network.send_command("OPEN_SPOTIFY")
else:
print("Cannot connect to Mac server")
The most crucial aspect of Unideck is networking. We wrote a script to connect Mac to Unihiker via a local network because it would be useless without a bridge between them. and function effectively, obtaining input and output with ease. each time your Mac boots up. In order to connect to the Unihiker, it must be running in the terminal; otherwise, it will not be connected. Therefore, a bridge is required to access it. and send or receive signals.
import customtkinter as ctk
class SpotifyPlayer(ctk.CTkFrame):
def __init__(self, master):
super().__init__(master, fg_color="#1a1a1a")
self.is_playing = False
self.setup_ui()
def setup_ui(self):
# Header with back button
header = ctk.CTkFrame(self, fg_color="transparent", height=35)
header.pack(fill="x", padx=8, pady=(5, 0))
back_btn = ctk.CTkButton(
header,
text="<",
width=35,
height=28,
corner_radius=14,
fg_color="#3d3d3d",
text_color="#ffffff",
command=self._go_back
)
back_btn.pack(side="left")
header_label = ctk.CTkLabel(
header,
text="SPOTIFY",
font=ctk.CTkFont(family="Arial", size=11, weight="bold"),
text_color="#1DB954"
)
header_label.pack(side="right", padx=8)
# Album art placeholder
art_frame = ctk.CTkFrame(
self,
width=105,
height=105,
corner_radius=12,
fg_color="#1a1a2e"
)
art_frame.pack(pady=(8, 0))
art_icon = ctk.CTkLabel(
art_frame,
text="♫",
font=ctk.CTkFont(size=42, weight="bold"),
text_color="#1DB954"
)
art_icon.place(relx=0.5, rely=0.5, anchor="center")
# Track info
self.track_label = ctk.CTkLabel(
self,
text="Not Playing",
font=ctk.CTkFont(family="Arial", size=13, weight="bold"),
text_color="#ffffff"
)
self.track_label.pack(pady=(8, 0))
self.artist_label = ctk.CTkLabel(
self,
text="Open Spotify on Mac",
font=ctk.CTkFont(family="Arial", size=10),
text_color="#a0a0a0"
)
self.artist_label.pack(pady=(0, 6))
# Progress bar
self.progress_bar = ctk.CTkProgressBar(
self,
width=140,
height=6,
corner_radius=3,
fg_color="#3d3d3d",
progress_color="#1DB954"
)
self.progress_bar.pack(pady=(0, 5))
self.progress_bar.set(0)
# Main controls
controls_frame = ctk.CTkFrame(self, fg_color="transparent")
controls_frame.pack(pady=3)
# Previous button
prev_btn = ctk.CTkButton(
controls_frame,
text="<<",
width=48,
height=48,
corner_radius=24,
fg_color="#333333",
text_color="#ffffff",
command=self._prev_track
)
prev_btn.pack(side="left", padx=6)
# Play/Pause button
self.play_btn = ctk.CTkButton(
controls_frame,
text="▶",
width=60,
height=60,
corner_radius=30,
fg_color="#1DB954",
text_color="#000000",
font=ctk.CTkFont(size=22, weight="bold"),
command=self._play_pause
)
self.play_btn.pack(side="left", padx=8)
# Next button
next_btn = ctk.CTkButton(
controls_frame,
text=">>",
width=48,
height=48,
corner_radius=24,
fg_color="#333333",
text_color="#ffffff",
command=self._next_track
)
next_btn.pack(side="left", padx=6)
# Volume controls
volume_frame = ctk.CTkFrame(self, fg_color="transparent")
volume_frame.pack(pady=(8, 3))
vol_down_btn = ctk.CTkButton(
volume_frame,
text="-",
width=50,
height=32,
fg_color="#2a2a2a",
command=self._volume_down
)
vol_down_btn.pack(side="left", padx=3)
vol_up_btn = ctk.CTkButton(
volume_frame,
text="+",
width=50,
height=32,
fg_color="#2a2a2a",
command=self._volume_up
)
vol_up_btn.pack(side="left", padx=3)
def _play_pause(self):
self.is_playing = not self.is_playing
self.play_btn.configure(text="||" if self.is_playing else "▶")
print("Media: Play/Pause")
def _next_track(self):
print("Media: Next Track")
def _prev_track(self):
print("Media: Previous Track")
def _volume_up(self):
print("Media: Volume Up")
def _volume_down(self):
print("Media: Volume Down")
def _go_back(self):
print("Going back to main deck")
# Demo usage
if __name__ == "__main__":
ctk.set_appearance_mode("dark")
app = ctk.CTk()
app.geometry("240x320")
player = SpotifyPlayer(app)
player.pack(fill="both", expand=True)
app.mainloop()
This is not required. This facilitates the use of Spotify on the screen. additional features like the ability to switch between songs. Replace them or post them repeatedly. It just makes using it easier. to take control of it without using a mouse in order to be interactive because of the intense writing codes to switch apps or simply open git through the terminal with a single click. If I want to change it to code peacefully, I simply switch the playlist. With the ability to quickly switch up a somewhat useful device, it truly helps me during hours of saving.
import customtkinter as ctk
class BaseState(ctk.CTkFrame):
def __init__(self, master, app_controller):
super().__init__(master, fg_color="#1a1a1a")
self.app = app_controller
self.setup_ui()
def setup_ui(self):
raise NotImplementedError("Subclasses must implement setup_ui()")
def on_enter(self):
pass
def on_exit(self):
pass
class SplashState(BaseState):
def setup_ui(self):
label = ctk.CTkLabel(
self,
text="UNIHIKER\nRemote Deck",
font=ctk.CTkFont(size=20, weight="bold"),
text_color="#ffffff"
)
label.place(relx=0.5, rely=0.5, anchor="center")
def on_enter(self):
# Auto-transition after 2 seconds
self.after(2000, lambda: self.app.change_state("deck"))
class DeckState(BaseState):
def setup_ui(self):
# Header
header_label = ctk.CTkLabel(
self,
text="Remote Deck",
font=ctk.CTkFont(size=14, weight="bold"),
text_color="#ffffff"
)
header_label.pack(pady=20)
# Spotify button
spotify_btn = ctk.CTkButton(
self,
text="Open Spotify Player",
width=200,
height=50,
fg_color="#1DB954",
command=lambda: self.app.change_state("spotify")
)
spotify_btn.pack(pady=10)
class SpotifyState(BaseState):
def setup_ui(self):
# Back button
back_btn = ctk.CTkButton(
self,
text="← Back",
width=100,
height=30,
command=lambda: self.app.change_state("deck")
)
back_btn.pack(pady=10, anchor="w", padx=10)
# Player UI
player_label = ctk.CTkLabel(
self,
text="♫\nSpotify Player\nControls Here",
font=ctk.CTkFont(size=16),
text_color="#1DB954"
)
player_label.place(relx=0.5, rely=0.5, anchor="center")
class RemoteDeckApp(ctk.CTk):
def __init__(self):
super().__init__()
# Window setup
self.title("Unihiker Remote Deck")
self.geometry("240x320")
self.configure(fg_color="#1a1a1a")
# Initialize states
self.states = {}
self.current_state = None
self._init_states()
# Start with splash screen
self.change_state("splash")
def _init_states(self):
self.states = {
"splash": SplashState(self, self),
"deck": DeckState(self, self),
"spotify": SpotifyState(self, self),
}
def change_state(self, state_name):
if state_name not in self.states:
print(f"Error: Unknown state '{state_name}'")
return
# Exit current state
if self.current_state:
self.current_state.on_exit()
self.current_state.place_forget()
# Enter new state
self.current_state = self.states[state_name]
self.current_state.place(x=0, y=0, relwidth=1, relheight=1)
self.current_state.on_enter()
print(f"State changed to: {state_name}")
# Run the complete app
if __name__ == "__main__":
ctk.set_appearance_mode("dark")
app = RemoteDeckApp()
app.mainloop()
The system is constructed as a thin embedded user interface that is parked with a desktop logic agent. It connects via a minimal commands and state protocol with animation input handling and a coordinate system for stability.
import customtkinter as ctk
class BaseState(ctk.CTkFrame):
def __init__(self, master, app_controller):
super().__init__(master, fg_color="#1a1a1a")
self.app = app_controller
self.setup_ui()
def setup_ui(self):
raise NotImplementedError("Subclasses must implement setup_ui()")
def on_enter(self):
pass
def on_exit(self):
pass
class SplashState(BaseState):
def setup_ui(self):
label = ctk.CTkLabel(
self,
text="UNIHIKER\nRemote Deck",
font=ctk.CTkFont(size=20, weight="bold"),
text_color="#ffffff"
)
label.place(relx=0.5, rely=0.5, anchor="center")
def on_enter(self):
# Auto-transition after 2 seconds
self.after(2000, lambda: self.app.change_state("deck"))
class DeckState(BaseState):
def setup_ui(self):
# Header
header_label = ctk.CTkLabel(
self,
text="Remote Deck",
font=ctk.CTkFont(size=14, weight="bold"),
text_color="#ffffff"
)
header_label.pack(pady=20)
# Spotify button
spotify_btn = ctk.CTkButton(
self,
text="Open Spotify Player",
width=200,
height=50,
fg_color="#1DB954",
command=lambda: self.app.change_state("spotify")
)
spotify_btn.pack(pady=10)
class SpotifyState(BaseState):
def setup_ui(self):
# Back button
back_btn = ctk.CTkButton(
self,
text="← Back",
width=100,
height=30,
command=lambda: self.app.change_state("deck")
)
back_btn.pack(pady=10, anchor="w", padx=10)
# Player UI
player_label = ctk.CTkLabel(
self,
text="♫\nSpotify Player\nControls Here",
font=ctk.CTkFont(size=16),
text_color="#1DB954"
)
player_label.place(relx=0.5, rely=0.5, anchor="center")
class RemoteDeckApp(ctk.CTk):
def __init__(self):
super().__init__()
# Window setup
self.title("Unihiker Remote Deck")
self.geometry("240x320")
self.configure(fg_color="#1a1a1a")
# Initialize states
self.states = {}
self.current_state = None
self._init_states()
# Start with splash screen
self.change_state("splash")
def _init_states(self):
self.states = {
"splash": SplashState(self, self),
"deck": DeckState(self, self),
"spotify": SpotifyState(self, self),
}
def change_state(self, state_name):
if state_name not in self.states:
print(f"Error: Unknown state '{state_name}'")
return
# Exit current state
if self.current_state:
self.current_state.on_exit()
self.current_state.place_forget()
# Enter new state
self.current_state = self.states[state_name]
self.current_state.place(x=0, y=0, relwidth=1, relheight=1)
self.current_state.on_enter()
print(f"State changed to: {state_name}")
# Run the complete app
if __name__ == "__main__":
ctk.set_appearance_mode("dark")
app = RemoteDeckApp()
app.mainloop()
The system is constructed as a thin embedded user interface that is parked with a desktop logic agent. It connects via a minimal commands and state protocol with animation input handling and a coordinate system for stability.
from pinpong.board import Board
from pinpong.extension.unihiker import *
import threading
class BuzzerFeedback:
def __init__(self):
try:
Board().begin()
self.enabled = True
print("Buzzer initialized")
except Exception as e:
print(f"Buzzer init failed: {e}")
self.enabled = False
def click(self):
if self.enabled:
def beep():
try:
buzzer.pitch(800, 1)
except Exception as e:
print(f"Buzzer failed: {e}")
self.enabled = False
threading.Thread(target=beep, daemon=True).start()
buzzer_feedback = BuzzerFeedback()
# Add to button clicks:
def on_button_click():
buzzer_feedback.click()
print("Button clicked!")
# Usage in your app:
# In DeckState._send_command():
# buzzer_feedback.click()
#
# In SpotifyState._play_pause():
# buzzer_feedback.click()
#
# In SpotifyState._next_track():
# buzzer_feedback.click()
We can use its buzzer to make it more engaging and interactive. For example, when we open Terminal or VS Code, the buzzer will beep to indicate that we have switched apps or simply changed the song. Perhaps it can be used for something better. Although it is primarily utilized in IoT sensors, it can also be used for everyday purposes. but I didn't do it too crazy; it was only a quick thing to create from scratch, so it's nice to engage with