fix: keep local telemetry and display independent of monitor

This commit is contained in:
way
2026-09-27 20:54:05 +08:00
parent 72f305dd98
commit 1b9b61bcbc
6 changed files with 105 additions and 27 deletions
+32 -18
View File
@@ -5,6 +5,7 @@ import threading
import time
from pathlib import Path
from settings import COLORS,EFFECTS,GROUPS
from local_state import LocalState
def log(event,**fields):
@@ -59,6 +60,7 @@ class Controller:
def __init__(self,config,driver=None,clock=time.monotonic,temp_reader=temperature):
self.config=config;self.driver=driver;self.clock=clock;self.temp_reader=temp_reader
self.lock=threading.RLock();self.stop_event=threading.Event();self.threads=[]
self.local_reader=LocalState();self.local_state={'system':{},'network':{}};self.local_sampled_at=None
self.state=None;self.expires=0;self.ever_connected=False;self.first_seen={}
self.owner='NORMAL_HOME';self.page='NORMAL_HOME';self.page_since=clock()
self.fan_level=0;self.temp=None;self.errors={};self.i2c_errors=0;self.rgb_mode='OFF'
@@ -88,7 +90,7 @@ class Controller:
alerts=sorted(data['alerts'],key=lambda a:(-a['priority'],self.first_seen.get(a['id'],now),a['id']))
byid={a['id']:a for a in alerts};owner=alerts[0]['id'] if alerts else 'NORMAL_HOME'
if self.owner in byid and byid[self.owner]['priority']==byid[owner]['priority']:owner=self.owner
if not connected and self.ever_connected:owner='DISCONNECTED'
# External lease expiry clears only external alerts, never the local home.
if owner!=self.owner:
log('DISPLAY_OWNER',old=self.owner,new=owner)
self.owner=owner;self.page=owner;self.page_since=now
@@ -98,7 +100,7 @@ class Controller:
self.page=pages[(pages.index(self.page)+1)%len(pages)];self.page_since=now
rgb=byid.get(self.page) or byid.get(owner)
return {'owner':owner,'page':self.page,'connected':connected,'alerts':alerts,'rgb':rgb,
'system':data['system'],'network':data['network'],'config':self.config}
'system':self.local_state['system'],'network':self.local_state['network'],'config':self.config}
def view(self):
with self.lock:return copy.deepcopy(self._view_locked())
@@ -109,7 +111,8 @@ class Controller:
return {'available':self.driver is not None,'connected':v['connected'],'owner':v['owner'],
'oled_page':v['page'],'rgb_mode':self.rgb_mode,'fan_level':self.fan_level,
'fan_name':('OFF','L1','L2','L3','L4','MAX')[self.fan_level] if self.driver else 'UNAVAILABLE',
'temperature_c':self.temp,'errors':dict(self.errors),'i2c_errors':self.i2c_errors}
'temperature_c':self.temp,'local_state':copy.deepcopy(self.local_state),
'local_sample_age_seconds':None if self.local_sampled_at is None else max(0,self.clock()-self.local_sampled_at),'errors':dict(self.errors),'i2c_errors':self.i2c_errors}
def error(self,worker,exc):
with self.lock:
@@ -124,9 +127,22 @@ class Controller:
def start(self):
if self.driver is None:return
self.driver.off();self.driver.oled_init()
for name,work in (('fan',self.fan_loop),('oled',self.oled_loop),('rgb',self.rgb_loop)):
for name,work in (('telemetry',self.telemetry_loop),('fan',self.fan_loop),('oled',self.oled_loop),('rgb',self.rgb_loop)):
thread=threading.Thread(target=work,name=name,daemon=True);self.threads.append(thread);thread.start()
def telemetry_loop(self):
while not self.stop_event.is_set():
try:
state,errors=self.local_reader.sample()
with self.lock:
self.local_state=state;self.local_sampled_at=self.clock()
for key in ('cpu_percent','memory_percent','disk_percent','temperature_c','network'):
if key in errors:self.error('telemetry_'+key,errors[key])
else:self.recovered('telemetry_'+key)
self.recovered('telemetry')
except Exception as exc:self.error('telemetry',exc)
self.stop_event.wait(1)
def fan_loop(self):
initialized=False
while not self.stop_event.is_set():
@@ -155,20 +171,19 @@ class Controller:
while not self.stop_event.is_set():
try:
v=self.view();page=v['page'];system=v['system'];network=v['network'];home=False
if page=='DISCONNECTED':lines=[('MONITOR OFFLINE',''),('STATE EXPIRED',''),('LOCAL FAN ACTIVE',''),('CHECK MONITOR','')]
elif page=='NORMAL_HOME':
if page=='NORMAL_HOME':
home=True
if not v['connected']:
lines=[('STANDALONE',''),(f"TMP {self.temp:.1f}C" if self.temp is not None else 'TMP UNKNOWN',''),('FAN LOCAL',''),('NO MONITOR','')]
else:
ip=network.get('ip','NO IP');label=network.get('ip_label','IP4')
if label=='IP6' and len(ip)>18:ip=ip[:7]+'..'+ip[-7:]
metric=network.get('metric','');kind=network.get('kind','NET')
try:signal=max(0,min(100,round((float(metric)+100)*2)))
except ValueError:signal=None
lines=[(f"CPU {system['cpu_percent']:.1f}%",f"MEM {system['memory_percent']:.1f}%"),
(f"TMP {system['temperature_c']:.1f}C",'FAN '+('OFF','L1','L2','L3','L4','MAX')[self.fan_level]),
(f"DSK {system['disk_percent']:.1f}%",f'WIF {signal}%' if kind=='WIF' and signal is not None else f'{kind} {metric}'),(label+' '+ip,'')]
ip=network.get('ip','NO IP');label=network.get('ip_label','IP4')
if label=='IP6' and len(ip)>18:ip=ip[:7]+'..'+ip[-7:]
metric=network.get('metric','--');kind=network.get('kind','NET')
try:signal=max(0,min(100,round((float(metric)+100)*2)))
except (ValueError,TypeError):signal=None
def value(key):
n=system.get(key)
return f'{n:.1f}' if isinstance(n,(float,int)) and math.isfinite(n) else '--'
lines=[('CPU '+value('cpu_percent')+'%','MEM '+value('memory_percent')+'%'),
('TMP '+value('temperature_c')+'C','FAN '+('OFF','L1','L2','L3','L4','MAX')[self.fan_level]),
('DSK '+value('disk_percent')+'%',f'WIF {signal}%' if kind=='WIF' and signal is not None else f'{kind} {metric}'),(label+' '+ip,'')]
else:
a=next(a for a in v['alerts'] if a['id']==page);index=v['alerts'].index(a)+1
lines=[(a['title'],f"{index}/{len(v['alerts'])}"),(a['l2'],''),(a['l3'],''),(a['l4'],'')]
@@ -182,7 +197,6 @@ class Controller:
@staticmethod
def profile(view):
c=view['config']['rgb'];a=view['rgb']
if view['owner']=='DISCONNECTED':return ('off',),'DISCONNECTED_OFF'
if a:
group=a['category'];level=('warning','critical','emergency')[int(a['severity'])-1]
label=f"{group}_{level}_{c['alert_mode']}".upper()