temp_fan_plugin #1

Open
mleone607 wants to merge 4 commits from mleone607/FW_LED_System_Monitor:temp_fan_plugin into main
15 changed files with 870 additions and 167 deletions

5
.vscode/launch.json vendored
View file

@ -6,11 +6,12 @@
"configurations": [
{
"name": "Python: Current File",
"type": "python",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"justMyCode": true
"justMyCode": true,
"args": ["-ls", "left-snap.json", "-rs", "right-snap.json"]
}
]
}

68
README.md Normal file
View file

@ -0,0 +1,68 @@
# Framework 16 LED Matrix System Monitoring Application
This software is intended for use on a Framework 16 laptop with LED Matrix Panels installed.
## Capabilities
* Display system performance characteristics in real-time
* CPU utilization
* Battery charge level and plug status + memory utilization
* Disk Input/Output rates
* Network Upload/Download rates
* Temperature sensor readings
* Fan speeds
* Display any system monitoring app on any quadrant
* Top or bottom of left or right panel
* Specified via program arguments
* Display a "snapshot" from specified json file(s) on either or both panels. Continuous or periodic display is supported.
* Keyboard shortcut identifies apps running in each quadrant by displaying abbreviated name
* Plugin framework supports simplified development of addiitonal LED Panel applications
* Automatic detection of left and right LED panels
## Installation
* Install [PyEnv](https://github.com/pyenv/pyenv)
* Any other python virtual environment package may be used. Commands below work with PyEnv.
```
cd FW_LED_System_Monitor
pyenv install 3.11
pyenv virtualenv 3.11 3.11
pyenv activate
pip install -r requirements.txt
```
## Run
```
cd led-matrix
python led-sysyem-monitor.py [--help] [--top-left {cpu,net,disk,mem-bat,none,temp,fan}]
[--bottom-left {cpu,net,disk,mem-bat,none,temp,fan}]
[--top-right {cpu,net,disk,mem-bat,none,temp,fan}]
[--bottom-right {cpu,net,disk,mem-bat,none,temp,fan}] [--left-snap LEFT_SNAP]
[--right-snap RIGHT_SNAP] [--snapshot-path SNAPSHOT_PATH]
[--snapshot-interval SNAPSHOT_INTERVAL] [--snapshot-duration SNAPSHOT_DURATION]
[--no-key-listener] [--disable-plugins]
python led-sysyem-monitor.py --help #For more verbose help info
```
## Run as a Linux service
```
cd led-matrix
./install_as_service.sh [...args] #program args to be applied when starting or restarting the service
sudo systemctl start|stop|restart|status fwledmonitor
```
## Keyboard Shortcut
* Alt+I: displays app names in each quadrant while keys are pressed
* Disable key listener with `--no-key-listener` program arg
* To use the key listener, the app must have read permission on the keyboard device (e.g /dev/input/event7). The service runs as root, and therefore has the required access. If you want toi use the key listener while running the app directly, you need to add your user account to the `input` group and ensure there is a group read permission on the keyboard device. **NB:** Consider the implications of this. Any program running as a user in the `input` group will be able to capture your keystrokes.
## Plugin Development
* Add a file in the `plugins` dir with a name that matches the blob pattern `*_plugin.py`
* See `temp_fan_plugin.py` for an implementation example
## Notes
* To list your input devices, use the following python code after installing [Python-evdev](https://python-evdev.readthedocs.io/en/latest/index.html)
```
>>> import evdev
>>> devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
>>> for device in devices:
>>> print(device.path, device.name, device.phys)
/dev/input/event1 Dell Dell USB Keyboard usb-0000:00:12.1-2/input0
/dev/input/event0 Dell USB Optical Mouse usb-0000:00:12.0-2/input0
```
* The baseline reference for calculating the ratio used to display temperature and fan speed readings were arbitarily defined. See `TEMP_REF` and `MAX_FAN_SPEED` in `temp_fan_plugin.py`.
* To examine system performance measures manually and in detail, run `python ps-util-sensors.py`
* To use a different key combination for identifying the running apps, see `KEY_I` and `MODIFIER_KEYS` in `led_system_monitor.py`

View file

@ -2,88 +2,22 @@
import time
import math
import threading
import importlib.util
import sys
import os
import re
import json
from pathlib import Path
# Internal Dependencies
from commands import Commands, send_command
from patterns import lightning_bolt_bot, lightning_bolt_top, lookup_table, id_patterns
# External Dependencies
import numpy as np
import serial # pyserial
from serial.tools import list_ports
# This table represents the 3x3 grid of LEDs to be drawn for each fill ratio
lookup_table = np.array(
[
[
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]
],
[
[0, 0, 0],
[0, 1, 0],
[0, 0, 0]
],
[
[0, 1, 0],
[0, 1, 0],
[0, 0, 0]
],
[
[0, 1, 1],
[0, 1, 0],
[0, 0, 0]
],
[
[0, 1, 1],
[0, 1, 1],
[0, 0, 0]
],
[
[0, 1, 1],
[0, 1, 1],
[0, 0, 1]
],
[
[0, 1, 1],
[0, 1, 1],
[0, 1, 1]
],
[
[0, 1, 1],
[0, 1, 1],
[1, 1, 1]
],
[
[0, 1, 1],
[1, 1, 1],
[1, 1, 1]
],
[
[1, 1, 1],
[1, 1, 1],
[1, 1, 1]
]
]
)
lightning_bolt = np.array( [[0,0,0,0,0,0,0], # 0
[0,0,0,0,0,1,0], # 1
[0,0,0,0,1,1,0], # 2
[0,0,0,1,1,0,0], # 3
[0,0,1,1,1,0,0], # 4
[0,1,1,1,0,0,0], # 5
[0,1,1,1,1,1,0], # 6
[0,0,0,1,1,1,0], # 7
[0,0,1,1,1,0,0], # 8
[0,0,1,1,0,0,0], # 9
[0,1,1,0,0,0,0], #10
[0,1,0,0,0,0,0], #11
[0,0,0,0,0,0,0]],#12
dtype=bool).T
# Correct table orientation for visual orientation when drawn
for i in range(lookup_table.shape[0]):
lookup_table[i] = lookup_table[i].T
@ -92,25 +26,40 @@ for i in range(lookup_table.shape[0]):
def spiral_index(fill_ratio):
return int(round(fill_ratio * 9.999999 - 0.5))
# Takes up 15 rows, 7 columns, starting at 1,1
def draw_cpu(grid, cpu_values, fill_value):
## 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
for i, v in enumerate(cpu_values):
column_number = i % 2
row_number = i // 2
fill_grid = lookup_table[spiral_index(v)]
grid[1+column_number*4:4+column_number*4, 1+row_number*4:4+row_number*4] = fill_grid * fill_value
grid[1+column_number*4:4+column_number*4, y+row_number*4:y+3+row_number*4] = fill_grid * fill_value
# Takes up 2 rows, 7 columns, starting at 17,1
def draw_memory(grid, memory_ratio, fill_value):
# Takes up 2 rows, 7 columns, starting at y, 1
def draw_memory(grid, memory_ratio, fill_value, y):
lit_pixels = 7 * 2 * memory_ratio
pixels_bottom = int(round(lit_pixels / 2))
pixels_top = int(round((lit_pixels - 0.49) / 2))
grid[1:1+pixels_top,17] = fill_value
grid[1:1+pixels_bottom,18] = fill_value
grid[1:1+pixels_top,y+1] = fill_value
grid[1:1+pixels_bottom,y+2] = fill_value
# Takes up 13 rows, 7 columns, starting at 21,1
def draw_battery(grid, battery_ratio, battery_plugged, fill_value, battery_low_thresh = 0.07, battery_low_flash_time = 2, charging_pulse_time = 3):
lit_pixels = int(round(13 * 7 * battery_ratio))
# 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))
pixels_base = lit_pixels // 7
remainder = lit_pixels % 7
if battery_ratio <= battery_low_thresh and not battery_plugged:
@ -120,45 +69,15 @@ def draw_battery(grid, battery_ratio, battery_plugged, fill_value, battery_low_t
pixels_col = pixels_base
if i < remainder:
pixels_col += 1
grid[i+1,33-pixels_col:33] = fill_value
grid[i+1,bot-pixels_col:bot] = fill_value
if battery_plugged:
pulse_amount = math.sin(time.time() / charging_pulse_time)
grid[1:8,20:33][lightning_bolt] -= np.rint(fill_value + 10 * pulse_amount).astype(int)
indices = grid[1:8,20:33] < 0
grid[1:8,20:33][indices] = -grid[1:8,20:33][indices]
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]
def draw_borders_left(grid, border_value):
# Fill in the borders
# Cpu vertical partitions
grid[4, :16] = border_value
# Cpu horizontal partitions
grid[:, 4] = border_value
grid[:, 8] = border_value
grid[:, 12] = border_value
grid[:, 16] = border_value
# Memory bottom partition
grid[:, 19] = border_value
# Outer Edge borders
grid[:, 0] = border_value # Top
grid[0, :] = border_value # Left
grid[8, :] = border_value # Right
grid[:, 33] = border_value # Bottom
def draw_borders_right(grid, border_value):
# Fill in the borders
# Middle Partition borders
grid[:, 16] = border_value
grid[4, :] = border_value
# Outer Edge borders
grid[:, 0] = border_value # Top
grid[0, :] = border_value # Left
grid[8, :] = border_value # Right
grid[:, 33] = border_value # Bottom
def draw_bar(grid, bar_ratio, bar_value, bar_x_offset = 1,draw_at_bottom = True):
# 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):
bar_width = 3
bar_height = 16
lit_pixels = int(round(bar_height * bar_width * bar_ratio))
@ -168,11 +87,128 @@ def draw_bar(grid, bar_ratio, bar_value, bar_x_offset = 1,draw_at_bottom = True)
pixels_col = pixels_base
if i < remainder:
pixels_col += 1
if draw_at_bottom:
if y == 16:
grid[bar_x_offset+i,33-pixels_col:33] = bar_value
else:
grid[bar_x_offset+i,1:1+pixels_col] = bar_value
warned = set()
def draw_snapshot(grid, fill_value, file, path, panel):
global warned
subdirs = [ f.name for f in os.scandir(path) if f.is_dir() and f.name in ['left', 'right']]
subdir = panel if panel in subdirs else ''
try:
with open(os.path.join(path, subdir, file)) as f:
snap = json.load(f)
grid[:,:] = np.array(snap).T * fill_value
except FileNotFoundError as e:
if not file in warned:
print(f"File {file} not found")
warned.add(file)
## 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
direct_draw_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
},
"snap": {
"fn": draw_snapshot,
"border": lambda *x: None # No border
},
#noop
"none": {
"fn": lambda *x: x,
"border": lambda *x: x
}
}
# Draws the app for the specified arg value
def draw_app(app, *arguments, **kwargs):
direct_draw_funcs[app].get('fn')(*arguments, **kwargs)
# Draws the border for the specified arg value
def draw_app_border(app, *arguments):
direct_draw_funcs[app].get('border')(*arguments)
# Draw the IDs of apps currently assigned to the top and bottom of a panel
def draw_ids(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 ID of the app currently assigned to the full panel
def draw_id(grid, id, fill_value):
fill_grid = id_patterns[id]
grid[:,:] = fill_grid * fill_value
def draw_to_LEDs(s, grid):
for i in range(grid.shape[0]):
@ -181,7 +217,7 @@ def draw_to_LEDs(s, grid):
send_command(s, Commands.FlushCols)
def init_device(location = "1-4.2"):
def init_device(location = "1-3.2"):
try:
# VID = 1234
# PID = 5678
@ -212,4 +248,27 @@ class DrawingThread(threading.Thread):
del self.serial_port
time.sleep(1.0)
self.serial_port = init_device(self.port_location)
###############################################################
### 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.direct_draw_funcs.items():
direct_draw_funcs[k] = v
from drawing import id_patterns
for k,v in module.id_patterns.items():
id_patterns[k] = v
################################################################

23
find_ports.py Normal file
View file

@ -0,0 +1,23 @@
import serial
from serial.tools import list_ports
def init_device(location = "1-4.2"):
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)
print(device)
print(device, device.location, device.device, device.manufacturer, device.device_path, device.product, device.interface, device.description)
except Exception as e:
print(e)
init_device("1-3.")
"""
Panels on right side
Left: 1-3.2, ACM0
Right: 1-3.3, ACM1
"""

