diff --git a/poetry.lock b/poetry.lock index a573170..d61a97f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -387,6 +387,37 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "psutil" +version = "6.1.1" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +groups = ["main"] +files = [ + {file = "psutil-6.1.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9ccc4316f24409159897799b83004cb1e24f9819b0dcf9c0b68bdcb6cefee6a8"}, + {file = "psutil-6.1.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ca9609c77ea3b8481ab005da74ed894035936223422dc591d6772b147421f777"}, + {file = "psutil-6.1.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8df0178ba8a9e5bc84fed9cfa61d54601b371fbec5c8eebad27575f1e105c0d4"}, + {file = "psutil-6.1.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:1924e659d6c19c647e763e78670a05dbb7feaf44a0e9c94bf9e14dfc6ba50468"}, + {file = "psutil-6.1.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:018aeae2af92d943fdf1da6b58665124897cfc94faa2ca92098838f83e1b1bca"}, + {file = "psutil-6.1.1-cp27-none-win32.whl", hash = "sha256:6d4281f5bbca041e2292be3380ec56a9413b790579b8e593b1784499d0005dac"}, + {file = "psutil-6.1.1-cp27-none-win_amd64.whl", hash = "sha256:c777eb75bb33c47377c9af68f30e9f11bc78e0f07fbf907be4a5d70b2fe5f030"}, + {file = "psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8"}, + {file = "psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377"}, + {file = "psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003"}, + {file = "psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160"}, + {file = "psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3"}, + {file = "psutil-6.1.1-cp36-cp36m-win32.whl", hash = "sha256:384636b1a64b47814437d1173be1427a7c83681b17a450bfc309a1953e329603"}, + {file = "psutil-6.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8be07491f6ebe1a693f17d4f11e69d0dc1811fa082736500f649f79df7735303"}, + {file = "psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53"}, + {file = "psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649"}, + {file = "psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5"}, +] + +[package.extras] +dev = ["abi3audit", "black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"] +test = ["pytest", "pytest-xdist", "setuptools"] + [[package]] name = "pyyaml" version = "6.0.2" @@ -554,4 +585,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.1" python-versions = "^3.11" -content-hash = "71a1f02c9a8b16892110f2c82cbeb2b8a831cd016c56b27446d5eedcaa2a5971" +content-hash = "135cf9001d97689d659830abaa919636d20e76a494585757f9b59fbe9ca3adb5" diff --git a/pyproject.toml b/pyproject.toml index a1ac4f5..29fc418 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ spidev = "^3.6" numpy = "^2.2.1" pillow = "^11.1.0" pyyaml = "^6.0.2" +psutil = "^6.1.1" [tool.poetry.group.dev.dependencies] diff --git a/requirements.txt b/requirements.txt index 973aee7..1952c91 100644 --- a/requirements.txt +++ b/requirements.txt @@ -225,6 +225,24 @@ pillow==11.1.0 ; python_version >= "3.11" and python_version < "4.0" \ --hash=sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71 \ --hash=sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9 \ --hash=sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761 +psutil==6.1.1 ; python_version >= "3.11" and python_version < "4.0" \ + --hash=sha256:018aeae2af92d943fdf1da6b58665124897cfc94faa2ca92098838f83e1b1bca \ + --hash=sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377 \ + --hash=sha256:1924e659d6c19c647e763e78670a05dbb7feaf44a0e9c94bf9e14dfc6ba50468 \ + --hash=sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3 \ + --hash=sha256:384636b1a64b47814437d1173be1427a7c83681b17a450bfc309a1953e329603 \ + --hash=sha256:6d4281f5bbca041e2292be3380ec56a9413b790579b8e593b1784499d0005dac \ + --hash=sha256:8be07491f6ebe1a693f17d4f11e69d0dc1811fa082736500f649f79df7735303 \ + --hash=sha256:8df0178ba8a9e5bc84fed9cfa61d54601b371fbec5c8eebad27575f1e105c0d4 \ + --hash=sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160 \ + --hash=sha256:9ccc4316f24409159897799b83004cb1e24f9819b0dcf9c0b68bdcb6cefee6a8 \ + --hash=sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003 \ + --hash=sha256:c777eb75bb33c47377c9af68f30e9f11bc78e0f07fbf907be4a5d70b2fe5f030 \ + --hash=sha256:ca9609c77ea3b8481ab005da74ed894035936223422dc591d6772b147421f777 \ + --hash=sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5 \ + --hash=sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53 \ + --hash=sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649 \ + --hash=sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8 pyyaml==6.0.2 ; python_version >= "3.11" and python_version < "4.0" \ --hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \ --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \ diff --git a/spotiplayer_pi/cmap.py b/spotiplayer_pi/cmap.py new file mode 100644 index 0000000..5347b9c --- /dev/null +++ b/spotiplayer_pi/cmap.py @@ -0,0 +1,65 @@ +cmap = [ + [65, 153, 181], + [75, 164, 177], + [87, 178, 171], + [99, 191, 165], + [112, 198, 164], + [129, 204, 164], + [142, 209, 164], + [158, 216, 164], + [172, 221, 163], + [186, 227, 160], + [199, 232, 158], + [211, 237, 155], + [225, 243, 152], + [232, 246, 156], + [238, 248, 165], + [243, 250, 173], + [249, 252, 182], + [254, 254, 190], + [254, 248, 179], + [254, 241, 167], + [254, 234, 157], + [254, 227, 145], + [253, 220, 135], + [253, 208, 125], + [253, 198, 117], + [253, 186, 107], + [253, 174, 97], + [251, 162, 91], + [249, 147, 84], + [247, 134, 78], + [245, 119, 71], + [242, 107, 67], + [235, 96, 70], + [228, 85, 73], + [222, 75, 75], + [214, 64, 78], + [205, 53, 77], + [192, 39, 74], + [181, 27, 71], + [168, 12, 68], +] + +cmap2 = [ + [183, 29, 72], + [209, 58, 78], + [225, 81, 74], + [240, 103, 68], + [247, 131, 77], + [251, 165, 92], + [253, 190, 110], + [253, 214, 130], + [254, 232, 153], + [254, 247, 177], + [248, 252, 181], + [236, 247, 162], + [218, 240, 154], + [190, 229, 160], + [161, 217, 164], + [126, 203, 164], + [95, 187, 167], + [71, 159, 179], + [52, 132, 187], + [73, 105, 174], +] diff --git a/spotiplayer_pi/main.py b/spotiplayer_pi/main.py index e1625dc..d3bf954 100644 --- a/spotiplayer_pi/main.py +++ b/spotiplayer_pi/main.py @@ -1,5 +1,6 @@ from PIL import Image, ImageDraw, ImageFont import textwrap +from collections import deque import time import requests from io import BytesIO @@ -9,24 +10,39 @@ import yaml from typing import Dict import subprocess +import numpy as np + from spotiplayer_pi.lib import LCD_2inch from spotiplayer_pi.api import Api +from spotiplayer_pi.cmap import cmap, cmap2 def get_cpu_temp(): try: - result = subprocess.run( - ["vcgencmd", "measure_temp"], capture_output=True, text=True, check=True + return float( + subprocess.run( + ["vcgencmd", "measure_temp"], capture_output=True, text=True, check=True + ) + .stdout.split("=")[1] + .replace("'C", "") ) - temp_str = result.stdout.strip() - temp_value = temp_str.split("=")[1].replace("'C", "") - return float(temp_value) - except subprocess.CalledProcessError as e: - warnings.warn(f"Error executing vcgencmd: {e}") - return "None" - except (IndexError, ValueError) as e: - warnings.warn(f"Error parsing temperature: {e}") - return "None" + except (subprocess.CalledProcessError, IndexError, ValueError) as e: + warnings.warn(f"Error getting CPU temp: {e}") + return None + + +def get_mem_util(): + try: + return int( + subprocess.run( + ["bash", "-c", "free -m | awk '/^Mem:/ {print $3}'"], + capture_output=True, + text=True, + check=True, + ).stdout.strip() + ) + except Exception as e: + warnings.warn(f"Error getting Memory utilization: {e}") def parse_config(path="spotiplayer_pi/config.yaml"): @@ -45,11 +61,12 @@ def display_loop(api: Api, cfg: Dict): bg = Image.new("RGB", (d.height, d.width), (0, 0, 0)) d.ShowImage(bg) + Font00 = ImageFont.truetype("Font/GothamBold.ttf", 10) Font0 = ImageFont.truetype("Font/GothamBold.ttf", 14) Font1 = ImageFont.truetype("Font/GothamMedium.ttf", 18) Font1b = ImageFont.truetype("Font/GothamBold.ttf", 19) - Font2 = ImageFont.truetype("Font/GothamMedium.ttf", 20) - Font3 = ImageFont.truetype("Font/GothamMedium.ttf", 36) + Font2 = ImageFont.truetype("Font/GothamBold.ttf", 23) + Font3 = ImageFont.truetype("Font/GothamMedium.ttf", 52) unavailable_img = Image.open("imgs/unavailable.jpg") error_img = Image.new("RGB", (240, 320), color=(255, 0, 0)) @@ -57,20 +74,7 @@ def display_loop(api: Api, cfg: Dict): draw.text((150, 150), "Error :(", font=Font1, fill=cfg["color_theme"]["text"]) del draw - spoti_logo = Image.open("imgs/logo64.jpg") - qr = Image.open("imgs/qr.jpg") - spoti_logo = spoti_logo.resize((64, 64)) not_playing_img = Image.new("RGB", (320, 240), (0, 0, 0)) - not_playing_img.paste(spoti_logo, (25, 40)) - not_playing_img.paste(qr, (120, 40)) - draw = ImageDraw.Draw(not_playing_img) - draw.text( - (112, 15), - "Connect to speakers", - font=Font2, - fill=(255, 255, 255), - ) - del spoti_logo, qr, draw last_api_call = 0 last_auth_refresh = 0 @@ -78,6 +82,8 @@ def display_loop(api: Api, cfg: Dict): last_track = None data = None auth_interval = 0 + buffer_temp = deque([None] * 320, maxlen=320) + buffer_mem = deque([None] * 320, maxlen=320) while True: if time.time() - last_auth_refresh >= auth_interval: @@ -93,33 +99,94 @@ def display_loop(api: Api, cfg: Dict): d.ShowImage(error_img) elif data == "not-playing": + pixels = not_playing_img.load() draw = ImageDraw.Draw(not_playing_img) + draw.rectangle( + [(0, 100), (320, 240)], fill=(0, 0, 0) + ) # refresh stats bg + draw.rectangle([(95, 25), (250, 98)], fill=(0, 0, 0)) # refresh time bg + + # Draw Temp + buffer_temp.append(get_cpu_temp()) + temp_values = [x for x in buffer_temp if x is not None] + min_temp, max_temp = min(temp_values), max(temp_values) + if len(temp_values) >= 2 and max_temp != min_temp: + temp_data_scaled = [ + (idx, ((x - min_temp) / (max_temp - min_temp)) * 40) + for idx, x in enumerate(buffer_temp) + if x is not None + ] + for idx, val in temp_data_scaled: + color_idx = max( + 0, min(int(buffer_temp[idx]) - 40, len(cmap) - 1) + ) + pixels[idx, 150 - val] = tuple(cmap[color_idx]) + + draw.rectangle( + [(0, 149), (3, 151)], + fill=tuple( + cmap[max(0, min(len(cmap) - 1, int(min_temp) - 40))] + ), + ) # min + draw.rectangle( + [(0, 109), (3, 111)], + fill=tuple( + cmap[max(0, min(len(cmap) - 1, int(max_temp) - 40))] + ), + ) # max + draw.text((5, 150), f"{int(min_temp)}°", font=Font0) + draw.text((5, 100), f"{int(max_temp)}°", font=Font0) + + # Draw Mem usage + buffer_mem.append(get_mem_util()) + mem_data = [x for x in buffer_mem if x is not None] + group_size = 16 + n_bins = len(mem_data) // group_size + bins = np.arange(len(mem_data)) // group_size + if n_bins > 0: + binned_data = [ + np.mean(np.array(mem_data)[bins == i]) for i in range(n_bins) + ] + min_bin, max_bin = np.argmin(binned_data), np.argmax(binned_data) + for idx_, bin_i in enumerate(binned_data): + idx = len(binned_data) - idx_ - 1 + draw.rectangle( + [ + (321 - ((idx + 1) * 16), 240), + (319 - (idx * 16), 240 - bin_i // 65), + ], + fill=tuple(cmap2[idx]), + ) + if idx_ in [min_bin, max_bin]: + col = 0 if idx_ == max_bin else n_bins - 1 + draw.text( + (320 - ((idx + 1) * 16), 230 - bin_i // 65), + f"{bin_i / 1024:.2f}Gib", + font=Font00, + fill=tuple(cmap2[col]), + ) + + # Draw Time current_time = datetime.now() - draw.rectangle([(00, 120), (119, 190)], fill=(0, 0, 0)) - offset = 10 if current_time.hour < 9 else 0 + offset = 15 if current_time.hour < 10 and current_time.hour != 0 else 0 draw.text( - (10 + offset, 120), - f"{current_time.hour}:{current_time.minute:02d}", + (100 + offset, 30), + current_time.strftime("%-I:%M"), font=Font3, fill=(255, 255, 255), ) draw.text( - (25, 152), - current_time.strftime("%a %d"), + (120, 78), + current_time.strftime("%a, %d"), font=Font2, fill=(255, 255, 255), ) - draw.text( - (20, 175), - f"CPU: {get_cpu_temp():.2f}°", - font=Font0, - fill=(255, 255, 255), - ) + + d.ShowImage(not_playing_img) + time.sleep(0.1) if current_mode != 0: current_mode = 0 print("Standby mode") - d.ShowImage(not_playing_img) - time.sleep(cfg["api_interval"]) elif type(data) == dict: current_mode = 1