Dashboard web FastAPI + Chart.js
- 4 vues : spectre temps reel, historique detections, background, timeline CPS - API REST : /api/status, /api/spectrum/current, /api/spectrum/difference, /api/background, /api/background/spectrum, /api/history, /api/cps/timeline - Frontend vanilla JS + Chart.js (pas de Node.js, leger pour Pi 4) - Moniteur modifie pour exporter son etat dans /data/monitor_state.json et le CPS dans /data/cps_log.jsonl chaque cycle - Nouveau conteneur Docker 'web' sur port 8080 - Theme sombre, calibration energie (E = 0.33 + 2.97 * canal) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -25,6 +25,10 @@ THRESHOLD = float(os.environ.get("THRESHOLD", "0.5"))
|
||||
SAMPLE_INTERVAL = int(os.environ.get("SAMPLE_INTERVAL", "60"))
|
||||
REPORT_HOUR = int(os.environ.get("REPORT_HOUR", "0"))
|
||||
MIN_LIVE_TIME = int(os.environ.get("MIN_LIVE_TIME", "3600"))
|
||||
STATE_PATH = os.environ.get("STATE_PATH", "/data/monitor_state.json")
|
||||
CPS_LOG_PATH = os.environ.get("CPS_LOG_PATH", "/data/cps_log.jsonl")
|
||||
ENERGY_OFFSET = float(os.environ.get("ENERGY_CALIBRATION_OFFSET", "0.33"))
|
||||
ENERGY_SLOPE = float(os.environ.get("ENERGY_CALIBRATION_SLOPE", "2.97"))
|
||||
|
||||
# Logging
|
||||
logging.basicConfig(
|
||||
@ -86,6 +90,7 @@ class RadiacodeMonitor:
|
||||
self.cumulated_counts = np.zeros(1024, dtype=np.float64)
|
||||
self.cumulated_live_time = 0.0
|
||||
self.last_report_date = None
|
||||
self.connected = False
|
||||
|
||||
def try_connect(self):
|
||||
"""Tente de se connecter au Radiacode. Retourne le device ou None."""
|
||||
@ -94,9 +99,11 @@ class RadiacodeMonitor:
|
||||
|
||||
device = RadiaCode()
|
||||
log.info("Radiacode connecté")
|
||||
self.connected = True
|
||||
return device
|
||||
except Exception as e:
|
||||
log.debug(f"Détecteur non disponible : {e}")
|
||||
self.connected = False
|
||||
return None
|
||||
|
||||
def sample_once(self):
|
||||
@ -131,6 +138,55 @@ class RadiacodeMonitor:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def save_state(self):
|
||||
"""Ecrit l'etat actuel du moniteur dans un fichier JSON atomique."""
|
||||
energy_kev = [round(ENERGY_OFFSET + ENERGY_SLOPE * i, 2) for i in range(1024)]
|
||||
cps = float(self.cumulated_counts.sum() / self.cumulated_live_time) if self.cumulated_live_time > 0 else 0.0
|
||||
|
||||
isotopes = []
|
||||
if self.cumulated_live_time > 0:
|
||||
rate = self.cumulated_counts / self.cumulated_live_time
|
||||
if self.bg_counts is not None and self.bg_live_time is not None:
|
||||
bg_rate = self.bg_counts / self.bg_live_time
|
||||
net_rate = np.clip(rate - bg_rate, 0, None)
|
||||
else:
|
||||
net_rate = rate
|
||||
isotopes = self.run_inference(net_rate)
|
||||
|
||||
state = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"connected": self.connected,
|
||||
"cumulated_live_time_s": round(self.cumulated_live_time, 1),
|
||||
"cumulated_live_time_h": round(self.cumulated_live_time / 3600, 2),
|
||||
"total_counts": int(self.cumulated_counts.sum()),
|
||||
"cps": round(cps, 2),
|
||||
"background_subtracted": self.bg_counts is not None,
|
||||
"isotopes_detected": isotopes,
|
||||
"energy_kev": energy_kev,
|
||||
"counts": [round(float(c), 1) for c in self.cumulated_counts],
|
||||
}
|
||||
|
||||
state_path = Path(STATE_PATH)
|
||||
state_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp_path = state_path.with_suffix(".tmp")
|
||||
with open(tmp_path, "w") as f:
|
||||
json.dump(state, f)
|
||||
os.replace(tmp_path, state_path)
|
||||
|
||||
def log_cps(self, counts, live_time):
|
||||
"""Ajoute un point CPS au journal horodaté."""
|
||||
cps = float(counts.sum() / live_time) if live_time > 0 else 0.0
|
||||
entry = {
|
||||
"ts": datetime.now().isoformat(),
|
||||
"cps": round(cps, 2),
|
||||
"live_time_s": round(live_time, 1),
|
||||
"total_counts": int(counts.sum()),
|
||||
}
|
||||
log_path = Path(CPS_LOG_PATH)
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(log_path, "a") as f:
|
||||
f.write(json.dumps(entry) + "\n")
|
||||
|
||||
def run_inference(self, spectrum_rate):
|
||||
"""Exécute l'inférence PyTorch sur le spectre cumulé."""
|
||||
if spectrum_rate.max() > 0:
|
||||
@ -239,7 +295,12 @@ class RadiacodeMonitor:
|
||||
self.generate_report()
|
||||
self.last_report_date = now.date()
|
||||
|
||||
self.sample_once()
|
||||
success = self.sample_once()
|
||||
if success:
|
||||
self.save_state()
|
||||
self.log_cps(self.cumulated_counts, self.cumulated_live_time)
|
||||
else:
|
||||
self.save_state()
|
||||
time.sleep(SAMPLE_INTERVAL)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user