3
install_as_service.sh Normal file → Executable file
View file

@ -1,5 +1,8 @@
#!/bin/bash
args="$*"
chmod +x run.sh
rm -f fwledmonitor.service || true
sed -i "s#led_system_monitor.py.*\$#led_system_monitor.py ${args}#" run.sh
cat <<EOF >>./fwledmonitor.service
[Unit]
Description=Framework 16 LED System Monitor

View file

@ -1,20 +1,62 @@
# Built In Dependencies
import time
import queue
import sys
import re
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
import importlib.util
import sys
import os
# Internal Dependencies
from drawing import draw_cpu, draw_memory, draw_battery, draw_borders_left, draw_bar, draw_borders_right, DrawingThread
from drawing import draw_outline_border, draw_ids, draw_id, draw_app, draw_app_border, DrawingThread
from monitors import CPUMonitor, MemoryMonitor, BatteryMonitor, DiskMonitor, NetworkMonitor, get_monitor_brightness
# External Dependencies
import numpy as np
if __name__ == "__main__":
# Left LED Matrix location: "1-4.2"
# Right LED Matrix location: "1-3.3"
import evdev
from serial.tools import list_ports
# Set up monitors and serial for left LED Matrix
KEY_I = ('KEY_I', 23)
MODIFIER_KEYS = [('KEY_RIGHTALT', 100), ('KEY_LEFTALT', 56)]
def discover_led_devices():
locations = []
try:
device_list = list_ports.comports()
for device in device_list:
if 'LED Matrix Input Module' in str(device):
locations.append((device.location, device.device))
#location is of form: <bus>-<port>[-<port>]… port is of form x.y:n.m
# Sort by y:n.m to get the devices in left-right order
return sorted(locations, key = lambda x: re.sub('^\d+\-\d+\.', '', x[0]))
except Exception as e:
print(f"An Exception occured while tring to locate LED Matrix devices. {e}")
device = evdev.InputDevice('/dev/input/event7')
def main(args):
led_devices = discover_led_devices()
if not len(led_devices):
print("No LED devices found")
sys.exit(0)
elif len(led_devices) == 1:
print(f"Only one LED device found ({led_devices[0]}). Right panel args will be ignored")
args.top_right = args.bottom_right = "none"
else:
print(f"Found LED devices: Left: {led_devices[0]}, Right: {led_devices[1]}")
locations = list(map(lambda x: x[0], led_devices))
drawing_queues = []
# Track key presses to reveal metrics ID in each panel section
global alt_pressed
alt_pressed = False
global i_pressed
i_pressed = False
# Set up monitors and brightness parameters
min_background_brightness = 12
max_background_brightness = 35
min_foreground_brightness = 24
@ -23,50 +65,136 @@ if __name__ == "__main__":
cpu_monitor = CPUMonitor()
memory_monitor = MemoryMonitor()
battery_monitor = BatteryMonitor()
left_drawing_queue = queue.Queue(2)
left_drawing_thread = DrawingThread("1-4.2", left_drawing_queue)
left_drawing_thread.start()
# Set up monitors and serial for right LED Matrix
disk_monitor = DiskMonitor()
network_monitor = NetworkMonitor()
right_drawing_queue = queue.Queue(2)
right_drawing_thread = DrawingThread("1-3.3", right_drawing_queue)
right_drawing_thread.start()
# Setuop left panel drawing queue
left_drawing_queue = queue.Queue(2)
left_drawing_thread = DrawingThread(locations[0], left_drawing_queue)
left_drawing_thread.start()
drawing_queues.append(left_drawing_queue)
# Setup right panel drawing queue (if present)
if len(locations) == 2:
right_drawing_queue = queue.Queue(2)
right_drawing_thread = DrawingThread(locations[1], right_drawing_queue)
right_drawing_thread.start()
drawing_queues.append(right_drawing_queue)
def draw_cpu(arg, grid, foreground_value, idx):
last_cpu_values = cpu_monitor.get()
draw_app(arg, grid, last_cpu_values, foreground_value, idx)
def draw_mem_bat(arg, grid, foreground_value, idx):
last_memory_values = memory_monitor.get()
last_battery_values = battery_monitor.get()
draw_app("mem", grid, last_memory_values, foreground_value, idx)
draw_app("bat", grid, last_battery_values[0], last_battery_values[1], foreground_value, idx+3)
def draw_disk(arg, grid, foreground_value, idx):
last_disk_read, last_disk_write = disk_monitor.get()
draw_app(arg, grid, last_disk_read, foreground_value, bar_x_offset=1, y=idx) # Read
draw_app(arg, grid, last_disk_write, foreground_value, bar_x_offset=5, y=idx) # Write
def draw_net(arg, grid, foreground_value, idx):
last_network_upload, last_network_download = network_monitor.get()
draw_app(arg, grid, last_network_upload, foreground_value, bar_x_offset=1, y=idx)
draw_app(arg, grid, last_network_download, foreground_value, bar_x_offset=5, y=idx)
def draw_snap(grid, foreground_value, file, snap_path, panel):
draw_app("snap", grid, foreground_value, file, snap_path, panel)
app_functions = {
"cpu": draw_cpu,
"mem-bat": draw_mem_bat,
"disk": draw_disk,
"net": draw_net,
"snap": draw_snap,
"none": lambda *x: x # noop
}
#################################################
### Load app functions from plugins ###
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 obj in module.app_funcs:
app_functions[obj["name"]] = obj["fn"]
#################################################
if args.snapshot_duration > args.snapshot_interval:
print("Snapshot duration must be less than snapshot interval. Exiting...")
sys.exit(0)
start_time = time.time()
while True:
elapsed_time = time.time()
show_snapshot = True if args.snapshot_interval == 0 or elapsed_time % args.snapshot_interval <= args.snapshot_duration else False
try:
screen_brightness = get_monitor_brightness()
background_value = int(screen_brightness * (max_background_brightness - min_background_brightness) + min_background_brightness)
foreground_value = int(screen_brightness * (max_foreground_brightness - min_foreground_brightness) + min_foreground_brightness)
left_start_time = time.time()
# Draw to left LED Matrix
last_cpu_values = cpu_monitor.get()
last_memory_values = memory_monitor.get()
last_battery_values = battery_monitor.get()
grid = np.zeros((9,34), dtype = int)
draw_cpu(grid, last_cpu_values, foreground_value)
draw_memory(grid, last_memory_values, foreground_value)
draw_battery(grid, last_battery_values[0], last_battery_values[1], foreground_value)
draw_borders_left(grid, background_value)
left_drawing_queue.put(grid)
active_keys = device.active_keys(verbose=True)
if (MODIFIER_KEYS[0] in active_keys or MODIFIER_KEYS[1] in active_keys) and KEY_I in active_keys and not args.no_key_listener:
if args.left_snap and show_snapshot:
draw_id(grid, "snap", foreground_value)
else:
draw_outline_border(grid, background_value)
draw_ids(grid, args.top_left, args.bottom_left, foreground_value)
left_drawing_queue.put(grid)
grid = np.zeros((9,34), dtype = int)
if args.right_snap and show_snapshot:
draw_id(grid, "snap", foreground_value)
else:
draw_outline_border(grid, background_value)
draw_ids(grid, args.top_right, args.bottom_right, foreground_value)
right_drawing_queue.put(grid)
grid = np.zeros((9,34), dtype = int)
time.sleep(0.1)
continue
# Draw to right LED Matrix
last_disk_read, last_disk_write = disk_monitor.get()
last_network_upload, last_network_download = network_monitor.get()
grid = np.zeros((9,34), dtype = int)
draw_bar(grid, last_disk_read, foreground_value, bar_x_offset=1, draw_at_bottom=False) # Read
draw_bar(grid, last_disk_write, foreground_value, bar_x_offset=1, draw_at_bottom=True) # Write
draw_bar(grid, last_network_upload, foreground_value, bar_x_offset=5, draw_at_bottom=False) # Upload
draw_bar(grid, last_network_download, foreground_value, bar_x_offset=5, draw_at_bottom=True) # Download
draw_borders_right(grid, background_value)
right_drawing_queue.put(grid)
# Draw by half or whole panel, depending on program args
for i, draw_queue in enumerate(drawing_queues):
grid = np.zeros((9,34), dtype = int)
if i == 0:
panel = 'left'
if args.left_snap is not None and show_snapshot:
app_functions["snap"](grid, foreground_value, args.left_snap, args.snapshot_path, 'left')
draw_queue.put(grid)
continue
else:
_args = [args.top_left, args.bottom_left]
else:
panel = 'right'
if args.right_snap is not None and show_snapshot:
app_functions["snap"](grid, foreground_value, args.right_snap, args.snapshot_path, 'right')
draw_queue.put(grid)
continue
_args = [args.top_right, args.bottom_right]
for j, arg in enumerate(_args):
if j == 0:
idx = 0
loc = 'top'
else:
idx = 16
loc = 'bottom'
try:
func = app_functions[arg]
func(arg, grid, foreground_value, idx)
except KeyError:
print(f"Unrecognized display option {arg} for {loc} {panel}")
if arg == 'mem-bat': arg = 'mem' # Single border draw for mem and bat together
draw_app_border(arg, grid, background_value, idx)
draw_queue.put(grid)
except KeyboardInterrupt:
break
except Exception as e:
@ -76,4 +204,65 @@ if __name__ == "__main__":
time.sleep(1.0)
time.sleep(0.1)
print("Exiting")
print("Exiting")
if __name__ == "__main__":
app_names = ["cpu", "net", "disk", "mem-bat", "none"]
###############################################################
### Load additional app names from plugins for arg parser ###
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)
app_names += map(lambda o: o["name"], module.app_funcs)
#################################################################
parser = ArgumentParser(prog="FW LED System Monitor", add_help=False,
description="Displays system performance metrics in the Framework 16 LED Matrix input module",
formatter_class=ArgumentDefaultsHelpFormatter)
mode_group = parser.add_argument_group()
mode_group.add_argument("--help", "-h", action="help",
help="Show this help message and exit")
addGroup = parser.add_argument_group(title = "Metrics Display Options")
addGroup.add_argument("--top-left", "-tl", type=str, default="cpu", choices=app_names,
help="Metrics to display in the top section of the left matrix panel")
addGroup.add_argument("--bottom-left", "-bl", type=str, default="mem-bat", choices=app_names,
help="Metrics to display in the bottom section of the left matrix panel")
addGroup.add_argument("--top-right", "-tr", type=str, default="disk", choices=app_names,
help="Metrics to display in the top section of the right matrix panel")
addGroup.add_argument("--bottom-right", "-br", type=str, default="disk", choices=app_names,
help="Metrics to display in the top section of the right matrix panel")
addGroup.add_argument("--left-snap", "-ls", type=str, default=None,
help="Snapshot file to display on the left panel. Specify * to cycle through all files in the snapshot dir")
addGroup.add_argument("--right-snap", "-rs", type=str, default=None,
help="Snapshot file to display on the right panel. Specify * to cycle through all files in the snapshot dir")
addGroup.add_argument("--snapshot-path", "-sp", type=str, default="snapshot_files",
help="The file path that contains either the snapshot files that may be displayed on either panel or " +
"'left' and 'right' directories that contain files that may be displayed on the respective panel")
addGroup.add_argument("--snapshot-interval", "-si", type=int, default=0,
help="The interval (in seconds) at which the selected snapshot files will be rendered. A value " +
"of zero means the snapshots should be rendered continuously")
addGroup.add_argument("--snapshot-duration", "-sd", type=int, default=0,
help="The number of seconds that the snapshot file will be rendered at the specified interval. Must be " +
"less than the value of --snapshot-interval")
addGroup.add_argument("--no-key-listener", "-nkl", action="store_true", help="Do not listen for key presses")
addGroup.add_argument("--disable-plugins", "-dp", action="store_true", help="Do not load any plugin code")
args = parser.parse_args()
print(f"top left {args.top_left}")
print(f"bottom left {args.bottom_left}")
print(f"top right {args.top_right}")
print(f"bottom right {args.bottom_right}")
print(f"left snap {args.left_snap}")
print(f"right snap {args.right_snap}")
if args.no_key_listener: print("Key listener disabled")
main(args)

