Merge pull request #14 from JeffCurless/argoneonSupport

Argoneon support
This commit is contained in:
Jeff Curless
2025-12-30 18:53:07 -05:00
committed by GitHub
7 changed files with 319 additions and 452 deletions

View File

@@ -1,388 +0,0 @@
#!/bin/bash
echo "*************"
echo " Argon Setup "
echo "*************"
# Check time if need to 'fix'
NEEDSTIMESYNC=0
LOCALTIME=$(date -u +%s%N | cut -b1-10)
GLOBALTIME=$(curl -s 'http://worldtimeapi.org/api/ip.txt' | grep unixtime | cut -b11-20)
TIMEDIFF=$((GLOBALTIME-LOCALTIME))
# about 26hrs, max timezone difference
if [ $TIMEDIFF -gt 100000 ]
then
NEEDSTIMESYNC=1
fi
argon_time_error() {
echo "**********************************************"
echo "* WARNING: Device time seems to be incorrect *"
echo "* This may cause problems during setup. *"
echo "**********************************************"
echo "Possible Network Time Protocol Server issue"
echo "Try running the following to correct:"
echo " curl -k http://files.iamnet.com.ph/argon/setup/tools/setntpserver.sh | bash"
}
if [ $NEEDSTIMESYNC -eq 1 ]
then
argon_time_error
fi
# Helper variables
ARGONDOWNLOADSERVER=http://files.iamnet.com.ph/argon/setup
INSTALLATIONFOLDER=/etc/argon
pythonbin="sudo /usr/bin/python3"
versioninfoscript=$INSTALLATIONFOLDER/argon-versioninfo.sh
uninstallscript=$INSTALLATIONFOLDER/argon-uninstall.sh
configscript=$INSTALLATIONFOLDER/argon-config
unitconfigscript=$INSTALLATIONFOLDER/argon-unitconfig.sh
argondashboardscript=$INSTALLATIONFOLDER/argondashboard.py
setupmode="Setup"
if [ -f $configscript ]
then
setupmode="Update"
echo "Updating files"
else
sudo mkdir $INSTALLATIONFOLDER
sudo chmod 755 $INSTALLATIONFOLDER
fi
##########
# Start code lifted from raspi-config
# set_config_var based on raspi-config
if [ -e /boot/firmware/config.txt ] ; then
FIRMWARE=/firmware
else
FIRMWARE=
fi
CONFIG=/boot${FIRMWARE}/config.txt
set_config_var() {
if ! grep -q -E "$1=$2" $3 ; then
echo "$1=$2" | sudo tee -a $3 > /dev/null
fi
}
# End code lifted from raspi-config
##########
# Reuse set_config_var
set_nvme_default() {
set_config_var dtparam nvme $CONFIG
set_config_var dtparam=pciex1_gen 3 $CONFIG
}
argon_check_pkg() {
RESULT=$(dpkg-query -W -f='${Status}\n' "$1" 2> /dev/null | grep "installed")
if [ "" == "$RESULT" ]; then
echo "NG"
else
echo "OK"
fi
}
CHECKDEVICE="oneup" # Hardcoded for argononeup
CHECKGPIOMODE="libgpiod" # libgpiod or rpigpio
# Check if Raspbian, Ubuntu, others
CHECKPLATFORM="Others"
CHECKPLATFORMVERSION=""
CHECKPLATFORMVERSIONNUM=""
if [ -f "/etc/os-release" ]
then
source /etc/os-release
if [ "$ID" = "raspbian" ]
then
CHECKPLATFORM="Raspbian"
CHECKPLATFORMVERSION=$VERSION_ID
elif [ "$ID" = "debian" ]
then
# For backwards compatibility, continue using raspbian
CHECKPLATFORM="Raspbian"
CHECKPLATFORMVERSION=$VERSION_ID
elif [ "$ID" = "ubuntu" ]
then
CHECKPLATFORM="Ubuntu"
CHECKPLATFORMVERSION=$VERSION_ID
fi
echo ${CHECKPLATFORMVERSION} | grep -e "\." > /dev/null
if [ $? -eq 0 ]
then
CHECKPLATFORMVERSIONNUM=`cut -d "." -f2 <<< $CHECKPLATFORMVERSION `
CHECKPLATFORMVERSION=`cut -d "." -f1 <<< $CHECKPLATFORMVERSION `
fi
fi
gpiopkg="python3-libgpiod"
if [ "$CHECKGPIOMODE" = "rpigpio" ]
then
if [ "$CHECKPLATFORM" = "Raspbian" ]
then
gpiopkg="raspi-gpio python3-rpi.gpio"
else
gpiopkg="python3-rpi.gpio"
fi
fi
pkglist=($gpiopkg python3-smbus i2c-tools python3-evdev ddcutil)
for curpkg in ${pkglist[@]}; do
sudo apt-get install -y $curpkg
RESULT=$(argon_check_pkg "$curpkg")
if [ "NG" == "$RESULT" ]
then
echo "********************************************************************"
echo "Please also connect device to the internet and restart installation."
echo "********************************************************************"
exit
fi
done
# Ubuntu Mate for RPi has raspi-config too
command -v raspi-config &> /dev/null
if [ $? -eq 0 ]
then
# Enable i2c
sudo raspi-config nonint do_i2c 0
fi
# Added to enabled NVMe for pi5
set_nvme_default
# Fan Setup
basename="argononeup"
daemonname=$basename"d"
eepromrpiscript="/usr/bin/rpi-eeprom-config"
eepromconfigscript=$INSTALLATIONFOLDER/${basename}-eepromconfig.py
daemonscript=$INSTALLATIONFOLDER/$daemonname.py
daemonservice=/lib/systemd/system/$daemonname.service
if [ -f "$eepromrpiscript" ]
then
# EEPROM Config Script
sudo wget $ARGONDOWNLOADSERVER/scripts/argon-rpi-eeprom-config-psu.py -O $eepromconfigscript --quiet
sudo chmod 755 $eepromconfigscript
fi
# Daemon/Service Files
sudo wget $ARGONDOWNLOADSERVER/scripts/${daemonname}.py -O $daemonscript --quiet
sudo wget $ARGONDOWNLOADSERVER/scripts/${daemonname}.service -O $daemonservice --quiet
sudo chmod 644 $daemonservice
# Battery Images
if [ ! -d "$INSTALLATIONFOLDER/ups" ]
then
sudo mkdir $INSTALLATIONFOLDER/ups
fi
sudo wget $ARGONDOWNLOADSERVER/ups/upsimg.tar.gz -O $INSTALLATIONFOLDER/ups/upsimg.tar.gz --quiet
sudo tar xfz $INSTALLATIONFOLDER/ups/upsimg.tar.gz -C $INSTALLATIONFOLDER/ups/
sudo rm -Rf $INSTALLATIONFOLDER/ups/upsimg.tar.gz
# Other utility scripts
sudo wget $ARGONDOWNLOADSERVER/scripts/argondashboard.py -O $INSTALLATIONFOLDER/argondashboard.py --quiet
sudo wget $ARGONDOWNLOADSERVER/scripts/argon-versioninfo.sh -O $versioninfoscript --quiet
sudo chmod 755 $versioninfoscript
sudo wget $ARGONDOWNLOADSERVER/scripts/argonsysinfo.py -O $INSTALLATIONFOLDER/argonsysinfo.py --quiet
sudo wget $ARGONDOWNLOADSERVER/scripts/argonregister-v1.py -O $INSTALLATIONFOLDER/argonregister.py --quiet
# Argon Uninstall Script
sudo wget $ARGONDOWNLOADSERVER/scripts/argon-uninstall.sh -O $uninstallscript --quiet
sudo chmod 755 $uninstallscript
# Argon Config Script
if [ -f $configscript ]; then
sudo rm $configscript
fi
sudo touch $configscript
# To ensure we can write the following lines
sudo chmod 666 $configscript
echo '#!/bin/bash' >> $configscript
echo 'echo "--------------------------"' >> $configscript
echo 'echo "Argon Configuration Tool"' >> $configscript
echo "$versioninfoscript simple" >> $configscript
echo 'echo "--------------------------"' >> $configscript
echo 'get_number () {' >> $configscript
echo ' read curnumber' >> $configscript
echo ' if [ -z "$curnumber" ]' >> $configscript
echo ' then' >> $configscript
echo ' echo "-2"' >> $configscript
echo ' return' >> $configscript
echo ' elif [[ $curnumber =~ ^[+-]?[0-9]+$ ]]' >> $configscript
echo ' then' >> $configscript
echo ' if [ $curnumber -lt 0 ]' >> $configscript
echo ' then' >> $configscript
echo ' echo "-1"' >> $configscript
echo ' return' >> $configscript
echo ' elif [ $curnumber -gt 100 ]' >> $configscript
echo ' then' >> $configscript
echo ' echo "-1"' >> $configscript
echo ' return' >> $configscript
echo ' fi ' >> $configscript
echo ' echo $curnumber' >> $configscript
echo ' return' >> $configscript
echo ' fi' >> $configscript
echo ' echo "-1"' >> $configscript
echo ' return' >> $configscript
echo '}' >> $configscript
echo '' >> $configscript
echo 'mainloopflag=1' >> $configscript
echo 'while [ $mainloopflag -eq 1 ]' >> $configscript
echo 'do' >> $configscript
echo ' echo' >> $configscript
echo ' echo "Choose Option:"' >> $configscript
echo ' echo " 1. Get Battery Status"' >> $configscript
uninstalloption="3"
statusoption=$(($uninstalloption-1))
echo " echo \" $statusoption. Dashboard\"" >> $configscript
echo " echo \" $uninstalloption. Uninstall\"" >> $configscript
echo ' echo ""' >> $configscript
echo ' echo " 0. Exit"' >> $configscript
echo " echo -n \"Enter Number (0-$uninstalloption):\"" >> $configscript
echo ' newmode=$( get_number )' >> $configscript
echo ' if [ $newmode -eq 0 ]' >> $configscript
echo ' then' >> $configscript
echo ' echo "Thank you."' >> $configscript
echo ' mainloopflag=0' >> $configscript
echo ' elif [ $newmode -eq 1 ]' >> $configscript
echo ' then' >> $configscript
# Option 1
echo " $pythonbin $daemonscript GETBATTERY" >> $configscript
# Standard options
echo " elif [ \$newmode -eq $statusoption ]" >> $configscript
echo ' then' >> $configscript
echo " $pythonbin $argondashboardscript" >> $configscript
echo " elif [ \$newmode -eq $uninstalloption ]" >> $configscript
echo ' then' >> $configscript
echo " $uninstallscript" >> $configscript
echo ' mainloopflag=0' >> $configscript
echo ' fi' >> $configscript
echo 'done' >> $configscript
sudo chmod 755 $configscript
# Desktop Icon
destfoldername=$USERNAME
if [ -z "$destfoldername" ]
then
destfoldername=$USER
fi
if [ -z "$destfoldername" ]
then
destfoldername="pi"
fi
shortcutfile="/home/$destfoldername/Desktop/argononeup.desktop"
if [ -d "/home/$destfoldername/Desktop" ]
then
terminalcmd="lxterminal --working-directory=/home/$destfoldername/ -t"
if [ -f "/home/$destfoldername/.twisteros.twid" ]
then
terminalcmd="xfce4-terminal --default-working-directory=/home/$destfoldername/ -T"
fi
imagefile=argon40.png
sudo wget http://files.iamnet.com.ph/argon/setup/$imagefile -O /etc/argon/$imagefile --quiet
if [ -f $shortcutfile ]; then
sudo rm $shortcutfile
fi
# Create Shortcuts
echo "[Desktop Entry]" > $shortcutfile
echo "Name=Argon Configuration" >> $shortcutfile
echo "Comment=Argon Configuration" >> $shortcutfile
echo "Icon=/etc/argon/$imagefile" >> $shortcutfile
echo 'Exec='$terminalcmd' "Argon Configuration" -e '$configscript >> $shortcutfile
echo "Type=Application" >> $shortcutfile
echo "Encoding=UTF-8" >> $shortcutfile
echo "Terminal=false" >> $shortcutfile
echo "Categories=None;" >> $shortcutfile
chmod 755 $shortcutfile
fi
configcmd="$(basename -- $configscript)"
if [ "$setupmode" = "Setup" ]
then
if [ -f "/usr/bin/$configcmd" ]
then
sudo rm /usr/bin/$configcmd
fi
sudo ln -s $configscript /usr/bin/$configcmd
# Enable and Start Service(s)
sudo systemctl daemon-reload
sudo systemctl enable argononeupd.service
sudo systemctl start argononeupd.service
else
sudo systemctl daemon-reload
sudo systemctl restart argononeupd.service
fi
if [ "$CHECKPLATFORM" = "Raspbian" ]
then
if [ -f "$eepromrpiscript" ]
then
sudo apt-get update && sudo apt-get upgrade -y
sudo rpi-eeprom-update
# EEPROM Config Script
sudo $eepromconfigscript
fi
else
echo "WARNING: EEPROM not updated. Please run this under Raspberry Pi OS"
fi
echo "*********************"
echo " $setupmode Completed "
echo "*********************"
$versioninfoscript
echo
echo "Use '$configcmd' to configure device"
echo
if [ $NEEDSTIMESYNC -eq 1 ]
then
argon_time_error
fi

