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
|
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
|
import sys
|
||||||
from systemsupport import CPUInfo, CPULoad, multiDriveStat, CaseFan
|
from systemsupport import CPUInfo, CPULoad, multiDriveStat, NetworkLoad
|
||||||
from configfile import ConfigClass
|
from configfile import ConfigClass
|
||||||
|
from fanspeed import GetCaseFanSpeed
|
||||||
|
|
||||||
# --------------------------
|
# --------------------------
|
||||||
# Globals
|
# Globals
|
||||||
@@ -22,11 +23,11 @@ MIN_HEIGHT = 800
|
|||||||
# UI
|
# UI
|
||||||
# --------------------------
|
# --------------------------
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QTimer
|
from PyQt6.QtCore import Qt, QTimer
|
||||||
from PyQt5.QtGui import QPainter
|
from PyQt6.QtGui import QPainter
|
||||||
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QGridLayout
|
from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget, QGridLayout
|
||||||
from PyQt5.QtChart import QChart, QChartView, QLineSeries, QValueAxis
|
from PyQt6.QtCharts import QChart, QChartView, QLineSeries, QValueAxis
|
||||||
from PyQt5 import QtGui
|
from PyQt6 import QtGui
|
||||||
|
|
||||||
class RollingChart(QWidget):
|
class RollingChart(QWidget):
|
||||||
'''
|
'''
|
||||||
@@ -47,7 +48,7 @@ class RollingChart(QWidget):
|
|||||||
|
|
||||||
self.chart.setTitle(title)
|
self.chart.setTitle(title)
|
||||||
self.chart.legend().setVisible(len(series_defs) > 1)
|
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] = []
|
self.series:list[QLineSeries] = []
|
||||||
for name, color in series_defs:
|
for name, color in series_defs:
|
||||||
@@ -73,8 +74,8 @@ class RollingChart(QWidget):
|
|||||||
self.axis_y.setRange(y_min, y_max)
|
self.axis_y.setRange(y_min, y_max)
|
||||||
self.axis_y.setLabelFormat( "%d" )
|
self.axis_y.setLabelFormat( "%d" )
|
||||||
|
|
||||||
self.chart.addAxis(self.axis_x, Qt.AlignBottom)
|
self.chart.addAxis(self.axis_x, Qt.AlignmentFlag.AlignBottom)
|
||||||
self.chart.addAxis(self.axis_y, Qt.AlignLeft)
|
self.chart.addAxis(self.axis_y, Qt.AlignmentFlag.AlignLeft)
|
||||||
|
|
||||||
for s in self.series:
|
for s in self.series:
|
||||||
s.attachAxis(self.axis_x)
|
s.attachAxis(self.axis_x)
|
||||||
@@ -116,7 +117,7 @@ class RollingChart(QWidget):
|
|||||||
for s in self.series:
|
for s in self.series:
|
||||||
# Efficient trim: remove points with x < min_x_to_keep
|
# Efficient trim: remove points with x < min_x_to_keep
|
||||||
# QLineSeries doesn't provide O(1) pop from front, so we rebuild if large
|
# 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:
|
if points and points[0].x() < min_x_to_keep:
|
||||||
# binary search for first index >= min_x_to_keep
|
# binary search for first index >= min_x_to_keep
|
||||||
lo, hi = 0, len(points)
|
lo, hi = 0, len(points)
|
||||||
@@ -225,7 +226,7 @@ class RollingChartDynamic(RollingChart):
|
|||||||
maxV = 0
|
maxV = 0
|
||||||
for s in self.series:
|
for s in self.series:
|
||||||
drop = 0
|
drop = 0
|
||||||
points = s.pointsVector()
|
points = s.points()
|
||||||
for index, point in enumerate(points):
|
for index, point in enumerate(points):
|
||||||
if point.x() < min_x_to_keep:
|
if point.x() < min_x_to_keep:
|
||||||
drop = index
|
drop = index
|
||||||
@@ -243,7 +244,7 @@ class RollingChartDynamic(RollingChart):
|
|||||||
self.scale.prevScale()
|
self.scale.prevScale()
|
||||||
self.chart.setTitle( self.title + f" ({self.scale.name})")
|
self.chart.setTitle( self.title + f" ({self.scale.name})")
|
||||||
for s in self.series:
|
for s in self.series:
|
||||||
points = s.pointsVector()
|
points = s.points()
|
||||||
for point in points:
|
for point in points:
|
||||||
point.setY( self.scale.scaleUp(point.y()))
|
point.setY( self.scale.scaleUp(point.y()))
|
||||||
s.replace(points)
|
s.replace(points)
|
||||||
@@ -264,14 +265,17 @@ class MonitorWindow(QMainWindow):
|
|||||||
|
|
||||||
# Get all the filters loaded
|
# Get all the filters loaded
|
||||||
self.config = ConfigClass("/etc/sysmon.ini")
|
self.config = ConfigClass("/etc/sysmon.ini")
|
||||||
self.driveTempFilter = self.config.getValueAsList( 'temperature', 'ignore' )
|
self.driveTempFilter = self.config.getValueAsList( 'drive', 'temp_ignore' )
|
||||||
self.drivePerfFilter = self.config.getValueAsList( 'performance', 'ignore' )
|
self.drivePerfFilter = self.config.getValueAsList( 'drive', 'perf_ignore' )
|
||||||
|
|
||||||
# Get supporting objects
|
# Get supporting objects
|
||||||
self.cpuinfo = CPUInfo()
|
self.cpuinfo = CPUInfo()
|
||||||
self.cpuload = CPULoad()
|
self.cpuload = CPULoad()
|
||||||
self.casefan = CaseFan()
|
self.caseFanPin = self.config.getValue( 'cooling', 'casefan',None )
|
||||||
self.caseFanPin = None
|
if self.caseFanPin is None :
|
||||||
|
self.caseFan = None
|
||||||
|
else:
|
||||||
|
self.caseFan = GetCaseFanSpeed( int(self.caseFanPin) )
|
||||||
self.multiDrive = multiDriveStat()
|
self.multiDrive = multiDriveStat()
|
||||||
|
|
||||||
self.setWindowTitle("System Monitor")
|
self.setWindowTitle("System Monitor")
|
||||||
@@ -309,7 +313,6 @@ class MonitorWindow(QMainWindow):
|
|||||||
if self.caseFanPin is None:
|
if self.caseFanPin is None:
|
||||||
series = [("CPU",None)]
|
series = [("CPU",None)]
|
||||||
else:
|
else:
|
||||||
self.casefan.setTACHPin( self.caseFanPin)
|
|
||||||
series = [("CPU",None),("CaseFan",None)]
|
series = [("CPU",None),("CaseFan",None)]
|
||||||
|
|
||||||
self.fan_chart = RollingChart(
|
self.fan_chart = RollingChart(
|
||||||
@@ -334,14 +337,29 @@ class MonitorWindow(QMainWindow):
|
|||||||
window=window,
|
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)
|
# Layout: 2x2 grid (CPU, NVMe on top; IO full width bottom)
|
||||||
grid.addWidget(self.use_chart, 0, 0, 1, 2 )
|
grid.addWidget(self.use_chart, 0, 0, 1, 2 )
|
||||||
grid.addWidget(self.io_chart, 1, 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:
|
if self.fan_chart:
|
||||||
grid.addWidget(self.cpu_chart, 2, 0, 1, 1 )
|
grid.addWidget(self.cpu_chart, 3, 0, 1, 1 )
|
||||||
grid.addWidget(self.fan_chart, 2, 1, 1, 1 )
|
grid.addWidget(self.fan_chart, 3, 1, 1, 1 )
|
||||||
else:
|
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
|
# Get the initial information from the syste
|
||||||
self.refresh_metrics()
|
self.refresh_metrics()
|
||||||
@@ -363,10 +381,11 @@ class MonitorWindow(QMainWindow):
|
|||||||
if self.cpuinfo.model == 5:
|
if self.cpuinfo.model == 5:
|
||||||
try:
|
try:
|
||||||
if self.caseFanPin:
|
if self.caseFanPin:
|
||||||
fan_speed = [self.cpuinfo.CPUFanSpeed,self.casefan.speed]
|
fan_speed = [self.cpuinfo.CPUFanSpeed,self.caseFan.RPM]
|
||||||
else:
|
else:
|
||||||
fan_speed = [self.cpuinfo.CPUFanSpeed]
|
fan_speed = [self.cpuinfo.CPUFanSpeed]
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
print( f"error getting fan speed: {e}" )
|
||||||
fan_speed = [None,None]
|
fan_speed = [None,None]
|
||||||
else:
|
else:
|
||||||
fan_speed = [None,None]
|
fan_speed = [None,None]
|
||||||
@@ -398,6 +417,16 @@ class MonitorWindow(QMainWindow):
|
|||||||
except Exception :
|
except Exception :
|
||||||
rwData = [ None, None ]
|
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
|
# Get the CPU load precentages
|
||||||
try:
|
try:
|
||||||
p = self.cpuload.getPercentages()
|
p = self.cpuload.getPercentages()
|
||||||
@@ -410,13 +439,14 @@ class MonitorWindow(QMainWindow):
|
|||||||
if self.fan_chart:
|
if self.fan_chart:
|
||||||
self.fan_chart.append( fan_speed )
|
self.fan_chart.append( fan_speed )
|
||||||
self.io_chart.append( rwData )
|
self.io_chart.append( rwData )
|
||||||
|
self.network_chart.append( netData )
|
||||||
self.use_chart.append( values )
|
self.use_chart.append( values )
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
w = MonitorWindow(refresh_ms=1000)
|
w = MonitorWindow(refresh_ms=1000)
|
||||||
w.show()
|
w.show()
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec())
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
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)
|
return len(self._previousData)
|
||||||
|
|
||||||
class CaseFan:
|
class NetworkLoad:
|
||||||
'''
|
def __init__(self, networkIgnoreList : list[str]=[]):
|
||||||
Class used to monitor a TACH from a case fan.
|
self._networks = []
|
||||||
'''
|
with os.popen( 'ls -1 /sys/class/net') as command:
|
||||||
def __init__( self, pin=None ):
|
net_raw = command.read()
|
||||||
self.tach = pin
|
for l in net_raw.split('\n'):
|
||||||
self.rpm = 0
|
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):
|
def _getData( self, name : str ) -> tuple[int,int]:
|
||||||
self.tach = pin
|
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
|
@property
|
||||||
def speed( self ) -> float:
|
def stats(self) -> dict[tuple[int,int]]:
|
||||||
return self.rpm
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|
||||||
@@ -444,14 +471,14 @@ if __name__ == "__main__":
|
|||||||
print( f"CPU Fan Speed = {cpuinfo.CPUFanSpeed}" )
|
print( f"CPU Fan Speed = {cpuinfo.CPUFanSpeed}" )
|
||||||
print( f"CPU Model = {cpuinfo.model}" )
|
print( f"CPU Model = {cpuinfo.model}" )
|
||||||
|
|
||||||
caseFan = CaseFan( 18 )
|
|
||||||
print( f"RPM = {caseFan.speed}" )
|
|
||||||
time.sleep(1)
|
|
||||||
print( f"RPM = {caseFan.speed}" )
|
|
||||||
|
|
||||||
test = multiDriveStat()
|
test = multiDriveStat()
|
||||||
print( test.drives )
|
print( test.drives )
|
||||||
for drive in test.drives:
|
for drive in test.drives:
|
||||||
print( f"Drive {drive} size is {test.driveSize( drive )}" )
|
print( f"Drive {drive} size is {test.driveSize( drive )}" )
|
||||||
print( test.readWriteSectors() )
|
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