View file

@ -2,6 +2,13 @@
import time
import psutil
import os
from statistics import mean
# Reference for fractional measure of sensor temps (in degrees Celcius)
TEMP_REF = 120
# Reference for fractional measure of fan speeds (in rpm)
MAX_FAN_SPEED = 6_000
if os.name == 'nt':
import wmi
@ -123,7 +130,6 @@ class BatteryMonitor:
bat_status = open('/sys/class/power_supply/BAT1/status', 'r').read().strip()
battery_plugged = (bat_status != 'Discharging')
return battery_percentage, battery_plugged
def get_monitor_brightness():
try:

199
patterns.py Normal file
View file

@ -0,0 +1,199 @@
import numpy as np
import re
import os
import sys
import importlib
lightning_bolt_bot = np.array( [[0,0,0,0,0,0,0], # 0
[0,0,0,0,0,1,0], # 1
[0,0,0,0,1,1,0], # 2
[0,0,0,1,1,0,0], # 3
[0,0,1,1,1,0,0], # 4
[0,1,1,1,0,0,0], # 5
[0,1,1,1,1,1,0], # 6
[0,0,0,1,1,1,0], # 7
[0,0,1,1,1,0,0], # 8
[0,0,1,1,0,0,0], # 9
[0,1,1,0,0,0,0], #10
[0,1,0,0,0,0,0], #11
[0,0,0,0,0,0,0]],#12
dtype=bool).T
# Cut out bottom row because top segment is one row shorter
lightning_bolt_top = np.array( [[0,0,0,0,0,0,0], # 0
[0,0,0,0,0,1,0], # 1
[0,0,0,0,1,1,0], # 2
[0,0,0,1,1,0,0], # 3
[0,0,1,1,1,0,0], # 4
[0,1,1,1,0,0,0], # 5
[0,1,1,1,1,1,0], # 6
[0,0,0,1,1,1,0], # 7
[0,0,1,1,1,0,0], # 8
[0,0,1,1,0,0,0], # 9
[0,1,1,0,0,0,0], #10
[0,1,0,0,0,0,0]],#11
dtype=bool).T
# This table represents the 3x3 grid of LEDs to be drawn for each fill ratio
lookup_table = np.array(
[
[
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]
],
[
[0, 0, 0],
[0, 1, 0],
[0, 0, 0]
],
[
[0, 1, 0],
[0, 1, 0],
[0, 0, 0]
],
[
[0, 1, 1],
[0, 1, 0],
[0, 0, 0]
],
[
[0, 1, 1],
[0, 1, 1],
[0, 0, 0]
],
[
[0, 1, 1],
[0, 1, 1],
[0, 0, 1]
],
[
[0, 1, 1],
[0, 1, 1],
[0, 1, 1]
],
[
[0, 1, 1],
[0, 1, 1],
[1, 1, 1]
],
[
[0, 1, 1],
[1, 1, 1],
[1, 1, 1]
],
[
[1, 1, 1],
[1, 1, 1],
[1, 1, 1]
]
]
)
id_patterns = {
"cpu": np.array([
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 0, 0, 0],
[0, 0, 1, 0, 1, 0, 0],
[0, 0, 1, 1, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 1, 0, 0],
[0, 0, 1, 0, 1, 0, 0],
[0, 0, 1, 0, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0]
]).T,
"mem-bat": np.array([
[0, 1, 1, 0, 1, 1, 0],
[0, 1, 0, 1, 0, 1, 0],
[0, 1, 0, 0, 0, 1, 0],
[0, 1, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 0, 0, 0],
[0, 0, 1, 0, 1, 0, 0],
[0, 0, 1, 1, 0, 0, 0],
[0, 0, 1, 0, 1, 0, 0],
[0, 0, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
]).T,
"disk": np.array([
[0, 0, 1, 1, 0, 0, 0],
[0, 0, 1, 0, 1, 0, 0],
[0, 0, 1, 0, 1, 0, 0],
[0, 0, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 1, 0, 1, 0, 0],
[0, 0, 1, 1, 0, 0, 0],
[0, 0, 1, 0, 1, 0, 0],
]).T,
"net": np.array([
[0, 0, 1, 0, 0, 1, 0],
[0, 0, 1, 1, 0, 1, 0],
[0, 0, 1, 0, 1, 1, 0],
[0, 0, 1, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 1, 1, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
]).T,
"snap": np.array([
[0,0,0,0,0,0,0,0,0],
[0,0,0,1,1,1,1,0,0],
[0,0,1,0,0,0,0,0,0],
[0,0,1,0,0,0,0,0,0],
[0,0,0,1,1,1,0,0,0],
[0,0,0,0,0,0,1,0,0],
[0,0,0,0,0,0,1,0,0],
[0,0,1,1,1,1,0,0,0],
[0,0,0,0,0,0,0,0,0],
[0,0,1,1,0,0,1,0,0],
[0,0,1,1,0,0,1,0,0],
[0,0,1,0,1,0,1,0,0],
[0,0,1,0,1,0,1,0,0],
[0,0,1,0,0,1,1,0,0],
[0,0,1,0,0,1,1,0,0],
[0,0,1,0,0,0,1,0,0],
[0,0,0,0,0,0,0,0,0],
[0,0,0,1,1,1,0,0,0],
[0,0,1,0,0,0,1,0,0],
[0,0,1,0,0,0,1,0,0],
[0,0,1,1,1,1,1,0,0],
[0,0,1,0,0,0,1,0,0],
[0,0,1,0,0,0,1,0,0],
[0,0,1,0,0,0,1,0,0],
[0,0,0,0,0,0,0,0,0],
[0,0,1,1,1,1,0,0,0],
[0,0,1,0,0,0,1,0,0],
[0,0,1,0,0,0,1,0,0],
[0,0,1,1,1,1,0,0,0],
[0,0,1,0,0,0,0,0,0],
[0,0,1,0,0,0,0,0,0],
[0,0,1,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0]
]).T
}

0
plugins/__init__.py Normal file
View file

125
plugins/temp_fan_plugin.py Normal file
View file

@ -0,0 +1,125 @@
from statistics import mean
import psutil
import numpy as np
# Reference for fractional measure of sensor temps (in degrees Celcius)
TEMP_REF = 120
# Reference for fractional measure of fan speeds (in rpm)
MAX_FAN_SPEED = 6_000
#### Implement monitor functions ####
class TemperatureMonitor:
@staticmethod
def get():
temps = []
sensors = psutil.sensors_temperatures()
for _, entries in sensors.items():
temps.append(mean([entry.current for entry in entries if entry.current > 0]))
# We can handle up to eight temps on the matrix display
_temps = list(map(lambda x: x / TEMP_REF, temps))
return list(map(lambda x: x / TEMP_REF, temps))[:8]
class FanSpeedMonitor:
@staticmethod
def get():
fans = psutil.sensors_fans()
speeds = []
for _, entries in fans.items():
for entry in entries:
speeds.append(entry.current)
# We can handle up to two fan speeds on the matrix display
return list(map(lambda x: x / MAX_FAN_SPEED, speeds))[:2]
temperature_monitor = TemperatureMonitor()
fan_speed_monitor = FanSpeedMonitor()
#### Implement high-level drawing functions to be called by app functions below ####
import drawing
draw_app = getattr(drawing, 'draw_app')
def draw_temps(arg, grid, foreground_value, idx):
temp_values = temperature_monitor.get()
draw_app(arg, grid, temp_values, foreground_value, idx)
def draw_fans(arg, grid, foreground_value, idx):
fan_speeds = fan_speed_monitor.get()
draw_app(arg, grid, fan_speeds[0], foreground_value, bar_x_offset=1, y=idx)
draw_app(arg, grid, fan_speeds[1], foreground_value, bar_x_offset=5, y=idx)
draw_spiral_vals = getattr(drawing, 'draw_spiral_vals')
draw_8_x_8_grid = getattr(drawing, 'draw_8_x_8_grid')
draw_bar = getattr(drawing, 'draw_bar')
draw_2_x_1_horiz_grid = getattr(drawing, 'draw_2_x_1_horiz_grid')
#### Implement low-level drawing functions ####
# These functions will be dynamically imported by drawing.py and called by their correcsponding app function
direct_draw_funcs = {
"temp": {
"fn": draw_spiral_vals,
"border": draw_8_x_8_grid
},
"fan": {
"fn": draw_bar,
"border": draw_2_x_1_horiz_grid
}
}
# Implement app functions that call your direct_draw functions
# These functions will be dynamically imported by led_system_monitor.py. They call the direct_draw_funcs
# defined above, providing additional capabilities that can be targeted to panel quadrants
app_funcs = [
{
"name": "temp",
"fn": draw_temps
},
{
"name": "fan",
"fn": draw_fans
}
]
# Provide id patterns that identify your apps
# These items will be dynamically imported by drawing.py
id_patterns = {
"temp": np.array([
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 1, 1, 0, 1, 1, 0],
[0, 1, 0, 1, 0, 1, 0],
[0, 1, 0, 0, 0, 1, 0],
[0, 1, 0, 0, 0, 1, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 0, 0, 1, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
]).T,
"fan": np.array([
[0, 0, 1, 1, 1, 1, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 1, 1, 0, 0],
[0, 0, 1, 0, 0, 1, 0],
[0, 0, 1, 1, 1, 1, 0],
[0, 0, 1, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 1, 0],
[0, 0, 1, 1, 0, 1, 0],
[0, 0, 1, 0, 1, 1, 0],
[0, 0, 1, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0],
]).T
}

23
psutil-sensors.py Normal file
View file

@ -0,0 +1,23 @@
import psutil
from statistics import mean
print("Temperature sensors")
temps = psutil.sensors_temperatures()
for name, entries in temps.items():
print(f"Temp sensor {name}")
for entry in entries:
print(f"\tCurrent: {entry.current}\tHigh: {entry.high}\tCritical: {entry.critical}")
currAvg = mean([entry.current for entry in entries if entry.current > 0])
print(f"Avg: {currAvg}")
print()
print("Fan speeds")
fans = psutil.sensors_fans()
for name, entries in fans.items():
print(f"Fan sensor {name}")
for entry in entries:
print(f"\tCurrent: {entry.current}")
battery = psutil.sensors_battery()
print(battery)

5
requirements.txt Executable file
View file

@ -0,0 +1,5 @@
pyserial
numpy
psutils
psutil
evdev

4
run.sh
View file

@ -1,3 +1,3 @@
sudo apt install python3-numpy python3-psutil
sudo apt install -y python3-numpy python3-psutil python3-serial python3-evdev
python3 ./led_system_monitor.py
python3 ./led_system_monitor.py

View file

@ -0,0 +1 @@
[[0,0,0,1,0,0,0,1,0],[0,0,0,1,0,0,1,0,1],[0,0,0,1,0,0,1,1,1],[0,0,0,1,0,0,1,0,1],[0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,1,1],[0,0,0,0,0,0,0,1,0],[0,0,0,0,0,0,0,1,0],[0,0,0,0,0,0,0,1,0],[0,0,0,0,0,0,0,1,0],[0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,1],[0,0,0,0,1,0,0,0,1],[0,0,0,0,1,0,1,0,1],[0,0,0,0,1,1,1,1,1],[0,0,0,0,1,1,0,1,1],[0,0,0,0,0,0,0,0,0],[1,1,1,1,0,0,1,0,1],[0,1,0,0,0,0,1,0,1],[0,1,0,1,1,0,1,0,1],[0,1,0,1,0,0,1,0,1],[0,1,0,1,0,0,0,1,0],[0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,1,1],[0,0,0,1,0,0,0,1,0],[0,0,1,1,1,0,0,1,0],[0,0,0,1,0,0,0,1,0],[0,0,0,0,0,0,0,1,0],[0,0,0,0,0,0,0,0,0],[1,0,0,0,1,0,1,1,1],[1,0,0,0,1,0,1,0,0],[1,0,0,0,1,0,1,1,0],[1,0,0,0,1,0,1,0,0],[1,1,1,0,1,0,1,0,0]]

View file

@ -0,0 +1 @@
[[1,1,0,1,1,0,0,0,0],[1,1,1,1,1,0,0,0,0],[1,0,1,0,1,0,0,0,0],[1,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0],[1,0,1,0,1,1,1,0,0],[1,0,1,0,1,0,0,0,0],[1,1,1,0,1,1,0,0,0],[1,0,1,0,1,0,0,0,0],[1,0,1,0,1,1,1,0,0],[0,0,0,0,0,0,0,0,0],[0,1,0,0,1,0,1,0,0],[1,0,1,0,1,0,1,0,0],[1,1,1,0,0,1,0,0,0],[1,0,1,0,0,1,0,0,0],[1,0,1,0,0,1,0,0,0],[0,0,0,0,0,0,0,0,0],[1,1,1,0,1,0,1,0,0],[0,1,0,0,1,0,1,0,0],[0,1,0,0,1,1,1,0,0],[0,1,0,0,1,0,1,0,0],[0,1,0,0,1,0,1,0,0],[0,0,0,0,0,0,0,0,0],[1,0,1,0,1,1,1,0,0],[1,0,1,0,1,0,0,0,0],[1,1,1,0,1,1,0,0,0],[1,0,1,0,1,0,0,0,0],[1,0,1,0,1,1,1,0,0],[0,0,0,0,0,0,0,0,0],[1,1,1,0,0,0,0,0,0],[1,0,0,0,0,0,0,0,0],[1,1,0,0,0,0,0,0,0],[1,0,0,0,0,0,0,0,0],[1,1,1,0,0,0,0,0,0]]