diff --git a/controller/cava.conf b/controller/cava.conf index 3b49584..4ab8e91 100644 --- a/controller/cava.conf +++ b/controller/cava.conf @@ -1,138 +1,20 @@ -## Configuration file for CAVA. Default values are commented out. Use either ';' or '#' for commenting. - - [general] - -# Smoothing mode. Can be 'normal', 'scientific' or 'waves'. DEPRECATED as of 0.6.0 -; mode = normal - -# Accepts only non-negative values. framerate = 60 - -# 'autosens' will attempt to decrease sensitivity if the bars peak. 1 = on, 0 = off -# new as of 0.6.0 autosens of low values (dynamic range) -# 'overshoot' allows bars to overshoot (in % of terminal height) without initiating autosens. DEPRECATED as of 0.6.0 -; autosens = 1 -; overshoot = 20 - -# Manual sensitivity in %. Autosens must be turned off for this to take effect. -# 200 means double height. Accepts only non-negative values. -sensitivity = 1000 - -# The number of bars (0-200). 0 sets it to auto (fill up console). -# Bars' width and space between bars in number of characters. -;bars = 128 bars = 2 -; bar_width = 2 -; bar_spacing = 1 - - -# Lower and higher cutoff frequencies for lowest and highest bars -# the bandwidth of the visualizer. -# Note: there is a minimum total bandwidth of 43Mhz x number of bars. -# Cava will automatically increase the higher cutoff if a too low band is specified. -; lower_cutoff_freq = 50 -; higher_cutoff_freq = 10000 - - - -[input] - -# Audio capturing method. Possible methods are: 'pulse', 'alsa' or 'fifo'. -# Defaults to 'pulse', 'alsa' or 'fifo', in that order, dependent on what support cava was built with. -# -# All input methods uses the same config variable 'source' -# to define where it should get the audio. -# -# For pulseaudio 'source' will be the source. Default: 'auto', which uses the monitor source of the default sink -# (all pulseaudio sinks(outputs) have 'monitor' sources(inputs) associated with them). -# -# For alsa 'source' will be the capture device. -# For fifo 'source' will be the path to fifo-file. -; method = pulse -; source = 19 - -; method = alsa -; source = hw:Loopback,1 - -; method = fifo -; source = /tmp/mpd.fifo - - [output] - -# Ouput method. Can be 'ncurses', 'noncurses' or 'raw'. -# 'noncurses' is for systems that does not suport ncurses. -# 'raw' is a 16 bit data stream of the bar heights that can be used to send to other applications. -# 'raw' defaults to 200 bars, which can be adjusted in the 'bars' option above. method = raw - -# Visual channels. Can be 'stereo' or 'mono'. -# 'stereo' mirrors both channels with low frequencies in center. -# 'mono' averages both channels and outputs left to right lowest to highest frequencies. -; channels = stereo - -# Raw output target. A fifo will be created if target does not exist. -; raw_target = /dev/stdout - -# Raw data format. Can be 'binary' or 'ascii'. data_format = ascii - -# Binary bit format, can be '8bit' (0-255) or '16bit' (0-65530). -; bit_format = 8bit - -# Ascii max value. In 'ascii' mode range will run from 0 to value specified here ascii_max_range = 1000 -# Ascii delimiters. In ascii format each bar and frame is separated by a delimiters. -# Use decimal value in ascii table (i.e. 59 = ';' and 10 = '\n' (line feed)). -; bar_delimiter = 59 -; frame_delimiter = 10 - - - [color] - -# Colors can be one of seven predefined: black, blue, cyan, green, magenta, red, white, yellow. -# Or defined by hex code '#xxxxxx' (hex code must be within ''). User defined colors requires a -# terminal that can change color definitions such as Gnome-terminal or rxvt. -; background = black foreground = red -# Gradient mode, only hex defined colors are supported, background must also be defined in hex -# or remain commented out. 1 = on, 0 = off. Warning: for certain terminal emulators cava will -# not able to restore color definitions on exit, simply restart your terminal to restore colors. -; gradient = 0 -; gradient_color_1 = '#0099ff' -; gradient_color_2 = '#ff3399' - - - [smoothing] - -# Percentage value for integral smoothing. Takes values from 0 - 100. -# Higher values means smoother, but less precise. 0 to disable. integral = 20 - -# Disables or enables the so-called "Monstercat smoothing" with or without "waves". Set to 0 to disable. -; monstercat = 1 -; waves = 3; - -# Set gravity percentage for "drop off". Higher values means bars will drop faster. -# Accepts only non-negative values. 50 means half gravity, 200 means double. Set to 0 to disable "drop off". gravity = 200 - -# In bar height, bars that would have been lower that this will not be drawn. -; ignore = 0 - - [eq] - -# This one is tricky. You can have as much keys as you want. -# Remember to uncomment more then one key! More keys = more precision. -# Look at readme.md on github for further explanations and examples. 1 = 1 # bass 2 = 0 3 = 0 # midtone diff --git a/controller/gui.py b/controller/gui.py new file mode 100644 index 0000000..4bbe44c --- /dev/null +++ b/controller/gui.py @@ -0,0 +1,11 @@ +from flask import Flask, render_template + +app = Flask(__name__) + +@app.route('/') +def home(): + return render_template('index.html') + +@app.route('/set/') +def set(color): + return f'{color}' diff --git a/controller/main.bin b/controller/main.bin new file mode 100755 index 0000000..86d3f7c Binary files /dev/null and b/controller/main.bin differ diff --git a/controller/main.py b/controller/main.py index afe78b2..3f55645 100755 --- a/controller/main.py +++ b/controller/main.py @@ -5,6 +5,7 @@ import asyncio import pickle import re import sys +import subprocess async def scan_for_pi() -> dict(): ip = socket.gethostbyname(socket.gethostname()) @@ -71,9 +72,62 @@ def set_pixels(color): pi.send(color) pi.disconnect() +def visualizer(color, amp_strength=0.6): + cava_config = ''' + [general] + framerate = 60 + bars = 2 + + [output] + method = raw + data_format = ascii + ascii_max_range = 1000 + + [color] + foreground = red + + [smoothing] + integral = 20 + gravity = 200 + + [eq] + 1 = 1 # bass + 2 = 0 + 3 = 0 # midtone + 4 = 0 + 5 = 0 # treble + ''' + + with open('/tmp/lc_cava.conf', 'w') as f: + f.write(cava_config) + + r = int(color[0:2], 16) + g = int(color[2:4], 16) + b = int(color[4:6], 16) + + cava = subprocess.Popen(["cava", "-p", "/tmp/lc_cava.conf"], stdout=subprocess.PIPE) + + for line in cava.stdout: + cava_value = int(line.decode().split(';')[0]) + amp_factor = amp_strength * ((cava_value / 500) - 1) + 1 + # send(rgb_to_hex(int(r * amp_factor), int(g * amp_factor), int(b * amp_factor))) + print(r,g,b, amp_factor, line) + color = f'{int(r * amp_factor):02x}{int(g * amp_factor):02x}{int(b * amp_factor):02x}' + print(color) + set_pixels(color) + + cava.stdout.close() + sed.stdout.close() + + def main(arg): arg = ('help') if len(arg) == 0 else arg - + # if len(arg) == 0: + # from gui import app + # import webview + # webview.create_window('LED Controller', app) + # webview.start() + match arg[0]: case "set": if len(arg) < 2: @@ -81,6 +135,9 @@ def main(arg): return set_pixels(arg[1]) + case "off": + set_pixels('000000') + case "search": ip_list = asyncio.run(scan_for_pi()) pi_list = [PI(ip) for ip in ip_list] @@ -92,10 +149,13 @@ def main(arg): for pi in pi_list: print(pi) case "music": - pass + if len(arg) < 2: + print("color argument missing") + return + visualizer(arg[1]) case _: - print("lc [help|set|search|list]") + print("lc [help|set|off|search|list]") diff --git a/web/index.html b/controller/templates/index.html similarity index 100% rename from web/index.html rename to controller/templates/index.html diff --git a/test.html b/test.html new file mode 100644 index 0000000..2131334 --- /dev/null +++ b/test.html @@ -0,0 +1,6 @@ + diff --git a/web/app.py b/web/app.py deleted file mode 100644 index 67fc9bd..0000000 --- a/web/app.py +++ /dev/null @@ -1,36 +0,0 @@ -import socket -import asyncio -from flask import Flask, render_template - -app = Flask(__name__) - -async def scan_for_pi() -> dict(): - ip = socket.gethostbyname(socket.gethostname()) - baseIP = '.'.join(ip.split(".")[:3]) - - def scan(ip: str) -> str: - try: - with socket.socket() as s: - s.settimeout(0.5) - s.connect((ip, 5000)) - s.send("PING".encode()) - data = s.recv(1024).decode() - if data == "PONG": - print("found:", ip) - return ip - except OSError: - pass - - async def scan_async(ip: str) -> str: - return await asyncio.to_thread(scan, ip) - - pi_list = await asyncio.gather(*[scan_async(baseIP + "." + str(i)) for i in range(255)]) - return [pi for pi in pi_list if pi is not None] - -@app.route('/') -def home(): - return render_template('index.html') - -@app.route('/set/') -def set(color): - return f'{color}' diff --git a/web/controller.py b/web/controller.py deleted file mode 100755 index d9285c7..0000000 --- a/web/controller.py +++ /dev/null @@ -1,392 +0,0 @@ -#!/usr/bin/python3 -import socket -import os -import asyncio -import click -import pickle -import re -import webview - -# function generate fibonacci sequence -def fibonacci(n): - if n == 0: - return 0 - elif n == 1: - return 1 - else: - return fibonacci(n - 1) + fibonacci(n - 2) - -async def scan_for_pi() -> dict(): - ip = socket.gethostbyname(socket.gethostname()) - baseIP = '.'.join(ip.split(".")[:3]) - - def scan(ip: str) -> str: - try: - with socket.socket() as s: - s.settimeout(0.5) - s.connect((ip, 5000)) - s.send("PING".encode()) - data = s.recv(1024).decode() - if data == "PONG": - print("found:", ip) - return ip - except OSError: - pass - - async def scan_async(ip: str) -> str: - return await asyncio.to_thread(scan, ip) - - pi_list = await asyncio.gather(*[scan_async(baseIP + "." + str(i)) for i in range(255)]) - return [pi for pi in pi_list if pi is not None] - -class PI: - def __init__(self, ip, port=5000): - self.ip = ip - self.port = port - self.connected = False - self.socket = socket.socket() - - def __str__(self): - return self.ip - - def connect(self): - self.socket.connect((self.ip, self.port)) - - def send(self, data): - self.socket.send(data.encode()) - - def disconnect(self): - self.socket.close() - -def load_config(path=os.path.expanduser("~/.config/lc/lc.conf")): - try: - with open(path, 'rb') as f: - return [PI(ip) for ip in pickle.load(f)] - except FileNotFoundError: - print("Config does not exist") - exit() - -def save_config(obj, path=os.path.expanduser("~/.config/lc/lc.conf")): - os.makedirs(path[::-1].split('/',1)[-1][::-1], exist_ok=True) - with open(path, 'wb') as f: - pickle.dump(list([o.ip for o in obj]), f) - -def set_pixels(color): - if not re.match("[0-9a-f]{6}$", color): - print(f"{color} not a valid hex color code") - return - pi_list = load_config() - for pi in pi_list: - pi.connect() - pi.send(color) - pi.disconnect() - -@click.command() -@click.argument("arg", nargs=-1) -# @click.option("-s", help="Set HEX Color: -s ffffff") -# @click.option("-v", help="Set HEX Color as base visualizer color: -v ffffff") -def main(arg): - if arg == (): - # proc = subprocess.Popen(["python", "-m", "streamlit", "run", "/var/lib/lc/gui.py", "--server.headless", "True", "--theme.base", "dark", "--theme.primaryColor", "#9f0000", "--theme.backgroundColor", "#181a1b", "--theme.secondaryBackgroundColor", "#1f2123", "--theme.textColor", "#c4c0b8"], stdout=subprocess.PIPE) - # for line in proc.stdout: - # if line == b' You can now view your Streamlit app in your browser.\n': - # break - - import streamlit.web.bootstrap as bootstrap - from streamlit import config - import gui as gui - - di = {'global_disableWatchdogWarning': None, 'global_showWarningOnDirectExecution': None, 'global_developmentMode': None, 'global_logLevel': None, 'global_unitTest': None, 'global_suppressDeprecationWarnings': None, 'global_minCachedMessageSize': None, 'global_maxCachedMessageAge': None, 'global_dataFrameSerialization': None, 'logger_level': None, 'logger_messageFormat': None, 'logger_enableRich': None, 'client_caching': None, 'client_displayEnabled': None, 'client_showErrorDetails': None, 'runner_magicEnabled': None, 'runner_installTracer': None, 'runner_fixMatplotlib': None, 'runner_postScriptGC': None, 'runner_fastReruns': None, 'server_folderWatchBlacklist': None, 'server_fileWatcherType': None, 'server_cookieSecret': None, 'server_headless': True, 'server_runOnSave': None, 'server_allowRunOnSave': None, 'server_address': None, 'server_port': None, 'server_scriptHealthCheckEnabled': None, 'server_baseUrlPath': None, 'server_enableCORS': None, 'server_enableXsrfProtection': None, 'server_maxUploadSize': None, 'server_maxMessageSize': None, 'server_enableWebsocketCompression': None, 'browser_serverAddress': None, 'browser_gatherUsageStats': None, 'browser_serverPort': None, 'ui_hideTopBar': None, 'ui_hideSidebarNav': None, 'mapbox_token': None, 'deprecation_showfileUploaderEncoding': None, 'deprecation_showImageFormat': None, 'deprecation_showPyplotGlobalUse': None, 'theme_base': None, 'theme_primaryColor': None, 'theme_backgroundColor': None, 'theme_secondaryBackgroundColor': None, 'theme_textColor': None, 'theme_font': None} - - import multiprocessing - - def run(): - config.set_option('server.headless', True) - bootstrap.run(gui.__file__, 'streamlit run gui.py --server.headless True', [], di) - - p = multiprocessing.Process(target=run) - p.start() - - # import requests - - # while True: - # try: - # print(requests.get('http://localhost:8501')) - # except: - # continue - # print('asdf') - # break - # cli.main_run(str(gui.__file__)) - # from streamlit.web.server import Server - # server = Server(gui.__file__, 'streamlit run gui.py --server.headless True') - # import tornado.web - - # class MainHandler(tornado.web.RequestHandler): - # def get(self): - # self.write("Hello, world") - - # application = tornado.web.Application([ - # (r"/", ), ]) - # application.listen(8888) - - webview.create_window('LED Control', 'http://localhost:8501') - webview.start() - # proc.terminate() - exit() - - - match arg[0]: - case "help": - print("lc [help|set|search|list]") - - case "set": - if len(arg) < 2: - print("color argument missing") - set_pixels(arg[1]) - - case "search": - ip_list = asyncio.run(scan_for_pi()) - pi_list = [PI(ip) for ip in ip_list] - save_config(pi_list) - - case "list": - pi_list = load_config() - if pi_list is not None: - for pi in pi_list: - print(pi) - case "music": - pass - -if __name__ == '__main__': - main() - -# def helpmenu(): - # print("light controll\n") - # print("Options:") - # print("-h show help") - # print("-s set static color") - # print("-v visualizer") - # print("-i interactive interface") - # print("-a ambient light") - # print("-t test function (debug)") - -# def base_color(color): - # return [i // min(color) for i in color] - -# def visualizer(color, amp_strength=0.6): - # r,g,b = hex_to_rgb(color) - - # cava = subprocess.Popen(["cava", "-p", "/etc/lc/cava.conf"], stdout=subprocess.PIPE) - # sed = subprocess.Popen(["sed", "-u", "s/;.*;$//"], stdin=cava.stdout, stdout=subprocess.PIPE) - - # for line in sed.stdout: - # amp_factor = amp_strength * ((int(line) / 500) - 1) + 1 - # send(rgb_to_hex(int(r * amp_factor), int(g * amp_factor), int(b * amp_factor))) - # cava.stdout.close() - # sed.stdout.close() - -# def visualizer_cava_thread(): - # global volume_amp - - # cava = subprocess.Popen(["cava", "-p", "/etc/lc/cava.conf"], stdout=subprocess.PIPE) - # sed = subprocess.Popen(["sed", "-u", "s/;.*;$//"], stdin=cava.stdout, stdout=subprocess.PIPE) - - # for line in sed.stdout: - # volume_amp = int(line) - # print(volume_amp) - # cava.stdout.close() - # sed.stdout.close() - -# def amp_by_vol(color, amp_strength): - # global volume_amp - # # amp_strength in percentage - # amp_factor = amp_strength*((volume_amp/500)-1)+1 - # #print(amp_factor, color,[c*amp_factor for c in color], volume_amp) - # return [c*amp_factor for c in color] - -# def vibrant(r,g,b): - # intensity = 50 # usabel range 1-100 max:1000 - - # intensity = 1+intensity/1000 - # rgb = [r,g,b] - # #min_idx = rgb.index(min(rgb)) - # d = (r+g+b)/3 - # for c in range(3): - # if rgb[c] < d: - # rgb[c] = int(rgb[c]*(intensity**(rgb[c]-d))) - # elif rgb[c] > d: - # rgb[c] = int(rgb[c]*(-intensity**(-rgb[c]+d)+2)) - # if rgb[c] > 255: - # rgb[c] = 255 - # #rgb[min_idx] = int(rgb[min_idx]*(rgb[min_idx]/d)**2) - # return rgb - -# def ambient_light_thread(): - # r,g,b = 0,0,0 - # brighness = 1 - # active_color = '' - - # while True: - # # P-Regler - # r,g,b = [w+((y-w)*0.1) for y,w in zip((_r,_g,_b),(r,g,b))] - # if ((round(r),round(g),round(b)) == (_r,_g,_b)): - # active_color = '\033[0;32;40m' - # else: - # active_color = '\033[0;31;40m' - # r_out,g_out,b_out = amp_by_vol((r,g,b), 0.6) - # #r_out,g_out,b_out = r,g,b - # print(active_color, round(r),round(g),round(b), round(r_out),round(g_out),round(b_out), '\033[0;37;40m') - # send(rgb_to_hex(int(r_out*brighness),int(g_out*brighness),int(b_out*brighness))) - # time.sleep(0.01) - -# ups_counter = 0 -# start_time = time.time() - -# def ups(): - # global ups_counter - # global start_time - - # ups_counter += 1 - # time_d = time.time()-start_time - # ups = ups_counter/time_d - # print(ups) - -# def color_correction(r,g,b): - # amp = [1,1,0.8] - # threshold = 10 - # if r < threshold and g < threshold and b < threshold: - # return 0,0,0 - # return int(r*amp[0]), int(g*amp[1]), int(b*amp[2]) - -# def ambient_light(): - - # t1 = Thread(target=ambient_light_thread) - # t1.start() - - # t2 = Thread(target=visualizer_cava_thread) - # t2.start() - - # global _r,_g,_b - - # counter = 0 - # start_time = time.time() - # while True: - # # screenshot - - # # Xorg - # img = pyscreenshot.grab(backend="mss", childprocess=False, bbox=(1920,0,4480,1440)) - - # #Wayland - # #time.sleep(0.1) - # #cap = cv2.VideoCapture('/tmp/a') - # #count = cap.get(cv2.CAP_PROP_FRAME_COUNT) - # #cap.set(cv2.CAP_PROP_POS_FRAMES, count-1) - - # #ret, frame = cap.read() - - # #frame = cv2.cvtColor(frame,cv2.COLOR_BGR2RGB) - # #img = Image.fromarray(frame) - # #cap.release() - - # # find dominant color - # img.thumbnail((2,2)) - # r,g,b = img.getpixel((0, 0)) - # r,g,b = vibrant(r,g,b) - # _r,_g,_b = color_correction(r,g,b) - - # time.sleep(0.05) - - -# def rgb_to_hex(r,g,b): - # return "%02x%02x%02x" % (r,g,b) - -# def hex_to_rgb(hex): - # r = int(hex[0:2],16) - # g = int(hex[2:4],16) - # b = int(hex[4:6],16) - # return r,g,b - -# def test(): - # for i in range(256): - # h = rgb_to_hex(0,i,0) - # send(h) - # print(h) - # time.sleep(0.0) - -# def tui_main(scr, *args): - # # -- Perform an action with Screen -- - # scr.border(0) - # scr.addstr(5, 5, 'Hello from Curses!', curses.A_BOLD) - # scr.addstr(6, 5, 'Press q to close this screen', curses.A_NORMAL) - # scr.addstr(8, 5, '\u250C') - - # rgb = [0,0,0] - # color_selector = 0 - - # while True: - # status = '{},{},{} {}'.format(rgb[0], rgb[1], rgb[2], color_selector) - # scr.addstr(1, 1, status) - - # ch = scr.getch() - # if ch == ord('q'): - # break - # elif ch == ord('j'): - # if rgb[color_selector] > 0: - # rgb[color_selector] -= 1 - # send(rgb_to_hex(rgb[0], rgb[1], rgb[2])) - # elif ch == ord('k'): - # if rgb[color_selector] < 255: - # rgb[color_selector] += 1 - # send(rgb_to_hex(rgb[0], rgb[1], rgb[2])) - # elif ch == ord('l'): - # if color_selector < 3: - # color_selector += 1 - # elif ch == ord('h'): - # if color_selector > 0: - # color_selector -= 1 - -# def main(argv): - # if not sys.stdin.isatty(): - # connect() - # for volume in sys.stdin: - # volume = int(volume) - # hex_color = rgb_to_hex(volume,0,0) - # send(hex_color) - # sys.exit() - - # try: - # opts, args = getopt.getopt(argv, "s:v:ahti") - # except getopt.GetoptError: - # print(sys.argv[0], "invalid option") - # print("Try", sys.argv[0], "-h for help") - # sys.exit(1) - - # for opt, arg in opts: - # if opt == "-h": - # helpmenu() - # elif opt == "-s": - # connect() - # send(arg) - # disconnect() - # elif opt == "-a": - # connect() - # ambient_light() - # disconnect() - # elif opt == "-v": - # connect() - # visualizer(arg) - # disconnect() - # elif opt == "-t": - # connect() - # test() - # disconnect() - # elif opt == '-i': - # connect() - # curses.wrapper(tui_main) - # disconnect() - - # sys.exit() - -# if __name__ == "__main__": - - # main(sys.argv[1:])