Implement snapshot capability

Rename plugin funcs to make implemenation more intuitive

Add ID for snap. Remove redundant left/right id func. Show err if snap file not found

Track per-file snapshot error
This commit is contained in:
Leone, Mark A [LGS] 2025-06-28 18:06:09 -04:00
parent 9f71853179
commit 68989ed8e4
8 changed files with 128 additions and 30 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"]
}
]
}

View file

@ -6,6 +6,8 @@ import importlib.util
import sys
import os
import re
import json
from pathlib import Path
# Internal Dependencies
from commands import Commands, send_command
@ -89,6 +91,21 @@ def draw_bar(grid, bar_ratio, bar_value, bar_x_offset = 1, y=0):
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 ##
@ -141,7 +158,7 @@ def draw_outline_border(grid, border_value):
grid[8, :] = border_value # Right
# Maps an app arg value to abstract app and border draw functions
metrics_funcs = {
direct_draw_funcs = {
"cpu": {
"fn": draw_spiral_vals,
"border": draw_8_x_8_grid
@ -162,6 +179,10 @@ metrics_funcs = {
"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,
@ -171,25 +192,23 @@ metrics_funcs = {
# Draws the app for the specified arg value
def draw_app(app, *arguments, **kwargs):
metrics_funcs[app].get('fn')(*arguments, **kwargs)
direct_draw_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)
direct_draw_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):
# 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 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
# 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]):
@ -245,8 +264,8 @@ if not re.search(r"--disable-plugins|-dp", str(sys.argv)):
sys.modules[module_name] = module
spec.loader.exec_module(module)
for k,v in module.metrics_funcs.items():
metrics_funcs[k] = v
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():

0
install_as_service.sh Normal file → Executable file
View file

View file

@ -10,7 +10,7 @@ import os
# Internal Dependencies
from drawing import draw_outline_border, draw_ids_left, draw_ids_right, draw_app, draw_app_border, 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
@ -101,11 +101,15 @@ def main(args):
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
}
@ -126,7 +130,13 @@ def main(args):
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)
@ -134,26 +144,41 @@ def main(args):
grid = np.zeros((9,34), dtype = int)
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:
draw_outline_border(grid, background_value)
draw_ids_left(grid, args.top_left, args.bottom_left, foreground_value)
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)
draw_outline_border(grid, background_value)
draw_ids_right(grid, args.top_right, args.bottom_right, foreground_value)
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 by quadrants (i.e. to top and bottom of left and right panels)
# 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'
_args = [args.top_left, args.bottom_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]
grid = np.zeros((9,34), dtype = int)
for j, arg in enumerate(_args):
if j == 0:
idx = 0
@ -165,7 +190,6 @@ def main(args):
func = app_functions[arg]
func(arg, grid, foreground_value, idx)
except KeyError:
print(app_functions.keys())
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)
@ -197,12 +221,12 @@ if __name__ == "__main__":
sys.modules[module_name] = module
spec.loader.exec_module(module)
app_names += module.metrics_funcs.keys()
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_mutually_exclusive_group()
mode_group = parser.add_argument_group()
mode_group.add_argument("--help", "-h", action="help",
help="Show this help message and exit")
@ -215,6 +239,19 @@ if __name__ == "__main__":
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")
@ -224,6 +261,8 @@ if __name__ == "__main__":
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

@ -159,5 +159,41 @@ id_patterns = {
[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
}

View file

@ -56,9 +56,9 @@ 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 led_system_monitor.py
# These functions will be dynamically imported by drawing.py and called by their correcsponding app function
metrics_funcs = {
direct_draw_funcs = {
"temp": {
"fn": draw_spiral_vals,
"border": draw_8_x_8_grid
@ -69,8 +69,9 @@ metrics_funcs = {
}
}
# Implement app functions that call your high-level draw functions
# These functions will be dynamically imported by led_system_monitor.py
# 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 = [
{

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]]