diff --git a/pythonscript/.gitignore b/pythonscript/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/pythonscript/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/pythonscript/argon-config b/pythonscript/argon-config new file mode 100755 index 0000000..2577844 --- /dev/null +++ b/pythonscript/argon-config @@ -0,0 +1,57 @@ +#!/bin/bash +echo "--------------------------" +echo "Argon Configuration Tool" +/etc/argon/argon-versioninfo.sh simple +echo "--------------------------" +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 +} + +mainloopflag=1 +while [ $mainloopflag -eq 1 ] +do + echo + echo "Choose Option:" + echo " 1. Get Battery Status" + echo " 2. Dashboard" + echo " 3. Uninstall" + echo "" + echo " 0. Exit" + echo -n "Enter Number (0-3):" + newmode=$( get_number ) + if [ $newmode -eq 0 ] + then + echo "Thank you." + mainloopflag=0 + elif [ $newmode -eq 1 ] + then + sudo /usr/bin/python3 /etc/argon/argononeupd.py GETBATTERY + elif [ $newmode -eq 2 ] + then + sudo /usr/bin/python3 /etc/argon/argondashboard.py + elif [ $newmode -eq 3 ] + then + /etc/argon/argon-uninstall.sh + mainloopflag=0 + fi +done diff --git a/pythonscript/argon-uninstall.sh b/pythonscript/argon-uninstall.sh new file mode 100755 index 0000000..2693fcb --- /dev/null +++ b/pythonscript/argon-uninstall.sh @@ -0,0 +1,150 @@ +#!/bin/bash +echo "----------------------" +echo " Argon Uninstall Tool" +echo "----------------------" +echo -n "Press Y to continue:" +read -n 1 confirm +echo +if [ "$confirm" = "y" ] +then + confirm="Y" +fi + +if [ "$confirm" != "Y" ] +then + echo "Cancelled" + exit +fi + +destfoldername=$USERNAME +if [ -z "$destfoldername" ] +then + destfoldername=$USER +fi +if [ "$destfoldername" = "root" ] +then + destfoldername="" +fi +if [ -z "$destfoldername" ] +then + destfoldername="pi" +fi + + +shortcutfile="/home/$destfoldername/Desktop/argonone-config.desktop" +if [ -f "$shortcutfile" ]; then + sudo rm $shortcutfile + if [ -f "/usr/share/pixmaps/ar1config.png" ]; then + sudo rm /usr/share/pixmaps/ar1config.png + fi + if [ -f "/usr/share/pixmaps/argoneon.png" ]; then + sudo rm /usr/share/pixmaps/argoneon.png + fi +fi +shortcutfile="/home/$destfoldername/Desktop/argononeup.desktop" +if [ -f "$shortcutfile" ]; then + sudo rm $shortcutfile +fi + +INSTALLATIONFOLDER=/etc/argon + +argononefanscript=$INSTALLATIONFOLDER/argononed.py + +if [ -f $argononefanscript ]; then + sudo systemctl stop argononed.service + sudo systemctl disable argononed.service + + # Turn off the fan + /usr/bin/python3 $argononefanscript FANOFF + + # Remove files + sudo rm /lib/systemd/system/argononed.service +fi + +argononeupscript=$INSTALLATIONFOLDER/argononeupd.py +if [ -f $argononeupscript ]; then + sudo systemctl stop argononeupd.service + sudo systemctl disable argononeupd.service + + # Remove files + sudo rm /lib/systemd/system/argononeupd.service +fi + +# Remove RTC if any +argoneonrtcscript=$INSTALLATIONFOLDER/argoneond.py +if [ -f "$argoneonrtcscript" ] +then + # Disable Services + sudo systemctl stop argoneond.service + sudo systemctl disable argoneond.service + + # No need for sudo + /usr/bin/python3 $argoneonrtcscript CLEAN + /usr/bin/python3 $argoneonrtcscript SHUTDOWN + + # Remove files + sudo rm /lib/systemd/system/argoneond.service +fi + +# Remove UPS daemon if any +argononeupsscript=$INSTALLATIONFOLDER/argononeupsd.py +if [ -f "$argononeupsscript" ] +then + #sudo rmmod argonbatteryicon + # Disable Services + sudo systemctl stop argononeupsd.service + sudo systemctl disable argononeupsd.service + + sudo systemctl stop argonupsrtcd.service + sudo systemctl disable argonupsrtcd.service + + sudo systemctl --global stop argononeupsduser.service + sudo systemctl --global disable argononeupsduser.service + + # Remove files + sudo rm /lib/systemd/system/argononeupsd.service + sudo rm /lib/systemd/system/argonupsrtcd.service + sudo rm /etc/systemd/user/argononeupsduser.service + + find "/home" -maxdepth 1 -type d | while read line; do + shortcutfile="$line/Desktop/argonone-ups.desktop" + if [ -f "$shortcutfile" ]; then + sudo rm $shortcutfile + fi + done +fi + +if [ -f "/usr/bin/argon-config" ] +then + sudo rm /usr/bin/argon-config +fi + +if [ -f "/usr/bin/argonone-config" ] +then + sudo rm /usr/bin/argonone-config + sudo rm /usr/bin/argonone-uninstall +fi + + +if [ -f "/usr/bin/argonone-ir" ] +then + sudo rm /usr/bin/argonone-ir +fi + +# Delete config files +for configfile in argonunits argononed argononed-hdd argoneonrtc argoneonoled argonupsrtc argononeupd +do + if [ -f "/etc/${configfile}.conf" ] + then + sudo rm "/etc/${configfile}.conf" + fi +done + + + +sudo rm /lib/systemd/system-shutdown/argon-shutdown.sh + +sudo rm -R -f $INSTALLATIONFOLDER + +echo "Removed Argon Services." +echo "Cleanup will complete after restarting the device." diff --git a/pythonscript/argon-versioninfo.sh b/pythonscript/argon-versioninfo.sh new file mode 100755 index 0000000..9f79080 --- /dev/null +++ b/pythonscript/argon-versioninfo.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +VERSIONINFO="2509004" + +echo "Version $VERSIONINFO" +if [ -z "$1" ] +then + echo + echo "We acknowledge the valuable feedback of the following:" + echo "ghalfacree, NHHiker" + echo + echo "Feel free to join the discussions at https://forum.argon40.com" + echo +fi diff --git a/pythonscript/argon40.png b/pythonscript/argon40.png new file mode 100644 index 0000000..e69de29 diff --git a/pythonscript/argondashboard.py b/pythonscript/argondashboard.py new file mode 100755 index 0000000..d5c4032 --- /dev/null +++ b/pythonscript/argondashboard.py @@ -0,0 +1,371 @@ +#!/bin/python3 + +import time +import os +import sys + +import signal +import curses + + +sys.path.append("/etc/argon/") +from argonsysinfo import * +from argonregister import * + + + +############ +# Constants +############ +COLORPAIRID_DEFAULT=1 +COLORPAIRID_LOGO=2 +COLORPAIRID_DEFAULTINVERSE=3 +COLORPAIRID_ALERT=4 +COLORPAIRID_WARNING=5 +COLORPAIRID_GOOD=6 + + + + +INPUTREFRESHMS=100 +DISPLAYREFRESHMS=5000 +UPS_LOGFILE="/dev/shm/upslog.txt" + + +################### +# Display Elements +################### + +def displaydatetime(stdscr): + try: + curtimenow = time.localtime() + + stdscr.addstr(1, 1, time.strftime("%A", curtimenow), curses.color_pair(COLORPAIRID_DEFAULT)) + stdscr.addstr(2, 1, time.strftime("%b %d,%Y", curtimenow), curses.color_pair(COLORPAIRID_DEFAULT)) + stdscr.addstr(3, 1, time.strftime("%I:%M%p", curtimenow), curses.color_pair(COLORPAIRID_DEFAULT)) + except: + pass + +def displayipbattery(stdscr): + try: + displaytextright(stdscr,1, argonsysinfo_getip()+" ", COLORPAIRID_DEFAULT) + except: + pass + try: + status = "" + level = "" + outobj = {} + # 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("%","")) + + colorpairidx = COLORPAIRID_DEFAULT + if tmp_charging: + if tmp_battery > 99: + status="Plugged" + level="" + else: + status="Charging" + level=str(tmp_battery)+"%" + else: + status="Battery" + level=str(tmp_battery)+"%" + if tmp_battery <= 20: + colorpairidx = COLORPAIRID_ALERT + elif tmp_battery <= 50: + colorpairidx = COLORPAIRID_WARNING + else: + colorpairidx = COLORPAIRID_GOOD + + displaytextright(stdscr,2, status+" ", colorpairidx) + displaytextright(stdscr,3, level+" ", colorpairidx) + except: + pass + + + except: + pass + + +def displayramcpu(stdscr, refcpu, rowstart, colstart): + curusage_b = argonsysinfo_getcpuusagesnapshot() + try: + outputlist = [] + tmpraminfo = argonsysinfo_getram() + outputlist.append({"title": "ram ", "value": tmpraminfo[1]+" "+tmpraminfo[0]+" Free"}) + + for cpuname in refcpu: + if cpuname == "cpu": + continue + if refcpu[cpuname]["total"] == curusage_b[cpuname]["total"]: + outputlist.append({"title": cpuname, "value": "Loading"}) + else: + total = curusage_b[cpuname]["total"]-refcpu[cpuname]["total"] + idle = curusage_b[cpuname]["idle"]-refcpu[cpuname]["idle"] + outputlist.append({"title": cpuname, "value": str(int(100*(total-idle)/(total)))+"% Used"}) + displaytitlevaluelist(stdscr, rowstart, colstart, outputlist) + except: + pass + return curusage_b + + +def displaytempfan(stdscr, rowstart, colstart): + try: + outputlist = [] + try: + if busobj is not None: + fanspeed = argonregister_getfanspeed(busobj) + fanspeedstr = "Off" + if fanspeed > 0: + fanspeedstr = str(fanspeed)+"%" + outputlist.append({"title": "Fan ", "value": fanspeedstr}) + except: + pass + # Todo load from config + temperature = "C" + hddtempctr = 0 + maxcval = 0 + mincval = 200 + + + # Get min/max of hdd temp + hddtempobj = argonsysinfo_gethddtemp() + for curdev in hddtempobj: + if hddtempobj[curdev] < mincval: + mincval = hddtempobj[curdev] + if hddtempobj[curdev] > maxcval: + maxcval = hddtempobj[curdev] + hddtempctr = hddtempctr + 1 + + cpucval = argonsysinfo_getcputemp() + if hddtempctr > 0: + alltempobj = {"cpu": cpucval,"hdd min": mincval, "hdd max": maxcval} + # Update max C val to CPU Temp if necessary + if maxcval < cpucval: + maxcval = cpucval + + displayrowht = 8 + displayrow = 8 + for curdev in alltempobj: + if temperature == "C": + # Celsius + tmpstr = str(alltempobj[curdev]) + if len(tmpstr) > 4: + tmpstr = tmpstr[0:4] + else: + # Fahrenheit + tmpstr = str(32+9*(alltempobj[curdev])/5) + if len(tmpstr) > 5: + tmpstr = tmpstr[0:5] + if len(curdev) <= 3: + outputlist.append({"title": curdev.upper(), "value": tmpstr +temperature}) + else: + outputlist.append({"title": curdev.upper(), "value": tmpstr +temperature}) + else: + maxcval = cpucval + if temperature == "C": + # Celsius + tmpstr = str(cpucval) + if len(tmpstr) > 4: + tmpstr = tmpstr[0:4] + else: + # Fahrenheit + tmpstr = str(32+9*(cpucval)/5) + if len(tmpstr) > 5: + tmpstr = tmpstr[0:5] + + outputlist.append({"title": "Temp", "value": tmpstr +temperature}) + displaytitlevaluelist(stdscr, rowstart, colstart, outputlist) + except: + pass + + + +def displaystorage(stdscr, rowstart, colstart): + try: + outputlist = [] + tmpobj = argonsysinfo_listhddusage() + for curdev in tmpobj: + outputlist.append({"title": curdev, "value": argonsysinfo_kbstr(tmpobj[curdev]['total'])+ " "+ str(int(100*tmpobj[curdev]['used']/tmpobj[curdev]['total']))+"% Used" }) + displaytitlevaluelist(stdscr, rowstart, colstart, outputlist) + except: + pass + +################## +# Helpers +################## + +# Initialize I2C Bus +bus = argonregister_initializebusobj() + +def handle_resize(signum, frame): + # TODO: Not working? + curses.update_lines_cols() + # Ideally redraw here + +def displaytitlevaluelist(stdscr, rowstart, leftoffset, curlist): + rowidx = rowstart + while rowidx < curses.LINES and len(curlist) > 0: + curline = "" + tmpitem = curlist.pop(0) + curline = tmpitem["title"]+": "+str(tmpitem["value"]) + + stdscr.addstr(rowidx, leftoffset, curline) + rowidx = rowidx + 1 + + +def displaytextcentered(stdscr, rownum, strval, colorpairidx = COLORPAIRID_DEFAULT): + leftoffset = 0 + numchars = len(strval) + if numchars < 1: + return + elif (numchars > curses.COLS): + leftoffset = 0 + strval = strval[0:curses.COLS] + else: + leftoffset = (curses.COLS - numchars)>>1 + + stdscr.addstr(rownum, leftoffset, strval, curses.color_pair(colorpairidx)) + + +def displaytextright(stdscr, rownum, strval, colorpairidx = COLORPAIRID_DEFAULT): + leftoffset = 0 + numchars = len(strval) + if numchars < 1: + return + elif (numchars > curses.COLS): + leftoffset = 0 + strval = strval[0:curses.COLS] + else: + leftoffset = curses.COLS - numchars + + stdscr.addstr(rownum, leftoffset, strval, curses.color_pair(colorpairidx)) + + +def displaylinebreak(stdscr, rownum, colorpairidx = COLORPAIRID_DEFAULTINVERSE): + strval = " " + while len(strval) < curses.COLS: + strval = strval + " " + stdscr.addstr(rownum, 0, strval, curses.color_pair(colorpairidx)) + + + + +################## +# Main Loop +################## + +def mainloop(stdscr): + try: + # Set up signal handler + signal.signal(signal.SIGWINCH, handle_resize) + + maxloopctr = int(DISPLAYREFRESHMS/INPUTREFRESHMS) + sleepsecs = INPUTREFRESHMS/1000 + + loopctr = maxloopctr + loopmode = True + + stdscr = curses.initscr() + + # Turn off echoing of keys, and enter cbreak mode, + # where no buffering is performed on keyboard input + curses.noecho() + curses.cbreak() + curses.curs_set(0) + curses.start_color() + + #curses.COLOR_BLACK + #curses.COLOR_BLUE + #curses.COLOR_CYAN + #curses.COLOR_GREEN + #curses.COLOR_MAGENTA + #curses.COLOR_RED + #curses.COLOR_WHITE + #curses.COLOR_YELLOW + + curses.init_pair(COLORPAIRID_DEFAULT, curses.COLOR_WHITE, curses.COLOR_BLACK) + curses.init_pair(COLORPAIRID_LOGO, curses.COLOR_WHITE, curses.COLOR_RED) + curses.init_pair(COLORPAIRID_DEFAULTINVERSE, curses.COLOR_BLACK, curses.COLOR_WHITE) + curses.init_pair(COLORPAIRID_ALERT, curses.COLOR_RED, curses.COLOR_BLACK) + curses.init_pair(COLORPAIRID_WARNING, curses.COLOR_YELLOW, curses.COLOR_BLACK) + curses.init_pair(COLORPAIRID_GOOD, curses.COLOR_GREEN, curses.COLOR_BLACK) + + stdscr.nodelay(True) + + refcpu = argonsysinfo_getcpuusagesnapshot() + while True: + try: + key = stdscr.getch() + # if key == ord('x') or key == ord('X'): + # Any key + if key > 0: + break + except curses.error: + # No key was pressed + pass + + loopctr = loopctr + 1 + if loopctr >= maxloopctr: + loopctr = 0 + # Screen refresh loop + # Clear screen + stdscr.clear() + + displaytextcentered(stdscr, 0, " ", COLORPAIRID_LOGO) + displaytextcentered(stdscr, 1, " Argon40 Dashboard ", COLORPAIRID_LOGO) + displaytextcentered(stdscr, 2, " ", COLORPAIRID_LOGO) + displaytextcentered(stdscr, 3, "Press any key to close") + displaylinebreak(stdscr, 5) + + # Display Elements + displaydatetime(stdscr) + displayipbattery(stdscr) + + # Data Columns + rowstart = 7 + colstart = 20 + refcpu = displayramcpu(stdscr, refcpu, rowstart, colstart) + displaystorage(stdscr, rowstart, colstart+30) + displaytempfan(stdscr, rowstart, colstart+60) + + # Main refresh even + stdscr.refresh() + + time.sleep(sleepsecs) + + except Exception as initerr: + pass + + ########## + # Cleanup + ########## + + try: + curses.curs_set(1) + curses.echo() + curses.nocbreak() + curses.endwin() + except Exception as closeerr: + pass + +curses.wrapper(mainloop) diff --git a/pythonscript/argononeup-eepromconfig.py b/pythonscript/argononeup-eepromconfig.py new file mode 100755 index 0000000..d7c486f --- /dev/null +++ b/pythonscript/argononeup-eepromconfig.py @@ -0,0 +1,568 @@ +#!/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 PSU Mas Current etc if not yet set + foundnewsetting = 0 + addsetting="\nPSU_MAX_CURRENT=5000" + 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] == "PSU_MAX_CURRENT": + newsetting = "PSU_MAX_CURRENT=5000" + + 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/pythonscript/argononeup.sh b/pythonscript/argononeup.sh old mode 100644 new mode 100755 index f34e40b..3a08328 --- a/pythonscript/argononeup.sh +++ b/pythonscript/argononeup.sh @@ -201,6 +201,7 @@ sudo rm -Rf $INSTALLATIONFOLDER/ups/upsimg.tar.gz # Other utility scripts sudo wget $ARGONDOWNLOADSERVER/scripts/argondashboard.py -O $INSTALLATIONFOLDER/argondashboard.py --quiet +sudo wget $ARGONDOWNLOADSERVER/scripts/argonpowerbutton.py -O $INSTALLATIONFOLDER/argonpowerbutton.py --quiet sudo wget $ARGONDOWNLOADSERVER/scripts/argon-versioninfo.sh -O $versioninfoscript --quiet sudo chmod 755 $versioninfoscript diff --git a/pythonscript/argononeupd.py b/pythonscript/argononeupd.py new file mode 100755 index 0000000..3e0002a --- /dev/null +++ b/pythonscript/argononeupd.py @@ -0,0 +1,453 @@ +#!/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,0x00,0xB0,0x73,0x00,0x00,0x00,0x64,0x08,0xD3,0x77,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xAC] + + 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 0 + + +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) + + + except Exception as e: + try: + debuglog("battery-chargingerror", str(e)) + except: + debuglog("battery-chargingerror", "Read Charging Failed") + + return 0 + +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 = "" + + 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 != 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_charging == 0: + if device_battery>99: + statusstr = "Charged" + else: + 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" + else: + curnotifymsg="Low Battery" + 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) + + 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/pythonscript/argonpowerbutton.py b/pythonscript/argonpowerbutton.py new file mode 100755 index 0000000..e69de29 diff --git a/pythonscript/argonregister.py b/pythonscript/argonregister.py new file mode 100755 index 0000000..edd9c1d --- /dev/null +++ b/pythonscript/argonregister.py @@ -0,0 +1,74 @@ +#!/usr/bin/python3 + +# +# Argon Register Helper methods +# Same as argonregister, but no support for new register commands +# + +import time +import smbus + +# I2C Addresses +ADDR_ARGONONEFAN=0x1a +ADDR_ARGONONEREG=ADDR_ARGONONEFAN + +# ARGONONEREG Addresses +ADDR_ARGONONEREG_DUTYCYCLE=0x80 +ADDR_ARGONONEREG_FW=0x81 +ADDR_ARGONONEREG_IR=0x82 +ADDR_ARGONONEREG_CTRL=0x86 + +# Initialize bus +def argonregister_initializebusobj(): + try: + return smbus.SMBus(1) + except Exception: + try: + # Older version + return smbus.SMBus(0) + except Exception: + print("Unable to detect i2c") + return None + + +# Checks if the FW supports control registers +def argonregister_checksupport(busobj): + return False + +def argonregister_getbyte(busobj, address): + if busobj is None: + return 0 + return busobj.read_byte_data(ADDR_ARGONONEREG, address) + +def argonregister_setbyte(busobj, address, bytevalue): + if busobj is None: + return + busobj.write_byte_data(ADDR_ARGONONEREG,address,bytevalue) + time.sleep(1) + +def argonregister_getfanspeed(busobj, regsupport=None): + return 0 + +def argonregister_setfanspeed(busobj, newspeed, regsupport=None): + if busobj is None: + return + + if newspeed > 100: + newspeed = 100 + elif newspeed < 0: + newspeed = 0 + + busobj.write_byte(ADDR_ARGONONEFAN,newspeed) + time.sleep(1) + +def argonregister_signalpoweroff(busobj): + if busobj is None: + return + + busobj.write_byte(ADDR_ARGONONEFAN,0xFF) + +def argonregister_setircode(busobj, vallist): + if busobj is None: + return + + busobj.write_i2c_block_data(ADDR_ARGONONEREG, ADDR_ARGONONEREG_IR, vallist) diff --git a/pythonscript/argonsysinfo.py b/pythonscript/argonsysinfo.py new file mode 100755 index 0000000..102f2e5 --- /dev/null +++ b/pythonscript/argonsysinfo.py @@ -0,0 +1,394 @@ +#!/usr/bin/python3 + +# +# Misc methods to retrieve system information. +# + +import os +import time +import socket + +def argonsysinfo_listcpuusage(sleepsec = 1): + outputlist = [] + curusage_a = argonsysinfo_getcpuusagesnapshot() + time.sleep(sleepsec) + curusage_b = argonsysinfo_getcpuusagesnapshot() + + for cpuname in curusage_a: + if cpuname == "cpu": + continue + if curusage_a[cpuname]["total"] == curusage_b[cpuname]["total"]: + outputlist.append({"title": cpuname, "value": "0%"}) + else: + total = curusage_b[cpuname]["total"]-curusage_a[cpuname]["total"] + idle = curusage_b[cpuname]["idle"]-curusage_a[cpuname]["idle"] + outputlist.append({"title": cpuname, "value": int(100*(total-idle)/(total))}) + return outputlist + +def argonsysinfo_getcpuusagesnapshot(): + cpupercent = {} + errorflag = False + try: + cpuctr = 0 + # user, nice, system, idle, iowait, irc, softirq, steal, guest, guest nice + tempfp = open("/proc/stat", "r") + alllines = tempfp.readlines() + for temp in alllines: + temp = temp.replace('\t', ' ') + temp = temp.strip() + while temp.find(" ") >= 0: + temp = temp.replace(" ", " ") + if len(temp) < 3: + cpuctr = cpuctr +1 + continue + + checkname = temp[0:3] + if checkname == "cpu": + infolist = temp.split(" ") + idle = 0 + total = 0 + colctr = 1 + while colctr < len(infolist): + curval = int(infolist[colctr]) + if colctr == 4 or colctr == 5: + idle = idle + curval + total = total + curval + colctr = colctr + 1 + if total > 0: + cpupercent[infolist[0]] = {"total": total, "idle": idle} + cpuctr = cpuctr +1 + + tempfp.close() + except IOError: + errorflag = True + return cpupercent + + +def argonsysinfo_liststoragetotal(): + outputlist = [] + ramtotal = 0 + errorflag = False + + try: + hddctr = 0 + tempfp = open("/proc/partitions", "r") + alllines = tempfp.readlines() + + for temp in alllines: + temp = temp.replace('\t', ' ') + temp = temp.strip() + while temp.find(" ") >= 0: + temp = temp.replace(" ", " ") + infolist = temp.split(" ") + if len(infolist) >= 4: + # Check if header + if infolist[3] != "name": + parttype = infolist[3][0:3] + if parttype == "ram": + ramtotal = ramtotal + int(infolist[2]) + elif parttype[0:2] == "sd" or parttype[0:2] == "hd": + lastchar = infolist[3][-1] + if lastchar.isdigit() == False: + outputlist.append({"title": infolist[3], "value": argonsysinfo_kbstr(int(infolist[2]))}) + else: + # SD Cards + lastchar = infolist[3][-2] + if lastchar[0] != "p": + outputlist.append({"title": infolist[3], "value": argonsysinfo_kbstr(int(infolist[2]))}) + + tempfp.close() + #outputlist.append({"title": "ram", "value": argonsysinfo_kbstr(ramtotal)}) + except IOError: + errorflag = True + return outputlist + +def argonsysinfo_getram(): + totalram = 0 + totalfree = 0 + tempfp = open("/proc/meminfo", "r") + alllines = tempfp.readlines() + + for temp in alllines: + temp = temp.replace('\t', ' ') + temp = temp.strip() + while temp.find(" ") >= 0: + temp = temp.replace(" ", " ") + infolist = temp.split(" ") + if len(infolist) >= 2: + if infolist[0] == "MemTotal:": + totalram = int(infolist[1]) + elif infolist[0] == "MemFree:": + totalfree = totalfree + int(infolist[1]) + elif infolist[0] == "Buffers:": + totalfree = totalfree + int(infolist[1]) + elif infolist[0] == "Cached:": + totalfree = totalfree + int(infolist[1]) + if totalram == 0: + return "0%" + return [str(int(100*totalfree/totalram))+"%", str((totalram+512*1024)>>20)+"GB"] + +def argonsysinfo_getcputemp(): + try: + tempfp = open("/sys/class/thermal/thermal_zone0/temp", "r") + temp = tempfp.readline() + tempfp.close() + #cval = temp/1000 + #fval = 32+9*temp/5000 + return float(int(temp)/1000) + except IOError: + return 0 + + +def argonsysinfo_getmaxhddtemp(): + maxtempval = 0 + try: + hddtempobj = argonsysinfo_gethddtemp() + for curdev in hddtempobj: + if hddtempobj[curdev] > maxtempval: + maxtempval = hddtempobj[curdev] + return maxtempval + except: + return maxtempval + +def argonsysinfo_gethddtemp(): + # May 2022: Used smartctl, hddtemp is not available on some platforms + hddtempcmd = "/usr/sbin/smartctl" + if os.path.exists(hddtempcmd) == False: + # Fallback for now + hddtempcmd = "/usr/sbin/hddtemp" + + outputobj = {} + if os.path.exists(hddtempcmd): + try: + tmp = os.popen("lsblk | grep -e '0 disk' | awk '{print $1}'").read() + alllines = tmp.split("\n") + for curdev in alllines: + if curdev[0:2] == "sd" or curdev[0:2] == "hd": + tempval = argonsysinfo_getdevhddtemp(hddtempcmd,curdev) + if tempval > 0: + outputobj[curdev] = tempval + return outputobj + except: + return outputobj + return outputobj + +def argonsysinfo_getdevhddtemp(hddtempcmd, curdev): + cmdstr = "" + if hddtempcmd == "/usr/sbin/hddtemp": + cmdstr = "/usr/sbin/hddtemp -n sata:/dev/"+curdev + elif hddtempcmd == "/usr/sbin/smartctl": + cmdstr = "/usr/sbin/smartctl -d sat -A /dev/"+curdev+" | grep Temperature_Celsius | awk '{print $10}'" + + tempval = 0 + if len(cmdstr) > 0: + try: + temperaturestr = os.popen(cmdstr+" 2>&1").read() + tempval = float(temperaturestr) + except: + tempval = -1 + + return tempval + +def argonsysinfo_getip(): + ipaddr = "" + st = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + # Connect to nonexistent device + st.connect(('254.255.255.255', 1)) + ipaddr = st.getsockname()[0] + except Exception: + ipaddr = 'N/A' + finally: + st.close() + return ipaddr + + +def argonsysinfo_getrootdev(): + tmp = os.popen('mount').read() + alllines = tmp.split("\n") + + for temp in alllines: + temp = temp.replace('\t', ' ') + temp = temp.strip() + while temp.find(" ") >= 0: + temp = temp.replace(" ", " ") + infolist = temp.split(" ") + if len(infolist) >= 3: + + if infolist[2] == "/": + return infolist[0] + return "" + +def argonsysinfo_listhddusage(): + outputobj = {} + raidlist = argonsysinfo_listraid() + raiddevlist = [] + raidctr = 0 + while raidctr < len(raidlist['raidlist']): + raiddevlist.append(raidlist['raidlist'][raidctr]['title']) + # TODO: May need to use different method for each raid type (i.e. check raidlist['raidlist'][raidctr]['value']) + #outputobj[raidlist['raidlist'][raidctr]['title']] = {"used":int(raidlist['raidlist'][raidctr]['info']['used']), "total":int(raidlist['raidlist'][raidctr]['info']['size'])} + raidctr = raidctr + 1 + + rootdev = argonsysinfo_getrootdev() + + tmp = os.popen('df').read() + alllines = tmp.split("\n") + + for temp in alllines: + temp = temp.replace('\t', ' ') + temp = temp.strip() + while temp.find(" ") >= 0: + temp = temp.replace(" ", " ") + infolist = temp.split(" ") + if len(infolist) >= 6: + if infolist[1] == "Size": + continue + if len(infolist[0]) < 5: + continue + elif infolist[0][0:5] != "/dev/": + continue + curdev = infolist[0] + if curdev == "/dev/root" and rootdev != "": + curdev = rootdev + tmpidx = curdev.rfind("/") + if tmpidx >= 0: + curdev = curdev[tmpidx+1:] + + if curdev in raidlist['hddlist']: + # Skip devices that are part of a RAID setup + continue + elif curdev in raiddevlist: + # Skip RAID ID that already have size data + # (use df information otherwise) + if curdev in outputobj: + continue + elif curdev[0:2] == "sd" or curdev[0:2] == "hd": + curdev = curdev[0:-1] + else: + curdev = curdev[0:-2] + + # Aggregate values (i.e. sda1, sda2 to sda) + if curdev in outputobj: + outputobj[curdev] = {"used":outputobj[curdev]['used']+int(infolist[2]), "total":outputobj[curdev]['total']+int(infolist[1])} + else: + outputobj[curdev] = {"used":int(infolist[2]), "total":int(infolist[1])} + + return outputobj + +def argonsysinfo_kbstr(kbval, wholenumbers = True): + remainder = 0 + suffixidx = 0 + suffixlist = ["KB", "MB", "GB", "TB"] + while kbval > 1023 and suffixidx < len(suffixlist): + remainder = kbval & 1023 + kbval = kbval >> 10 + suffixidx = suffixidx + 1 + + #return str(kbval)+"."+str(remainder) + suffixlist[suffixidx] + remainderstr = "" + if kbval < 100 and wholenumbers == False: + remainder = int((remainder+50)/100) + if remainder > 0: + remainderstr = "."+str(remainder) + elif remainder >= 500: + kbval = kbval + 1 + return str(kbval)+remainderstr + suffixlist[suffixidx] + +def argonsysinfo_listraid(): + hddlist = [] + outputlist = [] + # cat /proc/mdstat + # multiple mdxx from mdstat + # mdadm -D /dev/md1 + + ramtotal = 0 + errorflag = False + try: + hddctr = 0 + tempfp = open("/proc/mdstat", "r") + alllines = tempfp.readlines() + for temp in alllines: + temp = temp.replace('\t', ' ') + temp = temp.strip() + while temp.find(" ") >= 0: + temp = temp.replace(" ", " ") + infolist = temp.split(" ") + if len(infolist) >= 4: + + # Check if raid info + if infolist[0] != "Personalities" and infolist[1] == ":": + devname = infolist[0] + raidtype = infolist[3] + #raidstatus = infolist[2] + hddctr = 4 + while hddctr < len(infolist): + tmpdevname = infolist[hddctr] + tmpidx = tmpdevname.find("[") + if tmpidx >= 0: + tmpdevname = tmpdevname[0:tmpidx] + hddlist.append(tmpdevname) + hddctr = hddctr + 1 + devdetail = argonsysinfo_getraiddetail(devname) + outputlist.append({"title": devname, "value": raidtype, "info": devdetail}) + + tempfp.close() + except IOError: + # No raid + errorflag = True + + return {"raidlist": outputlist, "hddlist": hddlist} + + +def argonsysinfo_getraiddetail(devname): + state = "" + raidtype = "" + size = 0 + used = 0 + total = 0 + working = 0 + active = 0 + failed = 0 + spare = 0 + rebuildstat = "" + tmp = os.popen('mdadm -D /dev/'+devname).read() + alllines = tmp.split("\n") + + for temp in alllines: + temp = temp.replace('\t', ' ') + temp = temp.strip() + while temp.find(" ") >= 0: + temp = temp.replace(" ", " ") + infolist = temp.split(" : ") + if len(infolist) == 2: + if infolist[0].lower() == "raid level": + raidtype = infolist[1] + elif infolist[0].lower() == "array size": + tmpidx = infolist[1].find(" ") + if tmpidx > 0: + size = (infolist[1][0:tmpidx]) + elif infolist[0].lower() == "used dev size": + tmpidx = infolist[1].find(" ") + if tmpidx > 0: + used = (infolist[1][0:tmpidx]) + elif infolist[0].lower() == "state": + tmpidx = infolist[1].rfind(" ") + if tmpidx > 0: + state = (infolist[1][tmpidx+1:]) + else: + state = infolist[1] + elif infolist[0].lower() == "total devices": + total = infolist[1] + elif infolist[0].lower() == "active devices": + active = infolist[1] + elif infolist[0].lower() == "working devices": + working = infolist[1] + elif infolist[0].lower() == "failed devices": + failed = infolist[1] + elif infolist[0].lower() == "spare devices": + spare = infolist[1] + elif infolist[0].lower() == "rebuild status": + tmpidx = infolist[1].find("%") + if tmpidx > 0: + rebuildstat = (infolist[1][0:tmpidx])+"%" + return {"state": state, "raidtype": raidtype, "size": int(size), "used": int(used), "devices": int(total), "active": int(active), "working": int(working), "failed": int(failed), "spare": int(spare), "rebuildstat": rebuildstat} \ No newline at end of file