diff --git a/source/argon-eeprom.sh b/source/argon-eeprom.sh new file mode 100644 index 0000000..bf99a8e --- /dev/null +++ b/source/argon-eeprom.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +echo "*************" +echo " Argon Setup " +echo "*************" + +# Helper variables +ARGONDOWNLOADSERVER=https://download.argon40.com + +eepromrpiscript="/usr/bin/rpi-eeprom-config" +eepromconfigscript=/dev/shm/argon-eeprom.py + +# Check if Raspbian, Ubuntu, others +CHECKPLATFORM="Others" +if [ -f "/etc/os-release" ] +then + source /etc/os-release + if [ "$ID" = "raspbian" ] + then + CHECKPLATFORM="Raspbian" + elif [ "$ID" = "debian" ] + then + # For backwards compatibility, continue using raspbian + CHECKPLATFORM="Raspbian" + elif [ "$ID" = "ubuntu" ] + then + CHECKPLATFORM="Ubuntu" + fi +fi + +# Check if original eeprom script exists before running +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 wget $ARGONDOWNLOADSERVER/scripts/argon-rpi-eeprom-config-default.py -O $eepromconfigscript --quiet + sudo chmod 755 $eepromconfigscript + sudo $eepromconfigscript + fi +else + echo "Please run this under Raspberry Pi OS" +fi diff --git a/source/argon1v5.sh b/source/argon1v5.sh new file mode 100644 index 0000000..21895a5 --- /dev/null +++ b/source/argon1v5.sh @@ -0,0 +1,822 @@ +#!/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 https://download.argon40.com/tools/setntpserver.sh | bash" +} + +if [ $NEEDSTIMESYNC -eq 1 ] +then + argon_time_error +fi + + +# Helper variables +ARGONDOWNLOADSERVER=https://download.argon40.com + +INSTALLATIONFOLDER=/etc/argon + +FLAGFILEV1=$INSTALLATIONFOLDER/flag_v1 + +versioninfoscript=$INSTALLATIONFOLDER/argon-versioninfo.sh + +uninstallscript=$INSTALLATIONFOLDER/argon-uninstall.sh +shutdownscript=/lib/systemd/system-shutdown/argon-shutdown.sh +configscript=$INSTALLATIONFOLDER/argon-config +unitconfigscript=$INSTALLATIONFOLDER/argon-unitconfig.sh +blstrdacconfigscript=$INSTALLATIONFOLDER/argon-blstrdac.sh +statusdisplayscript=$INSTALLATIONFOLDER/argon-status.sh + +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 +# is_pifive, get_serial_hw and do_serial_hw 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 +} + +is_pifive() { + grep -q "^Revision\s*:\s*[ 123][0-9a-fA-F][0-9a-fA-F]4[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]$" /proc/cpuinfo + return $? +} + + +get_serial_hw() { + if is_pifive ; then + if grep -q -E "dtparam=uart0=off" $CONFIG ; then + echo 1 + elif grep -q -E "dtparam=uart0" $CONFIG ; then + echo 0 + else + echo 1 + fi + else + if grep -q -E "^enable_uart=1" $CONFIG ; then + echo 0 + elif grep -q -E "^enable_uart=0" $CONFIG ; then + echo 1 + elif [ -e /dev/serial0 ] ; then + echo 0 + else + echo 1 + fi + fi +} + +do_serial_hw() { + if [ $1 -eq 0 ] ; then + if is_pifive ; then + set_config_var dtparam=uart0 on $CONFIG + else + set_config_var enable_uart 1 $CONFIG + fi + else + if is_pifive ; then + sudo sed $CONFIG -i -e "/dtparam=uart0.*/d" + else + set_config_var enable_uart 0 $CONFIG + fi + fi +} + +# End code lifted from raspi-config +########## + +# Reuse is_pifive, set_config_var +set_nvme_default() { + if is_pifive ; then + set_config_var dtparam nvme $CONFIG + set_config_var dtparam=pciex1_gen 3 $CONFIG + fi +} +set_maxusbcurrent() { + if is_pifive ; then + #set_config_var max_usb_current 1 $CONFIG + set_config_var usb_max_current_enable 1 $CONFIG + fi +} + + +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="oneoled" # Hardcoded for argon1oled +# Check if has RTC +# Todo for multiple OS + +#i2cdetect -y 1 | grep -q ' 51 ' +#if [ $? -eq 0 ] +#then +# CHECKDEVICE="eon" +#fi + +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 + +if [ "$CHECKPLATFORM" = "Raspbian" ] +then + if [ "$CHECKDEVICE" = "eon" ] + then + pkglist=($gpiopkg python3-smbus i2c-tools smartmontools) + elif [ "$CHECKDEVICE" = "oneoled" ] + then + pkglist=($gpiopkg python3-smbus i2c-tools python3-luma.oled) + else + pkglist=($gpiopkg python3-smbus i2c-tools) + fi +else + # Todo handle lgpio + # Ubuntu has serial and i2c enabled + if [ "$CHECKDEVICE" = "eon" ] + then + pkglist=($gpiopkg python3-smbus i2c-tools smartmontools) + elif [ "$CHECKDEVICE" = "oneoled" ] + then + pkglist=($gpiopkg python3-smbus i2c-tools python3-luma.oled) + else + pkglist=($gpiopkg python3-smbus i2c-tools) + fi +fi + +echo "Installing/updating dependencies..." + +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 + +echo "Updating configuration ..." + +# Ubuntu Mate for RPi has raspi-config too +command -v raspi-config &> /dev/null +if [ $? -eq 0 ] +then + # Enable i2c and serial + sudo raspi-config nonint do_i2c 0 + if [ ! "$CHECKDEVICE" = "fanhat" ] + then + + if [ "$CHECKPLATFORM" = "Raspbian" ] + then + # bookworm raspi-config prompts user when configuring serial + if [ $(get_serial_hw) -eq 1 ]; then + do_serial_hw 0 + fi + else + sudo raspi-config nonint do_serial 2 + fi + fi +fi + +if [ "$CHECKDEVICE" = "oneoled" ] +then + TMPCONFIGFILE="/dev/shm/tmpconfig.txt" + cat $CONFIG | grep -v 'dtoverlay=dwc2' > $TMPCONFIGFILE + chmod 755 $TMPCONFIGFILE + sudo cp $TMPCONFIGFILE $CONFIG + set_config_var dtoverlay=dwc2,dr_mode host $CONFIG + rm $TMPCONFIGFILE +fi + +# Additional config for pi5 +set_nvme_default +set_maxusbcurrent + +# Fan Setup +basename="argonone" +daemonname=$basename"d" +irconfigscript=$INSTALLATIONFOLDER/${basename}-ir +upsconfigscript=$INSTALLATIONFOLDER/${basename}-upsconfig.sh +fanconfigscript=$INSTALLATIONFOLDER/${basename}-fanconfig.sh +eepromrpiscript="/usr/bin/rpi-eeprom-config" +eepromconfigscript=$INSTALLATIONFOLDER/${basename}-eepromconfig.py +powerbuttonscript=$INSTALLATIONFOLDER/$daemonname.py +unitconfigfile=/etc/argonunits.conf +daemonconfigfile=/etc/$daemonname.conf +daemonfanservice=/lib/systemd/system/$daemonname.service + +daemonhddconfigfile=/etc/${daemonname}-hdd.conf + +echo "Installing/Updating scripts and services ..." + +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 + +if is_pifive +then + # UPS Config Script + sudo wget $ARGONDOWNLOADSERVER/scripts/argonone-upsconfig.sh -O $upsconfigscript --quiet + sudo chmod 755 $upsconfigscript +fi + +for TMPDIRECTORY in "/lib/systemd/system" "/lib/systemd/system-shutdown" +do + sudo mkdir -p "$TMPDIRECTORY" + sudo chmod 755 $TMPDIRECTORY + sudo chown root:root "$TMPDIRECTORY" +done + +# Fan Config Script +sudo wget $ARGONDOWNLOADSERVER/scripts/argonone-fanconfig.sh -O $fanconfigscript --quiet +sudo chmod 755 $fanconfigscript + + +# Fan Daemon/Service Files +sudo wget $ARGONDOWNLOADSERVER/scripts/argononed.py -O $powerbuttonscript --quiet +if [ "$CHECKDEVICE" = "oneoled" ] +then + sudo wget $ARGONDOWNLOADSERVER/scripts/argononeoledd.service -O $daemonfanservice --quiet +else + sudo wget $ARGONDOWNLOADSERVER/scripts/argononed.service -O $daemonfanservice --quiet +fi +sudo chmod 644 $daemonfanservice + +if [ ! "$CHECKDEVICE" = "fanhat" ] +then + # IR Files + sudo wget $ARGONDOWNLOADSERVER/scripts/argonone-irconfig.sh -O $irconfigscript --quiet + sudo chmod 755 $irconfigscript + + if [ ! "$CHECKDEVICE" = "eon" ] + then + sudo wget $ARGONDOWNLOADSERVER/scripts/argon-blstrdac.sh -O $blstrdacconfigscript --quiet + sudo chmod 755 $blstrdacconfigscript + fi +fi + +# Other utility scripts +sudo wget $ARGONDOWNLOADSERVER/scripts/argonstatus.py -O $INSTALLATIONFOLDER/argonstatus.py --quiet +sudo wget $ARGONDOWNLOADSERVER/scripts/argondashboard.py -O $INSTALLATIONFOLDER/argondashboard.py --quiet +sudo wget $ARGONDOWNLOADSERVER/scripts/argon-status.sh -O $statusdisplayscript --quiet +sudo chmod 755 $statusdisplayscript + + +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 + +if [ -f "$FLAGFILEV1" ] +then + sudo wget $ARGONDOWNLOADSERVER/scripts/argonregister-v1.py -O $INSTALLATIONFOLDER/argonregister.py --quiet +else + sudo wget $ARGONDOWNLOADSERVER/scripts/argonregister.py -O $INSTALLATIONFOLDER/argonregister.py --quiet +fi + +sudo wget "$ARGONDOWNLOADSERVER/scripts/argonpowerbutton-${CHECKGPIOMODE}.py" -O $INSTALLATIONFOLDER/argonpowerbutton.py --quiet + +sudo wget $ARGONDOWNLOADSERVER/scripts/argononed.py -O $powerbuttonscript --quiet + +sudo wget $ARGONDOWNLOADSERVER/scripts/argon-unitconfig.sh -O $unitconfigscript --quiet +sudo chmod 755 $unitconfigscript + + +# Generate default Fan config file if non-existent +if [ ! -f $daemonconfigfile ]; then + sudo touch $daemonconfigfile + sudo chmod 666 $daemonconfigfile + + echo '#' >> $daemonconfigfile + echo '# Argon Fan Speed Configuration (CPU)' >> $daemonconfigfile + echo '#' >> $daemonconfigfile + echo '55=30' >> $daemonconfigfile + echo '60=55' >> $daemonconfigfile + echo '65=100' >> $daemonconfigfile +fi + +if [ "$CHECKDEVICE" = "eon" ] +then + if [ ! -f $daemonhddconfigfile ]; then + sudo touch $daemonhddconfigfile + sudo chmod 666 $daemonhddconfigfile + + echo '#' >> $daemonhddconfigfile + echo '# Argon Fan Speed Configuration (HDD)' >> $daemonhddconfigfile + echo '#' >> $daemonhddconfigfile + echo '35=30' >> $daemonhddconfigfile + echo '40=55' >> $daemonhddconfigfile + echo '45=100' >> $daemonhddconfigfile + fi +fi + +# Generate default Unit config file if non-existent +if [ ! -f $unitconfigfile ]; then + sudo touch $unitconfigfile + sudo chmod 666 $unitconfigfile + + echo '#' >> $unitconfigfile +fi + + +if [ "$CHECKDEVICE" = "eon" ] +then + # RTC Setup + basename="argoneon" + daemonname=$basename"d" + + rtcconfigfile=/etc/argoneonrtc.conf + rtcconfigscript=$INSTALLATIONFOLDER/${basename}-rtcconfig.sh + daemonrtcservice=/lib/systemd/system/$daemonname.service + rtcdaemonscript=$INSTALLATIONFOLDER/$daemonname.py + + # Generate default RTC config file if non-existent + if [ ! -f $rtcconfigfile ]; then + sudo touch $rtcconfigfile + sudo chmod 666 $rtcconfigfile + + echo '#' >> $rtcconfigfile + echo '# Argon RTC Configuration' >> $rtcconfigfile + echo '#' >> $rtcconfigfile + fi + + # RTC Config Script + sudo wget $ARGONDOWNLOADSERVER/scripts/argoneon-rtcconfig.sh -O $rtcconfigscript --quiet + sudo chmod 755 $rtcconfigscript + + # RTC Daemon/Service Files + sudo wget $ARGONDOWNLOADSERVER/scripts/argonrtc.py -O $INSTALLATIONFOLDER/argonrtc.py --quiet + sudo wget $ARGONDOWNLOADSERVER/scripts/argoneond.py -O $rtcdaemonscript --quiet + sudo wget $ARGONDOWNLOADSERVER/scripts/argoneond.service -O $daemonrtcservice --quiet + sudo chmod 644 $daemonrtcservice +fi + + +if [ "$CHECKDEVICE" = "eon" ] || [ "$CHECKDEVICE" = "oneoled" ] +then + # OLED Setup + basename="argoneon" + daemonname=$basename"d" + + oledconfigscript=$INSTALLATIONFOLDER/${basename}-oledconfig.sh + oledlibscript=$INSTALLATIONFOLDER/${basename}oled.py + oledconfigfile=/etc/argoneonoled.conf + + # Generate default OLED config file if non-existent + if [ ! -f $oledconfigfile ]; then + sudo touch $oledconfigfile + sudo chmod 666 $oledconfigfile + + echo '#' >> $oledconfigfile + echo '# Argon OLED Configuration' >> $oledconfigfile + echo '#' >> $oledconfigfile + echo 'switchduration=30' >> $oledconfigfile + if [ "$CHECKDEVICE" = "eon" ] + then + echo 'screenlist="clock cpu storage raid ram temp ip"' >> $oledconfigfile + else + echo 'screenlist="logo1v5 clock cpu storage ram temp ip"' >> $oledconfigfile + fi + fi + + # OLED Config Script + if [ "$CHECKDEVICE" = "eon" ] + then + sudo wget $ARGONDOWNLOADSERVER/scripts/argoneonoled.py -O $oledlibscript --quiet + sudo wget $ARGONDOWNLOADSERVER/scripts/argoneon-oledconfig.sh -O $oledconfigscript --quiet + else + sudo wget $ARGONDOWNLOADSERVER/scripts/argononeoled.py -O $oledlibscript --quiet + sudo wget $ARGONDOWNLOADSERVER/scripts/argonone-oledconfig.sh -O $oledconfigscript --quiet + fi + sudo chmod 755 $oledconfigscript + + if [ ! -d $INSTALLATIONFOLDER/oled ] + then + sudo mkdir $INSTALLATIONFOLDER/oled + fi + + for binfile in font8x6 font16x12 font32x24 font64x48 font16x8 font24x16 font48x32 bgdefault bgram bgip bgtemp bgcpu bgraid bgstorage bgtime + do + sudo wget $ARGONDOWNLOADSERVER/oled/${binfile}.bin -O $INSTALLATIONFOLDER/oled/${binfile}.bin --quiet + done + + if [ "$CHECKDEVICE" = "oneoled" ] + then + for binfile in logo1v5 + do + sudo wget $ARGONDOWNLOADSERVER/oled/${binfile}.bin -O $INSTALLATIONFOLDER/oled/${binfile}.bin --quiet + done + fi +fi + + +# Argon Uninstall Script +sudo wget $ARGONDOWNLOADSERVER/scripts/argon-uninstall.sh -O $uninstallscript --quiet +sudo chmod 755 $uninstallscript + +# Argon Shutdown script +sudo wget $ARGONDOWNLOADSERVER/scripts/argon-shutdown.sh -O $shutdownscript --quiet +sudo chmod 755 $shutdownscript + +# 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 +if [ ! "$CHECKDEVICE" = "oneoled" ] +then + echo ' echo " 1. Configure Fan"' >> $configscript +fi + +blstrdacoption=0 + +if [ "$CHECKDEVICE" = "fanhat" ] +then + uninstalloption="4" +else + if [ ! "$CHECKDEVICE" = "oneoled" ] + then + echo ' echo " 2. Configure IR"' >> $configscript + fi + if [ "$CHECKDEVICE" = "eon" ] + then + # ArgonEON Has RTC + echo ' echo " 3. Configure RTC and/or Schedule"' >> $configscript + echo ' echo " 4. Configure OLED"' >> $configscript + uninstalloption="7" + elif [ "$CHECKDEVICE" = "oneoled" ] + then + echo ' echo " 1. Configure OLED"' >> $configscript + echo ' echo " 2. Argon Industria UPS"' >> $configscript + uninstalloption="5" + elif is_pifive + then + echo ' echo " 3. Argon Industria UPS"' >> $configscript + uninstalloption="7" + blstrdacoption=$(($uninstalloption-3)) + echo " echo \" $blstrdacoption. Configure BLSTR DAC (v3/v5 only)\"" >> $configscript + else + uninstalloption="6" + blstrdacoption=$(($uninstalloption-3)) + echo " echo \" $blstrdacoption. Configure BLSTR DAC (v3 only)\"" >> $configscript + fi +fi + +unitsoption=$(($uninstalloption-2)) +echo " echo \" $unitsoption. Configure Units\"" >> $configscript +statusoption=$(($uninstalloption-1)) +echo " echo \" $statusoption. System Information\"" >> $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 +if [ "$CHECKDEVICE" = "eon" ] +then + echo ' echo "Choose Triggers:"' >> $configscript + echo ' echo " 1. CPU Temperature"' >> $configscript + echo ' echo " 2. HDD Temperature"' >> $configscript + echo ' echo ""' >> $configscript + echo ' echo " 0. Cancel"' >> $configscript + echo " echo -n \"Enter Number (0-2):\"" >> $configscript + echo ' submode=$( get_number )' >> $configscript + + echo ' if [ $submode -eq 1 ]' >> $configscript + echo ' then' >> $configscript + echo " $fanconfigscript" >> $configscript + echo ' mainloopflag=0' >> $configscript + echo ' elif [ $submode -eq 2 ]' >> $configscript + echo ' then' >> $configscript + echo " $fanconfigscript hdd" >> $configscript + echo ' mainloopflag=0' >> $configscript + echo ' fi' >> $configscript +elif [ "$CHECKDEVICE" = "oneoled" ] +then + echo " $oledconfigscript" >> $configscript + echo ' mainloopflag=0' >> $configscript +else + echo " $fanconfigscript" >> $configscript + echo ' mainloopflag=0' >> $configscript +fi + +# Options 2 onwards +if [ ! "$CHECKDEVICE" = "fanhat" ] +then + if [ "$CHECKDEVICE" = "oneoled" ] + then + echo ' elif [ $newmode -eq 2 ]' >> $configscript + echo ' then' >> $configscript + echo " $upsconfigscript" >> $configscript + #echo ' mainloopflag=0' >> $configscript + + else + echo ' elif [ $newmode -eq 2 ]' >> $configscript + echo ' then' >> $configscript + echo " $irconfigscript" >> $configscript + #echo ' mainloopflag=0' >> $configscript + + if [ "$CHECKDEVICE" = "eon" ] + then + echo ' elif [ $newmode -eq 3 ]' >> $configscript + echo ' then' >> $configscript + echo " $rtcconfigscript" >> $configscript + #echo ' mainloopflag=0' >> $configscript + echo ' elif [ $newmode -eq 4 ]' >> $configscript + echo ' then' >> $configscript + echo " $oledconfigscript" >> $configscript + #echo ' mainloopflag=0' >> $configscript + elif is_pifive + then + echo ' elif [ $newmode -eq 3 ]' >> $configscript + echo ' then' >> $configscript + echo " $upsconfigscript" >> $configscript + #echo ' mainloopflag=0' >> $configscript + fi + + if [ $blstrdacoption -gt 0 ] + then + echo " elif [ \$newmode -eq $blstrdacoption ]" >> $configscript + echo ' then' >> $configscript + echo " $blstrdacconfigscript" >> $configscript + #echo ' mainloopflag=0' >> $configscript + fi + fi +fi + +# Standard options +echo " elif [ \$newmode -eq $unitsoption ]" >> $configscript +echo ' then' >> $configscript +echo " $unitconfigscript" >> $configscript +#echo ' mainloopflag=0' >> $configscript + +echo " elif [ \$newmode -eq $statusoption ]" >> $configscript +echo ' then' >> $configscript +echo " $statusdisplayscript" >> $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/argonone-config.desktop" +if [ -d "/home/$destfoldername/Desktop" ] +then + echo "Creating/Updating Desktop Elements ..." + + 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=ar1config.png + if [ "$CHECKDEVICE" = "eon" ] + then + imagefile=argoneon.png + fi + sudo wget https://download.argon40.com/$imagefile -O /usr/share/pixmaps/$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=/usr/share/pixmaps/$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)" + +echo "Initializing Services ..." + +if [ "$setupmode" = "Setup" ] +then + if [ -f "/usr/bin/$configcmd" ] + then + sudo rm /usr/bin/$configcmd + fi + sudo ln -s $configscript /usr/bin/$configcmd + + if [ "$CHECKDEVICE" = "one" ] || [ "$CHECKDEVICE" = "oneoled" ] + then + sudo ln -s $configscript /usr/bin/argonone-config + sudo ln -s $uninstallscript /usr/bin/argonone-uninstall + sudo ln -s $irconfigscript /usr/bin/argonone-ir + elif [ "$CHECKDEVICE" = "fanhat" ] + then + sudo ln -s $configscript /usr/bin/argonone-config + sudo ln -s $uninstallscript /usr/bin/argonone-uninstall + fi + + # Enable and Start Service(s) + sudo systemctl daemon-reload + sudo systemctl enable argononed.service + sudo systemctl start argononed.service + if [ "$CHECKDEVICE" = "eon" ] + then + sudo systemctl enable argoneond.service + sudo systemctl start argoneond.service + fi +else + sudo systemctl daemon-reload + sudo systemctl restart argononed.service + if [ "$CHECKDEVICE" = "eon" ] + then + sudo systemctl restart argoneond.service + fi +fi + + +if [ "$CHECKPLATFORM" = "Raspbian" ] +then + if [ -f "$eepromrpiscript" ] + then + echo "Checking EEPROM ..." + 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/source/argon40.png b/source/argon40.png new file mode 100644 index 0000000..d611b36 Binary files /dev/null and b/source/argon40.png differ diff --git a/source/argonone-irdecoder.py b/source/argonone-irdecoder.py new file mode 100644 index 0000000..b84c1b3 --- /dev/null +++ b/source/argonone-irdecoder.py @@ -0,0 +1,514 @@ +#!/usr/bin/python3 + +# Standard Headers +import sys +import smbus + +# For GPIO +import RPi.GPIO as GPIO +from datetime import datetime + +import os +import time + +# Check if Lirc Lib is installed +haslirclib = os.path.isfile("/usr/bin/mode2") +if haslirclib == True: + from multiprocessing import Process + +######################### +# Use GPIO + +def getGPIOPulseData(): + irreceiver_pin = 23 # IR Receiver Pin + GPIO.setmode(GPIO.BCM) + GPIO.setup(irreceiver_pin, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) + + # Counter + ctr = 0 + value = GPIO.input(irreceiver_pin) + + # mark time + startTime = datetime.now() + pulseTime = startTime + + # Pulse Data + pulsedata = [] + + aborted = False + while aborted == False: + # Wait for transition + try: + if value: + channel = GPIO.wait_for_edge(irreceiver_pin, GPIO.FALLING, timeout=PULSETIMEOUTMS) + else: + channel = GPIO.wait_for_edge(irreceiver_pin, GPIO.RISING, timeout=PULSETIMEOUTMS) + except Exception as e: + # GPIO Error + GPIO.cleanup() + return [(-2, -2)] + + if channel is None: + if ctr == 0: + continue + else: + aborted = True + if len(pulsedata) == 0: + # CTRL+C + return [(-1, -1)] + break + + # high/low Length + now = datetime.now() + pulseLength = now - pulseTime + pulseTime = now + + # Update value (changed triggered), this also inverts value before saving + if value: + value = 0 + else: + value = 1 + if pulseLength.microseconds > PULSETAIL_MAXMICROS_NEC and ctr == 0: + continue + + pulsedata.append((value, pulseLength.microseconds)) + + ctr = ctr + 1 + if pulseLength.microseconds > PULSETAIL_MAXMICROS_NEC: + break + elif ctr > PULSEDATA_MAXCOUNT: + break + + GPIO.cleanup() + # Data is most likely incomplete + if aborted == True: + return [] + elif ctr >= PULSEDATA_MAXCOUNT: + print (" * Unable to decode. Please try again *") + return [] + return pulsedata + + +######################### +# Use LIRC +def lircMode2Task(irlogfile): + os.system("mode2 > "+irlogfile+" 2>&1") + +def startLIRCMode2Logging(irlogfile): + # create a new process + loggerprocess = Process(target=lircMode2Task,args=(irlogfile,)) + loggerprocess.start() + # mode2 will start new process, terminate current + time.sleep(0.1) + loggerprocess.kill() + return True + +def endLIRCMode2Logging(irlogfile): + tmplogfile = irlogfile+".tmp" + os.system("ps | grep ode2 > "+tmplogfile+"") + + if os.path.exists(tmplogfile) == True: + ctr = 0 + fp = open(tmplogfile, "r") + for curline in fp: + if len(curline) > 0: + rowdata = curline.split(" ") + pid = "" + processname = "" + colidx = 0 + while colidx < len(rowdata): + if len(rowdata[colidx]) > 0: + if pid == "": + pid = rowdata[colidx] + else: + processname = rowdata[colidx] + + colidx = colidx + 1 + if processname=="mode2\n": + os.system("kill -9 "+pid) + fp.close() + os.remove(tmplogfile) + return True + +def getLIRCPulseData(): + if haslirclib == False: + print (" * LIRC Module not found, please reboot and try again *") + return [] + + irlogfile = "/dev/shm/lircdecoder.log" + + loggerresult = startLIRCMode2Logging(irlogfile) + if loggerresult == False: + return [(-1, -1)] + + # Wait for log file + logsize = 0 + while logsize == 0: + if os.path.exists(irlogfile) == True: + logsize = os.path.getsize(irlogfile) + if logsize == 0: + time.sleep(0.1) + + # Wait for data to start + newlogsize = logsize + while logsize == newlogsize: + time.sleep(0.1) + newlogsize = os.path.getsize(irlogfile) + + print(" Thank you") + + # Wait for data to stop + while logsize != newlogsize: + logsize = newlogsize + time.sleep(0.1) + newlogsize = os.path.getsize(irlogfile) + + # Finalize File + loggerresult = endLIRCMode2Logging(irlogfile) + if loggerresult == False: + return [(-1, -1)] + + # Decode logfile into Pulse Data + pulsedata = [] + + terminated = False + if os.path.exists(irlogfile) == True: + ctr = 0 + fp = open(irlogfile, "r") + for curline in fp: + if len(curline) > 0: + rowdata = curline.split(" ") + if len(rowdata) == 2: + duration = int(rowdata[1]) + value = 0 + if rowdata[0] == "pulse": + value = 1 + ctr = ctr + 1 + if value == 1 or ctr > 1: + if len(pulsedata) > 0 and duration > PULSELEADER_MINMICROS_NEC: + terminated = True + break + else: + pulsedata.append((value, duration)) + fp.close() + os.remove(irlogfile) + + # Check if terminating pulse detected + if terminated == False: + print (" * Unable to read signal. Please try again *") + return [] + return pulsedata + + +######################### +# Common +irconffile = "/etc/lirc/lircd.conf.d/argon.lircd.conf" + +# I2C +address = 0x1a # I2C Address +addressregister = 0xaa # I2C Address Register + +# Constants +PULSETIMEOUTMS = 1000 +VERIFYTARGET = 3 +PULSEDATA_MAXCOUNT = 200 # Fail safe + +# NEC Protocol Constants +PULSEBIT_MAXMICROS_NEC = 2500 +PULSEBIT_ZEROMICROS_NEC = 1000 + +PULSELEADER_MINMICROS_NEC = 8000 +PULSELEADER_MAXMICROS_NEC = 10000 +PULSETAIL_MAXMICROS_NEC = 12000 + +# Flags +FLAGV1ONLY = False + +try: + if os.path.isfile("/etc/argon/flag_v1"): + FLAGV1ONLY = True +except Exception: + FLAGV1ONLY = False + +# Standard Methods +def getbytestring(pulsedata): + outstring = "" + for curbyte in pulsedata: + tmpstr = hex(curbyte)[2:] + while len(tmpstr) < 2: + tmpstr = "0" + tmpstr + outstring = outstring+tmpstr + return outstring + +def displaybyte(pulsedata): + print (getbytestring(pulsedata)) + + +def pulse2byteNEC(pulsedata): + outdata = [] + bitdata = 1 + curbyte = 0 + bitcount = 0 + for (mode, duration) in pulsedata: + if mode == 1: + continue + elif duration > PULSEBIT_MAXMICROS_NEC: + continue + elif duration > PULSEBIT_ZEROMICROS_NEC: + curbyte = curbyte*2 + 1 + else: + curbyte = curbyte*2 + + bitcount = bitcount + 1 + if bitcount == 8: + outdata.append(curbyte) + curbyte = 0 + bitcount = 0 + # Shouldn't happen, but just in case + if bitcount > 0: + outdata.append(curbyte) + + return outdata + + +def bytecompare(a, b): + idx = 0 + maxidx = len(a) + if maxidx != len(b): + return 1 + while idx < maxidx: + if a[idx] != b[idx]: + return 1 + idx = idx + 1 + return 0 + + +# Main Flow +mode = "custom" +if len(sys.argv) > 1: + mode = sys.argv[1] + +powerdata = [] +buttonlist = ['POWER', 'UP', 'DOWN', 'LEFT', 'RIGHT', + 'VOLUMEUP', 'VOLUMEDOWN', 'OK', 'HOME', 'MENU' + 'BACK'] + +ircodelist = ['00ff39c6', '00ff53ac', '00ff4bb4', '00ff9966', '00ff837c', + '00ff01fe', '00ff817e', '00ff738c', '00ffd32c', '00ffb946', + '00ff09f6'] + +buttonidx = 0 + +if mode == "power": + buttonlist = ['POWER'] + ircodelist = [''] +elif mode == "resetpower": + # Just Set the power so it won't create/update the conf file + buttonlist = ['POWER'] + mode = "default" +elif mode == "custom": + buttonlist = ['POWER', 'UP', 'DOWN', 'LEFT', 'RIGHT', + 'VOLUMEUP', 'VOLUMEDOWN', 'OK', 'HOME', 'MENU' + 'BACK'] + ircodelist = ['', '', '', '', '', + '', '', '', '', '', + ''] + #buttonlist = ['POWER', 'VOLUMEUP', 'VOLUMEDOWN'] + #ircodelist = ['', '', ''] + +if mode == "default": + # To skip the decoding loop + buttonidx = len(buttonlist) + # Set MCU IR code + powerdata = [0x00, 0xff, 0x39, 0xc6] +else: + print ("************************************************") + print ("* WARNING: Current buttons are still active. *") + print ("* Please temporarily assign to a *") + print ("* different button if you plan to *") + print ("* reuse buttons. *") + print ("* e.g. Power Button triggers shutdown *") + print ("* *") + print ("* PROCEED AT YOUR OWN RISK *") + print ("* (Press CTRL+C to abort at any time) *") + print ("************************************************") + +readaborted = False +# decoding loop +while buttonidx < len(buttonlist): + print ("Press your button for "+buttonlist[buttonidx]+" (CTRL+C to abort)") + irprotocol = "" + outdata = [] + verifycount = 0 + readongoing = True + + # Handles NEC protocol Only + while readongoing == True: + # Try GPIO-based reading, if it fails, fallback to LIRC + pulsedata = getGPIOPulseData() + if len(pulsedata) == 1: + if pulsedata[0][0] == -2: + pulsedata = getLIRCPulseData() + + # Aborted + if len(pulsedata) == 1: + if pulsedata[0][0] == -1: + readongoing = False + readaborted = True + buttonidx = len(buttonlist) + break + # Ignore repeat code (NEC) + if len(pulsedata) <= 4: + continue + + # Get leading signal + (mode, duration) = pulsedata[0] + + # Decode IR Protocols + # https://www.sbprojects.net/knowledge/ir/index.php + + if duration >= PULSELEADER_MINMICROS_NEC and duration <= PULSELEADER_MAXMICROS_NEC: + irprotocol = "NEC" + # NEC has 9ms head, +/- 1ms + curdata = pulse2byteNEC(pulsedata) + if len(curdata) > 0: + if verifycount > 0: + if bytecompare(outdata, curdata) == 0: + verifycount = verifycount + 1 + else: + verifycount = 0 + else: + outdata = curdata + verifycount = 1 + + if verifycount >= VERIFYTARGET: + readongoing = False + print ("") + elif verifycount == 0: + print (" * IR code mismatch, please try again *") + elif VERIFYTARGET - verifycount > 1: + print (" Press the button "+ str(VERIFYTARGET - verifycount)+ " more times") + else: + print (" Press the button 1 more time") + else: + print (" * Decoding error. Please try again *") + else: + print (" * Unrecognized signal. Please try again *") + #curdata = pulse2byteLSB(pulsedata) + #displaybyte(curdata) + + # Check for duplicates + newircode = getbytestring(outdata) + if verifycount > 0: + checkidx = 0 + while checkidx < buttonidx and checkidx < len(buttonlist): + if ircodelist[checkidx] == newircode: + print (" Button already assigned. Please try again") + verifycount = 0 + break + checkidx = checkidx + 1 + + # Store code, and power button code if applicable + if verifycount > 0: + if buttonidx == 0: + powerdata = outdata + if buttonidx < len(buttonlist): + # Abort will cause out of bounds + ircodelist[buttonidx] = newircode + #print (buttonlist[buttonidx]+": "+ newircode) + buttonidx = buttonidx + 1 + +if len(powerdata) > 0 and readaborted == False: + # Send to device if completed or reset mode + #print("Writing " + getbytestring(powerdata)) + print("Updating Device...") + try: + bus=smbus.SMBus(1) + except Exception: + try: + # Older version + bus=smbus.SMBus(0) + except Exception: + bus=None + + if bus is None: + print("Device Update Failed: Unable to detect i2c") + else: + # Check for Argon Control Register Support + checkircodewrite = False + argoncyclereg = 0x80 + if FLAGV1ONLY == False: + oldval = bus.read_byte_data(address, argoncyclereg) + newval = oldval + 1 + if newval >= 100: + newval = 98 + bus.write_byte_data(address,argoncyclereg, newval) + time.sleep(1) + newval = bus.read_byte_data(address, argoncyclereg) + + if newval != oldval: + addressregister = 0x82 + checkircodewrite = True + bus.write_byte_data(address,argoncyclereg, oldval) + + bus.write_i2c_block_data(address, addressregister, powerdata) + + + if checkircodewrite == True: + # Check if data was written for devices that support it + print("Verifying ...") + time.sleep(2) + checkircodedata = bus.read_i2c_block_data(address, addressregister, 4) + checkircodecounter = 0 + while checkircodecounter < 4: + # Reuse readaborted flag as indicator if IR code was successfully updated + if checkircodedata[checkircodecounter] != powerdata[checkircodecounter]: + readaborted = True + checkircodecounter = checkircodecounter + 1 + if readaborted == False: + print("Device Update Successful") + else: + print("Verification Failed") + bus.close() + + # Update IR Conf if there are other button + if buttonidx > 1 and readaborted == False: + print("Updating Remote Control Codes...") + fp = open(irconffile, "w") + + # Standard NEC conf header + fp.write("#\n") + fp.write("# Based on NEC templates at http://lirc.sourceforge.net/remotes/nec/\n") + fp.write("# Configured codes based on data gathered\n") + fp.write("#\n") + fp.write("\n") + fp.write("begin remote\n") + fp.write(" name argon\n") + fp.write(" bits 32\n") + fp.write(" flags SPACE_ENC\n") + fp.write(" eps 20\n") + fp.write(" aeps 200\n") + fp.write("\n") + fp.write(" header 8800 4400\n") + fp.write(" one 550 1650\n") + fp.write(" zero 550 550\n") + fp.write(" ptrail 550\n") + fp.write(" repeat 8800 2200\n") + fp.write(" gap 38500\n") + fp.write(" toggle_bit 0\n") + fp.write("\n") + fp.write(" frequency 38000\n") + fp.write("\n") + fp.write(" begin codes\n") + + # Write Key Codes + buttonidx = 1 + while buttonidx < len(buttonlist): + fp.write(" KEY_"+buttonlist[buttonidx]+" 0x"+ircodelist[buttonidx]+"\n") + buttonidx = buttonidx + 1 + fp.write(" end codes\n") + fp.write("end remote\n") + fp.close() + + + diff --git a/source/argononeup.sh b/source/argononeup.sh new file mode 100644 index 0000000..8d667a3 --- /dev/null +++ b/source/argononeup.sh @@ -0,0 +1,471 @@ +#!/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 https://download.argon40.com/tools/setntpserver.sh | bash" +} + +if [ $NEEDSTIMESYNC -eq 1 ] +then + argon_time_error +fi + + +# Helper variables +ARGONDOWNLOADSERVER=https://download.argon40.com + +INSTALLATIONFOLDER=/etc/argon +pythonbin="sudo /usr/bin/python3" + +versioninfoscript=$INSTALLATIONFOLDER/argon-versioninfo.sh + +uninstallscript=$INSTALLATIONFOLDER/argon-uninstall.sh +configscript=$INSTALLATIONFOLDER/argon-config +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 +} + +set_external_antenna() { + set_config_var dtparam ant2 $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) + +echo "Installing/updating dependencies..." + +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 + +echo "Updating configuration ..." + +# 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 +userdaemonservice=/etc/systemd/user/${daemonname}user.service +daemonconfigfile=/etc/$daemonname.conf + +lidconfigscript=$INSTALLATIONFOLDER/${basename}-lidconfig.sh + + +for TMPDIRECTORY in "/lib/systemd/system" +do + sudo mkdir -p "$TMPDIRECTORY" + sudo chmod 755 $TMPDIRECTORY + sudo chown root:root "$TMPDIRECTORY" +done + +echo "Installing/Updating scripts and services ..." + +if [ ! -f $daemonconfigfile ]; then + # Generate config file for fan speed + sudo touch $daemonconfigfile + sudo chmod 666 $daemonconfigfile + echo '#' >> $daemonconfigfile + echo '# Argon One Up Configuration' >> $daemonconfigfile + echo '#' >> $daemonconfigfile + echo '# lidshutdownsecs number of seconds till shutdown when lid is closed 0 if do nothing' >> $daemonconfigfile + echo 'lidshutdownsecs=300' >> $daemonconfigfile +fi + +# Lid Config Script +sudo wget $ARGONDOWNLOADSERVER/scripts/argononeup-lidconfig.sh -O $lidconfigscript --quiet +sudo chmod 755 $lidconfigscript + + +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 + +sudo wget $ARGONDOWNLOADSERVER/scripts/${daemonname}user.service -O $userdaemonservice --quiet +sudo chmod 644 $userdaemonservice + + +# 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 + +sudo wget "$ARGONDOWNLOADSERVER/scripts/argonpowerbutton-${CHECKGPIOMODE}.py" -O $INSTALLATIONFOLDER/argonpowerbutton.py --quiet + +sudo wget $ARGONDOWNLOADSERVER/scripts/argonkeyboard.py -O $INSTALLATIONFOLDER/argonkeyboard.py --quiet + +# 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 +echo ' echo " 2. Configure Lid Behavior"' >> $configscript + + +uninstalloption="4" + +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 + +echo ' elif [ $newmode -eq 2 ]' >> $configscript +echo ' then' >> $configscript + +# Option 2 +echo " $lidconfigscript" >> $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 + echo "Creating/Updating Desktop Elements ..." + + 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 https://download.argon40.com/$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)" + +echo "Initializing Services ..." + +# Force remove lock files +sudo rm -f /dev/shm/argononeupkeyboardlock.txt +sudo rm -f /dev/shm/argononeupkeyboardlock.txt.a + +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 + +# Enable and Start User Service(s) +for tmpuser in `awk -F: '{ if ($3 >= 1000) print $1 }' /etc/passwd` +do + if [ "$tmpuser" != "nobody" ] + then + if [ "$setupmode" = "Setup" ] + then + sudo -u "$tmpuser" systemctl --user enable argononeupduser.service + sudo -u "$tmpuser" systemctl --user start argononeupduser.service + else + sudo -u "$tmpuser" systemctl --user restart argononeupduser.service + fi + fi +done + +# Current user / fallback +if [ "$setupmode" = "Setup" ] +then + systemctl --user enable argononeupduser.service + systemctl --user start argononeupduser.service +else + systemctl --user restart argononeupduser.service +fi + +if [ "$CHECKPLATFORM" = "Raspbian" ] +then + if [ -f "$eepromrpiscript" ] + then + echo "Checking EEPROM ..." + 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/source/firmware/ArgonOne.uf2 b/source/firmware/ArgonOne.uf2 new file mode 100644 index 0000000..357f66c Binary files /dev/null and b/source/firmware/ArgonOne.uf2 differ diff --git a/source/scripts/argon-rpi-eeprom-config-default.py b/source/scripts/argon-rpi-eeprom-config-default.py new file mode 100644 index 0000000..abb46a1 --- /dev/null +++ b/source/scripts/argon-rpi-eeprom-config-default.py @@ -0,0 +1,576 @@ +#!/usr/bin/env python3 + +# Based on /usr/bin/rpi-eeprom-config of bookworm +""" +rpi-eeprom-config +""" + +import argparse +import atexit +import os +import subprocess +import string +import struct +import sys +import tempfile +import time + +VALID_IMAGE_SIZES = [512 * 1024, 2 * 1024 * 1024] + +BOOTCONF_TXT = 'bootconf.txt' +BOOTCONF_SIG = 'bootconf.sig' +PUBKEY_BIN = 'pubkey.bin' + +# Each section starts with a magic number followed by a 32 bit offset to the +# next section (big-endian). +# The number, order and size of the sections depends on the bootloader version +# but the following mask can be used to test for section headers and skip +# unknown data. +# +# The last 4KB of the EEPROM image is reserved for internal use by the +# bootloader and may be overwritten during the update process. +MAGIC = 0x55aaf00f +PAD_MAGIC = 0x55aafeef +MAGIC_MASK = 0xfffff00f +FILE_MAGIC = 0x55aaf11f # id for modifiable files +FILE_HDR_LEN = 20 +FILENAME_LEN = 12 +TEMP_DIR = None + +# Modifiable files are stored in a single 4K erasable sector. +# The max content 4076 bytes because of the file header. +ERASE_ALIGN_SIZE = 4096 +MAX_FILE_SIZE = ERASE_ALIGN_SIZE - FILE_HDR_LEN + +DEBUG = False + +# BEGIN: Argon40 added methods +def argon_rpisupported(): + # bcm2711 = pi4, bcm2712 = pi5 + return rpi5() + +def argon_edit_config(): + # modified/stripped version of edit_config + + config_src = '' + # If there is a pending update then use the configuration from + # that in order to support incremental updates. Otherwise, + # use the current EEPROM configuration. + bootfs = shell_cmd(['rpi-eeprom-update', '-b']).rstrip() + pending = os.path.join(bootfs, 'pieeprom.upd') + if os.path.exists(pending): + config_src = pending + image = BootloaderImage(pending) + current_config = image.get_file(BOOTCONF_TXT).decode('utf-8') + else: + current_config, config_src = read_current_config() + + # Add NVMe boot priority etc if not yet set + foundnewsetting = 0 + addsetting="\nBOOT_UART=1\nWAKE_ON_GPIO=0\nPOWER_OFF_ON_HALT=1\nBOOT_ORDER=0xf416\nPCIE_PROBE=1" + current_config_lines = current_config.splitlines() + new_config = current_config + lineidx = 0 + while lineidx < len(current_config_lines): + current_config_pair = current_config_lines[lineidx].split("=") + newsetting = "" + if current_config_pair[0] == "BOOT_UART": + newsetting = "BOOT_UART=1" + elif current_config_pair[0] == "WAKE_ON_GPIO": + newsetting = "WAKE_ON_GPIO=0" + elif current_config_pair[0] == "POWER_OFF_ON_HALT": + newsetting = "POWER_OFF_ON_HALT=1" + elif current_config_pair[0] == "BOOT_ORDER": + newsetting = "BOOT_ORDER=0xf416" + elif current_config_pair[0] == "PCIE_PROBE": + newsetting = "PCIE_PROBE=1" + + if newsetting != "": + addsetting = addsetting.replace("\n"+newsetting,"",1) + if current_config_lines[lineidx] != newsetting: + foundnewsetting = foundnewsetting + 1 + new_config = new_config.replace(current_config_lines[lineidx], newsetting, 1) + + lineidx = lineidx + 1 + + if addsetting != "": + # Append additional settings after [all] + new_config = new_config.replace("[all]", "[all]"+addsetting, 1) + foundnewsetting = foundnewsetting + 1 + + if foundnewsetting == 0: + # Already configured + print("EEPROM settings up to date") + sys.exit(0) + + # Skipped editor and write new config to temp file + create_tempdir() + tmp_conf = os.path.join(TEMP_DIR, 'boot.conf') + out = open(tmp_conf, 'w') + out.write(new_config) + out.close() + + # Apply updates + + apply_update(tmp_conf, None, config_src) + +# END: Argon40 added methods + + +def debug(s): + if DEBUG: + sys.stderr.write(s + '\n') + + +def rpi4(): + compatible_path = "/sys/firmware/devicetree/base/compatible" + if os.path.exists(compatible_path): + with open(compatible_path, "rb") as f: + compatible = f.read().decode('utf-8') + if "bcm2711" in compatible: + return True + return False + +def rpi5(): + compatible_path = "/sys/firmware/devicetree/base/compatible" + if os.path.exists(compatible_path): + with open(compatible_path, "rb") as f: + compatible = f.read().decode('utf-8') + if "bcm2712" in compatible: + return True + return False + +def exit_handler(): + """ + Delete any temporary files. + """ + if TEMP_DIR is not None and os.path.exists(TEMP_DIR): + tmp_image = os.path.join(TEMP_DIR, 'pieeprom.upd') + if os.path.exists(tmp_image): + os.remove(tmp_image) + tmp_conf = os.path.join(TEMP_DIR, 'boot.conf') + if os.path.exists(tmp_conf): + os.remove(tmp_conf) + os.rmdir(TEMP_DIR) + +def create_tempdir(): + global TEMP_DIR + if TEMP_DIR is None: + TEMP_DIR = tempfile.mkdtemp() + +def pemtobin(infile): + """ + Converts an RSA public key into the format expected by the bootloader. + """ + # Import the package here to make this a weak dependency. + from Cryptodome.PublicKey import RSA + + arr = bytearray() + f = open(infile,'r') + key = RSA.importKey(f.read()) + + if key.size_in_bits() != 2048: + raise Exception("RSA key size must be 2048") + + # Export N and E in little endian format + arr.extend(key.n.to_bytes(256, byteorder='little')) + arr.extend(key.e.to_bytes(8, byteorder='little')) + return arr + +def exit_error(msg): + """ + Trapped a fatal error, output message to stderr and exit with non-zero + return code. + """ + sys.stderr.write("ERROR: %s\n" % msg) + sys.exit(1) + +def shell_cmd(args): + """ + Executes a shell command waits for completion returning STDOUT. If an + error occurs then exit and output the subprocess stdout, stderr messages + for debug. + """ + start = time.time() + arg_str = ' '.join(args) + result = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + while time.time() - start < 5: + if result.poll() is not None: + break + + if result.poll() is None: + exit_error("%s timeout" % arg_str) + + if result.returncode != 0: + exit_error("%s failed: %d\n %s\n %s\n" % + (arg_str, result.returncode, result.stdout.read(), result.stderr.read())) + else: + return result.stdout.read().decode('utf-8') + +def get_latest_eeprom(): + """ + Returns the path of the latest EEPROM image file if it exists. + """ + latest = shell_cmd(['rpi-eeprom-update', '-l']).rstrip() + if not os.path.exists(latest): + exit_error("EEPROM image '%s' not found" % latest) + return latest + +def apply_update(config, eeprom=None, config_src=None): + """ + Applies the config file to the latest available EEPROM image and spawns + rpi-eeprom-update to schedule the update at the next reboot. + """ + if eeprom is not None: + eeprom_image = eeprom + else: + eeprom_image = get_latest_eeprom() + create_tempdir() + + # Replace the contents of bootconf.txt with the contents of the config file + tmp_update = os.path.join(TEMP_DIR, 'pieeprom.upd') + image = BootloaderImage(eeprom_image, tmp_update) + image.update_file(config, BOOTCONF_TXT) + image.write() + + config_str = open(config).read() + if config_src is None: + config_src = '' + sys.stdout.write("Updating bootloader EEPROM\n image: %s\nconfig_src: %s\nconfig: %s\n%s\n%s\n%s\n" % + (eeprom_image, config_src, config, '#' * 80, config_str, '#' * 80)) + + sys.stdout.write("\n*** To cancel this update run 'sudo rpi-eeprom-update -r' ***\n\n") + + # Ignore APT package checksums so that this doesn't fail when used + # with EEPROMs with configs delivered outside of APT. + # The checksums are really just a safety check for automatic updates. + args = ['rpi-eeprom-update', '-d', '-i', '-f', tmp_update] + resp = shell_cmd(args) + sys.stdout.write(resp) + +def edit_config(eeprom=None): + """ + Implements something like 'git commit' for editing EEPROM configs. + """ + # Default to nano if $EDITOR is not defined. + editor = 'nano' + if 'EDITOR' in os.environ: + editor = os.environ['EDITOR'] + + config_src = '' + # If there is a pending update then use the configuration from + # that in order to support incremental updates. Otherwise, + # use the current EEPROM configuration. + bootfs = shell_cmd(['rpi-eeprom-update', '-b']).rstrip() + pending = os.path.join(bootfs, 'pieeprom.upd') + if os.path.exists(pending): + config_src = pending + image = BootloaderImage(pending) + current_config = image.get_file(BOOTCONF_TXT).decode('utf-8') + else: + current_config, config_src = read_current_config() + + create_tempdir() + tmp_conf = os.path.join(TEMP_DIR, 'boot.conf') + out = open(tmp_conf, 'w') + out.write(current_config) + out.close() + cmd = "\'%s\' \'%s\'" % (editor, tmp_conf) + result = os.system(cmd) + if result != 0: + exit_error("Aborting update because \'%s\' exited with code %d." % (cmd, result)) + + new_config = open(tmp_conf, 'r').read() + if len(new_config.splitlines()) < 2: + exit_error("Aborting update because \'%s\' appears to be empty." % tmp_conf) + apply_update(tmp_conf, eeprom, config_src) + +def read_current_config(): + """ + Reads the configuration used by the current bootloader. + """ + fw_base = "/sys/firmware/devicetree/base/" + nvmem_base = "/sys/bus/nvmem/devices/" + + if os.path.exists(fw_base + "/aliases/blconfig"): + with open(fw_base + "/aliases/blconfig", "rb") as f: + nvmem_ofnode_path = fw_base + f.read().decode('utf-8') + for d in os.listdir(nvmem_base): + if os.path.realpath(nvmem_base + d + "/of_node") in os.path.normpath(nvmem_ofnode_path): + return (open(nvmem_base + d + "/nvmem", "rb").read().decode('utf-8'), "blconfig device") + + return (shell_cmd(['vcgencmd', 'bootloader_config']), "vcgencmd bootloader_config") + +class ImageSection: + def __init__(self, magic, offset, length, filename=''): + self.magic = magic + self.offset = offset + self.length = length + self.filename = filename + debug("ImageSection %x offset %d length %d %s" % (magic, offset, length, filename)) + +class BootloaderImage(object): + def __init__(self, filename, output=None): + """ + Instantiates a Bootloader image writer with a source eeprom (filename) + and optionally an output filename. + """ + self._filename = filename + self._sections = [] + self._image_size = 0 + try: + self._bytes = bytearray(open(filename, 'rb').read()) + except IOError as err: + exit_error("Failed to read \'%s\'\n%s\n" % (filename, str(err))) + self._out = None + if output is not None: + self._out = open(output, 'wb') + + self._image_size = len(self._bytes) + if self._image_size not in VALID_IMAGE_SIZES: + exit_error("%s: Expected size %d bytes actual size %d bytes" % + (filename, self._image_size, len(self._bytes))) + self.parse() + + def parse(self): + """ + Builds a table of offsets to the different sections in the EEPROM. + """ + offset = 0 + magic = 0 + while offset < self._image_size: + magic, length = struct.unpack_from('>LL', self._bytes, offset) + if magic == 0x0 or magic == 0xffffffff: + break # EOF + elif (magic & MAGIC_MASK) != MAGIC: + raise Exception('EEPROM is corrupted %x %x %x' % (magic, magic & MAGIC_MASK, MAGIC)) + + filename = '' + if magic == FILE_MAGIC: # Found a file + # Discard trailing null characters used to pad filename + filename = self._bytes[offset + 8: offset + FILE_HDR_LEN].decode('utf-8').replace('\0', '') + debug("section at %d length %d magic %08x %s" % (offset, length, magic, filename)) + self._sections.append(ImageSection(magic, offset, length, filename)) + + offset += 8 + length # length + type + offset = (offset + 7) & ~7 + + def find_file(self, filename): + """ + Returns the offset, length and whether this is the last section in the + EEPROM for a modifiable file within the image. + """ + offset = -1 + length = -1 + is_last = False + + next_offset = self._image_size - ERASE_ALIGN_SIZE # Don't create padding inside the bootloader scratch page + for i in range(0, len(self._sections)): + s = self._sections[i] + if s.magic == FILE_MAGIC and s.filename == filename: + is_last = (i == len(self._sections) - 1) + offset = s.offset + length = s.length + break + + # Find the start of the next non padding section + i += 1 + while i < len(self._sections): + if self._sections[i].magic == PAD_MAGIC: + i += 1 + else: + next_offset = self._sections[i].offset + break + ret = (offset, length, is_last, next_offset) + debug('%s offset %d length %d is-last %d next %d' % (filename, ret[0], ret[1], ret[2], ret[3])) + return ret + + def update(self, src_bytes, dst_filename): + """ + Replaces a modifiable file with specified byte array. + """ + hdr_offset, length, is_last, next_offset = self.find_file(dst_filename) + update_len = len(src_bytes) + FILE_HDR_LEN + + if hdr_offset + update_len > self._image_size - ERASE_ALIGN_SIZE: + raise Exception('No space available - image past EOF.') + + if hdr_offset < 0: + raise Exception('Update target %s not found' % dst_filename) + + if hdr_offset + update_len > next_offset: + raise Exception('Update %d bytes is larger than section size %d' % (update_len, next_offset - hdr_offset)) + + new_len = len(src_bytes) + FILENAME_LEN + 4 + struct.pack_into('>L', self._bytes, hdr_offset + 4, new_len) + struct.pack_into(("%ds" % len(src_bytes)), self._bytes, + hdr_offset + 4 + FILE_HDR_LEN, src_bytes) + + # If the new file is smaller than the old file then set any old + # data which is now unused to all ones (erase value) + pad_start = hdr_offset + 4 + FILE_HDR_LEN + len(src_bytes) + + # Add padding up to 8-byte boundary + while pad_start % 8 != 0: + struct.pack_into('B', self._bytes, pad_start, 0xff) + pad_start += 1 + + # Create a padding section unless the padding size is smaller than the + # size of a section head. Padding is allowed in the last section but + # by convention bootconf.txt is the last section and there's no need to + # pad to the end of the sector. This also ensures that the loopback + # config read/write tests produce identical binaries. + pad_bytes = next_offset - pad_start + if pad_bytes > 8 and not is_last: + pad_bytes -= 8 + struct.pack_into('>i', self._bytes, pad_start, PAD_MAGIC) + pad_start += 4 + struct.pack_into('>i', self._bytes, pad_start, pad_bytes) + pad_start += 4 + + debug("pad %d" % pad_bytes) + pad = 0 + while pad < pad_bytes: + struct.pack_into('B', self._bytes, pad_start + pad, 0xff) + pad = pad + 1 + + def update_key(self, src_pem, dst_filename): + """ + Replaces the specified public key entry with the public key values extracted + from the source PEM file. + """ + pubkey_bytes = pemtobin(src_pem) + self.update(pubkey_bytes, dst_filename) + + def update_file(self, src_filename, dst_filename): + """ + Replaces the contents of dst_filename in the EEPROM with the contents of src_file. + """ + src_bytes = open(src_filename, 'rb').read() + if len(src_bytes) > MAX_FILE_SIZE: + raise Exception("src file %s is too large (%d bytes). The maximum size is %d bytes." + % (src_filename, len(src_bytes), MAX_FILE_SIZE)) + self.update(src_bytes, dst_filename) + + def write(self): + """ + Writes the updated EEPROM image to stdout or the specified output file. + """ + if self._out is not None: + self._out.write(self._bytes) + self._out.close() + else: + if hasattr(sys.stdout, 'buffer'): + sys.stdout.buffer.write(self._bytes) + else: + sys.stdout.write(self._bytes) + + def get_file(self, filename): + hdr_offset, length, is_last, next_offset = self.find_file(filename) + offset = hdr_offset + 4 + FILE_HDR_LEN + file_bytes = self._bytes[offset:offset+length-FILENAME_LEN-4] + return file_bytes + + def extract_files(self): + for i in range(0, len(self._sections)): + s = self._sections[i] + if s.magic == FILE_MAGIC: + file_bytes = self.get_file(s.filename) + open(s.filename, 'wb').write(file_bytes) + + def read(self): + config_bytes = self.get_file('bootconf.txt') + if self._out is not None: + self._out.write(config_bytes) + self._out.close() + else: + if hasattr(sys.stdout, 'buffer'): + sys.stdout.buffer.write(config_bytes) + else: + sys.stdout.write(config_bytes) + +def main(): + """ + Utility for reading and writing the configuration file in the + Raspberry Pi bootloader EEPROM image. + """ + description = """\ +Bootloader EEPROM configuration tool for the Raspberry Pi 4 and Raspberry Pi 5. +Operating modes: + +1. Outputs the current bootloader configuration to STDOUT if no arguments are + specified OR the given output file if --out is specified. + + rpi-eeprom-config [--out boot.conf] + +2. Extracts the configuration file from the given 'eeprom' file and outputs + the result to STDOUT or the output file if --output is specified. + + rpi-eeprom-config pieeprom.bin [--out boot.conf] + +3. Writes a new EEPROM image replacing the configuration file with the contents + of the file specified by --config. + + rpi-eeprom-config --config boot.conf --out newimage.bin pieeprom.bin + + The new image file can be installed via rpi-eeprom-update + rpi-eeprom-update -d -f newimage.bin + +4. Applies a given config file to an EEPROM image and invokes rpi-eeprom-update + to schedule an update of the bootloader when the system is rebooted. + + Since this command launches rpi-eeprom-update to schedule the EEPROM update + it must be run as root. + + sudo rpi-eeprom-config --apply boot.conf [pieeprom.bin] + + If the 'eeprom' argument is not specified then the latest available image + is selected by calling 'rpi-eeprom-update -l'. + +5. The '--edit' parameter behaves the same as '--apply' except that instead of + applying a predefined configuration file a text editor is launched with the + contents of the current EEPROM configuration. + + Since this command launches rpi-eeprom-update to schedule the EEPROM update + it must be run as root. + + The configuration file will be taken from: + * The blconfig reserved memory nvmem device + * The cached bootloader configuration 'vcgencmd bootloader_config' + * The current pending update - typically /boot/pieeprom.upd + + sudo -E rpi-eeprom-config --edit [pieeprom.bin] + + To cancel the pending update run 'sudo rpi-eeprom-update -r' + + The default text editor is nano and may be overridden by setting the 'EDITOR' + environment variable and passing '-E' to 'sudo' to preserve the environment. + +6. Signing the bootloader config file. + Updates an EEPROM binary with a signed config file (created by rpi-eeprom-digest) plus + the corresponding RSA public key. + + Requires Python Cryptodomex libraries and OpenSSL. To install on Raspberry Pi OS run:- + sudo apt install openssl python-pip + sudo python3 -m pip install cryptodomex + + rpi-eeprom-digest -k private.pem -i bootconf.txt -o bootconf.sig + rpi-eeprom-config --config bootconf.txt --digest bootconf.sig --pubkey public.pem --out pieeprom-signed.bin pieeprom.bin + + Currently, the signing process is a separate step so can't be used with the --edit or --apply modes. + + +See 'rpi-eeprom-update -h' for more information about the available EEPROM images. +""" + + if os.getuid() != 0: + exit_error("Please run as root") + elif not argon_rpisupported(): + # Skip + sys.exit(0) + argon_edit_config() + +if __name__ == '__main__': + atexit.register(exit_handler) + main() diff --git a/source/scripts/argonkeyboard.py b/source/scripts/argonkeyboard.py new file mode 100644 index 0000000..8d460cd --- /dev/null +++ b/source/scripts/argonkeyboard.py @@ -0,0 +1,824 @@ +#!/usr/bin/python3 + +# +# This script monitor battery via ic2 and keyboard events. +# +# Additional comments are found in each function below +# +# + + +from evdev import InputDevice, categorize, ecodes, list_devices +from select import select + +import subprocess + +import sys +import os +import time + +from threading import Thread +from queue import Queue + + +UPS_LOGFILE="/dev/shm/upslog.txt" +KEYBOARD_LOCKFILE="/dev/shm/argononeupkeyboardlock.txt" + + +KEYCODE_BRIGHTNESSUP = "KEY_BRIGHTNESSUP" +KEYCODE_BRIGHTNESSDOWN = "KEY_BRIGHTNESSDOWN" +KEYCODE_VOLUMEUP = "KEY_VOLUMEUP" +KEYCODE_VOLUMEDOWN = "KEY_VOLUMEDOWN" +KEYCODE_PAUSE = "KEY_PAUSE" +KEYCODE_MUTE = "KEY_MUTE" + + +################### +# Utilty Functions +################### + +# Debug Logger +def debuglog(typestr, logstr): + return + # try: + # DEBUGFILE="/dev/shm/argononeupkeyboarddebuglog.txt" + # tmpstrpadding = " " + + # with open(DEBUGFILE, "a") as txt_file: + # txt_file.write("["+time.asctime(time.localtime(time.time()))+"] "+typestr.upper()+" "+logstr.strip().replace("\n","\n"+tmpstrpadding)+"\n") + # except: + # pass + +def runcmdlist(key, cmdlist): + try: + cmdresult = subprocess.run(cmdlist, + capture_output=True, + text=True, + check=True + ) + #debuglog(key+"-result-output",str(cmdresult.stdout)) + if cmdresult.stderr: + debuglog(key+"-result-error",str(cmdresult.stderr)) + #debuglog(key+"-result-code",str(cmdresult.returncode)) + + except subprocess.CalledProcessError as e: + debuglog(key+"-error-output",str(e.stdout)) + if e.stderr: + debuglog(key+"-error-error",str(e.stderr)) + debuglog(key+"-error-code",str(e.returncode)) + except FileNotFoundError: + debuglog(key+"-error-filenotfound","Command Not Found") + except Exception as othererr: + try: + debuglog(key+"-error-other", str(othererr)) + except: + debuglog(key+"-error-other", "Other Error") + +def createlockfile(fname): + # try: + # if os.path.isfile(fname): + # return True + # except Exception as checklockerror: + # try: + # debuglog("keyboard-lock-error", str(checklockerror)) + # except: + # debuglog("keyboard-lock-error", "Error Checking Lock File") + # try: + # with open(fname, "w") as txt_file: + # txt_file.write(time.asctime(time.localtime(time.time()))+"\n") + # except Exception as lockerror: + # try: + # debuglog("keyboard-lock-error", str(lockerror)) + # except: + # debuglog("keyboard-lock-error", "Error Creating Lock File") + return False + +def deletelockfile(fname): + # try: + # os.remove(fname) + # except Exception as lockerror: + # try: + # debuglog("keyboard-lock-error", str(lockerror)) + # except: + # debuglog("keyboard-lock-error", "Error Removing Lock File") + return True + + +# System Notifcation +def notifymessage(message, iscritical): + if not isinstance(message, str) or len(message.strip()) == 0: + return + + wftype="notify" + if iscritical: + wftype="critical" + os.system("export SUDO_UID=1000; wfpanelctl "+wftype+" \""+message+"\"") + os.system("export DISPLAY=:0.0; lxpanelctl notify \""+message+"\"") + + +############# +# Battery (copied) +############# + +def battery_loadlogdata(): + # status, version, time, schedule + outobj = {} + try: + fp = open(UPS_LOGFILE, "r") + logdata = fp.read() + alllines = logdata.split("\n") + ctr = 0 + while ctr < len(alllines): + tmpval = alllines[ctr].strip() + curinfo = tmpval.split(":") + if len(curinfo) > 1: + tmpattrib = curinfo[0].lower().split(" ") + # The rest are assumed to be value + outobj[tmpattrib[0]] = tmpval[(len(curinfo[0])+1):].strip() + ctr = ctr + 1 + except Exception as einit: + try: + debuglog("keyboard-battery-error", str(einit)) + except: + debuglog("keyboard-battery-error", "Error getting battery status") + #pass + + return outobj + + +def keyboardevent_getdevicepaths(): + outlist = [] + try: + for path in list_devices(): + try: + tmpdevice = InputDevice(path) + keyeventlist = tmpdevice.capabilities().get(ecodes.EV_KEY, []) + # Keyboard has EV_KEY (key) and EV_REP (autorepeat) + if ecodes.KEY_BRIGHTNESSDOWN in keyeventlist and ecodes.KEY_BRIGHTNESSDOWN in keyeventlist: + outlist.append(path) + #debuglog("keyboard-device-keys", path) + #debuglog("keyboard-device-keys", str(keyeventlist)) + elif ecodes.KEY_F2 in keyeventlist and ecodes.KEY_F3 in keyeventlist: + # Keyboards with FN key sometimes do not include KEY_BRIGHTNESS in declaration + outlist.append(path) + #debuglog("keyboard-device-keys", path) + #debuglog("keyboard-device-keys", str(keyeventlist)) + tmpdevice.close() + except: + pass + except: + pass + return outlist + +def keyboardevent_devicechanged(curlist, newlist): + try: + for curpath in curlist: + if curpath not in newlist: + return True + for newpath in newlist: + if newpath not in curlist: + return True + except: + pass + return False + +def keyboardevent_getbrigthnesstoolid(): + toolid = 0 + try: + output = subprocess.check_output(["ddcutil", "--version"], text=True, stderr=subprocess.DEVNULL) + lines = output.splitlines() + if len(lines) > 0: + tmpline = lines[0].strip() + toolid = int(tmpline.split(" ")[1].split(".")[0]) + except Exception as einit: + try: + debuglog("keyboard-brightness-tool-error", str(einit)) + except: + debuglog("keyboard-brightness-tool-error", "Error getting tool id value") + + debuglog("keyboard-brightness-tool", toolid) + return toolid + +def keyboardevent_getbrigthnessinfo(toolid, defaultlevel=50): + level = defaultlevel + try: + # VCP code x10(Brightness ): current value = 90, max value = 100 + if toolid > 1: + # Disabled dynamic sleep "--disable-dynamic-sleep", "--sleep-multiplier", "0.1" + output = subprocess.check_output(["ddcutil", "--skip-ddc-checks", "--disable-dynamic-sleep", "--sleep-multiplier", "0.1", "getvcp", "10"], text=True, stderr=subprocess.DEVNULL) + else: + output = subprocess.check_output(["ddcutil", "--sleep-multiplier", "0.1", "getvcp", "10"], text=True, stderr=subprocess.DEVNULL) + debuglog("keyboard-brightness-info", output) + level = int(output.split(":")[-1].split(",")[0].split("=")[-1].strip()) + except Exception as einit: + try: + debuglog("keyboard-brightness-error", str(einit)) + except: + debuglog("keyboard-brightness-error", "Error getting base value") + + + return { + "level": level + } + + +def keyboardevent_adjustbrigthness(toolid, baselevel, adjustval=5): + curlevel = baselevel + if adjustval == 0: + return { + "level": baselevel + } + + # Moved reading because ddcutil has delay + # try: + # tmpobj = keyboardevent_getbrigthnessinfo(toolid, curlevel) + # curlevel = tmpobj["level"] + # except Exception: + # pass + + tmpval = max(10, min(100, curlevel + adjustval)) + if tmpval != curlevel: + try: + debuglog("keyboard-brightness", str(curlevel)+"% to "+str(tmpval)+"%") + if toolid > 1: + # Disabled dynamic sleep "--disable-dynamic-sleep", "--sleep-multiplier", "0.1" + runcmdlist("brightness", ["ddcutil", "--skip-ddc-checks", "--disable-dynamic-sleep", "--sleep-multiplier", "0.1", "setvcp", "10", str(tmpval)]) + else: + runcmdlist("brightness", ["ddcutil", "--sleep-multiplier", "0.1", "setvcp", "10", str(tmpval)]) + notifymessage("Brightness: "+str(tmpval)+"%", False) + except Exception as adjusterr: + try: + debuglog("keyboard-brightness-error", str(adjusterr)) + except: + debuglog("keyboard-brightness-error", "Error adjusting value") + return { + "level": curlevel + } + + # DEBUG: Checking + #keyboardevent_getbrigthnessinfo(toolid, tmpval) + return { + "level": tmpval + } + + +def keyboardevent_getvolumesinkid(usedefault=True): + if usedefault == True: + return "@DEFAULT_SINK@" + cursinkid = 0 + try: + output = subprocess.check_output(["wpctl", "status"], text=True, encoding='utf-8', stderr=subprocess.DEVNULL) + + # Find Audio section + tmpline = "" + foundidx = 0 + lines = output.splitlines() + lineidx = 0 + while lineidx < len(lines): + tmpline = lines[lineidx].strip() + if tmpline == "Audio": + foundidx = lineidx + break + lineidx = lineidx + 1 + + if foundidx < 1: + return 0 + + # Find Sinks section + foundidx = 0 + lineidx = lineidx + 1 + while lineidx < len(lines): + if "Sinks:" in lines[lineidx]: + foundidx = lineidx + break + elif len(lines[lineidx]) < 1: + break + lineidx = lineidx + 1 + + if foundidx < 1: + return 0 + + # Get find default id, or first id + lineidx = lineidx + 1 + while lineidx < len(lines): + if "vol:" in lines[lineidx] and "." in lines[lineidx]: + tmpstr = lines[lineidx].split(".")[0] + tmplist = tmpstr.split() + if len(tmplist) > 1: + if tmplist[len(tmplist)-2] == "*": + return int(tmplist[len(tmplist)-1]) + if len(tmplist) > 0 and cursinkid < 1: + cursinkid = int(tmplist[len(tmplist)-1]) + elif len(lines[lineidx]) < 3: + break + lineidx = lineidx + 1 + except Exception as einit: + try: + debuglog("keyboard-volume-error", str(einit)) + except: + debuglog("keyboard-volume-error", "Error getting device ID") + + return cursinkid + + +def keyboardevent_getvolumeinfo(deviceidstr="", defaultlevel=50, defaultmuted=0): + muted = defaultmuted + level = defaultlevel + try: + if deviceidstr == "": + audioidstr = str(keyboardevent_getvolumesinkid()) + if audioidstr == "0": + debuglog("keyboard-volume-error", "Error getting device id") + return { + "level": defaultmuted, + "muted": defaultlevel + } + + deviceidstr = audioidstr + + output = subprocess.check_output(["wpctl", "get-volume", deviceidstr], text=True, stderr=subprocess.DEVNULL) + debuglog("keyboard-volume-info", output) + + muted = 0 + level = 0 + # Parse output, examples + # Volume: 0.65 + # Volume: 0.55 [MUTED] + outlist = output.split() + if len(outlist) > 0: + # Get last element + tmpstr = outlist[len(outlist)-1] + # Check if muted + if "MUTE" in tmpstr: + muted = 1 + if len(outlist) > 1: + tmpstr = outlist[len(outlist)-2] + if tmpstr.endswith("%"): + # Level 100% to 0% + level = int(float(tmpstr[:-1])) + elif tmpstr.replace('.', '').isdigit(): + # Level 1.00 to 0.00 + level = int(float(tmpstr) * 100.0) + except Exception as einit: + try: + debuglog("keyboard-volume-error", str(einit)) + except: + debuglog("keyboard-volume-error", "Error getting base value") + return { + "level": defaultmuted, + "muted": defaultlevel + } + + #debuglog("keyboard-volume-get", str(level)+"% Mute:"+str(muted)) + + return { + "level": level, + "muted": muted + } + + +def keyboardevent_adjustvolume(baselevel, basemuted, adjustval=5): + curlevel = baselevel + curmuted = basemuted + needsnotification = False + + deviceidstr = str(keyboardevent_getvolumesinkid()) + if deviceidstr == "0": + debuglog("keyboard-volume-error", "Error getting device id") + return { + "level": baselevel, + "muted": basemuted + } + + # try: + # tmpobj = keyboardevent_getvolumeinfo(deviceidstr, curlevel, curmuted) + # curlevel = tmpobj["level"] + # curmuted = tmpobj["muted"] + # except Exception: + # pass + + tmpmuted = curmuted + if adjustval == 0: + # Toggle Mute + if curmuted == 0: + tmpmuted = 1 + else: + tmpmuted = 0 + + tmpval = max(10, min(100, curlevel + adjustval)) + if tmpval != curlevel: + try: + debuglog("keyboard-volume", str(curlevel)+"% to "+str(tmpval)+"%") + runcmdlist("volume", ["wpctl", "set-volume", deviceidstr, f"{tmpval}%"]) + needsnotification = True + tmpmuted = 0 + except Exception as adjusterr: + try: + debuglog("keyboard-volume-error", str(adjusterr)) + except: + debuglog("keyboard-volume-error", "Error adjusting value") + return { + "level": curlevel, + "muted": curmuted + } + elif adjustval != 0: + # To unmute even if no volume level change + tmpmuted = 0 + + if tmpmuted != curmuted: + try: + debuglog("keyboard-mute", str(tmpmuted)) + runcmdlist("mute", ["wpctl", "set-mute", deviceidstr, str(tmpmuted)]) + needsnotification = True + except Exception as adjusterr: + try: + debuglog("keyboard-mute-error", str(adjusterr)) + except: + debuglog("keyboard-mute-error", "Error adjusting value") + return { + "level": tmpval, + "muted": curmuted + } + #if tmpmuted == 1: + # notifymessage("Volume: Muted", False) + #else: + # notifymessage("Volume: "+str(tmpval)+"%", False) + + # DEBUG: Checking + #keyboardevent_getvolumeinfo(deviceidstr, tmpval, tmpmuted) + + return { + "level": tmpval, + "muted": tmpmuted + } + +def keyboard_getlayoutfieldvalue(tmpval): + debuglog("keyboard-layout-lang", tmpval) + if "us" in tmpval: + debuglog("keyboard-layout-langout", "us") + return "us" + debuglog("keyboard-layout-langout", "gb") + return "gb" # uk, gb, etc + #return tmpval + + +def keyboard_getdevicefw(kbdevice): + # info: vendor 0x6080=24704, product 0x8062=32866 + try: + if kbdevice.info.vendor == 24704 and kbdevice.info.product == 32866: + # Special HID + return "314" + except Exception as infoerr: + pass + + return "" + + +def keyboardevemt_keyhandler(readq): + + ADJUSTTYPE_NONE=0 + ADJUSTTYPE_BRIGHTNESS=1 + ADJUSTTYPE_VOLUME=2 + ADJUSTTYPE_MUTE=3 + ADJUSTTYPE_BATTERYINFO=4 + + DATAREFRESHINTERVALSEC = 10 + + PRESSWAITINTERVALSEC = 0.5 + FIRSTHOLDINTERVALSEC = 0.5 + HOLDWAITINTERVALSEC = 0.5 + + + # Get current levels + volumetime = time.time() + curvolumemuted = 0 + curvolume = 50 + + brightnesstime = volumetime + curbrightness = 50 + brightnesstoolid = 0 + + try: + brightnesstoolid = keyboardevent_getbrigthnesstoolid() + except Exception: + brightnesstoolid = 0 + pass + + try: + tmpobj = keyboardevent_getbrigthnessinfo(brightnesstoolid) + curbrightness = tmpobj["level"] + except Exception: + pass + + try: + tmpobj = keyboardevent_getvolumeinfo() + curvolumemuted = tmpobj["muted"] + curvolume = tmpobj["level"] + except Exception: + pass + + while True: + try: + tmpkeymode = 0 + tmpkeycode = "" + adjustval = 0 + adjusttype = ADJUSTTYPE_NONE + + tmpcode = readq.get() # Blocking + try: + codeelements = tmpcode.split("+") + if len(codeelements) == 2: + if codeelements[0] == "PRESS": + tmpkeymode = 1 + else: + tmpkeymode = 2 + tmpkeycode = codeelements[1] + elif tmpcode == "EXIT": + readq.task_done() + return + + except Exception: + tmpkeycode = "" + tmpkeymode = 0 + pass + tmptime = time.time() + if tmpkeycode in [KEYCODE_BRIGHTNESSDOWN, KEYCODE_BRIGHTNESSUP]: + if tmpkeymode == 1 and tmptime - brightnesstime > DATAREFRESHINTERVALSEC: + # Do not update value during hold + try: + tmpobj = keyboardevent_getbrigthnessinfo(brightnesstoolid) + curbrightness = tmpobj["level"] + except Exception: + pass + + adjusttype = ADJUSTTYPE_BRIGHTNESS + if tmpkeycode == KEYCODE_BRIGHTNESSDOWN: + adjustval = -5*tmpkeymode + else: + adjustval = 5*tmpkeymode + brightnesstime = tmptime + elif tmpkeycode in [KEYCODE_MUTE, KEYCODE_VOLUMEDOWN, KEYCODE_VOLUMEUP]: + if tmpkeymode == 1 and tmptime - volumetime > DATAREFRESHINTERVALSEC and tmpkeymode == 1: + # Do not update value during hold + try: + tmpobj = keyboardevent_getvolumeinfo() + curvolumemuted = tmpobj["muted"] + curvolume = tmpobj["level"] + except Exception: + pass + + if tmpkeycode == KEYCODE_MUTE: + adjusttype = ADJUSTTYPE_MUTE + adjustval = 0 + else: + adjusttype = ADJUSTTYPE_VOLUME + if tmpkeycode == KEYCODE_VOLUMEDOWN: + adjustval = -5*tmpkeymode + else: + adjustval = 5*tmpkeymode + volumetime = tmptime + + elif tmpkeycode == KEYCODE_PAUSE: + adjusttype = ADJUSTTYPE_BATTERYINFO + else: + readq.task_done() + continue + + try: + tmplockfilea = KEYBOARD_LOCKFILE+".a" + if createlockfile(tmplockfilea) == False: + # Debug ONLY + # if tmpkeymode == 1: + # debuglog("keyboard-event", "Press Key Code: "+str(tmpkeycode)) + # else: + # debuglog("keyboard-event", "Hold Key Code: "+str(tmpkeycode)) + + if adjusttype == ADJUSTTYPE_BRIGHTNESS: + try: + tmpobj = keyboardevent_adjustbrigthness(brightnesstoolid, curbrightness, adjustval) + curbrightness = tmpobj["level"] + except Exception as brightnesserr: + try: + debuglog("keyboard-brightnessother-error", str(brightnesserr)) + except: + debuglog("keyboard-brightnessother-error", "Error adjusting value") + pass + elif adjusttype == ADJUSTTYPE_VOLUME or adjusttype == ADJUSTTYPE_MUTE: + try: + tmpobj = keyboardevent_adjustvolume(curvolume, curvolumemuted, adjustval) + curvolumemuted = tmpobj["muted"] + curvolume = tmpobj["level"] + except Exception as volumeerr: + try: + debuglog("keyboard-volumeother-error", str(volumeerr)) + except: + debuglog("keyboard-volumeother-error", "Error adjusting value") + pass + elif adjusttype == ADJUSTTYPE_BATTERYINFO: + outobj = battery_loadlogdata() + try: + notifymessage(outobj["power"], False) + except: + pass + deletelockfile(tmplockfilea) + + + except Exception as keyhandlererr: + try: + debuglog("keyboard-handlererror", str(keyhandleerr)) + except: + debuglog("keyboard-handlererror", "Error") + + readq.task_done() + + except Exception as mainerr: + time.sleep(10) + # While True + + +def keyboardevent_monitor(writeq): + + READTIMEOUTSECS = 1.0 + + FIRSTHOLDINTERVALSEC = 0.5 + HOLDWAITINTERVALSEC = 0.5 + + while True: + try: + keypresstimestamp = {} + keyholdtimestamp = {} + # Get Devices + devicelist = [] + devicefdlist = [] + devicepathlist = keyboardevent_getdevicepaths() + devicefwlist = [] + + deviceidx = 0 + while deviceidx < len(devicepathlist): + try: + tmpdevice = InputDevice(devicepathlist[deviceidx]) + devicelist.append(tmpdevice) + devicefdlist.append(tmpdevice.fd) + devicefwlist.append(keyboard_getdevicefw(tmpdevice)) + #debuglog("keyboard-device-info", devicepathlist[deviceidx]) + #debuglog("keyboard-device-info", str(tmpdevice.info)) + except Exception as deverr: + try: + debuglog("keyboard-deviceerror", str(deverr)+ " "+ devicepathlist[deviceidx]) + except: + debuglog("keyboard-deviceerror", "Error "+devicepathlist[deviceidx]) + deviceidx = deviceidx + 1 + + try: + debuglog("keyboard-update", str(len(devicefdlist))+" Devices") + while len(devicefdlist) > 0: + # Exception when one of the devices gets removed + # Wait for events on any registered device + r, w, x = select(devicefdlist, [], [], READTIMEOUTSECS) + for fd in r: + found = False + curdevicefw = "" + deviceidx = 0 + while deviceidx < len(devicefdlist): + if devicefdlist[deviceidx] == fd: + curdevicefw = devicefwlist[deviceidx] + found = True + break + deviceidx = deviceidx + 1 + if found: + for event in devicelist[deviceidx].read(): + try: + # Process the event + #print("Device: "+devicelist[deviceidx].path+", Event: ", event) + #debuglog("keyboard-event", "Device: "+devicelist[deviceidx].path+", Event: "+str(event)) + if event.type == ecodes.EV_KEY: + key_event = categorize(event) + keycodelist = [] + # 2 hold, 0 release, 1 press + if event.value == 2 or event.value == 1: + #debuglog("keyboard-event", "Mode:"+str(event.value)+" Key Code: "+str(key_event.keycode)) + + if isinstance(key_event.keycode, str): + keycodelist = [key_event.keycode] + else: + keycodelist = key_event.keycode + else: + continue + + keycodelistidx = 0 + while keycodelistidx < len(keycodelist): + tmpkeycode = keycodelist[keycodelistidx] + if curdevicefw == "314": + # Remap printscreen event as pause and vice versa for special handling + if tmpkeycode == "KEY_PRINTSCREEN": + tmpkeycode = KEYCODE_PAUSE + elif tmpkeycode == "KEY_SYSRQ": + # This gets fired for some devices + tmpkeycode = KEYCODE_PAUSE + elif tmpkeycode == KEYCODE_PAUSE: + # Some other key so it will not fire + tmpkeycode = "KEY_PRINTSCREEN" + #debuglog("keyboard-event", "FW:" + curdevicefw+ " Key Code: "+tmpkeycode + " Press:"+keycodelist[keycodelistidx]) + + + keycodelistidx = keycodelistidx + 1 + # if tmpkeycode not in [KEYCODE_BRIGHTNESSDOWN, KEYCODE_BRIGHTNESSUP, KEYCODE_VOLUMEDOWN, KEYCODE_VOLUMEUP]: + # if event.value == 2: + # # Skip hold for unhandled keys + # continue + # elif tmpkeycode not in [KEYCODE_PAUSE, KEYCODE_MUTE]: + # # Skip press for unhandled keys + # continue + if tmpkeycode not in [KEYCODE_BRIGHTNESSDOWN, KEYCODE_BRIGHTNESSUP]: + if event.value == 2: + # Skip hold for unhandled keys + continue + elif tmpkeycode not in [KEYCODE_PAUSE]: + # Skip press for unhandled keys + continue + + tmptime = time.time() + finalmode = event.value + if event.value == 2: + # Hold needs checking + if tmpkeycode in keypresstimestamp: + # Guard time before first for hold + if (tmptime - keypresstimestamp[tmpkeycode]) >= FIRSTHOLDINTERVALSEC: + # Guard time for hold + if tmpkeycode in keyholdtimestamp: + if (tmptime - keyholdtimestamp[tmpkeycode]) < HOLDWAITINTERVALSEC: + #debuglog("keyboard-event", "Hold Key Code: "+str(tmpkeycode)+" - Skip") + continue + else: + #debuglog("keyboard-event", "Hold Key Code: "+str(tmpkeycode)+" - Skip") + continue + else: + # Should not happen, but treat as if first press + finalmode = 1 + + #debuglog("keyboard-event", "Mode:"+str(event.value) + " Final:"+str(finalmode)+" " +str(tmpkeycode)) + + if finalmode == 1: + keypresstimestamp[tmpkeycode] = tmptime + writeq.put("PRESS+"+tmpkeycode) + else: + keyholdtimestamp[tmpkeycode] = tmptime + writeq.put("HOLD+"+tmpkeycode) + + except Exception as keyhandleerr: + try: + debuglog("keyboard-keyerror", str(keyhandleerr)) + except: + debuglog("keyboard-keyerror", "Error") + + newpathlist = keyboardevent_getdevicepaths() + if keyboardevent_devicechanged(devicepathlist, newpathlist): + debuglog("keyboard-update", "Device list changed") + break + + except Exception as e: + try: + debuglog("keyboard-mainerror", str(e)) + except: + debuglog("keyboard-mainerror", "Error") + + # Close devices + while len(devicelist) > 0: + tmpdevice = devicelist.pop(0) + try: + tmpdevice.close() + except: + pass + + except Exception as mainerr: + time.sleep(10) + # While True + try: + writeq.put("EXIT") + except Exception: + pass + + +if len(sys.argv) > 1: + cmd = sys.argv[1].upper() + if cmd == "SERVICE": + if createlockfile(KEYBOARD_LOCKFILE) == True: + debuglog("keyboard-service", "Already running") + else: + try: + debuglog("keyboard-service", "Service Starting") + ipcq = Queue() + t1 = Thread(target = keyboardevemt_keyhandler, args =(ipcq, )) + t2 = Thread(target = keyboardevent_monitor, args =(ipcq, )) + t1.start() + t2.start() + + ipcq.join() + + except Exception as einit: + try: + debuglog("keyboard-service-error", str(einit)) + except: + debuglog("keyboard-service-error", "Error") + debuglog("keyboard-service", "Service Stopped") + deletelockfile(KEYBOARD_LOCKFILE) diff --git a/source/scripts/argonone-irdecoder-libgpiod.py b/source/scripts/argonone-irdecoder-libgpiod.py new file mode 100644 index 0000000..f9bc41b --- /dev/null +++ b/source/scripts/argonone-irdecoder-libgpiod.py @@ -0,0 +1,525 @@ +#!/usr/bin/python3 + +# Standard Headers +import sys +import smbus + +# For GPIO +import gpiod +from datetime import datetime + +import os +import time + +# Check if Lirc Lib is installed +haslirclib = os.path.isfile("/usr/bin/mode2") +if haslirclib == True: + from multiprocessing import Process + +######################### +# Use GPIO + +def getGPIOPulseData(): + # Counter + ctr = 0 + + # Pin Assignments + LINE_IRRECEIVER=23 + + try: + try: + # Pi5 mapping + chip = gpiod.Chip('4') + except Exception as gpioerr: + # Old mapping + chip = gpiod.Chip('0') + lineobj = chip.get_line(LINE_IRRECEIVER) + lineobj.request(consumer="argon", type=gpiod.LINE_REQ_EV_BOTH_EDGES) + except Exception as e: + # GPIO Error + return [(-2, -2)] + + # Start reading + value = lineobj.get_value() + + # mark time + startTime = datetime.now() + pulseTime = startTime + + # Pulse Data + pulsedata = [] + + aborted = False + while aborted == False: + # Wait for transition + try: + while True: + hasevent = lineobj.event_wait(10) + if hasevent: + # Event data needs to be read + eventdata = lineobj.event_read() + break + except Exception as e: + # GPIO Error + lineobj.release() + chip.close() + return [(-2, -2)] + + # high/low Length + now = datetime.now() + pulseLength = now - pulseTime + pulseTime = now + + # Update value (changed triggered), this also inverts value before saving + if value: + value = 0 + else: + value = 1 + + if pulseLength.microseconds > PULSETAIL_MAXMICROS_NEC and ctr == 0: + continue + + pulsedata.append((value, pulseLength.microseconds)) + + ctr = ctr + 1 + if pulseLength.microseconds > PULSETAIL_MAXMICROS_NEC: + break + elif ctr > PULSEDATA_MAXCOUNT: + break + + lineobj.release() + chip.close() + + # Data is most likely incomplete + if aborted == True: + return [] + elif ctr >= PULSEDATA_MAXCOUNT: + print (" * Unable to decode. Please try again *") + return [] + return pulsedata + + +######################### +# Use LIRC +def lircMode2Task(irlogfile): + os.system("mode2 > "+irlogfile+" 2>&1") + +def startLIRCMode2Logging(irlogfile): + # create a new process + loggerprocess = Process(target=lircMode2Task,args=(irlogfile,)) + loggerprocess.start() + # mode2 will start new process, terminate current + time.sleep(0.1) + loggerprocess.kill() + return True + +def endLIRCMode2Logging(irlogfile): + tmplogfile = irlogfile+".tmp" + os.system("ps | grep ode2 > "+tmplogfile+"") + + if os.path.exists(tmplogfile) == True: + ctr = 0 + fp = open(tmplogfile, "r") + for curline in fp: + if len(curline) > 0: + rowdata = curline.split(" ") + pid = "" + processname = "" + colidx = 0 + while colidx < len(rowdata): + if len(rowdata[colidx]) > 0: + if pid == "": + pid = rowdata[colidx] + else: + processname = rowdata[colidx] + + colidx = colidx + 1 + if processname=="mode2\n": + os.system("kill -9 "+pid) + fp.close() + os.remove(tmplogfile) + return True + +def getLIRCPulseData(): + if haslirclib == False: + print (" * LIRC Module not found, please reboot and try again *") + return [] + + irlogfile = "/dev/shm/lircdecoder.log" + + loggerresult = startLIRCMode2Logging(irlogfile) + if loggerresult == False: + return [(-1, -1)] + + # Wait for log file + logsize = 0 + while logsize == 0: + if os.path.exists(irlogfile) == True: + logsize = os.path.getsize(irlogfile) + if logsize == 0: + time.sleep(0.1) + + # Wait for data to start + newlogsize = logsize + while logsize == newlogsize: + time.sleep(0.1) + newlogsize = os.path.getsize(irlogfile) + + print(" Thank you") + + # Wait for data to stop + while logsize != newlogsize: + logsize = newlogsize + time.sleep(0.1) + newlogsize = os.path.getsize(irlogfile) + + # Finalize File + loggerresult = endLIRCMode2Logging(irlogfile) + if loggerresult == False: + return [(-1, -1)] + + # Decode logfile into Pulse Data + pulsedata = [] + + terminated = False + if os.path.exists(irlogfile) == True: + ctr = 0 + fp = open(irlogfile, "r") + for curline in fp: + if len(curline) > 0: + rowdata = curline.split(" ") + if len(rowdata) == 2: + duration = int(rowdata[1]) + value = 0 + if rowdata[0] == "pulse": + value = 1 + ctr = ctr + 1 + if value == 1 or ctr > 1: + if len(pulsedata) > 0 and duration > PULSELEADER_MINMICROS_NEC: + terminated = True + break + else: + pulsedata.append((value, duration)) + fp.close() + os.remove(irlogfile) + + # Check if terminating pulse detected + if terminated == False: + print (" * Unable to read signal. Please try again *") + return [] + return pulsedata + + +######################### +# Common +irconffile = "/etc/lirc/lircd.conf.d/argon.lircd.conf" + +# I2C +address = 0x1a # I2C Address +addressregister = 0xaa # I2C Address Register + +# Constants +PULSETIMEOUTMS = 1000 +VERIFYTARGET = 3 +PULSEDATA_MAXCOUNT = 200 # Fail safe + +# NEC Protocol Constants +PULSEBIT_MAXMICROS_NEC = 2500 +PULSEBIT_ZEROMICROS_NEC = 1000 + +PULSELEADER_MINMICROS_NEC = 8000 +PULSELEADER_MAXMICROS_NEC = 10000 +PULSETAIL_MAXMICROS_NEC = 12000 + +# Flags +FLAGV1ONLY = False + +try: + if os.path.isfile("/etc/argon/flag_v1"): + FLAGV1ONLY = True +except Exception: + FLAGV1ONLY = False + + +# Standard Methods +def getbytestring(pulsedata): + outstring = "" + for curbyte in pulsedata: + tmpstr = hex(curbyte)[2:] + while len(tmpstr) < 2: + tmpstr = "0" + tmpstr + outstring = outstring+tmpstr + return outstring + +def displaybyte(pulsedata): + print (getbytestring(pulsedata)) + + +def pulse2byteNEC(pulsedata): + outdata = [] + bitdata = 1 + curbyte = 0 + bitcount = 0 + for (mode, duration) in pulsedata: + if mode == 1: + continue + elif duration > PULSEBIT_MAXMICROS_NEC: + continue + elif duration > PULSEBIT_ZEROMICROS_NEC: + curbyte = curbyte*2 + 1 + else: + curbyte = curbyte*2 + + bitcount = bitcount + 1 + if bitcount == 8: + outdata.append(curbyte) + curbyte = 0 + bitcount = 0 + # Shouldn't happen, but just in case + if bitcount > 0: + outdata.append(curbyte) + + return outdata + + +def bytecompare(a, b): + idx = 0 + maxidx = len(a) + if maxidx != len(b): + return 1 + while idx < maxidx: + if a[idx] != b[idx]: + return 1 + idx = idx + 1 + return 0 + + +# Main Flow +mode = "custom" +if len(sys.argv) > 1: + mode = sys.argv[1] + +powerdata = [] +buttonlist = ['POWER', 'UP', 'DOWN', 'LEFT', 'RIGHT', + 'VOLUMEUP', 'VOLUMEDOWN', 'OK', 'HOME', 'MENU' + 'BACK'] + +ircodelist = ['00ff39c6', '00ff53ac', '00ff4bb4', '00ff9966', '00ff837c', + '00ff01fe', '00ff817e', '00ff738c', '00ffd32c', '00ffb946', + '00ff09f6'] + +buttonidx = 0 + +if mode == "power": + buttonlist = ['POWER'] + ircodelist = [''] +elif mode == "resetpower": + # Just Set the power so it won't create/update the conf file + buttonlist = ['POWER'] + mode = "default" +elif mode == "custom": + buttonlist = ['POWER', 'UP', 'DOWN', 'LEFT', 'RIGHT', + 'VOLUMEUP', 'VOLUMEDOWN', 'OK', 'HOME', 'MENU' + 'BACK'] + ircodelist = ['', '', '', '', '', + '', '', '', '', '', + ''] + #buttonlist = ['POWER', 'VOLUMEUP', 'VOLUMEDOWN'] + #ircodelist = ['', '', ''] + +if mode == "default": + # To skip the decoding loop + buttonidx = len(buttonlist) + # Set MCU IR code + powerdata = [0x00, 0xff, 0x39, 0xc6] +else: + print ("************************************************") + print ("* WARNING: Current buttons are still active. *") + print ("* Please temporarily assign to a *") + print ("* different button if you plan to *") + print ("* reuse buttons. *") + print ("* e.g. Power Button triggers shutdown *") + print ("* *") + print ("* PROCEED AT YOUR OWN RISK *") + print ("* (Press CTRL+C to abort at any time) *") + print ("************************************************") + +readaborted = False +# decoding loop +while buttonidx < len(buttonlist): + print ("Press your button for "+buttonlist[buttonidx]+" (CTRL+C to abort)") + irprotocol = "" + outdata = [] + verifycount = 0 + readongoing = True + + # Handles NEC protocol Only + while readongoing == True: + # Try GPIO-based reading, if it fails, fallback to LIRC + pulsedata = getGPIOPulseData() + if len(pulsedata) == 1: + if pulsedata[0][0] == -2: + pulsedata = getLIRCPulseData() + + # Aborted + if len(pulsedata) == 1: + if pulsedata[0][0] == -1: + readongoing = False + readaborted = True + buttonidx = len(buttonlist) + break + # Ignore repeat code (NEC) + if len(pulsedata) <= 4: + continue + + # Get leading signal + (mode, duration) = pulsedata[0] + + # Decode IR Protocols + # https://www.sbprojects.net/knowledge/ir/index.php + + if duration >= PULSELEADER_MINMICROS_NEC and duration <= PULSELEADER_MAXMICROS_NEC: + irprotocol = "NEC" + # NEC has 9ms head, +/- 1ms + curdata = pulse2byteNEC(pulsedata) + if len(curdata) > 0: + if verifycount > 0: + if bytecompare(outdata, curdata) == 0: + verifycount = verifycount + 1 + else: + verifycount = 0 + else: + outdata = curdata + verifycount = 1 + + if verifycount >= VERIFYTARGET: + readongoing = False + print ("") + elif verifycount == 0: + print (" * IR code mismatch, please try again *") + elif VERIFYTARGET - verifycount > 1: + print (" Press the button "+ str(VERIFYTARGET - verifycount)+ " more times") + else: + print (" Press the button 1 more time") + else: + print (" * Decoding error. Please try again *") + else: + print (" * Unrecognized signal. Please try again *") + #curdata = pulse2byteLSB(pulsedata) + #displaybyte(curdata) + + # Check for duplicates + newircode = getbytestring(outdata) + if verifycount > 0: + checkidx = 0 + while checkidx < buttonidx and checkidx < len(buttonlist): + if ircodelist[checkidx] == newircode: + print (" Button already assigned. Please try again") + verifycount = 0 + break + checkidx = checkidx + 1 + + # Store code, and power button code if applicable + if verifycount > 0: + if buttonidx == 0: + powerdata = outdata + if buttonidx < len(buttonlist): + # Abort will cause out of bounds + ircodelist[buttonidx] = newircode + #print (buttonlist[buttonidx]+": "+ newircode) + buttonidx = buttonidx + 1 + +if len(powerdata) > 0 and readaborted == False: + # Send to device if completed or reset mode + #print("Writing " + getbytestring(powerdata)) + print("Updating Device...") + try: + bus=smbus.SMBus(1) + except Exception: + try: + # Older version + bus=smbus.SMBus(0) + except Exception: + bus=None + + if bus is None: + print("Device Update Failed: Unable to detect i2c") + else: + # Check for Argon Control Register Support + checkircodewrite = False + argoncyclereg = 0x80 + if FLAGV1ONLY == False: + oldval = bus.read_byte_data(address, argoncyclereg) + newval = oldval + 1 + if newval >= 100: + newval = 98 + bus.write_byte_data(address,argoncyclereg, newval) + time.sleep(1) + newval = bus.read_byte_data(address, argoncyclereg) + + if newval != oldval: + addressregister = 0x82 + checkircodewrite = True + bus.write_byte_data(address,argoncyclereg, oldval) + + bus.write_i2c_block_data(address, addressregister, powerdata) + + + if checkircodewrite == True: + # Check if data was written for devices that support it + print("Verifying ...") + time.sleep(2) + checkircodedata = bus.read_i2c_block_data(address, addressregister, 4) + checkircodecounter = 0 + while checkircodecounter < 4: + # Reuse readaborted flag as indicator if IR code was successfully updated + if checkircodedata[checkircodecounter] != powerdata[checkircodecounter]: + readaborted = True + checkircodecounter = checkircodecounter + 1 + if readaborted == False: + print("Device Update Successful") + else: + print("Verification Failed") + bus.close() + + # Update IR Conf if there are other button + if buttonidx > 1 and readaborted == False: + print("Updating Remote Control Codes...") + fp = open(irconffile, "w") + + # Standard NEC conf header + fp.write("#\n") + fp.write("# Based on NEC templates at http://lirc.sourceforge.net/remotes/nec/\n") + fp.write("# Configured codes based on data gathered\n") + fp.write("#\n") + fp.write("\n") + fp.write("begin remote\n") + fp.write(" name argon\n") + fp.write(" bits 32\n") + fp.write(" flags SPACE_ENC\n") + fp.write(" eps 20\n") + fp.write(" aeps 200\n") + fp.write("\n") + fp.write(" header 8800 4400\n") + fp.write(" one 550 1650\n") + fp.write(" zero 550 550\n") + fp.write(" ptrail 550\n") + fp.write(" repeat 8800 2200\n") + fp.write(" gap 38500\n") + fp.write(" toggle_bit 0\n") + fp.write("\n") + fp.write(" frequency 38000\n") + fp.write("\n") + fp.write(" begin codes\n") + + # Write Key Codes + buttonidx = 1 + while buttonidx < len(buttonlist): + fp.write(" KEY_"+buttonlist[buttonidx]+" 0x"+ircodelist[buttonidx]+"\n") + buttonidx = buttonidx + 1 + fp.write(" end codes\n") + fp.write("end remote\n") + fp.close() + + + diff --git a/source/scripts/argonone-oledconfig.sh b/source/scripts/argonone-oledconfig.sh new file mode 100644 index 0000000..6d4e622 --- /dev/null +++ b/source/scripts/argonone-oledconfig.sh @@ -0,0 +1,294 @@ +#!/bin/bash + +oledconfigfile=/etc/argoneonoled.conf + +get_number () { + read curnumber + if [ -z "$curnumber" ] + then + echo "-2" + return + elif [[ $curnumber =~ ^[+-]?[0-9]+$ ]] + then + if [ $curnumber -lt 0 ] + then + echo "-1" + return + elif [ $curnumber -gt 100 ] + then + echo "-1" + return + fi + echo $curnumber + return + fi + echo "-1" + return +} + +get_pagename() { + if [ "$1" == "clock" ] + then + pagename="Current Date/Time" + elif [ "$1" == "cpu" ] + then + pagename="CPU Utilization" + elif [ "$1" == "storage" ] + then + pagename="Storage Utilization" + elif [ "$1" == "ram" ] + then + pagename="Available RAM" + elif [ "$1" == "temp" ] + then + pagename="CPU Temperature" + elif [ "$1" == "ip" ] + then + pagename="IP Address" + elif [ "$1" == "logo1v5" ] + then + pagename="Logo:One v5" + else + pagename="Invalid" + fi +} + +configure_pagelist () { + pagemasterlist="logo1v5 clock cpu storage ram temp ip" + newscreenlist="$1" + pageloopflag=1 + while [ $pageloopflag -eq 1 ] + do + echo "--------------------------------" + echo " OLED Pages " + echo "--------------------------------" + i=1 + for curpage in $newscreenlist + do + get_pagename $curpage + echo " $i. Remove $pagename" + i=$((i+1)) + done + if [ $i -eq 1 ] + then + echo " No page configured" + fi + echo + echo " $i. Add Page" + echo + echo " 0. Done" + echo -n "Enter Number (0-$i):" + + cmdmode=$( get_number ) + if [ $cmdmode -eq 0 ] + then + pageloopflag=0 + elif [[ $cmdmode -eq $i ]] + then + + echo "--------------------------------" + echo " Choose Page to Add" + echo "--------------------------------" + echo + i=1 + for curpage in $pagemasterlist + do + get_pagename $curpage + echo " $i. $pagename" + i=$((i+1)) + done + + echo + echo " 0. Cancel" + echo -n "Enter Number (0-$i):" + pagenum=$( get_number ) + if [[ $pagenum -ge 1 && $pagenum -le $i ]] + then + i=1 + for curpage in $pagemasterlist + do + if [ $i -eq $pagenum ] + then + if [ "$newscreenlist" == "" ] + then + newscreenlist="$curpage" + else + newscreenlist="$newscreenlist $curpage" + fi + fi + i=$((i+1)) + done + fi + elif [[ $cmdmode -ge 1 && $cmdmode -lt $i ]] + then + tmpscreenlist="" + i=1 + for curpage in $newscreenlist + do + if [ ! $i -eq $cmdmode ] + then + tmpscreenlist="$tmpscreenlist $curpage" + fi + i=$((i+1)) + done + if [ "$tmpscreenlist" == "" ] + then + newscreenlist="$tmpscreenlist" + else + # Remove leading space + newscreenlist="${tmpscreenlist:1}" + fi + fi + done +} + +saveconfig () { + echo "#" > $oledconfigfile + echo "# Argon OLED Configuration" >> $oledconfigfile + echo "#" >> $oledconfigfile + echo "enabled=$1" >> $oledconfigfile + echo "switchduration=$2" >> $oledconfigfile + echo "screensaver=$3" >> $oledconfigfile + echo "screenlist=\"$4\"" >> $oledconfigfile +} + +updateconfig=1 +oledloopflag=1 +while [ $oledloopflag -eq 1 ] +do + if [ $updateconfig -eq 1 ] + then + . $oledconfigfile + fi + + updateconfig=0 + if [ -z "$enabled" ] + then + enabled="Y" + updateconfig=1 + fi + + if [ -z "$screenlist" ] + then + screenlist="ip cpu ram" + updateconfig=1 + fi + + if [ -z "$screensaver" ] + then + screensaver=120 + updateconfig=1 + fi + + if [ -z "$switchduration" ] + then + switchduration=0 + updateconfig=1 + fi + + # Write default values to config file, daemon already uses default so no need to restart service + if [ $updateconfig -eq 1 ] + then + saveconfig $enabled $switchduration $screensaver "$screenlist" + updateconfig=0 + fi + + displaystring=": Manually" + if [ $switchduration -gt 1 ] + then + displaystring="Every $switchduration secs" + fi + + echo "-----------------------------" + echo "Argon OLED Configuration Tool" + echo "-----------------------------" + echo "Choose from the list:" + echo " 1. Switch Page $displaystring" + echo " 2. Configure Pages" + echo " 3. Turn OFF OLED Screen when unchanged after $screensaver secs" + echo " 4. Enable OLED Pages: $enabled" + echo + echo " 0. Back" + echo -n "Enter Number (0-3):" + + newmode=$( get_number ) + if [ $newmode -eq 0 ] + then + oledloopflag=0 + elif [ $newmode -eq 1 ] + then + echo + echo -n "Enter # of Seconds (10-60, Manual if 0):" + + cmdmode=$( get_number ) + if [ $cmdmode -eq 0 ] + then + switchduration=0 + updateconfig=1 + elif [[ $cmdmode -ge 10 && $cmdmode -le 60 ]] + then + updateconfig=1 + switchduration=$cmdmode + else + echo + echo "Invalid duration" + echo + fi + elif [ $newmode -eq 3 ] + then + echo + echo -n "Enter # of Seconds (60 or above, Manual if 0):" + + cmdmode=$( get_number ) + if [ $cmdmode -eq 0 ] + then + screensaver=0 + updateconfig=1 + elif [ $cmdmode -ge 60 ] + then + updateconfig=1 + screensaver=$cmdmode + else + echo + echo "Invalid duration" + echo + fi + elif [ $newmode -eq 2 ] + then + configure_pagelist "$screenlist" + if [ ! "$screenlist" == "$newscreenlist" ] + then + screenlist="$newscreenlist" + updateconfig=1 + fi + elif [ $newmode -eq 4 ] + then + echo + echo -n "Enable OLED Pages (Y/n)?:" + read -n 1 confirm + tmpenabled="$enabled" + if [[ "$confirm" == "n" || "$confirm" == "N" ]] + then + tmpenabled="N" + elif [[ "$confirm" == "y" || "$confirm" == "Y" ]] + then + tmpenabled="Y" + else + echo "Invalid response" + fi + if [ ! "$enabled" == "$tmpenabled" ] + then + enabled="$tmpenabled" + updateconfig=1 + fi + + fi + + if [ $updateconfig -eq 1 ] + then + saveconfig $enabled $switchduration $screensaver "$screenlist" + sudo systemctl restart argononed.service + fi +done + +echo diff --git a/source/scripts/argononeup-lidconfig.sh b/source/scripts/argononeup-lidconfig.sh new file mode 100644 index 0000000..bcc756a --- /dev/null +++ b/source/scripts/argononeup-lidconfig.sh @@ -0,0 +1,114 @@ +#!/bin/bash + +tmpfile="/dev/shm/argontmpconf.txt" +daemonconfigfile="/etc/argononeupd.conf" + +if [ -f "$daemonconfigfile" ] +then + . $daemonconfigfile +fi + +if [ -z "$lidshutdownsecs" ] +then + lidshutdownsecs=0 +fi + + +mainloopflag=1 +newmode=0 + + +get_number () { + read curnumber + if [ -z "$curnumber" ] + then + echo "-2" + return + elif [[ $curnumber =~ ^[+-]?[0-9]+$ ]] + then + if [ $curnumber -lt 0 ] + then + echo "-1" + return + fi + echo $curnumber + return + fi + echo "-1" + return +} + +while [ $mainloopflag -eq 1 ] +do + + lidshutdownmins=$((lidshutdownsecs / 60)) + + + echo "------------------------------------------" + echo " Argon One Up Lid Configuration Tool" + echo "------------------------------------------" + + echo + echo "Lid Close Behavior:" + if [ $lidshutdownsecs -lt 1 ] + then + echo "(Do Nothing)" + else + echo "(Shut down after $lidshutdownmins minute(s))" + fi + echo " 1. Do Nothing" + echo " 2. Shutdown" + echo + echo " 0. Exit" + echo "NOTE: You can also edit $daemonconfigfile directly" + echo -n "Enter Number (0-2):" + newmode=$( get_number ) + + if [[ $newmode -eq 0 ]] + then + mainloopflag=0 + elif [ $newmode -eq 1 ] + then + lidshutdownsecs=0 + elif [ $newmode -eq 2 ] + then + maxmins=120 + echo "Please provide number of minutes until shutdown:" + echo -n "Enter Number (1-$maxmins):" + curval=$( get_number ) + if [ $curval -gt $maxmins ] + then + newmode=0 + echo "Invalid input" + elif [ $curval -lt 1 ] + then + newmode=0 + echo "Invalid input" + else + lidshutdownsecs=$((curval * 60)) + fi + fi + + if [ $newmode -eq 1 ] || [ $newmode -eq 2 ] + then + if [ -f "$daemonconfigfile" ] + then + grep -v 'lidshutdownsecs' "$daemonconfigfile" > $tmpfile + else + echo '#' > $tmpfile + echo '# Argon One Up Configuration' >> $tmpfile + echo '#' >> $tmpfile + fi + echo '# lidshutdownsecs number of seconds till shutdown when lid is closed 0 if do nothing' >> $tmpfile + echo "lidshutdownsecs=$lidshutdownsecs" >> $tmpfile + + sudo cp $tmpfile $daemonconfigfile + sudo chmod 666 $daemonconfigfile + + echo "Configuration updated." + + fi +done + +echo + diff --git a/source/scripts/argononeupd.py b/source/scripts/argononeupd.py new file mode 100644 index 0000000..cb82727 --- /dev/null +++ b/source/scripts/argononeupd.py @@ -0,0 +1,474 @@ +#!/usr/bin/python3 + +# +# This script monitor battery via ic2 and keyboard events. +# +# Additional comments are found in each function below +# +# + +import sys +import os +import time + +from threading import Thread +from queue import Queue + +sys.path.append("/etc/argon/") +from argonregister import * +from argonpowerbutton import * + +# Initialize I2C Bus +bus = argonregister_initializebusobj() + +# Constants +ADDR_BATTERY = 0x64 + +UPS_LOGFILE="/dev/shm/upslog.txt" + + +################### +# Utilty Functions +################### + +# Debug Logger +def debuglog(typestr, logstr): + try: + DEBUGFILE="/dev/shm/argononeupdebuglog.txt" + tmpstrpadding = " " + + with open(DEBUGFILE, "a") as txt_file: + txt_file.write("["+time.asctime(time.localtime(time.time()))+"] "+typestr.upper()+" "+logstr.strip().replace("\n","\n"+tmpstrpadding)+"\n") + except: + pass + + +# System Notifcation +def notifymessage(message, iscritical): + if not isinstance(message, str) or len(message.strip()) == 0: + return + + wftype="notify" + if iscritical: + wftype="critical" + os.system("export SUDO_UID=1000; wfpanelctl "+wftype+" \""+message+"\"") + os.system("export DISPLAY=:0.0; lxpanelctl notify \""+message+"\"") + + +############# +# Battery +############# +REG_CONTROL = 0x08 +REG_SOCALERT = 0x0b +REG_PROFILE = 0x10 +REG_ICSTATE = 0xA7 + + + +def battery_restart(): + # Set to active mode + try: + maxretry = 3 + while maxretry > 0: + maxretry = maxretry - 1 + + # Restart + bus.write_byte_data(ADDR_BATTERY, REG_CONTROL, 0x30) + time.sleep(0.5) + # Activate + bus.write_byte_data(ADDR_BATTERY, REG_CONTROL, 0x00) + time.sleep(0.5) + + # Wait for Ready Status + maxwaitsecs = 5 + while maxwaitsecs > 0: + tmpval = bus.read_byte_data(ADDR_BATTERY, REG_ICSTATE) + if (tmpval&0x0C) != 0: + debuglog("battery-activate", "Activated Successfully") + return 0 + time.sleep(1) + maxwaitsecs = maxwaitsecs - 1 + + + debuglog("battery-activate", "Failed to activate") + return 2 + except Exception as e: + try: + debuglog("battery-activateerror", str(e)) + except: + debuglog("battery-activateerror", "Activation Failed") + return 1 + + +def battery_getstatus(restartifnotactive): + try: + tmpval = bus.read_byte_data(ADDR_BATTERY, REG_CONTROL) + if tmpval != 0: + if restartifnotactive == True: + tmpval = battery_restart() + + if tmpval != 0: + debuglog("battery-status", "Inactive "+str(tmpval)) + return 2 + + tmpval = bus.read_byte_data(ADDR_BATTERY, REG_SOCALERT) + if (tmpval&0x80) == 0: + debuglog("battery-status", "Profile not ready "+str(tmpval)) + return 3 + + # OK + #debuglog("battery-status", "OK") + return 0 + except Exception as e: + try: + debuglog("battery-status-error", str(e)) + except: + debuglog("battery-status-error", "Battery Status Failed") + + return 1 + +def battery_checkupdateprofile(): + try: + REG_GPIOCONFIG = 0x0A + + PROFILE_DATALIST = [0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xA8,0xAA,0xBE,0xC6,0xB8,0xAE,0xC2,0x98,0x82,0xFF,0xFF,0xCA,0x98,0x75,0x63,0x55,0x4E,0x4C,0x49,0x98,0x88,0xDC,0x34,0xDB,0xD3,0xD4,0xD3,0xD0,0xCE,0xCB,0xBB,0xE7,0xA2,0xC2,0xC4,0xAE,0x96,0x89,0x80,0x74,0x67,0x63,0x71,0x8E,0x9F,0x85,0x6F,0x3B,0x20,0x00,0xAB,0x10,0xFF,0xB0,0x73,0x00,0x00,0x00,0x64,0x08,0xD3,0x77,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFA] + + PROFILE_LEN = len(PROFILE_DATALIST) + + # Try to compare profile if battery is active + tmpidx = 0 + + tmpval = battery_getstatus(True) + if tmpval == 0: + # Status OK, check profile + tmpidx = 0 + while tmpidx < PROFILE_LEN: + tmpval = bus.read_byte_data(ADDR_BATTERY, REG_PROFILE+tmpidx) + if tmpval != PROFILE_DATALIST[tmpidx]: + debuglog("battery-profile-error", "Mismatch") + break + tmpidx = tmpidx + 1 + + if tmpidx == PROFILE_LEN: + # Matched + return 0 + else: + debuglog("battery-profile", "Status Error "+str(tmpval)+", will attempt to update") + + # needs update + debuglog("battery-profile", "Updating...") + + # Device Sleep state + + # Restart + bus.write_byte_data(ADDR_BATTERY, REG_CONTROL, 0x30) + time.sleep(0.5) + # Sleep + bus.write_byte_data(ADDR_BATTERY, REG_CONTROL, 0xF0) + time.sleep(0.5) + + # Write Profile + tmpidx = 0 + while tmpidx < PROFILE_LEN: + bus.write_byte_data(ADDR_BATTERY, REG_PROFILE+tmpidx, PROFILE_DATALIST[tmpidx]) + tmpidx = tmpidx + 1 + + debuglog("battery-profile", "Profile Updated,Restarting...") + + # Set Update Flag + bus.write_byte_data(ADDR_BATTERY, REG_SOCALERT, 0x80) + time.sleep(0.5) + + # Close Interrupts + bus.write_byte_data(ADDR_BATTERY, REG_GPIOCONFIG, 0) + time.sleep(0.5) + + # Restart Battery + tmpval = battery_restart() + if tmpval == 0: + debuglog("battery-profile", "Update Completed") + return 0 + + debuglog("battery-profile", "Unable to restart") + return 3 + except Exception as e: + try: + debuglog("battery-profile-error", str(e)) + except: + debuglog("battery-profile-error", "Battery Profile Check/Update Failed") + + return 1 + + + +def battery_getpercent(): + # State of Charge (SOC) + try: + SOC_HIGH_REG = 0x04 + + socpercent = bus.read_byte_data(ADDR_BATTERY, SOC_HIGH_REG) + if socpercent > 100: + return 100 + elif socpercent > 0: + return socpercent + + # Support Fraction percent + #SOC_LOW_REG = 0x05 + #soc_low = bus.read_byte_data(ADDR_BATTERY, SOC_LOW_REG) + #socpercentfloat = socpercent + (soc_low / 256.0) + #if socpercentfloat > 100.0: + # return 100.0 + #elif socpercentfloat > 0.0: + # return socpercentfloat + + except Exception as e: + try: + debuglog("battery-percenterror", str(e)) + except: + debuglog("battery-percenterror", "Read Battery Failed") + + return -1 + + +def battery_isplugged(): + # State of Charge (SOC) + try: + CURRENT_HIGH_REG = 0x0E + + current_high = bus.read_byte_data(ADDR_BATTERY, CURRENT_HIGH_REG) + + if (current_high & 0x80) > 0: + return 1 + + #CURRENT_LOW_REG = 0x0F + #R_SENSE = 10.0 + #current_low = bus.read_byte_data(ADDR_BATTERY, CURRENT_LOW_REG) + #raw_current = int.from_bytes([current_high, current_low], byteorder='big', signed=True) + #current = (52.4 * raw_current) / (32768 * R_SENSE) + + return 0 + except Exception as e: + try: + debuglog("battery-chargingerror", str(e)) + except: + debuglog("battery-chargingerror", "Read Charging Failed") + + return -1 + +def battery_loadlogdata(): + # status, version, time, schedule + outobj = {} + try: + fp = open(UPS_LOGFILE, "r") + logdata = fp.read() + alllines = logdata.split("\n") + ctr = 0 + while ctr < len(alllines): + tmpval = alllines[ctr].strip() + curinfo = tmpval.split(":") + if len(curinfo) > 1: + tmpattrib = curinfo[0].lower().split(" ") + # The rest are assumed to be value + outobj[tmpattrib[0]] = tmpval[(len(curinfo[0])+1):].strip() + ctr = ctr + 1 + except OSError: + pass + + return outobj + +def battery_check(readq): + CMDSTARTBYTE=0xfe + CMDCONTROLBYTECOUNT=3 + CHECKSTATUSLOOPFREQ=50 + + CMDsendrequest = [ 0xfe, 0, 0, 0xfe, 0xfe, 0, 0, 0xfe, 0, 0, 0] + + lastcmdtime="" + loopCtr = CHECKSTATUSLOOPFREQ + sendcmdid = -1 + + debuglog("battery", "Starting") + + updatedesktopicon("Argon ONE UP", "/etc/argon/argon40.png") + + maxretry = 5 + while maxretry > 0: + try: + if battery_checkupdateprofile() == 0: + break + except Exception as mainerr: + try: + debuglog("battery-mainerror", str(mainerr)) + except: + debuglog("battery-mainerror", "Error") + # Give time before retry + time.sleep(10) + maxretry = maxretry - 1 + + while maxretry > 0: # Outer loop; maxretry never decrements so infinite + qdata = "" + if readq.empty() == False: + qdata = readq.get() + + if battery_getstatus(True) != 0: + # Give time before retry + time.sleep(3) + continue + + prevnotifymsg = "" + previconfile = "" + statusstr = "" + + shutdowntriggered=False + needsupdate=False + device_battery=0 + device_charging=0 + + while True: # Command loop + try: + if sendcmdid < 0: + cmddatastr = "" + + if cmddatastr == "": + if loopCtr >= CHECKSTATUSLOOPFREQ: + # Check Battery Status + sendcmdid = 0 + loopCtr = 0 + else: + loopCtr = loopCtr + 1 + if (loopCtr&1) == 0: + sendcmdid = 0 # Check Battery Status + + if sendcmdid == 0: + tmp_battery = battery_getpercent() + tmp_charging = battery_isplugged() + + if tmp_charging < 0 or tmp_battery < 0: + # communication error, retain old value + tmp_charging = device_charging + tmp_battery = device_battery + + if tmp_charging != device_charging or tmp_battery!=device_battery: + device_battery=tmp_battery + device_charging=tmp_charging + tmpiconfile = "/etc/argon/ups/" + needsupdate=True + curnotifymsg = "" + curnotifycritical = False + + if device_battery>99: + # Prevents switching issue + statusstr = "Charged" + curnotifymsg = statusstr + tmpiconfile = tmpiconfile+"charge_"+str(device_battery) + elif device_charging == 0: + statusstr = "Charging" + curnotifymsg = statusstr + tmpiconfile = tmpiconfile+"charge_"+str(device_battery) + else: + statusstr = "Battery" + tmpiconfile = tmpiconfile+"discharge_"+str(device_battery) + + if device_battery > 50: + curnotifymsg="Battery Mode" + elif device_battery > 20: + curnotifymsg="50%% Battery" + elif device_battery > 10: + curnotifymsg="20%% Battery" + elif device_battery > 5: + #curnotifymsg="Low Battery" + curnotifymsg="Low Battery: The device may power off automatically soon." + curnotifycritical=True + else: + curnotifymsg="CRITICAL BATTERY: Shutting Down in 1 minute" + curnotifycritical=True + + tmpiconfile = tmpiconfile + ".png" + statusstr = statusstr + " " + str(device_battery)+"%" + + # Add/update desktop icons too; add check to minimize write + if previconfile != tmpiconfile: + updatedesktopicon(statusstr, tmpiconfile) + previconfile = tmpiconfile + + # Send notification if necessary + if prevnotifymsg != curnotifymsg: + notifymessage(curnotifymsg, curnotifycritical) + if shutdowntriggered==False and device_battery <= 5 and device_charging != 0: + shutdowntriggered=True + os.system("shutdown +1 """+curnotifymsg+".""") + debuglog("battery-shutdown", "Shutdown in 1 minute") + + if shutdowntriggered==True and (device_charging == 0 or device_battery>=10): + shutdowntriggered=False + os.system("shutdown -c ""Charging, shutdown cancelled.""") + debuglog("battery-shutdown", "Abort") + + prevnotifymsg = curnotifymsg + + + sendcmdid=-1 + + if needsupdate==True: + # Log File + otherstr = "" + with open(UPS_LOGFILE, "w") as txt_file: + txt_file.write("Status as of: "+time.asctime(time.localtime(time.time()))+"\n Power:"+statusstr+"\n"+otherstr) + + needsupdate=False + + except Exception as e: + try: + debuglog("battery-error", str(e)) + except: + debuglog("battery-error", "Error") + break + time.sleep(3) + +def updatedesktopicon(statusstr, tmpiconfile): + try: + icontitle = "Argon ONE UP" + tmp = os.popen("find /home -maxdepth 1 -type d").read() + alllines = tmp.split("\n") + for curfolder in alllines: + if curfolder == "/home" or curfolder == "": + continue + #debuglog("desktop-update-path", curfolder) + #debuglog("desktop-update-text", statusstr) + #debuglog("desktop-update-icon", tmpiconfile) + with open(curfolder+"/Desktop/argononeup.desktop", "w") as txt_file: + txt_file.write("[Desktop Entry]\nName="+icontitle+"\nComment="+statusstr+"\nIcon="+tmpiconfile+"\nExec=lxterminal --working-directory="+curfolder+"/ -t \"Argon ONE UP\" -e \"/etc/argon/argon-config\"\nType=Application\nEncoding=UTF-8\nTerminal=false\nCategories=None;\n") + except Exception as desktope: + #pass + try: + debuglog("desktop-update-error", str(desktope)) + except: + debuglog("desktop-update-error", "Error") + + +if len(sys.argv) > 1: + cmd = sys.argv[1].upper() + if cmd == "GETBATTERY": + outobj = battery_loadlogdata() + try: + print(outobj["power"]) + except: + print("Error retrieving battery status") + elif cmd == "RESETBATTERY": + battery_checkupdateprofile() + + elif cmd == "SERVICE": + # Starts sudo level services + try: + ipcq = Queue() + if len(sys.argv) > 2: + cmd = sys.argv[2].upper() + t1 = Thread(target = battery_check, args =(ipcq, )) + t2 = Thread(target = argonpowerbutton_monitorlid, args =(ipcq, )) + + t1.start() + t2.start() + + ipcq.join() + except Exception: + sys.exit(1) diff --git a/source/scripts/argononeupd.service b/source/scripts/argononeupd.service new file mode 100644 index 0000000..6ba565b --- /dev/null +++ b/source/scripts/argononeupd.service @@ -0,0 +1,10 @@ +[Unit] +Description=Argon ONE UP Service +After=multi-user.target +[Service] +Type=simple +Restart=always +RemainAfterExit=true +ExecStart=/usr/bin/python3 /etc/argon/argononeupd.py SERVICE +[Install] +WantedBy=multi-user.target diff --git a/source/scripts/argononeupduser.service b/source/scripts/argononeupduser.service new file mode 100644 index 0000000..62481a3 --- /dev/null +++ b/source/scripts/argononeupduser.service @@ -0,0 +1,10 @@ +[Unit] +Description=Argon ONE UP Service +After=multi-user.target +[Service] +Type=simple +Restart=always +RemainAfterExit=true +ExecStart=/usr/bin/python3 /etc/argon/argonkeyboard.py SERVICE +[Install] +WantedBy=default.target diff --git a/source/scripts/argononeupsd.py b/source/scripts/argononeupsd.py new file mode 100644 index 0000000..48f3bff --- /dev/null +++ b/source/scripts/argononeupsd.py @@ -0,0 +1,106 @@ +#!/usr/bin/python3 + +import time +import os + +UPS_LOGFILE="/dev/shm/upslog.txt" +#UPS_DEVFILE="/dev/argonbatteryicon" + +def notifymessage(message, iscritical): + wftype="notify" + if iscritical: + wftype="critical" + os.system("export SUDO_UID=1000; wfpanelctl "+wftype+" \""+message+"\"") + os.system("export DISPLAY=:0.0; lxpanelctl notify \""+message+"\"") + +try: + outobj = {} + #os.system("insmod /etc/argon/ups/argonbatteryicon.ko") + prevnotifymsg="" + + tmp_battery = 100 + tmp_charging = 1 + + device_battery = -1 + device_charging = -1 + + while True: + try: + # Load status + fp = open(UPS_LOGFILE, "r") + logdata = fp.read() + alllines = logdata.split("\n") + ctr = 0 + while ctr < len(alllines): + tmpval = alllines[ctr].strip() + curinfo = tmpval.split(":") + if len(curinfo) > 1: + tmpattrib = curinfo[0].lower().split(" ") + # The rest are assumed to be value + outobj[tmpattrib[0]] = tmpval[(len(curinfo[0])+1):].strip() + ctr = ctr + 1 + + # Map to data + try: + statuslist = outobj["power"].lower().split(" ") + if statuslist[0] == "battery": + tmp_charging = 0 + else: + tmp_charging = 1 + tmp_battery = int(statuslist[1].replace("%","")) + except: + tmp_charging = device_charging + tmp_battery = device_battery + + # Update module data if changed + if tmp_charging != device_charging or tmp_battery!=device_battery: + device_charging = tmp_charging + device_battery = tmp_battery + + # No longer using default battery indicator + #try: + # with open(UPS_DEVFILE, 'w') as f: + # f.write("capacity = "+str(device_battery)+"\ncharging = "+str(device_charging)+"\n") + #except Exception as e: + # pass + + curnotifymsg = "" + curnotifycritical=False + + if tmp_charging: + if "Shutting Down" in prevnotifymsg: + os.system("shutdown -c ""Charging, shutdown cancelled.""") + + if tmp_battery > 99: + curnotifymsg="Fully Charged" + elif tmp_charging: + curnotifymsg="Charging" + else: + if tmp_battery > 50: + curnotifymsg="Battery Mode" + elif tmp_battery > 20: + curnotifymsg="50%% Battery" + elif tmp_battery > 10: + curnotifymsg="20%% Battery" + elif tmp_battery > 5: + #curnotifymsg="Low Battery" + curnotifymsg="Low Battery: The device may power off automatically soon." + curnotifycritical=True + else: + curnotifymsg="CRITICAL BATTERY: Shutting Down in 1 minute" + curnotifycritical=True + + + if prevnotifymsg != curnotifymsg: + notifymessage(curnotifymsg, curnotifycritical) + if tmp_battery <= 5 and tmp_charging != 0: + os.system("shutdown +1 """+curnotifymsg+".""") + + prevnotifymsg = curnotifymsg + + except OSError: + pass + time.sleep(60) +except: + pass + diff --git a/source/scripts/argononeupsd.service b/source/scripts/argononeupsd.service new file mode 100644 index 0000000..e5cd439 --- /dev/null +++ b/source/scripts/argononeupsd.service @@ -0,0 +1,14 @@ +[Unit] +Description=Argon Industria UPS Battery Service +DefaultDependencies=no + +[Install] +WantedBy=sysinit.target + +[Service] +Type=simple +KillSignal=SIGINT +TimeoutStopSec=8 +Restart=on-failure +WorkingDirectory=/etc/argon/ups/ +ExecStart=/usr/bin/python3 /etc/argon/argononeupsd.py diff --git a/source/scripts/argonupsrtcd.py b/source/scripts/argonupsrtcd.py new file mode 100644 index 0000000..3231442 --- /dev/null +++ b/source/scripts/argonupsrtcd.py @@ -0,0 +1,568 @@ +#!/usr/bin/python3 + +import json + +import sys +import datetime +import math + +import os +import time +import serial + +from threading import Thread +from queue import Queue + +sys.path.append("/etc/argon/") +import argonrtc + + +################# +# Common/Helpers +################# +#UPS_SERIALPORT="/dev/ttyUSB0" +UPS_SERIALPORT="/dev/ttyACM0" +UPS_LOGFILE="/dev/shm/upslog.txt" +UPS_CMDFILE="/dev/shm/upscmd.txt" + +RTC_CONFIGFILE = "/etc/argonupsrtc.conf" + + +############# +# RTC +############# + +def hexAsDec(hexval): + return (hexval&0xF) + 10*((hexval>>4)&0xf) + +def decAsHex(decval): + return (decval%10) + (math.floor(decval/10)<<4) + +# Returns RTC timestamp as datetime object +def getDatetimeObj(dataobj, datakey): + try: + datetimearray = dataobj[datakey].split(" ") + if len(datetimearray)>1: + datearray = datetimearray[0].split("/") + timearray = datetimearray[1].split(":") + if len(datearray) == 3 and len(timearray) > 1: + year = int(datearray[2]) + month = int(datearray[0]) + caldate = int(datearray[1]) + hour = int(timearray[0]) + minute = int(timearray[1]) + second = 0 + if len(timearray) > 2: + second = int(timearray[2]) + return datetime.datetime(year, month, caldate, hour, minute, second)+argonrtc.getLocaltimeOffset() + except: + pass + + return datetime.datetime(1999, 1, 1, 0, 0, 0) + + +def getRTCpoweronschedule(): + outobj = ups_sendcmd("7") + return getDatetimeObj(outobj, "schedule") + + +def getRTCdatetime(): + outobj = ups_sendcmd("5") + return getDatetimeObj(outobj, "time") + + +# set RTC time using datetime object (Local time) +def setRTCdatetime(): + # Set local time to UTC + outobj = ups_sendcmd("3") + return getDatetimeObj(outobj, "time") + + +# Set Next Alarm on RTC +def setNextAlarm(commandschedulelist, prevdatetime): + nextcommandtime, weekday, caldate, hour, minute = argonrtc.getNextAlarm(commandschedulelist, prevdatetime) + if prevdatetime >= nextcommandtime: + return prevdatetime + if weekday < 0 and caldate < 0 and hour < 0 and minute < 0: + # No schedule + # nextcommandtime is current time, which will be replaced/checked next iteration + return nextcommandtime + + # Convert to RTC timezone + alarmtime = nextcommandtime - argonrtc.getLocaltimeOffset() + + outobj = ups_sendcmd("6 "+alarmtime.strftime("%Y %m %d %H %M")) + return getDatetimeObj(outobj, "schedule") + + +############# +# Status +############# + +def ups_debuglog(typestr, logstr): + try: + UPS_DEBUGFILE="/dev/shm/upsdebuglog.txt" + + tmpstrpadding = " " + + with open(UPS_DEBUGFILE, "a") as txt_file: + txt_file.write("["+datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")+"] "+typestr.upper()+" "+logstr.strip().replace("\n","\n"+tmpstrpadding)+"\n") + except: + pass + +def ups_sendcmd(cmdstr): + # status, version, time, schedule + ups_debuglog("sendcmd", cmdstr) + try: + outstr = datetime.datetime.now().strftime("%Y%m%d%H%M%S") + + with open(UPS_CMDFILE, "w") as txt_file: + txt_file.write(datetime.datetime.now().strftime("%Y%m%d%H%M%S")+"\n"+cmdstr+"\n") + time.sleep(3) + except: + pass + + outobj = ups_loadlogdata() + try: + ups_debuglog("sendcmd-response", json.dumps(outobj)) + except: + pass + + return outobj + +def ups_loadlogdata(): + # status, version, time, schedule + outobj = {} + try: + fp = open(UPS_LOGFILE, "r") + logdata = fp.read() + alllines = logdata.split("\n") + ctr = 0 + while ctr < len(alllines): + tmpval = alllines[ctr].strip() + curinfo = tmpval.split(":") + if len(curinfo) > 1: + tmpattrib = curinfo[0].lower().split(" ") + # The rest are assumed to be value + outobj[tmpattrib[0]] = tmpval[(len(curinfo[0])+1):].strip() + ctr = ctr + 1 + except OSError: + pass + + return outobj + +def ups_check(readq): + CMDSTARTBYTE=0xfe + CMDCONTROLBYTECOUNT=3 + CHECKSTATUSLOOPFREQ=50 + + CMDsendrequest = [ 0xfe, 0, 0, 0xfe, 0xfe, 0, 0, 0xfe, 0, 0, 0] + + lastcmdtime="" + loopCtr = CHECKSTATUSLOOPFREQ + sendcmdid = -1 + + ups_debuglog("serial", "Starting "+UPS_SERIALPORT) + + updatedesktopicon("Argon UPS", "Argon UPS", "/etc/argon/ups/loading_0.png") + + while True: # Outer loop to reconnect to device + + qdata = "" + if readq.empty() == False: + qdata = readq.get() + + try: + ser = serial.Serial(UPS_SERIALPORT, 115200, timeout = 1) + ser.close() + ser.open() + except Exception as mainerr: + try: + ups_debuglog("serial-mainerror", str(mainerr)) + except: + ups_debuglog("serial-mainerror", "Error") + # Give time before retry + time.sleep(10) + continue + + + previconfile = "" + statusstr = "" + device_battery=0 + device_charging=0 + device_chargecurrent=-1 + device_version=-1 + device_rtctime= [-1, -1, -1, -1, -1, -1] + device_powerontime= [-1, -1, -1, -1, -1] + + while True: # Command loop + try: + if sendcmdid < 0: + cmddatastr = "" + try: + fp = open(UPS_CMDFILE, "r") + cmdlog = fp.read() + alllines = cmdlog.split("\n") + if len(alllines) > 1: + if lastcmdtime != alllines[0]: + lastcmdtime=alllines[0] + cmddatastr=alllines[1] + tmpcmdarray = cmddatastr.split(" ") + sendcmdid = int(tmpcmdarray[0]) + if sendcmdid == 3: + # Get/rebuild time here to minimize delay/time gap + newrtcdatetime = datetime.datetime.now() - argonrtc.getLocaltimeOffset() + cmddatastr = ("3 "+newrtcdatetime.strftime("%Y %m %d %H %M %S")) + tmpcmdarray = cmddatastr.split(" ") + if len(tmpcmdarray) != 7: + cmddatastr = "" + sendcmdid = 0 + elif sendcmdid == 6: + if len(tmpcmdarray) != 6: + cmddatastr = "" + sendcmdid = 0 + except OSError: + cmddatastr = "" + + if cmddatastr == "": + if loopCtr >= CHECKSTATUSLOOPFREQ: + # Check Battery Status + sendcmdid = 0 + loopCtr = 0 + else: + loopCtr = loopCtr + 1 + if loopCtr == 2: + sendcmdid = 5 # Get RTC Time + elif loopCtr == 3: + sendcmdid = 7 # Get Power on Time + elif loopCtr == 4: + sendcmdid = 4 # Get Version + elif loopCtr == 5: + sendcmdid = 2 # Get Charge Current + elif (loopCtr&1) == 0: + sendcmdid = 0 # Check Battery Status + + if sendcmdid >= 0: + sendSize = 0 + cmdSize = 0 + if len(cmddatastr) > 0: + # set RTC Time (3, 6 bytes) + # set Power of Time (6, 5 bytes) + tmpcmdarray = cmddatastr.split(" ") + CMDsendrequest[1] = len(tmpcmdarray) - 1 # Length + CMDsendrequest[2] = sendcmdid + + cmdSize = CMDsendrequest[1] + 4 + + # Copy payload + tmpdataidx = cmdSize - 1 # Start at end + while tmpdataidx > 3: + tmpdataidx = tmpdataidx - 1 + if tmpdataidx == 3 and (sendcmdid == 3 or sendcmdid == 6): + tmpval = int(tmpcmdarray[tmpdataidx-2]) + if tmpval >= 2000: + tmpval = tmpval - 2000 + else: + tmpval = 0 + CMDsendrequest[tmpdataidx] = decAsHex(tmpval) + else: + CMDsendrequest[tmpdataidx] = decAsHex(int(tmpcmdarray[tmpdataidx-2])) + + datasum = 0 + tmpdataidx = cmdSize - 1 + while tmpdataidx > 0: + tmpdataidx = tmpdataidx - 1 + datasum = (datasum+CMDsendrequest[tmpdataidx]) & 0xff + + CMDsendrequest[cmdSize-1] = datasum + sendSize = ser.write(serial.to_bytes(CMDsendrequest[0:cmdSize])) + + ups_debuglog("serial-out-cmd", serial.to_bytes(CMDsendrequest[0:cmdSize]).hex(" ")) + + else: + # Default Get/Read command + CMDsendrequest[1] = 0 # Length + CMDsendrequest[2] = sendcmdid + CMDsendrequest[3] = (sendcmdid+CMDsendrequest[0]) & 0xff + sendSize = ser.write(serial.to_bytes(CMDsendrequest[0:4])) + cmdSize = CMDsendrequest[1] + 4 + + #ups_debuglog("serial-out-def", serial.to_bytes(CMDsendrequest[0:4]).hex(" ")) + + if cmdSize > 0: + sendcmdid=-1 + if sendSize == cmdSize: + # Give time to respond + time.sleep(1) + else: + break + + # read incoming data + readOut = ser.read() + + if len(readOut) == 0: + continue + + readdatalen = 1 + while True: + tmpreadlen = ser.inWaiting() # Check remaining byte size + if tmpreadlen < 1: + break + readOut += ser.read(tmpreadlen) + readdatalen += tmpreadlen + + readintarray = [tmpint for tmpint in readOut] + + if len(cmddatastr) > 0: + ups_debuglog("serial-in ", readOut.hex(" ")) + cmddatastr = "" + # Parse command stream + tmpidx = 0 + while tmpidx < readdatalen: + if readintarray[tmpidx] == CMDSTARTBYTE and tmpidx + CMDCONTROLBYTECOUNT < readdatalen: + # Cmd format: Min 4 bytes + # tmpidx tmpidx+1 tmpidx+2 + # 0xfe (byte count) (cmd ID) (payload; byte count) (datasum) + + tmpdatalen = readintarray[tmpidx+1] + tmpcmd = readintarray[tmpidx+2] + if tmpidx + CMDCONTROLBYTECOUNT + tmpdatalen < readdatalen: + # Validate datasum + datasum = 0 + tmpdataidx = tmpidx + tmpdatalen + CMDCONTROLBYTECOUNT + while tmpdataidx > tmpidx: + tmpdataidx = tmpdataidx - 1 + datasum = (datasum+readintarray[tmpdataidx]) & 0xff + if datasum != readintarray[tmpidx + tmpdatalen + CMDCONTROLBYTECOUNT]: + # Invalid sum + pass + else: + needsupdate=False + if tmpcmd == 0: + # Check State + if tmpdatalen >= 2: + needsupdate=True + tmp_battery = readintarray[tmpidx+CMDCONTROLBYTECOUNT] + if tmp_battery>100: + tmp_battery=100 + elif tmp_battery<1: + tmp_battery=0 + tmp_charging = readintarray[tmpidx+CMDCONTROLBYTECOUNT+1] + #ups_debuglog("battery-data", str(tmp_charging)+" "+str(tmp_battery)) + + if tmp_charging != device_charging or tmp_battery!=device_battery: + device_battery=tmp_battery + device_charging=tmp_charging + tmpiconfile = "/etc/argon/ups/" + + icontitle = "Argon UPS" + if device_charging == 0: + if device_battery==100: + statusstr = "Charged" + #tmpiconfile = tmpiconfile+"battery_plug" + else: + #icontitle = str(device_battery)+"%"+" Full" + statusstr = "Charging" + #tmpiconfile = tmpiconfile+"battery_charging" + tmpiconfile = tmpiconfile+"charge_"+str(device_battery) + else: + #icontitle = str(device_battery)+"%"+" Left" + statusstr = "Battery" + tmp_battery = round(tmp_battery/20) + if tmp_battery > 4: + tmp_battery = 4 + #tmpiconfile = tmpiconfile+"battery_"+str(tmp_battery) + tmpiconfile = tmpiconfile+"discharge_"+str(device_battery) + tmpiconfile = tmpiconfile + ".png" + + statusstr = statusstr + " " + str(device_battery)+"%" + + #ups_debuglog("battery-info", statusstr) + + # Add/update desktop icons too; add check to minimize write + if previconfile != tmpiconfile: + updatedesktopicon(icontitle, statusstr, tmpiconfile) + previconfile = tmpiconfile + + elif tmpcmd == 2: + # Charge Current + if tmpdatalen >= 2: + device_chargecurrent = ((readintarray[tmpidx+CMDCONTROLBYTECOUNT])<<8) | readintarray[tmpidx+CMDCONTROLBYTECOUNT+1] + elif tmpcmd == 4: + # Version + if tmpdatalen >= 1: + needsupdate=True + device_version = readintarray[tmpidx+CMDCONTROLBYTECOUNT] + elif tmpcmd == 5: + # RTC Time + if tmpdatalen >= 6: + needsupdate=True + tmpdataidx = 0 + while tmpdataidx < 6: + device_rtctime[tmpdataidx] = hexAsDec(readintarray[tmpidx+CMDCONTROLBYTECOUNT+tmpdataidx]) + tmpdataidx = tmpdataidx + 1 + elif tmpcmd == 7: + # Power On Time + if tmpdatalen >= 5: + needsupdate=True + tmpdataidx = 0 + while tmpdataidx < 5: + device_powerontime[tmpdataidx] = hexAsDec(readintarray[tmpidx+CMDCONTROLBYTECOUNT+tmpdataidx]) + tmpdataidx = tmpdataidx + 1 + elif tmpcmd == 8: + # Send Acknowledge + sendcmdid = tmpcmd + elif tmpcmd == 3: + # New RTC Time set + sendcmdid = 5 + elif tmpcmd == 6: + # New Power On Time set + sendcmdid = 7 + + if needsupdate==True: + # Log File + otherstr = "" + if device_version >= 0: + otherstr = otherstr + " Version:"+str(device_version)+"\n" + if device_rtctime[0] >= 0: + otherstr = otherstr + " Time:"+str(device_rtctime[1])+"/"+str(device_rtctime[2])+"/"+str(device_rtctime[0]+2000)+" "+str(device_rtctime[3])+":"+str(device_rtctime[4])+":"+str(device_rtctime[5])+"\n" + if device_powerontime[1] > 0: + otherstr = otherstr + " Schedule:"+str(device_powerontime[1])+"/"+str(device_powerontime[2])+"/"+str(device_powerontime[0]+2000)+" "+str(device_powerontime[3])+":"+str(device_powerontime[4])+"\n" + with open(UPS_LOGFILE, "w") as txt_file: + txt_file.write("Status as of: "+time.asctime(time.localtime(time.time()))+"\n Power:"+statusstr+"\n"+otherstr) + #ups_debuglog("status-update", "\n Power:"+statusstr+"\n"+otherstr) + # Point to datasum, so next loop iteration will be correct + tmpidx = tmpidx + tmpdatalen + CMDCONTROLBYTECOUNT + tmpidx = tmpidx + 1 + except Exception as e: + try: + ups_debuglog("serial-error", str(e)) + except: + ups_debuglog("serial-error", "Error") + break + +def updatedesktopicon(icontitle, statusstr, tmpiconfile): + try: + tmp = os.popen("find /home -maxdepth 1 -type d").read() + alllines = tmp.split("\n") + for curfolder in alllines: + if curfolder == "/home" or curfolder == "": + continue + #ups_debuglog("desktop-update-path", curfolder) + #ups_debuglog("desktop-update-text", statusstr) + #ups_debuglog("desktop-update-icon", tmpiconfile) + with open(curfolder+"/Desktop/argonone-ups.desktop", "w") as txt_file: + txt_file.write("[Desktop Entry]\nName="+icontitle+"\nComment="+statusstr+"\nIcon="+tmpiconfile+"\nExec=lxterminal --working-directory="+curfolder+"/ -t \"Argon UPS\" -e \"/etc/argon/argonone-upsconfig.sh argonupsrtc\"\nType=Application\nEncoding=UTF-8\nTerminal=false\nCategories=None;\n") + except Exception as desktope: + #pass + try: + ups_debuglog("desktop-update-error", str(desktope)) + except: + ups_debuglog("desktop-update-error", "Error") + + +def allowshutdown(): + uptime = 0.0 + errorflag = False + try: + cpuctr = 0 + tempfp = open("/proc/uptime", "r") + alllines = tempfp.readlines() + for temp in alllines: + infolist = temp.split(" ") + if len(infolist) > 1: + uptime = float(infolist[0]) + break + tempfp.close() + except IOError: + errorflag = True + # 120=2mins minimum up time + return uptime > 120 + + +###### +if len(sys.argv) > 1: + cmd = sys.argv[1].upper() + if cmd == "GETBATTERY": + #outobj = ups_sendcmd("0") + outobj = ups_loadlogdata() + try: + print(outobj["power"]) + except: + print("Error retrieving battery status") + + elif cmd == "GETRTCSCHEDULE": + tmptime = getRTCpoweronschedule() + if tmptime.year > 1999: + print("Alarm Setting:", tmptime) + else: + print("Alarm Setting: None") + + elif cmd == "GETRTCTIME": + tmptime = getRTCdatetime() + if tmptime.year > 1999: + print("RTC Time:", tmptime) + else: + print("Error reading RTC Time") + + elif cmd == "UPDATERTCTIME": + tmptime = setRTCdatetime() + if tmptime.year > 1999: + print("RTC Time:", tmptime) + else: + print("Error reading RTC Time") + + elif cmd == "GETSCHEDULELIST": + argonrtc.describeConfigList(RTC_CONFIGFILE) + + elif cmd == "SHOWSCHEDULE": + if len(sys.argv) > 2: + if sys.argv[2].isdigit(): + # Display starts at 2, maps to 0-based index + configidx = int(sys.argv[2])-2 + configlist = argonrtc.loadConfigList(RTC_CONFIGFILE) + if len(configlist) > configidx: + print (" ",argonrtc.describeConfigListEntry(configlist[configidx])) + else: + print(" Invalid Schedule") + + elif cmd == "REMOVESCHEDULE": + if len(sys.argv) > 2: + if sys.argv[2].isdigit(): + # Display starts at 2, maps to 0-based index + configidx = int(sys.argv[2])-2 + argonrtc.removeConfigEntry(RTC_CONFIGFILE, configidx) + + elif cmd == "SERVICE": + ipcq = Queue() + + tmprtctime = getRTCdatetime() + if tmprtctime.year >= 2000: + argonrtc.updateSystemTime(tmprtctime) + commandschedulelist = argonrtc.formCommandScheduleList(argonrtc.loadConfigList(RTC_CONFIGFILE)) + nextrtcalarmtime = setNextAlarm(commandschedulelist, datetime.datetime.now()) + + t1 = Thread(target = ups_check, args =(ipcq, )) + t1.start() + + serviceloop = True + while serviceloop==True: + tmpcurrenttime = datetime.datetime.now() + if nextrtcalarmtime <= tmpcurrenttime: + # Update RTC Alarm to next iteration + nextrtcalarmtime = setNextAlarm(commandschedulelist, nextrtcalarmtime) + if len(argonrtc.getCommandForTime(commandschedulelist, tmpcurrenttime, "off")) > 0: + # Shutdown detected, issue command then end service loop + if allowshutdown(): + os.system("shutdown now -h") + serviceloop = False + # Don't break to sleep while command executes (prevents service to restart) + + time.sleep(60) + + ipcq.join() + + +elif False: + print("System Time: ", datetime.datetime.now()) + print("RTC Time: ", getRTCdatetime()) diff --git a/source/scripts/argonupsrtcd.service b/source/scripts/argonupsrtcd.service new file mode 100644 index 0000000..5f14750 --- /dev/null +++ b/source/scripts/argonupsrtcd.service @@ -0,0 +1,10 @@ +[Unit] +Description=Argon UPS RTC Service +After=multi-user.target +[Service] +Type=simple +Restart=always +RemainAfterExit=true +ExecStart=/usr/bin/python3 /etc/argon/argonupsrtcd.py SERVICE +[Install] +WantedBy=multi-user.target diff --git a/source/ups/battery_0.png b/source/ups/battery_0.png new file mode 100644 index 0000000..e81a06d Binary files /dev/null and b/source/ups/battery_0.png differ diff --git a/source/ups/battery_1.png b/source/ups/battery_1.png new file mode 100644 index 0000000..1cf88e0 Binary files /dev/null and b/source/ups/battery_1.png differ diff --git a/source/ups/battery_2.png b/source/ups/battery_2.png new file mode 100644 index 0000000..55013af Binary files /dev/null and b/source/ups/battery_2.png differ diff --git a/source/ups/battery_3.png b/source/ups/battery_3.png new file mode 100644 index 0000000..0dd9a32 Binary files /dev/null and b/source/ups/battery_3.png differ diff --git a/source/ups/battery_4.png b/source/ups/battery_4.png new file mode 100644 index 0000000..724b702 Binary files /dev/null and b/source/ups/battery_4.png differ diff --git a/source/ups/battery_alert.png b/source/ups/battery_alert.png new file mode 100644 index 0000000..47adc9b Binary files /dev/null and b/source/ups/battery_alert.png differ diff --git a/source/ups/battery_charging.png b/source/ups/battery_charging.png new file mode 100644 index 0000000..d5bc61a Binary files /dev/null and b/source/ups/battery_charging.png differ diff --git a/source/ups/battery_plug.png b/source/ups/battery_plug.png new file mode 100644 index 0000000..c27055a Binary files /dev/null and b/source/ups/battery_plug.png differ diff --git a/source/ups/battery_unknown.png b/source/ups/battery_unknown.png new file mode 100644 index 0000000..c7908ef Binary files /dev/null and b/source/ups/battery_unknown.png differ diff --git a/source/ups/upsimg.tar.gz b/source/ups/upsimg.tar.gz new file mode 100644 index 0000000..5d039d4 Binary files /dev/null and b/source/ups/upsimg.tar.gz differ