163 lines
5.5 KiB
Python
Executable File
163 lines
5.5 KiB
Python
Executable File
#! /usr/bin/env python3
|
|
import argparse
|
|
import sys
|
|
import logging
|
|
import launchpad_py
|
|
import time
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
SCRIPT_DIR = Path("~/.config/launchpad-scripts").expanduser()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def blink_cell(
|
|
launchpad: launchpad_py.Launchpad, x: int, y: int, red: int, green: int, times: int
|
|
):
|
|
for i in range(times):
|
|
time.sleep(0.125)
|
|
launchpad.LedCtrlXY(x, y, red, green)
|
|
time.sleep(0.125)
|
|
launchpad.LedCtrlXY(x, y, 0, 0)
|
|
|
|
|
|
def main() -> int:
|
|
# Handle program arguments
|
|
ap = argparse.ArgumentParser(
|
|
prog="launchpad-script-launcher",
|
|
description="Allows a Launchpad Mini to run scripts",
|
|
)
|
|
|
|
ap.add_argument(
|
|
"-v", "--verbose", help="Enable verbose logging", action="store_true"
|
|
)
|
|
args = ap.parse_args()
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=logging.DEBUG if args.verbose else logging.INFO,
|
|
format="%(levelname)s: %(message)s",
|
|
)
|
|
|
|
# If the script directory doesn't exist, stop
|
|
if not SCRIPT_DIR.exists():
|
|
logger.error(f"Script directory {SCRIPT_DIR} does not exist")
|
|
return 1
|
|
|
|
# Set up an interface object
|
|
launchpad = launchpad_py.Launchpad()
|
|
logger.info("Found the following MIDI devices:")
|
|
launchpad.ListAll()
|
|
|
|
# Connect
|
|
logger.info("Connecting to Launchpad Mini")
|
|
result = launchpad.Open(0, "Launchpad Mini")
|
|
if not result:
|
|
logger.error("Failed to connect to Launchpad Mini")
|
|
return 1
|
|
|
|
# Do a start-up blink
|
|
logger.info("Blinking the Launchpad Mini")
|
|
launchpad.LedAllOn(1)
|
|
time.sleep(0.125)
|
|
launchpad.Reset()
|
|
launchpad.ButtonFlush()
|
|
|
|
# Watch for button press events
|
|
logger.info("Listening for button presses")
|
|
try:
|
|
while True:
|
|
# Search for all scripts with coordinates
|
|
all_known_scripts = list(SCRIPT_DIR.glob("lps_*_*.*"))
|
|
|
|
# Build a list of registered coordinates
|
|
registered_coords = set()
|
|
for script in all_known_scripts:
|
|
parts = script.name.split("_")
|
|
if len(parts) != 3:
|
|
logger.error(f"Invalid script name {script}")
|
|
continue
|
|
x = int(parts[1])
|
|
y = int(parts[2].split(".")[0])
|
|
registered_coords.add((x, y))
|
|
|
|
# Dimly light all registered cells
|
|
for x, y in registered_coords:
|
|
launchpad.LedCtrlXY(x, y + 1, 1, 1)
|
|
|
|
# Check if there has been a button event
|
|
if launchpad.ButtonChanged():
|
|
event = launchpad.ButtonStateXY()
|
|
# Determine the normalized XY coordinate
|
|
x = event[0]
|
|
raw_y = event[1]
|
|
y = raw_y - 1
|
|
|
|
# If the button is outside of 0,0 - 7,7, ignore it
|
|
if x < 0 or x > 7 or y < 0 or y > 7:
|
|
logger.info(f"Ignoring button press at {x},{y}")
|
|
continue
|
|
|
|
# We can determine if this was a press or a release
|
|
was_pressed = event[2]
|
|
|
|
# Ignore release events
|
|
if not was_pressed:
|
|
continue
|
|
|
|
# If the button was pressed, check for a script
|
|
script_name = f"lps_{x}_{y}"
|
|
|
|
# Check if there is a file with this name, and it is executable
|
|
all_scripts = list(SCRIPT_DIR.glob(f"{script_name}.*"))
|
|
if len(all_scripts) == 0:
|
|
logger.info(f"No script found for button {x},{y}")
|
|
blink_cell(launchpad, x, raw_y, 1, 1, 2)
|
|
continue
|
|
if len(all_scripts) > 1:
|
|
logger.error(f"Multiple scripts found for button {x},{y}")
|
|
blink_cell(launchpad, x, raw_y, 1, 1, 2)
|
|
continue
|
|
if not all_scripts[0].is_file():
|
|
logger.error(f"Script for button {x},{y} is not a file")
|
|
blink_cell(launchpad, x, raw_y, 1, 1, 2)
|
|
continue
|
|
if not all_scripts[0].stat().st_mode & 0o111:
|
|
logger.error(f"Script for button {x},{y} is not executable")
|
|
blink_cell(launchpad, x, raw_y, 1, 1, 2)
|
|
continue
|
|
|
|
# Set the cell to orange to indicate that the script is running
|
|
time.sleep(0.125)
|
|
launchpad.LedCtrlXY(x, raw_y, 3, 3)
|
|
time.sleep(0.125)
|
|
|
|
# Run the script
|
|
logger.info(f"Running script {all_scripts[0]}")
|
|
proc = subprocess.Popen([str(all_scripts[0])])
|
|
proc.wait()
|
|
|
|
# If we get a bad return code, blink the cell red
|
|
if proc.returncode != 0:
|
|
logger.error(f"Script {all_scripts[0]} returned {proc.returncode}")
|
|
blink_cell(launchpad, x, raw_y, 3, 0, 2)
|
|
continue
|
|
|
|
# If we get a good return code, blink the cell green
|
|
launchpad.LedCtrlXY(x, raw_y, 0, 3)
|
|
time.sleep(0.5)
|
|
launchpad.LedCtrlXY(x, raw_y, 0, 0)
|
|
|
|
|
|
|
|
except KeyboardInterrupt:
|
|
logger.info("Shutting down")
|
|
launchpad.Reset()
|
|
launchpad.Close()
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|