Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d067b76
Enhance --rotate option for smoother mouse movement adjustments
stedwick Oct 2, 2025
78f778e
Update X-keys support for Pi3 Matrix Board and enhance test output
stedwick Oct 2, 2025
f083089
Optimize X-keys button state reading with caching to reduce blocking …
stedwick Oct 2, 2025
cadb489
Refactor XKeysPedal to support multiple devices and enhance state man…
stedwick Oct 2, 2025
7ccbb22
Refactor XKeysPedal to centralize state updates for improved efficiency
stedwick Oct 2, 2025
8facade
Refactor XKeysPedal to improve data handling and responsiveness
stedwick Oct 2, 2025
5a3409b
Update cache duration in XKeysPedal for improved responsiveness
stedwick Oct 2, 2025
9d27d51
Enhance XKeysPedal caching mechanism for improved responsiveness
stedwick Oct 3, 2025
41d9daa
Refactor XKeysPedal to implement event-driven reading and improve mul…
stedwick Oct 3, 2025
8a1e124
Add X-keys support for macOS with event-driven multiplier control
stedwick Oct 3, 2025
dd18e17
Refactor X-keys macOS interface to improve callback handling and erro…
stedwick Oct 3, 2025
2a20ee5
Refactor X-keys test script to use callback-based approach and improv…
stedwick Oct 3, 2025
55a1dff
Refactor threading in macOS X-keys interface for improved clarity and…
stedwick Oct 3, 2025
3ed3a53
Refactor X-keys device enumeration in macOS interface for improved lo…
stedwick Oct 3, 2025
bb66917
Refactor X-keys macOS interface to streamline device reading and enha…
stedwick Oct 3, 2025
7a827a4
Update logging in X-keys macOS interface to use print for device conn…
stedwick Oct 3, 2025
dce9786
Merge branch 'main' into xkeys
stedwick Oct 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions GEMINI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Project Overview

This project, PhilNav, is a high-performance, open-source head-tracking mouse system. It allows for hands-free computer operation by tracking a reflective sticker on the user's head and translating its movement into mouse cursor motion.

The system is built with a client-server architecture:

* **Server:** Runs on a Raspberry Pi equipped with a PiCamera Module 3 NoIR. It uses Python with `picamera2` for camera interfacing and `opencv-python` for real-time blob detection of the reflective sticker. The server calculates the movement delta and broadcasts it over the local network via UDP.

* **Client:** Runs on a Windows, macOS, or Linux machine. It listens for the UDP broadcasts from the server and uses platform-specific libraries to control the mouse cursor.

The communication between the client and server is done using the OpenTrack protocol, with the movement data packed as `struct` doubles.

# Building and Running

## Dependencies

### Server (Raspberry Pi)

* `python3-opencv`
* `picamera2`
* `libcamera`

These can be installed via `apt`:

```bash
sudo apt install python3-opencv
```

### Client (Desktop)

* `evdev` (for Linux/Wayland)

This can be installed via `pip`:

```bash
pip install evdev
```

## Running the Application

1. **Start the Server:** On the Raspberry Pi, run the following command. The `--preview` flag is useful for initial setup to ensure the camera is positioned correctly.

```bash
python3 server_raspberrypi/main.py --verbose --preview
```

2. **Start the Client:** On your desktop machine, run:

```bash
python3 client_win-mac-nix/main.py --verbose
```

The application can be configured using various command-line arguments on both the client and server. Use the `--help` flag to see all available options.

# Development Conventions

* **Code Style:** The code follows standard Python conventions (PEP 8).
* **Modularity:** The project is organized into two main components: `client_win-mac-nix` and `server_raspberrypi`. Within the client, platform-specific logic is further separated into individual modules (e.g., `mouse_mac.py`, `mouse_win.py`, `mouse_nix_uinput.py`).
* **Configuration:** All configuration is handled through command-line arguments via the `argparse` library. There are no configuration files.
* **Networking:** Communication is done via UDP multicast, which simplifies network configuration as the client and server don't need to know each other's IP addresses.
* **Error Handling:** The code includes basic error handling, such as timeouts for network operations.
13 changes: 13 additions & 0 deletions client_win-mac-nix/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
case "Darwin": # macOS
from mouse_mac import getCursorPos, setCursorPos
from hotkey_win_mac import hotkey_run
from xkeys_mac import xkeys_run, xkeys_devices
case "Windows":
from mouse_win import getCursorPos, setCursorPos
from hotkey_win_mac import hotkey_run
Expand Down Expand Up @@ -109,13 +110,25 @@ def toggle_multiplier():
multiplier_enabled = not multiplier_enabled
logging.info(f"Speed multiplier ({args.multiplier}x) {'enabled' if multiplier_enabled else 'disabled'}\n")

