#! /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())