From 755a2ca2624bdf25552ebd5101c3b85774ecb370 Mon Sep 17 00:00:00 2001 From: okunze <65952933+okunze@users.noreply.github.com> Date: Wed, 11 Jun 2025 12:21:48 +0000 Subject: [PATCH] Automated Change by GitHub Action --- source/ar1uninstall.png | Bin 120571 -> 0 bytes source/argoneon.png | Bin 0 -> 31108 bytes source/oled/bgcpu.bin | Bin 0 -> 1024 bytes source/oled/bgdefault.bin | Bin 0 -> 1024 bytes source/oled/bgip.bin | Bin 0 -> 1024 bytes source/oled/bgraid.bin | Bin 0 -> 1024 bytes source/oled/bgram.bin | Bin 0 -> 1024 bytes source/oled/bgstorage.bin | Bin 0 -> 1024 bytes source/oled/bgtemp.bin | Bin 0 -> 1024 bytes source/oled/bgtime.bin | Bin 0 -> 1024 bytes source/oled/font16x12.bin | Bin 0 -> 6144 bytes source/oled/font16x8.bin | Bin 0 -> 4096 bytes source/oled/font24x16.bin | Bin 0 -> 12288 bytes source/oled/font32x24.bin | Bin 0 -> 24576 bytes source/oled/font48x32.bin | Bin 0 -> 65536 bytes source/oled/font64x48.bin | Bin 0 -> 98304 bytes source/oled/font8x6.bin | Bin 0 -> 1536 bytes source/oled/logo1v5.bin | Bin 0 -> 1024 bytes source/scripts/argon-blstrdac.sh | 194 ++++++ source/scripts/argon-rpi-eeprom-config-psu.py | 568 ++++++++++++++++ source/scripts/argon-shutdown.sh | 22 + source/scripts/argon-status.sh | 106 +++ source/scripts/argon-uninstall.sh | 131 ++++ source/scripts/argon-unitconfig.sh | 105 +++ source/scripts/argon-versioninfo.sh | 14 + source/scripts/argondashboard.py | 371 ++++++++++ source/scripts/argoneon-oledconfig.sh | 294 ++++++++ source/scripts/argoneon-rtcconfig.sh | 421 ++++++++++++ source/scripts/argoneond.py | 487 +++++++++++++ source/scripts/argoneond.service | 10 + source/scripts/argoneonoled.py | 345 ++++++++++ source/scripts/argonone-fanconfig.sh | 254 +++++++ source/{ => scripts}/argonone-irconfig.sh | 23 +- source/scripts/argonone-upsconfig.sh | 305 +++++++++ source/scripts/argononed.py | 600 ++++++++++++++++ source/scripts/argononed.service | 10 + source/scripts/argononeoled.py | 333 +++++++++ source/scripts/argononeoledd.service | 10 + source/scripts/argonpowerbutton-libgpiod.py | 89 +++ source/scripts/argonpowerbutton-rpigpio.py | 66 ++ source/scripts/argonregister-v1.py | 74 ++ source/scripts/argonregister.py | 109 +++ source/scripts/argonrtc.py | 642 ++++++++++++++++++ source/scripts/argonstatus.py | 172 +++++ source/scripts/argonsysinfo.py | 394 +++++++++++ source/tools/setntpserver.sh | 42 ++ 46 files changed, 6182 insertions(+), 9 deletions(-) delete mode 100644 source/ar1uninstall.png create mode 100644 source/argoneon.png create mode 100644 source/oled/bgcpu.bin create mode 100644 source/oled/bgdefault.bin create mode 100644 source/oled/bgip.bin create mode 100644 source/oled/bgraid.bin create mode 100644 source/oled/bgram.bin create mode 100644 source/oled/bgstorage.bin create mode 100644 source/oled/bgtemp.bin create mode 100644 source/oled/bgtime.bin create mode 100644 source/oled/font16x12.bin create mode 100644 source/oled/font16x8.bin create mode 100644 source/oled/font24x16.bin create mode 100644 source/oled/font32x24.bin create mode 100644 source/oled/font48x32.bin create mode 100644 source/oled/font64x48.bin create mode 100644 source/oled/font8x6.bin create mode 100644 source/oled/logo1v5.bin create mode 100644 source/scripts/argon-blstrdac.sh create mode 100644 source/scripts/argon-rpi-eeprom-config-psu.py create mode 100644 source/scripts/argon-shutdown.sh create mode 100644 source/scripts/argon-status.sh create mode 100644 source/scripts/argon-uninstall.sh create mode 100644 source/scripts/argon-unitconfig.sh create mode 100644 source/scripts/argon-versioninfo.sh create mode 100644 source/scripts/argondashboard.py create mode 100644 source/scripts/argoneon-oledconfig.sh create mode 100644 source/scripts/argoneon-rtcconfig.sh create mode 100644 source/scripts/argoneond.py create mode 100644 source/scripts/argoneond.service create mode 100644 source/scripts/argoneonoled.py create mode 100644 source/scripts/argonone-fanconfig.sh rename source/{ => scripts}/argonone-irconfig.sh (93%) create mode 100644 source/scripts/argonone-upsconfig.sh create mode 100644 source/scripts/argononed.py create mode 100644 source/scripts/argononed.service create mode 100644 source/scripts/argononeoled.py create mode 100644 source/scripts/argononeoledd.service create mode 100644 source/scripts/argonpowerbutton-libgpiod.py create mode 100644 source/scripts/argonpowerbutton-rpigpio.py create mode 100644 source/scripts/argonregister-v1.py create mode 100644 source/scripts/argonregister.py create mode 100644 source/scripts/argonrtc.py create mode 100644 source/scripts/argonstatus.py create mode 100644 source/scripts/argonsysinfo.py create mode 100644 source/tools/setntpserver.sh diff --git a/source/ar1uninstall.png b/source/ar1uninstall.png deleted file mode 100644 index 2232b6f5ae58228bed96ed798c92612c5243ca9d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 120571 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⋙mjM)zmIykJo=X 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%0DJDihuFaf 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�R?c%1TtR z)(j7@T3nJMf){~}1+lO`GyP9qmq$uRlP=sH3!&JMY-5jXx*aT37N$W3>IUNv-~ zg9P~|o(5+BmRR8JP zV3}$dLPp1>5JL20_*+8_fB-JxfZNpG(%5)%47jJPJ$v8exPrkBW8n&^4_E6UVy}h< zWO&-GDI!)n9wCzRKF4b~lM44^ehqX#XgPeq-rH+bM#G^LbTZyd=~R6kcoSSAfpUTg zV3u5_x%NLb_THVYnBQ!6G+6ksl@m01bbcSp`8V_O)0xr-m{P=Ef94+Rw{kAPwHm!+ zE}%6+qM69!rzV-S1035Bz*QI17Ai2hgFg#CHF>&P5aXZ|x#~@#yOel?{*FI} zmP=FZuEeeamC`dkj2^2*`YfP3t7-?O-Scv-wIS{icpxZ!ruwKo=yW2+uVegdEu>@K zW~24P(elHivEMDbV6Y0q>d|yoNQXfTne}X(70FB4)=57oV+HG8%K>RwH^|G9Ca14a=N-%p(Z6GHX053MK>SL6oaErW3KOQcuVA zfyehSz*)pFuHd>h@rKY!Qh(*g&hcKq>kh2XSSl(i{?Z2jRb5Rp6gv!PlX07E9vd~& zR6&$zztYkdvYX8lIhJe7|5x6r)OlW{Oc%zpGV{VT1D`Tp^$nxcb3@f&+ zL=b9_yM8$fj6jex0;W4Jq~l7Cu8k+suX1!P_u@xV$j{pCsXu^cnY&obb>T3)bL{)V zdJ-sKEt;LX1;U9&px}ijVU0XnSs)CliaQ70L;XBCDZA4p7&WV;!*HQW#Q*oB`pwBz YwWfMRXZdkBkaz`YtLv*(Vy(je2kmQ^*Z=?k literal 0 HcmV?d00001 diff --git a/source/oled/bgcpu.bin b/source/oled/bgcpu.bin new file mode 100644 index 0000000000000000000000000000000000000000..8bbb026585f774867410193b2122f0b3071991fc GIT binary patch literal 1024 zcmZQzAQ^lhSqCW^>>M2(l^E(dIXT%F>>V5&>@hU|#}J{Ozz3k`Kp2^X@DX;<#Qqr| z@b4cx`?F{1>1jX!BcDBEXaDyPEDd!6P3%tuf&W0GK^Pf8#K6+Dv>)t2kX`ll2`Hex l9^@vP`=1d6Kq1Y>2ExcBC|1DIwDdo6kP~3b6OboK8UP>yP2~Up literal 0 HcmV?d00001 diff --git a/source/oled/bgdefault.bin b/source/oled/bgdefault.bin new file mode 100644 index 0000000000000000000000000000000000000000..a7a4446250fc4179b5f07045d76dbf1ecdb4234d GIT binary patch literal 1024 zcmZQz7zHGSK*NI%5CGKm9}IvDsL+E3l8hopF`qrq&fgyoGBN^n%Yy-s!N_><<8PpZ zJs&w{lBkD~T^{I&KR;l;ZFunG5726Pc94_k1(^Be>p_kHSzRyB&rC0ShnqbBS|v9- literal 0 HcmV?d00001 diff --git a/source/oled/bgip.bin b/source/oled/bgip.bin new file mode 100644 index 0000000000000000000000000000000000000000..a5fcd5f62a37c1097dde086776e7e65064154262 GIT binary patch literal 1024 zcmZQz7zKko1RBT&)bSW24)E}yA{ab;h(iG*b?nD51sWdx>sMI(_+hbPeLqwHLm`a> Z?DsEt00ay6+w<`A^B{OMvW}+40s!{+JXHVy literal 0 HcmV?d00001 diff --git a/source/oled/bgraid.bin b/source/oled/bgraid.bin new file mode 100644 index 0000000000000000000000000000000000000000..3ebd92ca5c3ec76095b79acdf9696a4f82a71026 GIT binary patch literal 1024 zcmZQzAQ^lhSqF7B{HLyQ2-6PW1_(tA3^cR<|Nn-D4m1E1X=wP5VkHU(*vuuIdO5AXu&V_^Jv@56m5E?u-D-aLXN&4O$4Hfi8Iw zWDUwF03#!sM`&`RviAOjwI%FKeu%c-Gv?y=I@fd7={%!~vayAeKj(;yW#&9_Tc`68 ai7=w8^y*s5m}llM?qmKtpTDMihxRiWlf cCV~kbqGso->rb6zWZ%rw4C`F8eqj9oZe6t>RsaA1 literal 0 HcmV?d00001 diff --git a/source/oled/bgtemp.bin b/source/oled/bgtemp.bin new file mode 100644 index 0000000000000000000000000000000000000000..aef7da58e013bde13d649ff9e60bb971237fd16f GIT binary patch literal 1024 zcmZQzAQ^lhSqIS?7#Zss85!z5Jv|*6>X~?$>KW=eIXT%-_5VlZ(t`Ivf`fxY0*6QG zU_Zo){|u9W0KuGO4HcOGjKz@GWEkoM1qJIE7#Zss85!z5Jv|*!_5VlZQk8e$z=0121_lyP z0Azju3ZYs-Bl{10u;JlnW`^1I=MRj<%*@YY^8w)q8rlCJ)m(XbXc$18fh3En4(3XV zX=Z*KP+()41BwzGet5tz(8zwU#mu0<=HY?GKadHG5`@)YHMF!J)AyJn@|Z&8ivR#_ Cx+UHK literal 0 HcmV?d00001 diff --git a/source/oled/font16x12.bin b/source/oled/font16x12.bin new file mode 100644 index 0000000000000000000000000000000000000000..e8c819f17fafd6a38aff73a2556b758889586528 GIT binary patch literal 6144 zcmeH~&5El)5QPP|g0IrxHsGr_xE1m$f!PY)eDmFt);r81v$`{rf@pcVeomF8cDn8* z|5pdLEqu_ts&(7^bzQBcTrTAfxRAyZya-}!}WHTa6am(xIP39sl4ls+>ZQ@ z@wZJDWC%`o#f=pkSReJXy$r2*7_bZHT(K6;=^r-RMjpGx1c)_0a7*4J&(Co9Qic)#di_|#FzmZOzT?yM_BKt-8(;IjcOovAW#OIw@qImxZP&#v zY#hme=REg4ar(QU_r1BZs{>KQGkfmkw4Ud6U6xws8Jk-7J$swm%@y~AwJr;L$Z_OZ zp`PIS&hzpv`PZ`V=APY|g^+vS$z|So zW}ig_a5Xr@hKl#Sbqag_*su=^Zau9(+>>+-n{YNhYxq))XLY%S4?RzO>N0OB%n&M-dSXZx8r;bVMfhRoEB$Uj`V zj`>1-zw6y>#)#@)QMe-h@NAVGS1dfm+a0HIxBlrk?Z+4eh zsFjh3^2?L3vorhV&4)igWRf_6-fw#0rfqxtwbDA*IOnAK)4=IO&epy{K~C^M&er7A zxv^(z@#~vb=f9U$Is-X&IJQ2&rK1yikBx!9bv)^h6hjL~!gC-R9}j$|4)P%Jd8in{ zepJL**Xk*0LfjGgV|*WYylq8oH0J&0qA%x4h60TNL=EmRL!TEX9$_sehuLcS{y+J; zz#r?epd~N)D2Tt(75G|LUHri6T!6&};Lxs448g1_jzFoTIeE?bFoV~sV0)8+6xdq1 z4R-s6m-dyX6z&kg9X>|43}!t4sky2hw5SF~H^Xz>g$#m=>!=Pq0P_+tNb_Ug(QvN9 z{akT7$vqQr?u6F@<)rKQaw~3}X+n%np!agY(F}%e{c&_%U?0B0b4lS-?;rH>3fvXM zh3}*A+7GXYE6`+)PGC1gWLb^BOqQ$Fuhr_am^@2?(~8WJEH_#Y%1N{jq@|RaA6d$BA?THfqz-%q^W%pDih}wQ z{8s821$EmJvJN&Xv7vqzj&|x4X)QEN2x~rGhD?>9D3%t+hs2thk#PcuS0?od*&yD)`oeZf_D^@LZ|(DLF?T)R zA&;h!RBhHz3KFA#Ze#y>o;@z<@qQq1alk)gK;%cf&OMbh&R_Xvf{OzA3M@Mhb)eta zKfD{x&n_P|kH^Cf_Yc};AmZhP3-*Ol)0pHvCZOZo;OXV{xj_bZ-wo&qa@PcEFEZ4D fkgmJ9-!oWWbK5PS&mBIa)Og3u^#weNyb16ZAaeYc literal 0 HcmV?d00001 diff --git a/source/oled/font24x16.bin b/source/oled/font24x16.bin new file mode 100644 index 0000000000000000000000000000000000000000..9a4b1a7bc1ed5be91ddbd19e4b387bb9b5c16caa GIT binary patch literal 12288 zcmeI2&1&OH5QPQa3i2v~*&6)n25kj-6~Sx`y7_W)?x~jK83ubMfvCVy*Z-;NR!c3? zbq5_C*mhu^s|>D6)_m2@^DtCf;#}XUjo8{;ufx#yw&?p|xL(cu-7A?*&Uw97bg@8h zTdEv}V6zX>ko1lK7oGZK9dhEYc}4jBU;Os0ww+9dy?oKiv(~d`A3a)DLalJiZ{r8G zx{kl1L}OW2n(M7IrTi>5tvPg6&v=_Zcz<2r{`|<>Rd4gH^ZzLybh)pREgh;OW3r5z zH9MBH*ZGZFNx1u(*m!i$);-_DuzVoh1k~_ig_?zp=94 zaXmWl868-!#5MmYrf0`{6wl#*bl^E1U@s4CV_Mg0BK1hu*6KTQ_5CVu(Eon_XrI{M zaVCzMzfK31rDAz`={kN;|3=HAA1C(ndHkPFr>;A_6Yr;e58ri{OXZLdMLKb9Hp#lq zQqC^RLJTyV?!cQ))wZriGN}t0A5AtMhxnY>5baQG*6`W4fl)X(d*P|SuVNP9zY{Ul-_$nr>D!;au zKK^U_wLY;j!bT+7vbb|qY8&IZT#yLQ>E!Q8;#=Eg2|hYpGs_;mPTc4(m)K`Mu9a{SiWzl$i2diakx|v*%BpPTcIymeFyI*^a#{TYFrO4t!n*Sdr(m z)+H#s@}G&Q-bK%hd&D|A@VOmW8sd3WiRY=PMHwmYk)hV=J92rOW2EPK`$T`ND~j(k zz`aXj{jh059J z%2rSHCdVhPPb^!#tldXp;5*Ct>~Yt*T1(VHbM?B8BVFd#`T2+geyT8mkuNVM@Y^04 z`(^a{J`x^_)M-rk`-2e`xew?y#!pe?2RoPT~#n4{C*fFtWyvAGC zyP|pZYrRx|^jW>*=sVIe+kWVjZls=n+4^txqkIQw`@{bg)%`~pol#t^QWKk%X^Z1} zbl@v?z$=+`>-*_(oa?*x@rXx+qXS=|13t<7lz)$JvD&JY9-QBm^wi}2N+X{Lp4B0} Lk31{d^WyvmxH-4; literal 0 HcmV?d00001 diff --git a/source/oled/font32x24.bin b/source/oled/font32x24.bin new file mode 100644 index 0000000000000000000000000000000000000000..0ed12db72ff1f3debd6ca84324d1aada4bda09ec GIT binary patch literal 24576 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 WO3k~!)*pBGm@yc{U3N#rU-NHFe8;;0 literal 0 HcmV?d00001 diff --git a/source/oled/logo1v5.bin b/source/oled/logo1v5.bin new file mode 100644 index 0000000000000000000000000000000000000000..43aeed056ccaade84ece5b7cefe109adca9b2d5b GIT binary patch literal 1024 zcmZ`%F>b{m3bkX+;jTGB0N)x$K&xUbFv!oHc#{J^@$6 zz;wCw6H4?K9@gnrGkDYn7&<6}xW zUn97RIj49A&+I}R$MOCFwztOv(uzq3B8?qg9TeQS`j~s$Wd`IJ3iMEV>rGPg)-}Rc zz6{_eGL=%rHitxw_}rWGHFX5 $blstrdactmpconfigfile + cat $blstrdactmpconfigfile | sudo tee $CONFIG 1> /dev/null + sudo rm $blstrdactmpconfigfile + + echo "Uninstall Completed" + echo + + needrestart=1 + +elif [ $newmode -eq 3 ] +then + # Audio Conf + + loopflag=1 + while [ $loopflag -eq 1 ] + do + echo + echo "Select audio configuration:" + echo " 1. PulseAudio" + echo " 2. Pipewire" + echo " 3. Cancel" + echo -n "Enter Number (1-3):" + + newmode=$( get_number ) + if [[ $newmode -ge 1 && $newmode -le 3 ]] + then + loopflag=0 + fi + done + + if [[ $newmode -ge 1 && $newmode -le 2 ]] + then + sudo raspi-config nonint do_audioconf $newmode + else + echo "Cancelled" + fi + +elif [ $newmode -eq 1 ] +then + # Install + + echo "$CONFIGSETTING" | sudo tee -a $CONFIG 1> /dev/null + + #sudo raspi-config nonint do_audioconf 1 + #systemctl --global -q disable pipewire-pulse + #systemctl --global -q disable wireplumber + #systemctl --global -q enable pulseaudio + #if [ -e /etc/alsa/conf.d/99-pipewire-default.conf ] ; then + # rm /etc/alsa/conf.d/99-pipewire-default.conf + #fi + + echo "Please run configuration and choose different audio configuration if there are problems" + + needrestart=1 +else + echo "Cancelled" + #exit +fi + + +echo +#echo "Thank you." +if [ $needrestart -eq 1 ] +then + echo "Changes should take after reboot." +fi + diff --git a/source/scripts/argon-rpi-eeprom-config-psu.py b/source/scripts/argon-rpi-eeprom-config-psu.py new file mode 100644 index 0000000..d7c486f --- /dev/null +++ b/source/scripts/argon-rpi-eeprom-config-psu.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/source/scripts/argon-shutdown.sh b/source/scripts/argon-shutdown.sh new file mode 100644 index 0000000..fdcd539 --- /dev/null +++ b/source/scripts/argon-shutdown.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +pythonbin=/usr/bin/python3 +argononefanscript=/etc/argon/argononed.py +argoneonrtcscript=/etc/argon/argoneond.py +argonirconfigscript=/etc/argon/argonone-ir + +if [ ! -z "$1" ] +then + $pythonbin $argononefanscript FANOFF + if [ "$1" = "poweroff" ] || [ "$1" = "halt" ] + then + if [ -f $argonirconfigscript ] + then + if [ -f $argoneonrtcscript ] + then + $pythonbin $argoneonrtcscript SHUTDOWN + fi + $pythonbin $argononefanscript SHUTDOWN + fi + fi +fi diff --git a/source/scripts/argon-status.sh b/source/scripts/argon-status.sh new file mode 100644 index 0000000..7c13f44 --- /dev/null +++ b/source/scripts/argon-status.sh @@ -0,0 +1,106 @@ +#!/bin/bash + + +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 +} + +INSTALLATIONFOLDER=/etc/argon +pythonbin="sudo /usr/bin/python3" +argonstatusscript=$INSTALLATIONFOLDER/argonstatus.py +argondashboardscript=$INSTALLATIONFOLDER/argondashboard.py +argononefanscript=$INSTALLATIONFOLDER/argononed.py +argoneonrtcscript=$INSTALLATIONFOLDER/argoneond.py + + +echo "--------------------------" +echo " Argon System Information" +echo "--------------------------" + + +loopflag=1 +while [ $loopflag -eq 1 ] +do + echo + echo " 1. Temperatures" + echo " 2. CPU Utilization" + echo " 3. Storage Capacity" + echo " 4. RAM" + echo " 5. IP Address" + lastoption=5 + if [ -f $argononefanscript ] + then + echo " 6. Fan Speed" + lastoption=6 + fi + if [ -f "$argoneonrtcscript" ] + then + echo " 7. RTC Schedules" + echo " 8. RAID" + lastoption=8 + fi + lastoption=$((lastoption + 1)) + echo " ${lastoption}. Dashboard" + echo + echo " 0. Back" + echo -n "Enter Number (0-${lastoption}):" + + newmode=$( get_number ) + if [ $newmode -eq 0 ] + then + loopflag=0 + elif [ $newmode -gt 0 ] && [ $newmode -le $lastoption ] + then + echo "--------------------------" + if [ $newmode -eq $lastoption ] + then + $pythonbin $argondashboardscript + elif [ $newmode -eq 1 ] + then + $pythonbin $argonstatusscript "temperature" + elif [ $newmode -eq 2 ] + then + $pythonbin $argonstatusscript "cpu usage" + elif [ $newmode -eq 3 ] + then + $pythonbin $argonstatusscript "storage" + elif [ $newmode -eq 4 ] + then + $pythonbin $argonstatusscript "ram" + elif [ $newmode -eq 5 ] + then + $pythonbin $argonstatusscript "ip" + elif [ $newmode -eq 6 ] + then + $pythonbin $argonstatusscript "temperature" "fan configuration" "fan speed" + elif [ $newmode -eq 7 ] + then + $pythonbin $argoneonrtcscript GETSCHEDULELIST + elif [ $newmode -eq 8 ] + then + $pythonbin $argonstatusscript "raid" + fi + echo "--------------------------" + fi +done + diff --git a/source/scripts/argon-uninstall.sh b/source/scripts/argon-uninstall.sh new file mode 100644 index 0000000..88e9e99 --- /dev/null +++ b/source/scripts/argon-uninstall.sh @@ -0,0 +1,131 @@ +#!/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 + + +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 + +# 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 + + # Remove files + sudo rm /lib/systemd/system/argononeupsd.service + sudo rm /lib/systemd/system/argonupsrtcd.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 + +sudo rm /usr/bin/argon-config + +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 +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/source/scripts/argon-unitconfig.sh b/source/scripts/argon-unitconfig.sh new file mode 100644 index 0000000..636490e --- /dev/null +++ b/source/scripts/argon-unitconfig.sh @@ -0,0 +1,105 @@ +#!/bin/bash + +unitconfigfile=/etc/argonunits.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 +} + +saveconfig () { + echo "#" > $unitconfigfile + echo "# Argon Unit Configuration" >> $unitconfigfile + echo "#" >> $unitconfigfile + echo "temperature=$1" >> $unitconfigfile +} + +updateconfig=1 +unitloopflag=1 +while [ $unitloopflag -eq 1 ] +do + if [ $updateconfig -eq 1 ] + then + . $unitconfigfile + fi + + updateconfig=0 + if [ -z "$temperature" ] + then + temperature="C" + 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 $temperature + updateconfig=0 + fi + + + echo "-----------------------------" + echo "Argon Display Units" + echo "-----------------------------" + echo "Choose from the list:" + echo " 1. Temperature: $temperature" + echo + echo " 0. Back" + echo -n "Enter Number (0-1):" + + newmode=$( get_number ) + if [ $newmode -eq 0 ] + then + unitloopflag=0 + elif [ $newmode -eq 1 ] + then + echo + echo "-----------------------------" + echo "Temperature Display" + echo "-----------------------------" + echo "Choose from the list:" + echo " 1. Celsius" + echo " 2. Fahrenheit" + echo + echo " 0. Cancel" + echo -n "Enter Number (0-2):" + + cmdmode=$( get_number ) + if [ $cmdmode -eq 1 ] + then + temperature="C" + updateconfig=1 + elif [ $cmdmode -eq 2 ] + then + temperature="F" + updateconfig=1 + fi + fi + + if [ $updateconfig -eq 1 ] + then + saveconfig $temperature + sudo systemctl restart argononed.service + fi +done + +echo diff --git a/source/scripts/argon-versioninfo.sh b/source/scripts/argon-versioninfo.sh new file mode 100644 index 0000000..e10889d --- /dev/null +++ b/source/scripts/argon-versioninfo.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +VERSIONINFO="2505003" + +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/source/scripts/argondashboard.py b/source/scripts/argondashboard.py new file mode 100644 index 0000000..d5c4032 --- /dev/null +++ b/source/scripts/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/source/scripts/argoneon-oledconfig.sh b/source/scripts/argoneon-oledconfig.sh new file mode 100644 index 0000000..f95e1e8 --- /dev/null +++ b/source/scripts/argoneon-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" == "raid" ] + then + pagename="RAID Information" + elif [ "$1" == "ram" ] + then + pagename="Available RAM" + elif [ "$1" == "temp" ] + then + pagename="CPU/HDD Temperature" + elif [ "$1" == "ip" ] + then + pagename="IP Address" + else + pagename="Invalid" + fi +} + +configure_pagelist () { + pagemasterlist="clock cpu storage raid 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="clock ip" + 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/argoneon-rtcconfig.sh b/source/scripts/argoneon-rtcconfig.sh new file mode 100644 index 0000000..e5199bd --- /dev/null +++ b/source/scripts/argoneon-rtcconfig.sh @@ -0,0 +1,421 @@ +#!/bin/bash + +if [ -z "$1" ] +then + rtcdaemonname=argoneond + rtcconfigfile=/etc/argoneonrtc.conf +else + rtcdaemonname=${1}d + rtcconfigfile=/etc/${1}.conf +fi + + +pythonbin=/usr/bin/python3 +argonrtcscript=/etc/argon/$rtcdaemonname.py + +CHECKPLATFORM="Others" +# Check if Raspbian +grep -q -F 'Raspbian' /etc/os-release &> /dev/null +if [ $? -eq 0 ] +then + CHECKPLATFORM="Raspbian" +else + # Ubuntu needs elevated access for SMBus + pythonbin="sudo /usr/bin/python3" +fi + + +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 +} + +configure_schedule () { + scheduleloopflag=1 + while [ $scheduleloopflag -eq 1 ] + do + echo "--------------------------------" + echo " Configure Schedule " + echo "--------------------------------" + echo " 1. Add Schedule" + echo " or" + echo " Remove Schedule" + $pythonbin $argonrtcscript GETSCHEDULELIST + echo + echo " 99. Main Menu" + echo " 0. Back" + #echo "NOTE: You can also edit $rtcconfigfile directly" + echo -n "Enter Number:" + + newmode=$( get_number ) + if [ $newmode -eq 0 ] + then + scheduleloopflag=0 + elif [ $newmode -eq 99 ] + then + scheduleloopflag=0 + rtcloopflag=2 + elif [ $newmode -eq 1 ] + then + configure_newschedule + elif [ $newmode -gt 1 ] + then + echo "CONFIRM SCHEDULE REMOVAL" + $pythonbin $argonrtcscript SHOWSCHEDULE $newmode + echo -n "Press Y to remove schedule #$newmode:" + read -n 1 confirm + if [ "$confirm" = "y" ] + then + confirm="Y" + fi + if [ "$confirm" = "Y" ] + then + $pythonbin $argonrtcscript REMOVESCHEDULE $newmode + sudo systemctl restart $rtcdaemonname.service + fi + echo "" + fi + done +} + +configure_newschedule () { + + cmdmode=1 + hour=8 + minute=0 + minuteprefix=":0" + dayidx=0 + repeat=1 + + subloopflag=1 + while [ $subloopflag -eq 1 ] + do + minuteprefix=":0" + if [ $minute -ge 10 ] + then + minuteprefix=":" + fi + + typestr="Shutdown" + if [ $cmdmode -eq 1 ] + then + typestr="Startup" + fi + + daystr="Daily" + if [ $dayidx -eq 1 ] + then + daystr="Mon" + elif [ $dayidx -eq 2 ] + then + daystr="Tue" + elif [ $dayidx -eq 3 ] + then + daystr="Wed" + elif [ $dayidx -eq 4 ] + then + daystr="Thu" + elif [ $dayidx -eq 5 ] + then + daystr="Fri" + elif [ $dayidx -eq 6 ] + then + daystr="Sat" + elif [ $dayidx -eq 7 ] + then + daystr="Sun" + fi + + repeatstr="Yes" + if [ $repeat -eq 0 ] + then + repeatstr="Once" + if [ $dayidx -eq 0 ] + then + daystr="Next Occurence" + fi + fi + + echo "--------------------------------" + echo " Configure Schedule" + echo "--------------------------------" + echo " 1. Type: $typestr" + echo " 2. Set Time: $hour$minuteprefix$minute" + echo " 3. Repeating: $repeatstr" + echo " 4. Day: $daystr" + echo + echo " 5. Add Schedule" + echo + echo " 0. Cancel" + echo -n "Enter Number (0-5):" + + setmode=$( get_number ) + if [ $setmode -eq 0 ] + then + subloopflag=0 + elif [ $setmode -eq 1 ] + then + echo "--------------------------------" + echo " Schedule Type " + echo "--------------------------------" + echo " 1. Startup" + echo " 2. Shutdown" + echo + echo -n "Enter Number (1-2):" + + tmpval=$( get_number ) + if [ $tmpval -eq 1 ] + then + cmdmode=1 + elif [ $tmpval -eq 2 ] + then + cmdmode=0 + else + echo "Invalid Option" + fi + elif [ $setmode -eq 2 ] + then + echo -n "Enter Hour (0-23):" + tmphour=$( get_number ) + echo -n "Enter Minute (0-59):" + tmpminute=$( get_number ) + if [[ $tmpminute -ge 0 && $tmpminute -le 59 && $tmphour -ge 0 && $tmphour -le 23 ]] + then + minute=$tmpminute + hour=$tmphour + else + echo "Invalid value(s)" + fi + elif [ $setmode -eq 3 ] + then + echo -n "Repeat schedule (Y/n)?:" + read -n 1 confirm + if [ "$confirm" = "y" ] + then + repeat=1 + else + repeat=0 + fi + elif [ $setmode -eq 4 ] + then + echo "Select Day of the Week:" + echo " 0. Daily" + echo " 1. Monday" + echo " 2. Tuesday" + echo " 3. Wednesday" + echo " 4. Thursday" + echo " 5. Friday" + echo " 6. Saturday" + echo " 7. Sunday" + + echo -n "Enter Number (0-7):" + tmpval=$( get_number ) + if [[ $tmpval -ge 0 && $tmpval -le 7 ]] + then + dayidx=$tmpval + else + echo "Invalid Option" + fi + elif [ $setmode -eq 5 ] + then + if [ $dayidx -eq 0 ] + then + cronweekday="*" + elif [ $dayidx -eq 7 ] + then + cronweekday="7" + else + cronweekday=$dayidx + fi + cmdcode="off" + if [ $cmdmode -eq 1 ] + then + cmdcode="on" + fi + + echo "$minute $hour * * $cronweekday $cmdcode" >> $rtcconfigfile + sudo systemctl restart $rtcdaemonname.service + subloopflag=0 + fi + done +} + +configure_newcron () { + subloopflag=1 + while [ $subloopflag -eq 1 ] + do + echo "--------------------------------" + echo " Schedule Type " + echo "--------------------------------" + echo " 1. Startup" + echo " 2. Shutdown" + echo + echo " 0. Cancel" + echo -n "Enter Number (0-2):" + + cmdmode=$( get_number ) + if [ $cmdmode -eq 0 ] + then + subloopflag=0 + elif [[ $cmdmode -ge 1 && $cmdmode -le 2 ]] + then + cmdcode="on" + echo "--------------------------------" + if [ $cmdmode -eq 1 ] + then + echo " Schedule Startup" + else + echo " Schedule Shutdown" + cmdcode="off" + fi + echo "--------------------------------" + echo "Select Schedule:" + echo " 1. Hourly" + echo " 2. Daily" + echo " 3. Weekly" + echo " 4. Monthly" + echo + echo " 0. Back" + echo -n "Enter Number (0-4):" + + newmode=$( get_number ) + if [[ $newmode -ge 1 && $newmode -le 4 ]] + then + echo "" + if [ $cmdmode -eq 1 ] + then + echo "New Startup Schedule" + else + echo "New Shutdown Schedule" + fi + + if [ $newmode -eq 1 ] + then + echo -n "Enter Minute (0-59):" + minute=$( get_number ) + if [[ $minute -ge 0 && $minute -le 59 ]] + then + echo "$minute * * * * $cmdcode" >> $rtcconfigfile + sudo systemctl restart $rtcdaemonname.service + subloopflag=0 + else + echo "Invalid value" + fi + elif [ $newmode -eq 2 ] + then + echo -n "Enter Hour (0-23):" + hour=$( get_number ) + echo -n "Enter Minute (0-59):" + minute=$( get_number ) + if [[ $minute -ge 0 && $minute -le 59 && $hour -ge 0 && $hour -le 23 ]] + then + echo "$minute $hour * * * $cmdcode" >> $rtcconfigfile + sudo systemctl restart $rtcdaemonname.service + subloopflag=0 + else + echo "Invalid value(s)" + fi + elif [ $newmode -eq 3 ] + then + echo "Select Day of the Week:" + echo " 0. Sunday" + echo " 1. Monday" + echo " 2. Tuesday" + echo " 3. Wednesday" + echo " 4. Thursday" + echo " 5. Friday" + echo " 6. Saturday" + + echo -n "Enter Number (0-6):" + weekday=$( get_number ) + echo -n "Enter Hour (0-23):" + hour=$( get_number ) + echo -n "Enter Minute (0-59):" + minute=$( get_number ) + + if [[ $minute -ge 0 && $minute -le 59 && $hour -ge 0 && $hour -le 23 && $weekday -ge 0 && $weekday -le 6 ]] + then + echo "$minute $hour * * $weekday $cmdcode" >> $rtcconfigfile + sudo systemctl restart $rtcdaemonname.service + subloopflag=0 + else + echo "Invalid value(s)" + fi + elif [ $newmode -eq 4 ] + then + echo -n "Enter Date (1-31):" + monthday=$( get_number ) + if [[ $monthday -ge 29 ]] + then + echo "WARNING: This schedule will not trigger for certain months" + fi + echo -n "Enter Hour (0-23):" + hour=$( get_number ) + echo -n "Enter Minute (0-59):" + minute=$( get_number ) + + if [[ $minute -ge 0 && $minute -le 59 && $hour -ge 0 && $hour -le 23 && $monthday -ge 1 && $monthday -le 31 ]] + then + echo "$minute $hour $monthday * * $cmdcode" >> $rtcconfigfile + sudo systemctl restart $rtcdaemonname.service + subloopflag=0 + else + echo "Invalid value(s)" + fi + fi + fi + fi + done +} + +rtcloopflag=1 +while [ $rtcloopflag -eq 1 ] +do + echo "----------------------------" + echo "Argon RTC Configuration Tool" + echo "----------------------------" + $pythonbin $argonrtcscript GETRTCTIME + echo "Choose from the list:" + echo " 1. Update RTC Time" + echo " 2. Configure Startup/Shutdown Schedules" + echo + echo " 0. Exit" + echo -n "Enter Number (0-2):" + + newmode=$( get_number ) + if [ $newmode -eq 0 ] + then + rtcloopflag=0 + elif [[ $newmode -ge 1 && $newmode -le 2 ]] + then + if [ $newmode -eq 1 ] + then + echo "Matching RTC Time to System Time..." + $pythonbin $argonrtcscript UPDATERTCTIME + elif [ $newmode -eq 2 ] + then + configure_schedule + fi + fi +done + +echo diff --git a/source/scripts/argoneond.py b/source/scripts/argoneond.py new file mode 100644 index 0000000..c00612b --- /dev/null +++ b/source/scripts/argoneond.py @@ -0,0 +1,487 @@ +#!/usr/bin/python3 + + +import sys +import datetime +import math + +import os +import time + +sys.path.append("/etc/argon/") +from argonregister import argonregister_initializebusobj +import argonrtc + + +# Initialize I2C Bus +bus = argonregister_initializebusobj() + +ADDR_RTC=0x51 + +################# +# Common/Helpers +################# + +RTC_CONFIGFILE = "/etc/argoneonrtc.conf" + +RTC_ALARM_BIT = 0x8 +RTC_TIMER_BIT = 0x4 + +# PCF8563 number system Binary Coded Decimal (BCD) + +# BCD to Decimal +def numBCDtoDEC(val): + return (val & 0xf)+(((val >> 4) & 0xf)*10) + +# Decimal to BCD +def numDECtoBCD(val): + return (math.floor(val/10)<<4) + (val % 10) + +# Check if Event Bit is raised +def hasRTCEventFlag(flagbit): + if bus is None: + return False + bus.write_byte(ADDR_RTC,1) + out = bus.read_byte_data(ADDR_RTC, 1) + return (out & flagbit) != 0 + +# Clear Event Bit if raised +def clearRTCEventFlag(flagbit): + if bus is None: + return False + + out = bus.read_byte_data(ADDR_RTC, 1) + if (out & flagbit) != 0: + # Unset only if fired + bus.write_byte_data(ADDR_RTC, 1, out&(0xff-flagbit)) + return True + return False + +# Enable Event Flag +def setRTCEventFlag(flagbit, enabled): + if bus is None: + return + + # 0x10 = TI_TP flag, 0 by default + ti_tp_flag = 0x10 + # flagbit=0x4 for timer flag, 0x1 for enable timer flag + # flagbit=0x8 for alarm flag, 0x2 for enable alarm flag + enableflagbit = flagbit>>2 + disableflagbit = 0 + if enabled == False: + disableflagbit = enableflagbit + enableflagbit = 0 + + out = bus.read_byte_data(ADDR_RTC, 1) + bus.write_byte_data(ADDR_RTC, 1, (out&(0xff-flagbit-disableflagbit - ti_tp_flag))|enableflagbit) + + +######### +# Describe Methods +######### + +# Describe Timer Setting +def describeTimer(showsetting): + if bus is None: + return "Error" + + out = bus.read_byte_data(ADDR_RTC, 14) + tmp = out & 3 + if tmp == 3: + outstr = " Minute(s)" + elif tmp == 2: + outstr = " Second(s)" + elif tmp == 1: + outstr = "/64th Second" + elif tmp == 0: + outstr = "/4096th Second" + + if (out & 0x80) != 0: + out = bus.read_byte_data(ADDR_RTC, 15) + return "Every "+(numBCDtoDEC(out)+1)+outstr + elif showsetting == True: + return "Disabled (Interval every 1"+outstr+")" + # Setting might matter to save resources + return "None" + + +# Describe Alarm Setting +def describeAlarm(): + if bus is None: + return "Error" + + minute = -1 + hour = -1 + caldate = -1 + weekday = -1 + + out = bus.read_byte_data(ADDR_RTC, 9) + if (out & 0x80) == 0: + minute = numBCDtoDEC(out & 0x7f) + + out = bus.read_byte_data(ADDR_RTC, 10) + if (out & 0x80) == 0: + hour = numBCDtoDEC(out & 0x3f) + + out = bus.read_byte_data(ADDR_RTC, 11) + if (out & 0x80) == 0: + caldate = numBCDtoDEC(out & 0x3f) + + out = bus.read_byte_data(ADDR_RTC, 12) + if (out & 0x80) == 0: + weekday = numBCDtoDEC(out & 0x7) + + if weekday < 0 and caldate < 0 and hour < 0 and minute < 0: + return "None" + + # Convert from UTC + utcschedule = argonrtc.describeSchedule([-1], [weekday], [caldate], [hour], [minute]) + weekday, caldate, hour, minute = argonrtc.convertAlarmTimezone(weekday, caldate, hour, minute, False) + + return argonrtc.describeSchedule([-1], [weekday], [caldate], [hour], [minute]) + " Local (RTC Schedule: "+utcschedule+" UTC)" + + +# Describe Control Flags +def describeControlRegisters(): + if bus is None: + print("Error") + return + + out = bus.read_byte_data(ADDR_RTC, 1) + + print("\n***************") + print("Control Status 2") + print("\tTI_TP Flag:", ((out & 0x10) != 0)) + print("\tAlarm Flag:", ((out & RTC_ALARM_BIT) != 0),"( Enabled =", (out & (RTC_ALARM_BIT>>2)) != 0, ")") + print("\tTimer Flag:", ((out & RTC_TIMER_BIT) != 0),"( Enabled =", (out & (RTC_TIMER_BIT>>2)) != 0, ")") + + print("Alarm Setting:") + print("\t"+describeAlarm()) + + print("Timer Setting:") + print("\t"+describeTimer(True)) + + print("***************\n") + + +######### +# Alarm +######### + +# Check if RTC Alarm Flag is ON +def hasRTCAlarmFlag(): + return hasRTCEventFlag(RTC_ALARM_BIT) + +# Clear RTC Alarm Flag +def clearRTCAlarmFlag(): + return clearRTCEventFlag(RTC_ALARM_BIT) + +# Enables RTC Alarm Register +def enableAlarm(registeraddr, value, mask): + if bus is None: + return + + # 0x00 is Enabled + bus.write_byte_data(ADDR_RTC, registeraddr, (numDECtoBCD(value)&mask)) + +# Disables RTC Alarm Register +def disableAlarm(registeraddr): + if bus is None: + return + + # 0x80 is disabled + bus.write_byte_data(ADDR_RTC, registeraddr, 0x80) + +# Removes all alarm settings +def removeRTCAlarm(): + setRTCEventFlag(RTC_ALARM_BIT, False) + + disableAlarm(9) + disableAlarm(10) + disableAlarm(11) + disableAlarm(12) + +# Set RTC Alarm (Negative values ignored) +def setRTCAlarm(enableflag, weekday, caldate, hour, minute): + + weekday, caldate, hour, minute = argonrtc.getRTCAlarm(weekday, caldate, hour, minute) + if caldate < 1 and weekday < 0 and hour < 0 and minute < 0: + return -1 + + clearRTCAlarmFlag() + setRTCEventFlag(RTC_ALARM_BIT, enableflag) + + if minute >= 0: + enableAlarm(9, minute, 0x7f) + else: + disableAlarm(9) + + if hour >= 0: + enableAlarm(10, hour, 0x7f) + else: + disableAlarm(10) + + if caldate >= 0: + enableAlarm(11, caldate, 0x7f) + else: + disableAlarm(11) + + if weekday >= 0: + # 0 - Sun (datetime 0 - Mon) + if weekday > 5: + weekday = 0 + else: + weekday = weekday + 1 + enableAlarm(12, weekday, 0x7f) + else: + disableAlarm(12) + + return 0 + +# Set RTC Hourly Alarm +def setRTCAlarmHourly(enableflag, minute): + return setRTCAlarm(enableflag, -1, -1, -1, minute) + +# Set RTC Daily Alarm +def setRTCAlarmDaily(enableflag, hour, minute): + return setRTCAlarm(enableflag, -1, -1, hour, minute) + +# Set RTC Weekly Alarm +def setRTCAlarmWeekly(enableflag, dayofweek, hour, minute): + return setRTCAlarm(enableflag, dayofweek, -1, hour, minute) + +# Set RTC Monthly Alarm +def setRTCAlarmMonthly(enableflag, caldate, hour, minute): + return setRTCAlarm(enableflag, -1, caldate, hour, minute) + +######### +# Timer +######### + +# Check if RTC Timer Flag is ON +def hasRTCTimerFlag(): + return hasRTCEventFlag(RTC_TIMER_BIT) + +# Clear RTC Timer Flag +def clearRTCTimerFlag(): + return clearRTCEventFlag(RTC_TIMER_BIT) + +# Remove RTC Timer Setting +def removeRTCTimer(): + if bus is None: + return + + setRTCEventFlag(RTC_TIMER_BIT, False) + + # Timer disable and Set Timer frequency to lowest (0x3=1 per minute) + bus.write_byte_data(ADDR_RTC, 14, 3) + bus.write_byte_data(ADDR_RTC, 15, 0) + +# Set RTC Timer Interval +def setRTCTimerInterval(enableflag, value, inSeconds = False): + if bus is None: + return -1 + + if value > 255 or value < 1: + return -1 + clearRTCTimerFlag() + setRTCEventFlag(RTC_TIMER_BIT, enableflag) + + # 0x80 Timer Enabled, mode: 0x3=1/Min, 0x2=1/Sec, 0x1=Per 64th Sec, 0=Per 4096th Sec + timerconfigFlag = 0x83 + if inSeconds == True: + timerconfigFlag = 0x82 + + bus.write_byte_data(ADDR_RTC, 14, timerconfigFlag) + bus.write_byte_data(ADDR_RTC, 15, numDECtoBCD(value&0xff)) + return 0 + +############# +# Date/Time +############# + +# Returns RTC timestamp as datetime object +def getRTCdatetime(): + if bus is None: + return datetime.datetime(2000, 1, 1, 0, 0, 0) + + # Data Sheet Recommends to read this manner (instead of from registers) + bus.write_byte(ADDR_RTC,2) + + out = bus.read_byte(ADDR_RTC) + out = numBCDtoDEC(out & 0x7f) + second = out + #warningflag = (out & 0x80)>>7 + + out = bus.read_byte(ADDR_RTC) + minute = numBCDtoDEC(out & 0x7f) + + out = bus.read_byte(ADDR_RTC) + hour = numBCDtoDEC(out & 0x3f) + + out = bus.read_byte(ADDR_RTC) + caldate = numBCDtoDEC(out & 0x3f) + + out = bus.read_byte(ADDR_RTC) + #weekDay = numBCDtoDEC(out & 7) + + out = bus.read_byte(ADDR_RTC) + month = numBCDtoDEC(out & 0x1f) + + out = bus.read_byte(ADDR_RTC) + year = numBCDtoDEC(out) + + #print({"year":year, "month": month, "date": caldate, "hour": hour, "minute": minute, "second": second}) + + if month == 0: + # Reset, uninitialized RTC + month = 1 + + # Timezone is GMT/UTC +0 + # Year is from 2000 + try: + return datetime.datetime(year+2000, month, caldate, hour, minute, second)+argonrtc.getLocaltimeOffset() + except: + return datetime.datetime(2000, 1, 1, 0, 0, 0) + +# set RTC time using datetime object (Local time) +def setRTCdatetime(localdatetime): + if bus is None: + return + # Set local time to UTC + localdatetime = localdatetime - argonrtc.getLocaltimeOffset() + + # python Sunday = 6, RTC Sunday = 0 + weekDay = localdatetime.weekday() + if weekDay == 6: + weekDay = 0 + else: + weekDay = weekDay + 1 + + # Write to respective registers + bus.write_byte_data(ADDR_RTC,2,numDECtoBCD(localdatetime.second)) + bus.write_byte_data(ADDR_RTC,3,numDECtoBCD(localdatetime.minute)) + bus.write_byte_data(ADDR_RTC,4,numDECtoBCD(localdatetime.hour)) + bus.write_byte_data(ADDR_RTC,5,numDECtoBCD(localdatetime.day)) + bus.write_byte_data(ADDR_RTC,6,numDECtoBCD(weekDay)) + bus.write_byte_data(ADDR_RTC,7,numDECtoBCD(localdatetime.month)) + + # Year is from 2000 + bus.write_byte_data(ADDR_RTC,8,numDECtoBCD(localdatetime.year-2000)) + + +######### +# Config +######### + +# 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 + removeRTCAlarm() + return nextcommandtime + + setRTCAlarm(True, nextcommandtime.weekday(), nextcommandtime.day, nextcommandtime.hour, nextcommandtime.minute) + return nextcommandtime + + +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() + + # Enable Alarm/Timer Flags + enableflag = True + + if cmd == "CLEAN": + removeRTCAlarm() + removeRTCTimer() + elif cmd == "SHUTDOWN": + clearRTCAlarmFlag() + clearRTCTimerFlag() + + elif cmd == "GETRTCSCHEDULE": + print("Alarm Setting:") + print("\t"+describeAlarm()) + #print("Timer Setting:") + #print("\t"+describeTimer(True)) + + elif cmd == "GETRTCTIME": + print("RTC Time:", getRTCdatetime()) + + elif cmd == "UPDATERTCTIME": + setRTCdatetime(datetime.datetime.now()) + print("RTC Time:", getRTCdatetime()) + + 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": + argonrtc.updateSystemTime(getRTCdatetime()) + + commandschedulelist = argonrtc.formCommandScheduleList(argonrtc.loadConfigList(RTC_CONFIGFILE)) + nextrtcalarmtime = setNextAlarm(commandschedulelist, datetime.datetime.now()) + serviceloop = True + while serviceloop==True: + clearRTCAlarmFlag() + clearRTCTimerFlag() + + 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) + + +elif False: + print("System Time: ", datetime.datetime.now()) + print("RTC Time: ", getRTCdatetime()) + + describeControlRegisters() diff --git a/source/scripts/argoneond.service b/source/scripts/argoneond.service new file mode 100644 index 0000000..3330da2 --- /dev/null +++ b/source/scripts/argoneond.service @@ -0,0 +1,10 @@ +[Unit] +Description=Argon EON RTC Service +After=multi-user.target +[Service] +Type=simple +Restart=always +RemainAfterExit=true +ExecStart=/usr/bin/python3 /etc/argon/argoneond.py SERVICE +[Install] +WantedBy=multi-user.target diff --git a/source/scripts/argoneonoled.py b/source/scripts/argoneonoled.py new file mode 100644 index 0000000..bc344a6 --- /dev/null +++ b/source/scripts/argoneonoled.py @@ -0,0 +1,345 @@ +#!/usr/bin/python3 + + +import sys +import datetime +import math + +import os +import time + +# Initialize I2C Bus +import smbus + +try: + bus=smbus.SMBus(1) +except Exception: + try: + # Older version + bus=smbus.SMBus(0) + except Exception: + print("Unable to detect i2c") + bus=None + + +OLED_WD=128 +OLED_HT=64 +OLED_SLAVEADDRESS=0x6a +ADDR_OLED=0x3c + +OLED_NUMFONTCHAR=256 + +OLED_BUFFERIZE = ((OLED_WD*OLED_HT)>>3) +oled_imagebuffer = [0] * OLED_BUFFERIZE + + +def oled_getmaxY(): + return OLED_HT + +def oled_getmaxX(): + return OLED_WD + +def oled_loadbg(bgname): + if bgname == "bgblack": + oled_clearbuffer() + return + elif bgname == "bgwhite": + oled_clearbuffer(1) + return + try: + file = open("/etc/argon/oled/"+bgname+".bin", "rb") + bgbytes = list(file.read()) + file.close() + ctr = len(bgbytes) + if ctr == OLED_BUFFERIZE: + oled_imagebuffer[:] = bgbytes + elif ctr > OLED_BUFFERIZE: + oled_imagebuffer[:] = bgbytes[0:OLED_BUFFERIZE] + else: + oled_imagebuffer[0:ctr] = bgbytes + # Clear the rest of the buffer + while ctr < OLED_BUFFERIZE: + oled_imagebuffer[ctr] = 0 + ctr=ctr+1 + except FileNotFoundError: + oled_clearbuffer() + + +def oled_clearbuffer(value = 0): + if value != 0: + value = 0xff + ctr = 0 + while ctr < OLED_BUFFERIZE: + oled_imagebuffer[ctr] = value + ctr=ctr+1 + +def oled_writebyterow(x,y,bytevalue, mode = 0): + bufferoffset = OLED_WD*(y>>3) + x + if mode == 0: + oled_imagebuffer[bufferoffset] = bytevalue + elif mode == 1: + oled_imagebuffer[bufferoffset] = bytevalue^oled_imagebuffer[bufferoffset] + else: + oled_imagebuffer[bufferoffset] = bytevalue|oled_imagebuffer[bufferoffset] + + +def oled_writebuffer(x,y,value, mode = 0): + + yoffset = y>>3 + yshift = y&0x7 + ybit = (1<>3 + blocksize = 32 + if bus is None: + return + try: + # Set COM-H Addressing + bus.write_byte_data(ADDR_OLED, 0, 0x20) + bus.write_byte_data(ADDR_OLED, 0, 0x1) + + # Set Column range + bus.write_byte_data(ADDR_OLED, 0, 0x21) + bus.write_byte_data(ADDR_OLED, 0, xoffset) + bus.write_byte_data(ADDR_OLED, 0, xoffset+blocksize-1) + + # Set Row Range + bus.write_byte_data(ADDR_OLED, 0, 0x22) + bus.write_byte_data(ADDR_OLED, 0, yoffset) + bus.write_byte_data(ADDR_OLED, 0, yoffset) + + # Set Display Start Line + bus.write_byte_data(ADDR_OLED, 0, 0x40) + + bufferoffset = OLED_WD*yoffset + xoffset + # Write Out Buffer + bus.write_i2c_block_data(ADDR_OLED, OLED_SLAVEADDRESS, oled_imagebuffer[bufferoffset:(bufferoffset+blocksize)]) + except: + return + +def oled_drawfilledrectangle(x, y, wd, ht, mode = 0): + ymax = y + ht + cury = y&0xF8 + + xmax = x + wd + curx = x + if ((y & 0x7)) != 0: + yshift = y&0x7 + bytevalue = (0xFF<>yshift) + + while curx < xmax: + oled_writebyterow(curx,cury,bytevalue, mode) + curx = curx + 1 + cury = cury + 8 + # Draw 8 rows at a time when possible + while cury + 8 < ymax: + curx = x + while curx < xmax: + oled_writebyterow(curx,cury,0xFF, mode) + curx = curx + 1 + cury = cury + 8 + + if cury < ymax: + yshift = 8-((ymax-cury)&0x7) + bytevalue = (0xFF>>yshift) + + curx = x + while curx < xmax: + oled_writebyterow(curx,cury,bytevalue, mode) + curx = curx + 1 + + +def oled_writetextaligned(textdata, x, y, boxwidth, alignmode, charwd = 6, mode = 0): + leftoffset = 0 + if alignmode == 1: + # Centered + leftoffset = (boxwidth-len(textdata)*charwd)>>1 + elif alignmode == 2: + # Right aligned + leftoffset = (boxwidth-len(textdata)*charwd) + + oled_writetext(textdata, x+leftoffset, y, charwd, mode) + + +def oled_writetext(textdata, x, y, charwd = 6, mode = 0): + if charwd < 6: + charwd = 6 + + charht = int((charwd<<3)/6) + if charht & 0x7: + charht = (charht&0xF8) + 8 + + try: + file = open("/etc/argon/oled/font"+str(charht)+"x"+str(charwd)+".bin", "rb") + fontbytes = list(file.read()) + file.close() + except FileNotFoundError: + try: + # Default to smallest + file = open("/etc/argon/oled/font8x6.bin", "rb") + fontbytes = list(file.read()) + file.close() + except FileNotFoundError: + return + + if ((y & 0x7)) == 0: + # Use optimized loading + oled_fastwritetext(textdata, x, y, charht, charwd, fontbytes, mode) + return + + numfontrow = charht>>3 + ctr = 0 + while ctr < len(textdata): + fontoffset = ord(textdata[ctr])*charwd + fontcol = 0 + while fontcol < charwd and x < OLED_WD: + fontrow = 0 + row = y + while fontrow < numfontrow and row < OLED_HT and x >= 0: + curbit = 0x80 + curbyte = (fontbytes[fontoffset + fontcol + (OLED_NUMFONTCHAR*charwd*fontrow)]) + subrow = 0 + while subrow < 8 and row < OLED_HT: + value = 0 + if (curbyte&curbit) != 0: + value = 1 + oled_writebuffer(x,row,value, mode) + curbit = curbit >> 1 + row = row + 1 + subrow = subrow + 1 + fontrow = fontrow + 1 + fontcol = fontcol + 1 + x = x + 1 + ctr = ctr + 1 + +def oled_fastwritetext(textdata, x, y, charht, charwd, fontbytes, mode = 0): + + numfontrow = charht>>3 + ctr = 0 + while ctr < len(textdata): + fontoffset = ord(textdata[ctr])*charwd + fontcol = 0 + while fontcol < charwd and x < OLED_WD: + fontrow = 0 + row = y&0xF8 + while fontrow < numfontrow and row < OLED_HT and x >= 0: + curbyte = (fontbytes[fontoffset + fontcol + (OLED_NUMFONTCHAR*charwd*fontrow)]) + oled_writebyterow(x,row,curbyte, mode) + fontrow = fontrow + 1 + row = row + 8 + fontcol = fontcol + 1 + x = x + 1 + ctr = ctr + 1 + return + + +def oled_power(turnon = True): + cmd = 0xAE + if turnon == True: + cmd = cmd|1 + if bus is None: + return + try: + bus.write_byte_data(ADDR_OLED, 0, cmd) + except: + return + + +def oled_inverse(enable = True): + cmd = 0xA6 + if enable == True: + cmd = cmd|1 + if bus is None: + return + try: + bus.write_byte_data(ADDR_OLED, 0, cmd) + except: + return + + +def oled_fullwhite(enable = True): + cmd = 0xA4 + if enable == True: + cmd = cmd|1 + if bus is None: + return + + try: + bus.write_byte_data(ADDR_OLED, 0, cmd) + except: + return + + + +def oled_reset(): + if bus is None: + return + try: + # Set COM-H Addressing + bus.write_byte_data(ADDR_OLED, 0, 0x20) + bus.write_byte_data(ADDR_OLED, 0, 0x1) + + # Set Column range + bus.write_byte_data(ADDR_OLED, 0, 0x21) + bus.write_byte_data(ADDR_OLED, 0, 0) + bus.write_byte_data(ADDR_OLED, 0, OLED_WD-1) + + # Set Row Range + bus.write_byte_data(ADDR_OLED, 0, 0x22) + bus.write_byte_data(ADDR_OLED, 0, 0) + bus.write_byte_data(ADDR_OLED, 0, (OLED_HT>>3)-1) + + # Set Page Addressing + bus.write_byte_data(ADDR_OLED, 0, 0x20) + bus.write_byte_data(ADDR_OLED, 0, 0x2) + # Set GDDRAM Address + bus.write_byte_data(ADDR_OLED, 0, 0xB0) + + # Set Display Start Line + bus.write_byte_data(ADDR_OLED, 0, 0x40) + except: + return + + diff --git a/source/scripts/argonone-fanconfig.sh b/source/scripts/argonone-fanconfig.sh new file mode 100644 index 0000000..6e1b585 --- /dev/null +++ b/source/scripts/argonone-fanconfig.sh @@ -0,0 +1,254 @@ +#!/bin/bash + +daemonconfigfile=/etc/argononed.conf +unitconfigfile=/etc/argonunits.conf +fanmode="CPU" + +if [ "$1" == "hdd" ] +then + daemonconfigfile=/etc/argononed-hdd.conf + fanmode="HDD" +fi + +if [ -f "$unitconfigfile" ] +then + . $unitconfigfile +fi + +if [ -z "$temperature" ] +then + temperature="C" +fi + +echo "------------------------------------------" +echo " Argon Fan Speed Configuration Tool ($fanmode)" +echo "------------------------------------------" +echo "WARNING: This will remove existing configuration." +echo -n "Press Y to continue:" +read -n 1 confirm +echo + + +fanloopflag=1 +newmode=0 +if [ "$confirm" = "y" ] +then + confirm="Y" +fi + +if [ "$confirm" != "Y" ] +then + fanloopflag=0 + echo "Cancelled." +else + echo "Thank you." +fi + +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 212 ] + then + # 212F = 100C + echo "-1" + return + fi + echo $curnumber + return + fi + echo "-1" + return +} + +while [ $fanloopflag -eq 1 ] +do + echo + echo "Select fan mode:" + echo " 1. Always on" + if [ "$fanmode" == "HDD" ] + then + if [ "$temperature" == "C" ] + then + echo " 2. Adjust to temperatures (35C, 40C, and 45C)" + else + echo " 2. Adjust to temperatures (95F, 104F, and 113F)" + fi + else + if [ "$temperature" == "C" ] + then + echo " 2. Adjust to temperatures (55C, 60C, and 65C)" + else + echo " 2. Adjust to temperatures (130F, 140F, and 150F)" + fi + fi + echo " 3. Customize temperature cut-offs" + echo + echo " 0. Exit" + echo "NOTE: You can also edit $daemonconfigfile directly" + echo -n "Enter Number (0-3):" + newmode=$( get_number ) + + if [[ $newmode -eq 0 ]] + then + fanloopflag=0 + elif [ $newmode -eq 1 ] + then + echo "#" > $daemonconfigfile + echo "# Argon Fan Speed Configuration $fanmode" >> $daemonconfigfile + echo "#" >> $daemonconfigfile + echo "# Min Temp=Fan Speed" >> $daemonconfigfile + + errorfanflag=1 + while [ $errorfanflag -eq 1 ] + do + echo -n "Please provide fan speed (30-100 only):" + + curfan=$( get_number ) + if [ $curfan -ge 30 ] + then + errorfanflag=0 + elif [ $curfan -gt 100 ] + then + errorfanflag=0 + fi + done + + echo "1="$curfan >> $daemonconfigfile + sudo systemctl restart argononed.service + echo "Fan always on." + elif [ $newmode -eq 2 ] + then + echo "#" > $daemonconfigfile + echo "# Argon Fan Speed Configuration $fanmode" >> $daemonconfigfile + echo "#" >> $daemonconfigfile + echo "# Min Temp=Fan Speed" >> $daemonconfigfile + + echo "Please provide fan speeds for the following temperatures:" + curtemp=55 + maxtemp=70 + if [ "$fanmode" == "HDD" ] + then + curtemp=30 + maxtemp=60 + fi + while [ $curtemp -lt $maxtemp ] + do + errorfanflag=1 + while [ $errorfanflag -eq 1 ] + do + displaytemp=$curtemp + if [ "$temperature" == "F" ] + then + # Convert C to F + displaytemp=$((($curtemp*9/5)+32)) + fi + echo -n ""$displaytemp"$temperature (30-100 only):" + + curfan=$( get_number ) + if [ $curfan -ge 30 ] + then + errorfanflag=0 + elif [ $curfan -gt 100 ] + then + errorfanflag=0 + fi + done + echo $curtemp"="$curfan >> $daemonconfigfile + curtemp=$((curtemp+5)) + done + + sudo systemctl restart argononed.service + echo "Configuration updated." + elif [ $newmode -eq 3 ] + then + echo "Please provide fan speeds and temperature pairs" + echo + + subloopflag=1 + paircounter=0 + while [ $subloopflag -eq 1 ] + do + errortempflag=1 + errorfanflag=1 + echo "(You may set a blank value to end configuration)" + while [ $errortempflag -eq 1 ] + do + echo -n "Provide minimum temperature of $fanmode (in $temperature) then [ENTER]:" + + curtemp=$( get_number ) + if [ $curtemp -ge 0 ] + then + errortempflag=0 + elif [ $curtemp -eq -2 ] + then + # Blank + errortempflag=0 + errorfanflag=0 + subloopflag=0 + fi + done + while [ $errorfanflag -eq 1 ] + do + echo -n "Provide fan speed for "$curtemp"$temperature (30-100) then [ENTER]:" + curfan=$( get_number ) + if [ $curfan -ge 30 ] + then + errorfanflag=0 + elif [ $curfan -gt 100 ] + then + errorfanflag=0 + elif [ $curfan -eq -2 ] + then + # Blank + errortempflag=0 + errorfanflag=0 + subloopflag=0 + fi + done + if [ $subloopflag -eq 1 ] + then + if [ $paircounter -eq 0 ] + then + echo "#" > $daemonconfigfile + echo "# Argon Fan Configuration" >> $daemonconfigfile + echo "#" >> $daemonconfigfile + echo "# Min Temp=Fan Speed" >> $daemonconfigfile + fi + + displaytemp=$curtemp + paircounter=$((paircounter+1)) + if [ "$temperature" == "F" ] + then + # Convert to F to C + curtemp=$((($curtemp-32)*5/9)) + fi + echo $curtemp"="$curfan >> $daemonconfigfile + + echo "* Fan speed will be set to "$curfan" once $fanmode temperature reaches "$displaytemp"$temperature" + echo + fi + done + + echo + if [ $paircounter -gt 0 ] + then + echo "Thank you! We saved "$paircounter" pairs." + sudo systemctl restart argononed.service + echo "Changes should take effect now." + else + echo "Cancelled, no data saved." + fi + fi +done + +echo + diff --git a/source/argonone-irconfig.sh b/source/scripts/argonone-irconfig.sh similarity index 93% rename from source/argonone-irconfig.sh rename to source/scripts/argonone-irconfig.sh index 9be2028..3d0cb2b 100644 --- a/source/argonone-irconfig.sh +++ b/source/scripts/argonone-irconfig.sh @@ -1,5 +1,6 @@ #!/bin/bash + if [ -e /boot/firmware/config.txt ] ; then FIRMWARE=/firmware else @@ -24,9 +25,10 @@ then fi fi -echo "--------------------------------" -echo "Argon One IR Configuration Tool" -echo "--------------------------------" + +echo "-----------------------------" +echo " Argon IR Configuration Tool" +echo "------------------------------" echo "WARNING: This only supports NEC" echo " protocol only." echo -n "Press Y to continue:" @@ -70,8 +72,8 @@ get_number () { } irexecrcfile=/etc/lirc/irexec.lircrc -irexecshfile=/usr/bin/argonirexec -irdecodefile=/usr/bin/argonirdecoder +irexecshfile=/etc/argon/argonirexec +irdecodefile=/etc/argon/argonirdecoder kodiuserdatafolder="$HOME/.kodi/userdata" kodilircmapfile="$kodiuserdatafolder/Lircmap.xml" remotemode="" @@ -176,9 +178,9 @@ then fi elif [ $newmode -eq 2 ] then - echo "--------------------------------" - echo "Argon One IR Configuration Tool" - echo "--------------------------------" + echo "-----------------------------" + echo " Argon IR Configuration Tool" + echo "-----------------------------" echo "WARNING: This will install LIRC" echo " and related libraries." echo -n "Press Y to agree:" @@ -320,7 +322,10 @@ then echo ' KEY_DOWN' | tee -a $kodilircmapfile 1> /dev/null echo ' ' | tee -a $kodilircmapfile 1> /dev/null echo ' KEY_HOME' | tee -a $kodilircmapfile 1> /dev/null - echo ' KEY_MENUBACK' | tee -a $kodilircmapfile 1> /dev/null + # 20240611: User reported mapping is incorrect + #echo ' KEY_MENUBACK' | tee -a $kodilircmapfile 1> /dev/null + echo ' KEY_MENU' | tee -a $kodilircmapfile 1> /dev/null + echo ' KEY_BACK' | tee -a $kodilircmapfile 1> /dev/null echo ' KEY_VOLUMEUP' | tee -a $kodilircmapfile 1> /dev/null echo ' KEY_VOLUMEDOWN' | tee -a $kodilircmapfile 1> /dev/null echo ' ' | tee -a $kodilircmapfile 1> /dev/null diff --git a/source/scripts/argonone-upsconfig.sh b/source/scripts/argonone-upsconfig.sh new file mode 100644 index 0000000..351ba8f --- /dev/null +++ b/source/scripts/argonone-upsconfig.sh @@ -0,0 +1,305 @@ +#!/bin/bash + + +if [ -e /boot/firmware/config.txt ] ; then + FIRMWARE=/firmware +else + FIRMWARE= +fi +CONFIG=/boot${FIRMWARE}/config.txt + +CHECKGPIOMODE="libgpiod" # gpiod 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 + +pythonbin=/usr/bin/python3 + +# Files +ARGONDOWNLOADSERVER=https://download.argon40.com +INSTALLATIONFOLDER=/etc/argon +basename="argononeups" +daemonname=$basename"d" + +daemonupsservice=/lib/systemd/system/$daemonname.service +upsdaemonscript=$INSTALLATIONFOLDER/$daemonname.py + +rtcdaemonname="argonupsrtcd" + +daemonrtcservice=/lib/systemd/system/$rtcdaemonname.service +rtcdaemonscript=$INSTALLATIONFOLDER/$rtcdaemonname.py + + +requireinstall=0 +newmode=0 +echo "-----------------------------------" +echo " Argon Industria UPS Configuration" +echo "-----------------------------------" +if [ ! -f "$upsdaemonscript" ] +then + echo "Install Argon Industria UPS Tools" + 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 + + requireinstall=1 + newmode=3 # Reinstall + +fi + + +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 +} + +UPSCMDFILE="/dev/shm/upscmd.txt" +UPSSTATUSFILE="/dev/shm/upslog.txt" +rtcconfigscript=$INSTALLATIONFOLDER/argonups-rtcconfig.sh + +if [ -f "$UPSSTATUSFILE" ] +then +# cat $UPSSTATUSFILE + sudo $pythonbin $rtcdaemonscript GETBATTERY +fi + + +loopflag=1 +while [ $loopflag -eq 1 ] +do + if [ $requireinstall -eq 0 ] + then + echo + echo "Select option:" + echo " 1. UPS Battery Status" + echo " 2. Configure RTC and/or Schedule" + echo " 3. Reinstall UPS Tools" + echo " 4. Uninstall UPS Tools" + echo "" + echo " 0. Back" + + echo -n "Enter Number (0-4):" + + newmode=$( get_number ) + fi + if [[ $newmode -ge 0 && $newmode -le 4 ]] + then + if [ $newmode -eq 1 ] + then + sudo $pythonbin $rtcdaemonscript GETBATTERY + #if [ -f "$UPSSTATUSFILE" ] + #then + # cat $UPSSTATUSFILE + #else + # echo "Unable to retrieve status" + #fi + elif [ $newmode -eq 2 ] + then + $rtcconfigscript "argonupsrtc" + #TMPTIMESTR=`date +"%Y%d%m%H%M%S"` + #TMPDATASTR=`date +"%Y %m %d %H %M %S"` + + #echo "$TMPTIMESTR" > $UPSCMDFILE + #echo "3 $TMPDATASTR" >> $UPSCMDFILE + elif [ $newmode -eq 3 ] + then + # Start installation + if [ ! -d "$INSTALLATIONFOLDER/ups" ] + then + sudo mkdir $INSTALLATIONFOLDER/ups + fi + + + rtcconfigfile=/etc/argonupsrtc.conf + # 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 + + for iconfile in battery_0 battery_2 battery_4 battery_charging battery_unknown battery_1 battery_3 battery_alert battery_plug + do + sudo wget $ARGONDOWNLOADSERVER/ups/${iconfile}.png -O $INSTALLATIONFOLDER/ups/${iconfile}.png --quiet + done + + 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 + + # Desktop Icon + destfoldername=$USERNAME + if [ -z "$destfoldername" ] + then + destfoldername=$USER + fi + if [ -z "$destfoldername" ] + then + destfoldername="pi" + fi + + shortcutfile="/home/$destfoldername/Desktop/argonone-ups.desktop" + if [ -d "/home/$destfoldername/Desktop" ] + then + terminalcmd="lxterminal --working-directory=/home/$destfoldername/ -t" + if [ -f "/home/$destfoldername/.twisteros.twid" ] + then + terminalcmd="xfce4-terminal --default-working-directory=/home/$destfoldername/ -T" + fi + + echo "[Desktop Entry]" > $shortcutfile + echo "Name=Argon UPS" >> $shortcutfile + echo "Comment=Argon UPS" >> $shortcutfile + echo "Icon=/etc/argon/ups/loading_0.png" >> $shortcutfile + echo 'Exec='$terminalcmd' "Argon UPS" -e "'$rtcconfigscript' argonupsrtc"' >> $shortcutfile + echo "Type=Application" >> $shortcutfile + echo "Encoding=UTF-8" >> $shortcutfile + echo "Terminal=false" >> $shortcutfile + echo "Categories=None;" >> $shortcutfile + chmod 755 $shortcutfile + fi + + # Stopped using default battery indicator + ## Build Kernel Module + #sourcecodefolder=$INSTALLATIONFOLDER/tmp + #buildfolder=$sourcecodefolder/build + #if [ -d $sourcecodefolder ] + #then + # sudo rm -rf $sourcecodefolder + #fi + #if [ "$CHECKPLATFORM" = "Ubuntu" ] + #then + # sudo apt-get install build-essential + #fi + #sudo mkdir -p $buildfolder + #sudo chmod -R 755 $buildfolder + + #FILELIST="COPYING Makefile argonbatteryicon.c" + #for fname in $FILELIST + #do + # sudo wget $ARGONDOWNLOADSERVER/modules/argonbatteryicon/$fname -O $buildfolder/#$fname --quiet + #done + + ## Start Build + #cd $buildfolder/ + #sudo make + #sudo cp "$buildfolder/argonbatteryicon.ko" "$INSTALLATIONFOLDER/ups/" + + ## Cleanup + #cd $INSTALLATIONFOLDER/ + #sudo rm -Rf "$sourcecodefolder" + + sudo wget $ARGONDOWNLOADSERVER/scripts/argononeupsd.py -O "$upsdaemonscript" --quiet + sudo wget $ARGONDOWNLOADSERVER/scripts/argononeupsd.service -O "$daemonupsservice" --quiet + sudo chmod 666 $daemonupsservice + #echo "User=$destfoldername" >> "$daemonupsservice" + #echo "Group=$destfoldername" >> "$daemonupsservice" + + sudo chmod 644 $daemonupsservice + + sudo wget $ARGONDOWNLOADSERVER/scripts/argoneon-rtcconfig.sh -O $rtcconfigscript --quiet + sudo chmod 755 $rtcconfigscript + + sudo wget $ARGONDOWNLOADSERVER/scripts/argonrtc.py -O $INSTALLATIONFOLDER/argonrtc.py --quiet + sudo wget $ARGONDOWNLOADSERVER/scripts/argonupsrtcd.py -O "$rtcdaemonscript" --quiet + sudo wget $ARGONDOWNLOADSERVER/scripts/argonupsrtcd.service -O "$daemonrtcservice" --quiet + sudo chmod 644 $daemonrtcservice + + if [ $requireinstall -eq 1 ] + then + requireinstall=0 + sudo systemctl enable "$daemonname.service" + sudo systemctl start "$daemonname.service" + + sudo systemctl enable "$rtcdaemonname.service" + sudo systemctl start "$rtcdaemonname.service" + else + sudo systemctl restart "$daemonname.service" + sudo systemctl restart "$rtcdaemonname.service" + loopflag=0 + fi + # Serial I/O is here + sudo systemctl restart argononed.service + elif [ $newmode -eq 4 ] + then + sudo systemctl stop "$daemonname.service" + sudo systemctl disable "$daemonname.service" + sudo rm $daemonupsservice + sudo rm $upsdaemonscript + + sudo systemctl stop "$rtcdaemonname.service" + sudo systemctl disable "$rtcdaemonname.service" + sudo rm $daemonrtcservice + sudo rm $rtcdaemonscript + + sudo rm -R -f $INSTALLATIONFOLDER/ups + + echo "Uninstall Completed" + loopflag=0 + else + echo "Cancelled" + loopflag=0 + fi + fi +done + + diff --git a/source/scripts/argononed.py b/source/scripts/argononed.py new file mode 100644 index 0000000..d38ee27 --- /dev/null +++ b/source/scripts/argononed.py @@ -0,0 +1,600 @@ +#!/usr/bin/python3 + +# +# This script set fan speed and monitor power button events. +# +# Fan Speed is set by sending 0 to 100 to the MCU (Micro Controller Unit) +# The values will be interpreted as the percentage of fan speed, 100% being maximum +# +# Power button events are sent as a pulse signal to BCM Pin 4 (BOARD P7) +# A pulse width of 20-30ms indicates reboot request (double-tap) +# A pulse width of 40-50ms indicates shutdown request (hold and release after 3 secs) +# +# Additional comments are found in each function below +# +# Standard Deployment/Triggers: +# * Raspbian, OSMC: Runs as service via /lib/systemd/system/argononed.service +# * lakka, libreelec: Runs as service via /storage/.config/system.d/argononed.service +# * recalbox: Runs as service via /etc/init.d/ +# + +import sys +import os +import time +from threading import Thread +from queue import Queue + +sys.path.append("/etc/argon/") +from argonsysinfo import * +from argonregister import * +from argonpowerbutton import * + +# Initialize I2C Bus +bus = argonregister_initializebusobj() + +OLED_ENABLED=False + +if os.path.exists("/etc/argon/argoneonoled.py"): + import datetime + from argoneonoled import * + OLED_ENABLED=True + +OLED_CONFIGFILE = "/etc/argoneonoled.conf" +UNIT_CONFIGFILE = "/etc/argonunits.conf" + +# This function converts the corresponding fanspeed for the given temperature +# The configuration data is a list of strings in the form "=" + +def get_fanspeed(tempval, configlist): + for curconfig in configlist: + curpair = curconfig.split("=") + tempcfg = float(curpair[0]) + fancfg = int(float(curpair[1])) + if tempval >= tempcfg: + if fancfg < 1: + return 0 + elif fancfg < 25: + return 25 + return fancfg + return 0 + +# This function retrieves the fanspeed configuration list from a file, arranged by temperature +# It ignores lines beginning with "#" and checks if the line is a valid temperature-speed pair +# The temperature values are formatted to uniform length, so the lines can be sorted properly + +def load_config(fname): + newconfig = [] + try: + with open(fname, "r") as fp: + for curline in fp: + if not curline: + continue + tmpline = curline.strip() + if not tmpline: + continue + if tmpline[0] == "#": + continue + tmppair = tmpline.split("=") + if len(tmppair) != 2: + continue + tempval = 0 + fanval = 0 + try: + tempval = float(tmppair[0]) + if tempval < 0 or tempval > 100: + continue + except: + continue + try: + fanval = int(float(tmppair[1])) + if fanval < 0 or fanval > 100: + continue + except: + continue + newconfig.append( "{:5.1f}={}".format(tempval,fanval)) + if len(newconfig) > 0: + newconfig.sort(reverse=True) + except: + return [] + return newconfig + +# Load OLED Config file +def load_oledconfig(fname): + output={} + screenduration=-1 + screenlist=[] + try: + with open(fname, "r") as fp: + for curline in fp: + if not curline: + continue + tmpline = curline.strip() + if not tmpline: + continue + if tmpline[0] == "#": + continue + tmppair = tmpline.split("=") + if len(tmppair) != 2: + continue + if tmppair[0] == "switchduration": + output['screenduration']=int(tmppair[1]) + elif tmppair[0] == "screensaver": + output['screensaver']=int(tmppair[1]) + elif tmppair[0] == "screenlist": + output['screenlist']=tmppair[1].replace("\"", "").split(" ") + elif tmppair[0] == "enabled": + output['enabled']=tmppair[1].replace("\"", "") + except: + return {} + return output + +# Load Unit Config file +def load_unitconfig(fname): + output={"temperature": "C"} + try: + with open(fname, "r") as fp: + for curline in fp: + if not curline: + continue + tmpline = curline.strip() + if not tmpline: + continue + if tmpline[0] == "#": + continue + tmppair = tmpline.split("=") + if len(tmppair) != 2: + continue + if tmppair[0] == "temperature": + output['temperature']=tmppair[1].replace("\"", "") + except: + return {} + return output + +def load_fancpuconfig(): + fanconfig = ["65=100", "60=55", "55=30"] + tmpconfig = load_config("/etc/argononed.conf") + if len(tmpconfig) > 0: + fanconfig = tmpconfig + return fanconfig + + +def load_fanhddconfig(): + fanhddconfig = ["50=100", "40=55", "30=30"] + fanhddconfigfile = "/etc/argononed-hdd.conf" + + if os.path.isfile(fanhddconfigfile): + tmpconfig = load_config(fanhddconfigfile) + if len(tmpconfig) > 0: + fanhddconfig = tmpconfig + else: + fanhddconfig = [] + return fanhddconfig + +# This function is the thread that monitors temperature and sets the fan speed +# The value is fed to get_fanspeed to get the new fan speed +# To prevent unnecessary fluctuations, lowering fan speed is delayed by 30 seconds +# +# Location of config file varies based on OS +# +def temp_check(): + INITIALSPEEDVAL = 200 # ensures fan speed gets set during initialization (e.g. change settings) + argonregsupport = argonregister_checksupport(bus) + + fanconfig = load_fancpuconfig() + fanhddconfig = load_fanhddconfig() + + prevspeed=INITIALSPEEDVAL + while True: + # Speed based on CPU Temp + val = argonsysinfo_getcputemp() + newspeed = get_fanspeed(val, fanconfig) + # Speed based on HDD Temp + val = argonsysinfo_getmaxhddtemp() + tmpspeed = get_fanspeed(val, fanhddconfig) + + # Use faster fan speed + if tmpspeed > newspeed: + newspeed = tmpspeed + + if prevspeed == newspeed: + time.sleep(30) + continue + elif newspeed < prevspeed and prevspeed != INITIALSPEEDVAL: + # Pause 30s before speed reduction to prevent fluctuations + time.sleep(30) + prevspeed = newspeed + try: + if newspeed > 0: + # Spin up to prevent issues on older units + argonregister_setfanspeed(bus, 100, argonregsupport) + # Set fan speed has sleep + argonregister_setfanspeed(bus, newspeed, argonregsupport) + time.sleep(30) + except IOError: + time.sleep(60) + +# +# This function is the thread that updates OLED +# +def display_loop(readq): + weekdaynamelist = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] + monthlist = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] + oledscreenwidth = oled_getmaxX() + + fontwdSml = 6 # Maps to 6x8 + fontwdReg = 8 # Maps to 8x16 + stdleftoffset = 54 + + temperature="C" + tmpconfig=load_unitconfig(UNIT_CONFIGFILE) + if "temperature" in tmpconfig: + temperature = tmpconfig["temperature"] + + screensavermode = False + screensaversec = 120 + screensaverctr = 0 + + screenenabled = ["clock", "ip"] + prevscreen = "" + curscreen = "" + screenid = 0 + screenjogtime = 0 + screenjogflag = 0 # start with screenid 0 + cpuusagelist = [] + curlist = [] + + tmpconfig=load_oledconfig(OLED_CONFIGFILE) + + if "screensaver" in tmpconfig: + screensaversec = tmpconfig["screensaver"] + if "screenduration" in tmpconfig: + screenjogtime = tmpconfig["screenduration"] + if "screenlist" in tmpconfig: + screenenabled = tmpconfig["screenlist"] + + if "enabled" in tmpconfig: + if tmpconfig["enabled"] == "N": + screenenabled = [] + + while len(screenenabled) > 0: + if len(curlist) == 0 and screenjogflag == 1: + # Reset Screen Saver + screensavermode = False + screensaverctr = 0 + + # Update screen info + screenid = screenid + screenjogflag + if screenid >= len(screenenabled): + screenid = 0 + prevscreen = curscreen + curscreen = screenenabled[screenid] + + if screenjogtime == 0: + # Resets jogflag (if switched manually) + screenjogflag = 0 + else: + screenjogflag = 1 + + needsUpdate = False + if curscreen == "cpu": + # CPU Usage + if len(curlist) == 0: + try: + if len(cpuusagelist) == 0: + cpuusagelist = argonsysinfo_listcpuusage() + curlist = cpuusagelist + except: + curlist = [] + if len(curlist) > 0: + oled_loadbg("bgcpu") + + # Display List + yoffset = 0 + tmpmax = 4 + while tmpmax > 0 and len(curlist) > 0: + curline = "" + tmpitem = curlist.pop(0) + curline = tmpitem["title"]+": "+str(tmpitem["value"])+"%" + oled_writetext(curline, stdleftoffset, yoffset, fontwdSml) + oled_drawfilledrectangle(stdleftoffset, yoffset+12, int((oledscreenwidth-stdleftoffset-4)*tmpitem["value"]/100), 2) + tmpmax = tmpmax - 1 + yoffset = yoffset + 16 + + needsUpdate = True + else: + # Next page due to error/no data + screenjogflag = 1 + elif curscreen == "storage": + # Storage Info + if len(curlist) == 0: + try: + tmpobj = argonsysinfo_listhddusage() + for curdev in tmpobj: + curlist.append({"title": curdev, "value": argonsysinfo_kbstr(tmpobj[curdev]['total']), "usage": int(100*tmpobj[curdev]['used']/tmpobj[curdev]['total']) }) + #curlist = argonsysinfo_liststoragetotal() + except: + curlist = [] + if len(curlist) > 0: + oled_loadbg("bgstorage") + + yoffset = 16 + tmpmax = 3 + while tmpmax > 0 and len(curlist) > 0: + tmpitem = curlist.pop(0) + # Right column first, safer to overwrite white space + oled_writetextaligned(tmpitem["value"], 77, yoffset, oledscreenwidth-77, 2, fontwdSml) + oled_writetextaligned(str(tmpitem["usage"])+"%", 50, yoffset, 74-50, 2, fontwdSml) + tmpname = tmpitem["title"] + if len(tmpname) > 8: + tmpname = tmpname[0:8] + oled_writetext(tmpname, 0, yoffset, fontwdSml) + + tmpmax = tmpmax - 1 + yoffset = yoffset + 16 + needsUpdate = True + else: + # Next page due to error/no data + screenjogflag = 1 + + elif curscreen == "raid": + # Raid Info + if len(curlist) == 0: + try: + tmpobj = argonsysinfo_listraid() + curlist = tmpobj['raidlist'] + except: + curlist = [] + if len(curlist) > 0: + oled_loadbg("bgraid") + tmpitem = curlist.pop(0) + oled_writetextaligned(tmpitem["title"], 0, 0, stdleftoffset, 1, fontwdSml) + oled_writetextaligned(tmpitem["value"], 0, 8, stdleftoffset, 1, fontwdSml) + oled_writetextaligned(argonsysinfo_kbstr(tmpitem["info"]["size"]), 0, 56, stdleftoffset, 1, fontwdSml) + + if len(tmpitem['info']['state']) > 0: + oled_writetext( tmpitem['info']['state'], stdleftoffset, 8, fontwdSml ) + + if len(tmpitem['info']['rebuildstat']) > 0: + oled_writetext("Rebuild:" + tmpitem['info']['rebuildstat'], stdleftoffset, 16, fontwdSml) + + # TODO: May need to use different method for each raid type (i.e. check raidlist['raidlist'][raidctr]['value']) + #oled_writetext("Used:"+str(int(100*tmpitem["info"]["used"]/tmpitem["info"]["size"]))+"%", stdleftoffset, 24, fontwdSml) + + + oled_writetext("Active:"+str(int(tmpitem["info"]["active"]))+"/"+str(int(tmpitem["info"]["devices"])), stdleftoffset, 32, fontwdSml) + oled_writetext("Working:"+str(int(tmpitem["info"]["working"]))+"/"+str(int(tmpitem["info"]["devices"])), stdleftoffset, 40, fontwdSml) + oled_writetext("Failed:"+str(int(tmpitem["info"]["failed"]))+"/"+str(int(tmpitem["info"]["devices"])), stdleftoffset, 48, fontwdSml) + needsUpdate = True + else: + # Next page due to error/no data + screenjogflag = 1 + + elif curscreen == "ram": + # RAM + try: + oled_loadbg("bgram") + tmpraminfo = argonsysinfo_getram() + oled_writetextaligned(tmpraminfo[0], stdleftoffset, 8, oledscreenwidth-stdleftoffset, 1, fontwdReg) + oled_writetextaligned("of", stdleftoffset, 24, oledscreenwidth-stdleftoffset, 1, fontwdReg) + oled_writetextaligned(tmpraminfo[1], stdleftoffset, 40, oledscreenwidth-stdleftoffset, 1, fontwdReg) + needsUpdate = True + except: + needsUpdate = False + # Next page due to error/no data + screenjogflag = 1 + elif curscreen == "temp": + # Temp + try: + oled_loadbg("bgtemp") + 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: + oled_writetext(curdev.upper()+": "+ tmpstr+ chr(167) +temperature, stdleftoffset, displayrow, fontwdSml) + + else: + oled_writetext(curdev.upper()+":", stdleftoffset, displayrow, fontwdSml) + + oled_writetext(" "+ tmpstr+ chr(167) +temperature, stdleftoffset, displayrow+displayrowht, fontwdSml) + displayrow = displayrow + displayrowht*2 + 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] + + oled_writetextaligned(tmpstr+ chr(167) +temperature, stdleftoffset, 24, oledscreenwidth-stdleftoffset, 1, fontwdReg) + + # Temperature Bar: 40C is min, 80C is max + maxht = 21 + barht = int(maxht*(maxcval-40)/40) + if barht > maxht: + barht = maxht + elif barht < 1: + barht = 1 + oled_drawfilledrectangle(24, 20+(maxht-barht), 3, barht, 2) + + + needsUpdate = True + except: + needsUpdate = False + # Next page due to error/no data + screenjogflag = 1 + elif curscreen == "ip": + # IP Address + try: + oled_loadbg("bgip") + oled_writetextaligned(argonsysinfo_getip(), 0, 8, oledscreenwidth, 1, fontwdReg) + needsUpdate = True + except: + needsUpdate = False + # Next page due to error/no data + screenjogflag = 1 + elif curscreen == "logo1v5": + # Logo + try: + oled_loadbg("logo1v5") + needsUpdate = True + except: + needsUpdate = False + # Next page due to error/no data + screenjogflag = 1 + else: + try: + oled_loadbg("bgtime") + # Date and Time HH:MM + curtime = datetime.datetime.now() + + # Month/Day + outstr = str(curtime.day).strip() + if len(outstr) < 2: + outstr = " "+outstr + outstr = monthlist[curtime.month-1]+outstr + oled_writetextaligned(outstr, stdleftoffset, 8, oledscreenwidth-stdleftoffset, 1, fontwdReg) + + # Day of Week + oled_writetextaligned(weekdaynamelist[curtime.weekday()], stdleftoffset, 24, oledscreenwidth-stdleftoffset, 1, fontwdReg) + + # Time + outstr = str(curtime.minute).strip() + if len(outstr) < 2: + outstr = "0"+outstr + outstr = str(curtime.hour)+":"+outstr + if len(outstr) < 5: + outstr = "0"+outstr + oled_writetextaligned(outstr, stdleftoffset, 40, oledscreenwidth-stdleftoffset, 1, fontwdReg) + + needsUpdate = True + except: + needsUpdate = False + # Next page due to error/no data + screenjogflag = 1 + + if needsUpdate == True: + if screensavermode == False: + # Update screen if not screen saver mode + oled_power(True) + oled_flushimage(prevscreen != curscreen) + oled_reset() + + timeoutcounter = 0 + while timeoutcounter= 60 and screensavermode == False: + # Refresh data every minute, unless screensaver got triggered + screenjogflag = 0 + break + display_defaultimg() + +def display_defaultimg(): + # Load default image + #oled_power(True) + #oled_loadbg("bgdefault") + #oled_flushimage() + oled_fill(0) + oled_reset() + +if len(sys.argv) > 1: + cmd = sys.argv[1].upper() + if cmd == "SHUTDOWN": + # Signal poweroff + argonregister_signalpoweroff(bus) + + elif cmd == "FANOFF": + # Turn off fan + argonregister_setfanspeed(bus,0) + + if OLED_ENABLED == True: + display_defaultimg() + + elif cmd == "SERVICE": + # Starts the power button and temperature monitor threads + try: + ipcq = Queue() + if len(sys.argv) > 2: + cmd = sys.argv[2].upper() + if cmd == "OLEDSWITCH": + t1 = Thread(target = argonpowerbutton_monitorswitch, args =(ipcq, )) + else: + t1 = Thread(target = argonpowerbutton_monitor, args =(ipcq, )) + + t2 = Thread(target = temp_check) + if OLED_ENABLED == True: + t3 = Thread(target = display_loop, args =(ipcq, )) + + t1.start() + t2.start() + if OLED_ENABLED == True: + t3.start() + + ipcq.join() + except Exception: + sys.exit(1) diff --git a/source/scripts/argononed.service b/source/scripts/argononed.service new file mode 100644 index 0000000..463a5ef --- /dev/null +++ b/source/scripts/argononed.service @@ -0,0 +1,10 @@ +[Unit] +Description=Argon One Fan and Button Service +After=multi-user.target +[Service] +Type=simple +Restart=always +RemainAfterExit=true +ExecStart=/usr/bin/python3 /etc/argon/argononed.py SERVICE +[Install] +WantedBy=multi-user.target diff --git a/source/scripts/argononeoled.py b/source/scripts/argononeoled.py new file mode 100644 index 0000000..cd98e46 --- /dev/null +++ b/source/scripts/argononeoled.py @@ -0,0 +1,333 @@ +#!/usr/bin/python3 + +from luma.core.interface.serial import i2c +from luma.oled.device import ssd1306 +from PIL import Image + +import sys +import datetime +import math + +import os +import time + +# Initialize I2C Bus +import smbus + +oledport=1 + +try: + bus=smbus.SMBus(1) +except Exception: + try: + oledport=0 + # Older version + bus=smbus.SMBus(0) + except Exception: + print("Unable to detect i2c") + bus=None + + +ADDR_OLED=0x3c +OLED_WD=1 +OLED_HT=1 +oled_device=None +try: + oled_device=ssd1306(i2c(port=oledport, address=ADDR_OLED)) + + OLED_WD=oled_device.bounding_box[2]+1 + OLED_HT=oled_device.bounding_box[3]+1 +except Exception: + print("Unable to initialize OLED") + bus=None + +OLED_NUMFONTCHAR=256 + +OLED_BUFFERIZE = ((OLED_WD*OLED_HT)>>3) +oled_imagebuffer = [0] * OLED_BUFFERIZE + + +def oled_getmaxY(): + return OLED_HT + +def oled_getmaxX(): + return OLED_WD + +def oled_loadbg(bgname): + if bgname == "bgblack": + oled_clearbuffer() + return + elif bgname == "bgwhite": + oled_clearbuffer(1) + return + try: + file = open("/etc/argon/oled/"+bgname+".bin", "rb") + bgbytes = list(file.read()) + file.close() + ctr = len(bgbytes) + if ctr == OLED_BUFFERIZE: + oled_imagebuffer[:] = bgbytes + elif ctr > OLED_BUFFERIZE: + oled_imagebuffer[:] = bgbytes[0:OLED_BUFFERIZE] + else: + oled_imagebuffer[0:ctr] = bgbytes + # Clear the rest of the buffer + while ctr < OLED_BUFFERIZE: + oled_imagebuffer[ctr] = 0 + ctr=ctr+1 + except FileNotFoundError: + oled_clearbuffer() + + +def oled_clearbuffer(value = 0): + if value != 0: + value = 0xff + ctr = 0 + while ctr < OLED_BUFFERIZE: + oled_imagebuffer[ctr] = value + ctr=ctr+1 + +def oled_writebyterow(x,y,bytevalue, mode = 0): + bufferoffset = OLED_WD*(y>>3) + x + if mode == 0: + oled_imagebuffer[bufferoffset] = bytevalue + elif mode == 1: + oled_imagebuffer[bufferoffset] = bytevalue^oled_imagebuffer[bufferoffset] + else: + oled_imagebuffer[bufferoffset] = bytevalue|oled_imagebuffer[bufferoffset] + + +def oled_writebuffer(x,y,value, mode = 0): + + yoffset = y>>3 + yshift = y&0x7 + ybit = (1<> 1 + xidx = xidx + 1 + if xidx >= 8: + tmplist[srcidx+outoffsetidx + outyoffset] = outbyte + xmask = 0x80 + xidx = 0 + outbyte = 0 + outoffsetidx = outoffsetidx + 1 + + xoffset = xoffset + 1 + + outyoffset = outyoffset + (OLED_WD>>3) + yidx = yidx + 1 + ymask = ymask << 1 + + srcidx = srcidx + OLED_WD + + oled_device.display(Image.frombytes("1", [OLED_WD, OLED_HT], bytes(tmplist))) + + if hidescreen == True: + # Display + oled_power(True) + + + +def oled_drawfilledrectangle(x, y, wd, ht, mode = 0): + ymax = y + ht + cury = y&0xF8 + + xmax = x + wd + curx = x + if ((y & 0x7)) != 0: + yshift = y&0x7 + bytevalue = (0xFF<>yshift) + + while curx < xmax: + oled_writebyterow(curx,cury,bytevalue, mode) + curx = curx + 1 + cury = cury + 8 + # Draw 8 rows at a time when possible + while cury + 8 < ymax: + curx = x + while curx < xmax: + oled_writebyterow(curx,cury,0xFF, mode) + curx = curx + 1 + cury = cury + 8 + + if cury < ymax: + yshift = 8-((ymax-cury)&0x7) + bytevalue = (0xFF>>yshift) + + curx = x + while curx < xmax: + oled_writebyterow(curx,cury,bytevalue, mode) + curx = curx + 1 + + +def oled_writetextaligned(textdata, x, y, boxwidth, alignmode, charwd = 6, mode = 0): + leftoffset = 0 + if alignmode == 1: + # Centered + leftoffset = (boxwidth-len(textdata)*charwd)>>1 + elif alignmode == 2: + # Right aligned + leftoffset = (boxwidth-len(textdata)*charwd) + + oled_writetext(textdata, x+leftoffset, y, charwd, mode) + + +def oled_writetext(textdata, x, y, charwd = 6, mode = 0): + if charwd < 6: + charwd = 6 + + charht = int((charwd<<3)/6) + if charht & 0x7: + charht = (charht&0xF8) + 8 + + try: + file = open("/etc/argon/oled/font"+str(charht)+"x"+str(charwd)+".bin", "rb") + fontbytes = list(file.read()) + file.close() + except FileNotFoundError: + try: + # Default to smallest + file = open("/etc/argon/oled/font8x6.bin", "rb") + fontbytes = list(file.read()) + file.close() + except FileNotFoundError: + return + + if ((y & 0x7)) == 0: + # Use optimized loading + oled_fastwritetext(textdata, x, y, charht, charwd, fontbytes, mode) + return + + numfontrow = charht>>3 + ctr = 0 + while ctr < len(textdata): + fontoffset = ord(textdata[ctr])*charwd + fontcol = 0 + while fontcol < charwd and x < OLED_WD: + fontrow = 0 + row = y + while fontrow < numfontrow and row < OLED_HT and x >= 0: + curbit = 0x80 + curbyte = (fontbytes[fontoffset + fontcol + (OLED_NUMFONTCHAR*charwd*fontrow)]) + subrow = 0 + while subrow < 8 and row < OLED_HT: + value = 0 + if (curbyte&curbit) != 0: + value = 1 + oled_writebuffer(x,row,value, mode) + curbit = curbit >> 1 + row = row + 1 + subrow = subrow + 1 + fontrow = fontrow + 1 + fontcol = fontcol + 1 + x = x + 1 + ctr = ctr + 1 + +def oled_fastwritetext(textdata, x, y, charht, charwd, fontbytes, mode = 0): + + numfontrow = charht>>3 + ctr = 0 + while ctr < len(textdata): + fontoffset = ord(textdata[ctr])*charwd + fontcol = 0 + while fontcol < charwd and x < OLED_WD: + fontrow = 0 + row = y&0xF8 + while fontrow < numfontrow and row < OLED_HT and x >= 0: + curbyte = (fontbytes[fontoffset + fontcol + (OLED_NUMFONTCHAR*charwd*fontrow)]) + oled_writebyterow(x,row,curbyte, mode) + fontrow = fontrow + 1 + row = row + 8 + fontcol = fontcol + 1 + x = x + 1 + ctr = ctr + 1 + return + + +def oled_power(turnon = True): + if bus is None: + return + try: + if turnon == True: + oled_device.show() + else: + oled_device.hide() + except: + return + + +def oled_inverse(enable = True): + # Not supported? + return + + +def oled_fullwhite(enable = True): + # Not supported? + return + + + +def oled_reset(): + return + + diff --git a/source/scripts/argononeoledd.service b/source/scripts/argononeoledd.service new file mode 100644 index 0000000..f735236 --- /dev/null +++ b/source/scripts/argononeoledd.service @@ -0,0 +1,10 @@ +[Unit] +Description=Argon One Fan and Button Service +After=multi-user.target +[Service] +Type=simple +Restart=always +RemainAfterExit=true +ExecStart=/usr/bin/python3 /etc/argon/argononed.py SERVICE OLEDSWITCH +[Install] +WantedBy=multi-user.target diff --git a/source/scripts/argonpowerbutton-libgpiod.py b/source/scripts/argonpowerbutton-libgpiod.py new file mode 100644 index 0000000..8f8c6d8 --- /dev/null +++ b/source/scripts/argonpowerbutton-libgpiod.py @@ -0,0 +1,89 @@ + +# For Libreelec/Lakka, note that we need to add system paths +# import sys +# sys.path.append('/storage/.kodi/addons/virtual.rpi-tools/lib') +import gpiod +import os +import time + +# This function is the thread that monitors activity in our shutdown pin +# The pulse width is measured, and the corresponding shell command will be issued + +def argonpowerbutton_monitor(writeq): + + try: + # Reference https://github.com/brgl/libgpiod/blob/master/bindings/python/examples/gpiomon.py + + # Pin Assignments + LINE_SHUTDOWN=4 + try: + # Pi5 mapping + chip = gpiod.Chip('4') + except Exception as gpioerr: + # Old mapping + chip = gpiod.Chip('0') + + lineobj = chip.get_line(LINE_SHUTDOWN) + lineobj.request(consumer="argon", type=gpiod.LINE_REQ_EV_BOTH_EDGES) + while True: + hasevent = lineobj.event_wait(10) + if hasevent: + pulsetime = 1 + eventdata = lineobj.event_read() + if eventdata.type == gpiod.LineEvent.RISING_EDGE: + # Time pulse data + while lineobj.get_value() == 1: + time.sleep(0.01) + pulsetime += 1 + + if pulsetime >=2 and pulsetime <=3: + # Testing + #writeq.put("OLEDSWITCH") + writeq.put("OLEDSTOP") + os.system("reboot") + break + elif pulsetime >=4 and pulsetime <=5: + writeq.put("OLEDSTOP") + os.system("shutdown now -h") + break + elif pulsetime >=6 and pulsetime <=7: + writeq.put("OLEDSWITCH") + lineobj.release() + chip.close() + except Exception: + writeq.put("ERROR") + + +def argonpowerbutton_monitorswitch(writeq): + + try: + # Reference https://github.com/brgl/libgpiod/blob/master/bindings/python/examples/gpiomon.py + + # Pin Assignments + LINE_SHUTDOWN=4 + try: + # Pi5 mapping + chip = gpiod.Chip('4') + except Exception as gpioerr: + # Old mapping + chip = gpiod.Chip('0') + + lineobj = chip.get_line(LINE_SHUTDOWN) + lineobj.request(consumer="argon", type=gpiod.LINE_REQ_EV_BOTH_EDGES) + while True: + hasevent = lineobj.event_wait(10) + if hasevent: + pulsetime = 1 + eventdata = lineobj.event_read() + if eventdata.type == gpiod.LineEvent.RISING_EDGE: + # Time pulse data + while lineobj.get_value() == 1: + time.sleep(0.01) + pulsetime += 1 + + if pulsetime >= 10: + writeq.put("OLEDSWITCH") + lineobj.release() + chip.close() + except Exception: + writeq.put("ERROR") diff --git a/source/scripts/argonpowerbutton-rpigpio.py b/source/scripts/argonpowerbutton-rpigpio.py new file mode 100644 index 0000000..4472654 --- /dev/null +++ b/source/scripts/argonpowerbutton-rpigpio.py @@ -0,0 +1,66 @@ + +# For Libreelec/Lakka, note that we need to add system paths +# import sys +# sys.path.append('/storage/.kodi/addons/virtual.rpi-tools/lib') +import RPi.GPIO as GPIO +import os +import time + +# This function is the thread that monitors activity in our shutdown pin +# The pulse width is measured, and the corresponding shell command will be issued + +def argonpowerbutton_monitor(writeq): + try: + # Pin Assignments + PIN_SHUTDOWN=4 + + GPIO.setwarnings(False) + GPIO.setmode(GPIO.BCM) + GPIO.setup(PIN_SHUTDOWN, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) + + while True: + pulsetime = 1 + GPIO.wait_for_edge(PIN_SHUTDOWN, GPIO.RISING) + time.sleep(0.01) + while GPIO.input(PIN_SHUTDOWN) == GPIO.HIGH: + time.sleep(0.01) + pulsetime += 1 + if pulsetime >=2 and pulsetime <=3: + # Testing + #writeq.put("OLEDSWITCH") + writeq.put("OLEDSTOP") + os.system("reboot") + break + elif pulsetime >=4 and pulsetime <=5: + writeq.put("OLEDSTOP") + os.system("shutdown now -h") + break + elif pulsetime >=6 and pulsetime <=7: + writeq.put("OLEDSWITCH") + except Exception: + writeq.put("ERROR") + GPIO.cleanup() + + + +def argonpowerbutton_monitorswitch(writeq): + try: + # Pin Assignments + PIN_SHUTDOWN=4 + + GPIO.setwarnings(False) + GPIO.setmode(GPIO.BCM) + GPIO.setup(PIN_SHUTDOWN, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) + + while True: + pulsetime = 1 + GPIO.wait_for_edge(PIN_SHUTDOWN, GPIO.RISING) + time.sleep(0.01) + while GPIO.input(PIN_SHUTDOWN) == GPIO.HIGH: + time.sleep(0.01) + pulsetime += 1 + if pulsetime >= 10: + writeq.put("OLEDSWITCH") + except Exception: + writeq.put("ERROR") + GPIO.cleanup() diff --git a/source/scripts/argonregister-v1.py b/source/scripts/argonregister-v1.py new file mode 100644 index 0000000..edd9c1d --- /dev/null +++ b/source/scripts/argonregister-v1.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/source/scripts/argonregister.py b/source/scripts/argonregister.py new file mode 100644 index 0000000..d156c8b --- /dev/null +++ b/source/scripts/argonregister.py @@ -0,0 +1,109 @@ +#!/usr/bin/python3 + +# +# Argon Register Helper methods +# + +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): + if busobj is None: + return False + try: + oldval = argonregister_getbyte(busobj, ADDR_ARGONONEREG_DUTYCYCLE) + newval = oldval + 1 + if newval >= 100: + newval = 98 + argonregister_setbyte(busobj, ADDR_ARGONONEREG_DUTYCYCLE, newval) + newval = argonregister_getbyte(busobj, ADDR_ARGONONEREG_DUTYCYCLE) + if newval != oldval: + argonregister_setbyte(busobj, ADDR_ARGONONEREG_DUTYCYCLE, oldval) + return True + return False + except: + 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): + if busobj is None: + return 0 + + usereg=False + if regsupport is None: + usereg=argonregister_checksupport(busobj) + else: + usereg=regsupport + if usereg == True: + return argonregister_getbyte(busobj, ADDR_ARGONONEREG_DUTYCYCLE) + else: + return 0 + + +def argonregister_setfanspeed(busobj, newspeed, regsupport=None): + if busobj is None: + return + + if newspeed > 100: + newspeed = 100 + elif newspeed < 0: + newspeed = 0 + usereg=False + if regsupport is None: + usereg=argonregister_checksupport(busobj) + else: + usereg=regsupport + if usereg == True: + argonregister_setbyte(busobj, ADDR_ARGONONEREG_DUTYCYCLE, newspeed) + else: + busobj.write_byte(ADDR_ARGONONEFAN,newspeed) + time.sleep(1) + +def argonregister_signalpoweroff(busobj): + if busobj is None: + return + + if argonregister_checksupport(busobj): + argonregister_setbyte(busobj, ADDR_ARGONONEREG_CTRL, 1) + else: + 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/source/scripts/argonrtc.py b/source/scripts/argonrtc.py new file mode 100644 index 0000000..ba6c64e --- /dev/null +++ b/source/scripts/argonrtc.py @@ -0,0 +1,642 @@ +#!/usr/bin/python3 + +import os +import datetime + +######### +# Describe Methods +######### + +# Helper method to add proper suffix to numbers +def getNumberSuffix(numval): + onesvalue = numval % 10 + if onesvalue == 1: + return "st" + elif onesvalue == 2: + return "nd" + elif onesvalue == 3: + return "rd" + return "th" + +def describeHourMinute(hour, minute): + if hour < 0: + return "" + outstr = "" + ampmstr = "" + if hour <= 0: + hour = 0 + outstr = outstr + "12" + ampmstr = "am" + elif hour <= 12: + outstr = outstr + str(hour) + if hour == 12: + ampmstr = "pm" + else: + ampmstr = "am" + else: + outstr = outstr + str(hour-12) + ampmstr = "pm" + + if minute >= 10: + outstr = outstr+":" + elif minute > 0: + outstr = outstr+":0" + else: + if hour == 0: + ampmstr = "mn" + elif hour == 12: + ampmstr = "nn" + return outstr+ampmstr + + if minute <= 0: + minute = 0 + outstr = outstr+str(minute) + + return outstr+ampmstr + +# Describe Schedule Parameter Values +def describeSchedule(monthlist, weekdaylist, datelist, hourlist, minutelist): + weekdaynamelist = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] + monthnamelist = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + + curprefix = "" + hasDate = False + hasMonth = False + foundvalue = False + monthdatestr = "" + for curmonth in monthlist: + for curdate in datelist: + if curdate >= 0: + hasDate = True + if curmonth >= 0: + hasMonth = True + monthdatestr = monthdatestr + "," + monthnamelist[curmonth-1]+" "+str(curdate) + getNumberSuffix(curdate) + else: + monthdatestr = monthdatestr + ","+str(curdate) + getNumberSuffix(curdate) + else: + if curmonth >= 0: + monthdatestr = monthdatestr + "," + monthnamelist[curmonth-1] + + if len(monthdatestr) > 0: + foundvalue = True + # Remove Leading Comma + monthdatestr = monthdatestr[1:] + if hasMonth == True: + curprefix = "Annually:" + else: + curprefix = "Monthly:" + monthdatestr = monthdatestr + " of the Month" + monthdatestr = " Every "+monthdatestr + + weekdaystr = "" + for curweekday in weekdaylist: + if curweekday >= 0: + hasDate = True + weekdaystr = weekdaystr + "," + weekdaynamelist[curweekday] + + if len(weekdaystr) > 0: + foundvalue = True + # Remove Leading Comma + weekdaystr = weekdaystr[1:] + if len(curprefix) == 0: + curprefix = "Weekly:" + weekdaystr = " on " + weekdaystr + else: + weekdaystr = ",on " + weekdaystr + + hasHour = False + hasMinute = False + hourminstr = "" + for curhour in hourlist: + for curminute in minutelist: + if curhour >= 0: + hasHour = True + if curminute >= 0: + hasMinute = True + hourminstr = hourminstr + "," + describeHourMinute(curhour, curminute) + elif curminute >= 0: + hasMinute = True + hourminstr = hourminstr + "," + str(curminute) + getNumberSuffix(curminute) + + if len(hourminstr) > 0: + foundvalue = True + # Remove Leading Comma + hourminstr = hourminstr[1:] + if hasHour == True: + if hasDate == True: + hourminstr = "at " + hourminstr + else: + hourminstr = "Daily: " + hourminstr + if hasMinute == False: + hourminstr = hourminstr + " every minute" + else: + if hourminstr == "0": + hourminstr = "At the start of every hour" + else: + hourminstr = "Hourly: At " + hourminstr + " minute" + else: + hourminstr = "Every minute" + + if len(curprefix) > 0: + hourminstr = ","+hourminstr + + return (curprefix + monthdatestr + weekdaystr + hourminstr).strip() + + +######### +# Alarm +######### + +# Alarm to UTC/Local time +def convertAlarmTimezone(weekday, caldate, hour, minute, toutc): + utcdiffsec = getLocaltimeOffset().seconds + if toutc == False: + utcdiffsec = utcdiffsec*(-1) + + utcdiffsec = utcdiffsec - (utcdiffsec%60) + utcdiffmin = utcdiffsec % 3600 + utcdiffhour = int((utcdiffsec - utcdiffmin)/3600) + utcdiffmin = int(utcdiffmin/60) + + addhour = 0 + if minute >= 0: + minute = minute - utcdiffmin + if minute < 0: + addhour = -1 + minute = minute + 60 + elif minute > 59: + addhour = 1 + minute = minute - 60 + + addday = 0 + if hour >= 0: + hour = hour - utcdiffhour + tmphour = hour + addhour + if hour < 0: + hour = hour + 24 + elif hour > 23: + hour = hour - 24 + if tmphour < 0: + addday = -1 + elif tmphour > 23: + addday = 1 + + if addday != 0: + if weekday >= 0: + weekday = weekday + addday + if weekday < 0: + weekday = weekday + 7 + elif weekday > 6: + weekday = weekday - 7 + if caldate > 0: + # Edge cases might not be handled properly though + curtime = datetime.datetime.now() + maxmonthdate = getLastMonthDate(curtime.year, curtime.month) + caldate = caldate + addday + if caldate == 0: + # move to end of the month + caldate = maxmonthdate + elif caldate > maxmonthdate: + # move to next month + caldate = 1 + + return [weekday, caldate, hour, minute] + + +# Get RTC Alarm Setting (Negative values ignored) +def getRTCAlarm(weekday, caldate, hour, minute): + hasError = False + if caldate < 1 and weekday < 0 and hour < 0 and minute < 0: + hasError = True + elif minute > 59: + hasError = True + elif hour > 23: + hasError = True + elif weekday > 6: + hasError = True + elif caldate > 31: + hasError = True + + if hasError == True: + return [-1, -1, -1, -1] + # Convert to UTC + return convertAlarmTimezone(weekday, caldate, hour, minute, True) + +######### +# Date/Time tools +######### + +# Get local time vs UTC +def getLocaltimeOffset(): + localdatetime = datetime.datetime.now() + utcdatetime = datetime.datetime.fromtimestamp(localdatetime.timestamp(), datetime.timezone.utc) + # Remove TZ info to allow subtraction + utcdatetime = utcdatetime.replace(tzinfo = None) + + return localdatetime - utcdatetime + + +# Sync Time to RTC Time (for Daemon use) +def updateSystemTime(rtctime): + os.system("date -s '"+rtctime.isoformat()+"' >/dev/null 2>&1") + + +######### +# Config +######### + +# Load config value as array of integers +def getConfigValue(valuestr): + try: + if valuestr == "*": + return [-1] + tmplist = valuestr.split(",") + map_object = map(int, tmplist) + return list(map_object) + except: + return [-1] + +# Load config line data as array of Command schedule +def newCommandSchedule(curline): + result = [] + linedata = curline.split(" ") + if len(linedata) < 6: + return result + + minutelist = getConfigValue(linedata[0]) + hourlist = getConfigValue(linedata[1]) + datelist = getConfigValue(linedata[2]) + #monthlist = getConfigValue(linedata[3]) + monthlist = [-1] # Certain edge cases will not be handled properly + weekdaylist = getConfigValue(linedata[4]) + + cmd = "" + ctr = 5 + while ctr < len(linedata): + cmd = cmd + " " + linedata[ctr] + ctr = ctr + 1 + cmd = cmd.strip() + + for curmin in minutelist: + for curhour in hourlist: + for curdate in datelist: + for curmonth in monthlist: + for curweekday in weekdaylist: + result.append({ "minute": curmin, "hour": curhour, "date": curdate, "month":curmonth, "weekday": curweekday, "cmd":cmd }) + + return result + +# Save updated config file +def saveConfigList(fname, configlist): + f = open(fname, "w") + f.write("#\n") + f.write("# Argon RTC Configuration\n") + f.write("# - Follows cron general format, but with only * and csv support\n") + f.write("# - Each row follows the following format:\n") + f.write("# min hour date month dayOfWeek Command\n") + f.write("# e.g. Shutdown daily at 1am\n") + f.write("# 0 1 * * * off\n") + f.write("# Shutdown daily at 1am and 1pm\n") + f.write("# 0 1,13 * * * off\n") + f.write("# - Commands are currently on or off only\n") + f.write("# - Limititations\n") + f.write("# Requires MINUTE value\n") + f.write("# Month values are ignored (edge cases not supported)\n") + f.write("#\n") + + for config in configlist: + f.write(config+"\n") + f.close() + +# Remove config line +def removeConfigEntry(fname, entryidx): + configlist = loadConfigList(fname) + if len(configlist) > entryidx: + configlist.pop(entryidx) + saveConfigList(fname, configlist) + +# Load config list (removes invalid data) +def loadConfigList(fname): + try: + result = [] + with open(fname, "r") as fp: + for curline in fp: + if not curline: + continue + curline = curline.strip().replace('\t', ' ') + # Handle special characters that get encoded + tmpline = "".join([c if 0x20<=ord(c) and ord(c)<=0x7e else "" for c in curline]) + + if not tmpline: + continue + if tmpline[0] == "#": + continue + checkdata = tmpline.split(" ") + if len(checkdata) > 5: + # Don't include every minute type of schedule + if checkdata[0] != "*": + result.append(tmpline) + return result + except: + return [] + +# Form Command Schedule list from config list +def formCommandScheduleList(configlist): + try: + result = [] + for config in configlist: + result = result + newCommandSchedule(config) + return result + except: + return [] + +# Describe config list entry +def describeConfigListEntry(configlistitem): + linedata = configlistitem.split(" ") + if len(linedata) < 6: + return "" + + minutelist = getConfigValue(linedata[0]) + hourlist = getConfigValue(linedata[1]) + datelist = getConfigValue(linedata[2]) + #monthlist = getConfigValue(linedata[3]) + monthlist = [-1] # Certain edge cases will not be handled properly + weekdaylist = getConfigValue(linedata[4]) + + cmd = "" + ctr = 5 + while ctr < len(linedata): + cmd = cmd + " " + linedata[ctr] + ctr = ctr + 1 + cmd = cmd.strip().lower() + if cmd == "on": + cmd = "Startup" + else: + cmd = "Shutdown" + + return cmd+" | "+describeSchedule(monthlist, weekdaylist, datelist, hourlist, minutelist) + +# Describe config list and show indices +def describeConfigList(fname): + # 1 is reserved for New schedule + ctr = 2 + configlist = loadConfigList(fname) + for config in configlist: + tmpline = describeConfigListEntry(config) + if len(tmpline) > 0: + print(" "+str(ctr)+". ", tmpline) + ctr = ctr + 1 + if ctr == 2: + print(" No Existing Schedules") + +# Check Command schedule if it should fire for the give time +def checkDateForCommandSchedule(commandschedule, datetimeobj): + testminute = commandschedule.get("minute", -1) + testhour = commandschedule.get("hour", -1) + testdate = commandschedule.get("date", -1) + testmonth = commandschedule.get("month", -1) + testweekday = commandschedule.get("weekday", -1) + + if testminute < 0 or testminute == datetimeobj.minute: + if testhour < 0 or testhour == datetimeobj.hour: + if testdate < 0 or testdate == datetimeobj.day: + if testmonth < 0 or testmonth == datetimeobj.month: + if testweekday < 0: + return True + else: + # python Sunday = 6, RTC Sunday = 0 + weekDay = datetimeobj.weekday() + if weekDay == 6: + weekDay = 0 + else: + weekDay = weekDay + 1 + if testweekday == weekDay: + return True + return False + +# Get current command +def getCommandForTime(commandschedulelist, datetimeobj, checkcmd): + ctr = 0 + while ctr < len(commandschedulelist): + testcmd = commandschedulelist[ctr].get("cmd", "") + if (testcmd.lower() == checkcmd or len(checkcmd) == 0) and len(testcmd) > 0: + if checkDateForCommandSchedule(commandschedulelist[ctr], datetimeobj) == True: + return testcmd + ctr = ctr + 1 + return "" + +# Get Last Date of Month +def getLastMonthDate(year, month): + if month < 12: + testtime = datetime.datetime(year, month+1, 1) + else: + testtime = datetime.datetime(year+1, 1, 1) + testtime = testtime - datetime.timedelta(days=1) + return testtime.day + +# Increment to the next iteration of command schedule +def incrementCommandScheduleTime(commandschedule, testtime, addmode): + testminute = commandschedule.get("minute", -1) + testhour = commandschedule.get("hour", -1) + testdate = commandschedule.get("date", -1) + testmonth = commandschedule.get("month", -1) + testweekday = commandschedule.get("weekday", -1) + + if addmode == "minute": + testfield = commandschedule.get(addmode, -1) + if testfield < 0: + if testtime.minute < 59: + return testtime + datetime.timedelta(minutes=1) + else: + return incrementCommandScheduleTime(commandschedule, testtime.replace(minute=0), "hour") + else: + return incrementCommandScheduleTime(commandschedule, testtime, "hour") + elif addmode == "hour": + testfield = commandschedule.get(addmode, -1) + if testfield < 0: + if testtime.hour < 23: + return testtime + datetime.timedelta(hours=1) + else: + return incrementCommandScheduleTime(commandschedule, testtime.replace(hour=0), "date") + else: + return incrementCommandScheduleTime(commandschedule, testtime, "date") + elif addmode == "date": + testfield = commandschedule.get(addmode, -1) + if testfield < 0: + maxmonthdate = getLastMonthDate(testtime.year, testtime.month) + if testtime.day < maxmonthdate: + return testtime + datetime.timedelta(days=1) + else: + return incrementCommandScheduleTime(commandschedule, testtime.replace(day=1), "month") + else: + return incrementCommandScheduleTime(commandschedule, testtime, "month") + elif addmode == "month": + testfield = commandschedule.get(addmode, -1) + if testfield < 0: + nextmonth = testtime.month + nextyear = testtime.year + while True: + if nextmonth < 12: + nextmonth = nextmonth + 1 + else: + nextmonth = 1 + nextyear = nextyear + 1 + maxmonthdate = getLastMonthDate(nextyear, nextmonth) + if testtime.day <= maxmonthdate: + return testtime.replace(month=nextmonth, year=nextyear) + else: + return incrementCommandScheduleTime(commandschedule, testtime, "year") + else: + # Year + if testtime.month == 2 and testtime.day == 29: + # Leap day handling + nextyear = testtime.year + while True: + nextyear = nextyear + 1 + maxmonthdate = getLastMonthDate(nextyear, testtime.month) + if testtime.day <= maxmonthdate: + return testtime.replace(year=nextyear) + else: + return testtime.replace(year=(testtime.year+1)) + +# Set Next Alarm on RTC +def getNextAlarm(commandschedulelist, prevdatetime): + curtime = datetime.datetime.now() + if prevdatetime > curtime: + return [prevdatetime, -1, -1, -1, -1] + + # Divisible by 4 for leap day + checklimityears = 12 + foundnextcmd = False + nextcommandschedule = {} + # To be sure it's later than any schedule + nextcommandtime = curtime.replace(year=(curtime.year+checklimityears)) + + ctr = 0 + while ctr < len(commandschedulelist): + testcmd = commandschedulelist[ctr].get("cmd", "").lower() + if testcmd == "on": + invaliddata = False + testminute = commandschedulelist[ctr].get("minute", -1) + testhour = commandschedulelist[ctr].get("hour", -1) + testdate = commandschedulelist[ctr].get("date", -1) + testmonth = commandschedulelist[ctr].get("month", -1) + testweekday = commandschedulelist[ctr].get("weekday", -1) + + tmpminute = testminute + tmphour = testhour + tmpdate = testdate + tmpmonth = testmonth + tmpyear = curtime.year + + if tmpminute < 0: + tmpminute = curtime.minute + + if tmphour < 0: + tmphour = curtime.hour + + if tmpdate < 0: + tmpdate = curtime.day + + if tmpmonth < 0: + tmpmonth = curtime.month + + maxmonthdate = getLastMonthDate(tmpyear, tmpmonth) + if tmpdate > maxmonthdate: + # Invalid month date + if testdate < 0: + tmpdate = maxmonthdate + else: + # Date is fixed + if testminute < 0: + tmpminute = 0 + if testhour < 0: + tmphour = 0 + if testmonth < 0 and testdate <= 31: + # Look for next valid month + while tmpdate > maxmonthdate: + if tmpmonth < 12: + tmpmonth = tmpmonth + 1 + else: + tmpmonth = 1 + tmpyear = tmpyear + 1 + maxmonthdate = getLastMonthDate(tmpyear, tmpmonth) + elif tmpdate == 29 and tmpmonth == 2: + # Fixed to leap day + while tmpdate > maxmonthdate: + tmpyear = tmpyear + 1 + maxmonthdate = getLastMonthDate(tmpyear, tmpmonth) + else: + invaliddata = True + if invaliddata == False: + try: + testtime = datetime.datetime(tmpyear, tmpmonth, tmpdate, tmphour, tmpminute) + except: + # Force time diff + testtime = curtime - datetime.timedelta(hours=1) + tmptimediff = (curtime - testtime).total_seconds() + else: + tmptimediff = 0 + + if testweekday >= 0: + # Day of Week check + # python Sunday = 6, RTC Sunday = 0 + weekDay = testtime.weekday() + if weekDay == 6: + weekDay = 0 + else: + weekDay = weekDay + 1 + + + if weekDay != testweekday or tmptimediff > 0: + # Resulting 0-ed time will be <= the testtime + if testminute < 0: + testtime = testtime.replace(minute=0) + if testhour < 0: + testtime = testtime.replace(hour=0) + + dayoffset = testweekday-weekDay + if dayoffset < 0: + dayoffset = dayoffset + 7 + elif dayoffset == 0: + dayoffset = 7 + + testtime = testtime + datetime.timedelta(days=dayoffset) + + # Just look for the next valid weekday; Can be optimized + while checkDateForCommandSchedule(commandschedulelist[ctr], testtime) == False and (testtime.year - curtime.year) < checklimityears: + testtime = testtime + datetime.timedelta(days=7) + + if (testtime.year - curtime.year) >= checklimityears: + # Too many iterations, abort/ignore + tmptimediff = 0 + else: + tmptimediff = (curtime - testtime).total_seconds() + if tmptimediff > 0: + # Find next iteration that's greater than the current time (Day of Week check already handled) + while tmptimediff >= 0: + testtime = incrementCommandScheduleTime(commandschedulelist[ctr], testtime, "minute") + tmptimediff = (curtime - testtime).total_seconds() + + if nextcommandtime > testtime and tmptimediff < 0: + nextcommandschedule = commandschedulelist[ctr] + nextcommandtime = testtime + foundnextcmd = True + + + ctr = ctr + 1 + if foundnextcmd == True: + # Schedule Alarm + # Assume no date,weekday involved just shift the hour and minute accordingly + paramminute = nextcommandschedule.get("minute", -1) + paramhour = nextcommandschedule.get("hour", -1) + if nextcommandschedule.get("weekday", -1) >=0 or nextcommandschedule.get("date", -1) > 0: + # Set alarm based on hour/minute of next occurrence to factor in timezone changes if any + paramminute = nextcommandtime.minute + paramhour = nextcommandtime.hour + weekday, caldate, hour, minute = getRTCAlarm(nextcommandschedule.get("weekday", -1), nextcommandschedule.get("date", -1), paramhour, paramminute) + return [nextcommandtime, weekday, caldate, hour, minute] + + # This will ensure that this will be replaced next iteration + return [curtime, -1, -1, -1, -1] + diff --git a/source/scripts/argonstatus.py b/source/scripts/argonstatus.py new file mode 100644 index 0000000..843f0a8 --- /dev/null +++ b/source/scripts/argonstatus.py @@ -0,0 +1,172 @@ +#!/usr/bin/python3 + +import sys +import os + +sys.path.append("/etc/argon/") +from argonsysinfo import * +from argonregister import * +from argononed import * + +def getFahrenheit(celsiustemp): + try: + return (32+9*(celsiustemp)/5) + except: + return 0 + + +temperature="C" +tmpconfig=load_unitconfig(UNIT_CONFIGFILE) +if "temperature" in tmpconfig: + temperature = tmpconfig["temperature"] + +baseleftoffset = "" + +stdleftoffset = " " + +#if len(sys.argv) > 2: +# baseleftoffset = stdleftoffset +baseleftoffset = stdleftoffset + +argctr = 1 +while argctr < len(sys.argv): + cmd = sys.argv[argctr].lower() + argctr = argctr + 1 + if baseleftoffset != "": + print(cmd.upper(),"INFORMATION:") + if cmd == "cpu usage": + # CPU Usage + curlist = argonsysinfo_listcpuusage() + + while len(curlist) > 0: + curline = "" + tmpitem = curlist.pop(0) + curline = tmpitem["title"]+": "+str(tmpitem["value"])+"%" + print(baseleftoffset+curline) + elif cmd == "storage": + # Storage Info + curlist = [] + try: + tmpobj = argonsysinfo_listhddusage() + for curdev in tmpobj: + curlist.append({"title": curdev, "value": argonsysinfo_kbstr(tmpobj[curdev]['total']), "usage": int(100*tmpobj[curdev]['used']/tmpobj[curdev]['total']) }) + #curlist = argonsysinfo_liststoragetotal() + except Exception: + curlist = [] + + while len(curlist) > 0: + tmpitem = curlist.pop(0) + # Right column first, safer to overwrite white space + print(baseleftoffset+tmpitem["title"], str(tmpitem["usage"])+"%","used of", tmpitem["value"]) + + elif cmd == "raid": + # Raid Info + curlist = [] + try: + tmpobj = argonsysinfo_listraid() + curlist = tmpobj['raidlist'] + except Exception: + curlist = [] + + if len(curlist) > 0: + tmpitem = curlist.pop(0) + print(baseleftoffset+tmpitem["title"], tmpitem["value"], argonsysinfo_kbstr(tmpitem["info"]["size"])) + + if len(tmpitem['info']['state']) > 0: + print(baseleftoffset+stdleftoffset,tmpitem['info']['state']) + + if len(tmpitem['info']['rebuildstat']) > 0: + print(baseleftoffset+stdleftoffset,"Rebuild:" + tmpitem['info']['rebuildstat']) + + + print(baseleftoffset+stdleftoffset,"Active:"+str(int(tmpitem["info"]["active"]))+"/"+str(int(tmpitem["info"]["devices"]))) + print(baseleftoffset+stdleftoffset,"Working:"+str(int(tmpitem["info"]["working"]))+"/"+str(int(tmpitem["info"]["devices"]))) + print(baseleftoffset+stdleftoffset,"Failed:"+str(int(tmpitem["info"]["failed"]))+"/"+str(int(tmpitem["info"]["devices"]))) + else: + print(baseleftoffset+stdleftoffset,"N/A") + + elif cmd == "ram": + # RAM + try: + tmpraminfo = argonsysinfo_getram() + print(baseleftoffset+tmpraminfo[0],"of", tmpraminfo[1]) + except Exception: + pass + + elif cmd == "temperature": + # Temp + try: + hddtempctr = 0 + maxcval = 0 + mincval = 200 + + alltempobj = {"cpu": argonsysinfo_getcputemp()} + # Get min/max of hdd temp + hddtempobj = argonsysinfo_gethddtemp() + for curdev in hddtempobj: + alltempobj[curdev] = hddtempobj[curdev] + if hddtempobj[curdev] < mincval: + mincval = hddtempobj[curdev] + if hddtempobj[curdev] > maxcval: + maxcval = hddtempobj[curdev] + hddtempctr = hddtempctr + 1 + + if hddtempctr > 0: + alltempobj["hdd min"]=mincval + alltempobj["hdd max"]=maxcval + + for curdev in alltempobj: + if temperature == "C": + # Celsius + tmpstr = str(alltempobj[curdev]) + if len(tmpstr) > 4: + tmpstr = tmpstr[0:4] + else: + # Fahrenheit + tmpstr = str(getFahrenheit(alltempobj[curdev])) + if len(tmpstr) > 5: + tmpstr = tmpstr[0:5] + print(baseleftoffset+curdev.upper()+": "+ tmpstr+ chr(176) +temperature) + + except Exception: + pass + elif cmd == "ip": + # IP Address + try: + print(baseleftoffset+argonsysinfo_getip()) + except Exception: + pass + elif cmd == "fan speed": + # Fan Speed + try: + newspeed = argonregister_getfanspeed(argonregister_initializebusobj()) + if newspeed <= 0: + fanconfig = load_fancpuconfig() + fanhddconfig = load_fanhddconfig() + + # Speed based on CPU Temp + val = argonsysinfo_getcputemp() + newspeed = get_fanspeed(val, fanconfig) + + val = argonsysinfo_getmaxhddtemp() + tmpspeed = get_fanspeed(val, fanhddconfig) + if tmpspeed > newspeed: + newspeed = tmpspeed + print(baseleftoffset+"Fan Speed",str(newspeed)) + except Exception: + pass + elif cmd == "fan configuration": + fanconfig = load_fancpuconfig() + fanhddconfig = load_fanhddconfig() + + if len(fanhddconfig) > 0: + print(baseleftoffset+"Fan Temp-Speed cut-offs") + for curconfig in fanconfig: + print(baseleftoffset+stdleftoffset,curconfig) + + if len(fanhddconfig) > 0: + print(baseleftoffset+"HDD Temp-Speed cut-offs") + for curconfig in fanhddconfig: + print(baseleftoffset+stdleftoffset,curconfig) + + diff --git a/source/scripts/argonsysinfo.py b/source/scripts/argonsysinfo.py new file mode 100644 index 0000000..102f2e5 --- /dev/null +++ b/source/scripts/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 diff --git a/source/tools/setntpserver.sh b/source/tools/setntpserver.sh new file mode 100644 index 0000000..1baf7f9 --- /dev/null +++ b/source/tools/setntpserver.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +NTPSERVER="time.google.com" +TMPCONFIG=/dev/shm/tmpconfig.conf + + +# timesyncd +CONFIG=/etc/systemd/timesyncd.conf +if [ -f "$CONFIG" ] +then + cat "$CONFIG" | grep -v -e 'NTP=' > "$TMPCONFIG" + echo "NTP=$NTPSERVER" >> "$TMPCONFIG" + + sudo chown root:root "$TMPCONFIG" + sudo chmod 644 "$TMPCONFIG" + sudo mv "$TMPCONFIG" "$CONFIG" + + # /usr/sbin/ntpd + + sudo service systemd-timesyncd restart > /dev/null 2>&1 +fi + + +for CURSERVICECONFIG in ntp chrony +do + CONFIG=/etc/${CURSERVICECONFIG}.conf + if [ -f "$CONFIG" ] + then + cat "$CONFIG" | grep -v -e 'pool ' > "$TMPCONFIG" + #echo "server $NTPSERVER" >> "$TMPCONFIG" + echo "pool time1.google.com iburst" >> "$TMPCONFIG" + echo "pool time2.google.com iburst" >> "$TMPCONFIG" + echo "pool time3.google.com iburst" >> "$TMPCONFIG" + echo "pool time4.google.com iburst" >> "$TMPCONFIG" + + sudo chown root:root "$TMPCONFIG" + sudo chmod 644 "$TMPCONFIG" + sudo mv "$TMPCONFIG" "$CONFIG" + + sudo service ${CURSERVICECONFIG} restart > /dev/null 2>&1 + fi +done