From 1d07d8196a8aacf47ba7eaa82537d683294bfd45 Mon Sep 17 00:00:00 2001 From: Jeff Curless Date: Mon, 3 Nov 2025 17:54:15 -0500 Subject: [PATCH 1/4] Add support for Argon40 EON The argon40 EON is a RPI4 with case fan, and 4 drives. --- argononeup.sh | 388 --------------------------------------- monitor/oneUpMon.py | 19 +- monitor/systemsupport.py | 39 +++- 3 files changed, 43 insertions(+), 403 deletions(-) delete mode 100644 argononeup.sh diff --git a/argononeup.sh b/argononeup.sh deleted file mode 100644 index f34e40b..0000000 --- a/argononeup.sh +++ /dev/null @@ -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 - diff --git a/monitor/oneUpMon.py b/monitor/oneUpMon.py index 676e003..4eb7686 100755 --- a/monitor/oneUpMon.py +++ b/monitor/oneUpMon.py @@ -298,10 +298,16 @@ class MonitorWindow(QMainWindow): y_min=20, y_max=80, window=window ) + + casefan = self.config.getValue( "cooling", "casefan", None ) + if casefan is None: + series = [("RPM",None)] + else: + series = [("CPU", None),("CaseFan",None)] self.fan_chart = RollingChart( title="Fan Speed", - series_defs=[("RPM",None)], + series_defs=series, y_min=0,y_max=6000, window=window ) @@ -342,9 +348,12 @@ class MonitorWindow(QMainWindow): ''' # Obtain the current fan speed - try: - fan_speed = self.cpuinfo.CPUFanSpeed - except Exception: + if self.cpuinfo.model == 5: + try: + fan_speed = self.cpuinfo.CPUFanSpeed + except Exception: + fan_speed = None + else: fan_speed = None # Setup the temperature for the CPU and Drives @@ -353,7 +362,7 @@ class MonitorWindow(QMainWindow): temperatures.append( float(self.cpuinfo.temperature) ) except Exception: temperatures.append( 0.0 ) - + # Obtain the drive temperatures try: for _drive in self.multiDrive.drives: diff --git a/monitor/systemsupport.py b/monitor/systemsupport.py index c0345e7..8a92da1 100755 --- a/monitor/systemsupport.py +++ b/monitor/systemsupport.py @@ -229,6 +229,22 @@ class CPUInfo: def __init__( self ): self._cputemp = CPUTemperature() + def _cpuModel( self ) -> int: + with os.popen( "grep Model /proc/cpuinfo" ) as command: + data = command.read() + if "Compute Module 5" in data: + return 5 + elif "Raspberry Pi 4" in data: + return 4 + elif "Raspberry Pi 5" in data: + return 5 + else: + return 0 + + @property + def model( self ) -> int: + return self._cpuModel() + @property def temperature( self ) -> float: ''' @@ -249,15 +265,17 @@ class CPUInfo: Return: The fanspeed as a floating point number ''' - speed= 0 - try: - command = os.popen( 'cat /sys/devices/platform/cooling_fan/hwmon/*/fan1_input' ) - speed = int( command.read().strip()) - except Exception as error: - print( f"Could not determine fan speed, error {error}" ) - finally: - command.close() - + speed = 0 + if self._cpuModel() == 5: + try: + command = os.popen( 'cat /sys/devices/platform/cooling_fan/hwmon/*/fan1_input' ) + speed = int( command.read().strip()) + except Exception as error: + print( f"Could not determine fan speed, error {error}" ) + finally: + command.close() + else: + speed = 0 return float(speed) class CPULoad: @@ -378,7 +396,8 @@ if __name__ == "__main__": cpuinfo = CPUInfo() print( f"CPU Temperature = {cpuinfo.temperature}" ) - print( f"CPU Fan Speed = {cpuinfo.CPUFanSpeed}" ) + print( f"CPU Fan Speed = {cpuinfo.CPUFanSpeed}" ) + print( f"CPU Model = {cpuinfo.model}" ) test = multiDriveStat() print( test.drives ) From adab150746565cd9b7092b9f4da9ce26638e5db2 Mon Sep 17 00:00:00 2001 From: Jeff Curless Date: Wed, 19 Nov 2025 23:12:54 -0500 Subject: [PATCH 2/4] Add process to allow modified smartctl commands --- monitor/configfile.py | 11 ++++---- monitor/oneUpMon.py | 59 +++++++++++++++++++++++++--------------- monitor/systemsupport.py | 58 ++++++++++++++++++++++++++++++++++++--- monitor/test.ini | 5 ++++ 4 files changed, 102 insertions(+), 31 deletions(-) diff --git a/monitor/configfile.py b/monitor/configfile.py index 318eaef..e3ba858 100644 --- a/monitor/configfile.py +++ b/monitor/configfile.py @@ -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(",")] @@ -76,6 +74,9 @@ if __name__ == "__main__": print( f"Value = {cfg.getValueAsList( 'temperature', 'ignore' )}" ) 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" ) diff --git a/monitor/oneUpMon.py b/monitor/oneUpMon.py index 4eb7686..544aeb0 100755 --- a/monitor/oneUpMon.py +++ b/monitor/oneUpMon.py @@ -8,13 +8,16 @@ Requires: PyQt5 (including QtCharts) """ import sys -from systemsupport import CPUInfo, CPULoad, multiDriveStat +from systemsupport import CPUInfo, CPULoad, multiDriveStat, CaseFan from configfile import ConfigClass # -------------------------- # Globals # -------------------------- +MIN_WIDTH = 1000 +MIN_HEIGHT = 800 + # -------------------------- # UI # -------------------------- @@ -265,12 +268,14 @@ class MonitorWindow(QMainWindow): self.drivePerfFilter = self.config.getValueAsList( 'performance', 'ignore' ) # Get supporting objects - self.cpuinfo = CPUInfo() - self.cpuload = CPULoad() + self.cpuinfo = CPUInfo() + self.cpuload = CPULoad() + self.casefan = CaseFan() + self.caseFanPin = None self.multiDrive = multiDriveStat() self.setWindowTitle("System Monitor") - self.setMinimumSize(900, 900) + self.setMinimumSize(MIN_WIDTH, MIN_HEIGHT) central = QWidget(self) grid = QGridLayout(central) @@ -299,18 +304,22 @@ class MonitorWindow(QMainWindow): window=window ) - casefan = self.config.getValue( "cooling", "casefan", None ) - if casefan is None: - series = [("RPM",None)] - else: - series = [("CPU", None),("CaseFan",None)] + if self.cpuinfo.model == 5: + self.caseFanPin = self.config.getValue( "cooling", "casefan", None ) + if self.caseFanPin is None: + series = [("CPU",None)] + else: + self.casefan.setTACHPin( self.caseFanPin) + series = [("CPU",None),("CaseFan",None)] - self.fan_chart = RollingChart( - title="Fan Speed", - series_defs=series, - y_min=0,y_max=6000, - window=window - ) + self.fan_chart = RollingChart( + title="Fan Speed", + series_defs=series, + y_min=0,y_max=6000, + window=window + ) + else: + self.fan_chart = None series = [] for name in self.multiDrive.drives: @@ -328,8 +337,11 @@ class MonitorWindow(QMainWindow): # 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 ) + if self.fan_chart: + grid.addWidget(self.cpu_chart, 2, 0, 1, 1 ) + grid.addWidget(self.fan_chart, 2, 1, 1, 1 ) + else: + grid.addWidget(self.cpu_chart, 2, 0, 1, 2 ) # Get the initial information from the syste self.refresh_metrics() @@ -350,11 +362,14 @@ class MonitorWindow(QMainWindow): # Obtain the current fan speed if self.cpuinfo.model == 5: try: - fan_speed = self.cpuinfo.CPUFanSpeed + if self.caseFanPin: + fan_speed = [self.cpuinfo.CPUFanSpeed,self.casefan.speed] + else: + fan_speed = [self.cpuinfo.CPUFanSpeed] except Exception: - fan_speed = None + fan_speed = [None,None] else: - fan_speed = None + fan_speed = [None,None] # Setup the temperature for the CPU and Drives temperatures = [] @@ -392,7 +407,8 @@ 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.use_chart.append( values ) @@ -404,4 +420,3 @@ def main(): if __name__ == "__main__": main() - diff --git a/monitor/systemsupport.py b/monitor/systemsupport.py index 8a92da1..5de71ea 100755 --- a/monitor/systemsupport.py +++ b/monitor/systemsupport.py @@ -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: @@ -230,14 +249,26 @@ class CPUInfo: 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 4" in data: - return 4 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 @@ -382,6 +413,20 @@ 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 + + def setTACHPin( self, pin = int): + self.tach = pin + + @property + def speed( self ) -> float: + return self.rpm if __name__ == "__main__": @@ -399,6 +444,11 @@ 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: diff --git a/monitor/test.ini b/monitor/test.ini index 2803e19..cac8142 100644 --- a/monitor/test.ini +++ b/monitor/test.ini @@ -3,3 +3,8 @@ [performance] ignore = "mmcblk0" + +[smartctl] + sda="foobar" + sda="duplicate" + sdb='hello' From 65774dbeaa2a5e54374d79bdd66831815cf4f8b8 Mon Sep 17 00:00:00 2001 From: Jeff Curless Date: Wed, 10 Dec 2025 18:22:38 -0500 Subject: [PATCH 3/4] 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. --- monitor/fanspeed.py | 46 ++++++++++++++++++++++ monitor/oneUpMon.py | 82 +++++++++++++++++++++++++++------------- monitor/sysmon.ini | 33 ++++++++++++++++ monitor/systemsupport.py | 65 +++++++++++++++++++++---------- 4 files changed, 181 insertions(+), 45 deletions(-) create mode 100644 monitor/fanspeed.py create mode 100644 monitor/sysmon.ini diff --git a/monitor/fanspeed.py b/monitor/fanspeed.py new file mode 100644 index 0000000..2564e67 --- /dev/null +++ b/monitor/fanspeed.py @@ -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() + + \ No newline at end of file diff --git a/monitor/oneUpMon.py b/monitor/oneUpMon.py index 544aeb0..7d4653e 100755 --- a/monitor/oneUpMon.py +++ b/monitor/oneUpMon.py @@ -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( @@ -330,18 +333,33 @@ class MonitorWindow(QMainWindow): self.io_chart = RollingChartDynamic( title="Disk I/O", series_defs=series, - range_y=[("Bytes/s", 1),("KiB/s",1024),("MiB/s", 1024*1024),("GiB/s",1024*1024*1024)], + range_y=[("Bytes/s", 1),("KiB/s", 1024),("MiB/s", 1024*1024),("GiB/s", 1024*1024*1024)], + 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,14 +381,15 @@ 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] - + # Setup the temperature for the CPU and Drives temperatures = [] try: @@ -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() diff --git a/monitor/sysmon.ini b/monitor/sysmon.ini new file mode 100644 index 0000000..469f95b --- /dev/null +++ b/monitor/sysmon.ini @@ -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 + + diff --git a/monitor/systemsupport.py b/monitor/systemsupport.py index 5de71ea..c460bd8 100755 --- a/monitor/systemsupport.py +++ b/monitor/systemsupport.py @@ -412,22 +412,49 @@ class CPULoad: Number of CPU's ''' 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 _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}" ) - def setTACHPin( self, pin = int): - self.tach = pin + #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__": load = CPULoad() @@ -443,15 +470,15 @@ if __name__ == "__main__": print( f"CPU Temperature = {cpuinfo.temperature}" ) 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) From 0fc4287b4476184d43e1abf49fe6747add29c419 Mon Sep 17 00:00:00 2001 From: Jeff Curless Date: Tue, 30 Dec 2025 18:49:23 -0500 Subject: [PATCH 4/4] Make this work on argonEon Seems to. --- monitor/oneUpMon.py | 44 ++++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/monitor/oneUpMon.py b/monitor/oneUpMon.py index 7d4653e..81e8813 100755 --- a/monitor/oneUpMon.py +++ b/monitor/oneUpMon.py @@ -9,6 +9,7 @@ Requires: PyQt6 (including QtCharts) import sys from systemsupport import CPUInfo, CPULoad, multiDriveStat, NetworkLoad +import gc from configfile import ConfigClass from fanspeed import GetCaseFanSpeed @@ -19,6 +20,8 @@ from fanspeed import GetCaseFanSpeed MIN_WIDTH = 1000 MIN_HEIGHT = 800 +DATA_WINDOW = 60 + # -------------------------- # UI # -------------------------- @@ -39,9 +42,9 @@ 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() @@ -173,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 @@ -187,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. @@ -260,7 +264,7 @@ 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 @@ -293,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)] @@ -305,7 +309,7 @@ class MonitorWindow(QMainWindow): title="Temperature (°C)", series_defs= series, y_min=20, y_max=80, - window=window + window=keepWindow ) if self.cpuinfo.model == 5: @@ -317,9 +321,9 @@ class MonitorWindow(QMainWindow): self.fan_chart = RollingChart( title="Fan Speed", - series_defs=series, + series_defs=[("RPM",None)], y_min=0,y_max=6000, - window=window + window=keepWindow ) else: self.fan_chart = None @@ -334,7 +338,7 @@ 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') @@ -348,7 +352,21 @@ class MonitorWindow(QMainWindow): 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, + 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) @@ -374,9 +392,10 @@ 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: @@ -443,6 +462,7 @@ class MonitorWindow(QMainWindow): self.use_chart.append( values ) def main(): + gc.enable() app = QApplication(sys.argv) w = MonitorWindow(refresh_ms=1000) w.show()