|
#!/bin/bash |
|
# ============================================================================== |
|
# Script: monitor-keyboard.sh |
|
# Description: Monitors for specific USB keyboards and triggers custom scripts |
|
# on plug/unplug events. Uses debouncing to handle event bursts. |
|
# Tags: linux, udev, usb, automation, hotplug, debounce, bash |
|
# ============================================================================== |
|
|
|
# Minimal PoC for USB Keyboard Detection using udevadm |
|
# Does not require sudo. |
|
|
|
# --- Configuration --- |
|
# Set your target keyboard to match against (use lowercase for case-insensitive matching). |
|
# These will match substrings. E.g., TARGET_VENDOR="keychron" matches "Keychron_K1". |
|
TARGET_VENDOR="keychron" |
|
TARGET_MODEL="k1" |
|
|
|
# USB connections fire a "burst" of events. We use a debounce timer |
|
# to ensure your custom script only triggers once per physical plug-in. |
|
DEBOUNCE_SECONDS=3 |
|
LAST_TRIGGER=0 |
|
LAST_ACTION="" |
|
|
|
# Variables to hold the state of the current udev property block being read |
|
ACTION="" |
|
IS_KEYBOARD=0 |
|
VENDOR="" |
|
MODEL="" |
|
|
|
log() { |
|
# Print with a timestamp for easy tracing and debugging |
|
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" |
|
} |
|
|
|
log "Starting USB Keyboard monitor..." |
|
log "Targeting keyboard: Vendor='*$TARGET_VENDOR*', Model='*$TARGET_MODEL*'" |
|
log "Waiting for input devices to connect or disconnect..." |
|
|
|
# We use udevadm monitor, filtering only 'input' subsystem events. |
|
# --environment outputs key/value pairs separated by an empty line per event. |
|
udevadm monitor --environment --subsystem-match=input | while read -r line; do |
|
|
|
# --- Property Streaming --- |
|
# Capture properties as they arrive line-by-line |
|
|
|
if [[ "$line" == ACTION=* ]]; then |
|
ACTION="${line#ACTION=}" |
|
continue |
|
fi |
|
|
|
if [[ "$line" == "ID_INPUT_KEYBOARD=1" ]]; then |
|
IS_KEYBOARD=1 |
|
continue |
|
fi |
|
|
|
if [[ "$line" == ID_VENDOR_ENC=* ]]; then |
|
VENDOR="${line#ID_VENDOR_ENC=}" |
|
# Unescape udev hex sequences like \x20 (space) to make it readable |
|
VENDOR=$(printf '%b' "${VENDOR//\\x/\\x}") |
|
continue |
|
fi |
|
|
|
if [[ "$line" == ID_MODEL_ENC=* ]]; then |
|
MODEL="${line#ID_MODEL_ENC=}" |
|
MODEL=$(printf '%b' "${MODEL//\\x/\\x}") |
|
continue |
|
fi |
|
|
|
# If the line isn't empty, it's just another udev property we don't need to parse right now. |
|
if [[ -n "$line" ]]; then |
|
continue |
|
fi |
|
|
|
|
|
# --- Event Evaluation (Empty line reached) --- |
|
# We evaluate the captured properties using early returns (guard clauses) to avoid nested ifs. |
|
|
|
# Guard 1: Missing action or not an add/remove event |
|
if [[ "$ACTION" != "add" && "$ACTION" != "remove" ]]; then |
|
# Reset state for the next event block |
|
ACTION="" |
|
IS_KEYBOARD=0 |
|
VENDOR="" |
|
MODEL="" |
|
continue |
|
fi |
|
|
|
# Guard 2: Ignore non-keyboards immediately |
|
if [[ $IS_KEYBOARD -eq 0 ]]; then |
|
ACTION="" |
|
IS_KEYBOARD=0 |
|
VENDOR="" |
|
MODEL="" |
|
continue |
|
fi |
|
|
|
# Guard 3: Missing vendor or model data |
|
if [[ -z "$VENDOR" || -z "$MODEL" ]]; then |
|
ACTION="" |
|
IS_KEYBOARD=0 |
|
VENDOR="" |
|
MODEL="" |
|
continue |
|
fi |
|
|
|
# Guard 4: Target Validation (Case-Insensitive Match) |
|
LOWER_VENDOR=$(echo "$VENDOR" | tr '[:upper:]' '[:lower:]') |
|
LOWER_MODEL=$(echo "$MODEL" | tr '[:upper:]' '[:lower:]') |
|
|
|
if [[ "$LOWER_VENDOR" != *"$TARGET_VENDOR"* || "$LOWER_MODEL" != *"$TARGET_MODEL"* ]]; then |
|
log " [DEBUG] Ignored keyboard action: '$ACTION' for '$VENDOR' '$MODEL' (Did not match target)" |
|
ACTION="" |
|
IS_KEYBOARD=0 |
|
VENDOR="" |
|
MODEL="" |
|
continue |
|
fi |
|
|
|
# Guard 5: Debounce/Throttle Check |
|
# We only debounce if the SAME action happens multiple times quickly (event bursts). |
|
# If the action changes (e.g., add immediately followed by remove), we process it. |
|
CURRENT_TIME=$(date +%s) |
|
TIME_DIFF=$((CURRENT_TIME - LAST_TRIGGER)) |
|
if [[ "$ACTION" == "$LAST_ACTION" && $TIME_DIFF -le $DEBOUNCE_SECONDS ]]; then |
|
log " [DEBUG] Ignored duplicate '$ACTION' event for target keyboard (Debounce active)" |
|
ACTION="" |
|
IS_KEYBOARD=0 |
|
VENDOR="" |
|
MODEL="" |
|
continue |
|
fi |
|
|
|
# --- Success --- |
|
# If we reached here, it's exactly the keyboard we want, and it's not a duplicate event burst. |
|
LAST_TRIGGER=$CURRENT_TIME |
|
LAST_ACTION=$ACTION |
|
|
|
log "=> [SUCCESS] Target Keyboard Event: Action='$ACTION', Vendor='$VENDOR', Model='$MODEL'" |
|
|
|
if [[ "$ACTION" == "add" ]]; then |
|
log " [ACTION] Keyboard Plugged In!" |
|
# ------------------------------------------------------------- |
|
# PUT YOUR CUSTOM SCRIPT OR COMMAND FOR **PLUG-IN** HERE |
|
# Example: /path/to/your/connected_script.sh "$VENDOR" "$MODEL" & |
|
# ,kbk |
|
# ------------------------------------------------------------- |
|
elif [[ "$ACTION" == "remove" ]]; then |
|
log " [ACTION] Keyboard Unplugged!" |
|
# ------------------------------------------------------------- |
|
# PUT YOUR CUSTOM SCRIPT OR COMMAND FOR **UNPLUG** HERE |
|
# Example: /path/to/your/disconnected_script.sh "$VENDOR" "$MODEL" & |
|
# ,kbn |
|
# ------------------------------------------------------------- |
|
fi |
|
|
|
# Reset state for the next hardware event |
|
ACTION="" |
|
IS_KEYBOARD=0 |
|
VENDOR="" |
|
MODEL="" |
|
done |
|
|
|
# ========================================================================= |
|
# ======================== FUTURE REFINEMENTS ============================= |
|
# ========================================================================= |
|
# |
|
# 1. Log Redirection (Simple File Logging): |
|
# If you just want to run this in the background and write to a file, run: |
|
# ./monitor_keyboard.sh >> ~/keyboard_monitor.log 2>&1 & |
|
# |
|
# 2. Convert to a User-level Systemd Service (Recommended Refinement Level 1): |
|
# This allows the script to start automatically on login without an open terminal, |
|
# restarts it if it crashes, and logs output natively to the system journal. |
|
# |
|
# Step A: Create the service directory if it doesn't exist: |
|
# mkdir -p ~/.config/systemd/user/ |
|
# |
|
# Step B: Create a file named `~/.config/systemd/user/keyboard-monitor.service` |
|
# |
|
# Step C: Add the following contents to the file (update ExecStart path!): |
|
# [Unit] |
|
# Description=USB Keyboard Monitor |
|
# |
|
# [Service] |
|
# ExecStart=/home/manu/monitor_keyboard.sh |
|
# Restart=always |
|
# RestartSec=3 |
|
# |
|
# [Install] |
|
# WantedBy=default.target |
|
# |
|
# Step D: Reload the systemd user daemon: |
|
# systemctl --user daemon-reload |
|
# |
|
# Step E: Enable (start on login) and start the service: |
|
# systemctl --user enable --now keyboard-monitor.service |
|
# |
|
# 3. Viewing Logs using Journalctl: |
|
# If you converted this to a systemd service, you DO NOT need a separate log file. |
|
# Instead, standard output goes to the systemd journal. You can view it by running: |
|
# |
|
# journalctl --user -u keyboard-monitor.service -f |
|
# |
|
# (Or use `journalctl --user -xe` for the tail of all your user services). |
|
# ========================================================================= |