From f9e0db4499c8b34b4f27ea203294b4e823d51340 Mon Sep 17 00:00:00 2001 From: alban Date: Fri, 9 Oct 2020 18:29:28 +0200 Subject: [PATCH 1/2] [enh] adds a command line runner for clitools --- clitools/.gitignore | 1 + clitools/_run.sh | 11 ++ clitools/runner.py | 116 +++++++++++++ clitools/runner_lib.py | 374 ++++++++++++++++++++++++++++++++++++++++ clitools/runner_midi.py | 12 ++ 5 files changed, 514 insertions(+) create mode 100644 clitools/.gitignore create mode 100755 clitools/_run.sh create mode 100755 clitools/runner.py create mode 100644 clitools/runner_lib.py create mode 100644 clitools/runner_midi.py diff --git a/clitools/.gitignore b/clitools/.gitignore new file mode 100644 index 0000000..b4c4cfb --- /dev/null +++ b/clitools/.gitignore @@ -0,0 +1 @@ +playlists diff --git a/clitools/_run.sh b/clitools/_run.sh new file mode 100755 index 0000000..96014a0 --- /dev/null +++ b/clitools/_run.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +killexit(){ + pkill -9 -s $$ + } + +trap killexit SIGTERM SIGINT SIGKILL SIGSTOP + +bash -c "$@" + +killbill(){ pkill -9 -s $(awk '{print $6}' /proc/$1/stat) } diff --git a/clitools/runner.py b/clitools/runner.py new file mode 100755 index 0000000..32f5a75 --- /dev/null +++ b/clitools/runner.py @@ -0,0 +1,116 @@ +#!/usr/bin/python3 + +import sys +import os +import signal +import subprocess +import time +import tty,termios +import re +import json +from pathlib import Path +import runner_lib as runner + + +bindings={ + "Show playlist" : "l", + "Launch [0-x] cmd" : "0-x", + "Previous command" : "p", + "Next command" : "o", + "New command" : "a", + "Edit command" : "e", + "Delete command" : "d", + "Load playlist" : "L", + "Save playlist" : "S", + "Save as new" : "A", + "New playlist" : "N", + "Command help" : "H", + "Kill process Id" : "K", + "Edit Laser Id" : "i", + "Edit Laser Scene" : "s", + "Information" : "I", + "Help" : "h", + "Quit" : "q", + +} + + +## Init user contact + + + +# Main Loop +runner.action_info() +runner.action_help() +print("\n\nLoad a playlist? [Y/n]: ") +if "y" == runner.inkey() : + runner.action_loadPlaylist() + +while True: + # Fuck zombies + runner._killBill() + runner._ok("> Next Action?") + k = runner.inkey() + + if bindings["Next command"] == k: + runner.action_changeCommand( 1 ) + runner.action_runCommand() + elif bindings["Previous command"] == k: + runner.action_changeCommand( -1 ) + runner.action_runCommand() + elif re.match( r'^\d+$',k): + runner.action_match(k) + runner.action_runCommand() + elif bindings["New command"] == k: + runner.action_newCommand() + continue + elif bindings["Show playlist"] == k: + runner.action_listAll() + continue + elif bindings["Delete command"] == k: + runner.action_deleteCommand() + continue + elif bindings["Edit command"] == k: + runner.action_listAll() + runner.action_edit() + continue + elif bindings["Load playlist"] == k: + if runner.action_loadPlaylist(): + runner.action_listAll() + continue + elif bindings["Save playlist"] == k: + runner.action_savePlaylist() + continue + elif bindings["Save as new"] == k: + runner.action_savePlaylist() + continue + elif bindings["New playlist"] == k: + runner.action_newPlaylist() + continue + elif bindings["Command help"] == k: + runner.action_commandHelp() + continue + elif bindings["Edit Laser Id"] == k: + runner.action_laserId() + continue + elif bindings["Edit Laser Scene"] == k: + runner.action_laserScene() + continue + elif bindings["Kill process Id"] == k: + runner.action_killPid() + continue + elif bindings["Help"] == k: + runner.action_help() + continue + elif bindings["Information"] == k: + runner.action_info() + continue + elif bindings["Quit"] == k: + runner.action_quit() + else: + runner._err("Unexpected key:{}".format(k)) + continue + + + + diff --git a/clitools/runner_lib.py b/clitools/runner_lib.py new file mode 100644 index 0000000..ef7b49c --- /dev/null +++ b/clitools/runner_lib.py @@ -0,0 +1,374 @@ + +import sys +import os +import signal +import subprocess +import time +import tty,termios +import re +import json +from pathlib import Path +import redis + +class bcolors: + HL = '\033[31m' + OKBLUE = '\033[94m' + OKGREEN = '\033[92m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + +class _Getch: + def __call__(self): + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(sys.stdin.fileno()) + ch = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + return ch +inkey = _Getch() + +def intkey(): + try: + i = int( inkey() ) + return(i) + except ValueError: + print("Error.") + +current_id=0 +current_cmd="" +process = None +current_filename = "" +currentPlayList = [] +playlistsDir = Path("./playlists") +if not playlistsDir.is_dir() : playlistsDir.mkdir() + +def ask(q): + print(q) + return inkey() + +def _ok(msg): + print( bcolors.BOLD+bcolors.OKBLUE+ msg + bcolors.ENDC) + +def _err(msg): + print( bcolors.HL + msg + bcolors.ENDC) + +def _kill(process): + if process : + try: + pid = os.getpgid(process.pid) + os.killpg(pid, signal.SIGTERM) + os.killpg(pid, signal.SIGKILL) + os.kill(pid, signal.SIGTERM) + os.kill(pid, signal.SIGKILL) + process.terminate() + process.kill() + except Exception as e: + print("woops:{}".format(e)) + + + +def _killBill(): + subprocess.run("ps --ppid 1 -fo pid,sess,ppid,cmd | grep 'toRedis.py' | while read pid sid other; do pkill -9 -s $sid; done", shell=True,executable='/bin/bash') + + + +def action_info(): + print(""" +Welcome to LJ playlist manager + +Currently running on + +IP : {} +Port : {} +Key : {} +Scene : {} +Laser : {} +""".format( + environ["REDIS_IP"], + environ["REDIS_PORT"], + environ["REDIS_KEY"], + environ["REDIS_SCENE"], + environ["REDIS_LASER"] +)) + + +def action_changeCommand( inc ): + global currentPlayList + global current_id + if 0 == len(currentPlayList): + _err("Empty playlist") + return False + current_id = (current_id + 1) % len(currentPlayList) + return True + +def action_match( k ): + if int(k) > (len(currentPlayList) - 1): + print( bcolors.HL + "This key does not exist" + bcolors.ENDC ) + return False + else : + current_id = int(k) + +def action_runCommand(): + global currentPlayList + global current_id + global process + + # Get new command + try: + current_cmd = currentPlayList[current_id] + except IndexError as e: + _err("woops:{}".format(e)) + return False + + print("\n[!]New command:'{}'\n".format(current_cmd)) + + # Start subprocess + try : + _kill(process) + process = subprocess.Popen("./_run.sh '"+current_cmd+" | exports/toRedis.py -i $REDIS_IP -k $REDIS_KEY'", shell=True, executable='/bin/bash', stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=environ, preexec_fn=os.setsid) + + except Exception as e: + print("woops:{}".format(e)) + + + +def action_newCommand(): + global currentPlayList + print("Enter new command or e(x)it.") + k = input() + # Exit early + if "x" == k: + return(False) + currentPlayList.append(k) + print(bcolors.OKBLUE + "Command added" + bcolors.ENDC) + return True + + + + +def action_deleteCommand(): + global currentPlayList + print("Select sequence to delete or e(x)it.") + action_listAll() + key = int(input()) + # Exit early + if "x" == k: + return(False) + del currentPlayList[key] + return True + + + +def action_listAll(): + global currentPlayList, current_cmd + print("\n--------------------------------------") + for i,seq in enumerate(currentPlayList): + pre="" + suf="" + if current_cmd == seq : + pre = bcolors.HL + suf = bcolors.ENDC + print( pre + "{}\t{}".format(i,seq) + suf ) + print("--------------------------------------\n") + + + + + +def action_help(): + global bindings + print("\nKey\tAction\n--------------------------------------") + for i in bindings: + print(" {}\t{}".format(bindings[i],i)) + print("--------------------------------------\n") + + + + + +def action_edit(): + print("Enter the command number to edit, or 'x' to abort.") + k = intkey() + if 'x' == k: + return + print("Enter the next value, or 'x' to abort.") + value = input() + if 'x' == value: + return + currentPlayList[k] = value + + + + + + + + +def action_loadPlaylist(): + + global playlistsDir + global currentPlayList + global current_playlist_name + # list files + i=0 + file_list = [x for x in playlistsDir.glob("*json")] + if 0 == len(file_list ): + print( bcolors.HL + "Error. No file in path '{}'\n".format(playlistsDir.name)) + return False + + print("\n Id\tName") + for k,name in enumerate(file_list) : + print(" {}\t{}".format(k,name),flush=True) + + # ask file + print("\nChoose a file or e(x)it:") + k = intkey() + if '' == k: + print("Invalid choice: '{}'".format(k)) + return + + # Exit early + if "x" == k: return(False) + + # todo : helper for detecting invalid keys + try: + if k > (len(file_list) - 1): + print( bcolors.HL + "This key '{}' does not exist".format(k) + bcolors.ENDC ) + return False + except TypeError: + print( bcolors.HL + "This key '{}' is not valid".format(k) + bcolors.ENDC ) + return False + + # Load file + playlistFile = Path("./playlists/"+file_list[k].name) + currentPlayList = json.loads(playlistFile.read_text()) + current_playlist_name = file_list[k].name + current_id = 0 + print( bcolors.OKBLUE + "Playlist loaded: {}\n".format(current_playlist_name)+ bcolors.ENDC) + return True + + + + + + + + + + +def action_newPlaylist(): + global playlistsDir + global currentPlayList + # ask for name + print("Enter new playlist name (without.json) or e(x)it question?") + k = input() + + # Exit early + if "x" == k: + return(False) + + # save file + currentPlayList = [] + _savePlaylist( k+".json" ) + currentPlayList = [] + + pass + + + + +def _savePlaylist( playlistname ): + global currentPlayList + filepath = Path("playlists/{}".format(playlistname)) + with filepath.open("w", encoding ="utf-8") as f: + f.write(json.dumps(currentPlayList, indent=4, sort_keys=True)) + return(True) + + + + + +def action_savePlaylist( name=False ): + global current_playlist_name + playlist_name = name if name else current_playlist_name + if not playlist_name : + _err("No name found.") + return False + try: + _savePlaylist(playlist_name) + print( bcolors.OKBLUE + "\nSaved as '{}'.\n".format(playlist_name) + bcolors.ENDC) + except Exception as e: + print("woops:{}".format(e)) + return False + + + + + +def action_commandHelp(): + global playlistsDir + + # iterate through files + file_list=[] + for folder in ["generators","filters","exports"]: + p = Path("./"+folder) + for plFile in Path("./"+folder).iterdir() : + if re.match("^.*py$",plFile.name): + file_list.append(os.path.join(folder,plFile.name)) + print("\n Id\tFile") + for k,filename in enumerate(file_list): + print(" {}\t{}".format(k,filename)) + print("\nChoose a file:") + k = int(input()) + print("\n-----------------------------------------------\n") + subprocess.run("python3 "+file_list[k]+" -h", shell=True, executable='/bin/bash') + print("\n-----------------------------------------------\n") + + + + +def _setKey( laser=0, scene=0 ): + global environ + laser = laser if laser else environ["REDIS_LASER"] + scene = scene if scene else environ["REDIS_SCENE"] + new_key = "/pl/{}/{}".format(scene,laser) + environ["REDIS_KEY"] = new_key + print("Sending new key '{}'".format(new_key)) + + +def action_laserId(): + k = int(ask("Enter the LJ Laser id [0-3]")) + _setKey( laser = k ) + + + +def action_laserScene(): + k = int(ask("Enter the LJ Scene id [0-3]")) + _setKey( scene = k ) + + + +def action_killPid(): + print("Enter pid to kill") + kill_pid = input() + subprocess.run("pkill -9 -s $(awk '{print $6}' /proc/$kill_pid/stat)", shell=True,executable='/bin/bash', env={"kill_pid":kill_pid}) + + +def action_quit(): + print("Quit [Y/n]?") + quit = inkey() + if quit != "n": + _kill(process) + sys.exit(1) + +environ = { +# "REDIS_IP" : "127.0.0.1", + "REDIS_IP" : "192.168.2.44", + "REDIS_PORT" : "6379", + "REDIS_KEY" : "/pl/0/0", + "REDIS_SCENE" : "0", + "REDIS_LASER" : "0" +} diff --git a/clitools/runner_midi.py b/clitools/runner_midi.py new file mode 100644 index 0000000..ecf4956 --- /dev/null +++ b/clitools/runner_midi.py @@ -0,0 +1,12 @@ +#!/usr/bin/python3 + +import sys +import os +import signal +import subprocess +import time +import tty,termios +import re +import json +from pathlib import Path +import runner_lib as runner From 75b3ae93a2ffdad8de49d658a06f2b0b86ee0d2b Mon Sep 17 00:00:00 2001 From: alban Date: Fri, 9 Oct 2020 18:30:28 +0200 Subject: [PATCH 2/2] [fix] clitools changes --- clitools/filters/anaglyph.py | 6 +- clitools/generators/dummy.py | 38 +++++- clitools/generators/example.py | 0 clitools/generators/redilysis_lines.py | 174 +++++++++++++++++++++++++ clitools/generators/tunnel.py | 0 5 files changed, 213 insertions(+), 5 deletions(-) mode change 100644 => 100755 clitools/generators/dummy.py mode change 100644 => 100755 clitools/generators/example.py create mode 100755 clitools/generators/redilysis_lines.py mode change 100644 => 100755 clitools/generators/tunnel.py diff --git a/clitools/filters/anaglyph.py b/clitools/filters/anaglyph.py index b67dc3e..93a8eb6 100755 --- a/clitools/filters/anaglyph.py +++ b/clitools/filters/anaglyph.py @@ -66,9 +66,9 @@ red = (41,24,24) white = (95,95,95) blue = (0,41,64) -red = (86,0,0) -blue = (0,55,86) -white = (125,125,125) +red = (255,0,0) +blue = (0,255,255) +white = (255,255,255) def anaglyph( pl ): debug(name,'--------------- new loop ------------------') diff --git a/clitools/generators/dummy.py b/clitools/generators/dummy.py old mode 100644 new mode 100755 index cf9059d..6a94eb0 --- a/clitools/generators/dummy.py +++ b/clitools/generators/dummy.py @@ -38,11 +38,45 @@ fps=args.fps verbose=args.verbose optimal_looptime = 1 / fps debug(name+" optimal looptime "+str(optimal_looptime)) +color = 65280 +square = [[100.0, 100.0, color], [100.0, 500.0, color], [500.0, 500.0, color], [500.0, 100.0, color], [100.0, 100.0, color]] +line =[] +for i in range(00,800,int(800/120)): + line.append([i, 400, color]) +square = [[100.0, 100.0, color], [100.0, 500.0, color], [500.0, 500.0, color], [500.0, 100.0, color], [100.0, 100.0, color]] +mire = [ + [600,600,0], + [600,600,color], + [700,600,color], + [700,700,color], + [600,700,color], + [600,600,color], + [100,100,0], + [100,100,color], + [200,100,color], + [200,200,color], + [100,200,color], + [100,100,color], + [0,0,0], + [0,0,color], + [800,0,color], + [800,800,color], + [0,800,color], + [0,0,color], + [350,400,0], + [350,400,color], + [450,400,color], + [400,350,0], + [400,350,color], + [400,450,color], +] + +shape = mire + while True: start = time.time() - print("[[100.0, 100.0, 65280], [100.0, 500.0, 65280], [500.0, 500.0, 65280], [500.0, 100.0, 65280], [100.0, 100.0, 65280]]", flush=True); - #print("[[100.0, 100.0, 65280], [110.0, 500.0, 65280], [510.0, 500.0, 65280], [510.0, 100.0, 65280], [100.0, 110.0, 65280]]", flush=True); + print(shape, flush=True); looptime = time.time() - start if( looptime < optimal_looptime ): time.sleep( optimal_looptime - looptime) diff --git a/clitools/generators/example.py b/clitools/generators/example.py old mode 100644 new mode 100755 diff --git a/clitools/generators/redilysis_lines.py b/clitools/generators/redilysis_lines.py new file mode 100755 index 0000000..123f487 --- /dev/null +++ b/clitools/generators/redilysis_lines.py @@ -0,0 +1,174 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +redilysis_lines +v0.1.0 + +Add a line on every frame and scroll + +see https://git.interhacker.space/teamlaser/redilysis for more informations +about the redilysis project + +LICENCE : CC + +by cocoa + + +''' +from __future__ import print_function +import argparse +import ast +import os +import math +import random +import redis +import sys +import time +name = "generator::redilysis_lines" + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) +def msNow(): + return time.time() + +CHAOS = 1 +REDIS_FREQ = 33 + +# General Args +argsparser = argparse.ArgumentParser(description="Redilysis filter") +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose") +# Redis Args +argsparser.add_argument("-i","--ip",help="IP address of the Redis server ",default="127.0.0.1",type=str) +argsparser.add_argument("-p","--port",help="Port of the Redis server ",default="6379",type=str) +argsparser.add_argument("-F","--redis-freq",help="Query Redis every x (in milliseconds). Default:{}".format(REDIS_FREQ),default=REDIS_FREQ,type=int) +# General args +argsparser.add_argument("-n","--nlines",help="number of lines on screen",default=60,type=int) +argsparser.add_argument("-x","--centerX",help="geometrical center X position",default=400,type=int) +argsparser.add_argument("-y","--centerY",help="geometrical center Y position",default=400,type=int) +argsparser.add_argument("-W","--max-width",help="geometrical max width",default=800,type=int) +argsparser.add_argument("-H","--max-height",help="geometrical max height",default=800,type=int) +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) + +args = argsparser.parse_args() +verbose = args.verbose +ip = args.ip +port = args.port +fps = args.fps +centerX = args.centerX +centerY = args.centerY +redisFreq = args.redis_freq / 1000 +maxWidth = args.max_width +maxHeight = args.max_height +nlines = args.nlines +optimal_looptime = 1 / fps + +redisKeys = ["spectrum_120","spectrum_10"] +debug(name,"Redis Keys:{}".format(redisKeys)) +redisData = {} +redisLastHit = msNow() - 99999 +r = redis.Redis( + host=ip, + port=port) + +white = 16777215 +lineList = [] + +scroll_speed = int(maxHeight / nlines ) +line_length = int(maxWidth / 10) +line_pattern = [] + +def rgb2int(rgb): + #debug(name,"::rgb2int rbg:{}".format(rgb)) + return int('0x%02x%02x%02x' % tuple(rgb),0) + +def spectrum_10( ): + delList = [] + spectrum = ast.literal_eval(redisData["spectrum_10"]) + debug( name, "spectrum:{}".format(spectrum)) + # scroll lines + for i,line in enumerate(lineList): + skip_line = False + new_y = int(line[0][1] + scroll_speed) + if( new_y >= maxHeight ): + debug(name,"{} > {}".format(new_y,maxHeight)) + debug(name,"delete:{}".format(i)) + delList.append(i) + continue + + for j,point in enumerate(line): + line[j][1] = new_y + lineList[i] = line + + for i in delList: + del lineList[i] + + # new line + currentLine = [] + for i in range(0,10): + x = int(i * line_length) + y = 0 + # get frequency level + level = spectrum[i] + # get color + comp = int(255*level) + color = rgb2int( (comp,comp,comp)) + # new point + currentLine.append( [x,y,color] ) + + # add line to list + lineList.append( currentLine) + + +def refreshRedis(): + global redisLastHit + global redisData + # Skip if cache is sufficent + diff = msNow() - redisLastHit + if diff < redisFreq : + #debug(name, "refreshRedis not updating redis, {} < {}".format(diff, redisFreq)) + pass + else: + #debug(name, "refreshRedis updating redis, {} > {}".format(diff, redisFreq)) + redisLastHit = msNow() + for key in redisKeys: + redisData[key] = r.get(key).decode('ascii') + #debug(name,"refreshRedis key:{} value:{}".format(key,redisData[key])) + # Only update the TTLs + if 'bpm' in redisKeys: + redisData['bpm_pttl'] = r.pttl('bpm') + #debug(name,"refreshRedis key:bpm_ttl value:{}".format(redisData["bpm_pttl"])) + #debug(name,"redisData:{}".format(redisData)) + return True + +def linelistToPoints( lineList ): + pl = [] + for i,line in enumerate(lineList): + # add a blank point + pl.append([ line[0][0], line[0][1], 0 ]) + # append all the points of the line + pl += line + #debug(name,"pl:{}".format(pl)) + debug(name,"pl length:{}".format(len(pl))) + return pl + +try: + while True: + refreshRedis() + start = time.time() + # Do the thing + pointsList = spectrum_10() + print( linelistToPoints(lineList), flush=True ) + looptime = time.time() - start + # debug(name+" looptime:"+str(looptime)) + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + # debug(name+" micro sleep:"+str( optimal_looptime - looptime)) +except EOFError: + debug(name+" break")# no more information + diff --git a/clitools/generators/tunnel.py b/clitools/generators/tunnel.py old mode 100644 new mode 100755