def set_multiplier(true_or_false):
global multiplier_enabled
multiplier_enabled = true_or_false
logging.info(f"Speed multiplier ({args.multiplier}x) {'enabled' if multiplier_enabled else 'disabled'}\n")

hotkey_thread = Thread(target=hotkey_run, kwargs={
"callback": toggle,
"multiplier_callback": toggle_multiplier
}, daemon=True)
hotkey_thread.start()

if platform.system() == "Darwin":
for device in xkeys_devices():
thread = Thread(target=xkeys_run, kwargs={
"device": device,
"callback": set_multiplier
}, daemon=True)
thread.start()


# initialize networking
# Read datagrams over UDP
Expand Down
82 changes: 82 additions & 0 deletions client_win-mac-nix/xkeys_mac.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""macOS helpers for reacting to X-keys USB foot pedal input."""

import hid
import logging
from typing import Callable, Optional

# X-keys vendor ID (PI Engineering)
XKEYS_VENDOR_ID = 0x05F3

# Pi3 Matrix Board product IDs (from hidutil list)
XKEYS_PRODUCT_IDS = [
0x042C, # Pi3 Matrix Board
0x0438, # Pi3 Matrix Board (alternate interface)
]

def xkeys_run(
device: hid.device, callback: Optional[Callable[[bool], None]] = None
):
"""
Continuously read pedal events from an opened HID device and invoke the
supplied callback with the middle-button state (True when pressed).

Args:
device: Forwarded ``hid.device`` instance returned by ``xkeys_devices``.
callback: Callable receiving the pedal state as a bool.
"""
while True:
try:
# Blocking read waits for data, using zero CPU when idle.
data = device.read(64)
if data and len(data) >= 3:
# Byte 2: Button state (0x04 = middle, 0x02 = right, 0x01 = left).
is_pressed = bool(data[2] & 0x04)
if callback:
callback(is_pressed)
except (IOError, OSError, ValueError):
# Device was likely disconnected; exit the thread.
logging.warning("X-keys device disconnected.")
break

def xkeys_devices():
"""Return all connected X-keys pedals as opened ``hid.device`` objects."""

devices: list[hid.device] = []

for device_info in hid.enumerate(XKEYS_VENDOR_ID):
product_id = device_info["product_id"]
if product_id not in XKEYS_PRODUCT_IDS:
continue
usage = device_info.get("usage")
if usage != 0x0001:
continue
usage_page = device_info.get("usage_page")
if usage_page != 0x000c:
continue

logging.debug(
"Enumerated X-keys device (PID: %s, Path: %s, usage=0x%04x, usage_page=0x%04x)",
hex(product_id),
device_info["path"],
usage,
usage_page,
)

try:
device = hid.device()
device.open_path(device_info["path"])
devices.append(device)

print(
"Connected to X-keys device (PID: %s, Path: %s, usage=0x%04x, usage_page=0x%04x)"
% (hex(product_id), device_info["path"], usage, usage_page)
)

except (IOError, OSError) as exc:
# logging.warning("Failed to connect to X-keys device: %s", exc)
pass

if not devices:
logging.debug("No X-keys foot pedal found.")

return devices
51 changes: 51 additions & 0 deletions client_win-mac-nix/xkeys_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Minimal manual test for the callback-based X-keys helpers."""

import logging
import sys
import threading
import time

from xkeys_mac import xkeys_devices, xkeys_run


def log_state(is_pressed: bool) -> None:
status = "PRESSED" if is_pressed else "RELEASED"
logging.info("Middle pedal %s", status)

def main() -> None:
"""Spin up listener threads for each pedal and log state changes."""

logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
)

print("X-keys Foot Pedal Callback Test")
print("Press Ctrl-C to exit\n")

devices = xkeys_devices()
if not devices:
print("ERROR: No X-keys device found. Make sure it's connected.")
sys.exit(1)

for idx, device in enumerate(devices, start=1):
thread = threading.Thread(
target=xkeys_run,
kwargs={"device": device, "callback": log_state},
daemon=True,
name=f"xkeys-{idx}",
)
thread.start()

print(f"Connected to {len(devices)} device(s). Waiting for pedal events...\n")

try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\nExiting...")


if __name__ == "__main__":
main()