Add support for network and cooling fan
Add support for a network display, and for a coolin fan. Note that at the moment we support one and only one coolin fan, along with the CPU fan.
This commit is contained in:
46
monitor/fanspeed.py
Normal file
46
monitor/fanspeed.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import RPi.GPIO as GPIO
|
||||
import time
|
||||
|
||||
WAIT_TIME = 1
|
||||
|
||||
class GetCaseFanSpeed:
|
||||
TACH = 18
|
||||
PULSE = 2
|
||||
def __init__( self, tachPin = TACH):
|
||||
self._tachPin = tachPin
|
||||
self._rpm = 0
|
||||
self._t = time.time()
|
||||
self.GPIO = GPIO
|
||||
self.GPIO.setmode( self.GPIO.BCM )
|
||||
self.GPIO.setwarnings( False )
|
||||
self.GPIO.setup( self._tachPin, self.GPIO.IN, pull_up_down=self.GPIO.PUD_UP )
|
||||
self.GPIO.add_event_detect( self._tachPin, self.GPIO.FALLING, self._calcRPM )
|
||||
|
||||
def __del__( self ):
|
||||
self.GPIO.cleanup()
|
||||
|
||||
def _calcRPM( self, n ):
|
||||
dt = time.time() - self._t
|
||||
if dt < 0.005: return # Reject spuriously short pulses
|
||||
|
||||
freq = 1 / dt
|
||||
self._rpm = (freq / GetCaseFanSpeed.PULSE) * 60
|
||||
self._t = time.time()
|
||||
|
||||
@property
|
||||
def RPM( self ):
|
||||
return self._rpm
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
fanSpeed = GetCaseFanSpeed()
|
||||
|
||||
try:
|
||||
for i in range( 10 ):
|
||||
print( f"{fanSpeed.RPM:.0f} RPM" )
|
||||
time.sleep( 2 )
|
||||
|
||||
except KeyboardInterrupt:
|
||||
GPIO.cleanup()
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
|
||||
"""
|
||||
Application that monitors current CPU and Drive temp, along with fan speed and IO utilization
|
||||
Requires: PyQt5 (including QtCharts)
|
||||
Requires: PyQt6 (including QtCharts)
|
||||
|
||||
"""
|
||||
|
||||
import sys
|
||||
from systemsupport import CPUInfo, CPULoad, multiDriveStat, CaseFan
|
||||
from systemsupport import CPUInfo, CPULoad, multiDriveStat, NetworkLoad
|
||||
from configfile import ConfigClass
|
||||
from fanspeed import GetCaseFanSpeed
|
||||
|
||||
# --------------------------
|
||||
# Globals
|
||||
@@ -22,11 +23,11 @@ MIN_HEIGHT = 800
|
||||
# UI
|
||||
# --------------------------
|
||||
|
||||
from PyQt5.QtCore import Qt, QTimer
|
||||
from PyQt5.QtGui import QPainter
|
||||
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QGridLayout
|
||||
from PyQt5.QtChart import QChart, QChartView, QLineSeries, QValueAxis
|
||||
from PyQt5 import QtGui
|
||||
from PyQt6.QtCore import Qt, QTimer
|
||||
from PyQt6.QtGui import QPainter
|
||||
from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget, QGridLayout
|
||||
from PyQt6.QtCharts import QChart, QChartView, QLineSeries, QValueAxis
|
||||
from PyQt6 import QtGui
|
||||
|
||||
class RollingChart(QWidget):
|
||||
'''
|
||||
@@ -47,7 +48,7 @@ class RollingChart(QWidget):
|
||||
|
||||
self.chart.setTitle(title)
|
||||
self.chart.legend().setVisible(len(series_defs) > 1)
|
||||
self.chart.legend().setAlignment(Qt.AlignBottom)
|
||||
self.chart.legend().setAlignment(Qt.AlignmentFlag.AlignBottom)
|
||||
|
||||
self.series:list[QLineSeries] = []
|
||||
for name, color in series_defs:
|
||||
@@ -73,8 +74,8 @@ class RollingChart(QWidget):
|
||||
self.axis_y.setRange(y_min, y_max)
|
||||
self.axis_y.setLabelFormat( "%d" )
|
||||
|
||||
self.chart.addAxis(self.axis_x, Qt.AlignBottom)
|
||||
self.chart.addAxis(self.axis_y, Qt.AlignLeft)
|
||||
self.chart.addAxis(self.axis_x, Qt.AlignmentFlag.AlignBottom)
|
||||
self.chart.addAxis(self.axis_y, Qt.AlignmentFlag.AlignLeft)
|
||||
|
||||
for s in self.series:
|
||||
s.attachAxis(self.axis_x)
|
||||
@@ -116,7 +117,7 @@ class RollingChart(QWidget):
|
||||
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()
|
||||
points = s.points()
|
||||
if points and points[0].x() < min_x_to_keep:
|
||||
# binary search for first index >= min_x_to_keep
|
||||
lo, hi = 0, len(points)
|
||||
@@ -225,7 +226,7 @@ class RollingChartDynamic(RollingChart):
|
||||
maxV = 0
|
||||
for s in self.series:
|
||||
drop = 0
|
||||
points = s.pointsVector()
|
||||
points = s.points()
|
||||
for index, point in enumerate(points):
|
||||
if point.x() < min_x_to_keep:
|
||||
drop = index
|
||||
@@ -243,7 +244,7 @@ class RollingChartDynamic(RollingChart):
|
||||
self.scale.prevScale()
|
||||
self.chart.setTitle( self.title + f" ({self.scale.name})")
|
||||
for s in self.series:
|
||||
points = s.pointsVector()
|
||||
points = s.points()
|
||||
for point in points:
|
||||
point.setY( self.scale.scaleUp(point.y()))
|
||||
s.replace(points)
|
||||
@@ -264,14 +265,17 @@ class MonitorWindow(QMainWindow):
|
||||
|
||||
# Get all the filters loaded
|
||||
self.config = ConfigClass("/etc/sysmon.ini")
|
||||
self.driveTempFilter = self.config.getValueAsList( 'temperature', 'ignore' )
|
||||
self.drivePerfFilter = self.config.getValueAsList( 'performance', 'ignore' )
|
||||
self.driveTempFilter = self.config.getValueAsList( 'drive', 'temp_ignore' )
|
||||
self.drivePerfFilter = self.config.getValueAsList( 'drive', 'perf_ignore' )
|
||||
|
||||
# Get supporting objects
|
||||
self.cpuinfo = CPUInfo()
|
||||
self.cpuload = CPULoad()
|
||||
self.casefan = CaseFan()
|
||||
self.caseFanPin = None
|
||||
self.caseFanPin = self.config.getValue( 'cooling', 'casefan',None )
|
||||
if self.caseFanPin is None :
|
||||
self.caseFan = None
|
||||
else:
|
||||
self.caseFan = GetCaseFanSpeed( int(self.caseFanPin) )
|
||||
self.multiDrive = multiDriveStat()
|
||||
|
||||
self.setWindowTitle("System Monitor")
|
||||
@@ -309,7 +313,6 @@ class MonitorWindow(QMainWindow):
|
||||
if self.caseFanPin is None:
|
||||
series = [("CPU",None)]
|
||||
else:
|
||||
self.casefan.setTACHPin( self.caseFanPin)
|
||||
series = [("CPU",None),("CaseFan",None)]
|
||||
|
||||
self.fan_chart = RollingChart(
|
||||
@@ -334,14 +337,29 @@ class MonitorWindow(QMainWindow):
|
||||
window=window,
|
||||
)
|
||||
|
||||
self.networkFilter = self.config.getValueAsList( 'network', 'device_ignore')
|
||||
self.network = NetworkLoad(self.networkFilter)
|
||||
series = []
|
||||
for name in self.network.names:
|
||||
series.append( (f"{name} Read", None) )
|
||||
series.append( (f"{name} Write", None) )
|
||||
|
||||
self.network_chart = RollingChartDynamic(
|
||||
title="Network I/O",
|
||||
series_defs=series,
|
||||
range_y=[("Bytes/s", 1),("KiB/s", 1024),("MiB/s", 1024*1024),("GiB/s", 1024*1024*1024)],
|
||||
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.network_chart, 2, 0, 1, 2 )
|
||||
if self.fan_chart:
|
||||
grid.addWidget(self.cpu_chart, 2, 0, 1, 1 )
|
||||
grid.addWidget(self.fan_chart, 2, 1, 1, 1 )
|
||||
grid.addWidget(self.cpu_chart, 3, 0, 1, 1 )
|
||||
grid.addWidget(self.fan_chart, 3, 1, 1, 1 )
|
||||
else:
|
||||
grid.addWidget(self.cpu_chart, 2, 0, 1, 2 )
|
||||
grid.addWidget(self.cpu_chart, 3, 0, 1, 2 )
|
||||
|
||||
# Get the initial information from the syste
|
||||
self.refresh_metrics()
|
||||
@@ -363,10 +381,11 @@ class MonitorWindow(QMainWindow):
|
||||
if self.cpuinfo.model == 5:
|
||||
try:
|
||||
if self.caseFanPin:
|
||||
fan_speed = [self.cpuinfo.CPUFanSpeed,self.casefan.speed]
|
||||
fan_speed = [self.cpuinfo.CPUFanSpeed,self.caseFan.RPM]
|
||||
else:
|
||||
fan_speed = [self.cpuinfo.CPUFanSpeed]
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
print( f"error getting fan speed: {e}" )
|
||||
fan_speed = [None,None]
|
||||
else:
|
||||
fan_speed = [None,None]
|
||||
@@ -398,6 +417,16 @@ class MonitorWindow(QMainWindow):
|
||||
except Exception :
|
||||
rwData = [ None, None ]
|
||||
|
||||
# obtain network device read and writes rates
|
||||
try:
|
||||
netData = []
|
||||
networks = self.network.stats
|
||||
for network in networks:
|
||||
netData.append( float( networks[network][0]))
|
||||
netData.append( float( networks[network][1]))
|
||||
except Exception:
|
||||
netData = [None,None]
|
||||
|
||||
# Get the CPU load precentages
|
||||
try:
|
||||
p = self.cpuload.getPercentages()
|
||||
@@ -410,13 +439,14 @@ class MonitorWindow(QMainWindow):
|
||||
if self.fan_chart:
|
||||
self.fan_chart.append( fan_speed )
|
||||
self.io_chart.append( rwData )
|
||||
self.network_chart.append( netData )
|
||||
self.use_chart.append( values )
|
||||
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
w = MonitorWindow(refresh_ms=1000)
|
||||
w.show()
|
||||
sys.exit(app.exec_())
|
||||
sys.exit(app.exec())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
33
monitor/sysmon.ini
Normal file
33
monitor/sysmon.ini
Normal file
@@ -0,0 +1,33 @@
|
||||
#
|
||||
# For drives, you can ignore a device from collecting the temperature,
|
||||
# or the performance data. A good device to ignore for temperature
|
||||
# collection is the mmcblk0 device, as no temp available. Also
|
||||
# in a system using RAID, the temperature is not available.
|
||||
#
|
||||
[drive]
|
||||
temp_ignore = mmcblk
|
||||
perf_ignore = mmcblk
|
||||
|
||||
#
|
||||
# When monitorin the network, you can ignore speciic devices. On
|
||||
# the Argon40 OneUP there is an eth0 thats available but not connected
|
||||
# to anything, so that an lo are good to ignore
|
||||
#
|
||||
[network]
|
||||
device_ignore = "lo,eth0"
|
||||
|
||||
#
|
||||
# if your system has a separate case fan, you can montor the speed if
|
||||
# it has a tachometer attached to a GPIO pin
|
||||
#
|
||||
#[cooling]
|
||||
# casefan = 18
|
||||
|
||||
#
|
||||
# In the rare case there is an additional smartctl command option needed
|
||||
# for a speciic drive, you can add that here.
|
||||
#
|
||||
#[smartctl]
|
||||
# drive = extra_smartctl_command
|
||||
|
||||
|
||||
@@ -413,20 +413,47 @@ class CPULoad:
|
||||
'''
|
||||
return len(self._previousData)
|
||||
|
||||
class CaseFan:
|
||||
'''
|
||||
Class used to monitor a TACH from a case fan.
|
||||
'''
|
||||
def __init__( self, pin=None ):
|
||||
self.tach = pin
|
||||
self.rpm = 0
|
||||
class NetworkLoad:
|
||||
def __init__(self, networkIgnoreList : list[str]=[]):
|
||||
self._networks = []
|
||||
with os.popen( 'ls -1 /sys/class/net') as command:
|
||||
net_raw = command.read()
|
||||
for l in net_raw.split('\n'):
|
||||
if len(l) == 0:
|
||||
continue
|
||||
if not l in networkIgnoreList:
|
||||
self._networks.append( l )
|
||||
self.prevStats = {}
|
||||
for net in self._networks:
|
||||
self.prevStats[net] = self._getData(net)
|
||||
@property
|
||||
def names( self ):
|
||||
return self._networks
|
||||
|
||||
def setTACHPin( self, pin = int):
|
||||
self.tach = pin
|
||||
def _getData( self, name : str ) -> tuple[int,int]:
|
||||
readData = 0
|
||||
writeData = 0
|
||||
try:
|
||||
with open( f"/sys/class/net/{name}/statistics/rx_bytes" ) as f:
|
||||
readData = f.read().strip()
|
||||
with open( f"/sys/class/net/{name}/statistics/tx_bytes" ) as f:
|
||||
writeData = f.read().strip()
|
||||
except Exception as e:
|
||||
print( f"Error {e}" )
|
||||
|
||||
#print( f"{readData} {writeData}" )
|
||||
return (int(readData), int(writeData))
|
||||
|
||||
@property
|
||||
def speed( self ) -> float:
|
||||
return self.rpm
|
||||
def stats(self) -> dict[tuple[int,int]]:
|
||||
data = {}
|
||||
curstats = {}
|
||||
for net in self._networks:
|
||||
curstats[net] = self._getData( net )
|
||||
data[net] = ((curstats[net][0] - self.prevStats[net][0]),
|
||||
(curstats[net][1] - self.prevStats[net][1]))
|
||||
self.prevStats = curstats
|
||||
return data
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -444,14 +471,14 @@ if __name__ == "__main__":
|
||||
print( f"CPU Fan Speed = {cpuinfo.CPUFanSpeed}" )
|
||||
print( f"CPU Model = {cpuinfo.model}" )
|
||||
|
||||
caseFan = CaseFan( 18 )
|
||||
print( f"RPM = {caseFan.speed}" )
|
||||
time.sleep(1)
|
||||
print( f"RPM = {caseFan.speed}" )
|
||||
|
||||
test = multiDriveStat()
|
||||
print( test.drives )
|
||||
for drive in test.drives:
|
||||
print( f"Drive {drive} size is {test.driveSize( drive )}" )
|
||||
print( test.readWriteSectors() )
|
||||
|
||||
network = NetworkLoad( ['lo','eth0'])
|
||||
print( f"Networks Available: {network.names}" )
|
||||
while True:
|
||||
print( f"Stats = {network.stats}" )
|
||||
time.sleep(1)
|
||||
|
||||
Reference in New Issue
Block a user