View File

@@ -22,10 +22,10 @@ class ConfigClass:
occasionally.
'''
if not self.readFile:
try:
_result = self.config.read( self.filename )
if len(_result) > 0:
self.readFile = True
except Exception as error:
print( f"{error}" )
def getValue( self, section : str, key : str, default="" ) -> str:
'''
@@ -41,7 +41,6 @@ class ConfigClass:
The value of the key from the section read.
'''
value = default
self._openConfig()
try:
value = self.config[section][key].replace('"','').strip()
except:
@@ -62,7 +61,6 @@ class ConfigClass:
a List of items
'''
value = default
self._openConfig()
try:
temp = self.config[section][name]
value = [ n.replace('"','').strip() for n in temp.split(",")]
@@ -77,6 +75,9 @@ if __name__ == "__main__":
print( f"Value = {cfg.getValue( 'performance', 'ignore' )}" )
print( f"Value = {cfg.getValueAsList( 'performance', 'ignore' )}" )
drive = cfg.getValue( 'smartctl', 'sda' )
print( f"drive = {drive}" )
cfg = ConfigClass( "missingfile.ini" )

46
monitor/fanspeed.py Normal file
View 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()

View File

@@ -3,27 +3,34 @@
"""
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
from systemsupport import CPUInfo, CPULoad, multiDriveStat, NetworkLoad
import gc
from configfile import ConfigClass
from fanspeed import GetCaseFanSpeed
# --------------------------
# Globals
# --------------------------
MIN_WIDTH = 1000
MIN_HEIGHT = 800
DATA_WINDOW = 60
# --------------------------
# 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):
'''
@@ -35,16 +42,16 @@ class RollingChart(QWidget):
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):
def __init__(self, title: str, series_defs: list[tuple], y_min: float, y_max: float, window: int = DATA_WINDOW, parent=None):
super().__init__(parent)
self.title = title
self.pointWindow = window
self.xpos = window - 1
self.chart = QChart()
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:
@@ -70,8 +77,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)
@@ -113,7 +120,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)
@@ -169,7 +176,7 @@ class scaleValues:
class RollingChartDynamic(RollingChart):
def __init__(self, title : str, series_defs: list[tuple], range_y : list[tuple], window=120,parent=None):
def __init__(self, title : str, series_defs: list[tuple], range_y : list[tuple], window=DATA_WINDOW,parent=None):
self.maxY = 512
super().__init__(title,series_defs,0,self.maxY,window,parent)
self.title = title
@@ -183,6 +190,7 @@ class RollingChartDynamic(RollingChart):
if value < i:
return i
return 4
def append(self, values: list[float]):
'''
Append one sample (for each series) at the next x value. Handles rolling window.
@@ -222,7 +230,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
@@ -240,7 +248,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)
@@ -256,21 +264,26 @@ class MonitorWindow(QMainWindow):
is a data refresh period.
Parent - Owning parent of this window... default is None.
'''
def __init__(self, refresh_ms: int = 1000, window = 120, parent=None):
def __init__(self, refresh_ms: int = 1000, keepWindow = DATA_WINDOW, parent=None):
super().__init__(parent)
# 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.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")
self.setMinimumSize(900, 900)
self.setMinimumSize(MIN_WIDTH, MIN_HEIGHT)
central = QWidget(self)
grid = QGridLayout(central)
@@ -284,7 +297,7 @@ class MonitorWindow(QMainWindow):
title="CPU Utilization",
series_defs=[ (name, None) for name in self.cpuload.cpuNames ],
y_min=0, y_max=100,
window=120
window=keepWindow
)
series = [("CPU", None)]
@@ -296,15 +309,24 @@ class MonitorWindow(QMainWindow):
title="Temperature (°C)",
series_defs= series,
y_min=20, y_max=80,
window=window
window=keepWindow
)
if self.cpuinfo.model == 5:
self.caseFanPin = self.config.getValue( "cooling", "casefan", None )
if self.caseFanPin is None:
series = [("CPU",None)]
else:
series = [("CPU",None),("CaseFan",None)]
self.fan_chart = RollingChart(
title="Fan Speed",
series_defs=[("RPM",None)],
y_min=0,y_max=6000,
window=window
window=keepWindow
)
else:
self.fan_chart = None
series = []
for name in self.multiDrive.drives:
@@ -316,14 +338,46 @@ class MonitorWindow(QMainWindow):
title="Disk I/O",
series_defs=series,
range_y=[("Bytes/s", 1),("KiB/s", 1024),("MiB/s", 1024*1024),("GiB/s", 1024*1024*1024)],
window=window,
window=keepWindow,
)
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=keepWindow,
)
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=keepWindow,
)
# 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 )
grid.addWidget(self.network_chart, 2, 0, 1, 2 )
if self.fan_chart:
grid.addWidget(self.cpu_chart, 3, 0, 1, 1 )
grid.addWidget(self.fan_chart, 3, 1, 1, 1 )
else:
grid.addWidget(self.cpu_chart, 3, 0, 1, 2 )
# Get the initial information from the syste
self.refresh_metrics()
@@ -338,14 +392,22 @@ class MonitorWindow(QMainWindow):
This routine is called periodically, as setup in the __init__ functon. Since this
routine calls out to other things, we want to make sure that there is no possible
exception, so everything needs to be wrapped in a handler.
'''
gc.collect()
# Obtain the current fan speed
if self.cpuinfo.model == 5:
try:
fan_speed = self.cpuinfo.CPUFanSpeed
except Exception:
fan_speed = None
if self.caseFanPin:
fan_speed = [self.cpuinfo.CPUFanSpeed,self.caseFan.RPM]
else:
fan_speed = [self.cpuinfo.CPUFanSpeed]
except Exception as e:
print( f"error getting fan speed: {e}" )
fan_speed = [None,None]
else:
fan_speed = [None,None]
# Setup the temperature for the CPU and Drives
temperatures = []
@@ -374,6 +436,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()
@@ -383,16 +455,18 @@ class MonitorWindow(QMainWindow):
# Append to charts
self.cpu_chart.append( temperatures )
self.fan_chart.append([fan_speed])
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():
gc.enable()
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
View 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

View File

@@ -84,6 +84,7 @@ class DriveStats:
def name(self) -> str:
return self._device
@property
def readAllStats( self ) -> list[int]:
'''
read all of the drive statisics from sysfs for the device.
@@ -93,19 +94,24 @@ class DriveStats:
'''
return self._getStats()
@property
def readSectors( self )-> int:
return self._getStats()[DriveStats.READ_SECTORS]
@property
def writeSectors( self ) -> int:
return self._getStats()[DriveStats.WRITE_SECTORS]
@property
def discardSectors( self ) -> int:
return self._getStats()[DriveStats.DISCARD_SECTORS]
@property
def readWriteSectors( self ) -> tuple[int,int]:
curData = self._getStats()
return (curData[DriveStats.READ_SECTORS],curData[DriveStats.WRITE_SECTORS])
@property
def readWriteBytes( self ) -> tuple[int,int]:
curData = self._getStats()
return (curData[DriveStats.READ_SECTORS]*512,curData[DriveStats.WRITE_SECTORS]*512)
@@ -168,6 +174,19 @@ class multiDriveStat():
return 0
def driveTemp(self,_drive:str, extracmd = None) -> float:
'''
Get the drive temperature using smart data. There are three basic temperature
settings we can read, smart ID 194, 190 and the Temperature: value. These are
depenent on the drive, so look for all of them, and depending on the result, we
get the value.
Parameters:
_drive : The device we wish to scan
extraCmd : An optional additional command to send to the device.
Returns:
The temperature as a float, or zero if there is an error.
'''
smartOutRaw = ""
if extracmd is None:
cmd = f'sudo smartctl -A /dev/{_drive}'
@@ -208,7 +227,7 @@ class multiDriveStat():
'''
curData = {}
for _ in self._stats:
curData[_.name] = _.readWriteSectors()
curData[_.name] = _.readWriteSectors
return curData
def readWriteBytes( self ) -> dict[str,tuple[int,int]]:
@@ -218,7 +237,7 @@ class multiDriveStat():
'''
curData = {}
for _ in self._stats:
curData[_.name] = _.readWriteBytes()
curData[_.name] = _.readWriteBytes
return curData
class CPUInfo:
@@ -229,6 +248,34 @@ class CPUInfo:
def __init__( self ):
self._cputemp = CPUTemperature()
def _cpuModel( self ) -> int:
'''
Check for the cpu model. Scan cpuinfo to see if we can locate a string that
matches something we are looking for.
Return:
Model of the Raspberry PI. This treats the Comput Modules the same as
standard model B's
'''
with os.popen( "grep Model /proc/cpuinfo" ) as command:
data = command.read()
if "Compute Module 5" in data:
return 5
elif "Raspberry Pi 5" in data:
return 5
elif "Raspberry Pi 4" in data:
return 4
elif "Compute Module 4" in data:
return 4
elif "Raspberry Pi 3" in data:
return 3
else:
return 0
@property
def model( self ) -> int:
return self._cpuModel()
@property
def temperature( self ) -> float:
'''
@@ -250,6 +297,7 @@ class CPUInfo:
The fanspeed as a floating point number
'''
speed = 0
if self._cpuModel() == 5:
try:
command = os.popen( 'cat /sys/devices/platform/cooling_fan/hwmon/*/fan1_input' )
speed = int( command.read().strip())
@@ -257,7 +305,8 @@ class CPUInfo:
print( f"Could not determine fan speed, error {error}" )
finally:
command.close()
else:
speed = 0
return float(speed)
class CPULoad:
@@ -364,6 +413,47 @@ class CPULoad:
'''
return len(self._previousData)
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 _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 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__":
@@ -379,6 +469,7 @@ if __name__ == "__main__":
cpuinfo = CPUInfo()
print( f"CPU Temperature = {cpuinfo.temperature}" )
print( f"CPU Fan Speed = {cpuinfo.CPUFanSpeed}" )
print( f"CPU Model = {cpuinfo.model}" )
test = multiDriveStat()
print( test.drives )
@@ -386,3 +477,8 @@ if __name__ == "__main__":
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)

View File

@@ -3,3 +3,8 @@
[performance]
ignore = "mmcblk0"
[smartctl]
sda="foobar"
sda="duplicate"
sdb='hello'