FW_LED_System_Monitor/drawing.py

256 lines
9.2 KiB
Python
Raw Normal View History

2024-08-17 16:49:21 -04:00
# Built In Dependencies
import time
import math
import threading
2025-06-23 20:37:30 -04:00
import importlib.util
import sys
import os
import re
2024-08-17 16:49:21 -04:00
# Internal Dependencies
from commands import Commands, send_command
2025-06-23 20:37:30 -04:00
from patterns import lightning_bolt_bot, lightning_bolt_top, lookup_table, id_patterns
2024-08-17 16:49:21 -04:00
# External Dependencies
import numpy as np
import serial # pyserial
from serial.tools import list_ports
# Correct table orientation for visual orientation when drawn
for i in range(lookup_table.shape[0]):
lookup_table[i] = lookup_table[i].T
def spiral_index(fill_ratio):
return int(round(fill_ratio * 9.999999 - 0.5))
2025-06-23 20:37:30 -04:00
## App Draw Functions ##
# Takes up 15 rows, 7 columns, starting at y,1
# For bottom segment, the 16th row will be empty
def draw_spiral_vals(grid, cpu_values, fill_value, y):
y += 1
2024-08-17 16:49:21 -04:00
for i, v in enumerate(cpu_values):
column_number = i % 2
row_number = i // 2
fill_grid = lookup_table[spiral_index(v)]
2025-06-23 20:37:30 -04:00
grid[1+column_number*4:4+column_number*4, y+row_number*4:y+3+row_number*4] = fill_grid * fill_value
2024-08-17 16:49:21 -04:00
2025-06-23 20:37:30 -04:00
# Takes up 2 rows, 7 columns, starting at y, 1
def draw_memory(grid, memory_ratio, fill_value, y):
2024-08-17 16:49:21 -04:00
lit_pixels = 7 * 2 * memory_ratio
pixels_bottom = int(round(lit_pixels / 2))
pixels_top = int(round((lit_pixels - 0.49) / 2))
2025-06-23 20:37:30 -04:00
grid[1:1+pixels_top,y+1] = fill_value
grid[1:1+pixels_bottom,y+2] = fill_value
# Takes up 12 (top segment) or 13 (bottom segment) rows, 7 columns, starting at y,1
def draw_battery(grid, battery_ratio, battery_plugged, fill_value, y,
battery_low_thresh = 0.07, battery_low_flash_time = 2, charging_pulse_time = 3):
if y == 19: # Placement on bottom
bot = 33
num_rows = 13
lightning_bolt = lightning_bolt_bot
else: # Placement on top (y == 3)
bot = 16
num_rows = 12
lightning_bolt = lightning_bolt_top
bat_top = y + 1
bat_bot = bat_top + num_rows
lit_pixels = int(round(num_rows * 7 * battery_ratio))
2024-08-17 16:49:21 -04:00
pixels_base = lit_pixels // 7
remainder = lit_pixels % 7
if battery_ratio <= battery_low_thresh and not battery_plugged:
if time.time() % battery_low_flash_time * 2 < battery_low_flash_time: # This will flash the battery indicator if too low
return
for i in range(7):
pixels_col = pixels_base
if i < remainder:
pixels_col += 1
2025-06-23 20:37:30 -04:00
grid[i+1,bot-pixels_col:bot] = fill_value
2024-08-17 16:49:21 -04:00
if battery_plugged:
pulse_amount = math.sin(time.time() / charging_pulse_time)
2025-06-23 20:37:30 -04:00
grid[1:8,bat_top:bat_bot][lightning_bolt] -= np.rint(fill_value + 10 * pulse_amount).astype(int)
indices = grid[1:8,bat_top:bat_bot] < 0
grid[1:8,bat_top:bat_bot][indices] = -grid[1:8,bat_top:bat_bot][indices]
2024-08-17 16:49:21 -04:00
2025-06-23 20:37:30 -04:00
# Takes up 16 (top segment) or 17 (bottom segment) rows, 3 columns, starting at y,1
def draw_bar(grid, bar_ratio, bar_value, bar_x_offset = 1, y=0):
2024-08-17 16:49:21 -04:00
bar_width = 3
bar_height = 16
lit_pixels = int(round(bar_height * bar_width * bar_ratio))
pixels_base = lit_pixels // bar_width
remainder = lit_pixels % bar_width
for i in range(bar_width):
pixels_col = pixels_base
if i < remainder:
pixels_col += 1
2025-06-23 20:37:30 -04:00
if y == 16:
2024-08-17 16:49:21 -04:00
grid[bar_x_offset+i,33-pixels_col:33] = bar_value
else:
grid[bar_x_offset+i,1:1+pixels_col] = bar_value
2025-06-23 20:37:30 -04:00
## Border Draw Functions ##
# Draws a border around a 16 (top segment) or a 17 (bottom segment)
# x 9 grid, divided into a 2 x 4 grid. For the bottom segment,
# the last grid will have an extra row
def draw_8_x_8_grid(grid, border_value, y):
height = 16 if y == 0 else 17
grid[:, y] = border_value # Top
grid[:, y+height] = border_value # Bottom
grid[0, y:y+height] = border_value # Left
grid[8, y:y+height] = border_value # Right
grid[4, y:y+height] = border_value # Middle
# Horizontal grid borders
grid[:, y+4] = border_value
grid[:, y+8] = border_value
grid[:, y+12] = border_value
# Draws a border around a 16 (top segment) or a 17 (bottom segment)
# x 9 grid, split horizontally into two sections at the specified column
def draw_2_x_1_horiz_grid(grid, border_value, y, x_split_idx=4):
height = 16 if y == 0 else 17
grid[:, y] = border_value # Top
grid[:, y+height] = border_value # Bottom
grid[0, y:y+height] = border_value # Left
grid[8, y:y+height] = border_value # Right
grid[x_split_idx, y:y+height] = border_value # Middle
# Draws a border around a 16 (top segment) or a 17 (bottom segment),
# split vertically into two sections at the specified row
def draw_1_x_2_vert_grid(grid, border_value, y, y_split_idx = 3):
height = 16 if y == 0 else 17
grid[:, y] = border_value # Top
grid[:, y+height] = border_value # Bottom
grid[:, y+y_split_idx] = border_value # Middle
grid[0, y:y+height] = border_value # Left
grid[8, y:y+height] = border_value # Right
# Draws a border around the entire panel, split
# vertically into two equal segments
def draw_outline_border(grid, border_value):
grid[:, 0] = border_value # Top
grid[:, 16] = border_value # Middle
grid[:, 33] = border_value # Bottom
grid[0, :] = border_value # Left
grid[8, :] = border_value # Right
# Maps an app arg value to abstract app and border draw functions
metrics_funcs = {
"cpu": {
"fn": draw_spiral_vals,
"border": draw_8_x_8_grid
},
"disk": {
"fn": draw_bar,
"border": draw_2_x_1_horiz_grid
},
"net": {
"fn": draw_bar,
"border": draw_2_x_1_horiz_grid
},
"mem": {
"fn": draw_memory,
"border": draw_1_x_2_vert_grid
},
"bat": {
"fn": draw_battery,
"border": draw_1_x_2_vert_grid
},
#noop
"none": {
"fn": lambda *x: x,
"border": lambda *x: x
}
}
# Draws the app for the specified arg value
def draw_app(app, *arguments, **kwargs):
metrics_funcs[app].get('fn')(*arguments, **kwargs)
# Draws the border for the specified arg value
def draw_app_border(app, *arguments):
metrics_funcs[app].get('border')(*arguments)
# Draw the IDs of apps currently assigned to the top and bottom of the left panel
def draw_ids_left(grid, top_left, bot_left, fill_value):
fill_grid_top = id_patterns[top_left]
fill_grid_bot = id_patterns[bot_left]
grid[1:8, 1:16] = fill_grid_top * fill_value
grid[1:8, 18:-1] = fill_grid_bot * fill_value
# Draw the IDs of apps currently assigned to the top and bottom of the right panel
def draw_ids_right(grid, top_right, bot_right, fill_value):
fill_grid_top = id_patterns[top_right]
fill_grid_bot = id_patterns[bot_right]
grid[1:8, 1:16] = fill_grid_top * fill_value
grid[1:8, 18:-1] = fill_grid_bot * fill_value
2024-08-17 16:49:21 -04:00
def draw_to_LEDs(s, grid):
for i in range(grid.shape[0]):
params = bytearray([i]) + bytearray(grid[i, :].tolist())
send_command(s, Commands.StageCol, parameters=params)
send_command(s, Commands.FlushCols)
2025-06-23 20:37:30 -04:00
def init_device(location = "1-3.2"):
2024-08-17 16:49:21 -04:00
try:
# VID = 1234
# PID = 5678
device_list = list_ports.comports()
for device in device_list:
if device.location and device.location.startswith(location):
s = serial.Serial(device.device, 115200)
return s
except Exception as e:
print(e)
class DrawingThread(threading.Thread):
def __init__(self, port_location, input_queue):
2024-08-17 16:49:21 -04:00
super().__init__()
self.daemon = True
self.port_location = port_location
self.serial_port = init_device(self.port_location)
2024-08-17 16:49:21 -04:00
self.input_queue = input_queue
def run(self):
while True:
try:
grid = self.input_queue.get()
draw_to_LEDs(self.serial_port, grid)
except Exception as e:
print(f"Error in DrawingThread: {e}")
del self.serial_port
time.sleep(1.0)
self.serial_port = init_device(self.port_location)
2025-06-23 20:37:30 -04:00
###############################################################
### Load metrics functions from plugins ###
###############################################################
# Keep this at the end of the module to avoid circular imports
if not re.search(r"--disable-plugins|-dp", str(sys.argv)):
plugins_dir = './plugins/'
for file in os.listdir(plugins_dir):
if file.endswith('_plugin.py'):
module_name = re.sub(file, "_plugin.py", "")
file_path = plugins_dir + file
spec = importlib.util.spec_from_file_location(module_name, file_path)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
for k,v in module.metrics_funcs.items():
metrics_funcs[k] = v
from drawing import id_patterns
for k,v in module.id_patterns.items():
id_patterns[k] = v
################################################################