Friday, August 8, 2025

Udev Rules and Systemd Service for Auto Running python scripts on USB insert / removal

How to Run .py When usb is plugged/unplugged

1. /etc/udev/rules.d/99-gamepad.rules - CREATED

# Start service when F310 is plugged in
ACTION=="add", SUBSYSTEM=="input", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c216", KERNEL=="event*", TAG+="systemd", ENV{SYSTEMD_WANTS}="macros@%k.service"

# Stop service when F310 is unplugged
ACTION=="remove", SUBSYSTEM=="input", ENV{ID_MODEL}=="Logitech_Dual_Action", KERNEL=="event*", RUN+="/bin/systemctl stop macros@%k.service"

Purpose: Detects when the Logitech F310 gamepad (product ID c216) is plugged in or unplugged and triggers systemd service actions.

2. /etc/systemd/system/macros@.service - CREATED

[Unit]
Description=Logitech F310 Macros for %i
After=multi-user.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 /home/mint22/macros.py
Restart=on-failure
User=mint22
Group=mint22
Environment=HOME=/home/mint22
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Purpose: Systemd template service that runs the Python script when triggered by udev. The @ makes it a template service that can be instantiated with device names (like macros@event13.service).

3. /home/mint22/macros.py - EXISTING (permissions updated)

Changes made:

  • Ensured executable permissions: chmod +x /home/mint22/macros.py
  • Added user to input group: sudo usermod -a -G input mint22

Purpose: Your existing Python script that contains the gamepad macro functionality.

Commands Run to Apply Changes:

# Reload udev rules to recognize the new gamepad detection rule
sudo udevadm control --reload-rules

# Reload systemd to recognize the new service template
sudo systemctl daemon-reload

# Set proper permissions
chmod +x /home/mint22/macros.py
sudo usermod -a -G input mint22

How It Works:

  1. When F310 is plugged in → udev detects it → starts macros@eventX.service → runs macros.py
  2. When F310 is unplugged → udev detects removal → stops the service → terminates macros.py

The system automatically handles starting and stopping the script based on the physical presence of the gamepad.

prettify json from cli with jq and this bash alias


alias prettifyjson='f(){ jq . "$1" > tmp && mv tmp "$1"; }; f'
source ~/.bashrc

Thursday, August 7, 2025

Using Logitech FCB310 Gamepad as a Macropad on Raspberry Pi 4

This tutorial shows how to use your Logitech FCB310 USB gamepad as a macropad on a Raspberry Pi 4, with automatic startup and hotplug detection. When plugged in, your gamepad buttons will trigger keyboard macros on the Pi, no manual script launching needed.


Requirements

  • Raspberry Pi 4 with Raspberry Pi OS (desktop)
  • Logitech FCB310 USB Gamepad
  • Internet connection for installing packages

Step 1: Install Required Software

Open a terminal and run:

sudo apt update
sudo apt install python3-pip xdotool
pip3 install inputs pyudev watchdog
  • xdotool: simulates keyboard keypresses
  • inputs: reads gamepad events
  • pyudev: detects USB device plug/unplug
  • watchdog: monitors macro config file changes

Step 2: Prepare Macro Configuration File

Create a CSV file to map gamepad buttons to keyboard shortcuts.

Example file: /home/pi/macropad/macros.csv

BTN_A,ctrl+alt+t
BTN_B,ctrl+w
  • First column: gamepad button code (e.g., BTN_A)
  • Second column: key combo for xdotool (e.g., ctrl+alt+t)

Step 3: Create the Macro Script

Create a folder and the Python script:

mkdir -p ~/macropad
vim ~/macropad/gamepad_macro.py

Paste this full script into it:

import csv
import subprocess
from inputs import get_gamepad
import pyudev
import threading
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
import time

mapping = {}

def load_macros():
    global mapping
    mapping = {}
    try:
        with open("/home/pi/macropad/macros.csv", newline='') as csvfile:
            reader = csv.reader(csvfile)
            for row in reader:
                if len(row) >= 2:
                    btn = row[0].strip()
                    keys = row[1].strip()
                    mapping[btn] = [keys]
        print("Macros reloaded:", mapping)
    except Exception as e:
        print("Failed to load macros:", e)

def send_macro(keys):
    for combo in keys:
        subprocess.run(["xdotool", "key", combo])

def listen_gamepad(stop_event):
    while not stop_event.is_set():
        try:
            events = get_gamepad()
            for event in events:
                if event.ev_type == "Key" and event.state == 1:
                    if event.code in mapping:
                        send_macro(mapping[event.code])
        except Exception:
            pass

class ConfigChangeHandler(FileSystemEventHandler):
    def on_modified(self, event):
        if event.src_path.endswith("macros.csv"):
            print("Config file changed, reloading macros")
            load_macros()

def device_event(observer, device):
    global gamepad_thread, stop_event
    if device.action == "add" and "event" in device.device_node:
        print("Gamepad connected, starting listener")
        load_macros()
        if gamepad_thread and gamepad_thread.is_alive():
            stop_event.set()
            gamepad_thread.join()
        stop_event.clear()
        gamepad_thread = threading.Thread(target=listen_gamepad, args=(stop_event,), daemon=True)
        gamepad_thread.start()
    elif device.action == "remove":
        print("Gamepad disconnected")
        stop_event.set()
        if gamepad_thread:
            gamepad_thread.join()

if __name__ == "__main__":
    stop_event = threading.Event()
    gamepad_thread = None

    context = pyudev.Context()
    monitor = pyudev.Monitor.from_netlink(context)
    monitor.filter_by('input')

    observer = pyudev.MonitorObserver(monitor, device_event)
    observer.start()

    # Watch macros.csv for changes
    config_handler = ConfigChangeHandler()
    config_observer = Observer()
    config_observer.schedule(config_handler, path="/home/pi/macropad/", recursive=False)
    config_observer.start()

    print("Waiting for gamepad to connect...")
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        stop_event.set()
        if gamepad_thread:
            gamepad_thread.join()
        config_observer.stop()
        config_observer.join()

Step 4: Make Script Executable

chmod +x ~/macropad/gamepad_macro.py

Step 5: Setup Autostart with systemd

Create a service file:

sudo vim /etc/systemd/system/macropad.service

Add:

[Unit]
Description=Gamepad Macro Pad Service
After=graphical.target

[Service]
ExecStart=/usr/bin/python3 /home/pi/macropad/gamepad_macro.py
Restart=always
User=pi

[Install]
WantedBy=graphical.target

Enable and start service:

sudo systemctl enable macropad.service
sudo systemctl start macropad.service

Step 6: Reboot and Test

Reboot your Pi:

sudo reboot
  • Plug in your FCB310 gamepad.
  • Press buttons mapped in macros.csv.
  • Macros should trigger automatically as keyboard inputs.

Customization

  • Edit /home/pi/macropad/macros.csv anytime.
  • The script auto-reloads macros on file changes.
  • Add/remove mappings without restarting.

Troubleshooting

  • Ensure your gamepad device shows up in /dev/input/ (use evtest to confirm).
  • Adjust key combos to fit your needs (see xdotool docs).
  • Check logs with journalctl -u macropad.service -f.