Files
argon40-battery-display/monitor/oneUpMon.py
Jeff Curless 542cdecab3 Add system monitoring code
Need a way to monitor the system, CPU temp, NVMe temp, etc
2025-10-12 23:02:09 -04:00

229 lines
7.0 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Application that monitors current CPU and Drive temp, along with fan speed and IO utilization
Requires: PyQt5 (including QtCharts)
"""
import sys
from typing import Tuple, List
from gpiozero import CPUTemperature
from oneUpSupport import systemData
from cpuload import CPULoad
import os
# --------------------------
# Globals
# --------------------------
sysdata = None
cpuload = CPULoad()
# --------------------------
# UI
# --------------------------
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QPainter
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QGridLayout, QLabel
from PyQt5.QtChart import QChart, QChartView, QLineSeries, QValueAxis
class RollingChart(QWidget):
"""
A reusable chart widget with one or more QLineSeries and a rolling X window.
Args:
title: Chart title
series_defs: List of (name, color_qt_str or None) for each line
y_min, y_max: Fixed Y axis range
window: number of points to keep (points are 1 per tick by default)
"""
def __init__(self, title: str, series_defs: List[tuple], y_min: float, y_max: float, window: int = 120, parent=None):
super().__init__(parent)
self.window = window
self.x = 0
self.series: List[QLineSeries] = []
self.chart = QChart()
self.chart.setTitle(title)
self.chart.legend().setVisible(len(series_defs) > 1)
self.chart.legend().setAlignment(Qt.AlignBottom)
for name, color in series_defs:
s = QLineSeries()
s.setName(name)
if color:
s.setColor(color) # QColor or string like "#RRGGBB"
self.series.append(s)
self.chart.addSeries(s)
# Axes
self.axis_x = QValueAxis()
self.axis_x.setRange(0, self.window)
#self.axis_x.setTitleText("Seconds")
self.axis_x.setLabelFormat("%d")
self.axis_y = QValueAxis()
self.axis_y.setRange(y_min, y_max)
self.chart.addAxis(self.axis_x, Qt.AlignBottom)
self.chart.addAxis(self.axis_y, Qt.AlignLeft)
for s in self.series:
s.attachAxis(self.axis_x)
s.attachAxis(self.axis_y)
self.view = QChartView(self.chart)
self.view.setRenderHints(QPainter.RenderHint.Antialiasing)
layout = QGridLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self.view, 0, 0)
def append(self, values: List[float]):
"""
Append one sample (for each series) at the next x value. Handles rolling window.
values must match the number of series.
"""
self.x += 1
for s, v in zip(self.series, values):
# Handle NaN by skipping, or plot zero—here we clamp None/NaN to None and skip
try:
if v is None:
continue
# If you want to clamp, do it here: v = max(self.axis_y.min(), min(self.axis_y.max(), v))
s.append(self.x, float(v))
except Exception:
# ignore bad data points
pass
# Trim series to rolling window
min_x_to_keep = max(0, self.x - self.window)
self.axis_x.setRange(min_x_to_keep, self.x)
for s in self.series:
# Efficient trim: remove points with x < min_x_to_keep
# QLineSeries doesn't provide O(1) pop from front, so we rebuild if large
points = s.pointsVector()
if points and points[0].x() < min_x_to_keep:
# binary search for first index >= min_x_to_keep
lo, hi = 0, len(points)
while lo < hi:
mid = (lo + hi) // 2
if points[mid].x() < min_x_to_keep:
lo = mid + 1
else:
hi = mid
s.replace(points[lo:]) # keep tail only
class MonitorWindow(QMainWindow):
def __init__(self, refresh_ms: int = 1000, window = 120, parent=None):
super().__init__(parent)
self.setWindowTitle("Argon 1UP Monitor")
self.setMinimumSize(900, 900)
central = QWidget(self)
grid = QGridLayout(central)
grid.setContentsMargins(8, 8, 8, 8)
grid.setHorizontalSpacing(8)
grid.setVerticalSpacing(8)
self.setCentralWidget(central)
# Charts
self.use_chart = RollingChart(
title="CPU Utilization",
series_defs=[ (name, None) for name in cpuload.cpuNames ],
y_min=0, y_max=100,
window=120
)
self.cpu_chart = RollingChart(
title="Temperature (°C)",
series_defs=[
("CPU", None),
("NVMe", None),
],
y_min=20, y_max=80,
window=window
)
self.fan_chart = RollingChart(
title="Fan Speed",
series_defs=[("RPM",None)],
y_min=0,y_max=6000,
window=window
)
self.io_chart = RollingChart(
title="NVMe I/O (MB/s)",
series_defs=[
("Read MB/s", None),
("Write MB/s", None),
],
y_min=0, y_max=1100, # adjust ceiling for your device
window=window
)
# Layout: 2x2 grid (CPU, NVMe on top; IO full width bottom)
grid.addWidget(self.use_chart, 0, 0, 1, 2 )
grid.addWidget(self.io_chart, 1, 0, 1, 2 )
grid.addWidget(self.cpu_chart, 2, 0, 1, 1 )
grid.addWidget(self.fan_chart, 2, 1, 1, 1 )
# Timer
self.timer = QTimer(self)
self.timer.timeout.connect(self.refresh_metrics)
self.timer.start(refresh_ms)
self.refresh_metrics()
def refresh_metrics(self):
# Gather metrics with safety
try:
cpu_c = float(sysdata.CPUTemperature)
except Exception:
cpu_c = None
try:
fan_speed = sysdata.fanSpeed
except Exception:
fan_speed = None
try:
nvme_c = sysdata.driveTemp
except Exception:
nvme_c = None
try:
read_mb, write_mb = sysdata.driveStats
read_mb = float(read_mb)
write_mb = float(write_mb)
except Exception:
read_mb, write_mb = None, None
try:
p = cpuload.getPercentages()
values = []
for i in range( len(cpuload) ):
values.append( round( p[f'cpu{i}'], 2 ) )
except Exception:
values = [ None for i in range( len( cpuload) ) ]
# Append to charts
self.cpu_chart.append([cpu_c,nvme_c])
self.fan_chart.append([fan_speed])
self.io_chart.append([read_mb, write_mb])
self.use_chart.append( values )
def main():
app = QApplication(sys.argv)
w = MonitorWindow(refresh_ms=1000)
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
sysdata = systemData()
main()