feat(qnap-lcd): Proxmox-correct front-panel LCD menu for esh-nas-pve; albok redeploy note
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python3
|
||||
# QNAP front-panel LCD menu for esh-nas-pve (QNAP hardware running Proxmox VE, not QTS).
|
||||
#
|
||||
# Replaces the menu script of DigitalFunk/QnapLCD-Proxmox (checked out at /usr/local/bin/qnap_lcd, commit f2c9c46),
|
||||
# whose "Proxmox" fork still opened on the TrueNAS screen: menu[0] ran `cli -c 'system version'`, a TrueNAS-only
|
||||
# command, so the script died on its first screen and the panel stayed on "Initializing..." with dead buttons.
|
||||
# The same script also hid the management address: it skipped every bridge, and on Proxmox the IP lives on vmbr0.
|
||||
# Uses the repo's own `qnaplcd` driver unchanged (A125 display, 2x16, /dev/ttyS1 @ 1200 baud).
|
||||
# Source of truth: eshpfi services/qnap-lcd/ (deployed by playbooks/qnap-lcd.yaml). Test without the panel:
|
||||
# python3 lcd-menu.py --dry-run (prints every screen instead of writing to the LCD)
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
sys.stdout.reconfigure(line_buffering=True)
|
||||
|
||||
DISPLAY_TIMEOUT = 30 # seconds the backlight stays on after a button press
|
||||
REFRESH = 30 # seconds between redraws of the current screen
|
||||
PORT, PORT_SPEED = '/dev/ttyS1', 1200
|
||||
WIDTH = 16
|
||||
|
||||
|
||||
def shell(cmd):
|
||||
return subprocess.check_output(cmd, shell=True, universal_newlines=True, stderr=subprocess.DEVNULL).strip()
|
||||
|
||||
|
||||
def fit(text):
|
||||
return str(text)[:WIDTH]
|
||||
|
||||
|
||||
# ---------- screens: each returns two lines ----------
|
||||
|
||||
def host_ip():
|
||||
addrs = ipv4_addresses()
|
||||
for name, ip in addrs:
|
||||
if name == 'vmbr0':
|
||||
return ip
|
||||
return addrs[0][1] if addrs else 'no IPv4'
|
||||
|
||||
|
||||
def screen_host():
|
||||
return [platform.node(), host_ip()]
|
||||
|
||||
|
||||
def screen_pve():
|
||||
v = shell('pveversion') # pve-manager/9.2.21/4f6e0ac8 (running kernel: 7.0.14-20-pve)
|
||||
ver = v.split('/')[1] if '/' in v else v
|
||||
return [f'PVE {ver}', platform.release()] # 'Proxmox VE 9.2.21' is 17 chars, one too many for the panel
|
||||
|
||||
|
||||
def screen_uptime():
|
||||
secs = int(float(open('/proc/uptime').read().split()[0]))
|
||||
d, rem = divmod(secs, 86400)
|
||||
h, rem = divmod(rem, 3600)
|
||||
m = rem // 60
|
||||
up = f'{d}d {h:02d}:{m:02d}' if d else f'{h:02d}:{m:02d}'
|
||||
load = os.getloadavg()
|
||||
return [f'Up {up}', f'Ld {load[0]:.2f} {load[1]:.2f}']
|
||||
|
||||
|
||||
def ipv4_addresses():
|
||||
out = []
|
||||
for iface in json.loads(shell('ip -json address show')):
|
||||
name = iface.get('ifname', '')
|
||||
if name == 'lo' or name.startswith(('veth', 'tap', 'fwbr', 'fwpr', 'fwln')):
|
||||
continue
|
||||
for a in iface.get('addr_info', []):
|
||||
if a.get('family') == 'inet':
|
||||
out.append((name, a['local']))
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
def pools():
|
||||
rows = shell('zpool list -H -o name,size,alloc,cap,health').splitlines()
|
||||
return [r.split('\t') for r in rows if r.strip()]
|
||||
|
||||
|
||||
def build_menu():
|
||||
menu = [screen_host, screen_pve, screen_uptime]
|
||||
for name, ip in ipv4_addresses():
|
||||
menu.append(lambda n=name, i=ip: [n, i])
|
||||
for name, size, alloc, cap, health in pools():
|
||||
menu.append(lambda n=name, s=size, a=alloc, c=cap, h=health: [f'{n} {h}', f'{a}/{s} {c}'])
|
||||
return menu
|
||||
|
||||
|
||||
# ---------- driver glue ----------
|
||||
|
||||
lcd = None
|
||||
lcd_timer = None
|
||||
menu = [screen_host]
|
||||
menu_item = 0
|
||||
lock = threading.Lock()
|
||||
|
||||
|
||||
def render(item):
|
||||
try:
|
||||
lines = menu[item]()
|
||||
except Exception as e: # one broken screen must never kill the menu
|
||||
lines = ['screen error', type(e).__name__]
|
||||
print(f'screen {item} failed: {e!r}')
|
||||
lines = [fit(l) for l in lines][:2]
|
||||
if lcd is None:
|
||||
print(f'[{item}] {lines[0]!r} | {lines[1] if len(lines) > 1 else ""!r}')
|
||||
return
|
||||
lcd.clear()
|
||||
lcd.write(0, lines)
|
||||
|
||||
|
||||
def lcd_on():
|
||||
global lcd_timer
|
||||
lcd.backlight(True)
|
||||
if lcd_timer:
|
||||
lcd_timer.cancel()
|
||||
lcd_timer = threading.Timer(DISPLAY_TIMEOUT, lambda: lcd.backlight(False))
|
||||
lcd_timer.daemon = True
|
||||
lcd_timer.start()
|
||||
|
||||
|
||||
def response_handler(command, data):
|
||||
global menu_item
|
||||
if command != 'Switch_Status':
|
||||
return
|
||||
with lock:
|
||||
lcd_on()
|
||||
if data == 0x01: # up
|
||||
menu_item = (menu_item - 1) % len(menu)
|
||||
elif data == 0x02: # down
|
||||
menu_item = (menu_item + 1) % len(menu)
|
||||
else:
|
||||
return
|
||||
render(menu_item)
|
||||
|
||||
|
||||
def main():
|
||||
global lcd, menu, menu_item
|
||||
if '--dry-run' in sys.argv:
|
||||
menu = build_menu()
|
||||
for i in range(len(menu)):
|
||||
render(i)
|
||||
return
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import qnaplcd
|
||||
lcd = qnaplcd.QnapLCD(PORT, PORT_SPEED, response_handler)
|
||||
lcd.reset()
|
||||
lcd.clear()
|
||||
lcd_on()
|
||||
while True:
|
||||
with lock:
|
||||
menu = build_menu()
|
||||
menu_item %= len(menu)
|
||||
render(menu_item)
|
||||
time.sleep(REFRESH)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,19 @@
|
||||
# QNAP front-panel LCD menu on esh-nas-pve. Source: eshpfi services/qnap-lcd/ (playbooks/qnap-lcd.yaml).
|
||||
# Replaces the repo's unit, which ran a shell wrapper with no restart: when the menu died (it always did on
|
||||
# Proxmox, see lcd-menu.py), nothing brought it back and the panel froze on "Initializing...".
|
||||
[Unit]
|
||||
Description=QNAP front-panel LCD menu
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/usr/local/bin/qnap_lcd
|
||||
ExecStartPre=/usr/bin/python3 /usr/local/bin/qnap_lcd/preinit.py
|
||||
ExecStart=/usr/bin/python3 /usr/local/bin/qnap_lcd/lcd-menu.py
|
||||
ExecStopPost=/usr/bin/python3 /usr/local/bin/qnap_lcd/shutdown.py
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Reference in New Issue
Block a user